TadaoYamaokaの開発日記

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

将棋でディープラーニングする その50(ブートストラップ【訂正】)

以前に書いたブートストラップの説明に誤りがあったのと、Chainerで誤差逆伝播の効率化ができたので、追加記事を書きます。

間違っていた内容

以前に書いた記事で、2確率変数の交差エントロピーは、確率変数がシグモイド関数の場合、
\displaystyle
\begin{eqnarray*}
H(p, q) &=& - \sum_t p(t) \log q(t) \\
 &=& -p \log q - (1 - p) \log(1-q)
\end{eqnarray*}
で表され、偏微分が、
\displaystyle
\frac{\partial H(p, q)}{\partial w} = q - p
となることを説明したが、偏微分するのは確率変数qの方なので、ニューラルネットワークの出力をqにする必要があります。
それを教師データの評価値を勝率にした値をqに与えていたので、間違った計算をしていました。

使用していた

def cross_entropy(p, q):
    return F.mean(-p * F.log(q) - (1 - p) * F.log(1 - q))

を使って、pの方をニューラルネットワークの出力として、以下のパーセプトロンで学習すると一見lossは下がりますが、間違った値に収束します。

import numpy as np
import cupy as cp
import chainer
from chainer import cuda, Variable
from chainer import optimizers
from chainer import Chain
import chainer.functions as F
import chainer.links as L

class Model(Chain):
    def __init__(self):
        super(Model, self).__init__()
        with self.init_scope():
            self.l1 = L.Linear(2, 5)
            self.l2 = L.Linear(5, 1)

    def __call__(self, x):
        h1 = F.relu(self.l1(x))
        return self.l2(h1)

def cross_entropy(p, q):
    return F.mean(-p * F.log(q) - (1 - p) * F.log1p(-q))

model = Model()
model.to_gpu()

optimizer = optimizers.SGD()
optimizer.setup(model)

x = Variable(cp.array([[0.2, 0.8], [0.6, 0.4]], dtype=np.float32))
t = Variable(cp.array([[0.4], [0.6]], dtype=np.float32))

for i in range(10000):
    y = model(x)
    y2 = F.sigmoid(y)
    loss = cross_entropy(y2, t) # fault
    print('loss = ', loss.data)
    model.cleargrads()
    loss.backward()
    optimizer.update()

print('y = \n', y2.data)
print('loss = \n', loss.data)

学習後の値は

y =  [[ 0.04580102], [ 0.94369036]]

となりました。

pとqを逆にして、

    loss = cross_entropy(t, y2) # correct

とすると、

y =  [[ 0.40164709], [ 0.59817547]]

と、正しい値に収束します。

ということで、今まで間違った学習をしていましたヾ(*`Д´*)ノ"

誤差逆伝播の効率化

順伝播を計算して、微分はChainerの計算グラフの処理に任せていましたが、微分は上記で書いた
\displaystyle
\frac{\partial H(p, q)}{\partial w} = q - p
の式で計算できるので、これをChainerのFunctionで実装しました。

import numpy

from chainer import cuda
from chainer import function
from chainer.functions.activation import sigmoid
from chainer import utils
from chainer.utils import type_check

class SigmoidCrossEntropy2(function.Function):

    def __init__(self):
        pass

    def check_type_forward(self, in_types):
        type_check.expect(in_types.size() == 2)

        x_type, t_type = in_types
        type_check.expect(
            x_type.dtype == numpy.float32,
            t_type.dtype == numpy.float32,
            x_type.shape == t_type.shape
        )

    def forward(self, inputs):
        xp = cuda.get_array_module(*inputs)
        x, t = inputs

        loss = t * xp.log1p(xp.exp(-x)) - (1 - t) * (xp.log(xp.exp(-x)) - xp.log1p(xp.exp(-x)))

        count = max(1, len(x))
        self.count = count

        return utils.force_array(
            xp.divide(xp.sum(loss), self.count, dtype=x.dtype)),

    def backward(self, inputs, grad_outputs):
        xp = cuda.get_array_module(*inputs)
        x, t = inputs
        gloss = grad_outputs[0]
        y, = sigmoid.Sigmoid().forward((x,))
        gx = xp.divide(
            gloss * (y - t), self.count,
            dtype=y.dtype)
        return gx, None


def sigmoid_cross_entropy2(x, t):
    return SigmoidCrossEntropy2()(x, t)

ChainerのGitHubリポジトリのソースのSigmoidCrossEntropyを改造して作りました。
cudaの処理を書く必要があると思っていましたが、cupyの関数だけで実装できました。
この関数の入力は、logitsになるので、sigmoidの計算が不要で、

    y = model(x)
    loss = sigmoid_cross_entropy2(y, t)

と記述できます。

この関数に置き換えても、

y = [[ 0.40164712], [ 0.59817553]]

と、正しい値に収束することを確認しました。

将棋AIのバリューネットワークの損失をブートストラップ項のみにして、学習してみたところ、2000万局面の学習で、

一致率
修正前 0.7061
修正後 0.7537

となって、正しく学習できることを確認しました。
むしろ修正前がそれなりに学習できていたのが不思議です。Policyも同時に学習していた効果かもしれません。バリューのみでは測定していません。


ということで、一から学習をやり直しています。

2017/12/9 追記

ブートストラップ項の修正版で再学習を行いました。
4.9億局面学習したところで、GPSfishと1手3秒50局で、

対局数50 先手勝ち26(54%) 後手勝ち22(45%) 引き分け2
dlshogi
勝ち20(41%) 先手勝ち11(45%) 後手勝ち9(37%)
GPSfish 0.2.1+r2837 gcc 4.8.1 osl wordsize 32 gcc 4.8.1 64bit
勝ち28(58%) 先手勝ち15(62%) 後手勝ち13(54%)

となりました。
バグがあったバージョンよりも同じ学習局面数で、勝率が伸びています。
序盤の事前探索による定跡化なしでこの勝率なので、35億局面学習した電王トーナメント版のdlshogiよりも強くなっていそうです。