Welcome to Mashykom WebSite



Pytorch による画像分類と転移学習

 PyTorch はディープラーニングを実装する際に用いられるディープラーニング用ライブラリのPython APIの一つです。もともとは、Torch7と呼ばれるLua言語で書かれたライブラリでした。PyTorchは、このTorch7とPreferred Networks社のChainerをベースに2017年2月に作られたPython用ライブラリです。Chainerは日本のPreferred Networks社が開発したライブラリですが、Pytorchに統合されました。Caffe2もPyTorchに併合されました。現在、PyTorch は Team PyTorch によって開発されています。

 PyTorchの利点はDefine by Run(動的計算グラフ)と呼ばれる特徴です。Define by Runは入力データのサイズや次元数に合わせてニューラルネットワークの形や計算方法を変更することができます。一方で、TensorFlowの特徴はDefine and Run(静的計算グラフ)と呼ばれます。Define and Runではニューラルネットワークの計算方法をはじめに決めてしまうため、入力データの次元がデータごとに異なる状況に対応しづらいという特徴があります 。Keras + Tensorflowは見やすく簡易で、非常に簡単にネットワークを作成できるので、人工知能の専門家以外の人たちにとって、使いやすい必須の道具となっています。しかし、アップグレードが後方互換性を持たないという欠点と、動作が遅いという問題点もあります。

 他方で、PyTorchは、Define by Runという特徴ゆえに、AIを開発する専門家に必須のアイテムになりつつあります。Object Detection用のライブラリの中では処理速度が最も最速です。

 動作確認は以下の環境下で行っています。

pythonのバージョンは3.7.4です
numpyのバージョンは1.17.2です
scipyのバージョンは1.3.1です
matplotlibのバージョンは3.1.1です
PIL(Pillow)のバージョンは6.2.0です
Pytorchのバージョンは1.4.1です。

 このページでは、 PyTorch を用いた画像分類の Python 実装の実例を紹介します。最初に、 PyTorch の使用についての簡単な説明をします。その後、CNN モデルの作成の手続、および、 Cifar10 などのデータセットを用いた学習の実例を取り上げます。

 画像分類(image classifier)のモデルを訓練するために使用するデータセットは Cifar10 やImageNet などですが、ImageNet は1400万枚を超える画像の集合体です。このようなビッグデータを用いた学習は高速なGPU を用いても相当な時間(日)数が必要とされます。こうした難題を回避するために、ビッグデータを用いて pretrained model が提供されるようになり、効率的に使用されるようになってきました。目的に応じて、異なるデータで pretrained model を再学習をすることも必要なってきました。

 以下では、 pretrained modelを用いた画像分類に Python 実装を説明すると同時に、転移学習とファインチューニングの実際についても簡単な説明をします。

 なお、Pytorchの公式サイトのTutorialsにある例題はすべてGoogle Colab で実行できます。したがって、pythonおよびPytorchの環境がインストールされていないPCやスマホからでも、Pytorchの公式サイトにアクセスすれば、Google Colabを用いて自習できます。Google Colabの使用法については、Python APIとGoogle Colabの使用法を参照ください。

 AIや機械学習に関するページで説明されるPython コードを自分の手で実行したいと希望する方は、Googleのアカウント登録、および、GitHubのアカウントの登録をすることをお勧めします。両方とも無料で行えます。


Last updated: 2020.4.17(first uploaded 2018.6.13)


PyTorchのインストールと簡単な利用方法

 以下のインストールでは、anaconda3がインストールされていることを前提にします。まず、PyTorchの公式サイトにアクセスして、「Get Started」のページに行きます。OSの選択をして、pipを選択し、pythn3.7、そして、GPUのnoneを選択すると、run this commandの欄に以下のコマンドが表示されます。


(base) $ pip3 install torch torchvision

このコマンドを入力します。pytorchのインストールが始まります。PyTorchは、ディレクトリ anaconda3/lib/python3.7/site-packages/ の下に 'torch' とういう名前でインストールされます。

 以下では、Pytorchの公式サイトのTutorialsに沿って説明します。このTutorials にある例題はすべてGoogle Colab で実行できます。

 PyTorchでは、numpyのndarrayと類似した(拡張した)Tensorsという概念を使います。pythonを起動して、以下のコマンドを入力してください。


from __future__ import print_function
import torch
x=torch.Tensor(5,3)
print(x)

以下のように表示されます。


tensor([[ 0.0000e+00, -2.5244e-29, -4.7749e+03],
        [-8.5920e+09,  6.2369e-30,  1.4013e-45],
        [ 6.2836e-30,  1.4013e-45,  6.2836e-30],
        [ 1.4013e-45,  6.2778e-30,  1.4013e-45],
        [ 6.2821e-30,  1.4013e-45,  3.8576e-32]])

5x3行列のインスタンスを作成してますが、要素を指定してませんので内容は無意味です。明らかに、tensorはnumpyのndarrayの拡張です。次に、要素が空である5x3 行列をtorchで作成します。


x = torch.empty(5, 3)
print(x)

以下のようになります。


tensor([[ 0.0000e+00, -2.5244e-29,  0.0000e+00],
        [-2.5244e-29,  5.6052e-45,  1.7180e+25],
        [ 5.7738e+16,  4.5912e-41,  0.0000e+00],
        [ 0.0000e+00,  0.0000e+00,  1.0951e+21],
        [-5.7835e+03, -1.5849e+29, -5.785

ランダムな値が入った行列を作成することもできます。


x = torch.rand(5, 3)
print(x)

以下のように表示されます。


tensor([[ 0.2561,  0.4928,  0.4191],
        [ 0.0556,  0.4421,  0.0621],
        [ 0.8881,  0.8851,  0.2572],
        [ 0.0567,  0.2491,  0.2788],
        [ 0.3648,  0.2398,  0.8449]])

加算をして見ましょう。


y=torch.rand(5,3)
print(x+y)

以下のようになります。


tensor([[ 0.9846,  1.3548,  0.9620],
        [ 0.1496,  1.2262,  0.4178],
        [ 1.2757,  1.3802,  0.8955],
        [ 0.7388,  0.2780,  1.1868],
        [ 0.9870,  0.7609,  1.4777]])

また、


print(torch.add(x, y))

と入力しても、同じ結果が得られます。このように、NumPy like な操作ができます。


 CNN を用いた画像識別や物体検出のために用意されている PyTorch のモジュールは以下の通りです。

 下のスクリプトは AlexNet のネットワークを作成するコードの例です。


class AlexNet(nn.Module):

    def __init__(self, num_classes):
        super(AlexNet, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=5),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(64, 192, kernel_size=5, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(192, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
        )
        self.classifier = nn.Linear(256, num_classes)

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)
        x = self.classifier(x)
        return x

net = AlexNet()
print(net)

 上記のような形式で書かれたコードが多いと思います。ネットワーク層を、__init__ メソッドを最初に置いて定義します。super(AlexNet, self).__init__() の書き方は定石になっていますので、こうして書くと理解して下さい。self.features = nn.sequential( ...) はネットワークの特徴抽出の各レイヤーを定義しています。順伝搬の処理は forward メソッドで書きます。x = x.view(x.size(0), -1) は、テンソルの形状を変形しています。reshape() と同じ機能を持ちます。x.size(0) はbatch-size なので、このサイズに合わせて形状変形されます。

 このコードを実行すると、以下に示されるネットワークが作成されていることが確認できます。


AlexNet(
  (features): Sequential(
    (0): Conv2d(3, 64, kernel_size=(11, 11), stride=(4, 4), padding=(5, 5))
    (1): ReLU(inplace)
    (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (3): Conv2d(64, 192, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (4): ReLU(inplace)
    (5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (6): Conv2d(192, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (7): ReLU(inplace)
    (8): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (9): ReLU(inplace)
    (10): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU(inplace)
    (12): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (classifier): Linear(in_features=256, out_features=10, bias=True)
)

 nn.Conv2d の入力は、(batch_size, channel, height, width)となっていて、出力も同じ形状をしています。例えば、 Conv2d(64, 192, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2)) は入力のチャネル数が64 、出力のチャネル数が 192 であることを意味します。kernel_size 、 stride 、および padding の説明は既に前のページで説明していますので、省略します。詳しくは、PytorchのTutorialsを読んでください。

 入力データ(画像など)を読み込み、前処理をするためには、torchvision などのモジュールが必要です。また、torchvision.datasets には、MNIST、CIFAR10、STL10 などの画像データセットが組み込まれています。torchvision.models には、ImageNet で学習された AlexNet 、 VGG16,19 、 ResNet , SqueezeNet などの画像分類モデルが利用可能になっています。

 画像の読み込み方法について簡単に説明します。/path/to/image に画像が配置されているとします。読み込んだ画像を Pytorch で処理できるようにテンソル化します。

import torch
import torchvision
import torchvision.transforms as transforms
from PIL import Image 

image = Image.open(/path/to/image)

imsize = (512, 512) #  example: dimension required to fit network's input dimensions
loader = transforms.Compose([
    transforms.Resize(imsize),  # scale imported image
    transforms.ToTensor()])  # transform it into a torch tensor

image =loader(image)

 coco val2017 のような多量のデータセットを読み込みケースでは、以下のようにします。


from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision import transforms

!mkdir data
!wget http://images.cocodataset.org/zips/val2017.zip
!unzip val2017.zip
!mv val2017 data
!rm val2017.zip

image_size = xxx
batch_size = xxx

transform = transforms.Compose([
        transforms.Resize(image_size),
        transforms.CenterCrop(image_size),
        transforms.ToTensor()
    ])
train_dataset = datasets.ImageFolder(data, transform)
train_loader = DataLoader(train_dataset, batch_size=batch_size)

 datasets.ImageFolder は保存した画像からデータセットを作成します。DataLoader は読み込んだデータセットを batch_size ごとに区切った画像のデータにしてくれます。
 Dataloader の出力= [[batch_1], [batch_2], ... [batch_n]]
ということですので
 len(train_datasets)="すべてのデータの数"
 len(train_loader)="イテレーションの数(すべてのデータ数/バッチサイズ)"
と単純に理解できます。DataLoader の引数のオプションでは、下の例で出てくるように、shuffle=True, num_workers=2 と指定することもできます。デフォルトでは、shuffle=False, num_workers=0 です。デフォルト値のままで使用する時は、これらのオプションの記述を省略します。

  Pytorch で画像を表示したい時は、Python の matplotlib モジュールを使用します。


%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.image as mpimg

img=mpimg.imread('/path/to/image') 

plt.figure()
plt.imshow(img)
plt.title('image')
plt.show()

 詳しい説明は、 matplotlib のサイトを参照ください。

PyTorchによるCNNモデルの学習

 PyTorchを用いてCNNモデルを作成して、このモデルをCifar10のデータを使った学習を取り上げます。Pytorchの公式サイトにあるTutorial for Deep Learning を開いて下さい。そこにあるDownload NotebookからJupyter Notebookのファイルをダウンロードして下さい。または、Run in Google ColabでPythonコードを開いて下さい。このコードは、 the CIFAR10 dataset を使って、10 classes( ‘airplane’, ‘automobile’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’)の識別を行うプログラムです。CIFAR-10 の画像サイズは 3x32x32, i.e. 3-channel color images of 32x32 pixels です。画像データ変換は、torchvision.datasets と torch.utils.data.DataLoader を活用します。

torchvisionを用いて, CIFAR10 データを読み込みます。transforms.Normalizeの引数がtorch.Tensorであることから,リストの初めから順に関数を実行して行きます。transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)は,引数の一つ目のタプルがRGBの各チャンネルの平均を表し,二つ目のタプルが標準偏差を表します。これらの平均と標準偏差にあわせて正規化します。


import torch
import torchvision
import torchvision.transforms as transforms

transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
                                          shuffle=True, num_workers=2)

testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=4,
                                         shuffle=False, num_workers=2)

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

import matplotlib.pyplot as plt
import numpy as np

# functions to show an image


def imshow(img):
    img = img / 2 + 0.5     # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.show()


# get some random training images
dataiter = iter(trainloader)
images, labels = dataiter.next()

# show images
imshow(torchvision.utils.make_grid(images))
# print labels
#print(' '.join('%5s' % classes[labels[j]] for j in range(4)))

 Dataloaderには __iter__ と __next__ が定義されているので、iter(Dataloader).next() としてあげれば, 最初から1バッチずつ取り出すことができます。trainloader から読み込まれたnpimg は形式(RGB, 縦,横)と並んでいます。plt.imshow の引数は,(n, m, RGB)という形式にする必要あり、np.transpose(1,2,0) を用いてその順に並び替えています。なお、torchvision.utils.make_grid はバッチ形式で与えられた画像を格子状に配置して一枚の画像とすることです。このコードを実行すると、1バッチのデータ画像のサンプルが表示されます。次に、Convolutional Neural Networkのモデルを作成します。


import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x


net = Net()

import torch.optim as optim

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

このモデルを学習させます。


for epoch in range(2):  # loop over the dataset multiple times

    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        # get the inputs; data is a list of [inputs, labels]
        inputs, labels = data

        # zero the parameter gradients
        optimizer.zero_grad()

        # forward + backward + optimize
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # print statistics
        running_loss += loss.item()
        if i % 2000 == 1999:    # print every 2000 mini-batches
            print('[%d, %5d] loss: %.3f' %
                  (epoch + 1, i + 1, running_loss / 2000))
            running_loss = 0.0

print('Finished Training')

以下の学習結果となります。


[1,  2000] loss: 2.266
[1,  4000] loss: 1.919
[1,  6000] loss: 1.704
[1,  8000] loss: 1.597
[1, 10000] loss: 1.531
[1, 12000] loss: 1.481
[2,  2000] loss: 1.410
[2,  4000] loss: 1.381
[2,  6000] loss: 1.330
[2,  8000] loss: 1.333
[2, 10000] loss: 1.318
[2, 12000] loss: 1.290
Finished Training

検出に利用された画像とそのラベルは以下のように表示されます。

obeject_detection_4.png
GroundTruth: cat ship ship plane


PATH = './cifar_net.pth'
torch.save(net.state_dict(), PATH)

dataiter = iter(testloader)
images, labels = dataiter.next()

# print images
imshow(torchvision.utils.make_grid(images[:4]))
print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(4)))

モデルの精度をテストデータで検証します。


net = Net()
net.load_state_dict(torch.load(PATH))
outputs = net(images)

_, predicted = torch.max(outputs, 1)

print('Predicted: ', ' '.join('%5s' % classes[predicted[j]]
                              for j in range(4)))

予測結果は、Predicted: cat ship ship ship となります。モデルの分類予測が正しかったでしょうか。予測の精度は5割を超える程度だと思われます。

 AlexNetをCifar-10 データを使用して学習するプログラムは、 このGoogle Colabにアップロードしておきました。よかったら自身で実行してみてください。

Pytorch の Examples パッケージはpytorch examplesからダウンロードできます。

VGG16 の pretrained model を用いた画像分類の例

 初めに、VGG 16 の学習済みモデルを用いた画像分類の例を取り上げてみます。ここで紹介する例は、この Google Colab にアップされています。小川雄太郎著『PyTorchによる発展ディープラーニング』付録のコードを修正しています。

 必要なモジュールを import します。


import numpy as np
import json
from PIL import Image
import matplotlib.pyplot as plt
%matplotlib inline

import torch
import torchvision
from torchvision import models, transforms

 VGG 16 の pretrained model を読み込みます。


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

 画像データを読み込みます。私の GitHub Repo に必要な画像がありますので、ダウンロードします。


!git clone https://github.com/mashyko/Image-Classifier
%cd Image-Classifier
!ls images/

#  画像読み込み
image_file_path = 'images/289376763_4f2dee00c9.jpg'
img = Image.open(image_file_path)  # [高さ][幅][色RGB]

#  元の画像の表示
plt.imshow(img)
plt.show()

入力画像の写真はこれです。

golden.jpg

 画像分類の推論を行います。


# ILSVRCのラベル情報をロードし辞書型変数を生成します
class_index = json.load(open('./data/imagenet_class_index.json', 'r'))

# 前処理の後、バッチサイズの次元を追加する
resize = 224
mean = (0.485, 0.456, 0.406)
std = (0.229, 0.224, 0.225)
transform = transforms.Compose([
        transforms.Resize(resize),  # 短い辺の長さがresizeの大きさになる
        transforms.CenterCrop(resize),  # 画像中央をresize × resizeで切り取り
        transforms.ToTensor(),  # Torchテンソルに変換
        transforms.Normalize(mean, std)  # 色情報の標準化
        ])
img_transformed = transform(img)  # torch.Size([3, 224, 224])
inputs = img_transformed.unsqueeze_(0)  # torch.Size([1, 3, 224, 224])

# モデルに入力し、モデル出力をラベルに変換する
out = net(inputs)  # torch.Size([1, 1000])
maxid = np.argmax(out.detach().numpy())

result = class_index[str(maxid)][1]

# 予測結果を出力する
print("入力画像の予測結果:", result)

 推論結果は、「Labrador_retriever」となります。正解ですか?実は、golden_retriever なのです。間違いやすいのは確かです。他の画像を適当に選択して、画像分類の推論を試みて下さい。


ResNet 18 を用いた 転移学習の例

 ファインチューニング(finetuning)は新しいデータに合わせて pretrained model の重み係数を再学習することです。転移学習(transfer training)は pretrained model の重み係数のうち、最後のレイヤ(分類きの全結合層)などの重みだけを再学習させることです。このような分類の仕方、この言葉の使い方は必ずしも正しいとは思いませんが、ここではとりあえずこうした使用をします。

 転移学習と Finetuning の具体例を紹介している Pytorch のコードはこの 公式 Tutorials にあります。この Tutroials に沿って説明します。

 まず必要なモジュールを import します。


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 numpy as np
import torchvision
from torchvision import datasets, models, transforms
import matplotlib.pyplot as plt
import time
import os
import copy

plt.ion()   # interactive mode

 ImageNet の一部であるアリとハチの画像集(hymenoptera_data)を利用します。


!wget https://download.pytorch.org/tutorial/hymenoptera_data.zip
!unzip hymenoptera_data.zip
!rm hymenoptera_data.zip

 前節でも行ったデータの前処理を行います。

# Data augmentation and normalization for training
# Just normalization for validation
data_transforms = {
    'train': transforms.Compose([
        transforms.RandomResizedCrop(224),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'val': transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}

data_dir = 'hymenoptera_data'
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
                                          data_transforms[x])
                  for x in ['train', 'val']}
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4,
                                             shuffle=True, num_workers=4)
              for x in ['train', 'val']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}
class_names = image_datasets['train'].classes

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

 この前処理も共通の形式をとっています。目新しい使用法は、

{x: datasets.ImageFolder(os.path.join(data_dir, x), 
     data_transforms[x]) for x in ['train', 'val']}

という Python の辞書内包表記です。ここで、 x が辞書の key となっています。

 入力画像の中から適当に4個の画像を表示します。クラス imshow の定義は定石通りです。

def imshow(inp, title=None):
    """Imshow for Tensor."""
    inp = inp.numpy().transpose((1, 2, 0))
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    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

# Get a batch of training data
inputs, classes = next(iter(dataloaders['train']))

# Make a grid from batch
out = torchvision.utils.make_grid(inputs)

imshow(out, title=[class_names[x] for x in classes])

以下のような入力画像の写真が表示されます。

ants_bees_1.png

 モデルの重み係数の再学習を始めるために、クラス train_model を定義します。このクラスが再学習と評価をするときに呼ばれる重要なモジュールです。パラメータ scheduler は 何ステップごとに学習率を減衰させるかを設定する"torch.optim.lr_scheduler" の LR scheduler オブジェクトです。詳細な説明はここでは無視して下さい。

def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
    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

            running_loss = 0.0
            running_corrects = 0

            # Iterate over data.
            for inputs, labels in dataloaders[phase]:
                inputs = inputs.to(device)
                labels = labels.to(device)

                # zero the parameter gradients
                optimizer.zero_grad()

                # forward
                # track history if only in train
                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()

                # statistics
                running_loss += loss.item() * inputs.size(0)
                running_corrects += torch.sum(preds == labels.data)

            epoch_loss = running_loss / dataset_sizes[phase]
            epoch_acc = running_corrects.double() / dataset_sizes[phase]

            print('{} Loss: {:.4f} Acc: {:.4f}'.format(
                phase, epoch_loss, epoch_acc))

            # deep copy the model
            if phase == 'val' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())

        print()

    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_elapsed // 60, time_elapsed % 60))
    print('Best val Acc: {:4f}'.format(best_acc))

    # load best model weights
    model.load_state_dict(best_model_wts)
    return model

 ResNet 18 の pretrained model をダウンロードします。上記の画像分類の例と異なるところは、分類器の出力が「アリとハチ」の2種類となるところです。model_ft.fc = nn.Linear(num_ftrs, 2) で分類器の全結合層の出力を2種類にしています。

model_ft = models.resnet18(pretrained=True)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, 2)

model_ft = model_ft.to(device)

criterion = nn.CrossEntropyLoss()

# Observe that all parameters are being optimized
optimizer_ft = optim.SGD(model_ft.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)

 実際に再学習を実行します。GPU では数分で終わります。


model_ft = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler,
                       num_epochs=25)

 結果を表示します。クラス visualize_model は評価の実行過程でまた使用します。

def visualize_model(model, num_images=6):
    was_training = model.training
    model.eval()
    images_so_far = 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_so_far += 1
                ax = plt.subplot(num_images//2, 2, images_so_far)
                ax.axis('off')
                ax.set_title('predicted: {}'.format(class_names[preds[j]]))
                imshow(inputs.cpu().data[j])

                if images_so_far == num_images:
                    model.train(mode=was_training)
                    return
        model.train(mode=was_training)

visualize_model(model_ft)

 以上でファインチューニング学習の部は終了です。


 次に、分類器以外の重み係数を固定しておいて、分類器層のパラメータだけを学習させます。requires_grad = False と requires_grad=True を使い分けます。requires_grad = False は微分計算をしないという意味です。微分計算をしないということは学習しないことを含意します。

model_conv = torchvision.models.resnet18(pretrained=True)
for param in model_conv.parameters():
    param.requires_grad = False

# Parameters of newly constructed modules have requires_grad=True by default
num_ftrs = model_conv.fc.in_features
model_conv.fc = nn.Linear(num_ftrs, 2)

model_conv = model_conv.to(device)
criterion = nn.CrossEntropyLoss()

# Observe that only parameters of final layer are being optimized as
# opoosed to before.
optimizer_conv = optim.SGD(model_conv.fc.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_conv, step_size=7, gamma=0.1)

 再学習を実行します。


model_conv = train_model(model_conv, criterion, optimizer_conv,
                         exp_lr_scheduler, num_epochs=25)

visualize_model(model_conv)

plt.ioff()
plt.show()

 この Colab のコードを実行して、結果をみて下さい。

 また、VGG16 の pretrained model の転移学習の実装コードは、この Google Colab にアップロードしておきました。上記の実装コードと若干違いますが、基本構造は同じです。よかったら検証してみて下さい。下のような画像が表示されると思います。

ants_bees_2.png

 なお、Pytorch 1.2.0 の Tutorials にファインチューニングの実装コードが掲載されています。 https://pytorch.org/tutorialsです。このコードの構造はここで取り上げたものと同じです。利用できる pretrained model は [resnet, alexnet, vgg, squeezenet, densenet, inception] の中から選択できるようになっています。そのオプション・コードは

def initialize_model(model_name, num_classes, feature_extract, use_pretrained=True):
    # Initialize these variables which will be set in this if statement. Each of these
    #   variables is model specific.
    model_ft = None
    input_size = 0

    if model_name == "resnet":
        """ Resnet18
        """
        model_ft = models.resnet18(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, feature_extract)
        num_ftrs = model_ft.fc.in_features
        model_ft.fc = nn.Linear(num_ftrs, num_classes)
        input_size = 224

    elif model_name == "alexnet":
        """ Alexnet
        """
        model_ft = models.alexnet(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, feature_extract)
        num_ftrs = model_ft.classifier[6].in_features
        model_ft.classifier[6] = nn.Linear(num_ftrs,num_classes)
        input_size = 224

    elif model_name == "vgg":
        """ VGG11_bn
        """
        model_ft = models.vgg11_bn(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, feature_extract)
        num_ftrs = model_ft.classifier[6].in_features
        model_ft.classifier[6] = nn.Linear(num_ftrs,num_classes)
        input_size = 224

    elif model_name == "squeezenet":
        """ Squeezenet
        """
        model_ft = models.squeezenet1_0(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, feature_extract)
        model_ft.classifier[1] = nn.Conv2d(512, num_classes, kernel_size=(1,1), stride=(1,1))
        model_ft.num_classes = num_classes
        input_size = 224

    elif model_name == "densenet":
        """ Densenet
        """
        model_ft = models.densenet121(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, feature_extract)
        num_ftrs = model_ft.classifier.in_features
        model_ft.classifier = nn.Linear(num_ftrs, num_classes) 
        input_size = 224

    elif model_name == "inception":
        """ Inception v3 
        Be careful, expects (299,299) sized images and has auxiliary output
        """
        model_ft = models.inception_v3(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, feature_extract)
        # Handle the auxilary net
        num_ftrs = model_ft.AuxLogits.fc.in_features
        model_ft.AuxLogits.fc = nn.Linear(num_ftrs, num_classes)
        # Handle the primary net
        num_ftrs = model_ft.fc.in_features
        model_ft.fc = nn.Linear(num_ftrs,num_classes)
        input_size = 299

    else:
        print("Invalid model name, exiting...")
        exit()
    
    return model_ft, input_size

# Initialize the model for this run
# Models to choose from [resnet, alexnet, vgg, squeezenet, densenet, inception]
model_name = "squeezenet"
model_ft, input_size = initialize_model(model_name, num_classes, feature_extract, use_pretrained=True)

# Print the model we just instantiated
print(model_ft)

となっています。ここで選択されているモデルは squeezenet です。また、feature_extract = False 設定すると、すべてのパラメータの再学習をします。反対に、 feature_extract = True と設定すると、最後の分類器のパラメータのみが再学習されます。

実際に実行できるコードはこの Colab にアップロードしましたので、試みて下さい。 densenet あるいは inception を選択して実行してみて下さい。それぞれの検証精度がどれくらい異なるか見てみるのも面白いです。


Mask R-CNN model を用いた 転移学習の例

また、学習済みのMask R-CNN model を the Penn-Fudan Database for Pedestrian Detection and Segmentationのデータを用いて再学習するプログラムをGoogle Colabで 実行することができます。


ページの先頭に戻る

Pytorch を用いた物体検出のページに行く

トップページに戻る