【Python】PyTorchで2値分類

# はじめに
前回はPyTorchを使って、単回帰モデルの実装・訓練を行いました。今回は二値分類を行います。

# やったこと
アイリス・データセットからラベルが0と1のデータを抽出し、二値分類を行いました。
- 機械学習モデル: ロジスティック回帰
- 損失関数: 交差エントロピー誤差
- 最適化関数: SGD
![決定境界 - コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/871a83c3-fcc8-9454-160d-c4c94c1707f6.png)
![学習曲線_損失 - コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/6dea5b0a-c13b-74c8-647a-de53571039e2.png)
![学習曲線_精度 - コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/b84d8dc9-9989-320b-9a85-fbbe1dc03f88.png)

https://github.com/grv2688/qt_pytorch/tree/a33c19ae16e0494689b494e9517f7dcd63281883/241027_1_PyTorch%E3%81%A72%E5%80%A4%E5%88%86%E9%A1%9E

# インポート
```python
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
from IPython.display import display


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

# scikit-learn関連ライブラリのインポート
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

# デフォルトフォントサイズ変更
plt.rcParams['font.size'] = 14
# デフォルトグラフサイズ変更
plt.rcParams['figure.figsize'] = (6,6)
# デフォルトで方眼表示ON
plt.rcParams['axes.grid'] = True
```

# データ準備
アイリス・データセットは150件あり、先頭の100行を抽出すると、setosaとversicolorの二値分類になります。特徴量は、sepal_lengthとsepal_widthのみとします。

```python

# データ読み込み
iris = load_iris()

# 入力データと正解データ取得
x_org, y_org = iris.data, iris.target

# 結果確認
print('元データ', x_org.shape, y_org.shape)
# 元データ (150, 4) (150,)

# 二次分類化
x_data = iris.data[:100, :2]
y_data = iris.target[:100]

# 結果確認
print('対象データ', x_data.shape, y_data.shape)
# 対象データ (100, 2) (100,)
```

# データ分割
データサイズを確認しておきます。
```python
# 元のデータのサイズ
print(x_data.shape, y_data.shape)
# (100, 2) (100,)
```
訓練データを70件、検証データを30件に分割します。
```python
# 訓練データ、検証データに分割（シャッフルも同時に実施）
x_train, x_test, y_train, y_test = train_test_split(x_data,
                 y_data,
                 train_size=70, 
                 test_size=30, 
                 random_state=123,
                 )
print(x_train.shape, x_test.shape, y_train.shape, y_test.shape)
# (70, 2) (30, 2) (70,) (30,)

```
散布図を表示させる。
```python
#  散布図の表示
x_t0 = x_train[y_train == 0]
x_t1 = x_train[y_train == 1]
plt.scatter(x_t0[:,0], x_t0[:, 1], marker='x', label='0 (setosa)')
plt.scatter(x_t1[:,0], x_t1[:, 1], marker='o', label='1 (versicolor)')
plt.xlabel('sepal_length')
plt.ylabel('sedal_width')
plt.legend()
plt.show()
```
![ch_06_scatter1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/24cdce62-7cbc-c182-5e1e-93c3e0f645ac.png)

# モデル定義
入力次元数は2(sepalの幅と長さ）で出力次元数は1（setonaか否か）になります。
```python
# 入力次元数（いまの場合2）
n_input = x_train.shape[1]

# 出力次元数
n_output = 1

# 結果確認
print(f"n_input: {n_input}, n_output: {n_output}")
# n_input: 2, n_output: 1
```
モデルを定義します。2入力1出力のロジスティック回帰モデルを定義します。（分類なのに、ロジスティック回帰…）
```python
class Net(nn.Module):
    def __init__(self, n_input, n_output):
        super().__init__()
        self.l1 = nn.Linear(n_input, n_output)
        self.sigmoid = nn.Sigmoid()

        # 初期値を全部1にする
        # 前著「ディープラーニングの数学」と条件を合わせる目的
        self.l1.weight.data.fill_(1.0)
        self.l1.bias.data.fill_(1.0)

    # 予測関数の定義
    def forward(self, x):
        # 最初に入力値を線形関数にかけた結果を計算する
        x1 = self.l1(x)
        # 計算結果にシグモイド関数をかける
        x2 = self.sigmoid(x1)
        return x2
```
インスタンス生成し、モデルの概要を確認しておきます。
```python
# インスタンスの生成
net = Net(n_input, n_output)

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

# モデルのサマリー表示
summary(net, (2,))
```
サマリは以下のような感じ。
```
==========================================================================================
Layer (type:depth-idx)                   Output Shape              Param #
==========================================================================================
Net                                      [1]                       --
├─Linear: 1-1                            [1]                       3
├─Sigmoid: 1-2                           [1]                       --
==========================================================================================
Total params: 3
Trainable params: 3
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.BCELoss()

# 最適化関数: 勾配降下法
optimizer = optim.SGD(net.parameters(), lr=lr)

# 繰り返し回数
num_epochs = 10000

# 記録用リストの初期化
# 1: 繰り返し数
# 2: 訓練データの損失
# 3: 訓練データの精度
# 4: 検証データの損失
# 5: 検証データの精度
history = np.zeros((0, 5))
```

# メインループ
データをテンソル化します。
```python
# 入力データx_trainと正解データy_trainをテンソル化
inputs = torch.tensor(x_train).float()
labels = torch.tensor(y_train).float()

# 正解データはN行1列の行列に変換する
# これをやっておかないと予測の時？にエラーになる。
labels1 = labels.view(-1, 1)

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

# 検証用の正解データもN行1列の行列に変換
labels1_test = labels_test.view(-1, 1)
```
訓練します。
```python
# 繰り返し計算メインループ

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

for epoch in range(num_epochs):
    # 訓練フェーズ ------------------------------------------
    
    #勾配値初期化
    optimizer.zero_grad()

    # 予測計算
    outputs = net(inputs)

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

    # 勾配計算
    loss.backward()
    
    # パラメータ修正
    optimizer.step()

    # ★損失の保存(スカラー値の取得)
    train_loss = loss.item()

    # ★予測ラベル(1 or 0)計算
    # 二値分類なのでこういう実装
    predicted = torch.where(outputs < 0.5, 0, 1)
    
    # ★精度計算
    train_acc = (predicted == labels1).sum() / len(y_train)

    # 予測フェーズ ------------------------------------------

    # 予測計算
    outputs_test = net(inputs_test)

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

    # 損失の保存（スカラー値の取得）
    val_loss =  loss_test.item()
        
    # 予測ラベル(1 or 0)計算
    predicted_test = torch.where(outputs_test < 0.5, 0, 1)

    # 精度計算
    val_acc = (predicted_test == labels1_test).sum() / len(y_test)
    
    if ( epoch % 1000 == 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))
```
結果は以下の通り。検証データの精度は0.967です。
```
Epoch [0/10000], loss: 4.77289 acc: 0.50000 val_loss: 4.49384, val_acc: 0.50000
Epoch [1000/10000], loss: 0.36160 acc: 1.00000 val_loss: 0.40759, val_acc: 0.96667
Epoch [2000/10000], loss: 0.23677 acc: 1.00000 val_loss: 0.29188, val_acc: 0.96667
Epoch [3000/10000], loss: 0.18060 acc: 1.00000 val_loss: 0.24044, val_acc: 0.96667
Epoch [4000/10000], loss: 0.14842 acc: 1.00000 val_loss: 0.21159, val_acc: 0.96667
Epoch [5000/10000], loss: 0.12737 acc: 1.00000 val_loss: 0.19317, val_acc: 0.96667
Epoch [6000/10000], loss: 0.11243 acc: 1.00000 val_loss: 0.18042, val_acc: 0.96667
Epoch [7000/10000], loss: 0.10121 acc: 1.00000 val_loss: 0.17109, val_acc: 0.96667
Epoch [8000/10000], loss: 0.09243 acc: 1.00000 val_loss: 0.16398, val_acc: 0.96667
Epoch [9000/10000], loss: 0.08535 acc: 1.00000 val_loss: 0.15839, val_acc: 0.96667
```

# 結果確認
```python
# 損失と精度の確認
print(f"初期状態： 損失: {history[0, 3]:.5f}, 精度: {history[0, 4]:.5f}")
print(f"最終状態： 損失: {history[-1, 3]:.5f}, 精度: {history[-1, 4]:.5f}")
# 初期状態： 損失: 4.49384, 精度: 0.50000
# 最終状態： 損失: 0.15395, 精度: 0.96667
```
![ch_06_学習曲線（損失）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/3becae7e-4dda-cbb4-631f-bbe02391ec6f.png)


```python
# 学習曲線の表示（損失）
plt.plot(history[:, 0], history[:, 1], 'b', label='train_loss')
plt.plot(history[:, 0], history[:, 3], 'k', label='train_test')
plt.xlabel('繰り返し回数')
plt.ylabel('損失')
plt.title('学習曲線（損失）')
plt.legend()
plt.show()
```
![ch_06_学習曲線（精度）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/9e8de4a8-a19a-20d2-543b-884ab35f75d0.png)



# 決定境界直線
```python
# 検証データを散布図用に準備

x_t0 = x_test[y_test==0]
x_t1 = x_test[y_test==1]
```

```python
# パラメータの取得

bias = net.l1.bias.data.numpy()
weight = net.l1.weight.data.numpy()
print(f'BIAS = {bias}, WEIGHT = {weight}')
# BIAS = [0.33861226], WEIGHT = [[ 2.9700334 -5.300017 ]]

# 決定境界描画用 x1の値から x2の値を計算する
def decision(x):
    return(-(bias + weight[0,0] * x)/ weight[0,1])

# 散布図のx1の最小値と最大値
xl = np.array([x_test[:,0].min(), x_test[:,0].max()])
yl = decision(xl)

# 結果確認
print(f'xl = {xl}  yl = {yl}')
# xl = [4.4 7. ]  yl = [2.52956916 3.98656204]
```
```python
# 散布図表示
plt.scatter(x_t0[:,0], x_t0[:, 1], marker='x', c='b', s=50, label='class 0')
plt.scatter(x_t1[:,0], x_t1[:, 1], marker='o', c='k', s=50, label='class 1')

# 決定境界直線
plt.plot(xl, yl, c='b')
plt.xlabel('sepal_length')
plt.ylabel('sepal_width')
plt.legend()
plt.show()
```
![ch_06_scatter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/82f76710-0968-33aa-0231-c4a573bef16d.png)

# おわりに
理論の説明は極力省き、必要な実装部分を抽出しました。

# 出典
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