LoginSignup
50
43

More than 3 years have passed since last update.

Pytorchで「月とすっぽん」の画像認識をしてみた(kerasのfrom_from_directryにあたるtorchvision.datasets.ImageFolder使用)

Last updated at Posted at 2020-04-21

月とすっぽん

月もすっぽんも同じように丸いが、比較にならないほどその違いは大きいこと。二つのものがひどく違っていることのたとえ。
https://dictionary.goo.ne.jp/word/%E6%9C%88%E3%81%A8%E9%BC%88/

大きな違いがあるらしいので、ディープラーニングを使い画像認識ができるか試してみましょう!

適宜、(少しですが)Pytorchの説明もします。(間違っていたらぜひ訂正してください。よろしくお願いします。)

コードはこちらです。
https://github.com/kyasby/Tuki-Suppon.git

今回のキーワード

月とすっぽん

似て非なるものらしいです。

pytorchのtorchvision.datasets.ImageFolder

kerasのfrom_from_directryにあたるpytorchのtorchvision.datasets.ImageFolder使用した記事があまりなかったので作りました。
フォルダーに画像を入れると自動でラベル付をしてくれます。便利です。

pytorchのtorch.utils.data.random_split

これのおかげで、フォルダに写真を入れる段階ではtrainとtestに分ける必要がないです。

使用するデータ

google画像から
・すっぽんの画像67枚
甲羅の上から見たような画像を集めました。
例えば、こんな感じの画像です。
image.png
(ぴぃさんのスッポン)http://photozou.jp/photo/show/235691/190390795
*すっぽんのいい感じの画像はあまりなかったので一部かめの画像で代用しています。

・月の画像70枚
丸い月が集まっている画像を集めました。手作業で切り抜いて、画面に大きく円が映るようにしました。
例えば、こんな感じの画像です。
image.png

*画像はスクレイピングしてきました。
*スクレイピングについては、ここでは触れません。

データセット作成

.
├── main.ipynb
├── pics
   ├── tuki
   |     |-tuki1.png
   |     |-tuki2.png
   |        
   └── kame
        |-kame1.png
        |-kame2.png

せっかく、ディレクトリに画像が分かれているので、torchvision.datasets.ImageFoldeを使用してディレクトリごとに自動でラベルづけをします。

モジュールのインポート

import matplotlib.pyplot as plt
import numpy as np
import copy
import time
import os
from tqdm import tqdm

import torchvision.transforms as transforms
import torchvision.models as models
import torchvision

import torch.nn as nn
import torch

前処理

transform_dict = {
        'train': transforms.Compose(
            [transforms.Resize((256,256)),
             transforms.RandomHorizontalFlip(),
             transforms.ToTensor(),
             transforms.Normalize(mean=[0.485, 0.456, 0.406],
                                  std=[0.229, 0.224, 0.225]),
             ]),
        'test': transforms.Compose(
            [transforms.Resize((256,256)),
             transforms.ToTensor(),
             transforms.Normalize(mean=[0.485, 0.456, 0.406],
                                  std=[0.229, 0.224, 0.225]),
             ])}

trainとtest用の前処理辞書を作ります。
transforms.Composeを使うことで、前処理のシーケンスを作ることができます。引数に入れていった順番で処理されるみたいです。

今回は、
transforms.Resize(256, 256)
→画像を256x256にリサイズします。

transforms.RandomHorizontalFlip()
→左右逆転した画像を作成します。

transforms.ToTensor()
→PILまたはnumpy.ndarray((height x width x channel)で(0~255))
 を
 Tensor((channel x height x width)で(0.0~1.0))に変換してくれます。

PILやnumpyでは画像は(height x width x channel)の順番ですが、Pytorchでは(channel x height x width)の点には注意が必要です。この順番の方が扱いやすいという理由のようです。

transforms.Normalize(mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225])
→GRBのそれぞれを指定した平均値と標準偏差で正規化します。

ドキュメント
https://pytorch.org/docs/stable/torchvision/transforms.html

データセット

# ex.
# data_folder = "./pics"
# transform   = transform_dict["train"]

data = torchvision.datasets.ImageFolder(root=data_folder, transform=transform_dict[phase])

上記のディレクトリからデータセットを作成します。

trainとtestの分離

# ex.
# train_ratio = 0.8

train_size = int(train_ratio * len(data))
# int()で整数に。
val_size  = len(data) - train_size      
data_size  = {"train":train_size, "val":val_size}
#          =>{"train": 112,       "val": 28}
data_train, data_val = torch.utils.data.random_split(data, [train_size, val_size])

torch.utils.data.random_split(dataset, lengths)
はデータセットをランダムに被りなく、分けてくれます。
datasetにはもちろんデータセットを、
lengthsにはデータセットの個数をリストで渡せます。

また、trainとvalidのデータサイズを辞書に格納しました。

# ex.
# data_train => Subset(data, [4,5,1,7])
# data_val  => Subset(data, [3,8,2,6])

返り値は、リストの長さの数だけあります。
それぞれの返り値には、データセットとインデックス番号のリストが含まれています。

(Subsetとは何ですか?)

データローダ

train_loader = torch.utils.data.DataLoader(data_train, batch_size=batch_size, shuffle=True)
val_loader   = torch.utils.data.DataLoader(data_val,   batch_size=batch_size, shuffle=False)
dataloaders  = {"train":train_loader, "val":val_loader}

データローダを作ります。
Pytorchでは、このようにデータローダを作成しデータを読み込ませます。
又、これを辞書に入れました。

画像の確認

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

# 訓練データをランダムに取得
dataiter = iter(dataloaders["train"])
images, labels = dataiter.next()

# 画像の表示
imshow(torchvision.utils.make_grid(images))
# ラベルの表示
print(' '.join('%5s' % labels[labels[j]] for j in range(8)))

上記のコードでこんな感じに表示してくれるみたいです。
こちらから頂きました。 https://qiita.com/kuto/items/0ff3ccb4e089d213871d
スクリーンショット 2020-04-20 18.37.17.png

モデル作成

model = models.resnet18(pretrained=True)
for param in model.parameters():
    print(param)
# => Parameter containing:
#tensor([[[[-1.0419e-02, -6.1356e-03, -1.8098e-03,  ...,  5.6615e-02,
#            1.7083e-02, -1.2694e-02],
#          ...
#           -7.1195e-02, -6.6788e-02]]]], requires_grad=True)

モデルはResNet18を使用します。引数にpretrained=Trueを入れることで、学習済みのモデルを使う事ができます。
既存のパラメータの学習はせずに、転移学習をします。
requires_grad=Trueと表示されるweightは更新されます。
更新されないようにするために、以下のように設定します。

model
# => ResNet(
#   (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
#   (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
#   (relu): ReLU(inplace=True)
#   (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
#   (layer1): Sequential(
#     (0): BasicBlock(
#       (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
#       (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
#       (relu): ReLU(inplace=True)
#       (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
#       (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
#     )
#   ...
#   (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
#   (fc): Linear(in_features=512, out_features=1000, bias=True)
# )

最終層が(fc)だとわかったので、

for p in model.parameters():
    p.requires_grad=False
model.fc = nn.Linear(512, 2)

model.parameters()で全てのパラメータを取り出し、requires_grad=Falseとした上で、最終層を上書きします。

学習の設定


model = model.cuda() #GPUなしの場合はこの行はいらない。
lr = 1e-4
epoch = 40
optim = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=1e-4)
criterion = nn.CrossEntropyLoss().cuda() #GPUなしの場合は.cuda()はいらない。

GPUを使う場合は、modelをGPUに送ってやる必要があります。

モデルはほぼチュートリアルのままです。
https://pytorch.org/tutorials/beginner/transfer_learning_tutorial.html

モデル作成


def train_model(model, criterion, optimizer, scheduler=None, num_epochs=25):
    #:bool値を返す。
    use_gpu = torch.cuda.is_available()
    #始まりの時間
    since = time.time()

    best_model_wts = copy.deepcopy(model.state_dict())
    best_acc = 0.0

    #途中経過保存用に、リストを持った辞書を作ります。
    loss_dict ={"train" : [],  "val" : []}
    acc_dict = {"train" : [],  "val" : []}  

    for epoch in tqdm(range(num_epochs)):
        if (epoch+1)%5 == 0:#5回に1回エポックを表示します。
            print('Epoch {}/{}'.format(epoch, num_epochs - 1))
            print('-' * 10)

        # それぞれのエポックで、train, valを実行します。
        # 辞書に入れた威力がここで発揮され、trainもvalも1回で書く事ができます。
        for phase in ['train', 'val']:
            if phase == 'train':
                model.train()   # 学習モード。dropoutなどを行う。
            else:
                model.val()  # 推論モード。dropoutなどを行わない。

            running_loss = 0.0
            running_corrects = 0

            for data in dataloaders[phase]:
                inputs, labels = data #ImageFolderで作成したデータは、
                                      #データをラベルを持ってくれます。

                #GPUを使わない場合不要
                if use_gpu:
                    inputs = inputs.cuda()
                    labels = labels.cuda()



                #~~~~~~~~~~~~~~forward~~~~~~~~~~~~~~~
                outputs = model(inputs)

                _, preds = torch.max(outputs.data, 1)
               #torch.maxは実際の値とインデクスを返します。
               #torch.max((0.8, 0.1),1)=> (0.8, 0)
               #引数の1は行方向、列方向、どちらの最大値を返すか、です。
                loss = criterion(outputs, labels)

                if phase == 'train':
                    optimizer.zero_grad()
                    loss.backward()
                    optimizer.step()

                # statistics #GPUなしの場合item()不要
                running_loss += loss.item() * inputs.size(0) 
                running_corrects += torch.sum(preds == labels)
                # (preds == labels)は[True, True, False]などをかえしますが、
                # pythonのTrue, Falseはそれぞれ1, 0に対応しているので、
                # sumで合計する事ができます。

               #リストに途中経過を格納
               loss_dict[phase].append(epoch_loss)
               acc_dict[phase].append(epoch_acc)

            # サンプル数で割って平均を求めます。
            # 辞書にサンプル数を入れたのが生きてきます。
            epoch_loss = running_loss / data_size[phase]
            #GPUなしの場合item()不要
            epoch_acc = running_corrects.item() / data_size[phase]

           #tensot().item()を使う事で、テンソルから値を取り出す事ができます。
           #print(tensorA)       => tensor(112, device='cuda:0')
           #print(tensorA.itme)) => 112

            #formatを使いますが、.nfとすると、小数点以下をn桁まで出力できます。
            #C言語と一緒ですね。
            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())
            # deepcopyをしないと、model.state_dict()の中身の変更に伴い、
            # コピーした(はずの)データも変わってしまいます。
            # copyとdeepcopyの違いはこの記事がわかりやすいです。 
            # https://www.headboost.jp/python-copy-deepcopy/

    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))

    # 最良のウェイトを読み込んで、返す。
    model.load_state_dict(best_model_wts)
    return model, loss_dict, acc_dict

学習

model_ft, loss, acc = train_model(model, criterion, optim, num_epochs=epoch)

学習を可視化


#loss, accを取り出します。
loss_train = loss["train"]
loss_val   = loss["val"]

acc_train = acc["train"]
acc_val   = acc["val"]


#このように書く事で、nrows x cols のグラフを作成する事ができます。
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10,5))

#0個目のグラフ
axes[0].plot(range(epoch), loss_train, label = "train")
axes[0].plot(range(epoch), loss_val,    label =  "val")
axes[0].set_title("Loss")
axes[0].legend()#各グラフのlabelを表示

#1個目のグラフ
axes[1].plot(range(epoch), acc_train, label = "train")
axes[1].plot(range(epoch), acc_val,    label =  "val")
axes[1].set_title("Acc")
axes[1].legend()

#0個目と1個目のグラフが重ならないように調整
fig.tight_layout()

11、12エポックくらいで過学習を起こしていますかね。
image.png

おまけ

GoogleColabolatory

手軽にGPUを使う方法としてコラボがあります。
https://colab.research.google.com/notebooks/welcome.ipynb?hl=ja
コラボで画像を使うときには、zipにしてアップロードすると便利です。
(1枚1枚アップロードするのは大変です。)(ドライブと連携する方法でもOK)
その際、解凍は以下のようにできます。

#/content/pics.zipはそれぞれ変えてください。
!unzip /content/pics.zip -d /content/data > /dev/null 2>&1 &

また、ファイルを右クリックすると出てくる「パスをコピー」も便利です。

matplotlib.plt

今回は、1行2列のグラフを出力しましたが、例えば2行2列だと例えば以下のようにグラフを作成できます。
ある、グラフにプロットを上書きして同時に2つプロットすることもできます。
グラフにつき、2つずつプロットしてみました。

loss_train = loss["train"]
loss_val    = loss["val"]

acc_train = acc["train"]
acc_val    = acc["val"]

fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10,5))
axes[0,0].plot(range(epoch), loss_train, label = "train")
axes[0,0].plot(range(epoch), loss_val,    label =  "val")
axes[0,0].set_title("Loss")
axes[0,0].legend()

axes[0,1].plot(range(epoch), acc_train, c="red",  label = "train")
axes[0,1].plot(range(epoch), acc_val,    c="pink", label =  "val")
axes[0,1].set_title("Train Loss")
axes[0,1].legend()

x = np.random.rand(100)
xx = np.random.rand(200)
axes[1,0].hist(xx, bins=25, label="xx")
axes[1,0].hist(x, bins=50,   label="x")
axes[1,0].set_title("histgram")

y = np.random.randn(100)
z = np.random.randn(100)
axes[1,1].scatter(y, z, alpha=0.8, label="y,z") 
axes[1,1].scatter(z, y, alpha=0.8, label="z,y")
axes[1,1].set_title("Scatter")
axes[1,1].legend()

fig.tight_layout()

image.png

50
43
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
43