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

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


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

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

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

##今回のキーワード
```python
「月とすっぽん」
```
似て非なるものらしいです。

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

```python
pytorchの「torch.utils.data.random_split」
```
これのおかげで、フォルダに写真を入れる段階ではtrainとtestに分ける必要がないです。


#使用するデータ
google画像から
・すっぽんの画像６７枚
甲羅の上から見たような画像を集めました。
例えば、こんな感じの画像です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455102/7df3fe59-fcc8-0a79-dcc7-61896bde248b.png)
（ぴぃさんのスッポン）http://photozou.jp/photo/show/235691/190390795
＊すっぽんのいい感じの画像はあまりなかったので一部かめの画像で代用しています。


・月の画像７０枚
丸い月が集まっている画像を集めました。手作業で切り抜いて、画面に大きく円が映るようにしました。
例えば、こんな感じの画像です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455102/bf9840b2-c2bb-c832-f9bb-b5a1ebeb66c5.png)


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

#データセット作成
```python
.
├── main.ipynb
├── pics
   ├── tuki
   |     |-tuki1.png
   |     |-tuki2.png
   |        
   └── kame
        |-kame1.png
        |-kame2.png
```
せっかく、ディレクトリに画像が分かれているので、`torchvision.datasets.ImageFolde`を使用してディレクトリごとに自動でラベルづけをします。

##モジュールのインポート
```python
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
```

##前処理
```python
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

##データセット
```python
# ex.
# data_folder = "./pics"
# transform   = transform_dict["train"]

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

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

##trainとtestの分離
```python
# 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のデータサイズを辞書に格納しました。


```python
# ex.
# data_train => Subset(data, [4,5,1,7])
# data_val 　=> Subset(data, [3,8,2,6])
```
返り値は、リストの長さの数だけあります。
それぞれの返り値には、データセットとインデックス番号のリストが含まれています。

（Subsetとは何ですか？）

##データローダ
```python
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では、このようにデータローダを作成しデータを読み込ませます。
又、これを辞書に入れました。

##画像の確認
```python
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455102/2c204cd7-da46-ef0e-1583-e7c6326b54fb.png)



##モデル作成
```python
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は更新されます。
更新されないようにするために、以下のように設定します。





```python
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)`だとわかったので、

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

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

##学習の設定
```python

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


##モデル作成
```python

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:#５回に１回エポックを表示します。
            print('Epoch {}/{}'.format(epoch, num_epochs - 1))
            print('-' * 10)
        
        # それぞれのエポックで、train, valを実行します。
        # 辞書に入れた威力がここで発揮され、trainもvalも１回で書く事ができます。
        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
```

##学習
```python
model_ft, loss, acc = train_model(model, criterion, optim, num_epochs=epoch)
```

##学習を可視化
```python

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

#０個目のグラフ
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を表示

#１個目のグラフ
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()

#０個目と１個目のグラフが重ならないように調整
fig.tight_layout()
```
１１、１２エポックくらいで過学習を起こしていますかね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455102/12d51209-c72a-0570-c802-d83c3ced2bb1.png)


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

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

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

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

```python
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455102/e3bd7012-8750-ac13-15c6-993359859987.png)