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()