Pytorchの機械学習のあれこれ

#はじめに
[こちら ~会社の業務をRPAで効率化した話~](https://qiita.com/hideto1198/items/d0641d9a9dacbb786bc2)の記事でわざわざ作成したニューラルネットワークについての記事です！
あまり効率のよいコーディングはしていませんが、学習までの流れのわかりやすさを重視しました！

#ネットワークをわざわざ作った経緯
PythonではTesseract OCRを使用すればPCの画面上の文字や数字を読み取ることが可能ですが、いろいろと都合が悪かったためニューラルネットワークを作成しました...LoL
###Tesseractの都合が悪いこと
>- パラメーターをいじっても誤認識がそこそこあった
>- いじくりまわせない
>- Pyinstallerでexe化したときにexeファイルがかなり肥大する
>- exe化後の問題が多発

シンクライアント端末でPythonやTesseractがインストールできないためexe化するときに一緒にパッケージングしてあげなければならず、Pyinstaller周りで非常に面倒で、数字認識しかしないのでネットワークを作成しました。

#開発環境
- iMac 2019モデル Big sur (Boot Campでwindows10を使用)
- Google Colaboratry

#本編
##データセットの準備
```python
digital_data = np.arange(47040000).reshape(60000,1,28,28)      #(28x28)が60000枚 60000x28x28=47040000
digital_ans = []
for i in range(10000):
    img = Image.open('0.png')                                 #デジタルデータ
    img = ImageOps.invert(img.convert('L')).resize((28,28))   #8bitのグレイスケールに変換して(28x28)にリサイズ
    data = np.array(img,np.float32).reshape(1,28,28)          #imgをfloat型にし、次元を増やして代入
    for n in range(28):
        digital_data[i][0][n] = data[0][n]                    #1行ずつ代入していく
    digital_ans.append(0)                                     #答えも同時に入れていく
```
まずはpngデータをnumpy配列に落とし込みます。
今回はデジタル数字を使用しましたが、ほかの画像も使用できるかと思います。
`digit_data`の３，４次元の値を任意のピクセル数に変え、for文内の`resize(),reshape`を同じ値に変えれば可能です！
digit_ansはそのままでも可能です。0->ピカ〇ュウ,1->ヒト〇ゲのように定義しておけば問題ありません。
##importしたもの
```python
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision.transforms as transforms
from torch.utils.data import DataLoader

import numpy as np

import random
from PIL import Image, ImageOps

import pickle
```
| ライブラリ | 特徴 |
|:-:|:--|
|os    |OS依存の機能を利用するためのライブラリ|
|torch |ニューラルネットワークを簡単に構築するフレームワーク|
|numpy |行列計算や次元を操るライブラリ|
|random|ランダムで値をつくれるようになるライブラリ|
|PIL   |画像処理を簡単に行うライブラリ|
|pickle|学習したニューラルネットをファイルとして保存するライブラリ|
##おまじない
```python
if torch.cuda.is_available():
    device = 'cuda'
else:
    device = 'cpu'
print('Device:',device)
```
Google ColaboratryではGPUが使用可能です。GPUが使用な環境ではできるだけGPUを使用すること強くお勧めします！
GPUのほうが学習時間が短く効率が良いです。しかし、学習結果をGPUが使用できない環境で用いる場合は**ニューラルネットワークがGPUと依存関係をもってしまうためCPUで学習しなければなりません。**
##学習データと訓練データの振り分け
```python
rand_idx = list(range(60000))                       #0~59999の数を生成
rand_idx = random.sample(rand_idx,len(rand_idx))    #rand_idxをシャッフルする  random.sampleで重複なしで取得できる

#訓練:テスト=8:2
rand_train = rand_idx[:48000]                       #0~47999までのインデックス
rand_test = rand_idx[48000:60000]                   #48000~59999までのインデックス

#各データ配列の初期化
x_dig_train = np.arange(37632000).reshape(48000,1,28,28)    #訓練用デジタルデータ
x_dig_test = np.arange(9408000).reshape(12000,1,28,28)      #テスト用デジタルデータ
t_dig_train = np.arange(48000)                              #訓練用の答え
t_dig_test  = np.arange(12000)                              #訓練用の答え

#各numpy配列にinsertしていく
for i in rand_train:
    np.insert(x_dig_train,count,digital_data[i][0][0])
    np.insert(t_dig_train,count,digital_ans[i])
    count += 1
for i in rand_test:
    np.insert(x_dig_test,count,digi_data[i][0][0])
    np.insert(t_dig_test,count,digi_ans[i])
    count += 1
```

##ネットワークの構築
```python
class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1,32,3)
        self.pool = nn.MaxPool2d(2,2)
        self.conv2 = nn.Conv2d(32,64,3)
        self.fc1 = nn.Linear(64*5*5,800)
        self.dropout = nn.Dropout(p=0.5)
        self.fc2 = nn.Linear(800,10)
    
    def forward(self,x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1,64*5*5)
        x = F.relu(self.fc1(x))
        x = self.dropout(x)
        x = F.relu(self.fc2(x))
        return x
```
||説明|
|:-:|:--|
|nn.Conv2d   |２次元畳み込み層 引数(サンプル数: int,チャネル数: int,フィルタサイズ: tuple(正方形ならint))|
|nn.MaxPool2d|２次元マックスプーリング層　引数(フィルタサイズ:tuple(正方形ならint),ストラド:tuple(正方形ならint))|
|nn.Linear   |全結合層 引数(直前のニューロンの数,結合後のニューロンの数)|
|nn.Dropout  |ドロップアウト層 引数(p: float) default p=0.5|
|F.relu      |出力層　 引数(直前の隠れ層)|

###畳み込みから全結合層へ入る前の計算方法

**畳み込み層**

```math
H = 入出力画像の高さ\\
W = 入出力画像の幅\\
\\
H_{out} = \lfloor \frac{H_{in}+2*padding[0]-dilation[0]*(kernelSize[0]-1)-1}{stride[0]} + 1 \rfloor\\
\\
W_{out} = \lfloor \frac{W_{in}+2*padding[1]-dilation[1]*(kernelSize[1]-1)-1}{stride[1]} + 1 \rfloor
\\ 
```
**入力画像が正方形**で**【stride,padding,dilation】=【1,0,1】(デフォルト値)**で**フィルターも正方形**なら
一般に,

```math
X=入力画像の高さ、幅\\
H_{out}=W_{out}=\lfloor X-kernelSize+1 \rfloor
```
**プーリング層**

```math
H = 入出力画像の高さ\\
W = 入出力画像の幅\\
\\
H_{out} = \lfloor \frac{H_{in}+2*padding[0]-dilation[0]*(kernelSize[0]-1)-1}{stride[0]} + 1 \rfloor\\
\\
W_{out} = \lfloor \frac{W_{in}+2*padding[1]-dilation[1]*(kernelSize[1]-1)-1}{stride[1]} + 1 \rfloor!
\\ 
```
>畳み込み層とプーリング層の計算式は同じ！！

####例
![NN.001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186268/9950cfdd-1b3a-8fe2-650e-df099f00b693.png)
>16x16の入力画像に対して、畳み込み層`Conv2d(1,32,3)`をかませると、上の式より、14x14の出力が32枚になります。
>その32枚に対してプーリング層`Maxpool2d(2,2)`をすると7x7の出力が32枚になります。
>よって、全結合層に入るときは、**32x7x7**になるというわけです！

##学習スタート
```python
net = Net()                                 #ニューラルネットのインスタンス生成
net.to(device)                              #CPUまたはGPUの切り替え

loss_func = nn.CrossEntropyLoss()           #損失関数の定義

optimizer = optim.Adam(net.parameters())    #最適化アルゴリズムの選定

record_loss_train = []                      #訓練時の損失関数の値を格納
record_loss_test = []                       #テスト時の損失関数の値を格納

n_epoch = 30                                #エポック数(＝学習回数)
batch_size = 1500                           #バッチ数(どれだけまとめて学習するか)

#*******************ここから学習スタート*******************#
for epoch in range(n_epoch):
    net.train()                                                                # トレーニングモード
    loss_train = 0
    perm = np.random.permutation(len(x_dig_train[0]))                          #トレーニングデータのindexをシャッフルした配列を生成
    for i in range(0, len(x_dig_train[0]), batch_size):
        x = torch.from_numpy(x_dig_train[perm[i:i+batch_size]]).to(device)     #配列permからbatch_size分とってくる
        t = torch.from_numpy(t_dig_train[perm[i:i+batch_size]]).to(device)
        y = net(x)                                                             #Net()のfowardが開始される
        loss = loss_func(y,t)
        loss_train += loss.item()
        optimizer.zero_grad()                                                  #勾配をクリアしてモデルを再計算できるようにする
        loss.backward()                                                        #誤差を逆伝播させる
        optimizer.step()                                                       #パラメーターを更新する
    loss_train /= i+1
    record_loss_train.append(loss_train)

    net.eval()                                                                 # 評価モード
    y_test = net(x)
    loss_test = loss_func(y_test, t).item()
    record_loss_test.append(loss_test)

    if i%1 == 0:
        print("Epoch:",epoch, "Loss_Train:", loss_train, "Loss_Test:", loss_test)
```
今回はミニバッチ学習をしました。画像分類の時の損失関数は交差エントロピーを使用することが多いのでそのまま交差エントロピーを使用しました。また、最適化関数は`adam`を使用しましたが、最適化アルゴリズムはたくさんあり、パラメーター次第で変わってくるのでネットワークに適した最適化アルゴリズムを選択すると良いと思います！
`perm = np.random.permutation(len(x_dig_train[0])) `ここは個人的に少し考えたところなので解説しときます。

```python
perm = np.random.permutation(range(10))
```
>実行結果
> array([6, 4, 2, 7, 0, 9, 5, 1, 8, 3])

このように引数に与えられた数をシャッフルして新しい配列を生成する機能になっています。引数を`len(x_dig_train[0])`とすることで０～４７９９９(トレーニングの画像枚数)の数字がランダムに入った配列が生成されます。そして配列permがスライスで1500個選ばれ`x_dig_train`のindexになるわけです！
>正直言うと、この辺りは理解しきらなくても、画像の枚数とかさえ合っていれば動きますので**おまじない**的に使用することも可能です

##学習済みのモデルについて
pickleを用いた保存方法と使用方法を紹介します！
####保存
```python
filename = 'digit_Learn.sav'
pickle.dump(net,open(filename,'wb'))
```
たった2行でモデルの保存が可能です。
####読み込み
```python
loaded_model = pickle.load(open(filename,'rb'))
```
これで保存したモデルが使用可能になります！
####別のファイルで使用する方法
#####~手順~
>1. ニューラルネットのクラスをコピペする
>2. CPU,GPUの選択
>3. モデルをロードする

```python:smaple.py
#ニューラルネットコピー
class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1,32,3)
        self.pool = nn.MaxPool2d(2,2)
        self.conv2 = nn.Conv2d(32,64,3)
        self.fc1 = nn.Linear(64*5*5,800)
        self.dropout = nn.Dropout(p=0.5)
        self.fc2 = nn.Linear(800,10)
    
    def forward(self,x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1,64*5*5)
        x = F.relu(self.fc1(x))
        x = self.dropout(x)
        x = F.relu(self.fc2(x))
        return x

#CPU,GPUの選択
if torch.cuda.is_available():
    device = 'cuda'
else:
    device = 'cpu'

#モデルのロード
loaded_model = pickle.load(open('digit_Learn.sav','rb'))

#予測結果を受け取る
output = loaded_model.forward(torch.from_numpy("イメージデータ").to(device)) 
predic = output.argmax()    #最も予測値の高いものを返す
```
これで別のファイルでも学習済みモデルが使用可能になります。
`loaded_model.forward(torch.from_numpy("イメージデータ").to(device))`の"イメージデータ"**学習したときの次元と合わせる必要があります。**この記事の場合、`reshape(1,1,28,28)`の四次元にして`forward`に渡してあげます！！

##最後に
非常に簡単にではありますが、Pytorchを使用した機械学習についての解説でした。この記事を書いて、今まで曖昧だった部分も詳しく調べ理解することができたのでよかったです！Pytorchはいろいろな機能が用意されているのでそれを使用してもっと遊んでみたいです☺

**～参考文献～**
[PyTorch documentation](https://pytorch.org/docs/stable/index.html)