【PyTorch】簡単なCNN画像分類器を実現
この論文では、Pytorchの公式ドキュメント、磐創チームの「PyTorch公式チュートリアル中国語版」および余霆嵩の「PyTorchモデル訓練実用チュートリアル」を参照して、pytorchベースの画像多分類器モデル構築プロセスを簡単に記録した.データセットのロードから始まり、モデル設計、トレーニング、テストなどのプロセスが含まれます.
pytorch中国語ネット:https://www.pytorchtutorial.com/pytorch公式ドキュメント:https://pytorch.org/docs/stable/index.html
一.データのロード
Pytorchのデータロードは、一般に
1.Datasetクラスを継承し、キーメソッドを書き換える
pytorchのdatasetクラスには、Map-style datasetsとIterable-style datasetsの2種類があります.前者は我々がよく用いる構造であり,後者はデータセットのランダム読み出しが困難(または不可能)な場合に用いる.ここではMap-style datasetを実装します.
いくつかのデータセットクラスは、一般的にはtransforms関数を入力して画像の前処理シーケンスを構築するが、transforms関数を入力する利点の一つは、パラメータとして入力すると、torchvisionによって直接取得されたいくつかのプリセットデータセットCIFAR 10など、いくつかの非ローカルデータセットのデータを操作することができることであり、それ以外はtorchvisionである.Transformsには、予め定義された画像操作関数があり、直接積み木のように画像処理シーケンスを組み立てることができ、便利です.私はここで自分でローカルのデータセットにダウンロードしたので、簡単に自分の関数で操作しました.
2.Dataloaderを使用してデータをロードする
カスタムデータセットクラスImageClassifyDatasetをインスタンス化した後、それをDataLoaderにパラメータとして渡し、遍歴可能なデータローダを得る.パラメータ
二.モデル設計
ここでは深さ学習モデルの設計のみについて議論するが、pytorchのネットワーク構造は積層されており、pytorchでは、Linear、CNN、RNN、Transformerなどのパラメータによって制御できる多くのネットワーク層構造が事前に定義されており、公式ドキュメントの
上で定義したモデルには、ボリュームコンポーネント
三.トレーニング
実例化モデルの後、ネットワークモデルの訓練は損失関数とオプティマイザを定義する必要があり、損失関数はネットワーク出力とラベルの差を定義し、異なるタスクに基づいて異なる適切な損失関数を定義する必要があり、オプティマイザはニューラルネットワークのパラメータがどのように損失に基づいて更新されるかを定義した.現在ニューラルネットワークで最もよく用いられているオプティマイザはSGD(ランダム勾配降下アルゴリズム)とその変種である.私のこの簡単な分類器モデルでは,直接用いられる多分類タスクで最もよく用いられる損失関数
訓練過程は実は簡単で、dataloaderを使ってbatchに従ってデータを読み出した後、inputをネットワークモデルに入れてネットワークの出力を計算し、ラベルに基づいて損失関数を通じてLossを計算し、Lossを逆方向にニューラルネットワークに伝播し(その前に前回のサイクルの勾配を整理する必要がある)、最後にオプティマイザを通じて重みを更新する.トレーニング部分のコードは以下の通りです.
四.テスト
テストとトレーニングの手順はそれほど悪くありません.つまり、モデルを読み出してdataloaderでデータを取得し、ネットワークに入力して出力しますが、逆伝播などの操作は必要ありません.比較的注目すべきは、正確率計算にはいくつかの小さなテクニックがある可能性がある.
ここでは、すべてのモデルを保存してすべてのモデルをロードする方法を採用しています.この方法の利点は、モデルを使用するときに完全にブラックボックスと見なすことができますが、モデルが大きい場合は面倒です.この場合、パラメータのみを保存してネットワーク構造を保存しない方法を採用することができ、モデルを使用するたびに、インスタンス化されたモデルにパラメータを割り当てる必要があります.
締めくくり
これでプロセス全体が終わり、小さな白レベルの画像分類タスクフローです.この間androidのことをずっとやっていたので、少し疎遠になっていましたが、このブログを書いて記録して、その後seq 2 seqとimage captionタスクの方面のモデル構造と訓練過程も書くはずです.完全コードの後もgithubに統一して参考にします.
pytorch中国語ネット:https://www.pytorchtutorial.com/pytorch公式ドキュメント:https://pytorch.org/docs/stable/index.html
一.データのロード
Pytorchのデータロードは、一般に
torch.utils.data.Dataset
とtorch.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に統一して参考にします.