以前に書いたブートストラップの説明に誤りがあったのと、Chainerで誤差逆伝播の効率化ができたので、追加記事を書きます。
間違っていた内容
以前に書いた記事で、2確率変数の交差エントロピーは、確率変数がシグモイド関数の場合、
で表され、偏微分が、
となることを説明したが、偏微分するのは確率変数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の計算グラフの処理に任せていましたが、微分は上記で書いた
の式で計算できるので、これを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よりも強くなっていそうです。