トランスファー学習は、ディープニューラルネットワークをトレーニングするための強力なテクニックで、あるディープラーニングの問題について学んだ知識を、別の、しかし類似の学習問題に適用することができます。
トランスファー学習を使用すると、設計中のアプリのデプロイメント速度が劇的に速くなり、ディープニューラルネットワークのトレーニングと実装の両方がよりシンプルで簡単になります。
この記事では、転移学習の背後にある理論について説明し、PyTorchで畳み込みニューラルネットワーク(CNN)の転移学習の例を実行する方法を見ます。
PyTorchとは?
Pytorchは、深層学習と自然言語処理に特化したPython用に開発されたライブラリです。PyTorchはGraphical Processing Units (GPU)の能力を活用し、CPUでネットワークを学習するよりも高速にディープニューラルネットワークを実装することができます。
PyTorchは、その速度と柔軟性のおかげで、深層学習の研究者の間で人気が高まっています。PyTorchは、3つの異なる特徴を売りにしています。
- シンプルで使いやすいインターフェイス
- Pythonデータサイエンススタックとの完全な統合
- 実行中に変更可能な柔軟で動的な計算グラフ(問題に必要なメモリ量がわからない場合、ニューラルネットワークのトレーニングが非常に簡単になる)。
PyTorch は NumPy と互換性があり、NumPy の配列をテンソルに変換したり、その逆も可能です。
必要な用語の定義
この先に進む前に、トランスファーラーニングに関連するいくつかの用語を定義しておこう。定義を明確にすることで、転移学習の背後にある理論を理解し、転移学習のインスタンスを実装することがより簡単になり、再現しやすくなります。
ディープラーニングとは?
ディープラーニングは機械学習の一分野であり、機械学習とは、コンピュータが明示的にプログラムされることなくタスクを実行することを可能にする行為と説明できる。
ディープラーニングは、人間の脳をモデルにした計算機フレームワークであるニューラルネットワークを利用したシステムです。
ニューラルネットワークは、3つの異なる構成要素を持っています。入力層、隠れ層(中間層)、出力層です。
入力層は、ニューラルネットワークに送られるデータが処理される場所であり、中間層/隠れ層は、ノードまたはニューロンと呼ばれる構造で構成されています。
これらのノードは数学的関数で、入力情報を何らかの方法で変更し、変更されたデータを最後の層、つまり出力層に渡します。単純なニューラルネットワークは、データポイントが互いにどのように関連しているかについての仮定(重み)を調整することで、入力データの単純なパターンを識別することができる。
ディープニューラルネットワークの名前の由来は、通常のニューラルネットワークを多数連結したものであることにある。ニューラルネットワークの数が多ければ多いほど、ディープニューラルネットワークはより複雑なパターンを識別することができ、用途が広がります。ニューラルネットワークには様々な種類があり、それぞれに得意分野があります。
例えば、長期短期記憶型ニューラルネットワークは、テキストや音声データのように時系列が重要な、時間に敏感なタスクを扱うときに非常に有効なネットワークである。
コンボリューショナル・ニューラル・ネットワークとは?
今回は、画像データの操作を得意とするニューラルネットワークの一種である畳み込みニューラルネットワーク(Convolutional Neural Networks)を取り上げます。
畳み込みニューラルネットワーク(CNN)は、視覚データの表現を得意とする特殊なニューラルネットワークです。CNNのデータは、画像の各ピクセルの明るさや色を表す値を含むグリッドとして表現される。
CNNは、畳み込み層、プーリング層、完全連結層の3つの異なる構成要素に分解される。
畳み込み層の役割は、2つの行列の内積を取ることによって画像の表現を作成することである。
最初の行列は学習可能なパラメータのセットであり、カーネルと呼ばれる。もう1つの行列は、分析される画像の一部で、高さ、幅、色チャンネルを持つ。畳み込み層はCNNの中で最も計算が行われる場所である。カーネルは画像の幅と高さの全体に渡って移動し、最終的に画像全体の2次元の表現、つまり活性化マップとして知られる表現を生成する。
CNNの畳み込み層に含まれる情報量が非常に多いため、ネットワークの学習に非常に長い時間がかかることがある。プーリング層の機能は、CNNの畳み込み層に含まれる情報量を減らすことであり、ある畳み込み層から出力を取り出し、それを縮小して表現をより単純にすることである。
プーリング層は、ネットワークの出力の異なる場所を調べ、近くの値を「プール」して、近くのすべての値を表す一つの値を考え出すことによって、これを達成する。言い換えれば、選択された領域の値の要約統計量を取るのである。
領域内の値を要約するということは、ネットワークがその情報を認識し、画像から意味のあるパターンを導き出すことを可能にする関連情報を保持したまま、表現のサイズと複雑さを大幅に削減できることを意味する。
領域の値を要約するために使用できる関数は、近傍領域の平均を取る – あるいは Average Pooling など、さまざまなものがある。近傍領域の加重平均や、領域のL2ノルムを取ることもできる。最も一般的なプーリング手法はマックスプーリングであり、リージョンの最大値を取って近傍領域を表現するのに使用する。
このような場合、”Full Connected Layer “は、すべてのニューロンを連結し、ネットワークの各前続層と後続層との間の接続を持つ。ここでは、畳み込み層で抽出され、プーリング層でプールされた情報が分析され、データのパターンが学習される。ここでの計算は、バイアス効果を併用した行列の乗算で行われる。
また、CNNにはいくつかの非線形性が存在する。画像自体が非線形なものであることを考えると、ネットワークは画像データを解釈できるようにするために、非線形成分を持たなければならない。非線形層は通常畳み込み層の直後にネットワークに挿入されるが、これは活性化マップに非線形性を持たせるためである。
ネットワークが画像データを適切に解釈できるようにするために、さまざまな非線形活性化関数が使用されます。最もポピュラーな非線形活性化関数はReLu(Rectified Linear Unit)である。ReLu関数は、実数値を0以上の正の値だけに圧縮することで、非線形入力を線形表現に変換します。
ReLu関数は、他の活性化関数に比べて約6倍の速さで実行される信頼性とスピードで人気があります。ReLuの欠点は、大きな勾配を扱うときに簡単に行き詰ってしまい、ニューロンを更新しないことである。この問題は、関数に学習率を設定することで対処することができる。
非線形関数としては他にシグモイド関数とTanh関数の2つがよく知られている。
シグモイド関数は、実数値を0から1の範囲に縮小することで動作するが、勾配の極値付近では値がほぼゼロになるため、活性化の処理に問題がある。
一方、Tanh関数は、出力が0付近を中心とし、-1~1の間に収まることを除けば、シグモイド関数と同様の動作をします。
トレーニングおよびテスト
ディープニューラルネットワークの作成と実装には、トレーニングとテストという2つの異なるフェーズがあります。
トレーニングフェーズでは、ネットワークにデータを与え、データが含むパターンを学習し始め、ネットワークの重み(データポイントが互いにどのように関連しているかについての仮定)を調整します。言い換えれば、学習段階では、ネットワークが与えられたデータについて「学習」する。
テスト段階では、ネットワークが学習したことを評価する。ネットワークに新しいデータセットを与え、ネットワークが学習したパターンに関する推測を新しいデータに適用するように要求する。モデルの精度は評価され、通常、設計者がモデルの性能に満足するまで、モデルの調整と再トレーニング、再テストが行われます。
転移学習の場合、使用されるネットワークは事前に学習されている。ネットワークの重みは既に調整され保存されているので、ネットワーク全体を一から学習し直す必要はない。つまり、そのネットワークをすぐにテストに使ったり、ネットワークの特定の層だけを微調整して再トレーニングしたりすることができるのです。これにより、ディープニューラルネットワークの展開が大幅にスピードアップします。
トランスファー・ラーニングとは?
転移学習の背後にある考え方は、あるタスクで学習したモデルを、2つ目の類似タスクに適用することである。2つ目のタスクの重みの一部または全部がすでに学習されているモデルは、より迅速に実装できることを意味します。これにより、迅速な性能評価とモデルのチューニングが可能になり、全体として迅速な展開が可能になる。大規模で複雑なデータセットに加え、深層学習モデルの学習に必要な膨大な計算資源と時間のおかげで、深層学習の分野では転移学習がますます普及している。
転移学習の主な制約は、最初のタスクで学習したモデルの特徴が一般的であり、最初のタスクに固有ではないことです。実際には、画像の一般的な特徴が類似している限り、ある種の画像を認識するために学習したモデルを他の画像の認識に再利用できることを意味する。
転送学習理論
転移学習の利用にはいくつかの重要なコンセプトがあります.転移学習の実装を理解するためには,事前に学習されたモデルがどのようなものか,そしてそのモデルをどのようにニーズに応じて微調整することができるかを確認する必要があります.
転移学習のためのモデルを選択する方法は2つあります。一つは自分のニーズに合わせてゼロからモデルを作り、そのモデルのパラメータと構造を保存しておき、後でそのモデルを再利用する方法です。
もう一つは、既にあるモデルを単純に再利用し、そのパラメータやハイパーパラメータを調整しながら導入する方法です。この例では、事前に学習させたモデルを使い、それを修正することになります。どのようなアプローチを取るか決めたら、モデルを選択します(事前学習済みモデルを使用する場合)。
PyTorchで使用できる事前学習済みモデルは非常に種類が豊富です。事前学習済みのCNNには以下のようなものがあります。
- AlexNet
- CaffeResNet
- Inception
- ResNetシリーズ
- VGGシリーズ
これらの学習済みモデルはPyTorchのAPIからアクセス可能で、指示があればPyTorchがその仕様をマシンにダウンロードします。今回使用するモデルはResNet34で、Resnetシリーズの一部です。
ResnetモデルはImageNetデータセットとCIFAR-10データセットで開発・学習されたものです。そのため、視覚認識タスクに最適化されており、VGGシリーズと比較して顕著な改善を示しているため、これを使用することにしました。
しかし、他の事前学習済みモデルも存在しますので、それらを使って比較実験してみるのも良いでしょう。
PyTorchの移転学習に関するドキュメントで説明されているように、移転学習の利用方法は大きく分けて2つあります:CNNを微調整する方法と、CNNを固定特徴抽出器として使用する方法です。
CNNを微調整する場合、事前に学習したネットワークの持つ重みをランダムに初期化する代わりに使用し、その後は通常のように学習を行います。これに対して、特徴抽出器としてのアプローチでは、最後の数層の重みを除いてCNNのすべての重みを維持し、ランダムに初期化して通常通り学習させることになる。
モデルの微調整が重要なのは、モデルが事前学習されているとはいえ、それは別の(できれば類似の)タスクで学習されているからである。事前学習されたモデルが持つ密結合の重みは、おそらくあなたのニーズにはやや不十分でしょうから、ネットワークの最後の数層を再トレーニングする必要があるでしょう。
一方、ネットワークの最初の数層は単なる特徴抽出層であり、類似の画像に対して同様のパフォーマンスを発揮するため、そのままにしておくことができる。したがって、データセットが小さく、類似していれば、必要な学習は最後の数層の学習のみである。データセットが大きく複雑になればなるほど、モデルの再トレーニングが必要になってきます。転送学習は、使用するデータセットが元の事前学習済みモデルよりも小さく、事前学習済みモデルに与えた画像と類似している場合に最も効果的に機能することを覚えておいてください。
Pytorchで転移学習モデルを扱うということは、どの層を凍結し、どの層を凍結解除するかを選択するということです。モデルを凍結するということは、指定した層のパラメータ(重み)を保持するようにPyTorchに指示することです。モデルの凍結を解除するということは、指定したレイヤーの重みを学習できるようにすることをPyTorchに伝えるということです。
事前学習済みモデルの選択した層の学習が完了したら、おそらく将来使用するために新しく学習された重みを保存したいと思うことでしょう。プリトレーニングされたモデルを使用することは、ゼロからモデルをトレーニングするよりも速いですが、それでもトレーニングには時間がかかりますので、最適なモデルの重みをコピーしておくとよいでしょう。
PyTorchによる転移学習による画像分類
データセットに転送学習を実装する準備ができました。ConvNetを微調整することと、固定特徴抽出器として使用することの両方をカバーします。
データ前処理
まず最初に、使用するデータセットを決める必要があります。ここでは、学習するために本当に鮮明な画像をたくさん持っているものを選ぼう。スタンフォードのCats and Dogsデータセットは非常によく使われるデータセットで、シンプルでありながら説明的なセットであることから選ばれた。このデータセットはここからダウンロードできる。
データセットを2つの同じ大きさのセットに分割してください。「train “と “val “である。
手動でファイルを移動したり、関数を書いて処理したりすることで、いずれにしても行うことができる。また、データセットのサイズを小さくすることもできます。なぜなら、各カテゴリに約12,000枚の画像があり、学習するのに長い時間がかかるからです。各カテゴリー5000枚程度に減らし、1000枚を検証用に確保するとよいでしょう。ただし、学習に使う画像の枚数はあなた次第です。
ここで、使用するデータを準備する方法の一つを紹介します。
import os
import shutil
import re
base_dir = "PetImages/"
# Create training folder
files = os.listdir(base_dir)
# Moves all training cat images to cats folder, training dog images to dogs folder
def train_maker(name):
train_dir = f"{base_dir}/train/{name}"
for f in files:
search_object = re.search(name, f)
if search_object:
shutil.move(f'{base_dir}/{name}', train_dir)
train_maker("Cat")
train_maker("Dog")
# Make the validation directories
try:
os.makedirs("val/Cat")
os.makedirs("val/Dog")
except OSError:
print ("Creation of the directory %s failed")
else:
print ("Successfully created the directory %s ")
# Create validation folder
cat_train = base_dir + "train/Cat/"
cat_val = base_dir + "val/Cat/"
dog_train = base_dir + "train/Dog/"
dog_val = base_dir + "val/Dog/"
cat_files = os.listdir(cat_train)
dog_files = os.listdir(dog_train)
# This will put 1000 images from the two training folders
# into their respective validation folders
for f in cat_files:
validationCatsSearchObj = re.search("5ddd", f)
if validationCatsSearchObj:
shutil.move(f'{cat_train}/{f}', cat_val)
for f in dog_files:
validationCatsSearchObj = re.search("5ddd", f)
if validationCatsSearchObj:
shutil.move(f'{dog_train}/{f}', dog_val)
データの読み込み
データの選択と準備ができたら、まずは必要なライブラリを全てインポートします。nnニューラルネットワーク、オプティマイザー、そして
DataLoadersのようなTorchのパッケージが必要でしょう。また、学習例を可視化するために
matplotlib` も必要です。
データ配列の作成には numpy
が必要で、その他にもいくつかの雑多なモジュールがあります。
from __future__ import print_function, division
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
import torchvision
from torchvision import datasets, models, transforms
import matplotlib.pyplot as plt
import numpy as np
import time
import os
import copy
まず始めに、学習データをロードし、ニューラルネットワークが使用できるように準備する必要があります。そのためにPytorchのtransforms
を利用することになります。トレーニングセットと検証セットの画像が同じサイズであることを確認する必要があるので、transforms.Resize
を使用することにします。
また、少しデータの拡張を行い、異なる角度や切り取りの画像について学習させることでモデルの性能を向上させようとします。そのため、ランダムに画像を切り取ったり回転させたりします。
次に、PyTorchがテンソルを扱えるように、画像からテンソルを作成します。最後に、画像を正規化します。これは、ネットワークが様々な値を扱うのに役立ちます。
そして、選択したすべての変換を合成
します。検証用の変換は、反転や回転をしないことに注意してください。
# Make transforms and use data loaders
# We'll use these a lot, so make them variables
mean_nums = [0.485, 0.456, 0.406]
std_nums = [0.229, 0.224, 0.225]
chosen_transforms = {'train': transforms.Compose([
transforms.RandomResizedCrop(size=256),
transforms.RandomRotation(degrees=15),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean_nums, std_nums)
]), 'val': transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean_nums, std_nums)
]),
}
では、データのディレクトリを設定し、PyTorchの ImageFolder
関数を使用してデータセットを作成します。
# Set the directory for the data
data_dir = '/data/'
# Use the image folder function to create datasets
chosen_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
chosen_transforms[x])
for x in ['train', 'val']}
必要な画像フォルダを選択したので、次はDataLoadersを使って反復可能なオブジェクトを作成します。どのデータセットを使いたいかを指定し、バッチサイズを指定して、データをシャッフルします。
# Make iterables with the dataloaders
dataloaders = {x: torch.utils.data.DataLoader(chosen_datasets[x], batch_size=4,
shuffle=True, num_workers=4)
for x in ['train', 'val']}
データセットに関するいくつかの情報を保存する必要があります。具体的には、データセットのサイズとデータセット内のクラスの名前です。また、CPUかGPUか、どのようなデバイスで作業しているかを指定する必要があります。以下のセットアップでは、GPUが利用可能な場合はGPUを使用し、そうでない場合はCPUを使用します。
dataset_sizes = {x: len(chosen_datasets[x]) for x in ['train', 'val']}
class_names = chosen_datasets['train'].classes
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
それでは、いくつかの画像を関数で可視化してみましょう。入力からNumpyの配列を作成し、それを転置します。次に、平均と標準偏差を使って入力を正規化します。最後に、配列の取り得る値に大きな幅がないように、値を0と1の間で切り取ってから、画像を表示します。
# Visualize some images
def imshow(inp, title=None):
inp = inp.numpy().transpose((1, 2, 0))
mean = np.array([mean_nums])
std = np.array([std_nums])
inp = std * inp + mean
inp = np.clip(inp, 0, 1)
plt.imshow(inp)
if title is not None:
plt.title(title)
plt.pause(0.001) # Pause a bit so that plots are updated
では、この関数を使って、実際にデータを可視化してみましょう。まず、DataLoader
から入力とクラス名を取得し、後で使用するために保存しておくことにします。そして、その入力を表示するためのグリッドを作って表示してみます。
# Grab some of the training data to visualize
inputs, classes = next(iter(dataloaders['train']))
# Now we construct a grid from batch
out = torchvision.utils.make_grid(inputs)
imshow(out, title=[class_names[x] for x in classes])
事前学習済みモデルの設定
次に、転移学習に使用する事前学習済みモデルをセットアップする必要があります。今回は、モデルをそのまま使い、最後の完全連結層をリセットして、特徴量とクラスの数を与えるだけにしておきます。
学習済みのモデルを使う場合、PyTorchはデフォルトでモデルをunfrozen(重みが調整される)に設定します。そのため、モデル全体を学習することになります。
# Setting up the model
# load in pretrained and reset final fully connected
res_mod = models.resnet34(pretrained=True)
num_ftrs = res_mod.fc.in_features
res_mod.fc = nn.Linear(num_ftrs, 2)
それでもまだよくわからない場合は、モデルの構成を可視化することで解決できるかもしれません。
for name, child in res_mod.named_children():
print(name)
その結果がこちらです。
conv1
bn1
relu
maxpool
layer1
layer2
layer3
layer4
avgpool
fc
最後の部分は「fc`」、つまり「Fully-Connected」であることに注意してください。この層だけが形状を変更し、2つのクラスを出力するようにします。
基本的には、最後の完全連結の部分の出力を2つのクラスだけに変更し、他のすべての層の重みを調整するつもりです。
さて、このモデルを学習装置に送る必要があります。また、このモデルで使用する損失基準とオプティマイザを選択する必要があります。CrossEntropyLossと
SGD`オプティマイザは良い選択ですが、他にも多くの選択肢があります。
また、学習率スケジューラを選択します。これは、オプティマイザの学習率を時間と共に減少させ、大きな学習率による非収束を防ぐのに役立ちます。学習率スケジューラについてもっと知りたければ、ここで学ぶことができます。
res_mod = res_mod.to(device)
criterion = nn.CrossEntropyLoss()
# Observe that all parameters are being optimized
optimizer_ft = optim.SGD(res_mod.parameters(), lr=0.001, momentum=0.9)
# Decay LR by a factor of 0.1 every 7 epochs
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)
あとは、モデルを学習させ、予測結果を可視化する関数を定義するだけです。
まず、学習関数から始めましょう。この関数には、選んだモデルと、選んだオプティマイザ、クライテリオン、スケジューラが取り込まれます。また、デフォルトの学習エポック数を指定します。
各エポックには、学習と検証のフェーズがあります。まず最初に、state_dict
を用いて、モデルの初期最適重みを事前学習モード時のものに設定します。
さて、選択されたエポック数において、もしトレーニングフェーズであれば、各エポックについて
- 学習率を減少させる。
- 勾配をゼロにする
- 前方学習パスを実行する
- 損失を計算する
- 後方伝搬を行い、オプティマイザで重みを更新する
また、学習段階でのモデルの精度を記録し、検証段階に移行して精度が向上した場合は、現在の重みを最適なモデルの重みとして保存します。
def train_model(model, criterion, optimizer, scheduler, num_epochs=10):
since = time.time()
best_model_wts = copy.deepcopy(model.state_dict())
best_acc = 0.0
for epoch in range(num_epochs):
print('Epoch {}/{}'.format(epoch, num_epochs - 1))
print('-' * 10)
# Each epoch has a training and validation phase
for phase in ['train', 'val']:
if phase == 'train':
scheduler.step()
model.train() # Set model to training mode
else:
model.eval() # Set model to evaluate mode
current_loss = 0.0
current_corrects = 0
# Here's where the training happens
print('Iterating through data...')
for inputs, labels in dataloaders[phase]:
inputs = inputs.to(device)
labels = labels.to(device)
# We need to zero the gradients, don't forget it
optimizer.zero_grad()
# Time to carry out the forward training poss
# We only need to log the loss stats if we are in training phase
with torch.set_grad_enabled(phase == 'train'):
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
loss = criterion(outputs, labels)
# backward + optimize only if in training phase
if phase == 'train':
loss.backward()
optimizer.step()
# We want variables to hold the loss statistics
current_loss += loss.item() * inputs.size(0)
current_corrects += torch.sum(preds == labels.data)
epoch_loss = current_loss / dataset_sizes[phase]
epoch_acc = current_corrects.double() / dataset_sizes[phase]
print('{} Loss: {:.4f} Acc: {:.4f}'.format(
phase, epoch_loss, epoch_acc))
# Make a copy of the model if the accuracy on the validation set has improved
if phase == 'val' and epoch_acc > best_acc:
best_acc = epoch_acc
best_model_wts = copy.deepcopy(model.state_dict())
print()
time_since = time.time() - since
print('Training complete in {:.0f}m {:.0f}s'.format(
time_since // 60, time_since % 60))
print('Best val Acc: {:4f}'.format(best_acc))
# Now we'll load in the best model weights and return it
model.load_state_dict(best_model_wts)
return model
トレーニングのプリントアウトはこのような感じになります。
Epoch 0/25
----------
Iterating through data...
train Loss: 0.5654 Acc: 0.7090
Iterating through data...
val Loss: 0.2726 Acc: 0.8889
Epoch 1/25
----------
Iterating through data...
train Loss: 0.5975 Acc: 0.7090
Iterating through data...
val Loss: 0.2793 Acc: 0.8889
Epoch 2/25
----------
Iterating through data...
train Loss: 0.5919 Acc: 0.7664
Iterating through data...
val Loss: 0.3992 Acc: 0.8627
可視化
では、モデルが行った予測を見るための関数を作成します。
def visualize_model(model, num_images=6):
was_training = model.training
model.eval()
images_handeled = 0
fig = plt.figure()
with torch.no_grad():
for i, (inputs, labels) in enumerate(dataloaders['val']):
inputs = inputs.to(device)
labels = labels.to(device)
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
for j in range(inputs.size()[0]):
images_handeled += 1
ax = plt.subplot(num_images//2, 2, images_handeled)
ax.axis('off')
ax.set_title('predicted: {}'.format(class_names[preds[j]]))
imshow(inputs.cpu().data[j])
if images_handeled == num_images:
model.train(mode=was_training)
return
model.train(mode=was_training)
これで、すべてを結びつけることができます。私たちの画像でモデルを学習させ、予測を表示します。
base_model = train_model(res_mod, criterion, optimizer_ft, exp_lr_scheduler, num_epochs=3)
visualize_model(base_model)
plt.show()
このトレーニングは、GPUではなくCPUを使用している場合、おそらく長い時間がかかるでしょう。GPUを使用している場合でも、ある程度の時間がかかります。
固定特徴抽出器
学習時間が長いため、多くの人は事前に学習したモデルを固定特徴抽出器として使用し、最後の1層程度を学習するだけにします。これにより、学習時間を大幅に短縮することができます。そのためには、私たちが構築したモデルを置き換える必要があります。ResNetの実装の両バージョンのGitHubレポへのリンクがあるはずです。
事前学習されたモデルが定義されている部分を、重みを固定し、勾配計算やバックプロップを行わないバージョンに置き換えてください。
勾配の計算が不要であることを除いては、以前とほとんど同じように見えます。
# Setting up the model
# Note that the parameters of imported models are set to requires_grad=True by default
res_mod = models.resnet34(pretrained=True)
for param in res_mod.parameters():
param.requires_grad = False
num_ftrs = res_mod.fc.in_features
res_mod.fc = nn.Linear(num_ftrs, 2)
res_mod = res_mod.to(device)
criterion = nn.CrossEntropyLoss()
# Here's another change: instead of all parameters being optimized
# only the params of the final layers are being optimized
optimizer_ft = optim.SGD(res_mod.fc.parameters(), lr=0.001, momentum=0.9)
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)
もし、選択的にレイヤーの凍結を解除し、選択されたいくつかのレイヤーだけに対してグラデーションを計算させたいとしたらどうでしょう。それは可能でしょうか?はい、可能です。
もう一度、モデルの子をプリントアウトして、どのようなレイヤー/コンポーネントを持っているかを覚えておきましょう。
for name, child in res_mod.named_children():
print(name)
これがそのレイヤーです。
conv1
bn1
relu
maxpool
layer1
layer2
layer3
layer4
avgpool
fc
これで、レイヤーが何であるかわかったので、レイヤー3と4だけのように、必要なレイヤーの凍結を解除することができます。
for name, child in res_mod.named_children():
if name in ['layer3', 'layer4']:
print(name + 'has been unfrozen.')
for param in child.parameters():
param.requires_grad = True
else:
for param in child.parameters():
param.requires_grad = False
もちろん、オプティマイザーを更新して、特定のレイヤーだけを最適化するようにする必要があります。
optimizer_conv = torch.optim.SGD(filter(lambda x: x.requires_grad, res_mod.parameters()), lr=0.001, momentum=0.9)
これで、ネットワーク全体をチューニングすることも、最後のレイヤーだけをチューニングすることも、その中間をチューニングすることもできることがわかったと思います。
結論
これでPyTorchで転移学習が実装できましたね。チューニングしたネットワークの実装と、固定特徴抽出器を使った実装を比較し、性能の違いを確認するとよいでしょう。また、特定のレイヤーを凍結したり凍結を解除したりする実験もお勧めします。
その他にも、以下のようなことを試してみてください。
- 異なる環境下でどのモデルがより良いパフォーマンスをするか見るために、異なる事前学習されたモデルを使用する。
- 学習率や運動量の調整など、モデルの引数をいくつか変更する。
- 2つ以上のクラスがあるデータセットで分類してみる。
転送学習のアプリケーションとその背後にある理論についてもっと知りたい場合は、その背後にある数学と使用例についての素晴らしい解説があります。
をご覧ください。
この記事のコードはGitHubのレポで見ることができます。