PyTorchでMNISTを扱ってみた (機械学習)

初心者がPyTorchを使ってみたく，PyTorchを用いてMNISTを扱ってみました！
その際のメモ書きです．

# 目標
今回は，PyTorchを用いて機械学習モデルの作成を目指す．

# 準備
### ライブラリのインポート
PyTorchを扱う際に必要な以下のライブラリをインポートする．
```Python
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
```

インポートしたパッケージの詳細は以下のようになっている．
| パッケージ |     名前      |
| --- | ----------- |
| torch    | PyTrochのリストを扱うもの
| torch.nn | ネットワークの構築
| torch.nn.funcctional |様々な関数の使用
| torch.optim |最適化アルゴリズムの使用
| torchvision|画像処理に関係する処理の使用
| torchvision.transform|画像変換機能の使用

### 乱数シードの固定
今回は，乱数シードを一度に設定する関数を作成する．
```Python
def setup_all_seed(seed=0):
    # numpyに関係する乱数シードの設定
    np.random.seed(seed)
    
    # pytorch
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.backends.cudnn.deterministic = True
```

# MNIST dataset
まずはじめに，MNIST datasetがどのような形をしているのか理解をする．
![slide3-1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/81bf6827-7421-1dac-dc40-59bd6ea71c6f.jpeg)

```Python
#訓練データ
train_dataset = torchvision.datasets.MNIST(root='./data',
                                           train=True,
                                           transform=transforms.ToTensor(),
                                           download = True)
#検証データ
test_dataset = torchvision.datasets.MNIST(root='./data',
                                           train=False,
                                           transform=transforms.ToTensor(),
                                           download = True)
```
ここで，作成した訓練データ`train_data`の中身を見てみる．
```Python
print(len(train_dataset))
train_dataset[0]
```
```
60000
(tensor([[[0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
           0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
           0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
           0.0000, 0.0000, 0.0000, 0.0000],
                             　　　 <中略>
          [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
           0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
           0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
           0.0000, 0.0000, 0.0000, 0.0000]]]), 5)
```
　以上より，60000枚ものデータが，`tensor([画像データ(28*28のサイズ)], ラベル)`の形で格納されていることが確認できる．
　また，以下のコードで，画像データと正解ラベルを分けることができる．
```Python
fig, label = train_dataset[0]
print("fig : {}, label : {}".format(fig,label))
print("fig.size() : {}".format(fig.size()))
plt.imshow(fig.view(-1,28), cmap='gray')
```
```
fig : tensor([[[0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
          0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
          0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
          0.0000, 0.0000, 0.0000, 0.0000],
                             　　　 <中略>
         [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
          0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
          0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
          0.0000, 0.0000, 0.0000, 0.0000]]])
label : 5
fig.size() : torch.Size([1, 28, 28])
```
![fig3-1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/cb7e5f54-a2fb-9b62-9b0d-1de5f9274bd4.png)

# 学習
　今回は，ミニバッチ学習にて学習を行う機械学習モデルの作成を目指す．
![DeepLearning.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/ee2ad3be-3eec-bf62-ba2e-23e7bd4595f2.jpeg)

```Python
batch_size = 256

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
                                           batch_size=batch_size,
                                           shuffle=True)

test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
                                           batch_size=batch_size,
                                           shuffle=True)
```

# ネットワークモデルの定義
![DeepLearning.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/9bbf8b9f-6cd7-11cb-ae11-96a28c3b7511.jpeg)

`__init__()`部分では，モデルの形状を定義する．
`forward()`部分では，モデルの順伝播の計算の流れを定義する．
この時，出力のy の部分で，活性化関数を通していない理由は次の節で説明をする．
```Python
class Net(nn.Module):
    def __init__(self, input_size, hidden1_size, hidden2_size, output_size):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden1_size)
        self.fc2 = nn.Linear(hidden1_size, hidden2_size)
        self.fc3 = nn.Linear(hidden2_size, output_size)

    def forward(self, x): # x : 入力
        z1 = F.relu(self.fc1(x))
        z2 = F.relu(self.fc2(z1))
        y = self.fc3(z2)
        return y
```
モデルのインスタンス化
```Python
input_size = 28*28
hidden1_size = 1024
hidden2_size = 512
output_size = 10

device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = Net(input_size, hidden1_size, hidden2_size, output_size).to(device)
print(model)
```
実行結果
```
Net(
  (fc1): Linear(in_features=784, out_features=1024, bias=True)
  (fc2): Linear(in_features=1024, out_features=512, bias=True)
  (fc3): Linear(in_features=512, out_features=10, bias=True)
)
```

# 学習の準備
### 損失関数と最適化法の指定
以下のように損失関数と最適化法の指定を行う．
今回は，クラス分類問題であるので，交差エントロピー誤差関数を用いた．
また，最適化法は，確率的勾配降下法を用いて学習を行うこととする．
ここで，前節で定義したネットワークモデルの`forward()`関数内の出力する部分を見ると，何も関数を通していないことがわかる．通常であれば，クラス分類問題では，それぞれの分類先の確率を示す`sotfmax()`関数が用いられるのが一般的である．しかし，今回定義したネットワークモデルの順伝播の出力層部分の活性化関数に`softmax()`関数が使われていない理由は，<strong>PyTorchの`nn.CrossEntropyLoss()`関数に`softmax()`関数の活性化関数部分が含まれている為，順伝播部分に活性化関数の記載が必要無い</strong> ということである．

```Python
# 損失関数　criterion：基準
# CrossEntropyLoss：交差エントロピー誤差関数
criterion = nn.CrossEntropyLoss()

# 最適化法の指定　optimizer：最適化
# SGD：確率的勾配降下法
optimizer = optim.SGD(model.parameters(), lr=0.01)
```
### 1epochの訓練を行う関数の定義
```Python
def train_model(model, train_loader, criterion, optimizer, device='cpu'):

    train_loss = 0.0
    num_train = 0

    # 学習モデルに変換
    model.train()

    for i, (images, labels) in enumerate(train_loader):
        # batch数をカウント
        num_train += len(labels)

        images, labels = images.view(-1, 28*28).to(device), labels.to(device)

        # 勾配を初期化
        optimizer.zero_grad()

        # 推論(順伝播)
        outputs = model(images)

        # 損失の算出
        loss = criterion(outputs, labels)

        # 誤差逆伝播
        loss.backward()

        # パラメータの更新
        optimizer.step()

        # lossを加算
        train_loss += loss.item()
    
    # lossの平均値を取る
    train_loss = train_loss / num_train

    return train_loss
```

### 検証データによるモデル評価を行う関数の定義
```Python
def test_model(model, test_loader, criterion, optimizer, device='cpu'):

    test_loss = 0.0
    num_test = 0

    # modelを評価モードに変更
    model.eval()

    with torch.no_grad(): # 勾配計算の無効化
        for i, (images, labels) in enumerate(test_loader):
            num_test += len(labels)
            images, labels = images.view(-1, 28*28).to(device), labels.to(device)
            outputs = model(images)
            loss = criterion(outputs, labels)
            test_loss += loss.item()
        
        # lossの平均値を取る
        test_loss = test_loss / num_test
    return test_loss
```
### モデル学習を行う関数の定義
```Python
def lerning(model, train_loader, test_loader, criterion, opimizer, num_epochs, device='cpu'):

    train_loss_list = []
    test_loss_list = []

    # epoch数分繰り返す
    for epoch in range(1, num_epochs+1, 1):

        train_loss = train_model(model, train_loader, criterion, optimizer, device=device)
        test_loss = test_model(model, test_loader, criterion, optimizer, device=device)
        
        print("epoch : {}, train_loss : {:.5f}, test_loss : {:.5f}" .format(epoch, train_loss, test_loss))

        train_loss_list.append(train_loss)
        test_loss_list.append(test_loss)
    
    return train_loss_list, test_loss_list
```
# 学習
```Python
num_epochs = 10
train_loss_list, test_loss_list = lerning(model, train_loader, test_loader, criterion, optimizer, num_epochs, device=device)
```
```
epoch : 1, train_loss : 0.00872, test_loss : 0.00844
epoch : 2, train_loss : 0.00728, test_loss : 0.00594
epoch : 3, train_loss : 0.00450, test_loss : 0.00343
epoch : 4, train_loss : 0.00291, test_loss : 0.00249
epoch : 5, train_loss : 0.00227, test_loss : 0.00207
epoch : 6, train_loss : 0.00194, test_loss : 0.00182
epoch : 7, train_loss : 0.00174, test_loss : 0.00165
epoch : 8, train_loss : 0.00161, test_loss : 0.00150
epoch : 9, train_loss : 0.00151, test_loss : 0.00143
epoch : 10, train_loss : 0.00144, test_loss : 0.00140
```

# 学習推移のグラフ化
```Python
plt.plot(range(len(train_loss_list)), train_loss_list, c='b', label='train loss')
plt.plot(range(len(test_loss_list)), test_loss_list, c='r', label='test loss')
plt.xlabel("epoch")
plt.ylabel("loss")
plt.legend()
plt.grid()
plt.show()
```
![pytorch_MNIST_result](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/fe2f0a98-aee5-1787-afde-21c082c961b8.png)

# 学習結果の確認
学習結果を確認するために，test_datasetの最初の10個のデータを順伝播させた結果をみてみることにする．
モデルのフォワードプロパゲーションの実行は，modelオブジェクトを関数のように呼び出すだけで順伝播を行うことができる．
正確には，`__call__`メソッドの呼び出しを行っている．引数には，入力データをテンソル型で渡せば，返り値として，出力結果がテンソル型で戻ってくる．
今回のモデルは，分類問題であり，0から9の手書き数字の推論を行うモデルとして設計し，出力層は10個のレイヤから成立している．
出力層の10個のレイヤには，0である確率から9である確率までの10個の数字それぞれに対して確率で出力される．
正確には，順伝播部分に確率に変換する`softmax()`関数を活性化関数として入れていない為，順伝播の出力自体は，確率ではないが，大小関係に変化はないので，そのまま値が大きいものが，確率として高いことになり，一番確率が高いと判断した数字を推論の結果としている．
モデルの順伝播の返り値として，出力層の結果が返されているが，出力層の10個のレイヤが，テンソル型の配列で返却される．今回の分類は，0から9の数字の分類なので，都合よく，返される配列のインデックスが分類する数字とみなせばよい為，順伝播の返却値のテンソル型配列の中の最大値のインデックスを返却する`torch.argmax()`を用いて分類結果としている．
```Python
plt.figure(figsize=(20, 10))
for i in range(10):
    image, label = test_dataset[i]
    image = image.view(-1, 28*28).to(device)

    # 推論
    prediction_label = torch.argmax(model(image))

    ax = plt.subplot(1, 10, i+1)

    plt.imshow(image.detach().to('cpu').numpy().reshape(28, 28), cmap='gray')
    ax.axis('off')
    ax.set_title('label : {}\n Prediction : {}'.format(label, prediction_label), fontsize=15)
plt.show()
``` 
![pytorch_MNIST_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/a1766aa0-7c96-9ec0-dd84-dfb21a63ee75.png)

結果として，10個のデータの内の正解数は9個で概ね分類が上手くいっていることがわかる．
9番目のデータは人間でも6に見間違える人がいそう...笑