TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

2値分類で中間の値も学習する

実験している将棋AIの学習では、今まで価値ネットワークの出力の活性化関数をシグモイドとして、勝ちと負けの2値で分類していた。
しかし、先日の世界コンピュータ将棋選手権に参加して、引き分けや千日手が結果を左右するゲームが多かったため、引き分けも教師にすべきと思い直した。
そこで、引き分けも学習できるように修正することにした。
(AlphaZeroのように出力をtanhにして損失をMSEにして学習することもできるが、確率的な事象のため交差エントロピーで学習した方がよいと思っている。)

Chainerで実装している学習処理では、損失をsigmoid_cross_entropyとしているため、そのままでは引き分けを学習できない。
交差エントロピーの定義通りに自分で計算する必要がある。

交差エントロピーの計算

ニューラルネットワークの出力の確率をp、教師データをtとすると交差エントロピーは以下の式で与えられる。
\displaystyle
\begin{align}
Loss(p, t) = -(t \log p + (1-t) \log(1-p))
\end{align}
これを、ロジットyを使って表すと、
\displaystyle
\begin{align}
Loss(y, t) = -(t (y - \log(e^y + 1)) - (1-t) \log(e^y + 1))
\end{align}
となる。

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

となり、正しく予測できている。