【PyTorch】簡単なCNN画像分類器を実現


この論文では、Pytorchの公式ドキュメント、磐創チームの「PyTorch公式チュートリアル中国語版」および余霆嵩の「PyTorchモデル訓練実用チュートリアル」を参照して、pytorchベースの画像多分類器モデル構築プロセスを簡単に記録した.データセットのロードから始まり、モデル設計、トレーニング、テストなどのプロセスが含まれます.
pytorch中国語ネット:https://www.pytorchtutorial.com/pytorch公式ドキュメント:https://pytorch.org/docs/stable/index.html
一.データのロード
Pytorchのデータロードは、一般にtorch.utils.data.Datasettorch.utils.data.Dataloaderの2つのクラスを組み合わせて行われる.Datasetを継承して独自のデータセットクラスを定義し、トレーニング時にDataloaderでカスタムデータセットクラスをロードする必要があります.
1.Datasetクラスを継承し、キーメソッドを書き換える
pytorchのdatasetクラスには、Map-style datasetsとIterable-style datasetsの2種類があります.前者は我々がよく用いる構造であり,後者はデータセットのランダム読み出しが困難(または不可能)な場合に用いる.ここではMap-style datasetを実装します.torch.utils.data.Datasetを継承した後、書き換える必要がある方法は、__len____getitem__の方法であり、__len__の方法はすべてのデータの数を返す必要があり、__getitem__は、与えられたデータインデックスに従って対応するtensorタイプのSampleを取得する必要があり、この2つの方法に加えて、いくつかの変数を初期化するために__init__の方法を実装する必要がある.あまり話さないで、直接コードをつけます.
'''
             ,          
'''
from torch.utils.data import Dataset
import torch
import os
import cv2
from Config import mycfg
import random
import numpy as np


class ImageClassifyDataset(Dataset):
    def __init__(self, imagedir, labelfile, classify_num, train=True):
    	'''
    	           。
    	'''
        self.imagedir = imagedir
        self.labelfile = labelfile
        self.classify_num = classify_num
        self.img_list = []
        #     
        with open(self.labelfile, 'r') as fp:
            lines = fp.readlines()
            for line in lines:
                filepath = os.path.join(self.imagedir, line.split(";")[0].replace('\\', '/'))
                label = line.split(";")[1].strip('
'
) self.img_list.append((filepath, label)) if not train: self.img_list = random.sample(self.img_list, 50) def __len__(self): return len(self.img_list) def __getitem__(self, item): ''' , item( ) , , ''' _int_label = int(self.img_list[item][1]) # label 0,1,2,3,4... label = torch.tensor(_int_label,dtype=torch.long) img = self.ProcessImgResize(self.img_list[item][0]) return img, label def ProcessImgResize(self, filename): ''' ''' _img = cv2.imread(filename) _img = cv2.resize(_img, (mycfg.IMG_WIDTH, mycfg.IMG_HEIGHT), interpolation=cv2.INTER_CUBIC) _img = _img.transpose((2, 0, 1)) _img = _img / 255 _img = torch.from_numpy(_img) _img = _img.to(torch.float32) return _img

いくつかのデータセットクラスは、一般的にはtransforms関数を入力して画像の前処理シーケンスを構築するが、transforms関数を入力する利点の一つは、パラメータとして入力すると、torchvisionによって直接取得されたいくつかのプリセットデータセットCIFAR 10など、いくつかの非ローカルデータセットのデータを操作することができることであり、それ以外はtorchvisionである.Transformsには、予め定義された画像操作関数があり、直接積み木のように画像処理シーケンスを組み立てることができ、便利です.私はここで自分でローカルのデータセットにダウンロードしたので、簡単に自分の関数で操作しました.
2.Dataloaderを使用してデータをロードする
カスタムデータセットクラスImageClassifyDatasetをインスタンス化した後、それをDataLoaderにパラメータとして渡し、遍歴可能なデータローダを得る.パラメータbatch_sizeによってバッチサイズを制御し、shuffleによって乱順読み出しの有無を制御し、num_workersによってデータ読み出しのためのスレッド数を制御することができる.
from torch.utils.data import DataLoader
from MyDataset import ImageClassifyDataset

dataset = ImageClassifyDataset(imagedir, labelfile, 10)
dataloader = DataLoader(dataset, batch_size=5, shuffle=True,num_workers=5)
for index, data in enumerate(dataloader):
	print(index)	# batch  
	print(data)		#   batch {img,label}

二.モデル設計
ここでは深さ学習モデルの設計のみについて議論するが、pytorchのネットワーク構造は積層されており、pytorchでは、Linear、CNN、RNN、Transformerなどのパラメータによって制御できる多くのネットワーク層構造が事前に定義されており、公式ドキュメントのtorch.nn部分を参照することができる.独自のモデル構造を設計するには、torch.nn.Moduleというクラスを継承し、forwardの方法を実装する必要があり、一般的に__init__にネットワークモデルのいくつかのコンポーネントを設定し、forwardの方法で入出力順にコンポーネントを組み立てる.
'''
       、    loss    、optimizer
'''
import torch.nn as nn


class Simple_CNN(nn.Module):
    def __init__(self, class_num):
        super(Simple_CNN, self).__init__()
        self.class_num = class_num
        self.conv1 = nn.Sequential(
            nn.Conv2d(		# input: 3,400,600
                in_channels=3,
                out_channels=8,
                kernel_size=5,
                stride=1,
                padding=2
            ),
            nn.Conv2d(
                in_channels=8,
                out_channels=16,
                kernel_size=5,
                stride=1,
                padding=2
            ),
            nn.AvgPool2d(2),  # 16,400,600 --> 16,200,300
            nn.BatchNorm2d(16),
            nn.LeakyReLU(),
            nn.Conv2d(
                in_channels=16,
                out_channels=16,
                kernel_size=5,
                stride=1,
                padding=2
            ),
            nn.Conv2d(
                in_channels=16,
                out_channels=8,
                kernel_size=5,
                stride=1,
                padding=2
            ),
            nn.AvgPool2d(2),  # 8,200,300 --> 8,100,150
            nn.BatchNorm2d(8),
            nn.LeakyReLU(),
            nn.Conv2d(
                in_channels=8,
                out_channels=8,
                kernel_size=3,
                stride=1,
                padding=1
            ),
            nn.Conv2d(
                in_channels=8,
                out_channels=1,
                kernel_size=3,
                stride=1,
                padding=1
            ),
            nn.AvgPool2d(2),  # 1,100,150 --> 1,50,75
            nn.BatchNorm2d(1),
            nn.LeakyReLU()
        )
        self.line = nn.Sequential(
            nn.Linear(
                in_features=50 * 75,
                out_features=self.class_num
            ),
            nn.Softmax()
        )

    def forward(self, x):
        x = self.conv1(x)
        x = x.view(-1, 50 * 75)
        y = self.line(x)
        return y


上で定義したモデルには、ボリュームコンポーネントconv1と全接続コンポーネントlineが含まれており、ボリュームコンポーネントにはいくつかのボリューム層が含まれており、一般的に{ボリューム層、プール化層、アクティブ化関数}の順序で結合されています.ここで私はまた、関数をアクティブにする前にBatchNorm 2 d層を追加して、上位層の出力を正規化し、アクティブ化関数に入力される値が小さすぎる(勾配が消失する)か、大きすぎる(勾配が爆発する)ようにした.アセンブリを接合するとき、私の全接続層の入力は1次元ベクトルなので、アセンブリの最後の50を畳む必要があります.× 75 50\times 75 50×75サイズのマトリクス展平成一次元の再導入全接続層(x.view(-1,50*75))
三.トレーニング
実例化モデルの後、ネットワークモデルの訓練は損失関数とオプティマイザを定義する必要があり、損失関数はネットワーク出力とラベルの差を定義し、異なるタスクに基づいて異なる適切な損失関数を定義する必要があり、オプティマイザはニューラルネットワークのパラメータがどのように損失に基づいて更新されるかを定義した.現在ニューラルネットワークで最もよく用いられているオプティマイザはSGD(ランダム勾配降下アルゴリズム)とその変種である.私のこの簡単な分類器モデルでは,直接用いられる多分類タスクで最もよく用いられる損失関数CrossEntropyLoss()およびオプティマイザSGDである.
self.cnnmodel = Simple_CNN(mycfg.CLASS_NUM)
self.criterion = nn.CrossEntropyLoss()	#    ,     0,1,2,3...         
self.optimizer = optim.SGD(self.cnnmodel.parameters(), lr=mycfg.LEARNING_RATE, momentum=0.9)

訓練過程は実は簡単で、dataloaderを使ってbatchに従ってデータを読み出した後、inputをネットワークモデルに入れてネットワークの出力を計算し、ラベルに基づいて損失関数を通じてLossを計算し、Lossを逆方向にニューラルネットワークに伝播し(その前に前回のサイクルの勾配を整理する必要がある)、最後にオプティマイザを通じて重みを更新する.トレーニング部分のコードは以下の通りです.
for each_epoch in range(mycfg.MAX_EPOCH):
            running_loss = 0.0
            self.cnnmodel.train()
            for index, data in enumerate(self.dataloader):
                inputs, labels = data
                outputs = self.cnnmodel(inputs)
                loss = self.criterion(outputs, labels)

                self.optimizer.zero_grad()	#           
                loss.backward()	#     
                self.optimizer.step()	#     
                running_loss += loss.item()
                if index % 200 == 199:
                    print("[{}] loss: {:.4f}".format(each_epoch, running_loss/200))
                    running_loss = 0.0
            #         
            model_name = 'classify-{}-{}.pth'.format(each_epoch,round(all_loss/all_index,3))
            torch.save(self.cnnmodel,model_name)	#       

四.テスト
テストとトレーニングの手順はそれほど悪くありません.つまり、モデルを読み出してdataloaderでデータを取得し、ネットワークに入力して出力しますが、逆伝播などの操作は必要ありません.比較的注目すべきは、正確率計算にはいくつかの小さなテクニックがある可能性がある.
acc = 0.0
count = 0
self.cnnmodel = torch.load('mymodel.pth')
self.cnnmodel.eval()
for index, data in enumerate(dataloader_eval):
	inputs, labels = data   # 5,3,400,600  5,10
	count += len(labels)
	outputs = cnnmodel(inputs)
	_,predict = torch.max(outputs, 1)
	acc += (labels == predict).sum().item()
print("[{}] accurancy: {:.4f}".format(each_epoch, acc / count))

ここでは、すべてのモデルを保存してすべてのモデルをロードする方法を採用しています.この方法の利点は、モデルを使用するときに完全にブラックボックスと見なすことができますが、モデルが大きい場合は面倒です.この場合、パラメータのみを保存してネットワーク構造を保存しない方法を採用することができ、モデルを使用するたびに、インスタンス化されたモデルにパラメータを割り当てる必要があります.
torch.save(cnnmodel.state_dict(), "my_resnet.pth")
cnnmodel = Simple_CNN()
cnnmodel.load_state_dict(torch.load("my_resnet.pth"))

締めくくり
これでプロセス全体が終わり、小さな白レベルの画像分類タスクフローです.この間androidのことをずっとやっていたので、少し疎遠になっていましたが、このブログを書いて記録して、その後seq 2 seqとimage captionタスクの方面のモデル構造と訓練過程も書くはずです.完全コードの後もgithubに統一して参考にします.