COSUMIで多数の小さな改良を行いました(2022年4月)

COSUMIで以前から気になっていた部分を、まとめて大量に追加・修正行いました。

囲碁ブラウザゲーム COSUMI
https://www.cosumi.net/

全て些細なことばかりで大した話は特にないのですが、一応その中で主だったのをここに書いておきます。

まず、対局リプレイページに、そのページのURLが入っている二次元コードを表示するようにしました。何かの時に使えると思います。それと、スライダーを動かしたりして進めたその局面を初期表示とするURLに変更するボタンも用意しました。先ほどの二次元コードやツイートボタンもこの新しいURLでのものになりますので、これも便利な時があると思います。

次に、ただの碁盤で、一番最後の局面で右矢印を連打してもらうと対局リプレイページに行けるようにしました。これ、以前できないかと聞かれたことがあって、その時にどういう仕様で作ればいいかなと考えている内に、もう作った気になっていました…(笑) 本当にすいません。馬鹿みたいにシンプルな作りですが、そこはご容赦ください

それから、オンライン棋譜ビューアのSGFのパーサを改良しました。もうあまりエラーを出さないと思います。

最後に、white shade囲碁ベンチマークのTensorFlow.jsのバージョンを、最新のに上げさせてもらいました。囲碁ベンチマークのベンチマーク結果については、当然、以前のバージョンとは比較不可ですので、こちらもVer. 3.0という新しいバージョンということにしておきました。

話は変わりますが、AWS EC2のC7gインスタンスはまだ使えないんでしょうか? Savings Plansがひとつ切れたので、どうすればいいのか迷ってます。

[追記 2022/6/5]
COSUMIのこれからの予定を、少しだけここに書いておきます。

C7gインスタンスがようやく来たので、ちょうどいい機会だと思い、これからのサーバ構成をどうしようかといろいろ調べていて気が付いたのですが、KataGoってめちゃくちゃ軽いんですね! b20c256playout 1CUDAmatchモードで、g5g.xlarge500~600手/秒g5.xlargeでは1300~1400手/秒出ることに気づいて本当にびっくりしました(さすがに桁を間違えてるだろと、何回も計算をし直してしまいました(笑))。一応補足しておくと、matchモードとかanalysisモードとかでは、複数の局面をまとめてNNに放り込んで高速化するという、とても素敵な実装になっている(これは前から知っていました。しかし凄い!)のが大きいのですが、今まで自分は一体何を勘違いしていたのか、もうよく分からなくなるぐらい思っていたより速いです。COSUMIはg5.xlargeの1.5倍以上のお金を掛けて120手/秒ぐらいなので、もはやKataGoの方が全然安いですね。

先月でCOSUMIは開始してから14年が経ちました。さすがに折り返し地点はすでに通り過ぎてしまっていると思っています。そして、何か新しいものを実装するのも、もう今が最後のチャンスだなという気持ちでもいます。以前からCOSUMI最後の仕事としてKataGoは出来れば使いたいと思っていましたが、直近は、やっぱり少し無理かなと感じてて半ば諦めモードでいたので、それをやらないのならその代わりにと、今ある悪手指摘の機能を置き換える、棋譜添削を日本語で行うNNを、ここ2か月間ほどずっと作っていました。これについてはほぼ見通しが立ったので、残りの作業は必ず最後まで進めますが、上に書いたようにKataGoがこんなに軽いのであれば使わない手はないので、そちらも少し頑張ってみます。安定して動きさえすれば、19路盤でlevel 10とかも余裕でしょう(それができたらなんかすごいですよね。COSUMIじゃないみたい!(笑))。とりあえず今は、analysisモードのレスポンスで、rootInfovisitsが1の時、moveInfosが空になってしまうのが理由が分からずストップしてます… これなんでなんだろう?

ちなみにC7gは、私がいつも使っているGNU Goを同時に100個走らせるベンチでC6gより26%ほど速く、料金は7%ほど高いって感じでした。

[追記 2022/6/9]
なんかいろいろ勘違いしていたのですが、moveInfosが空になってしまうのはそれでいいんですね。なんとか動きそうですが、いつものことながら棋力の調整が死ぬほど難しい…

white shadeをアップデートしました(2021年5月)

white shadeをアップデートしました。

white shade – 囲碁ブラウザゲーム COSUMI
https://www.cosumi.net/whiteshade.html

white shadeのVNの構造に、かなり小さな、けれどもたぶんだいぶ良いと思われる変更を加えました。実際、ロスは少し下がったのですが、以前のバージョンと対局させて強さを計測しても全く変わっていなくてちょっと意外… そういうわけなので、使うかどうか迷ったのですが、手がきれいにはなっているだろうと信じて、アップデートすることにしました。さらに、TensorFlow.jsも最新のバージョンに上げておきましたが、それ以外は基本的に以前と同じです。

今回のアップデートで、一応white shadeをいじるのは最後のつもりです。そんなに強くなくても、きれいにちゃんと打ってくれたら嬉しかったのですが、私にはちょっと難しかったですね。ただし、white shade諦める代わりとして、オンライン版の方でKataGoが使えないかを、時間のある時に少し調べてみます。サーバのリソースもできるだけ無駄なく使いたいので、なんとか実現させたいですね。COSUMI始めてからもう13年が経過したのですが、これがCOSUMIでの最後の大きな仕事でしょうか?

それにしても、今回の変更で少しも強くなっていないというのがまだちょっと信じられないので、CGOSでも少し動かしてみたいと思います。

[追記 2021/07/11]
こちらにも追記しましたが、CGOSで動かしてみました。今回の改良は小さい碁盤サイズで有効だと思われるものだったのですが、9路盤ではやはり少し強くなっているように見えます。手元でもう一度確認してみて、いけそうであれば9路盤にレベル6を戻したいと思います。

[追記 2022/4/23]
ここに書くのをずっと忘れたままだったのですが、あの後、手元で調べたところ、やはりあまり強くなっていないようなので、Tensorflow.jsのバージョン以外はなにも変更せず、そのままの状態で今に至ります

white shadeをCGOSで動かしています

なんとかかんとか、white shadeをTromp-Taylorな終局とPositional Superkoに対応させて、以前から一度は挑戦してみたかったCGOSで、今現在、動かさせていただいています。

Computer Go Server
http://www.yss-aya.com/cgos/

9路盤と13路盤でそれぞれひとつずつ動かしていて、player nameは9路盤がws-201217_9lv5、13路盤がws-201217_13lv6です。

Crosstable for ws-201217_9lv5
http://www.yss-aya.com/cgos/9×9/cross/ws-201217_9lv5.html

Crosstable for ws-201217_13lv6
http://www.yss-aya.com/cgos/13×13/cross/ws-201217_13lv6.html

それぞれ、こちらのレベル設定と同じに、内部の設定を合わせていますが、Tromp-Taylorな終局とPositional Superkoに対応させた関係で、厳密には全く同じ強さとは言えません。また、いろいろと無駄に重くもなっているのですが、まあそれでも、ほとんどノータイムで打つと思います。少し話変わりますが、以前から、入門者の方には純碁よりTromp-Taylorの方がいいのでは、と思っています。個人的には、どんどん推していきたいです。普及において、ルールは本当に本当に重要だと思いますが… あと少し気になったのですが、例えばfloodgateにはhumanがいることもあると思いますが、CGOSは全くいないと思っていいんですよね? 申し訳ないですが、終局は決してきれいではありません…

最初、すんなり動かなくて少し頭を抱えたのですが、どうやら、genmoveに対するレスポンスの末尾が、改行2つではなく3つだった(今の今まで、全く気づかなかったよ…(笑))のが、関係していたようでした。それ以外にも、Stopに対して一局やらかしてしまいましたが、それに関しても修正済みです。できるだけご迷惑が掛からないように、またおかしかったらできるだけすぐに止めれるよう気をつけますので、どうかよろしくお願いいたします。

[追記 2020/12/18]
13路盤でAya786m_10kに一局、時間切れ負けしていますが、ログを見る限りエンジンの問題ではなさそうでしょうか? 続けて次の対局も打てていたみたいですが、一旦OSごと再起動しました。

[追記 2020/12/21]
一旦、止めました。また少し期間を空けてから動かします。9路盤と13路盤の両方とも、最終的には少なくとも1000局は打たせるつもりにしています。

[追記 2021/02/12]
ここ最近も少し動かしていたのですが、とりあえず両方とも1000局を超えました。現時点で、9路盤が1630局打ってレートが2091、13路盤が1535局打ってレートが2474です。多様なプレーヤーがいることが、正確なレートの測定に不可欠だと思いますので、これからも可能であれば、定期的にCGOSでwhite shadeを動かす予定です。

[追記 2021/07/11]
ここ最近も、white shadeをCGOSで動かしていました。できれば最新のバージョンと以前のバージョンの両方を同時期にと思い、コンフィグファイルでpriorityを設定してやったりもしました。あとで見返した時のために書いておくと、現時点で、ws-201217_9lv5が2730局打ってレートが2124、ws-210530_9lv5が2962局打ってレートが2196、ws-201217_13lv6が1940局打ってレートが2468、ws-210530_13lv6が1265局打ってレートが2445です。当たり前ですけど、CGOSって対局数がなかなか稼げなくて辛いですね…

white shadeをアップデートしました(2020年12月)

white shadeをアップデートしました。

white shade – 囲碁ブラウザゲーム COSUMI
https://www.cosumi.net/whiteshade.html

今までのバージョンのwhite shadeが全体的に大きく/重くなりすぎてしまっていたので、今回、大幅に小さく/軽くしました(この変更はいろいろ考えてのことなのですが、COSUMI的には今回のバージョンより多くのリソースを食ってしまうものは基本的にNGだと考えています)。今まで動作が怪しかった環境でも、きちんと動くようになっている可能性が、結構あると思います。ただし、この結果、本当に申し訳ないのですが、9路盤のレベル6に関しては、維持することができなくなったので削除しました。あともう一回ぐらいは頑張ってみようかなと思っているので、その時はまた元に戻せるかもしれませんが、とりあえず今現在は無理です。もともと11路盤と13路盤には少し余裕があったのですが、9路盤にはほとんど無かったので、どうにもなりませんでした。

あと、変なタイミングでパスすることがちょくちょくあるのですが、これが減るように少し変更を加えました。実際にどれぐらい効果があるのかは、また時間があったら調べてみます。

さらに、TensorFlow.jsも最新のバージョンに上げておきました。

それから、かなり長い間ほったらかしだった、囲碁ベンチマークも併せてアップデートしました。詳細はこちらに追記しています。今の時期、スマホで5局ぐらい打たせるといい感じにカイロになりますので、ぜひお試しください(笑)。

ベッドに寝転がって、ぼけーっとベンチマーク動かして眺めていると、自分なんかがGTX 1060一発でよくこんなの作れたなと思います(最近はもやっていたし…)。5年前の自分にwhite shadeを見せてもまず信じてもらえないだろうとか、1060ではなく1070買っていたらもはや別の人生だったのではとか、妄想が捗る…(笑) ちょうど今、年季の入ったメインPCを新調しようかなと少し考えていて、もし新しくしたらそれは間違いなく別の人生ですが、まあとにかく面倒なんですよね。もう壊れるまで今のままでいいかな…

COSUMIのサーバを3台から2台に減らします

かなり長い期間、COSUMIは専用サーバ3台で運用してきたのですが、これを7月末までに2台に減らします。

囲碁ブラウザゲーム COSUMI
https://www.cosumi.net/

3台目のサーバというのは、実質的にアクセスの多い時間帯の9路盤~13路盤のレベルの高い対局のためだけにあるようなものだったのですが、さすがにそのためだけに年40万+はちょっと辛いので、解約することにしました。本当はもう少し早い時期に行う予定だったのですが、コロナの影響でアクセスがかなり多くなっていたので、ここまで延期することになってしまいました。未だにアクセスは高止まりしたままなので、解約するのは正直かなり怖いのですが、もしもどうにもならないようなら、Amazon EC2辺りを特定の時間帯だけ使おうかなと考えています。今回解約するサーバは6年間以上借りてたみたいで、確かに初期費用も小さくはなかったですが、それでもそろそろ割高になってきそうなのも理由のひとつで、実はまだちゃんと調べてはいないのですが、以前ほどはEC2などに価格の優位性はないはずです。

ただ、一番理想的なのは、9路盤~13路盤はwhite shadeがごっそり受け持ってくれることで、これをなんとかしたいのですが、中途半端な誘導ではみなさんほとんど打ってくれないんですよね…(泣) 私の環境からだとほぼ問題ないのですが、動作しないことも多いのでしょうか? 月末までにいろいろよく考えてみます。

あと、これはかなり今更ですが、サーバのリソースがたっぷり余る時間帯は、逆にそれを贅沢に使ったなにかをやってみたい気もしています。

[追記 2020/7/27 7:50]
やはり常時サーバ2台で済ますのはさすがにちょっと無理があるように思うので、曜日問わず、15:00から18:00まで、EC2をサポートに使う予定です。とりあえずのテストとして、今日(と明日?)は、その時間帯以外、2台でいってみて、データ取れたらまた考えます。

[追記 2020/7/28 8:00]
いやあ、昨日はちょっときつかったですね… とりあえず、今日も少しだけ設定を変更した上で、15:00~18:00以外はサーバ2台でいきます。

[追記 2020/7/29]
15:00~18:00のみというのは、どうも虫がよすぎたようです。少し薄くても構わないので10:00~22:00は必要ですね。しかしそうなると、EC2にはリザーブドインスタンスとかがありますから、時間帯を限らず24時間動かしっぱで良いですね。って、それはただのサーバの乗り換えでは…

ここ最近、EC2のコストパフォーマンスをちょくちょく調べていたのですが、先月出たばかりのC6gというArmのCPUが載ったインスタンスが、COSUMI的に激安なんですよね。24時間動かし続けても、ぜんぜん今のサーバより安そうです。なので、その辺りが落としどころかな… しかしなんにせよ、「サーバ解約する前に良く調べておけよ」って思いました(笑)。

[追記 2020/8/1]
昨日の朝、専用サーバ3台から専用サーバ2台+c6g.4xlargeに変更しました(間に合って良かった…)。今のところは特に問題ないようです。ほんの少しだけ全体の処理能力が落ちましたが、たいしたほどではありません。

white shadeをアップデートしました(2020年4月)

white shadeに7路盤が新しく追加され、9路盤~13路盤では強さをレベル6まで選択できるようになりました。

white shade – 囲碁ブラウザゲーム COSUMI
https://www.cosumi.net/whiteshade.html

ここ最近、COSUMIへのアクセスが本当にかなり多い状態が続いていて(昨日なんか36,010敗…)、なんとか早急に手を打たないと不味いので、実はまだいくつか問題点が残っているのですが、とりあえずwhite shadeをここで一度アップデートして、できるだけそちらへ誘導していきたいと思います。

その問題点としてはまず、7路盤で対局結果を1目間違えることがよくあります。できるだけ早く直したいと思いますが、きちんとやるには学習やり直さないといけないので、少なくとも2週間でしょうか? その次に、アップデート直前に自分の少し非力なiPadで動かすまで気づいていなかったのですが、今回の設定では動作がちょっと重すぎるみたいなので(そのせいで少し不安定かも?)、これも今後修正したいと思います。今は、white shadeもかなり強くなって、特に大きな碁盤サイズでは、今回のレベル6程度の強さなら、そんなにリソース使わなくてもいけるようになっているのですが、もろもろ事情があって、ここを直すのは、全部やり直さないと無理なので、だいぶ先のことになります。あと、レベルが低い時の手の汚さも気になります。white shadeが強くなるにしたがって、弱い設定にするのがかなり難しくなってきました。今回こんなにアップデートに時間が掛かったのも、かなりの部分、そのせいです。良い方法がどうしても見つからなければ、どうせ置き石もたくさん置けることですし、レベル1は今後無くすかもしれません。

white shadeは、一応COSUMI史上最強だと思いますし、もう少しブラッシュアップできたら、メインコンテンツのひとつに昇格かな? もうちょっとだけ頑張ってみます。

前回のwhite shadeのアップデートはちょうど一年前だったみたいですが、この一年間、私は一体何をしていたのでしょうか?(笑) ほんとに心配なるわ…

[追記 2020/5/10]
原因が思っていたのと違っていたのでだいぶ頭抱えましたが、7路盤でスコアをよく間違える理由がようやく判明しました。分かってしまうと、よくこれで7路盤を普通に打っていたなって感じだし、それ以外の碁盤サイズにもだいぶ悪影響があった気がします… このえげつないバグを見た後に、このまま7路盤を公開したままにするのは、ちょっと気が引けるので、一度、7路盤は削除しました。再度公開できるのは、一回学習を回さないといけないので早くて10日後ぐらいですね。ついでに、少し軽く動くようにもします。たぶん、全体的に手もきれいになるでしょう。レベル調整は狂ってきそうですが、それはまたおいおい修正していきます。あと、7路盤では互先はこのままずっと削除したままにするつもりです。やるならまた別の場所に用意します。

[追記 2020/5/21]
スコアをよく間違えるのが直ったので、互先以外は7路盤を元に戻しました。全碁盤サイズで少し軽くなって、おそらく手が自然になってもいるはずです。9路盤以上の碁盤サイズでは、同じレベルで比較した場合、少しだけ弱くなっているとも思いますが(ただ、元々がたぶん少し強すぎたと思う)、ここを調整するのが一番リソースを食うので、細かい修正を全部終わらせた後に行う予定です。もう少し全体的に軽くするために、できるだけ早くPNも学習し直したいのですが、一回先にやります。

[追記 2020/6/5]
今現在、2種類のPNを使用しているのですが、その内のひとつを大幅に小さくしました。少しだけ弱くなっていますが、だいぶリソースを食わなくなっているはずです。手は自然になっているかもしれません。あと、tensorflowjs_converterの--quantization_bytesというオプションを設定して、すべてのNNのウェイトを4byteから2byteに丸めました。半分になった今現在でファイルサイズが全部合わせて3MB弱ぐらいあって、今まではいくらなんでもちょっと大きすぎだったんですよね… 1byteとかだと、予めそういう学習の仕方をしておかないとだめだと思いますが、2byteだと、出力はたぶんあんまり変わらないんじゃないかと思います(以前調べた気がするのですが、詳しいことは忘れた)。それから、話がちょっと変わりますが、クリック/タップ周りも少し手を入れました。本当はこれもっと早くやらなければいけなかったのですが、こういうところを触るのは本当に怖くて、ずっと見て見ぬふりをしてました。なにかまずそうなら、出来るだけすぐに直します。問題無さそうなら、通常版などCOSUMI全体を書き換えたいと思います。

white shadeが動きにくいことはだいぶ減ったと思うので、ぜひ一度遊んでみてください。

[追記 2020/9/7]
white shadeが極端に早いタイミングでパスすることがあったのですが、ようやくひとつ理由がわかった! ちょっとおかしな話なのですが、左上10×10以外の箇所でコウが発生した時に、そこをコウだとしていませんでした(全く頓珍漢なコードだったので、本来なら盤面全体でそうなるはずが、かなり運悪く左上10×10では動いてしまってた…(泣))。なので、このバグは9路盤以下には影響がなく、11路盤でも限定的ですが、13路盤では大問題となっていました。うーん、条件がかなり限られるので、なかなか気づきませんでした… というわけで、急いで修正しておきましたが、結果的に13路盤(と11路盤)はおそらく大幅に強くなっています。もともと、今現在のこの強さに設定したかったのが、意図せず弱くなってしまっていたということです。いろいろと申し訳ありませんでした。

ただ、今回のバグは手元のPython版には関係なかったものですが、そちらでも早すぎるパスは見たことがありますので、単にVNの精度の問題と言えるケースは、これからも残っていくと思います。

COVID-19とCOSUMI

ここ最近、COSUMIへのアクセスがかなり多い状態が続いています。

囲碁ブラウザゲーム COSUMI
https://www.cosumi.net/

説得力のあるデータは出せませんし、私も決して断言する訳ではありませんが、これは新型コロナウイルス感染症の流行のみでほぼ説明できると、自分の中では確信しています。

今年に入ってからのCOSUMIが一日に負けた数の推移です。寒さの厳しい時期の話でもありますので、比較対象として前々年と前年もグラフに加えました。

以前からここでも何度か書いていますが、COSUMIは雨や雪が降ると、アクセスが増える傾向があります(これは、他のオンラインゲームなどでも同じようです)。COSUMIへのアクセスの約1/4は海外からで、さらにその内訳に韓国や中国など東アジアが占める割合が多いというのが計算を難しくしますが、とりあえず直近見られる28,000+という数字は、仮に日本全国で大雨が降ったとしても、特別な他の要因が無ければなかなかいくことのない、非常に大きな値です。新型肺炎による健康被害が最終的にどれぐらいのものになるのかは、現時点では私にはよくわかりませんが、もしも実際に、それほどまでに人の外出が抑えられているのならば、経済的な影響についてはすでに甚大なものなのではないでしょうか?

中国でオンラインゲームや動画アプリの利用急増、新型肺炎で – ロイター
https://jp.reuters.com/article/china-health-online-idJPKBN1ZY109

しかし、こんな時ですので、外出を控えること自体はとても良いことだと思います。実はCOSUMIでは、この春、3台あるサーバを2台に減らす予定だったのですが、これについてはとりあえず当面の間、延期します。サーバの負荷が高い時間帯が続いて申し訳ありませんが、あと一か月ほどでwhite shadeをかなり強くできそうなので、そのあたりでそちらへの誘導を強めます。しばらくお待ちください。

[追記 2020/4/1]
3月末までのデータを追加したグラフです。細い線は、7日移動平均です。

なんだかえぐみが増してきましたね… 3月中旬からの自粛ムードの中だるみも、うっすら観測できているような気もします。white shadeは、ちょっといろいろ手間取っています。申し訳ありませんが、もう少しだけお待ちください。

white shade使ったベンチマークテストを作ってみました

white shadeを強くしようと、時間のある時に少し手を動かしてはいたのですが、なかなかなかなか強くなりません。もう、真面目に探索しないと、レベル6は難しいですかね? で、なかなかなかなか強くならないのがつまらないので、今回はwhite shadeを利用したベンチマークテストを作ってみました。

囲碁ベンチマーク – 囲碁ブラウザゲーム COSUMI
https://www.cosumi.net/benchmark.html

ひたすらwhite shadeに手を打たせて、速度を測るというやつです。もちろん、こんなのはまともなベンチマークテストではなく、ただのネタにしかすぎませんので、真面目に使おうとはしないでくださいね。

今現在は、white shadeとの対局はレベル4までしかできませんが、今回のベンチマークはレベル5相当の強さになっています。手元のPython版での計測では、13路盤でGNU Go Level 10に、勝率95.2%、平均42.6目勝つぐらいなのですが、地たくさん勝つ割には、勝率が低くて、なんか安定性がないですね。めいいっぱいの設定にしても、勝率は98%に届かない感じ。でも、この強さでも対局できるように、ここらで一度、対局ページの方も後日アップデートしておきます。

このベンチマーク、私のiPhone 8だと速くて4手/秒ぐらい、GTX 1060(ブラウザが使ってくれているみたい)が載っているPC+google chromeで速くて10手/秒ぐらいになります。それだけ速いのならば、もう少したくさん考えさせてもいいのかもしれませんが、私のクソコードで、ユーザのバッテリーを無駄に消費することになるのは、どうも気が咎めるんですよね…(笑) もうしばらく、シンプルな形で強くならないか、探ってみます。

今回のこのプロダクトを、味気ないベンチマークに飽きた囲碁クラスタに贈ります。とんでもない手も多いですが、そこも含めてお楽しみください。後、これから寒さが厳しくなっていって、スマホをカイロ代わりにしたくなった時にも便利にお使いいただけると思いますので、ぜひお試しください(笑)。

[追記 2020/9/18]
少し書くのが遅くなりましたが、コウの扱いがおかしい時があったのを修正したVer. 1.1にアップデートしました。打つ手は以前のバージョンと同じではありませんが、ベンチマーク結果は基本的に比較可能です。

[追記 2020/12/13]
現在のwhite shadeの対局ゲームとほぼ同じ中身の、Ver. 2.0にアップデートしました。強さはレベル5.8ってぐらいで、全体的に自然な着手になっていると思います。ベンチマークテストの性格上、気軽に上げることができなかったTensorFlow.jsのバージョンも、かなり古いものから最新のにしてあります。ベンチマーク結果については、同じ環境でなら以前のバージョンとほぼ同じぐらいの数字が出るように調整しておきましたが、基本的に比較不可です。ご注意ください。

[追記 2022/4/23]
TensorFlow.jsを最新のバージョンに上げて、囲碁ベンチマークのバージョンをVer. 3.0とします。それ以外の変更はなにもありません。ベンチマーク結果については、当然、以前のバージョンとは比較不可です。全然ちゃんと調べていませんが、ほんのり速くなったような、なってないようなですね。

形勢を目数で返すNNを公開します

囲碁の局面の形勢を、目数で返すNNを公開します。ここ最近も改良しようといろいろと試していたのですが、ほとんど進歩が無かったので、今回公開するのは最新のバージョンですが、数か月前に作成した、今現在white shadeで使っているものと性能はほとんど同じです。

http://www.perfectsky.net/misc/whiteshade_vn-20190902.h5

white shade – 囲碁ブラウザゲーム COSUMI
https://www.cosumi.net/whiteshade.html

13路盤より大きな碁盤サイズには対応していませんが、それより小さい碁盤サイズは、特になにも問題はないと思います(12路盤、10路盤、8路盤とかもOK)。公開するのは、KerasのHDF5形式のファイルですが、TensorFlow.jsのコンバータも私の環境では問題なく通ります。このNNを使っても、そんなに強い思考エンジンは作れないと思いますが、目数で形勢を返すNNは比較的珍しいと思うので、だれかが何か面白いものを作ってくれたら嬉しいです。

以下、使い方を簡単に説明していきます。わたしも本当によく分かっていないので、難しいこと聞かれても、どうせちゃんとお答えできませんので、この説明で分からなかったら、もう諦めちゃってください(笑)。

入力は「盤上」・「次の手番の石」・「相手の石」・「コウで打てない場所」の4面です。「次の手番の石」・「相手の石」は、「黒石」・「白石」ではないので注意してください。出力は「次の手番から見た目数単位での形勢(中国ルール・コミは無いとして)」です。「黒から見た目数単位での形勢」ではないので注意してください。入力は当てはまる所は1、そうでない所は0です。

例えば次の局面の形勢を知りたい場合は、

次のようなコードになります。

# score.py

from keras.models import Model, load_model
import numpy as np

MODEL = load_model('whiteshade_vn-20190902.h5')

TURN       = 'WHITE' # or 'BLACK'
BOARD_SIZE = 9

BOARD = [
    [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
    [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
    [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
    [' ', ' ', 'W', 'B', ' ', ' ', ' ', ' ', ' '], 
    [' ', 'W', 'B', 'K', 'B', ' ', 'B', ' ', ' '], 
    [' ', ' ', 'W', 'B', ' ', ' ', ' ', ' ', ' '], 
    [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
    [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
    [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
]

NUM_PREDICT     =  1
MAX_BOARD_SIZE  = 13
NUM_INPUT_LAYER =  4

ON_BOARD = 0
MY_STONE = 1
YR_STONE = 2
KO       = 3


################################################################################


predict_input = np.zeros((NUM_PREDICT, MAX_BOARD_SIZE, MAX_BOARD_SIZE, NUM_INPUT_LAYER))

for i in range(BOARD_SIZE):
    for j in range(BOARD_SIZE):

        predict_input[0][i][j][ON_BOARD] = 1

        if BOARD[i][j] == 'B':
            if TURN == 'BLACK':
                predict_input[0][i][j][MY_STONE] = 1
            else:
                predict_input[0][i][j][YR_STONE] = 1
        elif BOARD[i][j] == 'W':
            if TURN == 'BLACK':
                predict_input[0][i][j][YR_STONE] = 1
            else:
                predict_input[0][i][j][MY_STONE] = 1
        elif BOARD[i][j] == 'K':
            predict_input[0][i][j][KO] = 1

predict_output = MODEL.predict(predict_input)

if TURN == 'BLACK':
    score = predict_output[0][0]
else:
    score = -(predict_output[0][0])

if score > 0:
    print('B+' + str(score))
else:
    print('W+' + str(abs(score)))

$ python ./score.py 2>/dev/null
B+18.516293

13路盤より小さい碁盤は、13×13のどこに描いても大丈夫だと思っていたのですが、四隅のどれかにぴたっとくっつけたデータでしか学習してなかったせいか、例えば9路盤を13×13のど真ん中に描いてpredictさせると、ちょっと精度が悪くなるような気がします。まあ普通に、0の0から使ってください。

このNNは、まず座標ごとにどちらの地になりそうなのかを予測し、それを集計したような構造になっています。これは、そういうふうにした方が、評価関数として性能が良かったからそうしただけなのですが、結果的にその集計前の途中の出力を、副産物として利用することも一応は可能です。ただ、本当に一応ですね。例えば9路盤をpredictさせると、盤外の座標にもスコアがしっかり付いていて(!)、でもどちらかに偏らないようになっていたりと、13×13全体で上手にバランスをとっている感じなので、まあ参考程度に見てください。例えば、次のようなコードで使えます。activation_103っていう名前のレイヤーの出力を使ってください。

# territory.py

from keras.models import Model, load_model
import numpy as np

OUTPUT_LAYER_NAME = 'activation_103'

MODEL   = load_model('whiteshade_vn-20190902.h5')
MODEL_2 = Model(inputs  = MODEL.input,
                outputs = MODEL.get_layer(OUTPUT_LAYER_NAME).output)

TURN       = 'WHITE' # or 'BLACK'
BOARD_SIZE = 9

BOARD = [
    [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
    [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
    [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
    [' ', ' ', 'W', 'B', ' ', ' ', ' ', ' ', ' '], 
    [' ', 'W', 'B', 'K', 'B', ' ', 'B', ' ', ' '], 
    [' ', ' ', 'W', 'B', ' ', ' ', ' ', ' ', ' '], 
    [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
    [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], 
    [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
]

NUM_PREDICT     =  1
MAX_BOARD_SIZE  = 13
NUM_INPUT_LAYER =  4

ON_BOARD = 0
MY_STONE = 1
YR_STONE = 2
KO       = 3


################################################################################


predict_input = np.zeros((NUM_PREDICT, MAX_BOARD_SIZE, MAX_BOARD_SIZE, NUM_INPUT_LAYER))

for i in range(BOARD_SIZE):
    for j in range(BOARD_SIZE):

        predict_input[0][i][j][ON_BOARD] = 1

        if BOARD[i][j] == 'B':
            if TURN == 'BLACK':
                predict_input[0][i][j][MY_STONE] = 1
            else:
                predict_input[0][i][j][YR_STONE] = 1
        elif BOARD[i][j] == 'W':
            if TURN == 'BLACK':
                predict_input[0][i][j][YR_STONE] = 1
            else:
                predict_input[0][i][j][MY_STONE] = 1
        elif BOARD[i][j] == 'K':
            predict_input[0][i][j][KO] = 1

predict_output = MODEL_2.predict(predict_input)

for i in range(MAX_BOARD_SIZE):
    for j in range(MAX_BOARD_SIZE):

        if TURN == 'BLACK':
            darkness = predict_output[0][i][j][0]
        else:
            darkness = -(predict_output[0][i][j][0])
        
        if darkness > 0.9:
            print ('B', end='')
        elif darkness > 0.5:
            print ('b', end='')
        elif darkness > -0.5:
            print ('.', end='')
        elif darkness > -0.9:
            print ('w', end='')
        else:
            print ('W', end='')
    
    print ('\n', end='')

$ python ./territory.py 2>/dev/null
bw.bBBBBBBWBB
.WWw...wBBWWB
WWwbBBBbBBWWB
WWwbBBBBBBWWB
WWwbBBBBBBWWB
WWwbBBBBBBWWB
WWw.BBBbBBWWB
WWWw...wBBWWB
b..bBBBBBBWWB
BBBBBBBBBBWWB
WBBBBBBBBBBBB
WWWWWWWWWWWWB
WWWWWWWWWWWWW

盤上は、左上の9×9なんですけどね。盤外がなんだか凄まじいことに…(笑)

white shadeの方は、NNの入力に「ダメの数」追加するか、少し深く探索した方が良さそうです。そういったことは無しでも、レベル5までは問題なくいけるのですが、できればレベル6が作りたい…

white shadeで11路盤と13路盤の対局ができるようになりました

今まで9路盤しか無かったwhite shadeで、11路盤と13路盤の対局ができるようになりました。

white shade – 囲碁ブラウザゲーム COSUMI
https://www.cosumi.net/whiteshade.html

少し前にできるようになっていたのですが、ブログ書くの忘れてました… 11路盤なら6子、13路盤なら8子まで石を置くことができます。前にも書きましたが、white shadeの9路盤のレベルごとの強さは、「レベル1はGNU GoのLevel 7に勝率60%、レベル4はFuego1.1の7000playoutに勝率50%、それ以外のレベルは、1レベル違いの自己対戦の勝率が同じ」に合わせていますが、11路盤と13路盤は、「9路盤の同じレベルに勝率50%の人間が勝率50%になるぐらいに」というふわっとした感じに定義しておきたいと思います。通常版の強さと少しずれていくかもしれませんが、とにかくこれでいきたいと思います。

使用した学習データにはほとんど含まれていない碁盤サイズですが、7路盤でもほぼ問題なく打つようなので、これは今度追加するかもしれません。

今回のアップデートでは、動作も大幅に高速になって、ほとんどの環境でサクサクだと思います(もし極端に遅い場合は何かおかしい可能性が高いです)。貴重なバッテリーを大切に使うようになりました。そして、打つ手自体も全体的にかなりきれいになったと思います。石をたくさん置いた時にも、こう打ってほしいなと私が考える理想に近い、個人的に好感もてる打ち方をするようになりました。今現在の一番の問題点は、シチョウがだめなのと、大石のアタリをうっかりする(というか、平たく言うと大石の攻め合いがかなりあやしい)ことですね。でもまあ、これから何とかできそうな気もします。そして、それが本当になんとかなれば19路盤も考えるのですが…

NNのモデルは、碁盤サイズにかかわらず同一のものを使っています。いろいろ考慮するとこれが一番良さそうかなと思いました。もうあと一回、VNとPN作り直して、次のアップデートでレベル6はいけるのではないかと… それで無理なら、少し深く探索してみます。そしてできれば、次のバージョンのVNは一度だけ、自由に使ってもらえるように公開したいなと考えています。13路盤以下だけですが碁盤サイズを選ばず、出力が目数単位のスコアなのは、結構使いやすいと思います。それを使って、だれかが何か素敵なのを作ってくれたりはしないかな?

最初は「とりあえず作ってみました」って感じだったwhite shadeですが、今現在はすでにかなり実用的だと思います。通常版と比較しても、特に大きく劣る点も無い気がしますし、実際、「こっちのほうが好き」って方も多いはず。ぜひ一度、試しに遊んでみてください。

完全にwhite shadeがオフラインでも動くようになりました

COSUMIのwhite shadeは、思考エンジン自体はクライアントサイドで動いているので、今までもオフラインでも対局することができたのですが、そもそもオフラインだとウェブページにアクセスして開くことができなかったので、あまり意味はありませんでした。それを、今まで一度でもオンラインの時にwhite shadeのページにアクセスしたことがあれば、次に開こうとした時に、それが仮にオフラインであっても、ちゃんとページが開けて対局もできるようにしました。

white shade – 囲碁ブラウザゲーム COSUMI
https://www.cosumi.net/whiteshade.html

自分で書いててもどういうことだよって感じで、なんだかとても摩訶不思議なのですが、そういうことができるみたいです。時代の進歩って本当に凄いですね。スマホのホーム画面にwhite shadeのページのアイコンを出しておけば、もうほとんどネイティブアプリと同じように使えます。

私は、Service Workerとか言われても、もうよくわかりませんので、今回はUpUpというライブラリをありがたく使わせてもらいました。ひとつだけ注意点として、オフラインでは対局のリプレイは見れませんので、そこだけはご容赦ください(これは今となっては、まあまあ面倒なんです…)。

今後は、空の旅のお供に、もしくは秘境においてのエクストリーム囲碁の対局相手として(笑)、white shadeをぜひご活用ください。

現在、COSUMIのサーバが落ちています

1月23日の朝6時ぐらいから、COSUMIのサーバが落ちています。現在復旧中ですが、再開時期は未定です。どうかしばらくお待ちください…

[追記 2019/1/23 19:30]
本当に申し訳ないのですが、タイミングが悪いことに私の体調がちょっと良くありません。復旧はどんなに早くても明日24日の早朝、もしかするとかなり遅くなるかもしれません。ご容赦ください。

こんな時こそのwhite shadeですよね… あらかじめperfectsky.netにでも用意しておけばよかったです。今はそんな元気はとてもありません…

[追記 2019/1/24 8:00]
朝までの復旧は間に合いませんでした。申し訳ありません。自信ないですが、お昼にはたぶん… 昨日はインフルエンザに罹ったかもと心配したのですが、どうもそうではなかったようで、そうと分かるとなんか急に元気がでてきました(笑)。

[追記 2019/1/24 12:30]
ごめんなさい。もうお昼も無理です… 夕方!

[追記 2019/1/24 17:30]
とりあえず直りました。ハードウェアのトラブルだったのですが、いろんな事情が重なって時間がかかってしまいました。これはいつものことなのですが、動かし始めた途端、tail -fしたウェブサーバのアクセスログ(ほぼ1行1手)がものすごい勢いで流れ始めて、もう本当に怖いです…(笑) お待たせして本当に申し訳ありませんでした。

囲碁ブラウザゲーム COSUMI
https://www.cosumi.net/

[追記 2019/1/24 18:00]
今頃気づいたのですが、こんなに長い時間COSUMIが止まったのは、今までで初めてですね。しかも断トツで一番だと思います。うーん、その割には私に危機感が無かったかもしれません…(笑)

クライアントサイド版COSUMIを作ってみました

このブログ記事は、以前書いた記事の続きです。よろしければ、そちらもどうぞ。

Keras/TensorFlowでDNNな囲碁の評価関数を作ってみる
http://www.perfectsky.net/blog/?p=350

Keras/TensorFlowでDNNな囲碁の評価関数を作ってみる その2
http://www.perfectsky.net/blog/?p=380

囲碁の思考エンジンを作ってみる
http://www.perfectsky.net/blog/?p=389

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

以前から作っていた囲碁思考エンジン「white shade」を、JavaScriptに書き直してブラウザで打てるようにしてみました。COSUMIのクライアントサイド版です。

white shade – 囲碁ブラウザゲーム COSUMI
https://www.cosumi.net/whiteshade.html

今現在は9路盤しかできませんが、強さはレベル1からレベル4まで選択できるようになっています。それぞれのレベルでの強さは、一応、通常版のとできるだけ合わせましたが、通常版を手元で再現するのがちょっとめんどくさくて、なかなか完璧にはいっていません。一応、クライアントサイド版では、レベル1はGNU GoのLevel 7に勝率60%、レベル4はFuego1.1の7000playoutに勝率50%、それ以外のレベルは、1レベル違いの自己対戦の勝率が同じになるようにしていきます(これでまあだいたい通常版と同じです)。最終的には、通常版にも無いレベル7ぐらいまで行きたいですね。まだやれることはたくさんあるので、それぐらいはなんとかなりそうな気はしています。現状は、目一杯の設定で、だいたいレベル4.5ぐらい。GNU Goに一局あたり平均16目ぐらい勝てるのですが、それでも勝率は90%を辛うじて超える程度で、100%っていうのはやはりかなり大変そうですね。

レベル1では、4子までの置き碁もできるようにしました。GNU Goにこれをさせると怪しいことになるのでちょっとあれなんですが、white shadeは、どれだけ形勢が悪くても結構自然に打つので、特に問題は無さそうです。今後はもっと大きな碁盤サイズでも対局できるようにしていきますが、8路盤以下は、もうこれで許してください…

JavaScriptのDNNライブラリ(って呼んでいいのかな?)は、TensorFlow.jsを使っています。私も使えたので(笑)、たぶんそんなに難しいものではないです。元々のPython版white shadeも、GTPとかデバッグ用のコードとかもろもろ除けば、実質200行ぐらい(?)のプログラムだったので、JavaScriptに書き直すのも大した手間ではありませんでした。一番たいへんだったのは、先ほどの強さの調整ですね。きれいに弱くするということがこんなに難しいことだとは、本当に思っていませんでした。後、少し心配しているのが、JavaScript版にした時に、NNのモデルのコンバートなんかで弱くなってたりしていないかなんですが、自分が打っている限りでは、大丈夫なように見えます。JavaScript版の強さの計測は、これも今後ちょっと厄介ですね。

簡単にwhite shadeの中身についても、書いておくと、基本的に、左上から右下まで本当に全幅で1手読んでるだけですが、人の手のみを学習したPolicy Networkの出力も少しだけ加味して手を決定していて、あとは、自然に打てるように微調整ですね。レベル4が少し重いかもですが、それ以外のレベルは、ちゃんと動きさえする環境ならば、おそらくサクサクだと思います。で、問題はそのちゃんと動く動作環境なんですが、今現在、iPhone/iPadのiOS系が安定して動かないと思います(後、IEもですがこれはもう本当にどうでもいい。Androidはちょっと分かっていません)。これは結構色々調べたのですが、まず、iPad+Chromeはだめで、Mac+Safariは大丈夫なので、iOSがだめっぽいのですが、NNのモデルを小さいものに変更するとかなり安定するようになるので(ちなみに今現在、Value Networkが92万、Policy Networkが45万パラメータぐらい。本当はもっと大きなNN使いたいぐらいなのに…)、端末が非力なことが単純に問題なのかもしれません。とはいえ、とりあえず動くだけは動いて欲しいのですが… iPhoneで動かない限り、トップページからのリンクもちょっと張れません。これはまた、なんとかします。

今現在、COSUMIは年間のサーバ代(最初に掛かった初期費用は入れず)が130万ぐらい掛かっているのですが、それがこのクライアントサイド版でいつか半分ぐらいにならないかなあと、つい皮算用してしまいます。私は今、車が欲しいんです(笑)。生まれてこのかた、一度も車なんて買ったことないのですが、今猛烈に欲しいんですね。軽でいいんですけど、新車が欲しい(笑)。そのためにも、このサーバ代はなんとかしなければいけません。話変わりますが、なんかネット見ていたら、さくらインターネットからお中元が来たって方がちょくちょくいるのですが、今までに新車のポルシェ一台分ぐらい貢いだ私はもらったことがない!(笑) うー、まだ足らないのかな… おいらもチョコが食べたい。

[追記 2018/9/2]
今回は、white shadeで囲碁の対局をできるようにしたわけですが、いつかは、9路盤以下の悪手指摘機能をこれで置き換えたいですし、もっと言うと、white shade Teach作りたいですね。この場合、teachするのは、囲碁というよりも、white shadeの囲碁に対する気持ちぐらいでしかありませんが(笑)、それでも、初心者の方には、十分有益なような気がします。忙しいので当面の間は無理ですが、またいつかがんばります。

[追記 2019/1/5]
ブラウザがロードするTensorFlow.jsのライブラリのバージョンを上げたら、iOSでもwhite shadeが動くようになったみたいです(やほい!)。ひさしぶりに私も対戦してみましたが、この子、そんなに弱くはないのですが、ときどきとんでもない転び方するので、ちょっと面白いです。ぜひiPhoneで一局打ってみてください。

今現在、9路盤以外でも対局できるように準備していますので、そちらはもうしばらくお待ちください。

囲碁の思考エンジンを作ってみる

このブログ記事は、以前書いた記事の続きです。できれば、まずはそちらをお読みください。

Keras/TensorFlowでDNNな囲碁の評価関数を作ってみる
http://www.perfectsky.net/blog/?p=350

Keras/TensorFlowでDNNな囲碁の評価関数を作ってみる その2
http://www.perfectsky.net/blog/?p=380

時間ができたので、以前から作っていたDNNな囲碁の評価関数を使って、囲碁の思考エンジンを作ってみました。「パスも含めて全幅で深さ1だけ読む」という単純なプログラムです。9路盤しか打てません。一応、名前も必要かと思ったので、コードネームだったのをそのまま使って、white shadeと名づけました。由来は、Procol Harumの例の曲です。特にそれ以上の深い意味はありません。ちなみにこの映像は、ちょうど今から50年前のものみたいですが、ポピュラー音楽って本当に進歩がないですね。コンピュータ囲碁は、この5年だけでもめっちゃくちゃ強くなったのに…(笑)

ということで、早速、GNU Goとの対戦を行ってみました。使用した評価関数は、BottleneckアーキテクチャになっているRes-Blockのネックの部分が、32Filterなのと48Filterなのとの2種類。共に10 Res-Block(ちなみに、32Filterはパラメータ数が210,769で、48Filterは368,529。できれば、このあたりのサイズで何とかしたい…)。それぞれ、8対称形の平均をとったのと、とらないのとの、計4種類です。対局数は、先後を換えて150局ずつ計300局。同じような対局ばかりになりがちなので、twogtpに付属していたオープニングブックを使用しています。結果は、

32Filter 106勝194敗 (勝率 35.33%)
32Filter/8対称形の平均 144勝156敗 (勝率 48.00%)
48Filter 128勝172敗 (勝率 42.67%)
48Filter/8対称形の平均 176勝124敗 (勝率 58.67%)

うーん、よくわからんけどまあこんなものかな? とりあえず、ここがスタートですね。棋譜を見ていると、序盤はかなり上手なんですが、この子どうやらアタリがよく分かってないみたいで(笑)、後半すさまじいファンタを見せてくれます。一番強い48Filterの8対称形平均版から適当に3局選んでみたので、ご覧ください。


Sorry, your browser doesn’t support WGo.js.

Sorry, your browser doesn’t support WGo.js.

Sorry, your browser doesn’t support WGo.js.

こんなのに半分以上負けるGNU Goもどうなのよって感じですが(笑)、まあ強い時は強いからしかたないか… でもって、何でこんなにアタリがわからないのかっていうと、いろいろ理由はあるんでしょうが、おそらく一番大きいのは、学習データにこういう局面があまり含まれていないからだと思います。もちろん、大石がアタリになっている局面はそれなりの数あるのですが、そのほとんどが、アタリにされている方の手番になっていて、つぐなり逃げるなりすれば大事にならないので、それで深刻なことだと学習できていない気がします。NNの入力にダメの数を入れるとか、深さ2読むとかしたら、ここまでひどいことにはたぶんならないと思いますが、そんなことしなくても評価関数だけでこれぐらいは分かってほしいですし、こんなことも分からなくて、もっと高度なことが分かるはずもないような気がするので、なんとかしたいのですが、どうするのがいいかな? 「いっぱい対局させて、それをRayに添削してもらって、酷そうな手の前後を学習データに追加していく」みたいな感じでだめかな? また少し試してみます。

9路盤での最終的な目標は、GNU Goに対して1局平均10目勝ちです(今はだいたいイーブンぐらい)。勝率はあまり気にせず、そこを目指していきたいと思っています。そこまでいけたら、ブラウザで打てるようにしたいですね。

いろいろやっている間に、Rayが出してくれる形勢判断が常に1目ずれていること(黒番の時と白番の時で向きが逆、平均すれば0。簡易的な日本ルール対策?)に気づいて、その分を修正しようとしたのですが、今度は別のところで矛盾が生じてきて絶賛混乱中です。もう一目ぐらいどうでもいいか… あと、現在、Policy Networkも作っています。Value Networkもそうですが、よくこんなのでちゃんとしたアウトプットが出てきますね… なんだか、狐につままれた気分です。

あとあと、CapsNetで囲碁やった人とかいないんでしょうか?

[追記 2018/5/6]
最近、Policy Networkを作っているのですが、学習データを普通の棋譜からランダムに切り出して使ったりすると、結構ラベルに偏りが出てくるのが気になります。ということで、囲碁で一局を通して、座標ごとにどれぐらいの回数打たれるのかっていうのを調べてみました。例えば、COSUMIの9路盤のレベル1の作り碁ならこんな感じ。一番打たれる回数の多い場所を100として、それとの割合です。

 14  29  40  52  59  52  40  29  14
 30  44  56  68  71  67  55  44  30
 40  56  76  86  88  86  75  56  41
 53  68  87  96  95  96  86  68  54
 60  73  89  95 100  95  88  73  60
 54  68  87  97  94  95  86  68  53
 41  57  76  86  87  85  75  56  41
 30  45  56  68  71  67  56  44  30
 15  31  41  53  59  52  40  30  14

そして、レベル5ではこんな感じです。

 24  48  56  67  71  67  57  48  24
 48  64  74  82  85  82  74  64  48
 57  74  89  95  97  95  89  75  57
 67  83  96  99  98  99  95  83  67
 71  86  97  99  99  98  97  86  72
 67  83  95 100  98  99  95  83  68
 57  74  89  95  96  94  88  74  56
 48  65  74  83  86  83  74  64  47
 25  48  57  67  71  67  57  48  25

どうでしょう、ちょっと不安になってきませんか?

今現在、学習に使っているデータは、COSUMIの棋譜から取って、いくつかの条件でふるいをかけたものですが、それの検証用データのラベルの合計がこちら。これを[1]とします。

 1628 2786 3627 4372 4508 4372 3627 2786 1628
 2786 4038 4507 5506 6126 5506 4507 4038 2786
 3627 4507 5296 6662 6550 6662 5296 4507 3627
 4372 5506 6662 8024 6928 8024 6662 5506 4372
 4508 6126 6550 6928 7928 6928 6550 6126 4508
 4372 5506 6662 8024 6928 8024 6662 5506 4372
 3627 4507 5296 6662 6550 6662 5296 4507 3627
 2786 4038 4507 5506 6126 5506 4507 4038 2786
 1628 2786 3627 4372 4508 4372 3627 2786 1628

そして、そのデータと同じ作り方をしている学習用データで学習したNNで、先ほどの検証用データを予測させた時の最後のsoftmaxの出力をそのまま合計したのがこちら(この数字をここで使うことが正しいのかがちょっと確信持てませんが…)。これを[2]とします。

 1584 2745 3695 4300 4598 4301 3668 2754 1594
 2735 3890 4561 5532 6039 5525 4578 3922 2746
 3678 4605 5399 6763 6706 6754 5334 4583 3663
 4272 5503 6705 7787 7231 7720 6611 5479 4303
 4623 5987 6656 7232 7764 7128 6517 5946 4598
 4308 5513 6673 7657 7236 7645 6586 5511 4306
 3756 4626 5447 6669 6662 6638 5312 4612 3694
 2820 3924 4578 5491 5972 5544 4613 3944 2766
 1609 2763 3694 4276 4583 4272 3667 2726 1592

それぞれの座標で、[2]/[1]*100したのがこちら。

  97  99 102  98 102  98 101  99  98
  98  96 101 100  99 100 102  97  99
 101 102 102 102 102 101 101 102 101
  98 100 101  97 104  96  99 100  98
 103  98 102 104  98 103 100  97 102
  99 100 100  95 104  95  99 100  98
 104 103 103 100 102 100 100 102 102
 101  97 102 100  97 101 102  98  99
  99  99 102  98 102  98 101  98  98

ほんの少しだけ、それっぽい傾向が見受けられるような気もしますが、まあこれぐらいならぜんぜんOKでしょうかね? とりあえずは気にしないことにします。

[追記 2018/5/25]
「white shadeの棋譜をRayに添削してもらって、悪手っぽいところの前後を学習データに追加して、それをもう一度学習する」ってやり方で、いきなりGNU Goに1局平均10目ぐらい勝てるようになったのですが、それってそれなりの棋力がないとできないはずだと思って実際に棋譜を眺めてみても、そこまで強そうには見えません。どうも、最後にねちねちやられてGNU Goが自爆していることが、ちょくちょくあるからみたいです。手法自体はかなり有効そうなので、目標を「1局平均20目」に変更して、現在、二周目やってます。

[追記 2018/8/29]
ブラウザで対局できるようにしてみました。続きの記事をどうぞ。

クライアントサイド版COSUMIを作ってみました
http://www.perfectsky.net/blog/?p=402

Keras/TensorFlowでDNNな囲碁の評価関数を作ってみる その2

このブログ記事は、以前書いた記事の続きです。できれば、まずはそちらをお読みください。

Keras/TensorFlowでDNNな囲碁の評価関数を作ってみる
http://www.perfectsky.net/blog/?p=350

ずいぶん長い間ほったらかしにしていたのですが、そろそろ自分でも、囲碁の思考エンジンを作ってみたいと思い、ここ最近、久しぶりに以前作っていたディープラーニングな評価関数の作成の続きをやっています。

ただ、思いつくことはある程度、前回の時に試していたこともあって、ほとんどの試行はたいした改良に繋がらないのですが、その中で唯一、非常に大きく数字が改善したのが、Squeeze-and-Excitation Networks(SENet)というやつです。

[1709.01507] Squeeze-and-Excitation Networks
https://arxiv.org/abs/1709.01507

このモデルがどのようなものかを解説するのは、私にはちょっと難しいので、詳しくはリンク先を読んでいただくとして、以下簡単に、私が試してみたテスト内容とその結果を書いてみたいと思います。

現在、最終的にはクライアントサイドで思考エンジンが動くウェブアプリの制作を目標にしていて、その関係もあって、とりあえず今回は9路盤です。データの作成方法などは前回とほぼ一緒。対称形に8倍して切りの良い数字にまで少し減らして、230万局面分。95%を学習用に、5%を検証用に使います。

NNのモデルは、基本的に、前回の最後の方で使っていた普通のResNetみたいなのが性能良いのでは、と思っているのですが、今回は非力なスマホなどでも動かしたいので、できるだけ小さなモデルにしなければいけません。特に、パラメータ数は、モデルのファイルサイズになってネットワークの転送量とかにまで影響してくるので、少ないにこしたことはないように思います。ということで、Residual Block内は1×1 -> 3×3 -> 1×1のいわゆるBottleneckアーキテクチャにしました。そもそも、たかだか19×19の囲碁で、3×3のConvが30も50も重なるのって、なんかおかしいような気が以前からしていて、なんというか、そんな遠くの場所よりも、まずはもっと近いところとの関係をよく見ないといけないのではと、つい思ってしまうんですよね… 9路盤なんか、たった4つの3×3のConvで、天元のところにすべての座標の入力の情報が来るわけで、そういう意味でも、3×3を一定量1×1に置き換えるのは、理にかなっているような気がしています。「5×5は3×3が2つの方が良いように、3×3はdepthwiseとpointwiseに分けたほうが良い」みたいなことを言われてしまうと、確かに3×3のConvはちょっと大きすぎですよね… 囲碁だったら、四隅の欠けた3×3の、「十字型」なんかどうなんでしょうか?

ってすみません。話がそれてしまいました。元に戻って今回のNNのモデルですが、前回からの変更点としてもうひとつ、入力層の所でまず、周囲をゼロパディングして、9×9だったフィールドを13×13に広げています。これはパラメータ増やさず、ロスを下げます。やっぱり9×9って小さすぎるんですよね、ってまた似たような話に…(笑)

入力は、「手番のプレーヤーの石の配置」、「相手の石の配置」、「コウで打てない場所」、「全部1」の4面(9,9,4)です。最後の「全部1」と、先ほどの入力層でのゼロパディングで、盤上/盤外を表現したつもりです。

その他の条件は、だいたい前回と同じかな?

コードはこんな感じ。まずは「SENetなし」。

BOARD_SIZE = 9
FIELD_SIZE = 13


def rn_block(input):

    relu_1 = Activation("relu")(input)
    bn_1   = BatchNormalization()(relu_1)
    conv_1 = Conv2D(32, (1, 1))(bn_1)

    relu_2 = Activation("relu")(conv_1)
    bn_2   = BatchNormalization()(relu_2)
    conv_2 = Conv2D(32, (3, 3), padding='same')(bn_2)

    relu_3 = Activation("relu")(conv_2)
    bn_3   = BatchNormalization()(relu_3)
    conv_3 = Conv2D(128, (1, 1))(bn_3)

    return conv_3


input = Input(shape=x_train.shape[1:])

main    = ZeroPadding2D(padding=(int((FIELD_SIZE-BOARD_SIZE)/2), int((FIELD_SIZE-BOARD_SIZE)/2)))(input)
rn_fork = Conv2D(128, (3, 3), padding='same')(main)

main    = rn_block(rn_fork)

rn_fork = add([main, rn_fork])

main    = rn_block(rn_fork)

rn_fork = add([main, rn_fork])

main    = rn_block(rn_fork)

rn_fork = add([main, rn_fork])

main    = rn_block(rn_fork)

rn_fork = add([main, rn_fork])

main    = rn_block(rn_fork)

rn_fork = add([main, rn_fork])

main    = rn_block(rn_fork)

main    = add([main, rn_fork])

main    = Activation("relu")(main)
main    = BatchNormalization()(main)
main    = Conv2D(1, (3, 3), padding='valid')(main)
main    = AveragePooling2D(pool_size=(FIELD_SIZE-2, FIELD_SIZE-2))(main)

output  = Flatten()(main)

そして「SENetあり」。

BOARD_SIZE = 9
FIELD_SIZE = 13


def rn_block(input):

    relu_1 = Activation("relu")(input)
    bn_1   = BatchNormalization()(relu_1)
    conv_1 = Conv2D(32, (1, 1))(bn_1)

    relu_2 = Activation("relu")(conv_1)
    bn_2   = BatchNormalization()(relu_2)
    conv_2 = Conv2D(32, (3, 3), padding='same')(bn_2)

    relu_3 = Activation("relu")(conv_2)
    bn_3   = BatchNormalization()(relu_3)
    conv_3 = Conv2D(128, (1, 1))(bn_3)

    return conv_3


def se_block(input):

    ap      = AveragePooling2D(pool_size=(FIELD_SIZE, FIELD_SIZE))(input)
    conv_1  = Conv2D(8, (1, 1))(ap)
    relu    = Activation("relu")(conv_1)
    conv_2  = Conv2D(128, (1, 1))(relu)
    sigmoid = Activation("sigmoid")(conv_2)
    us      = UpSampling2D(size=(FIELD_SIZE, FIELD_SIZE))(sigmoid)

    return us


main    = ZeroPadding2D(padding=(int((FIELD_SIZE-BOARD_SIZE)/2), int((FIELD_SIZE-BOARD_SIZE)/2)))(input)
rn_fork = Conv2D(128, (3, 3), padding='same')(main)

#main    = rn_block(rn_fork)
se_fork = rn_block(rn_fork)
se_out  = se_block(se_fork)
main    = multiply([se_fork, se_out])

rn_fork = add([main, rn_fork])

#main    = rn_block(rn_fork)
se_fork = rn_block(rn_fork)
se_out  = se_block(se_fork)
main    = multiply([se_fork, se_out])

rn_fork = add([main, rn_fork])

#main    = rn_block(rn_fork)
se_fork = rn_block(rn_fork)
se_out  = se_block(se_fork)
main    = multiply([se_fork, se_out])

rn_fork = add([main, rn_fork])

#main    = rn_block(rn_fork)
se_fork = rn_block(rn_fork)
se_out  = se_block(se_fork)
main    = multiply([se_fork, se_out])

rn_fork = add([main, rn_fork])

#main    = rn_block(rn_fork)
se_fork = rn_block(rn_fork)
se_out  = se_block(se_fork)
main    = multiply([se_fork, se_out])

rn_fork = add([main, rn_fork])

#main    = rn_block(rn_fork)
se_fork = rn_block(rn_fork)
se_out  = se_block(se_fork)
main    = multiply([se_fork, se_out])

main    = add([main, rn_fork])

main    = Activation("relu")(main)
main    = BatchNormalization()(main)
main    = Conv2D(1, (3, 3), padding='valid')(main)
main    = AveragePooling2D(pool_size=(FIELD_SIZE-2, FIELD_SIZE-2))(main)

output  = Flatten()(main)

「SENetなし」はResidual Blockが6つと7つの2種類、「SENetあり」はResidual Blockが6つの、計3種類をテストしてグラフにしてみました。

「SENetなし/Residual Block 7つ」と「SENetあり」は、パラメータ数、予測に掛かる時間、1エポックあたりの学習時間などがそれほどは大きく変わらず、それでいてこのロスの差なので、すばらしいです。ILSVRC2017チャンプは伊達ではない(笑)。しばらく忙しいのですぐには無理そうですが、いずれこいつを使って一手全幅君を作ってみたいと思います。

[追記 2018/4/4]
現在使用している学習データのラベルは、Rayに付けてもらったものですが、それをそのデータを学習したDNNで付け替えて、もう一度最初から学習し直したらどうなるのか、試してみました。

学習する局面は上と同じ230万局面分。95%を学習用、5%を検証用に。ネットワーク構成も上のSENetありと基本的に同じで、10 res-blockです。今回の複数のテストでの唯一の違いは学習データのラベルで、まずは次の3種類、

  • [1] Train/ValidateともRayが付けたもの
  • [2] Trainを[1]の50エポック目のDNNが付け、ValidateはRayが付けたもの
  • [3] Train/Validateとも[1]の50エポック目のDNNが付けたもの

です。[3][2]とTrainのラベルが同じなので、Validateだけ調べれば良かったのですが、実際にやってみると、想像以上に低い数字が出て来て自分の書いたコードが信用できなくなり(笑)、念のために、いつもと同じように最初から学習回しながら、Validateを計測してみました(どうやら、自分の書いたコードは合ってたみたい…)。乱数の加減も今回はあまり関係無かったようで、赤の実線は緑の実線にきれいに隠れていますが、そこにあります(一応、少し太くしておいた(笑))。

正直、驚きの結果です。DNNに予測させるのは、Rayにラベルを付けてもらうより、遥かにコストが掛かからないので、「もし、DNNが付けたラベルでそれなりに学習できたら、データの水増しが可能になるかも」ぐらいに思っていたのですが、ばっさりと全部差し替えても全く問題なさそうですし、グラフ見ているだけでははっきりしませんが、囲碁の神様が付けたラベルに対して、[1]より[2]/[3]の方が性能が高い可能性までありそうに見えます。しかし、そんなうまい話本当にあるのかなあ? どうも信じられないのですが…

以前にも書きましたが、同じ局面の対称形をDNNで予測させると、結構ばらばらな数字を返してくるので、

  • [4] Trainを[1]の50エポック目のDNNが予測した8対称形すべての平均にして、ValidateはRayが付けたもの

もテストしてみました。

このブログには書いていませんが、以前Trainのラベルに平均0の乱数を混ぜて学習させてみたことがあったのですが、その時も意外とValidateの数字が大きく悪くはなったりせず(もちろんTrainはノイズの分がっつり悪くなります)、たくさんのデータで鍛えるとそんなものなんだなあと思ったことがあったのですが、今回の[2]は、[4]に平均0の乱数を混ぜたようなものなので、似たような結果と言えるでしょうか、ってじゃあやっぱり精度の高い予測が欲しい時は、平均とって使った方が良さそうですね。うーん、めんどくさ…

[追記 2018/4/30]
続きの記事があります。

囲碁の思考エンジンを作ってみる
http://www.perfectsky.net/blog/?p=389

Keras/TensorFlowでDNNな囲碁の評価関数を作ってみる

「囲碁をディープラーニングするのは面白い」という噂なので(笑)、私も試しに一度やってみることにしました。作るならやっぱり評価関数。それも、その時の形勢を「目数」で教えてくれるやつがなんかいいですよね? とりあえず今回は19路盤用です。

まずは学習に使うデータについてです。とりあえず評価する局面は、COSUMIで打たれた19路盤互先の作り碁の棋譜から作りました。GNU Go、強い人、弱い人、意図した序盤早々の連続パス、意図しないクリックミスの混ぜ合わさった様々なよく分からない局面が出現しそうで、まあ良いのではないかと…(笑) まず、最後のパスパスを取り除き、1手から最終手の間の一様乱数にまで棋譜の手数を短くして、さらに対称形を考慮しない完全な重複分を取り除き、残った棋譜の最終局面を使うことにしました。

そして次に、その局面に付けるラベル、今回の場合は「目数単位の形勢判断」ですが、うーん、これが本当にどうするのが良いのか… とりあえず、今回の作成方法は以下のとおりです。

  • まず先ほど作った局面を、コミ6目でRayの2k playoutに考えさせます
  • 返ってきたwin rateが0.5に近づく方向にコミを10目ずらして、もう一度Rayに考えさせます
  • それをwin rateが0.5の反対側に行くまで、繰り返します
  • 0.5をまたいだ2点を結んで、0.5と交わるところを「大体の形勢」とします
  • 再度、コミを「大体の形勢」として、今度はRayの20k playoutに考えさせます
  • 返ってきたwin rateが0.5に近づく方向に、今度はコミを4目ずらして、もう一度Rayに考えさせます
  • 先ほどと同じように、それをwin rateが0.5の反対側に行くまで、繰り返します
  • 先ほどと同じように、0.5をまたいだ2点を結んで、0.5と交わるところを「最終的な形勢」とします

あまりにも素朴すぎる気はしますが、こんな感じで作りました。前半は消費リソースを減らすためにやっているだけなので、後半だけを行っても当然似たようなラベルができるはずです。

最初のころは、これを10,000局面分作っていろいろ試していたのですが、ちょっと遊んでみたいだけとはいえ、それではあまりにも少なすぎたので、50,000局面分まで増やしました。そしてそれを対称形に8倍して、ここでもう一度重複分を除去し、きりの良い数字にまで少し減らして399,000局面分できました。今回は、その内80%の319,200局面分を学習用に、残りの20%の79,800局面分を検証用に使用します。

ここまで、学習データは用意できましたので、次に実際に学習を始めます。

今回の実行環境は、

  • Amazon EC2 p2.xlarge
  • Ubuntu 16.04 LTS
  • CUDA 8.0
  • cuDNN 5.1

です。最初、手元のGPUなしのWindowsマシン(CPU:Intel Core i5-3470S メモリ:16GB)でいろいろ試していたのですが、実際に学習が動き始めると、さすがにやはりちょっと遅すぎるので、EC2使いました。学習内容によって結構変わってくるみたいですが、だいたい12倍ほど速かったです。もう少し速いとうれしいのですが、しかたないでしょうか?

DNNのフレームワークには、バックエンドにTensorFlowを使ったKerasを使ってみました。

Keras
https://keras.io/

TensorFlow
https://www.tensorflow.org/

Kerasはとても分かりやすくて、私のような素人には本当にありがたい。かなりおすすめです。TensorFlowもですが、本家のドキュメントがしっかりしているのがいいですよね。例えば、今回のケースだと、こんな感じのコードになります。

import numpy as np
from keras.models import Sequential
from keras.layers import Activation, AveragePooling2D, Conv2D, Flatten
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.normalization import BatchNormalization
from keras.optimizers import Adam

BATCH_SIZE   = 200
EPOCHS       = 20

x_train = np.load('x_train.npy');
y_train = np.load('y_train.npy');
x_test  = np.load('x_test.npy');
y_test  = np.load('y_test.npy');

model = Sequential()

model.add(Conv2D(32, (3, 3), padding='valid', input_shape=x_train.shape[1:]))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(1, (1, 1), padding='valid'))
model.add(AveragePooling2D(pool_size=(13, 13)))
model.add(Flatten())

model.summary()

model.compile(loss      = 'mean_absolute_error',
              optimizer = Adam())

model.fit(x_train, y_train,
          batch_size      = BATCH_SIZE,
          epochs          = EPOCHS,
          verbose         = 1,
          validation_data = (x_test, y_test))

驚くほどシンプルに書けます。

今回は、以下すべての場合において(ただし、追記に関してはこの限りではありません)、

バッチサイズ 200
エポック数 20
損失関数 平均絶対誤差(Mean Absolute Error)
最適化アルゴリズム Adam(パラメータはKerasのデフォルト)

です。バッチサイズは、実行速度などに影響がかなり大きいです。エポック数は、収束していなくても、過学習していても、なにがあっても、今回は一定でいきたいと思います。

ネットワークへの入力は、とりあえず最初、「次の手番のプレーヤーの石」と「相手のプレーヤーの石」の2面(19,19,2)、数値は0と1です。ちなみにですが、今回の学習データのラベルは、平均5.7、標準偏差32.9、平均偏差21.5ぐらいです。なので、とりあえず盤面見ないで「黒5.7目形勢が良い」って答えておけば、Lossは21.5にはなりますので(どちらが黒か教えませんので、実際はもう少し難しいはずですが)、最終的にその数字がどれくらい0に近づくのか、っていう感じで見てもらうと良いと思います。

それではいってみましょう。まず最初に考えたのはこんなネットワーク構成でした。

model.add(Conv2D(32, (3, 3), padding='valid', input_shape=x_train.shape[1:]))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(1, (1, 1), padding='valid'))
model.add(Activation('relu'))
model.add(AveragePooling2D(pool_size=(13, 13)))
model.add(Flatten())

model.summary()が吐いてくれるネットワークの要約がこちら。

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 17, 17, 32)        608       
_________________________________________________________________
activation_1 (Activation)    (None, 17, 17, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 15, 15, 32)        9248      
_________________________________________________________________
activation_2 (Activation)    (None, 15, 15, 32)        0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 13, 13, 32)        9248      
_________________________________________________________________
activation_3 (Activation)    (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 13, 13, 1)         33        
_________________________________________________________________
activation_4 (Activation)    (None, 13, 13, 1)         0         
_________________________________________________________________
average_pooling2d_1 (Average (None, 1, 1, 1)           0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 1)                 0         
=================================================================
Total params: 19,137.0
Trainable params: 19,137.0
Non-trainable params: 0.0

パタパタパタと畳んで、ペタンを押しつぶして、フワーと見るイメージなんですが(笑)、ところがこれ、全く学習してくれません。

Epoch 1/20
319200/319200 [==============================] - 29s - loss: 22.3445 - val_loss: 21.9808
Epoch 2/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 3/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 4/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 5/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 6/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 7/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 8/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 9/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 10/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 11/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 12/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 13/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 14/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 15/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 16/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 17/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 18/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 19/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808
Epoch 20/20
319200/319200 [==============================] - 25s - loss: 22.3445 - val_loss: 21.9808

試しに、活性化関数をtanhに変更してみます。

model.add(Conv2D(32, (3, 3), padding='valid', input_shape=x_train.shape[1:]))
model.add(Activation('tanh'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('tanh'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('tanh'))
model.add(Conv2D(1, (1, 1), padding='valid'))
model.add(Activation('tanh'))
model.add(AveragePooling2D(pool_size=(13, 13)))
model.add(Flatten())
Epoch 1/20
319200/319200 [==============================] - 29s - loss: 22.2877 - val_loss: 21.9071
Epoch 2/20
319200/319200 [==============================] - 26s - loss: 22.2663 - val_loss: 21.8958
Epoch 3/20
319200/319200 [==============================] - 26s - loss: 22.2548 - val_loss: 21.8842
Epoch 4/20
319200/319200 [==============================] - 26s - loss: 22.2462 - val_loss: 21.8796
Epoch 5/20
319200/319200 [==============================] - 26s - loss: 22.2417 - val_loss: 21.8763
Epoch 6/20
319200/319200 [==============================] - 26s - loss: 22.2386 - val_loss: 21.8739
Epoch 7/20
319200/319200 [==============================] - 26s - loss: 22.2370 - val_loss: 21.8727
Epoch 8/20
319200/319200 [==============================] - 26s - loss: 22.2341 - val_loss: 21.8692
Epoch 9/20
319200/319200 [==============================] - 26s - loss: 22.2325 - val_loss: 21.8677
Epoch 10/20
319200/319200 [==============================] - 26s - loss: 22.2305 - val_loss: 21.8665
Epoch 11/20
319200/319200 [==============================] - 26s - loss: 22.2290 - val_loss: 21.8653
Epoch 12/20
319200/319200 [==============================] - 26s - loss: 22.2273 - val_loss: 21.8669
Epoch 13/20
319200/319200 [==============================] - 26s - loss: 22.2259 - val_loss: 21.8618
Epoch 14/20
319200/319200 [==============================] - 26s - loss: 22.2245 - val_loss: 21.8624
Epoch 15/20
319200/319200 [==============================] - 26s - loss: 22.2234 - val_loss: 21.8621
Epoch 16/20
319200/319200 [==============================] - 26s - loss: 22.2221 - val_loss: 21.8590
Epoch 17/20
319200/319200 [==============================] - 26s - loss: 22.2208 - val_loss: 21.8604
Epoch 18/20
319200/319200 [==============================] - 26s - loss: 22.2197 - val_loss: 21.8587
Epoch 19/20
319200/319200 [==============================] - 26s - loss: 22.2191 - val_loss: 21.8621
Epoch 20/20
319200/319200 [==============================] - 26s - loss: 22.2178 - val_loss: 21.8557

ちょびっとだけ数字が動いた…(笑) 今度はLeakyReLUに。

model.add(Conv2D(32, (3, 3), padding='valid', input_shape=x_train.shape[1:]))
model.add(LeakyReLU(alpha=0.1))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(LeakyReLU(alpha=0.1))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(LeakyReLU(alpha=0.1))
model.add(Conv2D(1, (1, 1), padding='valid'))
model.add(LeakyReLU(alpha=0.1))
model.add(AveragePooling2D(pool_size=(13, 13)))
model.add(Flatten())
Epoch 1/20
319200/319200 [==============================] - 34s - loss: 21.7510 - val_loss: 20.9956
Epoch 2/20
319200/319200 [==============================] - 30s - loss: 21.1638 - val_loss: 20.7552
Epoch 3/20
319200/319200 [==============================] - 30s - loss: 20.8117 - val_loss: 20.4443
Epoch 4/20
319200/319200 [==============================] - 30s - loss: 20.5938 - val_loss: 20.1891
Epoch 5/20
319200/319200 [==============================] - 30s - loss: 20.3966 - val_loss: 19.9154
Epoch 6/20
319200/319200 [==============================] - 30s - loss: 20.1918 - val_loss: 19.6722
Epoch 7/20
319200/319200 [==============================] - 30s - loss: 20.0177 - val_loss: 19.7719
Epoch 8/20
319200/319200 [==============================] - 30s - loss: 19.8868 - val_loss: 19.3948
Epoch 9/20
319200/319200 [==============================] - 30s - loss: 19.7355 - val_loss: 19.4068
Epoch 10/20
319200/319200 [==============================] - 30s - loss: 19.5322 - val_loss: 19.0625
Epoch 11/20
319200/319200 [==============================] - 30s - loss: 19.3279 - val_loss: 19.1659
Epoch 12/20
319200/319200 [==============================] - 30s - loss: 19.1512 - val_loss: 18.8860
Epoch 13/20
319200/319200 [==============================] - 30s - loss: 18.8963 - val_loss: 18.6369
Epoch 14/20
319200/319200 [==============================] - 30s - loss: 18.6399 - val_loss: 18.4589
Epoch 15/20
319200/319200 [==============================] - 30s - loss: 18.4826 - val_loss: 18.1423
Epoch 16/20
319200/319200 [==============================] - 30s - loss: 18.3363 - val_loss: 18.1451
Epoch 17/20
319200/319200 [==============================] - 30s - loss: 18.1859 - val_loss: 18.0372
Epoch 18/20
319200/319200 [==============================] - 30s - loss: 18.0898 - val_loss: 17.8348
Epoch 19/20
319200/319200 [==============================] - 30s - loss: 18.0122 - val_loss: 17.7273
Epoch 20/20
319200/319200 [==============================] - 30s - loss: 17.9057 - val_loss: 17.9030

おお、がっつり動き始めました! ここで、なんとなく分かりましたよ。現在のネットワーク構成では、一番最後の活性化関数の後ろに、もう畳み込み層や全結合層がありません。活性化関数がひとつ余分なんですね。ReLUは正の値しか出力しないので、それを平均してもまた正の値の出力しか出てきませんが、ラベルの方には負の値(次の手番側が形勢悪い)もあります。その時にパラメータの更新ができないとか、たぶんそういう話です(合ってるかな?)。ということで、活性化関数をReLUに戻して、一番最後のは削ります。

model.add(Conv2D(32, (3, 3), padding='valid', input_shape=x_train.shape[1:]))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(1, (1, 1), padding='valid'))
model.add(AveragePooling2D(pool_size=(13, 13)))
model.add(Flatten())
Epoch 1/20
319200/319200 [==============================] - 29s - loss: 21.7389 - val_loss: 20.8649
Epoch 2/20
319200/319200 [==============================] - 25s - loss: 20.8634 - val_loss: 20.4426
Epoch 3/20
319200/319200 [==============================] - 25s - loss: 19.7709 - val_loss: 19.0222
Epoch 4/20
319200/319200 [==============================] - 25s - loss: 19.1506 - val_loss: 18.5475
Epoch 5/20
319200/319200 [==============================] - 25s - loss: 18.7009 - val_loss: 18.1697
Epoch 6/20
319200/319200 [==============================] - 25s - loss: 18.3530 - val_loss: 17.9657
Epoch 7/20
319200/319200 [==============================] - 25s - loss: 18.1615 - val_loss: 17.7496
Epoch 8/20
319200/319200 [==============================] - 25s - loss: 18.0063 - val_loss: 17.8551
Epoch 9/20
319200/319200 [==============================] - 25s - loss: 17.9094 - val_loss: 17.5887
Epoch 10/20
319200/319200 [==============================] - 25s - loss: 17.8051 - val_loss: 17.4792
Epoch 11/20
319200/319200 [==============================] - 25s - loss: 17.7149 - val_loss: 17.4250
Epoch 12/20
319200/319200 [==============================] - 25s - loss: 17.6149 - val_loss: 17.3268
Epoch 13/20
319200/319200 [==============================] - 25s - loss: 17.5354 - val_loss: 17.7732
Epoch 14/20
319200/319200 [==============================] - 25s - loss: 17.4814 - val_loss: 17.6514
Epoch 15/20
319200/319200 [==============================] - 25s - loss: 17.3799 - val_loss: 17.4220
Epoch 16/20
319200/319200 [==============================] - 25s - loss: 17.3349 - val_loss: 17.0786
Epoch 17/20
319200/319200 [==============================] - 25s - loss: 17.2229 - val_loss: 17.1846
Epoch 18/20
319200/319200 [==============================] - 25s - loss: 17.1549 - val_loss: 16.9264
Epoch 19/20
319200/319200 [==============================] - 25s - loss: 17.1092 - val_loss: 17.0422
Epoch 20/20
319200/319200 [==============================] - 25s - loss: 17.0327 - val_loss: 18.2891

OKのようです。

LeakyReLUってなんとなく好きなんですが、ReLUの方がやはり軽いみたいなので、ここから先はひとまずReLUを使います。

次に、ネットワークを深くしていきます。3×3の畳み込み層を全部で4層に。

model.add(Conv2D(32, (3, 3), padding='valid', input_shape=x_train.shape[1:]))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(1, (1, 1), padding='valid'))
model.add(AveragePooling2D(pool_size=(11, 11)))
model.add(Flatten())
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 17, 17, 32)        608       
_________________________________________________________________
activation_1 (Activation)    (None, 17, 17, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 15, 15, 32)        9248      
_________________________________________________________________
activation_2 (Activation)    (None, 15, 15, 32)        0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 13, 13, 32)        9248      
_________________________________________________________________
activation_3 (Activation)    (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 11, 11, 32)        9248      
_________________________________________________________________
activation_4 (Activation)    (None, 11, 11, 32)        0         
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 11, 11, 1)         33        
_________________________________________________________________
average_pooling2d_1 (Average (None, 1, 1, 1)           0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 1)                 0         
=================================================================
Total params: 28,385.0
Trainable params: 28,385.0
Non-trainable params: 0.0

そして5層、6層、7層、8層、と増やしていって、最後に全部で9層。今回はパディングを入れていないので、どんどん畳み込まれていって、3×3の畳み込みのみで1×1のサイズに。そうなると、最後の平均プーリングはもう意味がありませんので削除します。1×1の畳み込み層も、実質、ただの全結合になってしまいました。

model.add(Conv2D(32, (3, 3), padding='valid', input_shape=x_train.shape[1:]))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(1, (1, 1), padding='valid'))
model.add(Flatten())
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 17, 17, 32)        608       
_________________________________________________________________
activation_1 (Activation)    (None, 17, 17, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 15, 15, 32)        9248      
_________________________________________________________________
activation_2 (Activation)    (None, 15, 15, 32)        0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 13, 13, 32)        9248      
_________________________________________________________________
activation_3 (Activation)    (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 11, 11, 32)        9248      
_________________________________________________________________
activation_4 (Activation)    (None, 11, 11, 32)        0         
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 9, 9, 32)          9248      
_________________________________________________________________
activation_5 (Activation)    (None, 9, 9, 32)          0         
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 7, 7, 32)          9248      
_________________________________________________________________
activation_6 (Activation)    (None, 7, 7, 32)          0         
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 5, 5, 32)          9248      
_________________________________________________________________
activation_7 (Activation)    (None, 5, 5, 32)          0         
_________________________________________________________________
conv2d_8 (Conv2D)            (None, 3, 3, 32)          9248      
_________________________________________________________________
activation_8 (Activation)    (None, 3, 3, 32)          0         
_________________________________________________________________
conv2d_9 (Conv2D)            (None, 1, 1, 32)          9248      
_________________________________________________________________
activation_9 (Activation)    (None, 1, 1, 32)          0         
_________________________________________________________________
conv2d_10 (Conv2D)           (None, 1, 1, 1)           33        
_________________________________________________________________
flatten_1 (Flatten)          (None, 1)                 0         
=================================================================
Total params: 74,625.0
Trainable params: 74,625.0
Non-trainable params: 0.0

3層~9層すべてのTrain Lossをグラフにしてみます。カッコ内の秒数は、2エポック目に掛かった時間です。だいたいこれが、1エポックあたりの平均の実行時間になります。

ネットワークが深くなるにつれ、どんどん賢くなっていくのがよく分かります。しかし、小さく畳み込まれたのをさらに畳み込んでいっているので、学習時間はあまり増えていきません。とはいえ、パラメータ数はどんどん増えていくので、過学習しやすくなったりはしてそうです。

パディングを入れれば、3×3の畳み込みをもっと重ねていくことは可能ですが、ここから先はひとまず9層で続けていきます。

次は、畳み込み層のフィルターの数を増やしていきたいと思います。まずは48に。

model.add(Conv2D(48, (3, 3), padding='valid', input_shape=x_train.shape[1:]))
model.add(Activation('relu'))
model.add(Conv2D(48, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(48, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(48, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(48, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(48, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(48, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(48, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(48, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(1, (1, 1), padding='valid'))
model.add(Flatten())
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 17, 17, 48)        912       
_________________________________________________________________
activation_1 (Activation)    (None, 17, 17, 48)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 15, 15, 48)        20784     
_________________________________________________________________
activation_2 (Activation)    (None, 15, 15, 48)        0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 13, 13, 48)        20784     
_________________________________________________________________
activation_3 (Activation)    (None, 13, 13, 48)        0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 11, 11, 48)        20784     
_________________________________________________________________
activation_4 (Activation)    (None, 11, 11, 48)        0         
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 9, 9, 48)          20784     
_________________________________________________________________
activation_5 (Activation)    (None, 9, 9, 48)          0         
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 7, 7, 48)          20784     
_________________________________________________________________
activation_6 (Activation)    (None, 7, 7, 48)          0         
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 5, 5, 48)          20784     
_________________________________________________________________
activation_7 (Activation)    (None, 5, 5, 48)          0         
_________________________________________________________________
conv2d_8 (Conv2D)            (None, 3, 3, 48)          20784     
_________________________________________________________________
activation_8 (Activation)    (None, 3, 3, 48)          0         
_________________________________________________________________
conv2d_9 (Conv2D)            (None, 1, 1, 48)          20784     
_________________________________________________________________
activation_9 (Activation)    (None, 1, 1, 48)          0         
_________________________________________________________________
conv2d_10 (Conv2D)           (None, 1, 1, 1)           49        
_________________________________________________________________
flatten_1 (Flatten)          (None, 1)                 0         
=================================================================
Total params: 167,233.0
Trainable params: 167,233.0
Non-trainable params: 0.0

次は64に。

model.add(Conv2D(64, (3, 3), padding='valid', input_shape=x_train.shape[1:]))
model.add(Activation('relu'))
model.add(Conv2D(64, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(64, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(64, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(64, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(64, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(64, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(64, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(64, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(Conv2D(1, (1, 1), padding='valid'))
model.add(Flatten())
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 17, 17, 64)        1216      
_________________________________________________________________
activation_1 (Activation)    (None, 17, 17, 64)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 15, 15, 64)        36928     
_________________________________________________________________
activation_2 (Activation)    (None, 15, 15, 64)        0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 13, 13, 64)        36928     
_________________________________________________________________
activation_3 (Activation)    (None, 13, 13, 64)        0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 11, 11, 64)        36928     
_________________________________________________________________
activation_4 (Activation)    (None, 11, 11, 64)        0         
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 9, 9, 64)          36928     
_________________________________________________________________
activation_5 (Activation)    (None, 9, 9, 64)          0         
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 7, 7, 64)          36928     
_________________________________________________________________
activation_6 (Activation)    (None, 7, 7, 64)          0         
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 5, 5, 64)          36928     
_________________________________________________________________
activation_7 (Activation)    (None, 5, 5, 64)          0         
_________________________________________________________________
conv2d_8 (Conv2D)            (None, 3, 3, 64)          36928     
_________________________________________________________________
activation_8 (Activation)    (None, 3, 3, 64)          0         
_________________________________________________________________
conv2d_9 (Conv2D)            (None, 1, 1, 64)          36928     
_________________________________________________________________
activation_9 (Activation)    (None, 1, 1, 64)          0         
_________________________________________________________________
conv2d_10 (Conv2D)           (None, 1, 1, 1)           65        
_________________________________________________________________
flatten_1 (Flatten)          (None, 1)                 0         
=================================================================
Total params: 296,705.0
Trainable params: 296,705.0
Non-trainable params: 0.0

フィルター数の違いを、グラフにしてみます。

これも増やせば増やすほど、賢くなっていきますが、学習時間の増え方もすごいですね。フィルター数64の時のTotal paramsは30万近くに… 身の丈に合っていないような気がするので(笑)、ここから先はひとまずフィルター数は32で続けていきます。

ここまでは、ネットワーク構成をいろいろ試してきましたが、ここで一度、ネットワークに対する入力を変更してみたいと思います。今現在は、石の配置の2面だけですが、これに「その場所の石のダメの数」を加えた3面にしてみました。数値はtanh(ダメの数*0.05)して0と1の間に収めました(0~1に正規化するのは、Kerasのサンプルがそうなっていたので)。ダメの数/256min(1, ダメの数/32)など、まあ何でもいいような気はします。ところで19路盤の最大ダメ数っていくらなんでしょう?

ダメなしとダメありとでの違いを、グラフにしてみます。

うーん、ちょっと効果が薄いですね。実は劇的に良くなるかと期待していたのですが… ダメの数は特に必要な情報でないからなのか、石の配置を見ればそんなことは分かるからなのかちょっとはっきりしませんが、ネットワークへの入力でがんばれることは、意外とあんまり無いのかもしれません。とはいえ、効果が全く無いわけではないので、ここから先はひとまず入力はダメありの3面で続けていきます。

次は、みんな大好き(笑)Batch Normalizationです。私は当初、Batch Normalizationって畳み込み層や全結合層の前に置くものだと、完全に思い込んでいたのですが、どうやら活性化関数の前に置くのが正しい? そのあたりも含めて調べてみます。

まずは、活性化関数の前にBatch Normalizationを置くバージョン。

model.add(Conv2D(32, (3, 3), padding='valid', input_shape=x_train.shape[1:]))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(1, (1, 1), padding='valid'))
model.add(Flatten())
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 17, 17, 32)        896       
_________________________________________________________________
batch_normalization_1 (Batch (None, 17, 17, 32)        128       
_________________________________________________________________
activation_1 (Activation)    (None, 17, 17, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 15, 15, 32)        9248      
_________________________________________________________________
batch_normalization_2 (Batch (None, 15, 15, 32)        128       
_________________________________________________________________
activation_2 (Activation)    (None, 15, 15, 32)        0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 13, 13, 32)        9248      
_________________________________________________________________
batch_normalization_3 (Batch (None, 13, 13, 32)        128       
_________________________________________________________________
activation_3 (Activation)    (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 11, 11, 32)        9248      
_________________________________________________________________
batch_normalization_4 (Batch (None, 11, 11, 32)        128       
_________________________________________________________________
activation_4 (Activation)    (None, 11, 11, 32)        0         
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 9, 9, 32)          9248      
_________________________________________________________________
batch_normalization_5 (Batch (None, 9, 9, 32)          128       
_________________________________________________________________
activation_5 (Activation)    (None, 9, 9, 32)          0         
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 7, 7, 32)          9248      
_________________________________________________________________
batch_normalization_6 (Batch (None, 7, 7, 32)          128       
_________________________________________________________________
activation_6 (Activation)    (None, 7, 7, 32)          0         
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 5, 5, 32)          9248      
_________________________________________________________________
batch_normalization_7 (Batch (None, 5, 5, 32)          128       
_________________________________________________________________
activation_7 (Activation)    (None, 5, 5, 32)          0         
_________________________________________________________________
conv2d_8 (Conv2D)            (None, 3, 3, 32)          9248      
_________________________________________________________________
batch_normalization_8 (Batch (None, 3, 3, 32)          128       
_________________________________________________________________
activation_8 (Activation)    (None, 3, 3, 32)          0         
_________________________________________________________________
conv2d_9 (Conv2D)            (None, 1, 1, 32)          9248      
_________________________________________________________________
batch_normalization_9 (Batch (None, 1, 1, 32)          128       
_________________________________________________________________
activation_9 (Activation)    (None, 1, 1, 32)          0         
_________________________________________________________________
conv2d_10 (Conv2D)           (None, 1, 1, 1)           33        
_________________________________________________________________
flatten_1 (Flatten)          (None, 1)                 0         
=================================================================
Total params: 76,065.0
Trainable params: 75,489.0
Non-trainable params: 576.0

長い…(笑) 次は、活性化関数の後にBatch Normalizationを置くバージョン。

model.add(Conv2D(32, (3, 3), padding='valid', input_shape=x_train.shape[1:]))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(32, (3, 3), padding='valid'))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Conv2D(1, (1, 1), padding='valid'))
model.add(Flatten())
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 17, 17, 32)        896       
_________________________________________________________________
activation_1 (Activation)    (None, 17, 17, 32)        0         
_________________________________________________________________
batch_normalization_1 (Batch (None, 17, 17, 32)        128       
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 15, 15, 32)        9248      
_________________________________________________________________
activation_2 (Activation)    (None, 15, 15, 32)        0         
_________________________________________________________________
batch_normalization_2 (Batch (None, 15, 15, 32)        128       
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 13, 13, 32)        9248      
_________________________________________________________________
activation_3 (Activation)    (None, 13, 13, 32)        0         
_________________________________________________________________
batch_normalization_3 (Batch (None, 13, 13, 32)        128       
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 11, 11, 32)        9248      
_________________________________________________________________
activation_4 (Activation)    (None, 11, 11, 32)        0         
_________________________________________________________________
batch_normalization_4 (Batch (None, 11, 11, 32)        128       
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 9, 9, 32)          9248      
_________________________________________________________________
activation_5 (Activation)    (None, 9, 9, 32)          0         
_________________________________________________________________
batch_normalization_5 (Batch (None, 9, 9, 32)          128       
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 7, 7, 32)          9248      
_________________________________________________________________
activation_6 (Activation)    (None, 7, 7, 32)          0         
_________________________________________________________________
batch_normalization_6 (Batch (None, 7, 7, 32)          128       
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 5, 5, 32)          9248      
_________________________________________________________________
activation_7 (Activation)    (None, 5, 5, 32)          0         
_________________________________________________________________
batch_normalization_7 (Batch (None, 5, 5, 32)          128       
_________________________________________________________________
conv2d_8 (Conv2D)            (None, 3, 3, 32)          9248      
_________________________________________________________________
activation_8 (Activation)    (None, 3, 3, 32)          0         
_________________________________________________________________
batch_normalization_8 (Batch (None, 3, 3, 32)          128       
_________________________________________________________________
conv2d_9 (Conv2D)            (None, 1, 1, 32)          9248      
_________________________________________________________________
activation_9 (Activation)    (None, 1, 1, 32)          0         
_________________________________________________________________
batch_normalization_9 (Batch (None, 1, 1, 32)          128       
_________________________________________________________________
conv2d_10 (Conv2D)           (None, 1, 1, 1)           33        
_________________________________________________________________
flatten_1 (Flatten)          (None, 1)                 0         
=================================================================
Total params: 76,065.0
Trainable params: 75,489.0
Non-trainable params: 576.0

これも、グラフにします。今回はValidate Loss付きです。

もう少したくさん学習させてみないと、最終的に収束した時のLossが低くなるのか、学習が速いだけなのか、よく分かりませんが、なんにせよ、とりあえずBatch Normalizationはすばらしい! Validate Lossも10をはっきりと切ってきました。みんな大好きBatch Normalization、僕も大好きです(笑)。1エポックあたりの学習時間は大幅に増えて、たしかに重いのは重いんですが、学習が速くなるのであれば、それも少なくともある程度はペイしそうです。

そして、先ほどの「Batch Normalizationは活性化関数の前なのか後なのか問題」ですが、今回のケースでは、「活性化関数の後」が良さそうです。平均した数字だけ見てもそうなのですが、「活性化関数の前」のValidate Lossの上下にバタバタする感じがちょっと気持ち悪い… ただ、今回は検証用データの量が絶対的に少ないので、もう少しちゃんと調べないと、はっきりしたことは言えません。

今後は、ひとまず「活性化関数の後」にBatch Normalizationを置く形で続けていきます。

いやあ、それにしても長い記事になりました。しかも、まだぜんぜん終わってない… たぶん、大量に追記することになります。

ここまで、やってきて一番思うのは、「学習データって大事」ってことです。これは量も質もですね。最初始めた時、「学習データなんてなんでもいいよ。俺はディープなラーニングがしたいだけなんだよ」って思ってた自分を引っ叩いてやりたい(笑)。ただ、当初はここまで良い数字が出るとは、正直思ってなかったから、ということもあります。例えば、今現在のようなネットワーク構成でも、もう少しネットワークを深くして、もう少しフィルターを増やして、学習データ増やして、ワンコインぐらい課金すれば、Validate Lossが7ぐらいまでいけそうですが、そこまでいければ、今回の評価関数は(も?)同じような盤面は同じように形勢判断を間違えるのだろうと思うので、深さ1の全幅と組み合わせて、GNU Goぐらいならなんとか勝てないでしょうか? もし仮にそれができたら、Keras.js使って、もうこれは何て言うか一丁上がりなんですが、なかなか事前にそこまで夢みることはできませんでした。

そういう訳で、とにかく学習データです。今現在、COSUMIのサーバを使って学習データを大量に作成中です(負荷が低くなったら、自動的に作り始めるようにした)。それができあがったら、また引き続きいろいろ試してみたいと思います。

[追記 2017/5/2]
あの後、学習データをたくさん作りました。まずは、前回使用したデータと今回分との、形勢の分布のグラフを。比較しやすいように、スケールは調整してあります。

前回分のデータの分布を最初に見た時、いくらなんでもこれは分散が足らないんじゃないかと思ったので、今回は平均近辺を適当に間引きながらデータを作成したのですが、あんまりきれいな間引き方になっていないような気がして、少しもったいなかったのですが、思いきって新規作成分の平均近辺をスクエアにぱすっと捨てて、それに前回作成分を足しました。えっと、なに言っている分からないと思いますが(笑)、とにかくグラフのような感じに、真ん中減らしました。前回、21.5だった平均偏差は27.8に。そこだけでいうと、今回の方が厳しいデータセットになっていると思います。

今回使用分は15万局面分+α。これを対称形に8倍して、切り良く120万局面分に減らしました。そして、前回と同じく、その内80%を学習用に、残りの20%を検証用に使用します。

この新しい学習データで、前回最後のネットワーク構成から試してみたいと思います。現在地をおさらいすると、

  • 入力は「手番のプレーヤーの石の配置」「相手の石の配置」「その場所にある石のダメの数」の3面(19,19,3)
  • 3×3の畳み込み層が9層、1×1の畳み込み層が1層、フィルター数は最後以外32
  • 活性化関数は全部ReLU
  • ReLUの後にBatch Normalization

です。さらに今回はここから、入力層の所にパディング付の3×3の畳み込み層を追加していく形で、どんどん深くしてみました。最後は3×3の畳み込み層が18層です。今回はエポック数は固定にしないで、Validate Lossが下げ止まったら、学習を止めるようにしました。話が少しそれますが、Kerasには、そういう時に使うkeras.callbacks.EarlyStoppingというコールバック関数が用意されているのですが、それのpatienceという引数についてのまともな説明が、ウェブ上にほとんど無い! これは本家のドキュメントも一緒で、例えば、日本語版ではこうなってます。

patience: トレーニングが停止し,値が改善しなくなった時のエポック数.

なにを言っているのか、まじでぜんぜん分からん…(笑) 次に本家英語版。

patience: number of epochs with no improvement after which training will be stopped.

私の英語力が確かなら、これも間違っています。例えば、patience=1の時は、最高なり最低なりを1回でも更新できなかったらそこですぐに止まるわけではなく、2回連続して更新できなかったら止まるんです。patience=2の時は、3回です。1回でも更新できなかったらすぐ止まるpatience=0を基準に、さらに何回待つかっていうのがこのpatienceですよ。みなさん気をつけてください。で、今回は最初の「3×3の畳み込み層が9層」の時のみpatience=2(ちょっと少なかった。加減が結構難しい…)、10層からはpatience=3に設定してみました。

うーん、もうネットワークを深くすれば良いだけなのかな? 簡単に数字が良くなっていきます。もっと早くサチるかと思っていたのに、なかなか止まらないので、気持ちよくお金が溶けていきました…(泣) 「こんな風に深くするだけでいいんであれば、小細工なしにResNetってやつをやれば終了じゃない?」ということで、前半部分にショートカットを入れたバージョンの14層と18層も追加で試してみました。先ほどのpatienceは4にしました。14層ならコードはこんな感じ。本当にこれで良いのか、かなり不安ですが…

input = Input(shape=x_train.shape[1:])

fork = Conv2D(32, (3, 3), padding='same')(input)

main = Activation('relu')(fork)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)
main = Activation('relu')(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)

fork = add([main, fork])

main = Activation('relu')(fork)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)
main = Activation('relu')(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)

main = add([main, fork])

main = Activation('relu')(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='valid')(main)
main = Activation('relu')(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='valid')(main)
main = Activation('relu')(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='valid')(main)
main = Activation('relu')(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='valid')(main)
main = Activation('relu')(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='valid')(main)
main = Activation('relu')(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='valid')(main)
main = Activation('relu')(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='valid')(main)
main = Activation('relu')(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='valid')(main)
main = Activation('relu')(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='valid')(main)
main = Activation('relu')(main)
main = BatchNormalization()(main)
main = Conv2D(1, (1, 1), padding='valid')(main)

output = Flatten()(main)

model = Model(inputs=input, outputs=output)

ショートカットあるなしで比較してみます。

「18層の時は、もしかしたらショートカットが効いてるのかな?」ってぐらいですね。そもそも、この程度ではまだぜんぜんネットワークが深すぎるっていうほどのものじゃないのかもしれません。深くしたからサチったのではなくて、そろそろ学習データの精度の問題かも…

ここで一度、現在作っている評価関数が実際にどんな形勢判断を返してくるのか確認することにしてみました。使った評価関数のバージョンは、先ほどの「3×3の畳み込み層が18層/ショートカットあり」。これに、検証用データの先頭200局面分を予測させた時の数値のグラフがこちらです。

当たり前の話なんですが、本当に形勢判断できるんですね(笑)。感動します。ただ、ちょっと気になることもあって、この検証用データのラベルが8つずつ全く同じのが続くのは、同じ局面の対称形が連続して並んでいるからなんですが、評価関数の出力の方はなかなかきれいに揃わないですね。これを「まだ伸び代がある」とか、「対称形8つ全部を順番に評価関数に入れて平均とったら精度上がるんじゃない?」とか、ポジティブに捉えることもできなくはないかもしれませんが、個人的にはこういうのはただただ気持ち悪いです… こうなる理由として考えられるのは、「3×3の畳み込み層のフィルタの初期値が対称形でないから」とか、「学習用データに対称形がすべて含まれているけど、学習するタイミングが前後するので、先に学習した、後に学習したでモデルに与える影響が変わってくるから」とかあたりでしょうか? これはまた調べてみたいですね。

今回の200局面分の予測の中で検証用データのラベルと一番食い違っているのが、グラフ中央右寄りにある100を超えているやつなので、この局面を探して実際に盤面を見てみることにしました。それがこちら。


Sorry, your browser doesn’t support WGo.js.

検証用データのラベルは約138.61。これは、「コミがないとすれば次の手番である白が138.61目勝っている」の意味です。最初に盤面で確認しておかないといけないのは、下辺の黒の大石の死活ですが、これはセキにはなりますが生きてますね(見損じしてないよね?)。だとすると、形勢は白100目弱勝ちぐらいでしょうか? 自作評価関数の出力は対称形8つの平均で87.31。おお、自作評価関数の方がだいぶ近い… 先生より正確とはやるじゃん!(笑) まあ、「石いっぱいあるから強ーい」ぐらいに思っているだけで(笑)、Ray先生のような高度な判断をしている訳ではないような気がしますが、とはいえ、こういった不正確なラベルのせいで、40目も余分に間違えていることにされるようなことがちょくちょくあったら、下がるはずのLossも下がりません。「機械学習では質の良い学習データを大量に用意することが肝心」、という結論にまた落ち着いてしまいますね。ということで、現在、学習データの精度を上げるべく、COSUMIのサーバをまたぶん回しております。いつか、学習データの作成自体を、この評価関数にやらせたいですね。それができれば、量の問題は一発で解決なんですが…

[追記 2017/6/11]
あの後、ASUSのSTRIX-GTX1060-DC2O6GっていうGTX1060・メモリ6GBなビデオカード買いました。EC2への課金が100ドルを超えてきたので、EC2使い続けるのか、別の方法を取るのか、今決めてしまわないといけないと思い、かなりいろいろ考えて、結局GPU買っちゃいました。最初は、GPU買うなら中途半端はだめで、1080ti一択だなと思い込んでいたのですが、そうなってくると電源ユニットの買い直しが確定するので、それがちょっとなあと思っていました。けれども、よく調べてみると、その下のグレードでも十分実用性がありそうですし、なによりはるかに安いので、こういう選択肢になりました。1070でも良かったけど、電源が100%自信が持てなかったので1060に。使っているマザーはASUSのP8H77-Vで、H77と最近のビデオカードとでは動かない時がある、という話を見て少し心配していたのですが、全く問題ありませんでした。このビデオカードは、温度が低い時にファンが完全に止まる静音設計で、それも購入にあたって重視していた点なのですが、そもそもファンが回っていても、めちゃくちゃ静かです。良い買い物でした。こんな高価なビデオカードを買うのは、もちろん初めてですし、ビデオカード自体、一番最後に買ったのはいつのことだろう… Rage Fury MAXX(笑)が最後かな?(一番最後まで使っていたのは、たぶんG400) ちなみに、今現在のメインメモリは16GBなんですが、GPU買ってしまうと、今度はこれを32GBに増やしたくて仕方がない…(笑) ただ、4年半前に買った時は5,880円だった物が、今現在、値段が倍以上する感じで萎えまくりです。うーん、どうしたものか…

そして、学習データもこの前使っていたものを、さらに50k playoutで2目ずつずらしていく形でラベル付け直して精度を上げてみました(この前までは、20k playoutで4目ずつ)。量も少し増やして、計159万局面分。今までと同じく、その内80%を学習用に、残りの20%を検証用に使用します。

ということで、新しいGPUと新しいデータでいろいろ試してみましたが、結局一番数字が良くなるのは、次のようなパディングとショートカットを入れながら、ひたすら3×3の畳み込み層を重ねるだけというシンプルなやつでした。

input = Input(shape=x_train.shape[1:])

fork = Conv2D(32, (3, 3), padding='same')(input)

main = Activation("relu")(fork)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)
main = Activation("relu")(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)

fork = add([main, fork])

main = Activation("relu")(fork)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)
main = Activation("relu")(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)

fork = add([main, fork])

main = Activation("relu")(fork)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)
main = Activation("relu")(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)

fork = add([main, fork])

main = Activation("relu")(fork)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)
main = Activation("relu")(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)

fork = add([main, fork])

main = Activation("relu")(fork)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)
main = Activation("relu")(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)

fork = add([main, fork])

main = Activation("relu")(fork)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)
main = Activation("relu")(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)

fork = add([main, fork])

main = Activation("relu")(fork)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)
main = Activation("relu")(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)

fork = add([main, fork])

main = Activation("relu")(fork)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)
main = Activation("relu")(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)

fork = add([main, fork])

main = Activation("relu")(fork)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)
main = Activation("relu")(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)

fork = add([main, fork])

main = Activation("relu")(fork)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)
main = Activation("relu")(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)

fork = add([main, fork])

main = Activation("relu")(fork)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)
main = Activation("relu")(main)
main = BatchNormalization()(main)
main = Conv2D(32, (3, 3), padding='same')(main)

main = add([main, fork])

main = Activation("relu")(main)
main = BatchNormalization()(main)
main = Conv2D(1, (3, 3), padding='valid')(main)
main = AveragePooling2D(pool_size=(17, 17))(main)

output = Flatten()(main)

model = Model(inputs=input, outputs=output)

ショートカットなし版との比較がこちら。

ショートカットは、はっきり効果があるようです。そして問題は、深くするのが良いのか広くするのが良いのかなんですが、まずはフィルタ数を32で固定して、3×3の畳み込み層が24層、32層、40層の比較がこちら。

そして次に、3×3の畳み込み層を24層に固定して、フィルタ数が32、48、64の比較がこちら。

それ以外にもいろいろ試した結果としては、

  • ReLUとBatch Normalizationの順番は、BN -> ReLU -> ConvよりReLU -> BN -> Convの方が、やはり良さそう
  • 入力は、「だめの数なし」より「だめの数あり」の方が、やはり少し数字が良い
  • オプティマイザにNesterov MomentumなSGDを少し試してみたけど、特に良さそうには見えない

といった感じでしょうか。

数字はだいぶ良くなってきたので、本当に何か使い道も考えてみたいですね。

[追記 2017/6/14]
今のデータ量で行けるところまでやってみようと、3×3の畳み込み層が30層、フィルタ数が48で50エポック回してみました。さらに、その30エポック目からAdamの学習率をKerasのデフォルト(そしてそれは論文の推奨値だそうです)の1e-3から1e-4に小さくしたのと、またさらに、その40エポック目から学習率を1e-5に小さくしたのとのグラフがこちら。

この学習率を下げるのは手動でやっているのですが、本当はこのあたり、コンピュータにスマートによろしくやってもらわないといけないのでしょうね。keras.callbacks.LearningRateScheduler()使ったり、keras.optimizers.Adam()decayを設定すれば良いのかなと、少し試してみたりもしましたが、結局どのくらいずつ下げていけば良いのか事前にはっきり分からないので、もう手動でもいいかな…

それと、学習率下げてはっきりしましたが、最後はほんの少し過学習ぎみですね。対称形に8倍して1,272,000局面分のデータ量では、Trainable params586,513の今回の大きさのネットワークあたりが限界かな、という気がしてます。

しかしそれにしても、数字がかなりよくなってきて、Validate Loss3.6(!)を切ってきました。そんなのもう、ほとんどRay由来のノイズじゃないのかと思ってしまいます。というより、データ作成で何かやらかしていないか、心配になるレベルなのですが…(笑)

[追記 2017/8/2]
KerasのConv2Dkernel_initializerのデフォルトは、Glorot uniformってやつなんですが、He uniformも試して比較してみました(本当に申し訳ないのですが、今回の追記分のテストは1ヶ月以上前にやっていたことで、他の細かい条件がはっきりとは分からなくなってしまいました。さっさとブログに書けば良かった…(笑))。

なんだか、あまり小さくない差があるように見えます。

glorot_uniformVarianceScaling(scale=1., mode=’fan_avg’, distribution=’uniform’)と等価なんですが、次に、このscaleをいろいろな数値に変えた時の、1エポック目のTrain Lossをグラフにしてみました。

0.1ぐらいが一番良さそうで、Glorot uniformの1とそれなりに差があるように見えます。まあこれは、まだ1エポック目ですし、そしてValidate LossではなくTrain Lossですので、あまり真に受けてもいけないと思うのですが、「畳み込み層の初期値はなんでも良いわけではない」のは、間違いなさそうです。意外とこんな所に宝物が隠れていることが少なくないのかも…

[追記 2018/2/11]
続きの記事があります。

Keras/TensorFlowでDNNな囲碁の評価関数を作ってみる その2
http://www.perfectsky.net/blog/?p=380