PyTorchと人工知能と私~線形分類編

敢えて線形分類をやってみる

線形分類の問題は、機械学習フレームワークのscikit-Learnを使えば割と簡単にできてしまいますが、ここは敢えてPyTorchを使ってやってみたいとおもいます。

線形分類で予測するのはお馴染みのアヤメデータです。

まずは、ライブラリとアヤメのCSVデータを読み込んでいきます。

import pandas as pd
import numpy as np

from sklearn.model_selection import train_test_split

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
import torch.optim as optim

#アヤメデータのcSVをpandasで読み込み、データとラベルの変数に分解する。
csv = pd.read_csv('iris.csv')
data_csv = csv[['SepalLength','SepalWidth','PetalLength','PetalWidth']]
label_csv = csv[['Name']]

#アヤメはカテゴリとして整数で分類してnumpyのarrayに格納する。
Y = np.array(label_csv['Name'].astype('category').cat.codes).astype(int)
#データをnumpyのarrayに格納する。
X = np.array(data_csv)

#学習用データとテスト用データを分ける。
train_data,test_data,train_label,test_label = train_test_split(X,Y,test_size=0.3,random_state=0)

#データをPyTorchのテンソルに変換する。
train_x = torch.Tensor(train_data).requires_grad_(True)
test_x = torch.Tensor(test_data).requires_grad_(True)
train_y = torch.LongTensor(train_label)
test_y = torch.LongTensor(test_label)

次に勾配計算のオプションを設定していきます。今回の勾配計算では、確率的勾配降下法(Stochastic Gradient Descent)を使用します。そのため、ミニバッチサイズを設定したデータローダーを作成していきます。

#データ部とラベル部をマージしたデータセットを作成する。
dset_train = TensorDataset(train_x, train_y)
dset_test = TensorDataset(test_x, test_y)

#ミニバッチサイズを指定したデータローダーを作成する。
batch_train = DataLoader(dset_train, batch_size=5, shuffle=True)
batch_test = DataLoader(dset_test, batch_size=5, shuffle=False)

ミニバッチを使用した学習をミニバッチ学習といいます。ミニバッチ学習では、ランダム(確率的)にデータをいくつかピックアップして、それらに対して重みの更新を行います。このような勾配降下法を確率的勾配降下法(SGD)と言います。

線形分類を行う計算グラフを定義してから、勾配計算オプションを設定します。
今回のトライアルでは、勾配計算のハイパーパラメータはデフォルトにします。「lr=0.01」は学習率(Learning Ratio)です。学習率は、勾配降下法によりどの程度の大きさで重みの更新を行うかを設定するパラメータです。

学習率を大きくすると勾配計算は速く進みますが、発散する危険性が高くなります。また、学習率を小さくすると発散する危険性は低くなりますが、勾配計算は遅くなります。学習率の大きさに決まりはありません。学習時の正解率や汎化性能を見ながら、学習率の大きさをチューニングしていけばよいでしょう。

損失関数は、交差エントロピーを使用します。3種類のアヤメを分類するので多クラス分類になります。そのため、通常はsoftmax関数を使って確率で求めるのですが、PyTorchのnn.CrossEntropyLossは中にsoftmax計算が含まれているので、計算グラフの定義の中ではsoftmax関数を個別に定義しません。

#乱数発生シードを固定する。
torch.manual_seed(0)

#計算グラフの定義
class Network(nn.Module):
  def __init__(self):
    super(Network,self).__init__()
    self.slayer = nn.Linear(4, 1)

  def forward(self,x):
    x = self.slayer(x)
    return x

model = Network()

#勾配計算のオプション設定
optimizer = optim.SGD(model.parameters(), lr=0.01)

#損失関数を定義
loss_func = nn.CrossEntropyLoss()

学習してみる

では、実際にミニバッチ学習を実行してみましょう。

#配列と変数の初期化
train_loss = []
train_accu = []
num_iter = 0

#学習モードに設定
model.train()
for eoch in range(10):
  #ミニバッチごとに学習する
  correct = 0
  for data, target in batch_train:
    #勾配初期化
    optimizer.zero_grad
    #データを流して学習する(順伝搬)
    output = model(data) 
    #損失(誤差)計算
    loss = loss_func(output, target)
    #逆伝搬
    loss.backward()
    #重み更新
    optimizer.step()

    prediction = torch.max(output, 1)[1]
    correct += prediction.eq(target.data).sum().numpy()
    accuracy = correct / len(batch_train.dataset)

  if num_iter % 2 == 0:
    #print('学習ステップ: {}\t損失コスト: {:.3f}\t正解率: {:.3f}'.format(num_iter, loss.data.item(), accuracy))
    print('学習ステップ: {}\t損失コスト: {:.5f}\t正解率: {:.3f}%'.format(num_iter, loss.data.item(), accuracy * 100))

  num_iter += 1

print('学習ステップ: {}\t損失コスト: {:.3f}\t正解率: {:.3f}%'.format(num_iter, loss.data.item(), accuracy * 100))

ミニバッチの単位で10回の反復学習を行った結果、正解率は93%に到達しました。

学習ステップ: 0 損失コスト: 0.00308 正解率: 73.333%
学習ステップ: 2 損失コスト: 0.00017 正解率: 96.190%
学習ステップ: 4 損失コスト: 3.28668 正解率: 95.238%
学習ステップ: 6 損失コスト: 0.00000 正解率: 96.190%
学習ステップ: 8 損失コスト: 9.70090 正解率: 94.286%
学習ステップ: 10 損失コスト: 0.000 正解率: 93.333%

汎化性能を確認する

最後にテスト用データを使用して、汎化性能を確認してみます。

#推論モードに設定
model.eval()

outputs = model(Variable(test_x))
prediction = torch.max(outputs, 1)[1]

print('正解率: {:.3f}%'.format(prediction.eq(test_y).sum().numpy() / len(prediction) * 100))

正解率: 95.556%

テストデータによる推論の正解率は95%に達しているので、汎化性能としては十分な結果です。