実験している将棋AIの学習では、今まで価値ネットワークの出力の活性化関数をシグモイドとして、勝ちと負けの2値で分類していた。
しかし、先日の世界コンピュータ将棋選手権に参加して、引き分けや千日手が結果を左右するゲームが多かったため、引き分けも教師にすべきと思い直した。
そこで、引き分けも学習できるように修正することにした。
(AlphaZeroのように出力をtanhにして損失をMSEにして学習することもできるが、確率的な事象のため交差エントロピーで学習した方がよいと思っている。)
Chainerで実装している学習処理では、損失をsigmoid_cross_entropyとしているため、そのままでは引き分けを学習できない。
交差エントロピーの定義通りに自分で計算する必要がある。
交差エントロピーの計算
ニューラルネットワークの出力の確率をp、教師データをtとすると交差エントロピーは以下の式で与えられる。
これを、ロジットyを使って表すと、
となる。
Chainerでは以下のように実装できる。
log1p_ey = F.log1p(F.exp(y))
loss = F.mean(-(t * (y - log1p_ey) - (1 - t) * log1p_ey))
※log1p_eyの部分はsoftplusでもよさそう。
MNISTで検証
sigmoid_cross_entropyと結果が同じになることをMNISTデータセットを使って検証する。
2値分類とするため、5より大きい場合を1、5以下を0として学習する。
sigmoid_cross_entropyによる実装
import numpy as np import chainer from chainer import Chain import chainer.functions as F import chainer.links as L from chainer import cuda, Variable from chainer import datasets, iterators, optimizers, serializers import argparse import random random.seed(0) np.random.seed(0) cuda.cupy.random.seed(0) class MLP(Chain): def __init__(self, n_units): super(MLP, self).__init__() with self.init_scope(): self.l1 = L.Linear(None, n_units) self.l2 = L.Linear(None, n_units) self.l3 = L.Linear(None, 1) def __call__(self, x): h1 = F.relu(self.l1(x)) h2 = F.relu(self.l2(h1)) return self.l3(h2) parser = argparse.ArgumentParser(description='example: MNIST') parser.add_argument('--batchsize', '-b', type=int, default=100, help='Number of images in each mini-batch') parser.add_argument('--epoch', '-e', type=int, default=20, help='Number of sweeps over the dataset to train') parser.add_argument('--unit', '-u', default=1000, type=int, help='number of units') parser.add_argument('--gpu', '-g', type=int, default=-1, help='GPU ID (negative value indicates CPU)') args = parser.parse_args() model = MLP(args.unit) if args.gpu >= 0: cuda.get_device_from_id(args.gpu).use() model.to_gpu() optimizer = optimizers.SGD() optimizer.setup(model) train, test = datasets.get_mnist() train_iter = iterators.SerialIterator(train, args.batchsize) test_iter = iterators.SerialIterator(test, args.batchsize, shuffle=False) def mini_batch(batch): x_data = [] t_data = [] for data in batch: x_data.append(data[0].reshape((1, 28, 28))) if data[1] > 5: t_data.append([1]) else: t_data.append([0]) x = Variable(cuda.to_gpu(np.array(x_data, dtype=np.float32))) t = Variable(cuda.to_gpu(np.array(t_data, dtype=np.int32))) return x, t for epoch in range(1, args.epoch + 1): sum_loss = 0 itr = 0 for i in range(0, len(train), args.batchsize): train_batch = train_iter.next() x, t = mini_batch(train_batch) y = model(x) model.cleargrads() # 損失計算 loss = F.sigmoid_cross_entropy(y, t) loss.backward() optimizer.update() sum_loss += loss.data itr += 1 sum_test_loss = 0 test_itr = 0 for i in range(0, len(test), args.batchsize): test_batch = test_iter.next() x_test, t_test = mini_batch(test_batch) y_test = model(x_test) #損失計算 loss_test = F.sigmoid_cross_entropy(y_test, t_test) sum_test_loss += loss_test.data test_itr += 1 print('epoch={}, train loss={}, test loss={}'.format( optimizer.epoch + 1, sum_loss / itr, sum_test_loss / test_itr)) optimizer.new_epoch()
交差エントロピーを計算した実装
差分箇所のみ
def mini_batch(batch): ... if data[1] > 5: t_data.append([1.0]) else: t_data.append([0.0]) ... t = Variable(cuda.to_gpu(np.array(t_data, dtype=np.float32))) ... # 損失計算 log1p_ey = F.log1p(F.exp(y)) loss = F.mean(-(t * (y - log1p_ey) - (1 - t) * log1p_ey)) ... #損失計算 log1p_ey_test = F.log1p(F.exp(y_test)) loss_test = F.mean(-(t_test * (y_test - log1p_ey_test) - (1 - t_test) * log1p_ey_test)) ...
比較
それぞれの実行結果を比較した。
ランダムシードを固定しているため、結果の損失は同じになるはずである。
sigmoid_cross_entropyによる実装
GPU: 0 # unit: 1000 # Minibatch-size: 100 # epoch: 20 epoch=1, train loss=0.46885258, test loss=0.33833 epoch=2, train loss=0.30148864, test loss=0.25789934 epoch=3, train loss=0.23578335, test loss=0.20502168 epoch=4, train loss=0.19030152, test loss=0.17037769 epoch=5, train loss=0.15915988, test loss=0.14761437 epoch=6, train loss=0.13844858, test loss=0.1335538 epoch=7, train loss=0.1238606, test loss=0.12371666 epoch=8, train loss=0.11283412, test loss=0.1147375 epoch=9, train loss=0.10427594, test loss=0.10852282 epoch=10, train loss=0.0973152, test loss=0.1036295 epoch=11, train loss=0.091520935, test loss=0.09843105 epoch=12, train loss=0.08628643, test loss=0.09522996 epoch=13, train loss=0.08197385, test loss=0.095566705 epoch=14, train loss=0.078243226, test loss=0.0893925 epoch=15, train loss=0.07424576, test loss=0.087877 epoch=16, train loss=0.07114731, test loss=0.086056426 epoch=17, train loss=0.06794335, test loss=0.083081216 epoch=18, train loss=0.06522623, test loss=0.081139676 epoch=19, train loss=0.062456455, test loss=0.08545701 epoch=20, train loss=0.0602174, test loss=0.07909738
交差エントロピーを計算した実装
GPU: 0 # unit: 1000 # Minibatch-size: 100 # epoch: 20 epoch=1, train loss=0.46885142, test loss=0.33832955 epoch=2, train loss=0.30149198, test loss=0.25790796 epoch=3, train loss=0.23578809, test loss=0.20502794 epoch=4, train loss=0.1903022, test loss=0.17038527 epoch=5, train loss=0.15915494, test loss=0.14760974 epoch=6, train loss=0.13844547, test loss=0.13355586 epoch=7, train loss=0.12385938, test loss=0.12372275 epoch=8, train loss=0.11283637, test loss=0.11474205 epoch=9, train loss=0.10427626, test loss=0.10850772 epoch=10, train loss=0.09731431, test loss=0.10361102 epoch=11, train loss=0.09150928, test loss=0.09841354 epoch=12, train loss=0.08627809, test loss=0.09522151 epoch=13, train loss=0.08196366, test loss=0.095563896 epoch=14, train loss=0.07823428, test loss=0.089379594 epoch=15, train loss=0.07423272, test loss=0.08784414 epoch=16, train loss=0.07113342, test loss=0.085993215 epoch=17, train loss=0.06792906, test loss=0.08306415 epoch=18, train loss=0.06520847, test loss=0.081117764 epoch=19, train loss=0.062452678, test loss=0.085387066 epoch=20, train loss=0.060215034, test loss=0.079077914
小さな誤差があるが、ほぼ同じ結果になっている。
中間の値を学習する
中間の値を学習するには、交差エントロピーの教師データに0.5を与えればよい。
教師データを0.5にしても学習できるか確認するため、MNISTの学習を変更して、5より大きい場合1.0、5の場合0.5、5より小さい場合0.0として学習して検証する。
差分箇所のみ
def mini_batch(batch): ... if data[1] > 5: t_data.append([1.0]) elif data[1] < 5: t_data.append([0.0]) else: t_data.append([0.5]) ...
学習結果は、以下のようになった。
GPU: 0 # unit: 1000 # Minibatch-size: 100 # epoch: 20 epoch=1, train loss=0.50217974, test loss=0.3803524 epoch=2, train loss=0.34845084, test loss=0.30468062 epoch=3, train loss=0.28511474, test loss=0.25564748 epoch=4, train loss=0.24253067, test loss=0.22479586 epoch=5, train loss=0.21511348, test loss=0.20560989 epoch=6, train loss=0.19752434, test loss=0.19374746 epoch=7, train loss=0.18512638, test loss=0.18558586 epoch=8, train loss=0.17562735, test loss=0.17736126 epoch=9, train loss=0.16807537, test loss=0.17147815 epoch=10, train loss=0.16175319, test loss=0.16750234 epoch=11, train loss=0.15649119, test loss=0.16236024 epoch=12, train loss=0.15169033, test loss=0.15876935 epoch=13, train loss=0.1475925, test loss=0.16045952 epoch=14, train loss=0.14412253, test loss=0.15328114 epoch=15, train loss=0.14068681, test loss=0.152177 epoch=16, train loss=0.13768907, test loss=0.15108886 epoch=17, train loss=0.1348263, test loss=0.1473368 epoch=18, train loss=0.13233264, test loss=0.14487615 epoch=19, train loss=0.13000856, test loss=0.14718775 epoch=20, train loss=0.12774076, test loss=0.14208344
テストデータの5の画像データに対して、0.5を出力できるか確認した。
x_data = [] for data in test: if data[1] == 5: x_data.append(data[0].reshape((1, 28, 28))) x = Variable(cuda.to_gpu(np.array(x_data, dtype=np.float32))) y = model(x) p = F.sigmoid(y) print(F.mean(p))
0.5094662
平均すると正しく0.5を予測できている。
5より大きい値(たとえば6)だと、
0.9532776
5より小さい値(たとえば3)だと、
0.055663988
となり、正しく予測できている。