PyTorch (5) Multilayer Perceptron
今回は多層パーセプトロンでMNIST。おなじみ。
import torch import torch.nn as nn import torchvision import torchvision.datasets as dsets import torchvision.transforms as transforms # Hyperparameters input_size = 784 hidden_size = 500 num_classes = 10 num_epochs = 50 batch_size = 100 learning_rate = 0.001
入力層は28 x 28 = 784ユニット、隠れ層が500ユニット、出力層が0-9の10クラスという構成。
# MNIST Dataset (Images and Labels) train_dataset = dsets.MNIST(root='./data', train=True, transform=transforms.ToTensor(), download=True) test_dataset = dsets.MNIST(root='./data', train=False, transform=transforms.ToTensor()) # Dataset Loader (Input Pipline) train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True) test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False)
MNISTはPyTorchの標準機能で読み込める。PyTorchでデータを扱うにはDataSet
とDataLoader
の2つのクラスが重要。DataSet
はデータセットのまとまりを表していて、DataLoader
にDataSet
をセットすることでミニバッチ単位でロードできるようになる。MNIST
はDataSet
を継承したMNISTデータセットを扱うための組み込みクラス。カスタムデータセットを作成する方法はあとで試そう。
transforms
を使うといろいろなデータ前処理ができる。ここでは読み込んだ画像データ(PIL.Image.Image
)をテンソルに変換する ToTensor()
だけを指定。
print(len(train_dataset)) # 60000 print(len(test_dataset)) # 10000 print(len(train_loader)) # 600 print(len(test_loader)) # 100
DataSetのlenはサンプル数を返し、DataLoaderのlenはミニバッチ数を返すので注意!
# 1データだけ取得 image, label = iter(train_loader).next() print(type(image)) # <class 'torch.FloatTensor'> print(type(label)) # <class 'torch.LongTensor'> print(image.size()) # torch.Size([100, 1, 28, 28]) print(label.size()) # torch.Size([100])
<class 'torch.Tensor'> <class 'torch.Tensor'> torch.Size([100, 1, 28, 28]) torch.Size([100])
DataLoader
から1バッチ分のデータを取り出すには iter()
で囲んでから next()
を呼び出す。テストしたいときに便利!
可視化のコード。
# 可視化 import matplotlib.pyplot as plt import numpy as np %matplotlib inline def imshow(img): npimg = img.numpy() # [c, h, w] => [h, w, c] plt.imshow(np.transpose(npimg, (1, 2, 0))) images, labels = iter(train_loader).next() images, labels = images[:25], labels[:25] imshow(torchvision.utils.make_grid(images, nrow=5, padding=1)) plt.axis('off')
画像をブロック上に配置する make_grid()
がとても便利!
class MultiLayerPerceptron(nn.Module): def __init__(self, input_size, hidden_size, num_classes): super(MultiLayerPerceptron, self).__init__() self.fc1 = nn.Linear(input_size, hidden_size) self.relu = nn.ReLU() self.fc2 = nn.Linear(hidden_size, num_classes) def forward(self, x): # print(x.size()) out = self.fc1(x) # print(out.size()) out = self.relu(out) # print(out.size()) out = self.fc2(out) # print(out.size()) return out
多層パーセプトロンを構築。PyTorchはモデル構築の方法もいろいろある。Kerasのように Sequential
を使ったり、Relu
をFunctionにしたり。今後はいろいろ試していこう。
forward()
の処理では途中結果を普通に print
できるのでデバッグがとてもやりやすい。下のように1バッチ分をモデルに入力して途中出力のサイズを確認したりできる。
model = MultiLayerPerceptron(input_size, hidden_size, num_classes) # テスト image, label = iter(train_loader).next() print("befire view:", image.size()) image = image.view(-1, 28 * 28) print("after view:", image.size()) output = model(image) print(output.size())
上で view
はNumPyの reshape
みたいな関数。出力は
befire view: torch.Size([100, 1, 28, 28]) after view: torch.Size([100, 784]) torch.Size([100, 10])
あとはこれまでとほとんど一緒。
criterion = nn.CrossEntropyLoss() optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate) def train(train_loader): model.train() running_loss = 0 for batch_idx, (images, labels) in enumerate(train_loader): images = images.view(-1, 28 * 28) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) running_loss += loss.item() loss.backward() optimizer.step() train_loss = running_loss / len(train_loader) return train_loss def valid(test_loader): model.eval() running_loss = 0 correct = 0 total = 0 with torch.no_grad(): for batch_idx, (images, labels) in enumerate(test_loader): images = images.view(-1, 28 * 28) outputs = model(images) loss = criterion(outputs, labels) running_loss += loss.item() _, predicted = torch.max(outputs, 1) correct += (predicted == labels).sum().item() total += labels.size(0) val_loss = running_loss / len(test_loader) val_acc = float(correct) / total return val_loss, val_acc loss_list = [] val_loss_list = [] val_acc_list = [] for epoch in range(num_epochs): loss = train(train_loader) val_loss, val_acc = valid(test_loader) 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)
running_lossの計算はちょっと注意。PyTorchの損失関数のデフォルトは、size_average=True
になっている。つまり、蓄積したlossをミニバッチのサンプル数で割った平均が返される仕様になっている。そのため、ミニバッチ単位でlossを蓄積していって最後にtrain_loss
を計算するときはミニバッチ数 len(train_loader)
で割って平均とする。
ここらへんは人によって実装方法が違う。たとえば、Transfer Learning Tutorialの実装だとlossをサンプル数倍したものをrunning_loss
に蓄積していって最後にミニバッチ数ではなく、サンプル数 len(train_set)
で割っている。
あとrunning_lossに加えるときはtensorのまま加えずに loss.item()
としてテンソルから数値に変換してから加えるようにしたほうがよいようだ。
評価時は勾配は不要なので with torch.no_grad()
をつける。
こんな感じでログが出力される。
epoch 0, loss: 2.2276 val_loss: 2.1488 val_acc: 0.5230 epoch 1, loss: 2.0729 val_loss: 1.9791 val_acc: 0.7124 epoch 2, loss: 1.8919 val_loss: 1.7798 val_acc: 0.7401 epoch 3, loss: 1.6870 val_loss: 1.5648 val_acc: 0.7583 epoch 4, loss: 1.4785 val_loss: 1.3595 val_acc: 0.7808 epoch 5, loss: 1.2890 val_loss: 1.1827 val_acc: 0.8003 epoch 6, loss: 1.1307 val_loss: 1.0397 val_acc: 0.8160 epoch 7, loss: 1.0047 val_loss: 0.9280 val_acc: 0.8243 epoch 8, loss: 0.9059 val_loss: 0.8407 val_acc: 0.8337 epoch 9, loss: 0.8282 val_loss: 0.7718 val_acc: 0.8405 epoch 10, loss: 0.7664 val_loss: 0.7166 val_acc: 0.8482
グラフ化。
import matplotlib.pyplot as plt %matplotlib inline # 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.xlabel('epoch') plt.ylabel('loss') plt.grid() plt.figure() plt.plot(range(num_epochs), val_acc_list, 'g-', label='val_acc') plt.legend() plt.xlabel('epoch') plt.ylabel('acc') plt.grid()
そろそろGPUないときつくなってきたので次回からGPU使おう!
参考
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()
参考
PyTorch (3) Linear Regression
まずは基本ということで線形回帰(Linear Regression)から。人工データとBoston house price datasetを試してみた。まだ簡単なのでCPUモードのみ。GPU対応はまた今度。
人工データセット
import torch import torch.nn as nn import numpy as np import matplotlib.pyplot as plt # hyper parameters input_size = 1 output_size = 1 num_epochs = 60 learning_rate = 0.001
データセット作成
# toy dataset # 15 samples, 1 features x_train = np.array([3.3, 4.4, 5.5, 6.71, 6.93, 4.168, 9.779, 6.182, 7.59, 2.167, 7.042, 10.791, 5.313, 7.997, 3.1], dtype=np.float32) y_train = np.array([1.7, 2.76, 2.09, 3.19, 1.694, 1.573, 3.366, 2.596, 2.53, 1.221, 2.827, 3.465, 1.65, 2.904, 1.3], dtype=np.float32) x_train = x_train.reshape(15, 1) y_train = y_train.reshape(15, 1)
nn.Linear
への入力は (N,∗,in_features)
であるため reshape
が必要。*
には任意の次元を追加できるが今回は1次元データなのでない。
モデルを構築
# linear regression model class LinearRegression(nn.Module): def __init__(self, input_size, output_size): super(LinearRegression, self).__init__() self.linear = nn.Linear(input_size, output_size) def forward(self, x): out = self.linear(x) return out model = LinearRegression(input_size, output_size)
PyTorchのモデルはChainerと似ている。
- nn.Module を継承したクラスを作成
__init__()
に層オブジェクトを定義forward()
に順方向の処理
LossとOptimizer
# loss and optimizer
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
- 線形回帰なので平均二乗誤差(mean squared error)
- OptimizerはもっともシンプルなStochastic Gradient Descentを指定
訓練ループ
# train the model for epoch in range(num_epochs): inputs = torch.from_numpy(x_train) targets = torch.from_numpy(y_train) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, targets) loss.backward() optimizer.step() if (epoch + 1) % 10 == 0: print('Epoch [%d/%d], Loss: %.4f' % (epoch + 1, num_epochs, loss.item())) # save the model torch.save(model.state_dict(), 'model.pkl')
- 各エポックでは
zero_grad()
で勾配をクリアすること! - パラメータは
optimizer.step()
で更新される - 10エポックごとに訓練lossを表示する
- 最後にモデルを保存
訓練ループはもっと洗練させないと実用的ではないな。
実行結果
Epoch [10/100], Loss: 1.4917 Epoch [20/100], Loss: 0.3877 Epoch [30/100], Loss: 0.2065 Epoch [40/100], Loss: 0.1767 Epoch [50/100], Loss: 0.1719 Epoch [60/100], Loss: 0.1710 Epoch [70/100], Loss: 0.1709 Epoch [80/100], Loss: 0.1709 Epoch [90/100], Loss: 0.1709 Epoch [100/100], Loss: 0.1708
最後に訓練データと予測した直線を描画してみよう。
# plot the graph predicted = model(torch.from_numpy(x_train)).detach().numpy() plt.plot(x_train, y_train, 'ro', label='Original data') plt.plot(x_train, predicted, label='Fitted line') plt.legend() plt.show()
- 勾配(grad)を持っているTensorはそのままnumpy arrayに変換できない。
detach()
が必要
RuntimeError: Can't call numpy() on Variable that requires grad. Use var.detach().numpy() instead.
Boston house price dataset
次は家の価格のデータセットもやってみよう。13個の特徴量をもとに家の値段を予測する。入力層が13ユニットで出力層が1ユニットの線形回帰のネットワークを書いた。
import torch import torch.nn as nn import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import load_boston from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler import matplotlib.pyplot as plt # hyper parameters input_size = 13 output_size = 1 num_epochs = 5000 learning_rate = 0.01 boston = load_boston() X = boston.data y = boston.target X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.33, random_state = 5) # データの標準化 scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) y_train = np.expand_dims(y_train, axis=1) y_test = np.expand_dims(y_test, axis=1) # linear regression model class LinearRegression(nn.Module): def __init__(self, input_size, output_size): super(LinearRegression, self).__init__() self.linear = nn.Linear(input_size, output_size) def forward(self, x): out = self.linear(x) return out model = LinearRegression(input_size, output_size) # loss and optimizer criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) def train(X_train, y_train): inputs = torch.from_numpy(X_train).float() targets = torch.from_numpy(y_train).float() 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).float() outputs = model(inputs) val_loss = criterion(outputs, targets) return val_loss.item() # train the model loss_list = [] val_loss_list = [] for epoch in range(num_epochs): # data shuffle 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 = valid(X_test, y_test) if epoch % 200 == 0: print('epoch %d, loss: %.4f val_loss: %.4f' % (epoch, loss, val_loss)) loss_list.append(loss) val_loss_list.append(val_loss) # plot learning curve 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()
- データを平均0、標準偏差1に標準化すると結果が安定する
- テストデータには訓練データでfitしたscalerを適用する
train()
とvalid()
をそれぞれ関数として独立させた。このようにまとめると訓練ループがすっきりするのでよいかも。- 200エポックごとにログを出力した
実験結果
epoch 0, loss: 582.9910 val_loss: 594.2480 epoch 200, loss: 453.9804 val_loss: 479.6869 epoch 400, loss: 373.9557 val_loss: 402.7326 epoch 600, loss: 308.8472 val_loss: 337.8119 epoch 800, loss: 253.5647 val_loss: 281.1577 epoch 1000, loss: 206.5357 val_loss: 232.3899 epoch 1200, loss: 166.8685 val_loss: 191.0127 epoch 1400, loss: 133.7838 val_loss: 156.2874 epoch 1600, loss: 106.5488 val_loss: 127.4714 epoch 1800, loss: 84.4694 val_loss: 103.8716 epoch 2000, loss: 66.8853 val_loss: 84.8388 epoch 2200, loss: 53.1687 val_loss: 69.7598 epoch 2400, loss: 42.7244 val_loss: 58.0542 epoch 2600, loss: 34.9919 val_loss: 49.1742 epoch 2800, loss: 29.4506 val_loss: 42.6082 epoch 3000, loss: 25.6266 val_loss: 37.8878 epoch 3200, loss: 23.0996 val_loss: 34.5939 epoch 3400, loss: 21.5107 val_loss: 32.3652 epoch 3600, loss: 20.5666 val_loss: 30.9023 epoch 3800, loss: 20.0406 val_loss: 29.9688 epoch 4000, loss: 19.7679 val_loss: 29.3874 epoch 4200, loss: 19.6375 val_loss: 29.0325 epoch 4400, loss: 19.5806 val_loss: 28.8196 epoch 4600, loss: 19.5581 val_loss: 28.6940 epoch 4800, loss: 19.5501 val_loss: 28.6216