TadaoYamaokaの開発日記

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

将棋AIの進捗 その31(cuDNNによるSENetの推論処理の実装)

dlshogiの10ブロックのWideResnetの自己対局による強化学習を続けていましたが、230サイクルほどでほぼ頭打ちになりました。

訓練損失は下がり続けていますが、floodgateの棋譜に対する損失が上昇傾向になっており、技巧2のとの勝利も上がらないため、このモデルでの強化学習は打ち切ることにしました。
技巧2(4コア CPU)との勝率は、GPU2080Ti1枚、1手3秒で32%程です。

訓練損失

f:id:TadaoYamaoka:20190724003127p:plain

floodgateの棋譜に対する損失

f:id:TadaoYamaoka:20190724003418p:plain

floodgateの棋譜に対する正解率

f:id:TadaoYamaoka:20190724003437p:plain

188サイクルあたりで訓練損失が急に下がって、floodgateの棋譜に対する損失が上昇したため、189サイクル目からはエントロピー正則化とL2正則化を加えるようにしました。
少しだけ精度が上がりましたが、すぐにほぼ横ばいになりました。
このあたりが、このモデルの強化学習の限界のようです。

SENetのモデルの学習開始

以前に将棋AIでもSENetが有効であることを実験で確かめたので、SENetのモデルで強化学習を始めることにしました。
今回も、教師ありで事前学習を行ってから、自己対局で強化学習を行う予定です。
教師データはWCSC29のAperyで生成した5億局面を使用します。
WCSC29のAperyは学習局面のフォーマットに手数が追加されていましたが、以前と同じフォーマットで生成しました。
終局近くの手順も学習したいため、対局終了の閾値は、30000としました。
引き分けも生成します。

SENetの実装

ChainerでのSENetの実装は、以前に記事にしたものと同じです。

対局時の推論処理は、cuDNNで実装しているため、cuDNNを使って実装しました。
cuDNNのAPIはChainerよりも使える機能が少ないため、どのAPIで実現できるかを調べるのに結構苦労しました。

global average poolingは、averagePooling2Dのwindowを盤面のサイズにすることで実現しました。
Squeezeの結果とチャンネルごとの積は、cudnnOpTensorのCUDNN_OP_TENSOR_MULモードで実現できました。
(FP16の場合でも計算はfloatなので、係数のalphaとbetaもfloatにする必要があることに気付かず、何時間もデバッグしていましたorz)

cuDNNで実装したい人はほとんどいないと思いますが、ソースを公開しました。
https://github.com/TadaoYamaoka/DeepLearningShogi/blob/master/usi/nn_senet10.cpp

学習結果

Aperyで生成した5億局面で学習した結果、floodgateの棋譜に対する損失と正解率は以下の通りです。
エントロピー正則化とL2正則化は、はじめから加えています。

WideResnet10ブロックをelmoで生成した4.9億局面で学習した際も比較のために載せておきます。

WideResnet SENet
policy loss 0.93350023 0.9108452
value loss 0.58716744 0.57989293
policy accuracy 0.4090266 0.415206
value accuracy 0.68762606 0.68894225

教師データの違いがあるため単純に比較できませんが、精度は上がっています。

技巧2に対する勝率

事前学習したモデルで技巧2(CPU 4コア)に対する勝率を測りました。
1手3秒で100回対局した結果は11-1-88で、勝率11%となりました。

WideResnet10ブロックのモデルで事前学習のみでは技巧2には全く勝てなかったため、技巧2に勝率11%は思ったより良い結果です。

このモデルをベースに、自己対局による強化学習を開始する予定です。
少なくともGPU1枚で技巧2を超えるようになってほしいと思っています。

【書籍】将棋AIで学ぶディープラーニングのJupyter Notebookを公開

【書籍】将棋AIで学ぶディープラーニングのJupyter NotebookをGoogle Colabで公開しました。


Google Colab上で学習を実行して、ssh経由で将棋所から実行できます。

ssh経由で実行する方法は、Notebookにも記載していますが、この記事を参照してください。

イベントの案内

平日で有料ですが、今週水曜日の20時からハンズオンを開催します。
(参加していた勉強会の主催者からの依頼で開催することになりました。)
【初心者〜中級者向け】将棋AIで学ぶディープラーニング入門ハンズオン - connpass

まだ空席がありますので、興味のある方は参加してみてください。

このJupyter Notebookを使用して、解説を行う予定です。
すべてGoogle Colabで実行できるため、ノートPCを持参いただければ実行可能です。

将棋所の実行環境はWindowsを想定していますが、monoなどで将棋所を動かさせればmacbookでも可能です。
将棋所を使わずコマンド実行だけであれば、ブラウザさえあれば実行できます。

Google Colabに鍵ペアを使用してsshでログインする

Google Colabは、基本的にJupyter Notebookから使用します。

しかし、ngrokというサービスを使用することで、sshで接続する方法が知られています。
How can I ssh to google colaboratory VM? - Stack Overflow

上記サイトの方法では、sshでログインする際にパスワードが必要になりますが、鍵ペアを使用してログインしたかったため、試したところ接続することができましたので、方法を載せておきます。

なお、鍵ペアでアクセスしたい理由は、Google Colabで将棋AIのUSIエンジンを実行したかったためです。
将棋所からバッチファイル経由でssh接続する場合、パスワードを入力することができないため、パスワードなしでログインする必要があります。
sshコマンドは、引数からパスワードを渡すことができませんが、鍵ペアを使用するとパスワードなしでログインすることができます。

ngrok経由でsshdを起動

上記のサイトの方法に、鍵ペアでのログインを許可する設定を加えて、sshdを起動します。
加えたのは、

! echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config

の行です。
この時点では、まだクライアントの鍵の設定は行っていません。

#Generate root password
import random, string
password = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(20))

#Download ngrok
! wget -q -c -nc https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip
! unzip -qq -n ngrok-stable-linux-amd64.zip
#Setup sshd
! apt-get install -qq -o=Dpkg::Use-Pty=0 openssh-server pwgen > /dev/null
#Set root password
! echo root:$password | chpasswd
! mkdir -p /var/run/sshd
! echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
! echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config
! echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
! echo "LD_LIBRARY_PATH=/usr/lib64-nvidia" >> /root/.bashrc
! echo "export LD_LIBRARY_PATH" >> /root/.bashrc

#Run sshd
get_ipython().system_raw('/usr/sbin/sshd -D &')

#Ask token
print("Copy authtoken from https://dashboard.ngrok.com/auth")
import getpass
authtoken = getpass.getpass()

#Create tunnel
get_ipython().system_raw('./ngrok authtoken $authtoken && ./ngrok tcp 22 &')
#Print root password
print("Root password: {}".format(password))
#Get public address
! curl -s http://localhost:4040/api/tunnels | python3 -c \
    "import sys, json; print(json.load(sys.stdin)['tunnels'][0]['public_url'])"

実行すると、

Copy authtoken from https://dashboard.ngrok.com/auth

というリンクが表示されるので、リンク先にアクセスしてトークンをクリップボードにコピーして、入力欄に入力します。
初回アクセス時はngrokのサインインが必要です。

成功すると、以下のようにrootのパスワードと、URLが表示されます。パスワードとポートは毎回異なります。

Root password: mwbVRS9XnegtI3AVp3n7
tcp://0.tcp.ngrok.io:12451

クライアントで鍵ペア作成

sshで接続するクライアントで、鍵ペアを作成します。(作成済みの場合は、実行不要です。)
Windows 10 バージョン 1803 以降では OpenSSH クライアントがデフォルトでインストールされています。

ssh-keygen -t rsa

パスフレーズを聞かれますが、そのままEnterで問題ありません。

C:\Users\username\.ssh\に、

が作成されます。id_rsa秘密鍵なので、絶対に外に漏らさないようにしましょう。

公開鍵のアップロード

Google Colabのファイルブラウザ(左ペインからファイルを選択)から、アップロードをクリックして、作成したid_rsa.pubをアップロードします。
フォルダを変更していなければ、/content/にアップロードされます。

パスワードを使用してssh接続して、公開鍵を認証済みに設定する

クライアントから、パスワードを使用してssh接続します。

ssh root@0.tcp.ngrok.io -p 12451

-pオプションで指定するポートは、sshdを起動した際に表示されたポート番号です。
パスワードを聞かれますので、sshdを起動した際に表示されたパスワードを入力します。

rootのホームディレクトリに.sshというディレクトリを作成し、アップロードしたid_rsa.pubを/root/.ssh/authorized_keysに移動します。

mkdir /root/.ssh
mv /content/id_rsa.pub /root/.ssh/authorized_keys

これで、鍵ペアを使用してログインできるようになります。
一度sshをログアウトして、再度ログインする際には、パスワードを聞かれません。

学習中と並列で自己対局

先日の日記で、学習中にも自己対局を行うようにしたことを書いたが、現在dlshogiでは自己対局で生成した局面は過去10サイクル分をすべて学習に使うようにしている。
(AlphaZeroのようにサンプリングしないですべての局面を使うのは、Actor-Criticで敗着となった手(学習したい手)を学習する機会が減りそうという理由からだが、どっちがよいかは分かっていない。)

そのため、学習時間に依存して生成する局面が増えるので、次のサイクルで学習する局面が増えて、それにより学習時間も増える。
そうすると生成する局面も増えるので、どんどん学習時間伸びていくのではないかという素朴な疑問が起きる。

学習する局面に対して、学習中に自己対局で生成する局面は相対的に少ないので、直感的に収束しそうだが、念のため計算してみた。

以下、数学の問題です。高校数学を復習したい方はどうぞ。

収束判定問題

純化のために、過去2サイクル分だけ学習に使うとした場合、Tサイクル目の学習時間t_Tと学習中に生成する局面x_Tは、
 \displaystyle
t_T=a(x_{T-1}+c) \\
x_T=bt_T
となる。ここで、aは学習速度(時間/局面)、bは局面生成速度(局面/時間)、cは1サイクルの学習中以外で生成する局面数(固定値)。

x_{T-1}=bt_{T-1}を、上式のt_{t-1}に代入すると、
 \displaystyle
t_T=a(bt_{T-1}+c)
となる。

よって、T=0のとき、
 \displaystyle
t_0=ac

T=1のとき、
 \displaystyle
\begin{eqnarray}
t_1&=&a(bt_0+c) \\
&=& ac(ab+1)
\end{eqnarray}

T=2のとき、
 \displaystyle
\begin{eqnarray}
t_1&=&a(bt_1+c) \\
&=& ac(ab(ab+1)+1) \\
&=& ac((ab)^2+ab+1)
\end{eqnarray}

T=3のとき、
 \displaystyle
\begin{eqnarray}
t_3&=&a(bt_2+c) \\
&=& ac(ab(ab(ab+1)+1)+1) \\
&=& ac((ab)^3+(ab)^2+ab+1)
\end{eqnarray}

となる。
つまり、
T=nのとき、
 \displaystyle
\begin{eqnarray}
t_n&=&a(bt_{n-1}+c) \\
&=& ac((ab)^n+(ab)^{n-1}+\cdots+ab+1)
\end{eqnarray}
となり、等比級数和の形になっている。

等比級数の和は、高校の数学で習った通り(忘れてたけど)、
 \displaystyle
\frac{1-(ab)^n}{1-ab}
となるので、|ab|<1のときに、
 \displaystyle
\frac{1}{1-ab}
となり、収束する。

dlshogiでは、学習速度aは、3.9 \times 10^{-4}(秒/局面)、学習中の局面生成速度bは、39.6(局面/秒)なので、
 \displaystyle
ab\simeq0.015<1
となり収束する。

10サイクルにした場合も、基本的に同じ問題に落とし込めるので省略する。

WindowsでプロセスにSIGINTを送る

将棋AIの自己対局のプログラムで途中で中断(Ctrl+C)した場合に、それまでに生成した局面を安全にファイルに書き出すためにSIGINTをハンドリングしている。
ただし、今までは、基本的に途中で中断しないで、生成する局面の数を引数として、自己対局完了→学習の順にシーケンシャルに処理していた。

現在1台のPCにGPUを3枚刺して使用しており、学習は1枚のGPUで行っているため、学習中に他の2枚が無駄になっている。
そこで、GPUを有効活用するため、学習中にも自己対局を行い、学習完了時にSIGINTを送信して自己対局を安全に中断するようにした。

なお、自己対局のプログラムが新しいモデルがあれば取りに行くような仕組みの方がエレガントだが、一連の処理をWindowsのバッチファイルで処理するダサい仕組みになっている。

以下、処理方式に関する説明です。

SIGINTの受信処理

C++では、

#include <signal.h>

volatile sig_atomic_t stopflg = false;

void sigint_handler(int signum)
{
	stopflg = true;
}

int main() {
    signal(SIGINT, sigint_handler);
    // ...
}

のようにして、SIGINTをハンドリングできる。

学習と自己対局を並列で実行

バッチファイルでプログラムを並列で実行するために、startコマンドで自己対局を別のコマンドプロンプトで起動してから学習を行うようにした。
ログをリダイレクトで出力していたため、直接自己対局プログラムを起動するのではなく、cmdコマンドを起動してその引数に実行するコマンド行を記述するようにした。

start cmd /c "make_hcpe_by_self_play --mate_depth 25 --mate_nodes 100000 --threashold 1 d:\model\model_rl_val_wideresnet10_selfplay_%M% C:\hcp\roots12.hcp D:\selfplay\selfplay-%N%_org3 100000 1000 1 256 2 256 >> d:\log\%date:~0,4%%date:~5,2%%date:~8,2%_make_hcpe_by_self_play_%N%.txt"

SIGINTの送信

学習完了後に、並列で実行中の自己対局プログラムを終了するために、SIGINTを送信する。
Windowsで標準でSIGINTを送信する方法がないため、windows-killというコマンドを使用した。
なお、標準のプロセスを終了するコマンドとしてtaskkillコマンドがあるが、SIGINTは送信できない。

windows-killコマンドは引数がPIDになっているため、tasklistコマンドでPIDを取得する必要がある。

for /f "tokens=2 delims=, usebackq" %%i in (`tasklist /nh /fo csv /fi "imagename eq make_hcpe_by_self_play.exe"`) do set pid=%%i

これで、変数pidに自己対局プログラムのPIDが設定されるので、

C:\bin\windows-kill\windows-kill.exe -SIGINT %pid%

のようにしてSIGINGを送信する。

その後、自己対局プログラムが終了するのを待つ必要があるため、powershellのWait-Processを使って待つようにした。

powershell -Command Wait-Process -Id %pid%


以上、バッチファイルでプロセスの並列処理を頑張る方法についてでした。
たぶん、バッチファイルで頑張るよりPythonとかで作った方がよいです。

VSCodeでMarkdownの編集

今までMarkdownテキストエディタで手書きしていましたが、Visual Studio Codeは標準でMarkdownのプレビューに対応しているので、Markdownのエディタとして使い始めた。
拡張機能をインストールすると表のフォーマットも自動で行ってくれる。

Markdown Extended」という拡張を入れると、Excelからそのまま貼り付けができるのでMarkdownでの表の記述が楽になる。
この拡張はExcelから貼り付ける時に表のフォーマットも行ってくれるのだが、漢字は問題ないが、ひらながを使っていると幅が1と判断されて崩れてしまう。
f:id:TadaoYamaoka:20190702160944p:plain

ソースはGitHub公開されているので、簡単に直せないかと調べたら、正規表現を少し直すだけで対応できそうだった。
issuesの履歴を見ると漢字の対応は、中国からのPull Requestで対応しているようだった。

とりあえずissueを上げてみたが、ひらがなの対応は日本からPull Requestすべきと思ったので、Pull Requestしてみた。

以下、VSCode拡張機能をテストしてPull Requestするまでの手順のメモです。

VSCode拡張機能の開発環境

公式のページに解説がある。
Your First Extension | Visual Studio Code Extension API

新規で作成する場合の手順なので、既存のプロジェクトを修正する場合は、Node.jsがあればよい。

プロジェクトのフォーク

GitHubでプロジェクトをフォークして、フォークしたプロジェクトをローカルにcloneする。

TypeScriptインストール

元のソースはTypeScriptで記述されていたので、npmからTypeScriptをインストールした。

npm installl -g typescript

依存パッケージインストールとビルド

VSCodeでプロジェクトを開いて、依存パッケージをインストールする。
VSCodeのnpm拡張をインストールしている場合、コマンドパレットからnpm installを選択する。

ソースを修正して、Ctrl+Shift+Bでビルドする。
以下のエラーがでたので、ネットで調べてtsconfig.jsonのlibに"dom"を追加した。

node_modules/@types/puppeteer/index.d.ts(16,46): error TS2304: Cannot find name 'Element'.

コンパイルエラーになる行が1行だけあったので、コメントアウトした。
おそらくTypeScriptのバージョンに依存した問題と思われるが、テストしたい箇所とは関係ないのでコメントアウトで対応した。

テスト

F5を押して、実行する。
もう一つVSCodeが起動し、その中で拡張機能のテストができる。

Excelからの貼り付けで、正しくフォーマットされることが確認できた。
f:id:TadaoYamaoka:20190702161110p:plain

コミットとプッシュ

コミット時にissueと関連付けるために、「#51」のようにissueの番号をコメントの末尾に付加する。
フォークしたプロジェクトにプッシュする。

Pull Request

フォークしたプロジェクトのGitHubのページのNew pull requestからPull Requestを送る。

マージ

1分後にマージされた。
数時間前に他のissueの修正のコミットがあったのでちょうどコミッタが作業中のタイミングだったようだ。


まだマーケットプレイスには反映されていませんが、そのうち反映されると思います。

2019/7/2 追記

v1.0.10で反映されました。

2019/7/2 追記

カタカナの濁点でも1文字幅と判断されていたので、(自分が送られる側だとまとめて送ってほしいところだけど)再度プルリクエストを送った。

2019/7/3 追記

Pull requestを送った内容とは別の方法で修正されたのだが、長音記号を含んでいないためコメントを送った。
↓修正された方法

     "\\p{Script=Katakana}", 
     "\\p{Script=Hiragana}", 

後で気付いたが、全角の記号とか絵文字とかも含まれていない。
これは別問題で小出しに送るのもよろしくないので今回は記号の件はコメントしなかった。
多言語処理ややこしい。

【Android開発】実行時のパーミッションリクエスト

先日バージョンアップしたボーカル音程モニターで、起動できないというレビュー(評価★1)が多く付いたので、一旦リバートしました。
段階的な公開で10%にしていたので影響は抑えられましたが、テスト不足と反省しています。

手元のテスト機では動作しており、動いているレビューもあったので、特定の機種で発生する事象のようです。
レビューの機種を確認すると、Samsungの機種で発生が多いようでした。
再現させないと原因を調べるのも難しいので、海外の個人輸入サイトから安い端末を買いました。

無事海外から届いて再現させたところ、再現できました。
AudioRecordの作成時に以下のエラーが発生していました。

E/IAudioFlinger: createRecord returned error -22
E/AudioRecord: AudioFlinger could not create record track, status: -22
E/AudioRecord-JNI: Error creating AudioRecord instance: initialization check failed with status -22.
E/android.media.AudioRecord: Error code -20 when initializing native AudioRecord object.

はじめオーディオ関連のバグを疑って調べていましたが、Android6.0(APIレベル23)以上で必須となった実行時パーミッション確認を行っていないことが原因でした。

バージョンアップ前はターゲットAPIレベル22のため問題は起きなかったですが、Google Playの要件により最低のAPIレベルが26となったため、ターゲットAPIレベルを上げたことで発生するようになっていました。
すでにインストール済みでバージョンアップした場合は録音用のRECORD_AUDIOパーミッションが付与された状態になっていますが、新規インストールした場合はアプリが明示的にパーミッション確認を行わないと、アプリが黙って終了してしまいます。

手元のテスト機にもAndroid6以上の端末がありましたが、前の端末から移行を行ったので、アプリにパーミッションが付与された状態となっており発見できませんでした。
(ということは、端末買う必要なかったorz)
Samsungの機種が多かったのは単に母数が多かっただけのようです。

実行時パーミッションについて

Android6.0(APIレベル26)以上でセキュリティのために導入された仕組みです。
以前はOSが自動的に権限確認を行っていましたが、アプリ側で権限の確認が必要になりました。
以下のページに説明があります。
Request app permissions  |  Android Developers

このページの通り実装することで権限確認ダイアログを表示できました。
これで解決したはずです。

海外仕様端末

買ったのは海外仕様の端末だったので、音楽記号の「♯」がなぜか灰色で表示されるという問題を発見しました。
以前にイギリスの方からメールで、♯の色が薄くて見えづらいとコメントをもらっていて何を言っているのかわからなかったのですが、理解できました。
「♯」は「#」とは別の記号なので、英語圏の端末ではマルチバイト文字のため別のフォントで表示されていたようです。
対処として、Notoフォントをバンドルすることも考えましたが1文字のためにフォントをバンドルしたくなかったため、「♯」を線で描画するようにしました。

今回のことで海外仕様の端末でのテストも必要だということがわかりました。
(端末買ったのは無駄ではなかった!)


もう少しテストしてからまた段階リリースすることにします。
最近知ったのですが、段階リリースのことを「カナリアリリース」と言うそうです。