zuminote

個人的な勉強記録

PyTorchによる発展ディープラーニング 第1章 1-3 転移学習の実装 メモ

前回に引き続いて、転移学習についての学習メモ。

なお、ここで書く内容は処理の概説となるが、無料公開されているサンプルコードからも十分読み取れる内容に対するメモという位置づけで書きすすめる。

万が一こんな場所を見ている人がいたら本買った方がいいので本を買ってください。↓

サンプルコード

github.com

この章では、学習済みVGG-16の転移学習により、アリとハチの画像を分類するモデルを学習させる。

転移学習とは、学習済みモデルをベースに、最終の出力層を付け替えて学習させる手法。

入力層に近い部分の結合パラメータは学習済みの値から更新させず、出力層への結合パラメータを、手元にある少量のデータで学習し直す。

CNNでは、入力に近い層では汎用的な特徴を学習し、出力に近い層でそのデータセット固有の特徴を学習するため、このような手法を用いることができる。

参考:

転移学習:機械学習の次のフロンティアへの招待 - Qiita

ちなみに、入力層に近い層の結合パラメータも更新させる場合は「ファインチューニング」である。(この違いを初めて知った)

本章の流れは以下のとおり。

  1. Datasetの作成
    • 画像の前処理クラスの実装
    • ↑を用いた、Datasetクラスの実装
  2. 実装したDatasetクラスを使ってDataLoaderを作成
  3. ネットワークモデルを作成
  4. 損失関数の定義
  5. 最適化手法の設定
  6. 学習・検証

DeepLearningの実装工程の概観について詳しくは1-2に書いてあるが、どんなタスクでも基本的にはこの構成を踏襲することになるだろう。

(そもそも損失関数とか最適化手法とかってなんだっけ?となったらゼロつくあたりを見るとよい)

また、この章では特に、転移学習のときの最終層の付け替えと、結合パラメータを更新するかしないかの設定についての箇所が勉強になった。

1. Datasetの作成

このような単純な画像分類タスクのDatasetは通常、torchvision.datasets.ImageFolderクラスを利用するのが簡単だが、ここでは自分でDatasetをつくる場合の実装方法の例を示している。

画像の前処理クラスImageTransformの実装

今回は訓練と推論で違う前処理をする。

訓練のときだけ、データに対してepochごとに異なる画像変換を適用してデータを水増しするData Augmentation をかける。ここでも、1-1で使用したtransform.Composeを使う。

前処理の内容は以下。

  • RandomResizedCrop(resize, scale=(0.5, 1.0)):

    0.5〜1.0の大きさで画像を拡大縮小し、さらにアスペクト比を3/4〜4/3(これはデフォルト値)の間のいずれかで変更して画像を横か縦に引き伸ばし、最後にresizeで指定した大きさでトリミングする

  • RandomHorizontalFlip():画像の左右を50%の確率で反転させる。

  • torchテンソルに変換
  • 標準化(1-1と同様に、画像のRGB値のmean, stdをVGG-16の学習データに合わせる)

DatasetクラスHymenopteraDatasetの実装

torch.utils.data.Datasetクラスを継承して実装する。

なので、データを取り出すのためメソッド__getitem__()と、ファイル数を返すメソッド__len__()が必要。

__getitem__()で、先に実装した前処理クラスImageTransformインスタンスself.transformとして定義)を利用し、変換済み画像img_transformedとラベルlabelを取り出すようにする。

また、Datasetにする画像へのファイルパスをリスト型変数に格納する関数make_datapath_listも実装しておく。

Datasetって中身の確認がしづらい…と思っていたけれどこう書けば済む話だったんですね。(以下、サンプルコードから引用)

index = 0
print(train_dataset.__getitem__(index)[0].size()) #画像サイズ
print(train_dataset.__getitem__(index)[1]) #ラベル(アリ0,ハチ1)

2. 実装したDatasetクラスを使ってDataLoaderを作成

↑で実装したDatasetクラスで定義したDatasetを、torch.utils.data.DataLoaderに渡せばOK。

3. ネットワークモデルを作成

1-1と同様に学習済みVGG-16をロードする。

学習済みモデルは1000クラスの分類タスク用のモデルだが、今回はアリかハチの2値分類なので、最終層の出力を変更する。

(以下、サンプルコードより引用)

use_pretrained = True
net = models.vgg16(pretrained=use_pretrained)

# VGG16の最後の出力層の出力ユニットをアリとハチの2つに付け替える
net.classifier[6] = nn.Linear(in_features=4096, out_features=2)

ちなみに、net.classifier[6]というように指定するのは、ネットワークがこのような階層構造になっているから。

VGG(
  (features): Sequential(
    (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU(inplace=True)
    (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))

    (中略)

    (30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
  (classifier): Sequential(
    (0): Linear(in_features=25088, out_features=4096, bias=True)
    (1): ReLU(inplace=True)
    (2): Dropout(p=0.5, inplace=False)
    (3): Linear(in_features=4096, out_features=4096, bias=True)
    (4): ReLU(inplace=True)
    (5): Dropout(p=0.5, inplace=False)
    (6): Linear(in_features=4096, out_features=1000, bias=True)
  )
)

ここで、付け替えた最終層のtorch.nn.Linearクラスは「全結合層」である。入力データに対して線形変換を行う層であり、KerasではDense層と呼ばれている。

(たまに脳内でなんだっけ??となる…。)

全結合層は、畳み込み層・プーリング層を通して特徴部分が取り出された画像データを一つのノードに結合し、活性化関数によって変換された値(特徴変数)を出力する。

ノードの数が増えると特徴量空間の分割数が増し、各領域を特徴付ける特徴変数の数が増える。

参考: https://lp-tech.net/articles/LVB9R

4. 損失関数の定義

2値分類なので、クロスエントロピー誤差関数を使用する。

5. 最適化手法の定義

まず、最適化手法を定義する前に、学習させるパラメータ(ここでは最終層のパラメータ)以外は勾配計算をなくし、変化しないように設定する。

モデルのnamed_parameters()メソッドをイテレーションすることで、パラメータの名前とパラメータ自体を取り出すことができる。

それで取り出したパラメータparamに対してparam.requires_grad = Trueを設定することで、そのパラメータは誤差逆伝搬で勾配が計算される。Falseとすると更新しない。

ちなみに、net.named_parameters()の中身を見ると分かるが、パラメータの名前はfeatures.0.weight, features.0.bias, features.2.weight, features.2.bias, features.5.weight, features.5.bias, ...というようになっており、学習させる層(畳み込み層とか全結合層)に対してそれぞれ重みとバイアスがある。

(以下サンプルコードより引用)

# 転移学習で学習させるパラメータを、変数params_to_updateに格納する
params_to_update = []

# 学習させるパラメータ名 ここでは最終層(classifierの6番目)の重みとバイアス
update_param_names = ["classifier.6.weight", "classifier.6.bias"]

# 学習させるパラメータ以外は勾配計算をなくし、変化しないように設定
for name, param in net.named_parameters():
    # update_param_namesに入れたパラメータ名はparam.requires_grad = Trueにして、ほかはFalseにする
    if name in update_param_names:
        param.requires_grad = True
        params_to_update.append(param)
        print(name)
    else:
        param.requires_grad = False

その後、最適化のアルゴリズムを設定する。今回はMomentum SGDを使用する。 学習させたいパラメータを取り出して、それをoptim.SDGの引数paramsに与える。

6. 学習・検証

学習の関数train_modelを作成する。

pytorchで学習モード・検証モードを切り替えるのは、Dropout層など学習と検証で挙動が異なる層があるから。(これまでなんとなくそういうものと思っていたので、理由を知ってなるほどと思った…。)

今回の例では、学習済みモデルそのままの時の性能を確かめるためにepoch=0の訓練は省略されている。

lossにはミニバッチで平均した損失が格納されているので、その値をitem()で取り出し、ミニバッチサイズで掛け算して合計値に戻してやり、それを足し合わせることで、epochにおけるlossの合計値を出している。