konchangakita

KPSを一番楽しんでいたブログ 会社の看板を背負いません 転載はご自由にどうぞ

PyTorchを使ったDeep Learningのお勉強 画像認識編(CIFAR-10)

白黒画像(1チャネル)の数字判定までは実装できるようになったので次のステップです

PyTorch お勉強シリーズ
第1回 PyTorchを使ってDeep Learningのお勉強 基礎編
第2回 PyTorchを使ったDeep Learningのお勉強 PyTorch Lightning編
第3回 PyTorchを使ったDeep Learningのお勉強 TensorBoard編
第4回 PyTorchを使ったDeep Learningのお勉強 画像認識編(MNIST)
第5回 PyTorchを使ったDeep Learningのお勉強 画像認識編(CIFAR-10)(イマココ)

環境

Windows 10
・anaconda:4.8.3
・PyTorch:1.4.0
・PyTorch_Lighgning:0.7.3
GPUNVIDIA Geforce GTX 1070
  Cuda cuda_10.2.89_441.22_win10
  CuDNN v7.6.5.32

使うデータ

巷のDeep Learning界隈御用達(?)CIFAR-10という60000枚の画像のデータセットを使います
CIFAR-10 and CIFAR-100 datasets

こんな感じの 32x32 画像とクラスラベルが付与されています
f:id:konchangakita:20200504104503p:plain

正解ラベル 種別
0 airplane
1 automobile
2 bird
3 cat
4 deer
5 dog
6 frog
7 horse
8 ship
9 truck

データのインポート

CIFAR10のデータは PyTorch のデータセットに用意されているので簡単にインポートできます

import torch, torchvision
from torchvision import transforms

transform = transforms.Compose([
    transforms.ToTensor()
])

# CIFAR10のダウンロード
train_val = torchvision.datasets.CIFAR10(root='data', train=True, download=True, transform=transform)
test = torchvision.datasets.CIFAR10(root='data', train=False, download=True, transform=transform)

1枚だけ取り出して画像とクラスを確認してみます

import numpy as np
import matplotlib.pyplot as plt

print(train_val[0][0].shape)
img = np.transpose(train_val[0][0], (1, 2, 0))
plt.imshow(img)

---
torch.Size([3, 32, 32])

カエルちゃんです
f:id:konchangakita:20200503214026p:plain:w100

train_val[0][1]

---
6

6なので、Frog 示していますね、OK


学習用、検証用にデータをランダムに分割してデータをセット

# train : val = 8 : 2
n_train = int(len(train_val) * 0.8)
n_val = len(train_val) - n_train

# train と val を分割
train, val = torch.utils.data.random_split(train_val, [n_train, n_val])

len(train), len(val), len(test)
Output

(40000, 10000, 10000)


学習モデル

まずは前回(MNIST)のものをちょっと改造して、試します

毎回固定の、データの読み込みと学習ステップのクラスはそのまま流用できます

import pytorch_lightning as pl

boardtag = "cifar10-"

# 学習データ用クラス
class TrainNet(pl.LightningModule):
    
    @pl.data_loader
    def train_dataloader(self):
        return torch.utils.data.DataLoader(train, self.batch_size, shuffle=True)
    
    def training_step(self, batch, batch_nb):
        x, t = batch
        y = self.forward(x)
        loss = self.lossfun(y, t)
        y_label = torch.argmax(y, dim=1)
        acc = torch.sum(t == y_label) * 1.0 / len(t)
        tensorboard_logs = {boardtag+'train/train_loss': loss, boardtag+'train/train_acc': acc} # tensorboard
        results = {'loss': loss, 'log': tensorboard_logs}
        #results = {'loss': loss}
        return results

    
# 検証データ用クラス
class ValidationNet(pl.LightningModule):

    @pl.data_loader
    def val_dataloader(self):
        return torch.utils.data.DataLoader(val, self.batch_size)

    def validation_step(self, batch, batch_nb):
        x, t = batch
        y = self.forward(x)
        loss = self.lossfun(y, t)
        y_label = torch.argmax(y, dim=1)
        acc = torch.sum(t == y_label) * 1.0 / len(t)
        results = {'val_loss': loss, 'val_acc': acc}
        return results

    def validation_end(self, outputs):
        avg_loss = torch.stack([x['val_loss'] for x in outputs]).mean()
        avg_acc = torch.stack([x['val_acc'] for x in outputs]).mean()
        tensorboard_logs = {boardtag+'val/avg_loss': avg_loss, boardtag+'val/avg_acc': avg_acc}
        results = {'val_loss': avg_loss, 'val_acc': avg_acc, 'log': tensorboard_logs}        
        #results = {'val_loss': avg_loss, 'val_acc': avg_acc}
        return results

    
# テストデータ用クラス
class TestNet(pl.LightningModule):

    @pl.data_loader
    def test_dataloader(self):
        return torch.utils.data.DataLoader(test, self.batch_size)

    def test_step(self, batch, batch_nb):
        x, t = batch
        y = self.forward(x)
        loss = self.lossfun(y, t)
        y_label = torch.argmax(y, dim=1)
        acc = torch.sum(t == y_label) * 1.0 / len(t)
        results = {'test_loss': loss, 'test_acc': acc}
        return results

    def test_end(self, outputs):
        avg_loss = torch.stack([x['test_loss'] for x in outputs]).mean()
        avg_acc = torch.stack([x['test_acc'] for x in outputs]).mean()
        results = {'test_loss': avg_loss, 'test_acc': avg_acc}
        return results


学習モデルの層定義を CIFAR-10用(カラー画像)に畳み込み処理に改良を加えます
畳み込み層とバッチノーマライゼーションを複数層重ねてチャネルを増やしながら、最後全結合層で合体させます
VGGの縮小版のような感じです)

# 学習データ、検証データ、テストデータクラスの継承クラス
class Net(TrainNet, ValidationNet, TestNet):
    def __init__(self, batch_size=256):
        super(Net, self).__init__()
        self.batch_size = batch_size

        self.conv1 = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, padding=1, stride=1)
        self.bn1 = nn.BatchNorm2d(64)
        self.conv2 = nn.Conv2d(64, 128, 3, padding=1) 
        self.bn2 = nn.BatchNorm2d(128)
        self.conv3 = nn.Conv2d(128, 256, 3, padding=1)
        self.bn3 = nn.BatchNorm2d(256)
        self.conv4 = nn.Conv2d(256, 512, 3, padding=1)
        self.bn4 = nn.BatchNorm2d(512)
        self.L1 = nn.Linear(2048, 10) # 10クラス分類
                
    def forward(self, x):
        # 3ch > 64ch, shape 32 x 32 > 16 x 16
        x = self.conv1(x) # [64,32,32]
        x = self.bn1(x)
        x = F.relu(x)
        x = F.max_pool2d(x, 2, 2) # [64,16,16]
        
        # 64ch > 128ch, shape 16 x 16 > 8 x 8
        x = self.conv2(x) # [128,16,16]
        x = self.bn2(x)
        x = F.relu(x)
        x = F.max_pool2d(x, 2, 2) # [128,8,8]
        
        # 128ch > 256ch, shape 8 x 8 > 4 x 4
        x = self.conv3(x) # [256,8,8]
        x = self.bn3(x)
        x = F.relu(x)
        x = F.max_pool2d(x, 2, 2) # [256,4,4]   

        # 256ch > 512ch, shape 4 x 4 > 2 x 2
        x = self.conv4(x) # [512,4,4]
        x = self.bn4(x)
        x = F.relu(x)
        x = F.max_pool2d(x, 2, 2) # [512,2,2]   
        
        # 全結合層
        x = x.view( -1, 2048) # [256,2048]
        x = self.L1(x)
        
        return x
    
    def lossfun(self, y, t):
        return F.cross_entropy(y, t)
    
    def configure_optimizers(self):
        return torch.optim.SGD(self.parameters(), lr=0.01)
# 学習モデルをインスタンス化
net = Net()
net

Output


Net(
(conv1): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv4): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(bn4): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(L1): Linear(in_features=2048, out_features=10, bias=True)
)

エポック50 で学習開始

trainer = Trainer(max_epochs=50, gpus=1)
trainer.fit(net)

f:id:konchangakita:20200503223806p:plain

学習は進んでいるようですが、検証データに対しての結果はイマイチですね
f:id:konchangakita:20200503224556p:plain

テストデータで学習結果を確認

trainer.test()
~~~
--------------------------------------------------------------------------------
TEST RESULTS
{'test_acc': 0.7618164420127869,
 'test_loss': 0.8731542825698853}
--------------------------------------------------------------------------------

Lossはまだまだ大きいですが、テストデータで76%の正解率とのことでまぁまぁな感じです

精度向上へ向けての戦略

畳み込み層の工夫(層の数、チャネル数)や、今回導入もしているバッチノーマライゼーションがそれにあたります

他にも、コスト関数(損失関数)や重み更新の最適化アルゴリズムを変えてみるというのもありますので、やってみましょう

最適化アルゴリズム

パラメータ最適化アルゴリズムには以下のようなアルゴリズムがあります
 ・SGD
 ・Momentum SGD
 ・RMSprop
 ・Adam

いろいろ勉強したところによると、だいたい Adam を使っておけばよい、と言われたので Adamを使ってみます

    def configure_optimizers(self):
        #return torch.optim.SGD(self.parameters(), lr=0.01)
        return torch.optim.Adam(self.parameters(), lr=1e-3, weight_decay=1e-3)


オレンジが Adam を使ってモノです
f:id:konchangakita:20200504001947p:plain

早い段階で学習が進んだのは分かりますが、検証/テスト結果もあんまり変わりませんねー

--------------------------------------------------------------------------------
TEST RESULTS
{'test_acc': 0.7494140863418579, 
'test_loss': 1.027146816253662}
--------------------------------------------------------------------------------


次の精度向上戦略です

過学習の防止

過学習とは、いろいろ工夫した結果、学習データでは良い結果だが、検証やテストデータや本番ではイマイチな結果を招いてしまうあれです

対策としては、
 ・正則化(Regularization)
 ・ドロップアウト(DropOut)
 ・バッチノーマライゼーション(Batch Normalization)

今回はすでにバッチノーマライゼーションは取り込んでいるので、ドロップアウトも追加してみます

青が Adam + Dropout になります
f:id:konchangakita:20200504014211p:plain
少しだけ向上したように見えますが、まだまだですね

                                                                                                                                                              • -

TEST RESULTS
{'test_acc': 0.7850586175918579,
'test_loss': 0.9156473278999329}

                                                                                                                                                              • -

逆説的には、この VGG の縮小版を模した畳み込み処理とバッチノーマライゼーションの組み合わせは、優秀であるとも言える?なんでも継ぎ足せばよいってもんじゃないですね


推論してみるぞ

まずは推論に使う画像を読み込んで、内容を確認しておきます

# 保存したパラメータの読み込み
net = Net()
net.load_state_dict(torch.load('pytorch_cifar10_resnet.pt'))

# テストデータから推論用画像の読み込み(正解画像)
import numpy as np
import random
import matplotlib.pyplot as plt
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')  

sample = test[random.randint(0, 10000)]
#sample = train[random.randint(0, 10000)]
sample_label = sample[1]

img = np.transpose(sample[0], (1, 2, 0))
print("正解ラベル:", classes[sample[1]])
plt.imshow(img)

カエルくんの画像です
f:id:konchangakita:20200510124634p:plain


推論して、カエルくんで出れば推論成功となります

# 推論モード
net.eval()
net.freeze()

# 画像をモデルと形式を合わせる
x = sample[0].unsqueeze(0)

# 推論開始
y_predict = net(x)
print(y_predict)
print("推論結果: ", classes[y_predict.argmax()])

推論結果が「frog」(カエルくん)と出たので、正解ですね
何回か試してみましたが、結果通り80%くらいは正解の模様です

学習済みモデルの活用

今回はいろいろなとこからのコード参考にしながら、VGGの縮小版のようなことをしましたが、自分で一からモデルを構築し、検証を繰り返すのなかなか骨の折れる作業です(何が正解か分からない)
そこで既にエライ人たちが考えて公開されているモデルを活用することができます
ファインチューニングとか転移学習とか言われるやつです

torchvision.models では、画像認識コンペティション ImageNet Large Scale Visual Recognition Competition (ILSVRC) で使用された有名なモデルを 呼び出すことができます
 ・AlexNet
 ・VGG
 ・ResNet
 ・GoogLeNet
などなど

利用する際の注意点として、画像入力のサイズの変更や正規化をしてやる必要がありますので、本家サイトを参考にしましょう
pytorch.org


あと大抵複雑なモデルをしているので、GPUが無いとキツイですし時間もかかりますので要検討事項ですね