PyTorchを使って教師あり学習を実装する

## はじめに
前回『[教師あり学習とは](https://qiita.com/pippi_alola/items/5925f2d72621f0315e1e)』の続きです。今回は実践編です。

https://qiita.com/pippi_alola/items/5925f2d72621f0315e1e

scikit-learnに入っている、ワインのデータセットで教師あり学習を動かしてみます。

### ゴール
ワインの特徴（ 度数, 色, 成分, ... ）から、3種類のワインの銘柄（ 詳細は不明 ）を分類します。

<!--
参考（つーか丸パクリ）にしたのはこのサイトです
「Pandas DataFrameを徹底解説！(作成、行・列の追加と削除、indexなど)」
https://ai-inter1.com/pandas-dataframe_basic/
「わかりやすいPyTorch入門②（ニューラルネットワークによる分類）」
https://ex-ture.com/blog/2021/01/04/pytorch-nn/
-->

## 実装
### 実行環境
Google Colabでの実行を想定しています。詳しくはこちらを参照してください。

https://qiita.com/pippi_alola/private/cae35b60c81247590cbf


## ライブラリの読み込み

まずは使うライブラリを読み込みます。今回はNumPy, PyTorch, Pandas, scikit-learnを使います。

~~~Python
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import pandas as pd
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
~~~

### ライブラリのざっくり説明
・**NumPy**
数字を扱うときによく使います。いろいろな計算ができます。
・**PyTorch**
ニューラルネットワークのモデルが簡単に定義できます。
・**Pandas**
データフレームが使えます。万単位の大きなデータでもExcelのように扱えます。
・**scikit-learn**
機械学習に欲しい機能がいろいろ入っています。データセットもいろいろあります。

## データフレームの作成
~~~Python
wine = load_wine()
wine_df = pd.DataFrame(wine.data, columns=wine.feature_names)
wine_class = pd.DataFrame(wine.target, columns=["class"])
~~~

`load_wine()` でワインのデータを読み込みます。
`pd.DataFrame(読み込むデータ, index="行の名前", columns="列の名前")` でデータフレームを作成します。

**index** と **columns** は省略すると通し番号が振られます。
**wine_df** では **index**を省略しているため、行には0からの通し番号が振られています。
反対に、 **columns** には **wine.feature_names** が指定されています。

**wine.feature_names** の中身は、**['alcohol', 'malic_acid', ...]** のようなリストになっています。
下の図を見ての通り、リストがそのまま列の名前になっています。


![スクリーンショット (654).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3566695/89f7b212-00ae-0dce-dd0c-7638381297a2.png)

同様に、**wine_class** というデータフレームも作成。**columns** には **class**という名前をつけました。
**wine.target** にはワインの種類の情報が入っています。確かに、0, 1, 2 という3種類のワインがあるようです。

![スクリーンショット (656).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3566695/b2e3c0c8-b94d-1011-d3a0-5881bdaab690.png)

今更かもしれませんが、データフレームの行を **index**（インデックス）、列を **columns**（カラム）といいます。
行列の行と列はrowとcolumnsなんですが、謎です。


## 学習データの準備
~~~Python
wine_cat = pd.concat([wine_df, wine_class], axis=1)
wine_cat.drop(wine_cat[wine_cat["class"] == 2].index, inplace=True)
~~~

ここではデータフレームの結合と削除をやっています。

`pd.concat([データフレームA, データフレームB], axis = 1)` で、データフレームAとでデータフレームBを結合します。
`axis = 1` は横（右端）に縦方向の結合をさせるという意味です。
`axis = 0` なら縦（下端）にくっつくイメージです。

`pd.concat([wine_df, wine_class], axis = 1)` を実行すると、先ほどの **wine_df** の右端に **wine_class** が追加されます。それを **wine_cat** に代入します。

下の図をみると、先ほどの **wine_df** の右端に **class** が追加されていることがわかります。

![スクリーンショット (658).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3566695/16e36078-d2eb-fbe0-3760-a3a3365fb032.png)


<!--
データフレームの削除は以下を参考にしました。ありがとうございます。
『【pandas】行列の削除dropまとめ「複数条件・inplace・消えない理由」』
https://www.self-study-blog.com/dokugaku/pandas-dataframe-drop-inplace-errors-level/

**データフレーム名.drop()** でデータフレームを部分的に削除できます。**wine_cat** の削除は引数が少し複雑です。
`wine_cat[wine_cat["class"] == 2].index` で **wine_cat** の **class** が2である行を削除。
つまり、ワインの種類が0と1だけにしています。`inplace = True` にしないと消えないでそのままになります。
下の図の通り、**class** が0と1だけになっています。また、行数が178から130になっています。

![スクリーンショット (660).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3566695/d6c3deb9-06bf-76ba-344e-071c7a9d164e.png)
-->

### データセットの分割
~~~Python
wine_data = wine_cat.values[:, :13]
wine_target = wine_cat.values[:,13]
Train_X, Test_X, Train_Y, Test_Y = train_test_split(wine_data, wine_target, test_size=0.25)
~~~

`wine_cat.values[:, :13]` で **wine_cat** の12列目までのすべて（ 成分などの情報 ）を参照し、**wine_data** に代入。
`wine_cat.values[:,13]` は **wine_cat** の13列目（ 種類 ）を参照し、**wine_target** に代入しています。
`train_test_split(データ, ラベル, 検証データの数)` で学習用データとテスト用データを分割します。
`test_size=0.25` ということは、テスト用データが 25% で、75% が学習用データということです。

### PyTorchテンソルに変換
~~~Python
train_X = torch.FloatTensor(Train_X)
train_Y = torch.LongTensor(Train_Y)
test_X = torch.FloatTensor(Test_X)
test_Y = torch.LongTensor(Test_Y)

train = TensorDataset(train_X, train_Y)
test = TensorDataset(test_X, test_Y)
~~~

分割したデータセットをそれぞれ"学習用"と"テスト用"の変数に代入。学習用データは **train** に代入しておきます。

#### テンソルとは？
行列のようなものです。詳しくはこちらを参照してください。

「機械学習で使う数学を学ぶなら覚えておこう！「テンソル」とは」
https://aizine.ai/tensor-0917/

~~~Python
train_loader = DataLoader(train, batch_size=16, shuffle=False)
test_loader = DataLoader(test, batch_size=16, shuffle=False)
~~~

`DataLoader(データ, バッチサイズ, shuffle)` でデータの読み込み。
引数は順にデータ、バッチサイズ、シャッフルの有無です。
それを **train_loader** に代入しています。

#### バッチとは？
平たく言うと、「ひとつにまとめる」イメージです。バッチ処理とは、まとめて一気に処理するというイメージです。`batch_size=16` ということは、16個をまとめて扱います。


### ニューラルネットワークモデルの定義
~~~Python
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(13, 32)
        self.fc2 = nn.Linear(32, 8)
        self.fc3 = nn.Linear(8, 3)
    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x
net = Net()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001)
~~~

入力層, 隠れ層, 出力層の3層からなるニューラルネットワークを定義しています。

`self.fc1 = nn.Linear(13, 32)` の **13** は入力データの次元数です。ワインの特徴量（アルコール度数, 色, ...）が13種類あるため、13次元となっています。

`self.fc3 = nn.Linear(8, 3)` の **3** は出力データの次元数です。推定するワインの銘柄が3種類なので **3** となっています。

#### **criterion** とは？
誤差を計算する関数です。ここでは **CrossEntropyLoss**（ **交差エントロピー誤差** ）で誤差を計算します。
ほかには二乗和誤差などもあります。

#### **optimizer** とは？
最適化手法です。ここでは **SGD**（ **確率的勾配降下法** ）を使います。
画像の分類には　**Adam**（ **適応モーメント推定** ）なども使います。


`class Net(nn.Module):`
**nn.Module** はニューラルネットワークの親クラスです。それを継承した子クラスの **Net** クラスを定義します。


### 学習
~~~Python
for epoch in range(5000):
    total_loss = 0
    for train_x, train_label in train_loader:
        optimizer.zero_grad()
        loss = criterion(net(train_x), train_label)
        loss.backward()
        optimizer.step()
        total_loss += loss.data
    if(epoch+1)%1000 == 0:
        print(epoch+1, ":", total_loss)
~~~

ここの書き方はお作法的な側面が強いです。
`for epoch in range(5000):` で学習回数を指定しています。ここでは5000エポック学習します。
`if` 以下は誤差の出力です。1000回ずつ出力するよう設定しています。

### 検証
~~~Python
net.eval()

with torch.no_grad():
    correct = 0
    total = 0
    for train_x, train_label in train_loader:
        outputs = net(train_x)
        _, predicted = torch.max(outputs.data, 1)
        total += train_label.size(0)
        correct += (predicted == train_label).sum().item()

    train_accuracy = correct / total

with torch.no_grad():
    correct = 0
    total = 0
    for test_x, test_label in test_loader:
        outputs = net(test_x)
        _, predicted = torch.max(outputs.data, 1)
        total += test_label.size(0)
        correct += (predicted == test_label).sum().item()

    test_accuracy = correct / total

print(f"学習用データの正解率：{100 * train_accuracy:.2f}%")
print(f"テスト用データの正解率：{100 * test_accuracy:.2f}%")
~~~

`net.eval()` で学習モデルを検証モードにします。

学習用データとテスト用データの正解率をそれぞれ計算しています。

![スクリーンショット (1008).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3566695/24bf3a63-3ba7-ae76-6452-1b8970c69565.png)


私の環境で試した結果はこうなりました。
大体7割の正解率といったところでしょうか。

## おわりに
だいぶ駆け抜けた気がしますが、教師あり学習の実装はこのような流れになります。
次回は画像の分類（ MNIST ）をやってみます。

次回

https://qiita.com/pippi_alola/items/2a4e6ef56332848106c3