概要
個人的な備忘録を兼ねたPyTorchの基本的な解説とまとめです。第3回は分類問題になります。
機械学習の最初の一歩に、アヤメデータセットを用いたアヤメの分類問題があります。アヤメのデータセットは、入手のしやすさと扱いやすさで知られています。本記事では、アヤメデータセットのように数値で構成された表形式のデータで、クラスを識別する分類問題を扱います。
回帰問題からの変更点は大きく2箇所
- 最終層の全結合層の出力が分類数:Linear(in_features=___ , out_features=分類数)
- 損失関数がCrossEntropyLoss( )
「あっさり、こってり、塩」の3種類に分類された架空のラーメンデータを用いて分類問題の基本的なネットワークを構築していきます。
方針
- できるだけ同じコード進行
- できるだけ簡潔(細かい内容は割愛)
- 特徴量などの部分,あえて数値で記入(どのように変わるかがわかりやすい)
演習用のファイル(Githubからダウンロード)
1. 分類問題
分類問題とは、データをカテゴリー・クラスに振り分ける問題となります。画像の分類はその一例です。ニューラルネットワークを用いた分類では、最終層の全結合層を分類したいクラス数に設定し、出力値の大きさでどのクラスに属するのかを表すことが一般的です。今回は、ラーメンの脂質含有量、スープの濃度、塩分濃度などから「あっさり」「こってり」「塩」の3種類のラーメンに分類する問題を扱いつつ分類問題の実装方法をまとめていきます。
PyTorchによるプログラムの流れを確認します。基本的に下記の5つの流れとなります。Juypyter Labなどで実際に入力しながら進めるのがオススメ
- データの読み込みとtorchテンソルへの変換 (2.1)
- ネットワークモデルの定義と作成 (2.2)
- 誤差関数と誤差最小化の手法の選択 (2.3)
- 変数更新のループ (2.4)
- 検証 (2.5)
2. コードと解説
2.0 データについて
演習用に利用するデータはラーメンの種類を数値で分類した架空のデータとなります。
脂肪含有量、スープ濃度などを0〜10の間の数値で主観的に決めました
濃度、時間や量などすべて0〜10で評価されていますが、演習用ですので気にしないでください。実際のデータを利用する場合は、分類問題のデータセットを参考にしてください。以下の表は今回利用するCSVファイルの概要とサンプルです。
データの概要
変数名 | 意味 |
---|---|
脂質含有量 | 0〜10の数値 |
スープの濃度 | 0〜10の数値 |
後味の持続時間 | 0〜10の数値 |
透明度 | 0〜10の数値 |
ラーメンタイプ | あっさり:0、こってり:1、塩:2 |
サンプル
最初の5列までがラーメンのタイプを特徴づけるデータとなります。最終列の7列目が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個ほど用意してあります。簡単ながら特徴量を箱ひげ図で表してみました。きれいに分類できるようにデータが作られているかな
繰り返しになりますが、利用するデータは架空のものです。いよいよ分類問題のコードになります。
2.1 データの読み込みとtorchテンソルへの変換
まず利用するライブラリを読み込みます。
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で0,1,2という整数値の教師データです1。scikit-learnのtrain_test_splitを利用して、学習に利用するデータとテスト(検証)に利用するデータに分割します2。
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回の非線形回帰で利用したネットワークに近い構造をしています。
具体的には、脂質含有量、スープの濃度、塩分濃度、後味の持続時間、透明度の5種類の特徴量を入力してラーメンの種類を予測する形となります。
全結合層・線形層の書き方
nn.Linear(in_features, out_features, bias=True)
- in_features : 入力される特徴量の数
- out_features : 出力される特徴量の数
- bias : 定数項。定数項ありがデフォルト
詳細はPyTorchの公式ドキュメントに記載されています。
クラスを利用してネットワークを記述します。
(1) __init___()部分に利用するネットワーク名
(2) forward部分に実際の流れ
活性化関数を__init__()に記述する方法があります3。今回は__init__()に書く方法を採用しました。print(model)としたときに活性化関数も反映されているので見通しもよく、後々便利かと思います。
# ネットワークの定義
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()__に書かれているネットワーク層が上から順番にすべて出力されます。
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種類のラーメンの値を出力します。
- 5−−>10-->3 と特徴量の次元が変わっていきます。
2.3 誤差関数と誤差最小化の手法の選択
分類問題では予測値$y$ と教師データ$t$ (0, 1, 2のラーメンID)の誤差を計算するのに、クロスエントロピーを用いることが一般的なようです。PyTorchではCrossEntropyLoss() と記述します。
criterion = nn.CrossEntropyLoss() # 損失関数:cross_entropy
optimizer = torch.optim.Adam(model.parameters(), lr=0.05) # 学習率:lr=0.001がデフォルト
モデルの予測値はnn.Linear(10,3) であることから3次元のベクトルです。損失計算は、単なる3次元ベクトル$y$とラーメンIDを比較することになります。3次元のベクトル$y$のsoftmaxを求めることで予測値が確率の形で表現されます。一方、ラーメンIDは、0なら(1, 0, 0)のように確率1で「あっさり」が選ばれるように変形します。これらの操作によって予測値$y$と教師データ$t$の誤差が2つの確率ベクトルの差とみなせます。実装上はこの一連の作業が簡略化されてCrossEntropyLoss() を使うことで、criterion(y,t) として誤差計算が行われます。
- nn.CrossEntropyLoss() は予測値と実測値(教師データ)のクロスエントロピー損失。使用時は、criterion(x,t) とする。
- torch.optim.Adam() は誤差の最小値を求める方法の一つ
PyTorchで利用できるoptimizerについては、公式ドキュメントにたくさん書かれています。
2.4 変数更新のループ
LOOPで指定した回数、
- y=model(x) で予測値を求め、
- criterion(y, t) で指定した誤差関数を使い予測値と教師データの誤差を計算、
- 誤差が小さくなるようにoptimizerに従い全結合層の重みとバイアスをアップデート
します。
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です。
def accuracy(y, t):
_,argmax_list = torch.max(y, dim=1)
accuracy = sum(argmax_list == t).item()/len(t)
return accuracy
モデルの予測値$y$は3次元ベクトル。3つの中で一番大きな値を与える要素が、モデルが予想する正解となります。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で平均精度を求めれば完成です。
y_test = model(x_test)
acc = accuracy(y_test, t_test)
print("平均精度", acc)
# 平均精度 0.98
テストデータは50個なので、1個間違えがあるようです。学習回数などで結果は多少変わります。地味に一つずつ確認して表にまとめてみました。本当は「塩」なのに「あっさり」と予測している部分を発見!!精度に関する話題は別途用意する予定です。
prediction
あっさり こってり 塩
true あっさり 16 0 0
こってり 0 20 0
塩 1 0 13
次回
ニューラルネットワークを用いた分類問題といえば画像分類ではないでしょうか?次回は深層学習の名を世の中に広めた?CNN(Convolutional Neural Network)を使った画像分類を扱う予定です4。
注
-
0,1,2という整数値についてです。0は0番目(あっさり)が確率1で起こる(1,0,0)、1は1番目(こってり)が確率1で起こる(0,1,0)という意味、2は2番目(塩)が確率1で起こる(0,0,1)と同等になります。3つ並んでいる数値が、左から順に「あっさり」「こってり」「塩」の確率を表していると考えてください。 ↩
-
scikit-learnライブラリがない場合は、
pip install scikit-learn
などで別途インストールします。。 ↩ -
活性化関数のモジュール化などと呼ばれるらしい。 ↩
-
CNNといえばCable News Networkですよね。だから略語やジャーゴンは嫌なんですよ
↩