【転移学習】学習済みVGG16 による転移学習を行う方法【PyTorch】
今回は、PyTorch を使って、学習済みのモデル VGG16 を用いて転移学習をしてみました。
VGG16 は、ImageNet という大量の画像データセットで 1000カテゴリの分類を学習したモデルになります。
この VGG16 モデルに対して転移学習を行って、新たに「アリ」と「ハチ」の画像を学習させます。
Contents
データセットの準備
「アリ」と「ハチ」の画像を PyTorch の公式サイトからダウンロードします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
''' 1. アリとハチの画像データをダウンロードして解凍する ''' # PyTorchのチュートリアル # https://pytorch.org/tutorials/beginner/transfer_learning_tutorial.html # で用意されているデータセットを利用 import os import urllib.request import zipfile # データセットを格納する「data」フォルダー data_dir = './data/' # 指定したフォルダーが存在しない場合は作成する if not os.path.exists(data_dir): os.mkdir(data_dir) # アリとハチの画像のダウンロード先 url = 'https://download.pytorch.org/tutorial/hymenoptera_data.zip' # フォルダーのディレクトリにファイル名を連結してパスを作成 save_path = os.path.join(data_dir, 'hymenoptera_data.zip') # ZIPファイルを解答して保存 if not os.path.exists(save_path): urllib.request.urlretrieve(url, save_path) # ZIPファイルを取得 zip = zipfile.ZipFile(save_path) # ZIPファイルを読み込む zip.extractall(data_dir) # ZIPファイルを解凍 zip.close() # ZIPファイルをクローズ os.remove(save_path) # ZIPファイルを消去 |
「data」フォルダ以下にダウンロードされました。
画像に対して前処理を行うクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
''' 2. 前処理クラスの定義 ''' class ImageTransform(): '''画像の前処理クラス。訓練時、検証時で異なる動作をする。 Attributes: data_transform(dic): train: 訓練用のトランスフォーマーオブジェクト val : 検証用のトランスフォーマーオブジェクト ''' def __init__(self, resize, mean, std): '''トランスフォーマーオブジェクトを生成する。 Parameters: resize(int): リサイズ先の画像の大きさ mean(tuple): (R, G, B)各色チャネルの平均値 std : (R, G, B)各色チャネルの標準偏差 ''' # dicに訓練用、検証用のトランスフォーマーを生成して格納 self.data_transform = { 'train': transforms.Compose([ # ランダムにトリミングする transforms.RandomResizedCrop( resize, # トリミング後の出力サイズ scale=(0.5, 1.0)), # スケールの変動幅 transforms.RandomHorizontalFlip(p = 0.5), # 0.5の確率で左右反転 transforms.RandomRotation(15), # 15度の範囲でランダムに回転 transforms.ToTensor(), # Tensorオブジェクトに変換 transforms.Normalize(mean, std) # 標準化 ]), 'val': transforms.Compose([ transforms.Resize(resize), # リサイズ transforms.CenterCrop(resize), # 画像中央をresize×resizeでトリミング transforms.ToTensor(), # テンソルに変換 transforms.Normalize(mean, std) # 標準化 ]) } def __call__(self, img, phase='train'): '''オブジェクト名でコールバックされる Parameters: img: 画像 phase(str): 'train'または'val' 前処理のモード ''' return self.data_transform[phase](img) # phaseはdictのキー |
画像の処理前と処理後のものを比較してみます。
【処理前】
【処理後】
学習用とテスト用の画像パスをそれぞれリストに格納します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
''' 4. アリとハチの画像のファイルパスをリストにする ''' import os.path as osp import glob import pprint def make_datapath_list(phase="train"): ''' データのファイルパスを格納したリストを作成する。 Parameters: phase(str): 'train'または'val' Returns: path_list(list): 画像データのパスを格納したリスト ''' # 画像ファイルのルートディレクトリ rootpath = "./data/hymenoptera_data/" # 画像ファイルパスのフォーマットを作成 # rootpath + # train/ants/*.jpg # train/bees/*.jpg # val/ants/*.jpg # val/bees/*.jpg target_path = osp.join(rootpath + phase + '/**/*.jpg') # ファイルパスを格納するリスト path_list = [] # ここに格納する # glob()でファイルパスを取得してリストに追加 for path in glob.glob(target_path): path_list.append(path) return path_list # ファイルパスのリストを生成 train_list = make_datapath_list(phase="train") val_list = make_datapath_list(phase="val") # 訓練データのファイルパスの前後5要素ずつ出力 print('train') pprint.pprint(train_list[:5]) pprint.pprint(train_list[-6:-1]) # 検証データのファイルパスの前後5要素ずつ出力 print('val') pprint.pprint(val_list[:5]) pprint.pprint(val_list[-6:-1]) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
train ['./data/hymenoptera_data/train\\ants\\0013035.jpg', './data/hymenoptera_data/train\\ants\\1030023514_aad5c608f9.jpg', './data/hymenoptera_data/train\\ants\\1095476100_3906d8afde.jpg', './data/hymenoptera_data/train\\ants\\1099452230_d1949d3250.jpg', './data/hymenoptera_data/train\\ants\\116570827_e9c126745d.jpg'] ['./data/hymenoptera_data/train\\bees\\873076652_eb098dab2d.jpg', './data/hymenoptera_data/train\\bees\\90179376_abc234e5f4.jpg', './data/hymenoptera_data/train\\bees\\92663402_37f379e57a.jpg', './data/hymenoptera_data/train\\bees\\95238259_98470c5b10.jpg', './data/hymenoptera_data/train\\bees\\969455125_58c797ef17.jpg'] val ['./data/hymenoptera_data/val\\ants\\10308379_1b6c72e180.jpg', './data/hymenoptera_data/val\\ants\\1053149811_f62a3410d3.jpg', './data/hymenoptera_data/val\\ants\\1073564163_225a64f170.jpg', './data/hymenoptera_data/val\\ants\\1119630822_cd325ea21a.jpg', './data/hymenoptera_data/val\\ants\\1124525276_816a07c17f.jpg'] ['./data/hymenoptera_data/val\\bees\\65038344_52a45d090d.jpg', './data/hymenoptera_data/val\\bees\\6a00d8341c630a53ef00e553d0beb18834-800wi.jpg', './data/hymenoptera_data/val\\bees\\72100438_73de9f17af.jpg', './data/hymenoptera_data/val\\bees\\759745145_e8bc776ec8.jpg', './data/hymenoptera_data/val\\bees\\936182217_c4caa5222d.jpg'] |
データセット作成用のクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
''' 5. アリとハチの画像のデータセットを作成するクラス ''' import torch.utils.data as data class MakeDataset(data.Dataset): ''' アリとハチの画像のDatasetクラス PyTorchのDatasetクラスを継承 Attributes: file_list(list): 画像のパスを格納したリスト transform(object): 前処理クラスのインスタンス phase(str): 'train'または'val' Returns: img_transformed: 前処理後の画像データ label(int): 正解ラベル ''' def __init__(self, file_list, transform=None, phase='train'): '''インスタンス変数の初期化 ''' self.file_list = file_list # ファイルパスのリスト self.transform = transform # 前処理クラスのインスタンス self.phase = phase # 'train'または'val' def __len__(self): '''len(obj)で実行されたときにコールされる関数 画像の枚数を返す''' return len(self.file_list) def __getitem__(self, index): '''Datasetクラスの__getitem__()をオーバーライド obj[i]のようにインデックスで指定されたときにコールバックされる Parameters: index(int): データのインデックス Returns: 前処理をした画像のTensor形式のデータとラベルを取得 ''' # ファイルパスのリストからindex番目の画像をロード img_path = self.file_list[index] # ファイルを開く -> (高さ, 幅, RGB) img = Image.open(img_path) # 画像を前処理 -> torch.Size([3, 224, 224]) img_transformed = self.transform( img, self.phase) # 正解ラベルをファイル名から切り出す if self.phase == 'train': # 訓練データはファイルパスの31文字から34文字が'ants'または'bees' label = img_path[30:34] elif self.phase == 'val': # 検証データはファイルパスの29文字から32文字が'ants'または'bees' label = img_path[28:32] # 正解ラベルの文字列を数値に変更する if label == 'ants': label = 0 # アリは0 elif label == 'bees': label = 1 # ハチは1 return img_transformed, label |
データローダーを生成します。
ミニバッチのサイズは 32 となります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
''' 6. データローダーの生成 ''' import torch # ミニバッチのサイズを指定 batch_size = 32 # 画像のサイズ、平均値、標準偏差の定数値 size, mean, std = SIZE, MEAN, STD # MakeDatasetで前処理後の訓練データと正解ラベルを取得 train_dataset = MakeDataset( file_list=train_list, # 訓練データのファイルパス transform=ImageTransform(size, mean, std), # 前処理後のデータ phase='train') # MakeDatasetで前処理後の検証データと正解ラベルを取得 val_dataset = MakeDataset( file_list=val_list, # 検証データのファイルパス transform=ImageTransform(size, mean, std), # 前処理後のデータ phase='val') # 訓練用のデータローダー:(バッチサイズ, 3, 224, 224)を生成 train_dataloader = torch.utils.data.DataLoader( train_dataset, batch_size=batch_size, shuffle=True) # 検証用のデータローダー:(バッチサイズ, 3, 224, 224)を生成 val_dataloader = torch.utils.data.DataLoader( val_dataset, batch_size=batch_size, shuffle=False) # データローダーをdictにまとめる dataloaders = {'train': train_dataloader, 'val': val_dataloader} |
学習済みモデルをロード
学習済みモデル VGG16 をロードします。
出力層をデフォルトの 1000クラス分類から 「ハチ」か「アリ」かの2クラス分類に変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
''' 7. 学習済みのVGG16モデルをロード ''' from torchvision import models import torch.nn as nn # ImageNetで事前トレーニングされたVGG16モデルを取得 model = models.vgg16(pretrained=True) # VGG16の出力層のユニット数を2にする model.classifier[6] = nn.Linear( in_features=4096, # 入力サイズはデフォルトの4096 out_features=2) # 出力はデフォルトの1000から2に変更 # 使用可能なデバイス(CPUまたはGPU)を取得する device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) print(model) |
モデルの構成は以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
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)) (3): ReLU(inplace=True) (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (6): ReLU(inplace=True) (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (8): ReLU(inplace=True) (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (11): ReLU(inplace=True) (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (13): ReLU(inplace=True) (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (15): ReLU(inplace=True) (16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (18): ReLU(inplace=True) (19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (20): ReLU(inplace=True) (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (22): ReLU(inplace=True) (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (25): ReLU(inplace=True) (26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (27): ReLU(inplace=True) (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (29): ReLU(inplace=True) (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=2, bias=True) ) ) |
重みを更新するレイヤーを設定します。
「param.requires_grad = True」とすると、重みが更新され、
「param.requires_grad = False」とすると、重みが更新されなくなります。
今回は、出力層以外は学習しない(重みの更新を行わない)ようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
''' 8. VGG16で学習可能にする層を設定 ''' # 転移学習で学習させるパラメータを、変数params_to_updateに格納する params_to_update = [] # 出力層の重みとバイアスを更新可として登録 update_param_names = ['classifier.6.weight', 'classifier.6.bias'] # 出力層以外は勾配計算をなくし、変化しないように設定 for name, param in model.named_parameters(): if name in update_param_names: param.requires_grad = True # 勾配計算を行う params_to_update.append(param) # パラメーター値を更新 print(name) # 更新するパラメーター名を出力 else: param.requires_grad = False # 出力層以外は勾配計算なし |
更新するパラメータ名は以下のように出力されます。
1 2 |
classifier.6.weight classifier.6.bias |
VGG16 の出力層のインデックスは「6」であることが確認できます。
学習を行う関数を定義
実際に学習を行う関数を定義します。
1 2 3 4 5 6 7 8 9 |
''' 9. 損失関数とオプティマイザーを生成 ''' import torch.optim as optim # 損失関数 criterion = nn.CrossEntropyLoss() # オプティマイザー optimizer = optim.SGD(params=params_to_update, lr=0.001, momentum=0.9) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
''' 10. 学習を行う関数の定義 ''' from tqdm import tqdm def train_model(model, dataloaders, criterion, optimizer, num_epochs): '''モデルを使用して学習を行う Parameters: model: モデルのオブジェクト dataloaders(dict): 訓練、検証のデータローダー criterion: 損失関数 optimizer: オプティマイザー num_epochs: エポック数 ''' # epochの数だけ for epoch in range(num_epochs): print('Epoch {}/{}'.format(epoch+1, num_epochs)) print('-------------') # 学習と検証のループ for phase in ['train', 'val']: if phase == 'train': model.train() # モデルを訓練モードにする else: model.eval() # モデルを検証モードにする epoch_loss = 0.0 # 1エポックあたりの損失の和 epoch_corrects = 0 # 1エポックあたりの精度の和 # 未学習時の検証性能を確かめるため、epoch=0の学習は行わない if (epoch == 0) and (phase == 'train'): continue # 1ステップにおける訓練用ミニバッチを使用した学習 # tqdmでプログレスバーを表示する for inputs, labels in tqdm(dataloaders[phase]): # torch.Tensorオブジェクトにデバイスを割り当てる inputs, labels = inputs.to(device), labels.to(device) # オプティマイザーを初期化 optimizer.zero_grad() # 順伝搬(forward)計算 with torch.set_grad_enabled(phase == 'train'): outputs = model(inputs) # モデルの出力を取得 # 出力と正解ラベルの誤差から損失を取得 loss = criterion(outputs, labels) # 出力された要素数2のテンソルの最大値を取得 _, preds = torch.max(outputs, dim=1) # 訓練モードではバックプロパゲーション if phase == 'train': loss.backward() # 逆伝播の処理(自動微分による勾配計算) optimizer.step() # 勾配降下法でバイアス、重みを更新 # ステップごとの損失を加算、inputs.size(0)->32 epoch_loss += loss.item() * inputs.size(0) # ステップごとの精度を加算 epoch_corrects += torch.sum(preds == labels.data) # エポックごとの損失と精度を表示 epoch_loss = epoch_loss / len(dataloaders[phase].dataset) epoch_acc = epoch_corrects.double( ) / len(dataloaders[phase].dataset) # 出力 print('{} - loss: {:.4f} - acc: {:.4f}'.format( phase, epoch_loss, epoch_acc)) |
学習・検証を行う
実際に学習・検証を実行します。
1 2 3 4 5 6 7 |
%%time ''' 11. 学習・検証を実行する ''' num_epochs=3 train_model(model, dataloaders, criterion, optimizer, num_epochs=num_epochs) |
結果は以下のように、学習は約11秒で終了しました。
おそらく更新するパラメータが少ないため、早く学習が終了したのだと考えられます。
参考文献
関連記事
-
【機械学習】モンテカルロ法(Monte Carlo method)について。
モンテカルロ法(Monte Carlo method)とは、シュミレーションや数値計算を乱数を用いて
-
【Weka】アソシエーション・ルール(association rule)【機械学習】
フリーの機械学習ツール Weka でアソシエーション・ルール(association rule)を使
-
【PyTorch】畳込みニューラルネットワークを構築する方法【CNN】
今回は、PyTorch を使って畳込みニューラルネットワーク(CNN)を構築する方法について紹介しま
-
【Fashion-MNIST】ファッションアイテムのデータセットを使ってみた【TensorFlow】
今回は、機械学習用に公開されているデータセットの1つである「Fashion-MNIST」について紹介
-
【Weka】ARFF 形式から CSV 形式に簡単に変換する方法。
フリーのデータマイニングツールである WEKA では、ARFF 形式と CSV 形式のデータを読み込
-
【Weka】CSVファイルを読み込んで決定木を実行。
フリーの機械学習ソフト Weka を使って、CSVファイルを読み込んで決定木(Decision Tr
-
【探索】縦型・横型・反復深化法の探索手法の比較。
探索とは、チェスや将棋や囲碁などのゲームをコンピュータがプレイするときに、どの手を指すかを決定するの
-
【深層学習】 TensorFlow と Keras をインストールする【Python】
今回は、Google Colaboratory 上で、深層学習(DeepLearning)フレームワ
-
【Chainer】手書き数字認識をしてみた【Deep Learning】
Chainerを用いて、ニューラルネットワークを構築し、手書き数字認識を行ったときのメモです。
-
【機械学習】 scikit-learn で精度・再現率・F値を算出する方法【Python】
今回は、2クラス分類で Python の scikit-learn を使った評価指標である、精度(P