🔰PyTorchでニューラルネットワーク基礎 #03 【分類問題】

# 概要
個人的な備忘録を兼ねたPyTorchの基本的な解説とまとめです。第３回は分類問題になります。

![ラーメン.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/3ea61416-a8e3-41f0-b4e8-7b4e539dce60.jpeg)


機械学習の最初の一歩に、アヤメデータセットを用いたアヤメの分類問題があります。アヤメのデータセットは、入手のしやすさと扱いやすさで知られています。本記事では、アヤメデータセットのように**数値で構成された表形式のデータで、クラスを識別する分類問題**を扱います。

回帰問題からの変更点は大きく2箇所
1. 最終層の全結合層の出力が分類数：**Linear(in_features=___ , out_features=分類数)** 
1. 損失関数が**CrossEntropyLoss( )**

「あっさり、こってり、塩」の3種類に分類された架空のラーメンデータを用いて分類問題の基本的なネットワークを構築していきます。

:::note
**方針** 
1. できるだけ同じコード進行
1. できるだけ簡潔（細かい内容は割愛）
1. 特徴量などの部分，あえて数値で記入（どのように変わるかがわかりやすい）
:::




### 演習用のファイル（Githubからダウンロード）
* [データのファイル](https://github.com/azukiimo/qiita/blob/main/data/ramen_data.csv)
* [コード sample_03.ipynb (ipynb形式)](https://github.com/azukiimo/qiita/blob/main/sample_03.ipynb)


# 1. 分類問題
分類問題とは、データをカテゴリー・クラスに振り分ける問題となります:sweat:。画像の分類はその一例です。ニューラルネットワークを用いた分類では、最終層の全結合層を分類したいクラス数に設定し、出力値の大きさでどのクラスに属するのかを表すことが一般的です。今回は、ラーメンの脂質含有量、スープの濃度、塩分濃度などから「あっさり」「こってり」「塩」の３種類のラーメンに分類する問題を扱いつつ分類問題の実装方法をまとめていきます。

PyTorchによるプログラムの流れを確認します。基本的に下記の５つの流れとなります。Juypyter Labなどで実際に入力しながら進めるのがオススメ
1. データの読み込みとtorchテンソルへの変換　(2.1)
2. ネットワークモデルの定義と作成　(2.2)
3. 誤差関数と誤差最小化の手法の選択　(2.3)
4. 変数更新のループ　(2.4)
5. 検証　(2.5)

# 2. コードと解説

## 2.0 データについて

演習用に利用するデータはラーメンの種類を数値で分類した架空のデータとなります。
脂肪含有量、スープ濃度などを0〜10の間の数値で主観的に決めました:sweat_smile:
濃度、時間や量などすべて0〜10で評価されていますが、演習用ですので気にしないでください。実際のデータを利用する場合は、[分類問題のデータセット](https://qiita.com/AzukiImo/items/71693dc41692cd69bc18)を参考にしてください。以下の表は今回利用するCSVファイルの概要とサンプルです。


### データの概要
|変数名|意味|
|-----|--------|
|脂質含有量 | 0〜10の数値 |
|スープの濃度| 0〜10の数値|
|後味の持続時間| 0〜10の数値|
|透明度| 0〜10の数値|
|ラーメンタイプ| あっさり：0、こってり：1、塩:2|



### サンプル
最初の５列までがラーメンのタイプを特徴づけるデータとなります。最終列の７列目がID化されたラーメンのタイプとなります。

|脂質含有量|スープの濃度|塩分濃度|後味の持続時間 | 透明度 |ラーメンタイプ | ラーメンタイプID|
|--------|----------|-------|-------------|-------|------------|---------------|
| 3.3 |    4.1 |  4.9 |     1.6 | 6.5 |   あっさり    |      0 |
| 2.4 |    3.7 |  4.7 |     2.0 | 9.0 |   あっさり    |      0 |
| 6.8 |    8.0 |  6.7 |     9.5 | 1.8 |   こってり    |      1 |
| 3.1 |    6.6 |  8.4 |     3.6 | 6.6 |      塩      |    2  |
| 4.7 |    6.0 |  7.5 |     4.1 | 5.1 |      塩      |    2  |

各タイプごとそれぞれ160個ほど用意してあります。簡単ながら特徴量を箱ひげ図で表してみました。きれいに分類できるようにデータが作られているかな:smile:


![ramen_boxplots.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/e3be4ebe-3965-4df7-9ef6-10c420cd8c34.png)

繰り返しになりますが、**利用するデータは架空のもの**です。いよいよ分類問題のコードになります。


## 2.1 データの読み込みとtorchテンソルへの変換

まず利用するライブラリを読み込みます。

~~~python: ライブラリ
import numpy as np                 # データの読み込みに利用 
import torch                       # PyTorchのライブラリ
import torch.nn as nn              # PyTorchのネットワークのライブラリ
from sklearn.model_selection import train_test_split   # データ分割時に利用
~~~

### データの読み込み
ファイル名をramen_data.csvとしています。numpyのloadtxtを利用してcsvファイルを読み込みましょう。**x** が濃度や透明度などの説明変数、**t** がラーメンタイプIDで０，１，２という整数値の教師データです[^1]。scikit-learnのtrain_test_splitを利用して、学習に利用するデータとテスト（検証）に利用するデータに分割します[^2]。

~~~python: データの読み込み
filename = "./ramen_data.csv"
x = np.loadtxt(filename, delimiter=",", skiprows=1, usecols=(0,1,2,3,4))
t = np.loadtxt(filename, delimiter=",", skiprows=1, usecols=(6))
# numpyからtorchテンソルへ
x = torch.FloatTensor(x)   # 入力するデータは Float
t = torch.LongTensor(t)    # ラベル側はLong

# データの分割 
x, x_test, t, t_test = train_test_split(x,t, test_size=0.1, random_state=55)
~~~

* np.loadtxt(ファイル名，句切り記号 ，利用列)
* **skiprows=1**で先頭の行を飛ばしてデータを読み込むことになります。
* torch.FloatTensor()でフロート型、torch.LongTensor()で整数型
* 分類問題では目的変数・教師データはLongTensor()です。
* train_test_splitのオプションである **test_size** はテストデータ数の割合、 **random_state** は乱数シードで毎回同じ結果を得るための仕組みです。

## 2.2 ネットワークモデルの定義と作成

今回は下図のような全結合層を活性化関数でつないだ単純なネットワークのモデルを作成してみます。[第2回の非線形回帰で利用したネットワークに近い構造](https://qiita.com/AzukiImo/items/1f1497e5c52668147461)をしています。

![ラーメン分類ネットワーク.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725285/40c83912-ad4d-4b81-9d88-6b019d95b515.jpeg)


具体的には、脂質含有量、スープの濃度、塩分濃度、後味の持続時間、透明度の5種類の特徴量を入力してラーメンの種類を予測する形となります。

:::note
**全結合層・線形層の書き方**
**nn.Linear(in_features, out_features, bias=True)**

* in_features : 入力される特徴量の数
* out_features : 出力される特徴量の数
* bias : 定数項。定数項ありがデフォルト
:::
詳細はPyTorchの公式ドキュメントに記載されています。

https://pytorch.org/docs/stable/nn.html#linear-layers

クラスを利用してネットワークを記述します。
　(1)　\_\_init___()部分に利用するネットワーク名
　(2)　forward部分に実際の流れ

活性化関数を\_\_init__()に記述する方法があります[^3]。今回は\_\_init__()に書く方法を採用しました。print(model)としたときに活性化関数も反映されているので見通しもよく、後々便利かと思います。

```python: ネットワークモデルの定義と作成
# ネットワークの定義
class DNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(in_features=5, out_features=10)
        self.act = nn.ReLU()
        self.fc2 = nn.Linear(in_features=10, out_features=3) 

    def forward(self, x):
        h = self.fc1(x)
        h = self.act(h)
        y = self.fc2(h)
        return y

model = DNN()
```

print(model)の結果は、\_\_init()__に書かれているネットワーク層が上から順番にすべて出力されます。

~~~python:modelの構造
DNN(
  (fc1): Linear(in_features=5, out_features=10, bias=True)
  (act): ReLU()
  (fc2): Linear(in_features=10, out_features=3, bias=True)
)
~~~



### ネットワークの定義
* 入力される特徴量は（脂質含有量、スープの濃度、塩分濃度、後味の持続時間、透明度）の5種類、出力は3種類のラーメンなので最初の全結合層fc1のLinearについて**in_features=5**、最後の全結合層fc2のLinearについて**out_features=3**となります。中間層の値は10種類と適当に決めました。
* 全結合層の繋がり部分についてです。5種類のデータを最初の全結合層が受け取り、10種類の特徴量に変換、10種類の数値たちは活性化関数を経て、2つ目の全結合層は10種類の特徴量から3種類のラーメンの値を出力します。
* ５−−>１０-->３ と特徴量の次元が変わっていきます。


## 2.3 誤差関数と誤差最小化の手法の選択

分類問題では予測値$y$ と教師データ$t$ (0, 1, 2のラーメンID)の誤差を計算するのに、クロスエントロピーを用いることが一般的なようです。PyTorchでは**CrossEntropyLoss()** と記述します。

~~~python: 誤差関数と最適化
criterion = nn.CrossEntropyLoss()                          # 損失関数：cross_entropy
optimizer = torch.optim.Adam(model.parameters(), lr=0.05)  # 学習率：lr=0.001がデフォルト
~~~

モデルの予測値は**nn.Linear(10,3)** であることから３次元のベクトルです。損失計算は、単なる３次元ベクトル$y$とラーメンIDを比較することになります。３次元のベクトル$y$のsoftmaxを求めることで予測値が確率の形で表現されます。一方、ラーメンIDは、0なら(1, 0, 0)のように確率１で「あっさり」が選ばれるように変形します。これらの操作によって予測値$y$と教師データ$t$の誤差が２つの確率ベクトルの差とみなせます。実装上はこの一連の作業が簡略化されて**CrossEntropyLoss()** を使うことで、**criterion(y,t)** として誤差計算が行われます。
* **nn.CrossEntropyLoss()** は予測値と実測値（教師データ）のクロスエントロピー損失。使用時は、**criterion(x,t)** とする。
* **torch.optim.Adam()** は誤差の最小値を求める方法の一つ

PyTorchで利用できるoptimizerについては、[公式ドキュメント](https://pytorch.org/docs/stable/optim.html)にたくさん書かれています。


https://pytorch.org/docs/stable/optim.html



## 2.4 変数更新のループ
LOOPで指定した回数、
1. **y=model(x)** で予測値を求め、
1. **criterion(y, t)** で指定した誤差関数を使い予測値と教師データの誤差を計算、
1. 誤差が小さくなるようにoptimizerに従い全結合層の重みとバイアスをアップデート
します。
~~~python: 変数更新
LOOP = 50                                # LOOP : 学習回数
for epoch in range(LOOP):
    optimizer.zero_grad()                # 勾配初期化PyTorchの約束事項
    y = model(x)                         # ネットワークモデルによる予測
    loss = criterion(y, t)               # 誤差計算PyTorchの約束事項
    acc  = accuracy(y,t)                 # 精度の計算
    print(f"{epoch}: loss: {loss.item()}, acc:{acc}")   # 損失と精度の表示
    loss.backward()                      # backward(微分する部分)
    optimizer.step()                     # パラメータ更新PyTorchの約束事項
~~~
forループで変数を更新することになります。損失の減少や精度を表すaccの値を観察しながら、学習回数の**LOOP** や学習率の**lr** を適宜変更することになります。

ここまでで、基本的な学習は終わりとなります。

### 平均精度について
accuracyの関数について簡単な説明を残しておきます。予測値と教師データで等しい値なら正解として、正解数/問題数で精度を求める単純な平均精度を求める作成してみました。forループよりも前に記述すればOKです。


~~~python: accuracy関数
def accuracy(y, t):
    _,argmax_list = torch.max(y, dim=1)
    accuracy = sum(argmax_list == t).item()/len(t)
    return accuracy
~~~
モデルの予測値$y$は３次元ベクトル。３つの中で一番大きな値を与える要素が、モデルが予想する正解となります。```torch.max(y, dim=1)```の部分です。この予測正解リストと本当の正解リスト$t$で等しい値の合計数つまり正解数を求めます。```sum(argmax_list==t)```です。最後に正解数/問題数で正答率を求めれば平均精度となります。


## 2.5 検証
2.1のデータ分割で作成したテストデータ **x_test** と **t_test** を利用して学習結果をテストしてみましょう。x_testをmodelに入れた値 y_test = model(x_test) が予測値となります。accuracyで平均精度を求めれば完成です。


~~~python: 検証
y_test = model(x_test)
acc = accuracy(y_test, t_test)
print("平均精度", acc)
# 平均精度 0.98
~~~
テストデータは50個なので、1個間違えがあるようです。学習回数などで結果は多少変わります。地味に一つずつ確認して表にまとめてみました。本当は「塩」なのに「あっさり」と予測している部分を発見！！精度に関する話題は別途用意する予定です。
~~~python: 個別の精度
            		prediction
        		あっさり こってり 	塩
true 	あっさり 	16    0     0
        こってり 	 0 	 20     0
        塩           1    0    13
~~~
# 次回
ニューラルネットワークを用いた分類問題といえば画像分類ではないでしょうか？次回は深層学習の名を世の中に広めた?CNN（Convolutional Neural Network）を使った画像分類を扱う予定です[^4]。

# 注

[^1]: ０，１，２という整数値についてです。0は0番目（あっさり）が確率1で起こる(1,0,0)、1は1番目（こってり）が確率1で起こる(0,1,0)という意味、2は2番目（塩）が確率1で起こる(0,0,1)と同等になります。3つ並んでいる数値が、左から順に「あっさり」「こってり」「塩」の確率を表していると考えてください。
[^2]: scikit-learnライブラリがない場合は、```pip install scikit-learn```などで別途インストールします。。
[^3]: 活性化関数のモジュール化などと呼ばれるらしい。
[^4]: CNNといえばCable News Networkですよね。だから略語やジャーゴンは嫌なんですよ:sweat_smile::smile::sweat_smile: