人工知能に関する断創録

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

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でデータを扱うにはDataSetDataLoader の2つのクラスが重要DataSetはデータセットのまとまりを表していて、DataLoaderDataSetをセットすることでミニバッチ単位でロードできるようになる。MNISTDataSetを継承した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() がとても便利!

f:id:aidiary:20180204094901p:plain

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

f:id:aidiary:20180203151926p:plain f:id:aidiary:20180203151933p:plain

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

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

参考

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

f:id:aidiary:20180128195728p:plain

  • 勾配(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

f:id:aidiary:20180131172614p:plain

参考