2019年11月11日月曜日

PyTorchのTransfer Learning Tutorialでcifar10を試す

Transfer Learning Tutorial

Author: Sasank Chilamkurthy <https://chsasank.github.io>_
In this tutorial, you will learn how to train your network using transfer learning. You can read more about the transfer learning at cs231n
notes <https://cs231n.github.io/transfer-learning/>__

PyTorchホームページのTutorialに手を加えて、いきなりTransfer Learningを試してみます。
Deep learningを基礎から勉強する人は、ゼロからモデルを作ったり(from scratch)もっと違う勉強が必要でしょうが、画像の分類のような何かをしたい人は、とにかくプログラムを動かす事が、興味を高める第一歩だと思います。
fast.aiの考え方もそうなんですかね。それからfast.aiを使うことでこのプログラムcodeが如何に楽になるか、私が体感したことを他の人にも知ってほしいと思います。
そのため同じようなプログラムを次回fast.aiを使って書いてみます。

(注意)
PyTorch.orgのホームページも結構更新されており、Tutorialにあるデモプログラムも変更されているので、このプログラムも削除されているかも知れません。


import matplotlib.pyplot as plt
%matplotlib inline



# License: BSD
# Author: Sasank Chilamkurthy

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

import glob

plt.ion()   # interactive mode




# root dirの取得
import os

if os.name == 'posix':
    root_dir='/media/{ユーザー名}/DataScience'
elif os.name == 'nt':
    root_dir = 'F:'
else:
    pass 

dataset_dir=root_dir+'/DataSet/cifar10/cifar10_png'


私は、機械学習用の資料を外付けHDDに入れて、UbuntuとWindows10で使っています。
またデータ類(cifar10、MNIST、Oxford-IIIT-Pet、ImageNetなど)は、まとめてDatasetフォルダーに入れています。
そこで、OSが替わってもデータを保存しているフォルダーを探しにいけるように、上のプログラムを追加しました。もっと良い方法があるのでしょうが取り敢えずこれで・・・。

ドライブ名は{DataScience}でWindowsでは「F:」ドライブとして認識するようにしています。

今回試すサンプルデータは、cifar10の生画像データで、私の場合は下記の様になっています。

(F:DataScience)
      |
      |---Dataset
               |
               |---cifar10
                        |
                        |---cifar10_png
                                 |
                                 |---train
                                 |       |
                                 |       |---airplane
                                 |       |---automobile
                                 |       |---bird    
                                 |       ・・・
                                 |---val
                                 |       |
                                 |       |---airplane
                                 |       |---automobile
                                 |       |---bird    
                                 |       ・・・


Data augmentation and normalization for training


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])
    ]),
}



DataSetとDataLoader

data_dir=dataset_dir

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=16,
                                             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")


Training the model

# best modelを保存するDir
model_dir='./model'

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

    # グラフ化のデータを蓄積する為
    train_loss_list = []
    train_acc_list = []
    val_loss_list = []
    val_acc_list = []

    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)

        # 各エポックで訓練+バリデーションを実行
        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_epoch=epoch
                best_acc = epoch_acc
                best_loss = epoch_loss
                best_model_wts = copy.deepcopy(model.state_dict())
                
            # グラフ化のデータを蓄積する為
            if phase == 'train':
                train_loss_list.append(epoch_loss)   # trainのloss,acc
                train_acc_list.append(epoch_acc)   
            else:
                val_loss_list.append(epoch_loss)   # validationのloss,acc
                val_acc_list.append(epoch_acc)   

        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 --> best model weightsを最終行の'return model'で返す
    model.load_state_dict(best_model_wts)

    #  best model weightsの保存 --> 後で推論に使うため
    model_file = 'epoch%03d-%.3f-%.3f.pth' % (best_epoch, best_loss, best_acc)
    torch.save(model.state_dict(),os.path.join(model_dir, model_file))
   
    # plot learning curve
    plt.figure()
    plt.plot(range(num_epoch), train_loss_list, 'm-', label='train_loss')
    plt.plot(range(num_epoch), val_loss_list, 'g-', label='val_loss')
    plt.legend()
    plt.xlabel('epoch')
    plt.ylabel('loss')
    plt.grid()

    plt.figure()
    plt.plot(range(num_epoch), train_acc_list, 'r-', label='train_acc')
    plt.plot(range(num_epoch), val_acc_list, 'b-', label='val_acc')
    plt.legend()
    plt.xlabel('epoch')
    plt.ylabel('acc')
    plt.grid()    
    
    return model


traning中に最もAccuracyの良かったモデルをpthファイルで、同じ階層の./modelに保存するようにしています。
従って事前に「./model」フォルダーを作成しておく必要があります。

学習済みモデルをFine-tuning (Finetuning the convnet)

# epoch回数
num_epoch=4

# Fine-tuningするクラス数
class_num =10

# modelの選択
model_select='total'
# model_select='partial'

model_ft = models.resnet34(pretrained=True)

# fc層を1000 ---> 目的のクラス数へ置き換え
num_ftrs = model_ft.fc.in_features

model_ft.fc = nn.Linear(num_ftrs, class_num)

# 損失関数の定義
criterion = nn.CrossEntropyLoss()

Fine-tuningの選択
  • model_select='total' --- すべての重みを更新する
  • model_select='partial' --- 一部の重みのみを固定
model_select='total' では、重みを固定せずにResNet34の全レイヤの重みを更新対象としています。

しかし、Fine-tuningする場合は学習済みの重みを壊さないように固定した方がよいケースもある。

model_select='partial' では、最後の層を除くすべてのネットワークをフリーズする必要があります。 backward()で勾配が計算されないようにパラメータを固定するためにはrequire_grad == Falseを設定する必要があります。
  • require_grad = False とすると重みを固定できる。更新対象から除く
こうすることで、最後の (fc)のLinear(512, class_num) のみをパラメータ更新の対象として残りのレイヤの重みはすべて固定することになる。

models.resnet34でモデルを読み込むとき、pretrained=Trueと設定すると、ImageNetで学習済の重みも取り込める。

ResNetの構造はあとで詳しく見てみる予定だけどとりあえず最後の (fc) に注目。
もともと出力がImageNetの1000クラス分類なので Linear(512, 1000) になっている。
この1000を目的とするクラス数に置き換える。
今回のcifar10では10クラスなので、Linear(512, 10) になる。

損失関数の定義

損失関数はnn.CrossEntropyLoss()を使用する。

活性化関数の定義

  • optimizerには更新対象のパラメータのみ渡す必要がある ことに注意!従って、
    • model_select == 'total'では、optimizer_ft = optim.SGD(model_ft.parameters(), lr=0.001, momentum=0.9)
    • model_select == 'partial'では、optimizer_ft = optim.SGD(model_ft.fc.parameters(), lr=0.001, momentum=0.9)
    • model_select='partial'でmodel.parameters() と固定したパラメータ含めて全部渡そうとするとエラーになる。
  • backwardの勾配計算はネットワークの大部分で計算しなくて済むため前に比べて学習は早い(CPUでも動くレベル)。
  • しかし、lossを計算するためforwardは計算しないといけない。

# model別の活性化関数の定義

if model_select == 'total':
    # 活性化関数の定義
    #Observe that all parameters are being optimized
    optimizer_ft = optim.SGD(model_ft.parameters(), lr=0.001, momentum=0.9)

elif model_select == 'partial':
    # すべてのパラメータを固定
    for param in model_ft.parameters():
        param.requires_grad = False

    #  最後のfc層を置き換える
    # これはデフォルトの requires_grad=True のままなのでパラメータ更新対象
    num_ftrs = model_ft.fc.in_features
    model_ft.fc = nn.Linear(num_ftrs, class_num)


    # Optimizerの第1引数には更新対象のfc層のパラメータのみ指定
    optimizer_ft = optim.SGD(model_ft.fc.parameters(), lr=0.001, momentum=0.9)

else :
    pass
        
# 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)




model_ft = model_ft.to(device)




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

本当は、クラス毎のAccuracyや未知の画像のpredictなども書いているのですが、最小限に留めました。

実行の結果は

【model_select='partial' の場合】
Epoch 0/3
----------
train Loss: 1.3328 Acc: 0.5366
val Loss: 0.8412 Acc: 0.7126

Epoch 1/3
----------
train Loss: 1.2242 Acc: 0.5750
val Loss: 0.7916 Acc: 0.7264

Epoch 2/3
----------
train Loss: 1.1904 Acc: 0.5877
val Loss: 0.7457 Acc: 0.7448

Epoch 3/3
----------
train Loss: 1.1874 Acc: 0.5894
val Loss: 0.7143 Acc: 0.7606

Training complete in 10m 60s
Best val Acc: 0.760600



【model_select='total'の場合】
Epoch 0/3
----------
train Loss: 0.8535 Acc: 0.7068
val Loss: 0.2562 Acc: 0.9121

Epoch 1/3
----------
train Loss: 0.6138 Acc: 0.7905
val Loss: 0.2147 Acc: 0.9267

Epoch 2/3
----------
train Loss: 0.5497 Acc: 0.8136
val Loss: 0.1750 Acc: 0.9417

Epoch 3/3
----------
train Loss: 0.5030 Acc: 0.8267
val Loss: 0.1688 Acc: 0.9425

Training complete in 22m 53s
Best val Acc: 0.942500




  • training時間は、やはりすべてのパラメーターを更新する’total’の方が、倍の時間がかかっている。
  • 今回の場合、すべてのパラメーターを更新した方がBest val Acc: 0.942500、一部更新がBest val Acc: 0.760600という結果となった。
  • Fine-tuningするクラス数の指定、 Resnet34、VGG16、Densnetごとに最終層を1000 ---> 目的のクラス数へ置き換えの違いなど、設定に知識と労力を必要とする。




















0 件のコメント:

コメントを投稿