概要
個人的な備忘録を兼ねたPyTorchの基本的な解説とまとめです。第4回は画像の分類問題になります。
画像分類の第一歩といえば「0〜9の手書き数値データ」であるMNISTが有名です。今回はMNISTに類似したグレースケールの「あいうえお」画像を利用して画像分類の初歩をまとめていきます。「あ・い・う・え・お」の5種類に分類された「28ピクセル」×「28ピクセル」のひらがな画像データを用いた画像分類の簡単なネットワークを構築していきます1。
前回「数値データの分類問題」からの追加箇所は大きく1箇所
- CNN(Convolutional Neural Network):nn.Conv2d() を使う
方針
- できるだけ同じコード進行
- できるだけ簡潔(細かい内容は割愛)
- 特徴量などの部分,あえて数値で記入(どのように変わるかがわかりやすい)
演習用のファイル(Githubからダウンロード)
1. 分類問題
分類問題とは、データをカテゴリー・クラスに振り分ける問題となります。画像の分類はその典型的な例となります。代表的なデータセットを4つまとめておきます。
データセット名 | 内容 | 分類数 | カラー |
---|---|---|---|
MNIST | 0〜9の手書き数値 | 10 | グレースケール |
Fashin-MNIST | 靴・服などファッション画像 | 10 | グレースケール |
CIFAR10 | 32x32の飛行機、トラックなどの画像 | 10 | 3チャンネルRGB |
CIFAR100 | CIAFR10の増強版 | 100 | 3チャンネルRGB |
いずれも、PyTorchのDatasetsライブラリから利用することが可能です。
PyTorchによるプログラムの流れを確認します。基本的に下記の5つの流れとなります。Juypyter Labなどで実際に入力しながら進めるのがオススメ
- データの読み込みとtorchテンソルへの変換 (2.1)
- ネットワークモデルの定義と作成 (2.2)
- 誤差関数と誤差最小化の手法の選択 (2.3)
- 変数更新のループ (2.4)
- 検証 (2.5)
2. コードと解説
2.0 データについて
演習用に利用するデータは1チャンネルグレースケールの「あ・い・う・え・お」画像です。画像サイズはMNISTやFashon-MNISTに応用できるように28×28に調整してあります。下図が今回利用するサンプル画像となります。
利用するデータセットはすでに(バッチサイズ、チャンネル数、高さ、横幅)、具体的には(792, 1, 28, 28)に変更してあります2。バッチサイズという用語は 「一度の学習(パラメータ更新)で使用するデータの数」、今回の場合は単純にデータ数となります。
2.1 データの読み込みとtorchテンソルへの変換
まず利用するライブラリを読み込みます。
import numpy as np # データの読み込みに利用
import torch # PyTorchのライブラリ
import torch.nn as nn # PyTorchのネットワークのライブラリ
from sklearn.model_selection import train_test_split # データ分割時に利用
データの読み込み
演習ではaiueo.npz
というファイル名の画像データを使用します。numpyのloadでnpz形式のファイルを読み込みましょう。data["x"] が画像データ、data["t"] がラベルIDで0〜4の整数値の教師データとなります。
- 0:画像「あ」
- 1:画像「い」
- 2:画像「う」
- 3:画像「え」
- 4:画像「お」
に対応しています。scikit-learnのtrain_test_splitを利用して、学習に利用するデータとテスト(検証)に利用するデータとに分割します。
data = np.load("./aiueo.npz")
x = data["x"] # 画像データ
t = data["t"] # ラベル 0〜4の5種類
# numpyからtorchテンソルに変換
x = torch.FloatTensor(x) # 入力するデータはFloat
t = torch.LongTensor(t) # ラベル側はLong
# データの分割
x, x_test, t, t_test = train_test_split(x,t, test_size=0.1, random_state=55)
- np.load : Num配列をファイルから読み込むための関数です。npy形式やnpz形式のファイルを読み込めます。
- data["x"] : 配列を保存した時のキーがxの場合、data["x"] で配列にアクセスできます。
- data.keys() : 保存してあるキーを表示できます。
画像の分類はCPUだと若干時間がかかります。GPUが利用できる場合は、データやモデルにto("cuda")
をつけるとGPUを使った計算となります3。
x = torch.FloatTensor(x).to("cuda")
t = torch.LongTensor(t) .to("cuda")
2.2 ネットワークモデルの定義と作成
今回は下図のようなCNN(畳み込みネットワーク)と分類のための全結合層を利用したネットワークで分類問題を扱ってみたいと思います。
具体的には(1チャンネル、28ピクセル, 28ピクセル) の画像データ「あ・い・う・え・お」を入力データとする。CNN、一列化、全結合層を経て、5次元ベクトル(5種類の画像予測)を出力する形となります。
畳み込み層
nn.Conv2d(in_channels, out_channels, kernel_size, stride)
- in_channels : 入力されるチャンネル数
- out_channels : 出力されるチャンネル数
- kernel_size : カーネルサイズ(kernel_size $\times$ kernel_size)
- stride : カーネルの移動量
詳細はPyTorchの公式ドキュメントに記載されています。ネットワークの書き方はこれまで同様クラスを利用して記述します。
(1) __init___() : 利用するネットワーク名・活性化関数をすべて記述
(2) forward() : 実際の流れを記す
class DNN(nn.Module):
def __init__(self):
super(DNN, self).__init__()
self.cnn1 = nn.Conv2d(in_channels=1, out_channels=20 ,kernel_size=5)
self.act1 = nn.LeakyReLU(negative_slope=0.01) # Leaky ReLU導入してみた
self.flat = nn.Flatten() # 行列データを一行のデータにつなげる
self.fc1 = nn.Linear(in_fatures=5*24*24, out_features=100)
self.act2 = nn.ReLU()
self.fc2 = nn.Linear(in_features=100, out_features=5)
def forward(self, x):
h1 = self.cnn1(x)
h2 = self.act1(h1)
h = self.flat(h2)
h = self.act2(self.fc1(h))
y = self.fc2(h)
return y, h1, h2
model = DNN()
print(model)は、__init()__に書かれているネットワーク層を上から順番に出力します。ネットワークの定義に明示的に書いていない内容も表示されます。ネットワークの途中の特徴量を可視化するために、forward() の戻り値を最終出力の$y$、cnnの出力$h1$、活性化関数後の出力$h2$の3種類を用意してあります。GPUを利用する場合は、model.to("cuda")
を追加すればOKですが今回の演習はCPU仕様となっています。
DNN(
(cnn1): Conv2d(1, 5, kernel_size=(5, 5), stride=(1, 1))
(act1): LeakyReLU(negative_slope=0.01)
(flat): Flatten(start_dim=1, end_dim=-1)
(fc1): Linear(in_features=2880, out_features=100, bias=True)
(act2): ReLU()
(fc2): Linear(in_features=100, out_features=5, bias=True)
)
ネットワークの定義
- 入力されるデータは(1チャンネル、高さ28ピクセル、横28ピクセル)のグレースケール画像で(1, 28, 28)のテンソル
- 28x28の画像に対して、カーネルサイズ5x5のCNNを適用します。出力される特徴量の形状は24x24
- out_channels=5としているので24x24の特徴量が5種類となります。(5, 24, 24)のテンソル
- (5, 24, 24)を一列化(Flatten)するので、5x24x24が最初の全結合層fc1への入力となります。つまり、
Linear(in_features=5*24*24, ...)
となります。 - 出力は5種類の文字なので全結合層fc2について、
Linear(..., out_features=5)
となります。 - 今回は活性化関数としてLeakyReLUやReLUを利用してみました。__init__()の活性化関数の部分を他の関数に変更することで、様々な活性化関数の効果を検証することができます。
ネットワーク層の詳しい解説は?
CNN(畳み込み層)については、AIcia Solid ProjectさんのYouTube解説がとても丁寧です。深層学習、強化学習など様々なものがとてもわかりやすく丁寧に解説されています本当に感謝 。
Qiitaの記事にもわかりやすい記事がたくさんあります。いくつか紹介しておきます。
2.3 誤差関数と誤差最小化の手法の選択
分類問題では予測値$y$ と教師データ$t$の誤差を計算するのに、クロスエントロピーを用いることが一般的なようです。PyTorchではCrossEntropyLoss() と記述します。
criterion = nn.CrossEntropyLoss() # 損失関数:cross_entropy
optimizer = torch.optim.Adam(model.parameters(), lr=0.05) # 学習率:lr=0.001がデフォルト
モデルの出力は「あ・い・う・え・お」の予測値である5次元のベクトルです。損失計算は、単なる5次元ベクトル$y$と画像のラベルIDを比較することになります。5次元のベクトル$y$のsoftmaxを求めることで予測値が確率の形で表現されます。一方、画像のラベルIDは、0なら(1, 0, 0, 0 ,0)のように確率1で「あ」が選ばれるように変形します。2つの操作によって予測値$y$と教師データ$t$の誤差が2つの確率ベクトルの差となります。実装上、この一連の作業は CrossEntropyLoss() を使うことで、criterion(y,t) として誤差計算が行われることになります
。モデルの予測値$y$や教師データ$t$をそのまま使い criterion(y,t) とすればOKです。
2.4 変数更新のループ
LOOPで指定した回数、
- y=model(x) で予測値を求め、
- criterion(y, t) で指定した誤差関数を使い予測値と教師データの誤差を計算、
- accuracy(Y, t) で精度を計算、
- 誤差が小さくなるようにoptimizerに従い変数をアップデート
します。
LOOP = 150 # LOOP : 学習回数
for epoch in range(LOOP):
optimizer.zero_grad() # 勾配初期化PyTorchの約束事項
y = model(x) # ネットワークモデルによる予測
loss = criterion(y, t) # 誤差計算PyTorchの約束事項
acc = accuracy(y,t) # 精度の計算
print(f"{epoch}: loss: {loss.item()}, acc:{acc}") # 損失と精度の表示
loss.backward() # backward(微分する部分)
optimizer.step() # パラメータ更新PyTorchの約束事項
forループで変数を更新することになります。損失の減少や精度を表すaccの値を観察しながら、学習回数のLOOP や学習率のlr を適宜変更することになります。
ここまでで、基本的な学習は終わりとなります。
平均精度について
accuracyの関数について簡単な説明を残しておきます。予測値と教師データで等しい値なら正解として、正解数/問題数で精度を求める単純な平均精度を求める作成してみました。forループよりも前に記述すればOKです。
def accuracy(y, t):
_,argmax_list = torch.max(y, dim=1)
accuracy = sum(argmax_list == t).item()/len(t)
return accuracy
2.5 検証
2.1のデータ分割で作成したテストデータ x_test と t_test を利用して学習結果をテストしてみましょう。x_testをmodelに入れた値 y_test = model(x_test) が予測値となります。accuracyで平均精度を求めれば完成です。
y_test = model(x_test)
acc = accuracy(y_test, t_test)
print("平均精度", acc)
# 平均精度 0.98
ちょっと頑張れば個別の精度も求めることができますぞ。「え」と「お」が似ているのかな?きっとクセのある「え」や「お」が紛らわしいに違いない。
prediction
あ い う え お
true あ 12 0 1 0 0
い 0 23 0 0 0
う 0 0 10 0 0
え 0 0 0 13 2
お 0 0 0 1 18
3. CNN出力結果の可視化
CNNの出力値やカーネルを画像として表示する予定でしたが、長くなってきたので4.5に移動することにしました
注
-
独立行政法人産業技術総合研究所のETL文字データベースを利用させていただきました。「あ・い・う・え・お」の5種類の画像を抽出しMNISTと対応させるべく28x28サイズに縮小したものを利用しています。 ↩
-
MNISTを利用する時は形状を変更する必要があります。 ↩
-
GPUを使う計算方法の詳細は、GPUとCPUを使い分ける!や PytorchでのGPUの使用方法などを参考にしてください。 ↩