概要
個人的な備忘録を兼ねたPyTorchの基本的な解説とまとめです。第5回はプーリングを追加した画像分類 になります。
第4回畳み込み層のみの画像分類からの追加点は1箇所
- プーリング層の MaxPool2d() もしくは AvgPool2d() の追加
方針
- できるだけ同じコード進行
- できるだけ簡潔(細かい内容は割愛)
- 特徴量などの部分,あえて数値で記入(どのように変わるかがわかりやすい)
演習用のファイル(Githubからダウンロード)
1. 第4回の要約と今回の目的
第4回はひらがな「あ・い・う・え・お」の5種類を手書き文字画像をデータとして使い、画像分類の演習を行いました。ネットワーク構造は下図のような畳み込み層と全結合層を利用した単純なものでした。
今回の目的は畳み込み層のあとにプーリング層を追加してみることにあります1。具体的には、次のようなネットワークを作成してみたいと思います。ネットワークの図が少し混み合ってきました
第1回から全く同じですが、PyTorchによるプログラムの流れを確認します。基本的に下記の5つの流れとなります。Juypyter Labなどで実際に入力しながら進めるのがオススメ
- データの読み込みとtorchテンソルへの変換 (2.1)
- ネットワークモデルの定義と作成 (2.2)
- 誤差関数と誤差最小化の手法の選択 (2.3)
- 変数更新のループ (2.4)
- 検証 (2.5)
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
というファイル名の画像データを使用します2。numpyのloadでnpz形式のファイルを読み込みましょう。data["x"] が画像データ、data["t"] がラベルIDで0〜4の整数値の教師データとなります。
画像 | ラベルID |
---|---|
「あ」 | 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が利用できる場合は、xやt、モデルに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.MaxPool2d(kernel_size, stride=None)
- 平均値プーリング層: nn.AvgPool2d(kernel_size, stride=None)
- kernel_size : カーネルサイズ
- stride : 移動量、指定しない場合デフォルト値でカーネルサイズと同じだけ移動
- 最大値プーリングは、カーネルの範囲で最大値
- 平均値プーリングは、カーネルの範囲での平均値
プーリング層にも様々な種類があるようです。PyTorchの公式ドキュメントに詳細が記載されています。
CNNにプーリング層を追加したネットワークを作成していきます。今まで同様、
(1) __init___()部分に利用するネットワーク名
(2) forward部分に実際の流れ
を記述する方法で書いていきます。
class DNN(nn.Module):
def __init__(self):
super(DNN, self).__init__()
self.cnn1 = nn.Conv2d(in_channels=1, out_channels=5 ,kernel_size=5)
self.act1 = nn.LeakyReLU(negative_slope=0.01) # Leaky ReLU導入
self.pool1= nn.MaxPool2d(kernel_size=2, stride=2)
self.flat = nn.Flatten()
self.fc1 = nn.Linear(in_features=5*12*12, 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)
h = self.act1(h1)
h2 = self.pool1(h)
h = self.flat(h2)
h = self.fc1(h)
h = self.act2(h)
y = self.fc2(h)
return y, h1, h2
model = DNN()
プーリング層にMaxPool2dを利用していますが、AvgPool2dでも動作します。効果を比較するのも面白いと思います。活性化関数も2種類使ってみました。print(model)は下のような感じです。GPUを使う場合はmodel.to("cuda")
も追加します。
DNN(
(cnn1): Conv2d(1, 5, kernel_size=(5, 5), stride=(1, 1))
(act1): LeakyReLU(negative_slope=0.01)
(pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(flat): Flatten(start_dim=1, end_dim=-1)
(fc1): Linear(in_features=720, out_features=100, bias=True)
(act2): ReLU()
(fc2): Linear(in_features=100, out_features=5, bias=True)
)
ネットワーク層の解説
CNNやプーリング層については、AIcia Solid ProjectさんのYouTube解説がとても丁寧です。深層学習、強化学習など様々なものがとてもわかりやすく丁寧に解説されています本当に感謝 。
Qiitaの記事にもわかりやすい記事がたくさんあります。2つほど紹介しておきます。CNNのあとにプーリング層の解説があります。
2.3 誤差関数と誤差最小化の手法の選択
分類問題では予測値$y$ と教師データ$t$の誤差にはクロスエントロピーを使います。
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) とするだけです。
2.4 変数更新のループ
for文を利用して次の1〜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の関数について簡単な説明を残しておきます。予測値と教師データで等しい値なら正解として、正解数/問題数で精度を求める単純な平均精度を求める作成してみました。
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で平均精度を求めれば完成です。
model.eval()
y_test,_,_ = model(x_test)
test_acc = accuracy(y_test, t_test)
print(f"精度:{test_acc}")
# 精度:0.9625
おかしいな〜。第4回で実装したCNNだけのネットワークだとは平均精度が0.98、しかし、今回のCNN+プーリング層だと平均精度が0.96という結果でした
ラベルごとで検証してみた
やはり、「え」と「お」の判別が難しいみたい、と思いきや「あ」と「う」を間違えているだと!
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 0 19
3. CNN・プーリング層の出力結果の可視化
matplotlibを利用してCNNやプーリング層の出力結果を画像で表現してみます。どの文字でもできるのですが、プーリング層の効果は画像「い」が比較的わかりやすいかな?
prediction, cnn_feature_map, pool_feature_map = model(sample_image)
を使って、CNNの出力値とプーリング層の出力値を抽出します。prediction、cnn_feature_map、pool_reature_mapがそれぞれforwardの戻り値 y、h1、h2に対応しています。
import matplotlib.pyplot as plt
import japanize_matplotlib
# テスト用の一つの画像を選択
# あ:300, い:500 う:200 え:600 お:100
sample_image = x[500].unsqueeze(0) # バッチ次元を追加 (1, 1, 28, 28)
# モデルを評価モードに
model.eval()
# 推論実行
with torch.no_grad():
prediction, cnn_feature_map, pool_feature_map = model(sample_image)
ライブラリを読み込んで、500番の画像「い」を選択しておきます。cnn_feature_mapがCNNの出力値、pool_feature_mapがプーリング層の出力値となります。
CNNの出力値
cnn_feature_mapをmatplotlibで表示できるように、形状を行列の形に変換して5つの画像を順番に表示します。
# 特徴マップのサイズは (1, c=1, h, w)
# バッチとチャネル次元を削除して2D画像として表示
cnn_feature_map = cnn_feature_map.squeeze().cpu().numpy() # 2D配列に変換
fig, axes = plt.subplots(1, 5, figsize=(10, 3))
plt.suptitle('CNN出力 (特徴マップ)')
for i, ax in enumerate(axes):
ax.imshow(cnn_feature_map[i], cmap="gray")
ax.axis('off')
plt.tight_layout()
plt.show()
「い」ですね。CNNなのでエッジや傾きなどの特徴量が抽出されているはず!つづいて、プーリング層の出力結果を画像にしてみます。pool_feature_mapの形状を適切に変換して画像化します。
プーリング層の出力値
# 特徴マップのサイズは (1, c=1, h, w)
# バッチとチャネル次元を削除して2D画像として表示
pool_feature_map = pool_feature_map.squeeze().cpu().numpy()
fig, axes = plt.subplots(1, 5, figsize=(10,3))
plt.suptitle("プーリング層の出力結果")
for i, ax in enumerate(axes):
ax.imshow(pool_feature_map[i], cmap="gray_r")
ax.axis('off')
plt.tight_layout()
plt.show()
白い部分が重要な部分と考えると、「い」の最大の特徴は、どうも左右のはねている部分やとまっている部分のように感じます。2箇所の大きな白い塊が「い」ということなのでしょうか?
参考と発展
CNNやPooling層に加えてDropout層や正規化層を追加しながらCNNやPooling層の形を繰り返すことで精度が飛躍的に向上するようです。CNN系譜の代表的なモデルを5つほど紹介、というか5つしか知らないとも言える。発表年は目安と思ってください
モデル名 | 発表年 | 特徴 | 層の数 |
---|---|---|---|
AlexNet | 2012年 | CNNを利用した深層学習で画像分類コンペILSVRC2012に勝利 ![]() |
8層 |
VGG | 2014年 | シンプルな3x3畳み込み層の積み重ね | 16層と19層 |
GoogLeNet | 2014年 | Inceptionモジュール導入 | 22層 |
ResNet | 2015年 | スキップ接続(残差接続)の導入 | 152層 |
DenseNet | 2017年 | すべての層が後続のすべての層に密に接続 | 121〜264層 |
論文のタイトルも紹介しておきます。モデルの作成者・著者名を記入せず大変申し訳無いですが
4. 次回
第6回は中間層の出力値を出力させる方法についての予定です。今回はモデルを定義している forward()の戻り値に直接出力させる値を書き込んでいます。この方法だけしか知らないと学習完了後に他の層の出力も見たい!という時の対応が困難になります。第6回では学習済みmodelに対して関心のあるネットワーク層を登録して出力値にアクセスするというちょっとした豆知識回になりそうです。
目次ページ
注
-
プーリング層に関しては、Qiita記事の深層学習入門 ~畳み込みとプーリング編に詳しい解説があります。カラフルで非常にわかりやすいです
↩
-
独立行政法人産業技術総合研究所のETL文字データベースを利用させていただきました
「あ・い・う・え・お」の5種類の画像を抽出しMNISTと対応させるべく28x28サイズに縮小したものを利用しています。 ↩
-
GPUを使う計算方法の詳細は、GPUとCPUを使い分ける!や PytorchでのGPUの使用方法などを参考にしてください。 ↩