【Python】PyTorchで多値分類（アヤメ）

# はじめに
前回は、PyTorchを使って二値分類を行いました。今回は多値分類を行います。

# やったこと
- データセット: Iris
- 入力次元数: 4
- 出力次元数: 3
- 機械学習モデル: ロジスティック回帰
- 損失関数:  交差エントロピー関数
- 最適化アルゴリズム: 勾配降下法

![学習曲線_損失 - コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/3e3a43cc-420a-0a02-125b-686c233f31af.png)
![学習曲線_精度 - コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/c2c87834-438e-f78c-1e9a-cb6233ac7214.png)


# 実装の方針
- 予測関数側では活性化関数は不要で、**線形関数の出力をそのまま出力とする**
- 予測関数出力から **確率値を得たい場合は、予測関数出力にsoftmax関数をかける**
- **損失関数はCrossEntropyLoss関数**を利用する
- CrossEntropyLoss関数はその最終段がnn.NLLLoss関数なので、**第2引数として引き渡す正解値は、NLLLoss関数同様に整数値にする必要**がある

## 補足
- 別のやり方もあるが、一度かけた対数関数の計算結果を指数関数で元に戻す点が気持ち悪い（らしい）

# ライブラリ
```Python
# 必要ライブラリのインポート
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
from IPython.display import display

# sklearn関連
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

# torch関連ライブラリのインポート
import torch
import torch.nn as nn
import torch.optim as optim
from torchinfo import summary
from torchviz import make_dot

# デフォルトフォントサイズ変更
plt.rcParams['font.size'] = 14
# デフォルトグラフサイズ変更
plt.rcParams['figure.figsize'] = (6,6)
# デフォルトで方眼表示ON
plt.rcParams['axes.grid'] = True
# numpyの表示桁数設定
np.set_printoptions(suppress=True, precision=4)
```

# データ準備
まずはデータを読み込みます。
```Python
# データ読み込み
iris = load_iris()
# 入力データと正解データ取得
x_org, y_org = iris.data, iris.target
# 結果確認
print('元データ', x_org.shape, y_org.shape)
```

続いて、訓練データと検証データに分割します。

```python
# 訓練データ、検証データに分割 (シャフルも同時に実施)
x_train, x_test, y_train, y_test = train_test_split(
    x_org, y_org, train_size=75, test_size=75,
    random_state=123)
print(x_train.shape, x_test.shape, y_train.shape, y_test.shape)

# 入力次元数
n_input = x_train.shape[1]
print('入力データ(x)')
print(x_train[:5,:])
print(f'入力次元数: {n_input}')

# 出力次元数
# 分類先クラス数、今回は3になる
n_output = len(list(set(y_train)))
print(f"出力次元数: {n_output}")
```

# ゆるEDA
概要を表示しておきます。
```Python
print(iris.DESCR)
```
```
.. _iris_dataset:

Iris plants dataset
--------------------

**Data Set Characteristics:**

:Number of Instances: 150 (50 in each of three classes)
:Number of Attributes: 4 numeric, predictive attributes and the class
:Attribute Information:
    - sepal length in cm
    - sepal width in cm
    - petal length in cm
    - petal width in cm
    - class:
            - Iris-Setosa
            - Iris-Versicolour
            - Iris-Virginica

:Summary Statistics:

============== ==== ==== ======= ===== ====================
                Min  Max   Mean    SD   Class Correlation
============== ==== ==== ======= ===== ====================
sepal length:   4.3  7.9   5.84   0.83    0.7826
sepal width:    2.0  4.4   3.05   0.43   -0.4194
petal length:   1.0  6.9   3.76   1.76    0.9490  (high!)
petal width:    0.1  2.5   1.20   0.76    0.9565  (high!)
============== ==== ==== ======= ===== ====================

:Missing Attribute Values: None
:Class Distribution: 33.3% for each of 3 classes.
(以下省略）
```

散布図を描画します。
```Python
# データを正解値ごとに分割
x_t0 = x_train[y_train == 0]
x_t1 = x_train[y_train == 1]
x_t2 = x_train[y_train == 2]

# 散布図の表示
# 0: sepal_length, 2: petal_length
plt.scatter(x_t0[:,0], x_t0[:,2], marker='x', c='k', s=50, label='0 (setosa)')
plt.scatter(x_t1[:,0], x_t1[:,2], marker='o', c='b', s=50, label='1 (versicolour)')
plt.scatter(x_t2[:,0], x_t2[:,2], marker='+', c='k', s=50, label='2 (virginica)')
plt.xlabel('sepal_length')
plt.ylabel('petal_length')
plt.legend()
plt.show()
```

★ココに散布図の画像を配置

# モデル定義
モデルを定義します。冒頭でも書いたように、機械学習モデルはロジスティック回帰です。4入力3出力です。
```python
# モデル定義
# 4入力3出力のロジスティック回帰モデル

class Net(nn.Module):
    def __init__(self, n_input, n_output):
        super(Net, self).__init__()
        self.l1 = nn.Linear(n_input, n_output, bias=True)

        # 初期値を全部1にする
        # 前著"ディープラーニングの数学"と条件を合わせる目的
        self.l1.weight.data.fill_(1.0)
        self.l1.bias.data.fill_(1.0)
    
    def forward(self, x):
        x1 = self.l1(x)
        return x1
    
# インスタンスの生成
net = Net(n_input, n_output)

# モデルの概要を表示
print(net)

# モデルのサマリー表示
summary(net, (n_input,))
```
サマリは以下のような感じ。
```
==========================================================================================
Layer (type:depth-idx)                   Output Shape              Param #
==========================================================================================
Net                                      [3]                       --
├─Linear: 1-1                            [3]                       15
==========================================================================================
Total params: 15
Trainable params: 15
Non-trainable params: 0
Total mult-adds (Units.MEGABYTES): 0.00
==========================================================================================
Input size (MB): 0.00
Forward/backward pass size (MB): 0.00
Params size (MB): 0.00
Estimated Total Size (MB): 0.00
==========================================================================================
```

## 損失関数などの設定
下記参照。
```Python
# 学習率
lr = 0.01

# 初期化
net = Net(n_input, n_output)

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

# 最適化アルゴリズム: 勾配降下法
optimizer = optim.SGD(net.parameters(), lr=lr)

# 繰り返し回数
num_epochs = 10000

# 評価結果記録用
history = np.zeros((0,5))
```

## データのテンソル化
```Python
# 入力データ x_train と正解データ y_train のテンソル変数化
inputs = torch.tensor(x_train).float()
labels = torch.tensor(y_train).long()

# 検証用データのテンソル変数化
inputs_test = torch.tensor(x_test).float()
labels_test = torch.tensor(y_test).long()
```

## 計算グラフ可視化
```Python
# 予測計算
outputs = net(inputs)

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

# 損失の計算グラフ化
g = make_dot(loss, params=dict(net.named_parameters()))
display(g)
```

★計算グラフの画像をここに配置

## 補足: 予測値ラベルの取得方法
- 本来softmaxを通した後の値の中で一番大きい値を予測値とする
- softmax関数は、通す前と後で大小は変わらない
- softmax関数を通す前の値を取得すれば、それが予測ラベルになる

```Python
# torch.max関数呼び出し
# 二つ目の引数は軸を意味している。1だと行ごとの集計
print(torch.max(outputs, 1))
```

# メインループ
```Python
for epoch in range(num_epochs):

    # 訓練フェーズ

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

    # 予測計算
    outputs = net(inputs)

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

    # 勾配計算
    loss.backward()

    # パラメータ修正
    optimizer.step()

    #予測値算出
    predicted = torch.max(outputs, 1)[1]

    # 損失と精度の計算
    train_loss = loss.item()
    train_acc = (predicted == labels).sum()  / len(labels)

    #予測フェーズ

    # 予測計算
    outputs_test = net(inputs_test)

    # 損失計算
    loss_test = criterion(outputs_test, labels_test)

    # 予測ラベル算出
    predicted_test = torch.max(outputs_test, 1)[1]

    # 損失と精度の計算
    val_loss =  loss_test.item()
    val_acc =  (predicted_test == labels_test).sum() / len(labels_test)

    if ( epoch % 10 == 0):
        print (f'Epoch [{epoch}/{num_epochs}], loss: {train_loss:.5f} acc: {train_acc:.5f} val_loss: {val_loss:.5f}, val_acc: {val_acc:.5f}')
        item = np.array([epoch , train_loss, train_acc, val_loss, val_acc])
        history = np.vstack((history, item))
```

# 結果確認
```Python
# 損失と精度の確認
print(f'初期状態: 損失: {history[0,3]:.5f} 精度: {history[0,4]:.5f}' )
print(f'最終状態: 損失: {history[-1,3]:.5f} 精度: {history[-1,4]:.5f}' )
```
結果は以下の通り。
```
初期状態: 損失: 1.09158 精度: 0.26667
最終状態: 損失: 0.13724 精度: 0.96000
```

損失の学習曲線を描く。

```Python
# 学習曲線の表示 (損失)

plt.plot(history[:,0], history[:,1], 'b', label='訓練')
plt.plot(history[:,0], history[:,3], 'k', label='検証')
plt.xlabel('繰り返し回数')
plt.ylabel('損失')
plt.title('学習曲線(損失)')
plt.legend()
plt.show()
```

![学習曲線_損失.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/38d9fc30-a0c9-a670-a98a-1858c252b7fd.png)

精度の学習曲線を描く。

```Python
# 学習曲線の表示 (精度)

plt.plot(history[:,0], history[:,2], 'b', label='訓練')
plt.plot(history[:,0], history[:,4], 'k', label='検証')
plt.xlabel('繰り返し回数')
plt.ylabel('精度')
plt.title('学習曲線(精度)')
plt.legend()
plt.show()
```

![学習曲線_精度.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/e72cc65a-0b19-70c5-90b3-80f25e6d929b.png)

## モデル出力の確認
- 今回はsoftmax関数を損失関数側に含めてしまったので、モデルの出力がどんな値になっているか調べてみる
- また、出力から確率値を取得したい場合どうしたらいいかも調べる

```Python
# 正解データの0番目、2番目、3番目を抜き出すことにした
print(labels[[0, 2, 3]])
```
```
tensor([1, 0, 2])
```

0, 2, 3番目の入力値を抽出し、i3とする

```Python
# 該当する入力値を抽出
i3 = inputs[[0, 2, 3], :]
print(i3.data.numpy())
```
```
[[6.3 3.3 4.7 1.6]
 [5.  3.  1.6 0.2]
 [6.4 2.8 5.6 2.1]]

```
入力値をnetに通して出力値を取得し、o3とします。続いて、出力o3にsoftmax関数をかけたものを、k3とします。。

```Python
# 入力値をnetに通す。出力値を取得。
# 出力値にsoftmax関数をかけた結果を取得

softmax = torch.nn.Softmax(dim=1)
o3 = net(i3)
k3 = softmax(o3)
print(o3.data.numpy())
print(k3.data.numpy())
```

結果は以下の通り。k3は足すと1になっている（はず）。
```
[[14.3224 19.3516 17.0256]
 [17.1434 14.295   0.9613]
 [11.1292 19.7008 22.8696]]
[[0.0059 0.9056 0.0885]
 [0.9452 0.0548 0.    ]
 [0.     0.0404 0.9596]]
```

# 学習後の重みとバイアスを確認
```Python
# 重み行列
print(net.l1.weight.data)
# バイアス
print(net.l1.bias.data)
```
```
tensor([[ 1.8991,  3.0917, -1.8499, -0.3448],
        [ 1.5797,  1.1379,  0.9194, -0.1347],
        [-0.4789, -1.2295,  3.9305,  3.4794]])
tensor([1.4016, 1.5386, 0.0597])
```

# 終わりに
多値分類をした。決定境界直線がうまく書けなかった…。

# 出典
https://www.amazon.co.jp/%E6%9C%80%E7%9F%AD%E3%82%B3%E3%83%BC%E3%82%B9%E3%81%A7%E3%82%8F%E3%81%8B%E3%82%8B-PyTorch-%E6%B7%B1%E5%B1%A4%E5%AD%A6%E7%BF%92%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-%E8%B5%A4%E7%9F%B3-%E9%9B%85%E5%85%B8/dp/4296110322