TadaoYamaokaの日記

山岡忠夫 Home で公開しているプログラムの開発ネタを中心に書いていきます。

将棋でディープラーニングする その51(ディリクレノイズ)

電王トーナメント版のdlshogiでは、Policyの読み漏れを回避するために、自分の手番の局面だけ、Policyの予測するそれぞれの手について1/1000の確率で値を1.5倍にするということを行っていた。
自分の手番の局面だけにしたのは、相手の局面にもノイズを入れると、相手番で楽観的な手を選びやすくなり頓死しやすくなる傾向があったためである。
MCTSは並列で探索するため探索する各スレッドの探索する手をばらけさせる効果もあり、ノイズなしより少しノイズを入れた方が強くなることを確認していた。

その後、ルート局面のみ1/500の確率でランダムでノイズを付加して、
(1-\epsilon)*p(a) + \epsilon * 1.0, \epsilon=0.5
という式で、ランダムで全ての手が選ばれやすくすることを行っていた。
これもGPSfish相手に勝率を上げる効果があった。

AlphaGo Zeroでは、ルート局面のみにディリクレノイズを加えることを行っている。
そこで、ルート局面のノイズの付加方法をディリクレノイズに変更して、GPSfish相手に勝率が上がるか試してみた。

変更前

前に発見したブートストラップ項のバグを改修したバージョンで、elmoで生成した5.8億局面を学習したモデルで、GPSfishと1手3秒50回対局で勝率47%となっている。

対局数50 先手勝ち17(36%) 後手勝ち29(63%) 引き分け4
dlshogi
勝ち22(47%) 先手勝ち7(33%) 後手勝ち15(60%)
GPSfish 0.2.1+r2837 gcc 4.8.1 osl wordsize 32 gcc 4.8.1 64bit
勝ち24(52%) 先手勝ち10(40%) 後手勝ち14(66%)

ディリクレノイズに変更後

定数にAlphaZeroの論文と同じ値を使用し、
P(x,a)=(1-\epsilon)p_a + \epsilon \eta_a, \eta \sim Dir(0.15), \epsilon=0.25
として、GPSfishと対局させた結果は以下の通り。

対局数17 先手勝ち10(62%) 後手勝ち6(37%) 引き分け1
dlshogi
勝ち4(25%) 先手勝ち3(37%) 後手勝ち1(12%)
GPSfish 0.2.1+r2837 gcc 4.8.1 osl wordsize 32 gcc 4.8.1 64bit
勝ち12(75%) 先手勝ち7(87%) 後手勝ち5(62%)

明らかに弱くなったので、対局を途中で打ち切った。

次に、\epsilon=0.1として、対局を行った結果は以下の通り。

対局数20 先手勝ち9(47%) 後手勝ち10(52%) 引き分け1
dlshogi
勝ち10(52%) 先手勝ち5(50%) 後手勝ち5(55%)
GPSfish 0.2.1+r2837 gcc 4.8.1 osl wordsize 32 gcc 4.8.1 64bit
勝ち9(47%) 先手勝ち4(44%) 後手勝ち5(50%)

20回対局で、GPSfishに52%の勝率となった。

今度は、\epsilon=0にして対局を行った結果は以下の通り。

対局数20 先手勝ち10(52%) 後手勝ち9(47%) 引き分け1
dlshogi
勝ち5(26%) 先手勝ち3(30%) 後手勝ち2(22%)
GPSfish 0.2.1+r2837 gcc 4.8.1 osl wordsize 32 gcc 4.8.1 64bit
勝ち14(73%) 先手勝ち7(77%) 後手勝ち7(70%)

勝率が下がった。

考察

\epsilonを増やしすぎても、減らしすぎても勝率は低くなっている。
AlphaZeroの論文と同じ値だと勝率は低くなった。
1手3秒という試行時間では、ノイズにより探索の幅が広がったのに対して深さが足りなくなったためと思われる。
これは、「探索と活用のジレンマ」という問題で説明できる。
幅と深さはどちらかを増やせば、どちらかが減る関係にある。
UCTアルゴリズムでは理論的には適切なバランスに調整されるはずだが、現実的な思考時間では、深さを優先した探索が必要になる。
PUCTアルゴリズムではPolicyの遷移確率に従って優先度を調整する。
Policyの精度は完全ではないため、浅いトラップのあるゲームでは読み漏れがあると、すぐに頓死する。
ノイズを加えることで、ある程度読み漏れを防ぐことができる。
ノイズを増やしすぎると幅に対して深さが足りなくなるため、行動価値の予測精度が落ちる。
思考時間に応じた適切なバランスを見つける必要がある。

今回の結果で、モデルの精度だけではなく探索のハイパーパラメータの調整によっても勝率が大きく変わることが分かったので、今後調整していきたい。
ベイズ最適化まで行おうとすると相当な対局数が必要になるので、気長にやっていくことにする。

C++でディリクレ分布による乱数生成

C++にディリクレ分布で乱数生成する標準関数は用意されていない。

ガンマ分布で乱数生成する標準関数std::gamma_distributionが用意されているので、
Dirichlet distribution - Wikipedia
に書かれている方法を使って、ガンマ分布で乱数y1,...,yKを生成し、
x_i=\frac{y_i}{\sum_{j=1}^K y_j}
で、ディリクレ分布の乱数に変換できる。

これを実装した。

#include <iostream>
#include <random>
#include <vector>
#include <algorithm>

void random_dirichlet(std::mt19937_64 &mt, std::vector<double> &x, const double alpha) {
	std::gamma_distribution<double> gamma(alpha, 1.0);
	
	double sum_y = 0;
	for (int i = 0; i < x.size(); i++) {
		double y = gamma(mt);
		sum_y += y;
		x[i] = y;
	}
	std::for_each(x.begin(), x.end(), [sum_y](double &v) { v /= sum_y; });
}

int main() {
	std::random_device rd;
	std::mt19937_64 mt(rd());
	const int K = 5;
	const double alpha = 0.15;

	for (int i = 0; i < 10; i++) {
		std::vector<double> x(K);
		random_dirichlet(mt, x, alpha);

		for (int j = 0; j < x.size(); j++) {
			std::cout << x[j];
			if (j < x.size() - 1) std::cout << ", ";
		}
		std::cout << std::endl;
	}
}
実行結果

K=5, alpha=0.15の対称ディリクレ分布

0.00820617, 1.6067e-08, 1.82496e-06, 0.991569, 0.000223472
0.100336, 0.104018, 0.027749, 0.0115689, 0.756328
0.965557, 0.00401107, 7.88541e-18, 0.0285327, 0.00189949
0.721145, 0.0584562, 0.00153585, 2.04515e-05, 0.218842
0.834343, 0.00332245, 5.07e-09, 0.00101873, 0.161316
0.00615545, 0.993583, 0.000253227, 8.34935e-06, 3.51258e-07
0.893519, 0.0714379, 0.000327648, 0.00228227, 0.0324336
0.000104421, 0.00124077, 0.99865, 5.90869e-07, 4.54077e-06
0.0158551, 0.00067078, 0.0668421, 0.882654, 0.033978
8.98511e-09, 0.016774, 7.59551e-05, 0.0137781, 0.969372

ディリクレ分布の可視化

AlphaZeroのMCTSのルートノードではディリクレノイズを加えることで、全ての手をランダムで選ばれやすくしている。
P(x,a)=(1-\epsilon)p_a + \epsilon \eta_a, \eta \sim Dir(0.03)

以前の記事で、2次元のディリクレ分布を可視化したが、3次元の場合の可視化ができないか調べていたら、以下のページを見つけたので試してみた。
Visualizing Dirichlet Distributions with Matplotlib
x, y, zを2次元の正3角形に投影して、確率密度を色で表示している。
本当は、x, y, zを3角形の平面に投影して、確率密度を高さの軸にして3Dのグラフにしたかったが、やり方がわからなかった。

上記のページは、python2用のコードになっていたので、python3で動かすには、

import functools

を追加して、
reduceをfunctools.reduceに置換する。

[0.03, 0.03, 0.03]のディリクレ分布を可視化するコードは以下の通り。

%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.tri as tri

corners = np.array([[0, 0], [1, 0], [0.5, 0.75**0.5]])
triangle = tri.Triangulation(corners[:, 0], corners[:, 1])

# Mid-points of triangle sides opposite of each corner
midpoints = [(corners[(i + 1) % 3] + corners[(i + 2) % 3]) / 2.0 \
             for i in range(3)]
def xy2bc(xy, tol=1.e-3):
    '''Converts 2D Cartesian coordinates to barycentric.'''
    s = [(corners[i] - midpoints[i]).dot(xy - midpoints[i]) / 0.75 \
         for i in range(3)]
    return np.clip(s, tol, 1.0 - tol)

class Dirichlet(object):
    def __init__(self, alpha):
        from math import gamma
        from operator import mul
        self._alpha = np.array(alpha)
        self._coef = gamma(np.sum(self._alpha)) / \
                     functools.reduce(mul, [gamma(a) for a in self._alpha])
    def pdf(self, x):
        '''Returns pdf value for `x`.'''
        from operator import mul
        return self._coef * functools.reduce(mul, [xx ** (aa - 1)
                                         for (xx, aa)in zip(x, self._alpha)])

def draw_pdf_contours(dist, nlevels=200, subdiv=8, **kwargs):
    import math

    refiner = tri.UniformTriRefiner(triangle)
    trimesh = refiner.refine_triangulation(subdiv=subdiv)
    pvals = [dist.pdf(xy2bc(xy)) for xy in zip(trimesh.x, trimesh.y)]

    plt.tricontourf(trimesh, pvals, nlevels, **kwargs)
    plt.axis('equal')
    plt.xlim(0, 1)
    plt.ylim(0, 0.75**0.5)
    plt.axis('off')

draw_pdf_contours(Dirichlet([0.03, 0.03, 0.03]))
実行結果

f:id:TadaoYamaoka:20171209223116p:plain

よく見ないとわからないが、角の部分の色が赤色(=高い値)になっている。
それぞれの角は(1, 0, 0)、(0, 1, 0)、(0, 0, 1)を意味する。
つまり、いずれかの手が選ばれやすくなる。

[0.03, 0.03, 0.03]だと見にくいので、[0.9, 0.9, 0.9]とすると、

draw_pdf_contours(Dirichlet([0.9, 0.9, 0.9]))

f:id:TadaoYamaoka:20171209223400p:plain
角の値の色が変わっているのが分かりやすくなった。

alphaが正の値の場合、

draw_pdf_contours(Dirichlet([5, 5, 5]))

f:id:TadaoYamaoka:20171209223735p:plain
中央の値が選ばれやすくなる。

乱数生成

Pythonでディリクレ分布に従って乱数を生成するには、numpy.random.dirichletを使用する。

import numpy as np
[np.random.dirichlet([0.03, 0.03, 0.03]) for _ in range(10)]
[array([  9.30062319e-13,   2.57427053e-04,   9.99742573e-01]),
 array([  1.00000000e+00,   2.17381455e-19,   6.85158127e-35]),
 array([  1.00000000e+00,   1.16336290e-15,   3.83232435e-19]),
 array([  8.76967918e-01,   1.15399381e-36,   1.23032082e-01]),
 array([  1.94448447e-45,   5.91371213e-01,   4.08628787e-01]),
 array([  5.51018131e-56,   1.00000000e+00,   1.32521447e-17]),
 array([  2.68526987e-21,   5.73712429e-01,   4.26287571e-01]),
 array([  1.00000000e+00,   7.11978680e-14,   4.22528386e-24]),
 array([  3.84802928e-58,   5.40350337e-20,   1.00000000e+00]),
 array([  1.56164809e-01,   8.43835191e-01,   2.39362055e-11])]

どれか一つが選ばれやすくなっているのが分かる。

AlphaZero Chess/Shogiの論文を読む その3

前回までに個人的に気になった点はだいたい書いたので、今回は残った部分で気になったところを拾って書きます。

スケーラビリティ

思考時間を増やした場合、αβ探索よりもレーティングの伸びが良い。
これはAlphaZeroのMCTSがαβ探索より思考時間を短縮できるということを示しており、αβ探索の方がこの分野で優れているという信念が疑問視された。

引用されていた文献には、チェスにおけるMCTSの欠点として、トラップを発見するためにより長い試行時間が必要ということが挙げられていました。
ただし、十分に正確なシミュレーション戦略が見つかれば対処できるとも書かれていたので、今回それが精度の高いPolicyによって実現されたということでしょう。

先行研究

チェスの研究はたくさん上げられていますが、将棋の先行研究としては、ボナンザによる機械学習が取り上げられていました。

なぜうまく動くか

ニューラルネットワークによる非線形関数近似で局面評価をすると、強い表現力があるが疑わしい近似誤差を起こすことがある。
MCTSは、これらの近似誤差を平均するため相殺する傾向がある。
対照的に、αβ探索は近似誤差をルートに伝播させる。
MCTSによって、ニューラルネットワークを効果的に適用できるようになる。

ドメイン知識

使用したドメイン知識は以下の通り。

  1. 入力特徴と出力特徴を表現するために、盤がグリッド構造であることを使用
  2. シミュレーション、ゲーム終了のために、ゲームのルールを使用
  3. 千日手、移動、成り、持ち駒からの打ち手のルールを入力特徴と出力特徴に使用
  4. 上限手数で引き分けとする

以上のドメイン知識以外は使用していない。

考察

スケーラビリティ

将棋はチェス以上に浅いトラップを持つゲームであることが知られています。
将棋におけるモンテカルロ木探索の特性の解明
浅いトラップの存在がMCTSがMini-Max探索よりも弱い原因の一つとされていました。
AlphaZeroの手法でこの欠点が克服できることが示されたと思います。

先行研究

コンピュータ将棋でブレイクスルーとなったボナンザはしっかり取り上げられてますね。

なぜうまく動くか

非線形関数はMCTSと組み合わせることでうまく動くということです。
Ponanza方式のαβ探索とディープラーニングの組み合わせはこれとは異なるアプローチですが、近似誤差の平均化という点では狙いは同じだと思います。
AlphaZero方式はドメイン知識を用いない汎用性のある方法という主張もあるので目指すベクトルは異なりますが、将棋を強くするという目的であれば他にもアプローチはあると思います。

ドメイン知識

千日手を入力特徴に加えていますが、囲碁のコウと異なり、千日手が絡む手はそれほど重要ではないため、私は千日手は探索中にチェックしてもよいと思っていますが、ニューラルネットワークに学習させたかったようです。
AlphaGoZeroの論文の記事でも書きましたが、将棋を強くするという目的であれば効果的なドメイン知識は使って学習を効率化させたいところです。

今回で終了します。

AlphaZero Chess/Shogiの論文を読む その2(AlphaGo Zeroとの差分)

AlphaZero Chee/Shogiの論文についての続きです。
今回はAlphaGo Zeroとの差分について書きます。

AlphaGo Zeroの論文については、以前に書いた記事を参照ください。

ネットワーク構成

ニューラルネットワークの構成は、AlphaGo Zeroと同じ、PolicyとValueを同時出力する20ブロックのResNetです。
前回書いた通り入力特徴と出力ラベルが異なります。

探索アルゴリズム

基本的にはAlphaGo Zeroと同じ、モンテカルロ木探索にPolicyとValueを使ったPUCTアルゴリズムです。

以下の点が異なります。

  • AlphaGo Zeroでは探索のハイパーパラメータは、ベイジアン最適化で調整したが、チェス、将棋のゲーム固有の調整は行わず同じパラメータを使用
  • AlphaGo Zeroはニューラルネットワークに対称、回転の8局面からランダムに選んで入力していたが、局面は固定とする
  • AlphaGo Zeroと同様に探索を確実にするためにルート局面にディリクレノイズDir(α)を加えるが、合法手の数に反比例して、チェス、将棋、囲碁で、α={0.3, 0.15, 0.03}にスケーリングする

AlphaGo Zeroと同様に自己対局では、30手まではルートノードの訪問回数に応じた分布に従って確率で選択して、それ以降は最大訪問回数の手を選ぶグリーディー戦略としていましたが、チェス、将棋の場合は何手目まで確率的にしていたかは書かれていません。

elmoとの対局では、グリーディー戦略を使っています。

自己対局パイプライン

AlphaGo Zeroでは自己対局1,000回ごとにチェックポイントを設けて、評価のステップで、55%以上の勝率の場合にそれを最良のネットワークとし、その後の自己対局で使用していましたが、評価のステップは省略されました。
常に最新のネットワークが自己対局に使用されるようになっています。

囲碁は固定手数で終局しますが、将棋では上限手数を超えると引き分けとしています。

学習

以下はAlphaGo Zeroと同じ。

以下の点が異なる。

  • ミニバッチサイズ:4,096(AlphaGoZeroは2,048)
  • 自己対局時のシミュレーション回数は800回(AlphaGoZeroは1,600回)
  • 自己対局数は、将棋では2.4千万(AlphaGoZeroは4.9千万)
  • 学習率は0.2から0.02、0.002、0.0002の順に段階的に下げる。(AlphaGoZeroは0.01、0.001、0.0001)

考察

ネットワーク構成については、前回日記で書いたので省略。

探索アルゴリズム

AlphaGo Zeroでは、対称、回転を使うことでData Augmentationを行って、探索時もそれらをランダムに選ぶことで評価を平均化すると共にランダム性が加わっていました。
チェス、将棋では局面が固定されるので、ランダム性を加えるのはルート局面のディリクレノイズのみとなっています。
自己対局では、序盤については訪問回数に応じて確率的に手を選択するのでそこで局面のバリエーションを作っています。
何手まで確率的に指しているのかは書かれていませんでした。

AlphaGo Zeroの論文には、対局時に手を選択する方法が書かれていませんでしたが、elmoとの対局時はグリーディー戦略と書かれていましたので、AlphaGo Zeroも対局時はグリーディー戦略を使っていたと思われます。

自己対局パイプライン

評価のステップを省略しても学習できたようです。
これは意外な結果です。
強化学習は学習が発散することがあって学習が難しいことが知られています。
そのため、過去のネットワークに対して強くなっていることを確認するステップが必要と思っていました。
DQNのような方策オフの学習に比べて、AlphaGoZeroのような方策オンの場合は、学習が安定するということでしょうか。

学習

自己対局時のシミュレーション回数が半分になっています。
800回のシミュレーションではとても弱いですが、その回数でも強化学習できるようです。
AlphaGoのRL Policyの強化学習ではシミュレーション0回(Policyのみ)でしたので、それに比べたらまともな方策が学習できるということでしょう。

800回のシミュレーションで1手80msと書かれているので、1局平均150手とすると、1局の自己対局で12秒です。
並列化しない場合、2.4千万局の対局には、547.9年かかります。
GPUを使うと、処理速度を落とさず並列にゲームを進行できるので、これを短縮しています。
自己対局には、5,000個の第一世代のTPUを使ったと書かれています。
第一世代のTPUが何TFLOPSか不明ですが、個人レベルで試すのは不可能な規模であることは想像できます。

学習率は、AlphaGo Zeroよりも大きい値が使われています。
0.2と大きめの学習率から始めても学習できるようです。
Batch Normalizationを使用しているので、大きめの学習率が使用でき収束が速くできたということでしょう。

続く

AlphaZero Chess/Shogiの論文を読む

DeepMindからAlphaGo Zeroと同じ方法で、チェスと将棋でトップレベルを上回ったという論文が発表されました。
[1712.01815] Mastering Chess and Shogi by Self-Play with a General Reinforcement Learning Algorithm

ドメイン知識を用いないスクラッチから強化学習のみで達成しています。
将棋やチェスはモンテカルロ木探索(MCTS)は向かず、Mini-Max探索の方が良いというのが常識のようになっていましたが、将棋やチェスでもディープラーニングと組み合わせることでMCTSが有効であることが示されたというのも大きな成果だと思います。

まだ全部読み切れていませんが、気になる個所から読んだ内容を書いていきます。

個人的に一番気になるのは、入力特徴と出力ラベルの表現方法です。
チェスについても書かれていますが、将棋のみについて書きます。

入力特徴

  • 先手の駒の配置×14種類
  • 後手の駒の配置×14種類
  • 繰り返し回数(3まで)(千日手用)
  • 先手の持ち駒の枚数×7種類
  • 後手の持ち駒の枚数×7種類

これを8手の履歴局面分。
これに、先手か後手かの特徴(1面)と合計手数(1面)を加えて合計で(14+14+3+7+7)×8+2=362面。

出力ラベル

  • 桂馬以外の駒の位置×方向(8)×距離(8) = 64
  • 桂馬の位置×方向(2)
  • 桂馬以外の駒が成る場合(64)
  • 桂馬が成る場合(2)
  • 持ち駒から打つ位置×7種類

合計で、64+2+64+2+7=139面

他の表現方法も考えられるが、これで合理的な手を学習できたとのこと。

考察

入力特徴

私が試している将棋AIでは入力特徴は、

  • 先手の駒の配置×14種類
  • 後手の駒の配置×14種類
  • 先手の持ち駒×7種類×枚数
  • 先手の持ち駒×7種類×枚数
  • 駒の効き×14種類
  • 位置ごとの効き数(1)
  • 王手かどうか(1)

としていましたが、これとの比較で言うと、
まず、千日手を判定する情報が加わっています。
AlphaGo Zeroでは、コウを識別するために履歴局面を使っていましたが、入力特徴として与えています。

持ち駒は種類ごとの枚数で、7面となっています。
ニューラルネットワークの入力は0~1で正規化する場合が多いですが、枚数をどのような値で入力しているかは書かれていません。
AlphaGoでは整数はOneHotエンコーディングで複数枚で入力していましたが、整数をそのまま数値で入力して問題ないのでしょうか。
※「the number of captured prisoners of each type」で7面を、枚数なしで種類のみと解釈しましたが、コメントを頂いたので修正しました。

効きや王手の情報は入っていません。

8手の履歴局面を入力していますが、私が以前に実験したところでは履歴は効果ありませんでした。教師ありではなく自己対局による強化学習では必要なのかもしれません。

先手後手の特徴がありますが、将棋の場合は局面の価値は、先手でも後手でも変わらないと思いますが、必要なのでしょうか。あっても影響はないと思いますが。

合計手数が1面のみで表現されていますが、具体的にどのように値を入力するかが書かれていません。例えば256手を0~1の範囲の実数として入力するのでしょうか。
また、手数が同じでも局面の進み具合は異なるので、そのまま手数を入力してしまってよいか疑問です。
技巧などは、局面の進み具体を手番ではなく進行度という尺度で表現しています。

出力ラベル

私が試している将棋AIでは、

  • 移動先の座標
  • 移動方向(8+2(桂馬の動き))

として表現しています。
それとの比較で言うと、
座標は移動先ではなく、移動元で表現しています。
持ち駒から打つ場合は、移動先なのだから、移動先でそろえた方がよいと思っていますが、あまり違いはないかもしれません。

移動方向に加えて、移動距離も表現しています。
移動距離がなくても移動する駒は一意に特定できるので、必要かどうかは疑問です。
私の実験では、出力ラベルは必要最低限にした方がよいです。
私は以前は、移動する駒にもラベルを割り当てていましたが、ない方が良いことが分かったので、移動方向のみにしました。
AlphaZeroでも移動する駒は表現していません。

AlphaGoZeroの囲碁の入力特徴と出力ラベルの表現をベースにしていることがうかがえます。
必ずしも将棋でベストな表現ではないと思いますが、マシンパワーがあれば、結果は同じということでしょうか。

個人レベルで試すなら、学習効率の良い入力特徴と出力ラベルの表現を模索することも必要だと思います。

続く。。。
AlphaZero Chess/Shogiの論文を読む その2(AlphaGo Zeroとの差分) - TadaoYamaokaの日記
AlphaZero Chess/Shogiの論文を読む その3 - TadaoYamaokaの日記

将棋でディープラーニングする その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よりも強くなっていそうです。