人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

PyTorch (4) Logistic Regression

次は〜ロジスティック回帰(Logistic Regression)!ロジスティック回帰は、回帰とつくけど分類のアルゴリズムで、隠れ層がなく、活性化関数にシグモイド関数(2クラス分類のとき)、ソフトマックス関数(多クラス分類のとき)を使ったニューラルネットとしてモデル化できる。IrisとMNIST(Notebook参照)のデータセットで実装してみた。

Irisデータセット

import torch
import torch.nn as nn

import numpy as np
import matplotlib.pyplot as plt

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

PyTorchとともにscikit-learnの関数もいろいろ活用するのでインポート。

# hyperparameters
input_size = 4
num_classes = 3
num_epochs = 10000
learning_rate = 0.01

Irisデータセットは特徴量が4つ(sepal length、sepal width、petal length、petal width)なので入力ユニット数は4にした。またクラス数が3つ(Setosa、Versicolour、Virginica)なので出力ユニット数は3にした。

iris = load_iris()
X = iris.data
y = iris.target
print(X.shape)  # (150, 4)
print(y.shape)  # (150, )

データのロードはscikit-learnのload_iris()関数で簡単にできる。辞書で返ってくるが data でデータ本体が target でクラスラベルが取得できる。クラスラベルは1-of-Kになっていないので注意!PyTorchはクラスラベルを自分で1-of-Kに変換しなくてもクラスラベルのまま扱える。

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=5)
print(X_train.shape)  # (100, 4)
print(X_test.shape)   # (50, 4)
print(y_train.shape)  # (100, )
print(y_test.shape)   # (50, )

訓練データとバリデーションデータに分割。これもscikit-learnの関数を使えば簡単にできる。

# データの標準化
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
# print(np.mean(X_train, axis=0))  # [ -2.47274423e-15   3.85247390e-16  -4.26603197e-16  -7.66053887e-17]
# print(np.std(X_train, axis=0))   # [ 1.  1.  1.  1.]

データの各特徴量ごとに平均0、標準偏差1になるようにデータを標準化する。Irisデータでは標準化しなくても学習はできたけどやったほうが学習が安定すると思う。print してみると4つの特徴量それぞれで平均が0、標準偏差が1になってるのがわかる。

class LogisticRegression(nn.Module):

    def __init__(self, input_size, num_classes):
        super(LogisticRegression, self).__init__()
        self.linear = nn.Linear(input_size, num_classes)
    
    def forward(self, x):
        out = self.linear(x)
        return out

ロジスティック回帰モデルの定義。見た目は線形回帰のときとまったく同じ。実際、多クラスのロジスティック回帰は linear を通したあとに softmax を通すのだがPyTorchはモデルには含めないでロジットをそのまま返すのが流儀のようだ。なぜかというと損失関数を計算する torch.nn.CrossEntropyLoss の中に softmax の計算が含まれているため。

なぜこんな仕様なのか?と思って調べてみたら softmax が必要なのは訓練時の損失計算のときだけで、推論時には必要ないので入れない方が効率がよいとのこと。推論時はそもそも softmax を通してわざわざ確率にしなくてもロジットのまま大小比較ができるためだ。

Kerasだとモデルの最後に softmax の活性化関数も含めてモデル出力は確率にしていた。こんな感じで。

# Kerasの例
model = Sequential()
model.add(Dense(512, activation='relu', input_shape=(784,)))
model.add(Dropout(0.2))
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(num_classes, activation='softmax')) <= ここ!

なので推論のときもforwardするだけで確率で出てきた。

上のようなPyTorchのモデルだとforwardの出力は確率になってないので要注意!確率にしたいときは自分で nn.functional.softmax() を使う必要がある。上のForumにもあるようにPyTorchの動的グラフの特性をいかして訓練時と推論時で分けるのもよいかもね。

if self.training:
    # code for training
else:
    # code for inference

次はモデルのオブジェクトを作ってlossとoptimizerを定義。

model = LogisticRegression(input_size, num_classes)

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

確認のためnn.CrossEntropyLoss() のソースコードを見てみると softmax がちゃんと含まれているのが確認できる。

def cross_entropy(input, target, weight=None, size_average=True, ignore_index=-100, reduce=True):
    return nll_loss(log_softmax(input, 1), target, weight, size_average, ignore_index, reduce)

次はいよいよ訓練ループ!ここは前回と大体同じ。

def train(X_train, y_train):
    inputs = torch.from_numpy(X_train).float()
    targets = torch.from_numpy(y_train).long()
    
    optimizer.zero_grad()
    outputs = model(inputs)
    
    loss = criterion(outputs, targets)
    loss.backward()
    optimizer.step()
    
    return loss.item()

def valid(X_test, y_test):
    inputs = torch.from_numpy(X_test).float()
    targets = torch.from_numpy(y_test).long()

    outputs = model(inputs)
    val_loss = criterion(outputs, targets)
    
    # 精度を求める
    _, predicted = torch.max(outputs, 1)
    correct = (predicted == targets).sum().item()
    val_acc = float(correct) / targets.size(0)

    return val_loss.item(), val_acc

loss_list = []
val_loss_list = []
val_acc_list = []
for epoch in range(num_epochs):
    perm = np.arange(X_train.shape[0])
    np.random.shuffle(perm)
    X_train = X_train[perm]
    y_train = y_train[perm]
    
    loss = train(X_train, y_train)
    val_loss, val_acc = valid(X_test, y_test)
    
    if epoch % 1000 == 0:
        print('epoch %d, loss: %.4f val_loss: %.4f val_acc: %.4f'
              % (epoch, loss, val_loss, val_acc))
    
    # logging
    loss_list.append(loss)
    val_loss_list.append(val_loss)
    val_acc_list.append(val_acc)

いくつか注意点

  • エポックごとにデータをシャッフルする
  • PyTorchではデータは FloatTensor でラベルは LongTensor にする必要がある。Irisデータの特徴量は float64 (double) 型になっているため テンソルを float() でキャストする必要がある。ラベルはもともと int64 (long) 型なのでキャストは不要だったが念のため long() でキャスト
  • criterion = nn.CrossEntropyLoss に渡す正解ラベルは 1-of-Kにする必要がない! 0, 1, 2, 3というラベルのカテゴリのまま渡せる

最後にlossとaccのグラフ描くとこんな感じできれいに学習できているのが確認できた。

# plot learning curve
plt.figure()
plt.plot(range(num_epochs), loss_list, 'r-', label='train_loss')
plt.plot(range(num_epochs), val_loss_list, 'b-', label='val_loss')
plt.legend()

plt.figure()
plt.plot(range(num_epochs), val_acc_list, 'g-', label='val_acc')
plt.legend()

f:id:aidiary:20180203120031p:plain f:id:aidiary:20180203120049p:plain

参考