PyTorchを１から理解しながらMNISTの手書き数字認識モデルを作っていく！

# ０ はじめに
目次
１ 目標
２ モデル作成の前に
３ 必要ライブラリのインポート
４ 学習データの準備
５ ネットワークモデルの定義
６ 損失関数と最適化関数の定義
７ 学習１エポック分を関数化して定義
８ 検証データによるモデル評価を行う関数の定義
９ モデル学習を行う関数の定義
１０ 学習
１１ 学習推移の確認
１２ 学習済みモデルにデータを投入してみる
１３ おわりに


# １ 目標
今回は，PyTorchの基礎から学習していき，多層パーセプトロンを構築し，MNISTデータセットの手書き数字認識モデルの学習を目指す．

# ２ モデル作成の前に
モデル作成のプログラミングの前に，今回，作成する多層パーセプトロンや，PyTorchの基礎知識の確認から学習を進めることとする．

## 2.1 PyTorchの基礎
PyTorchは，Pythonのオープンソース機械学習ライブラリであり，研究分野で広く使われているライブラリである．PyTorchの大きな特徴として，扱うデータは，`Tensor`と呼ばれる特殊な型で演算等が行われるということだ．
![PyTorch_slide_1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/aec2e60f-ca79-f804-5b0f-8b3f1c5aeaa5.jpeg)
![PyTorch_slide_2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/90decfbb-971d-d996-407b-370b5a8475cc.jpeg)
さらに，PyTorchの機能として自動微分がある．この機能によって，ニューラルネットワークの学習における誤差逆伝播法などで行う微分が簡単に実装できるようになっている．
![PyTorch_slide_3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/32b23878-a091-711d-f0e7-f2479e9f0b24.jpeg)

## 2.2 機械学習・ディープラーニングの仕組み
機械学習，単純パーセプトロンや多層パーセプトロン，ディープラーニングの学習とは，予測モデルに含まれるパラメータを上手く更新していき，各パラメータの最適な値を見つけるということだ．この学習のプロセスを以下のスライドに示す．
![MachineLearning_slide_1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/0b020af8-0781-f563-01b5-fcad42ac8dc0.jpeg)

## 2.3 PyTorchでの実装方法
前節で示した，学習のプロセスをPyTorchでどのように実装するのかを見てみる．
![DeepLearning.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/6b4c6acb-2ace-aa90-f50f-050d02659fda.jpeg)

## 2.4 GPUの利用，CUDAとは
画像認識などのAI処理 (ディープラーニングなど) には，GPUを利用して演算の処理効率を上げるという手法がとられる．GPUの利用は，コンピューターが勝手にGPUを使って演算してくれるわけではないので，プログラミング中に，GPUを使う設定を行わなければならない．
![gpu_slide_1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/3851f549-1fb1-6ed0-a616-1df45c976e0d.jpeg)
CUDAとは，NVIDIAが開発・提供している，GPU向けの汎用並列コンピューティングプラットフォームおよびプログラミングモデルのことであるが，`torch.cuda`について，まだ理解が追い付いていなく，google.colabratoryのほうでもGUPで実行してようとすると，エラーを起こしてしまったので，一旦ここでスルーします．<strong>(後日修正します)</strong>
![cuda_slide_1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/429bc732-3b07-2dff-5c74-a66a90711e60.jpeg)

# 3 必要ライブラリのインポート
今回の学習において，必要なライブラリをインポートします．
```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
```
```Python
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
```
インポートしたパッケージの詳細は以下のようになっている．
| パッケージ |     名前      |
| --- | ----------- |
| torch    | PyTrochのリストを扱うもの
| torch.nn | ネットワークの構築
| torch.nn.funcctional |様々な関数の使用
| torch.optim |最適化アルゴリズムの使用
| torchvision|画像処理に関係する処理の使用
| torchvision.transform|画像変換機能の使用

# 4 学習データの準備
今回は，PyTorchから提供されている`MNIST datasets`を用いて手書き数字認識の行う．

## 4.1 データのダウンロード
MNIST datasetsは，`torchvision.datasets.MNIST`から取得することができる．
引数の詳細などは，以下のスライドを参照．
![MNIST_slide_1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/d201e54b-9992-3f55-1330-7b074aa85716.jpeg)
![MNIST_slide_2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/f38ed432-f896-31b6-5f83-ec483384a077.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_dataset`に学習用の画像 (60000枚)とそれに対応する正解ラベル，`test_dataset`にテスト用の画像 (10000枚)とそれに対応する正解ラベルが渡された．また，以下のコードの実行で，渡されたデータセットの内容を確認できる．
```Python
print("train_dataset\n", train_dataset)
print("\ntest_dataset\n", test_dataset)
```
実行結果
```
train_dataset
 Dataset MNIST
    Number of datapoints: 60000
    Root location: ./data
    Split: Train
    StandardTransform
Transform: ToTensor()

test_dataset
 Dataset MNIST
    Number of datapoints: 10000
    Root location: ./data
    Split: Test
    StandardTransform
Transform: ToTensor()
``` 
## 4.2 ミニバッチ処理
学習におけるデータの投入方法は以下の３つが挙げられる．
![MNIST_slide_3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/b5342e7b-e798-2e74-5a8f-4b7542384de8.jpeg)
今回は，ミニバッチ学習によって学習を進めていくため，データをミニバッチの形に変形させる必要がある．datasetからバッチごとに取り出すには，`torch.utils.data.DataLoader`を用いて行う．引数の詳細などは，以下のスライドを参照．
![PyTorch_slide_4](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/f876b471-d3d2-bd71-e318-edc4f3728cfb.jpeg)
今回は，バッチサイズ256で定義した．
```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)
``` 

# 5 ネットワークモデルの定義
## 5.1 予測関数の定義
![DeepLearning.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/6b4c6acb-2ace-aa90-f50f-050d02659fda.jpeg)
(1)予測関数をクラスで定義する．
Pytorch のニューラルネットワークの基底クラスである<strong>nn.Module</strong>を継承してサブクラスとして実装する．
`__init__()`部分では，モデルの形状を定義する．
`forward()`部分では，モデルの順伝播の計算の流れを定義する．
この時，出力のy の部分で，活性化関数を通していない理由は`6 損失関数と最適化関数の定義`で説明をする．
```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
``` 
## 5.2 予測関数のインスタンス化
前節で作成したネットワークモデルのインスタンス化を行う．
今回のデータセットの画像の大きさは，28×28の大きさであるため，入力層の数は，784に指定しなければならない．手書き数字の画像を0から9の10個の数字に分類するタスクでるため，出力層は10指定しなければならない．中間層は任意の値であるが，今回は，中間層を2層設けて，1つ目を1024，2つ目を512とする．
```Python
input_size = 28*28
hidden1_size = 1024
hidden2_size = 512
output_size = 10

device = 'cuda:0' if torch.cuda.is_available() else 'cpu'
print("device : ", device)
model = Net(input_size, hidden1_size, hidden2_size, output_size).to(device)
print(model)
``` 
実行結果
今回は，GPUを使わず，cpuを用いて実行する．
``` 
device :  cpu
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)
)
``` 


# 6 損失関数と最適化関数の定義
![DeepLearning.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/6b4c6acb-2ace-aa90-f50f-050d02659fda.jpeg)
(2)損失関数と，(3)最適化関数を定義する．
今回は，多値分類問題になるため，損失関数には，交差エントロピー誤差関数を，最適化法には，確率的勾配降下法を用いて学習を行うこととする．
今回は，クラス分類問題であるので，交差エントロピー誤差関数を用いた．
また，最適化法は，確率的勾配降下法を用いて学習を行うこととする．
ここで，前節で定義したネットワークモデルの`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)
``` 

# 7 学習１エポック分を関数化して定義
![DeepLearning.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/6b4c6acb-2ace-aa90-f50f-050d02659fda.jpeg)
学習１回のサイクルは，①予測計算→②損失計算→③勾配計算→④パラメータ修正 で成立している．これらを，関数 (メソッド) の呼び出しによって行っていく．
```Python
def train_model(model, train_loader, criterion, optimizer, device='cpu'):

    train_loss = 0.0  #trainの損失用の変数を定義
    num_train = 0     #学習回数の記録用の変数を定義 

    # モデルを学習モードに変換
    model.train()
    
    # データの分割数分繰り返す
    # バッチサイズ分のデータで1回パラメータを修正する
    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
``` 

# 8 検証データによるモデル評価を行う関数の定義
検証データによるモデル評価を行う関数を定義する．
```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, lebels = 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
``` 

# 9 モデル学習を行う関数の定義
1エポックごとに，テストデータでも評価を行い，学習データによる損失と，テストデータによる損失を表示させる関数を作成した．
```PYthon
def learning(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
``` 

# １０ 学習
これまでに定義した関数を用いて学習を行う．
学習を行うプロセスは，`learning()`にまとめられているので，以下のように簡単に記述する．
```Python
num_epochs = 10
train_loss_list, test_loss_list = learning(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
``` 
# １１ 学習推移の確認
matplotlibを用いて学習の推移を確認する．
```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()
```
実行結果
![ダウンロード (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072037/14af3588-e3fc-90d6-f2b8-aad9bec06a23.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/cd685b3c-046d-a4fc-772f-4728634fe2ed.png)

# １３ おわりに
以上で，学習が上手くできていることが確認できた．
12の結果で唯一間違えた結果となっている5の手書き数字画像は人間でも間違えそう笑
今後，GPUの使い方を学習して，本ページを更新したいと考えております．