Measuring Coding Challenge Competence With APPSを読んだメモ

 (GitHubリポジトリ)

概要

 自然言語の問題文を入力としてコードを出力する、競技プログラミングに似た形式のタスクについて、APPという新しいデータセットを構築した。

 データセットについてGPTモデルを評価したところ、簡単な問題についてAC率が0%~4%程度であった。

詳細

APPデータセット

  • 問題はCodewars, AtCoder, Kattis, Codeforcesから抽出
  • 総計1万問
  • 正当例となるソースコード約23万
  • 問題文の平均単語数は293.2単語
  • テストケースの数は1問あたり平均21.2ケース

 問題形式としては2種類

  • Call-Based Format problems(TopCoder形式) : 関数シグネチャは準備されたコードが与えられるので、関数内部にコードを記述
  • Standard Input Format problems(AtCoder形式) : 標準出力にprint関数などを使って答えを出力

 具体的な入力の仕様

"\nQUESTION:\n" + q_str + "\n" + starter_code_str + "\n" + "\nUse<br>Call-Based Format\n\nANSWER:\n"

 要するに

QUESTION
(問題文)
(関数のシグネチャ)
Use Call-Based Format
ANSWER:

 Standard Input Format problemsだとシグネチャは空で最後のUse ~ の部分が変わる。たとえばABC126のA問題なら

Question
You are given a string <var>S</var> of length <var>N</var> consisting of <code>A</code>, <code>B</code> and <code>C</code>, and an integer <var>K</var> which is between <var>1</var> and <var>N</var> (inclusive).
Print the string <var>S</var> after lowercasing the <var>K</var>-th character in it.

Use Standard Input Format
ANSWER:

みたいな感じだろう。latex解釈も含むという記述もあったので数式的なところもタグを残しつつ入れたりしているんじゃないか(ここは勝手な解釈)。

 問題を難易度で次の3つに分類

  1. Introductory Level
    • 1〜2年の経験を持つほとんどのプログラマーが複雑なアルゴリズムを必要とせずに答えられる問題
    • (例)部分文字列の出現数を数える、文字列が回文であるかどうかを見つける
    • 訓練データ3,639、テストデータ1,000
  2. Interview Level
    • よりアルゴリズム的で本質的に困難な問題、難しい技術面接で尋ねられる質問のレベル
    • (例)木やグラフなどのデータ構造に関連する問題や、一般的なアルゴリズムの変更が必要な問題
    • 訓練データ5,000、テストデータ3,000
  3. Competition Level
    • IOIとかICPCで出るレベル
    • 訓練データ1,361、テストデータ1, 000

評価実験

 GPT-2, GPT-3, GPT-Neoなどにこれらのタスクを解かせる。

  • GPT-2 : 事前学習済みパラメータが公開されているが、公開パラメータはソースコードをもとにした学習がされてないため、後述するような形で事前学習を追加で行い、さらにAPPデータセットでFine-Tuningする
  • GPT-3 : パラメータが公開されてないのでAPIを叩いて使うだけ(ちょっと理解に自信がない)
  • GPT-Neo : 事前学習済みパラメータが公開されているので、それをもとにAPPデータセットでFine-Tuningする

GPT-2の事前学習

 GitHubから集めたソースコードで事前学習する。GitHubのスターが1以上あるリポジトリから、APPデータセットに含まれている問題を解いているようなリポジトリを除外し、30GBのPythonコードを収集。単語数節約や表記統一のために複数スペースをタブに置き換えて学習を実行。

 (ここはちょっと知識不足で、入力・出力をどう決めるのかよくわからなかった。次の単語予測の言語モデルとして学習? それだと入力単語が自然言語文ではなくコードになるので事前学習としてどこまで意味があるのかピンとこない)

Fine-Tuning

 事前学習したGPT-2および、公開されている事前学習済みパラメータを使ったGPT-3,GPT-Neoに関して、今回作ったAPPデータセット1万問中のうち半分の5千問を使ってモデルをFineTuningする。

  • OptimizerはAdamW
  • バッチサイズ256
  • Weight Decay 0.05
  • 10エポック
  • DeepSpeedを使用(ZeRO optimizer)
  • 生成時はビーム幅5のビームサーチを使用(その他はHuggingFaceのデフォルトを使用)

評価指標

  • Test Case Average : 単純に全問題のテストケースに対しての正答率(あまり意味がなさそう)
  • Strict Accuracy : その問題についての全てのテストケースに正答できた問題の割合(要はACできた割合。これが重要そう)

結果

f:id:tokumini:20210522104313p:plain

 AC率は一番簡単なIntroductory Levelについて0.2%〜3.9%。Competition Levelでは全滅(まぁそれはそうだろう)。

 パラメータ数の多いGPT-3がGPT-2(0.1B)に負けており、Fine-Tuningは重要そう。

 Test Case Averageと構文エラー率を棒グラフで表示

f:id:tokumini:20210522104552p:plain

 構文エラー率は緑色のGPT-Neoで3%程度となっており、これはとても低い値を実現できている印象。

出力例1

f:id:tokumini:20210522112515p:plain

出力例2

f:id:tokumini:20210522112526p:plain

所感

 CodeNetはtrain/testみたいな切り分けが確かされておらず、どちらかというと入力予測とか分析系とか、そういうための元手として用意されていたのかなと感じていた。一方今回のAPPデータセットは真剣に競技プログラミング的なタスクとして確立させる気合いを感じる。

 ABCのA問題を1%でも解けるなら結構すごいことに思える。リーダーボードが出来て精度勝負が始まりだすとタスクの解決としては半分勝っているようなものに思えるので、これが流行れば競プロAIの発展も結構進むのかもしれない。

 でも結局これらは英語がメインになってしまうと思うので、母語が日本語である人らで日本語版を頑張って再現していく必要はありそう。GPT-2日本語版の学習済みパラメータが公開されていたと思うが、そこからコードの学習をしてさらにFine-Tuningして、という資源はなかなか大変か。

 競プロのプレイヤー的には一度出力して終わりではなくて、WAだったときに修正する機能みたいなのは欲しい、というか重要になるんじゃないかと感じるところ。でもタスクの定式化が難しくてあまり簡単には想像できない。

おまけ

 結局人手でデータセットを綺麗に作るのがとても大事なんだよね。

Several graduate students and undergraduates polished and refined this dataset spanning half a year, ensuring a high-quality set of problems.

研究生活の振り返り

 所属していた研究室では、修士論文として書いたものを適宜改稿して論文誌へ投稿するのが慣例となっており、自分もそれに倣って2月頃に投稿していた。

 大学院を卒業してニート生活も2週間が過ぎようとしているつい先日、その査読結果が返ってきて、結果は不採録だった。自分は研究、特に最終成果物として論文を生成することについてはとても向いてなかったなぁと痛感する。

 おそらく一生、少なくともしばらくの間は研究分野に戻ることはないだろうと思われるため、これで研究生活は一区切りということになる。世に出せなかった論文を成仏させる意味も込めて、研究生活を振り返る。

 所属していた大学ではB4になるときから研究室へ配属されるため、B4、M1、M2の3年間研究をしていたことになる。多少の誤差はあるが、概ねそれぞれ1年ずつ別の研究をしていたという感じだったので、それぞれについて思い出を記述する。

B4での研究(2018年4月〜2019年5,6月あたり)

 テーマ「評価値を確率分布化する」

 いつもの将棋でやっているこのテーマはB4のときから始めたものだった。AlphaZeroarXivに出たのが2017年12月5日で、これにとても惹かれるものを感じ、関連した研究をやりたいとは前から思っていた。

 なんにしても計算量が馬鹿にならないため、手元で再現できるくらいに学習速度向上(小さなネットワークでの精度向上)の工夫としてCategorical DQNを組み込むような形を試そうと思ったのが研究テーマの基本的な動機である。

研究初期 : 3層MLP・オセロ

 最初は深いネットワークではなく3層の全結合ネットワークで、オセロについて実験をしていた。浅いネットワークなのでNN用のフレームワークは使わず、行列計算ライブラリのEigenを用いて手動で全結合ネットワークを実装し、全てCPUで計算している。3層なので難しくはないが、誤差逆伝播も自力で実装している。

 オセロプログラムとして非常に低レベルな性能ではあるが、Categoricalモデルが多少有効だという結果は出たので、学部の卒論はこの結果で書いた。

研究後期(2019年2月頃〜) : LibTorch導入・将棋でも実装

 学部の卒論を書き終えて大きくプログラムを書き換える時間的余裕が出来たので、PyTorchのC++APIであるLibTorchを利用するように改修を行い、またオセロだけでなく将棋でも動かせるように実装した。大きくプログラムの仕組みが変わったのを機に、将棋ソフトの名前を『海底』から『Miacis』に変更して、以降これがメインのプログラムとなっていく。

論文

 卒論をベースに将棋の実験結果も加えて英語化し、IEEE Transaction on GamesというゲームAIに関する英文紙へ投稿したが、結果としてはReject。実装・実験が甘く、将棋でも技巧Depth10あたりとの比較だったので最先端とは程遠いというのも受けが悪い要因ではあった。

 一回Rejectが出たからといってすぐ諦める必要もないとはわかっているが、結局強いプログラムを作らなければ説得力が低いと思ったし、論文に対するやる気を完全に失って別のテーマをやりたくなってしまった。

M1での研究(2019年6月〜2020年6月)

 テーマ「ボードゲームにおける世界モデルの学習」

 この頃、深層強化学習分野で「世界モデル」という考え方がかなり流行っていた(と認識している)。強化学習における環境としてシミュレータを使うのではなく、その環境の変化の仕方自体も学習することを目指す分野である。

 自分の興味・関心もかなりこれに近いところにあったため、いくらかサーベイなどをしつつ、夏のインターンとしてDeNA社の強化学習コースに応募したところ採用されて、ほぼこのテーマでやらせてもらった。

 DeNA社の方ではそちらのゲーム(逆転オセロニア)でやりつつ、自分は自分で手元で将棋についてやっていた。Miacisのrepresentationというブランチがそれで、「表現ベクトル(Policy, Valueに分岐する直前の層) + 行動→次のターンの表現ベクトル」という予測を学習している。

 わりと順調に進んではいたが、2019年11月19日にMuZeroの論文がarXivに出て、完全に直撃してしまった。

 まぁアイデア的にこういう方向へ進むのはとても自然なので、むべなるかなといったところではある。

対外発表

 MuZeroとは被ってしまったが一応ドメインの差などを主張し、社の優秀な人たちからアドバイスを貰うことでなんとか形にして、人工知能学会で発表を行った。

M2での研究(2020年6月〜2021年1月)

 テーマ「時系列モデルを用いたゲーム木探索自体の学習」

 MuZeroで完全に被ったのがちょっとショックでもあり、また計算資源の差は大きいという現実も思い知って、M2として行うテーマを決めはなかなか難航した。時間もなくなっていく中で最終的にこういうテーマに落ち着いたわけだけど、自分の中で完全にしっくり来るものではなくて、追求しきれなかったところも多い。

 テーマの内容としては、探索における記憶部分(αβ探索やモンテカルロ木探索における置換表)を、時系列モデルで代替できないかという着想から始まっている。LSTMあるいはTransformerを使って、部分木を超えた情報伝達ができるようになれば良いと思っていたが、なかなか上手くはいかなかった。

 このテーマだと、最大限上手くいっても「同じ回数(20回程度)のMCTSよりは性能上がるかも」くらいであり、大会に出すプログラムとして性能が伸びるという手法ではない。結局、Miacis自体の性能改善をやりたくなってそっちに注力してしまったところもあり、今思えばもっと性能に直結することをテーマにすれば良かったかもしれない。(ただ、残された時間がそれほど多くない中ではっきりと性能を上げる自信がなく、結果が上手くでなくても逃げやすそうなものを選んでしまった。)

論文

 情報処理学会に投稿してReject。やはりテーマに対する自信のなさが尾を引いて、実験結果が詰めきれていないし、論調も優柔不断になっていて、箸にも棒にもかからないという感じであった。

 自分としても研究室の慣例なのでとりあえず出したという感触ではあり、これは仕方がないかなと思う。根本的に取り組む時間が短すぎたのも研究の質へもろに響いたと言えるだろう。

総括

 M1のものは査読なしの4ページなので半分くらいのものと見るとして、2.5本書いて世に出せたのは0.5本という感じか。もう少しやれれば良かったが、こんなもんかなとも思う。

 論文としての業績を重視するなら、B4のときに書いたものを別の論文誌に出し直すとか、粘ってみるべきだったのだろう。しかしそこに強いモチベーションは抱けなかった。将棋プログラムとしてこれで明確に性能が上がってほぼトップソフトになっているならともかく、そうでもない現状では他人に伝えるほどの手法ではないと感じてしまう。

 結局自分は研究はあまり好きではなく、実装パズルゲームがやりたいだけだったなというのが正直な気持ちである。たとえばアイデアを一直線に検証することに集中するなら、dlshogiのリポジトリをフォークして必要な改造だけ施して実験するやり方もあったわけで、目的に対して最短経路を取れていなかったなとは反省するところだ。

 自分としてはそれよりも新しいアイデアを今までのMiacisにどう組み込むかという部分を考えるのが一番楽しかったし、自分なりに奇麗だと思う実装をできたときは充実感があった。しかしそういう実装パートは研究全体からすれば部分的な要素でしかない。そこに齟齬があったというのが現状の認識となる。

 とはいえ、基本的には好きな・楽しい実装ばかりできたので研究生活は楽しかったといって間違いではない。やってみて、向いてなかったなとわかったのも一つの収穫だろう。結果を出せなかったのは残念だけど、満足ではある。以上。

Miacis for Android開発日記

 深層強化学習を用いた将棋ソフト『Miacis』を開発しており、PyTorchのライブラリを眺めていたらAndroidで動かせるなーということに気づいたので作ってみました。

 アプリのリンク

 ソースコードのリンク

 Androidアプリというか、一般にGUI付きのアプリを開発する事自体が初めてだったので、記録を残した方が面白いかなと思って開発当初からの日記をつけていました。途中から書くのが面倒になってかなり雑になってしまったが、お蔵入りにする意味もなさそうなので公開。

3月15日

 開発環境はUbuntu20.04。目標は「将棋アプリを作り、Miacisと対局できるようにする」

 以下の記事を参考にした。

 まずはAndroid Studioをインストールするところから。公式ページに従ってやるととりあえず起動はできた。

 手元のAndroidをUSBケーブルで繋いでも認識しない問題が発生。まずそもそもadb serverが見つからないとかいう話だったので開発パソコンにインストール。

sudo apt install adb

 あとはAndroid側で開発者モードをオンにする作業を行う。「設定>システム>端末情報>ビルド番号」を連打して、開発者向けオプションを有効にし、「設定>システム>開発者向けオプション>USBデバッグ」をONに変える。

 それでも見つからなかったけど、Android本体とパソコンを繋ぐケーブルを別のものに替えたら認識した。ひょっとしてこういうケーブルって充電しかできないものと情報のやり取りまでちゃんとできるものの区別がある? そういうことすら知らないレベルです。

 とりあえずコードもなにも変えないまま最初のHello World!が手元のデバイスで表示された。

3月16日

 とりあえず公式チュートリアルを読んでいくことにする。

 アクティビティやエントリポイントとかいう概念がイマイチ掴みきれない。アプリ、プロジェクト、モジュールあたりの用語もどれがどういう感じのものを指すのか。まぁこのあたりはやっていくうちにわかってくるだろう。

 チュートリアル通りに進めて、テキストボックスとボタンを表示させた。

 Android StudioGUIでテキストボックスなどの配置をいじると自動的にxmlが更新されて適切に配置してくれるということらしい。すごいもんだ。そもそもxmlがなんなのか正確には理解していないが、なんかそういう書式の決まったファイルなんだろうという程度。

 画面遷移と画面上部のメニューによる「もとの画面に戻る」まで実現するところでチュートリアルが終わった。意外と短く、まだなんもわからない感じはする。まぁなんかxmlファイルと上手く連携しつつ中身だけプログラムで受け渡しながらやっていくという感じなのだろうな。

 流石にこれだけではどうしようもないのでなんかそれっぽいやつに進む。

 Unit1の3つ目までやった。なんだかわかるようなわからないような感じ。

3月17日

 Unit1の4つ目を終わらせて、サイコロが振れるようになった。

 Unit2からチュートリアルに日本語訳がないようだ。英語、つらい。

3月18日

 Unit2-2まで終わらせた。

 色を変えるとかテーマを変えるとか、アイコンを設定するとか、スクロールバーを追加するとか。適切に動作を示したアイコンを追加するとか、そういう振る舞いができる気がしない。難しい。

 あとTalkBackの項目が何度探しても見つからなくてそこは検証できなかった。検索してもそんな症状なさそうだし、よくわからない。

3月19日

 Unit2-3まで終わらせた。

 List系は特になんてことはないけど、RecyclerViewの細かいところは理解が及ばないところもあった。

3月21日

 Collection型は大丈夫、だと言いたいところだが、Setの順番が違う可能性があるって、内部的にこれはどういうふうに保持しているんだろう。挿入とか削除の計算量が示されないのであとで調べておかなきゃ。

 とうとう画面遷移がまともに始まった。Activityが画面単位というのはわかるが、IntentやらContextやらがよくわからない。というか画面遷移的なコードはAdapter部分に書くのか。そのあたりの役割分担もまだ馴染んでいない。

 なにがなんやらさっぱりわからなかった。

 Activity lifecycleは、考え自体はわかるが実際に自分でアプリを作って適切に設定するのは大変そう。注意力コンテストか?

3月22日

 Unit3-2、フラグメントについて。なんもわからん。指示に従うことすらまともにできないレベルで何もわからない。さらにナビゲーショングラフとかいうものまで出てきた。意味不明。

 これを英語で読んでいくのは不可能と判断したのでとりあえず断念。

 基本を学ぶのにも飽きてきたので自分が作りたいものをやっていくフェーズにとりあえず移行してみる。

 まずは以下の記事を参考に、根本的にNNモデルをアプリ内で動かせるかどうかの検証。

 とりあえずメインから2つのActivityに遷移できるような状態を作成し、そのうち片方で特定の画像に対して学習済みResNetを用いて推論する機能を実装。

f:id:tokumini:20210322154809p:plainf:id:tokumini:20210322154811p:plain

 バージョンの違いで少し手間取ったけど上手くできた。

 Miacisで実際に学習したモデルを使った推論もできた。ここができたということはかなり完成できる可能性が高まったように思う。

 将棋盤部分を作っていく。たとえば以下のようなものを参考にした。

 素材には以下のフリー素材を使った。

 ガチャガチャやってこんな感じ。

 ImageViewを81マス分用意して、そのマスに置かれている駒を表示させる。駒がマスギリギリの大きさで詰まっていてやや違和感はあるんだけど、そこを直すのは微妙に面倒なような。僕の頭が悪いだけでなんか簡単なやり方があるか?

 あとこれ見ていて気がついたけど駒台を作らなくてはいけないんだな。上下に適当に置いておけば良いか。レイアウト的に入らないものがスマートフォンがあったりしたらどうしましょ。

3月23日

 PositionMoveクラスなどをC++からKotlinへ移植していく。結構記法が違うので面倒が多い。

 とりあえず初期局面を設定して描画することはできた。

 合法判定は入れていないが、駒を動かすこともできた。

 しかしここからの合法性判定が難しい。やっぱり合法手を全て生成してその中に入っているかどうかで判定するのが楽な気がするので合法手生成からやりたいが、Bitboardがおそらく使えないのでどうするか。PEXTは無理なんじゃないかな。Rotated Bitboardなら作れるかもしれないが、元のMiacisがそういう仕組みじゃないので勉強するところから始めないといけない。

 多少遅くていいから配列から上手く生成する手段を考えたい。ピンされた駒が動いて自殺手になるのが大変ではあるんだよな。

 とりあえず特定のマスに利きがあるかどうかを求める関数を作ればなんとかなるか。というわけで実装して、二歩とかはまだ詰めてないけど合法手生成を入れた。

3月24日

 駒台を追加して持ち駒を表示できるようにした。

 基本的には合法手しか指せないし、かなり将棋盤っぽくはなってきたが、まだ成るか成らないかを選べない。これ結構面倒かもしれないな。

 ぴよ将棋のこれとかどうやってるんだろう。

 ダイアログか、言葉がわからないと検索もできないので大変だったけど、多分これはダイアログで実現するものだろう。

 えー、でもモーダルダイアログというものじゃないとダイアログを表示している間に勝手に処理が進んでしまう。これどうすれば良いんだろう。Androidではモーダルダイアログなんてないよって言ってる記事ばかりだ。ビジーループでも待てないし、どういうこと? ダイアログなんてそこの入力をもとに動作を切り替えたいから使うんじゃないの? なんでそれができないの?

 関数として切り出してコールバックで呼び出せば良いのか。それでできたが、なんだかなー。

 選択中の駒を強調表示する(背景色を変える)機能を入れた。

 かなり将棋アプリらしくなってきた。しかしまだ思考部分も全然書いてないし対局設定とかも全然設定できる感じではないので先は長い。

3月25日

  • こっちが指したら相手も一手指す仕組みを導入して、対局ができるようになった。
  • ホーム画面にボタンを追加して以下の4モードで起動できるように変更
    • 検討モード
    • 先手として対局
    • 後手として対局
    • 手番をランダムに決めて対局
  • 指し手生成のバグを修正

3月26日

  • Policyを5手分表示するように修正
  • Valueを棒グラフで表示する参考

 KotlinのRange checkは計算量大丈夫なんだろうか。多分大丈夫なんだろうけど、確信が持てるまでは変えたくない。

3月29日

 後手で指す場合は盤面を180°回転させて表示したいわけだけど、これが難しい。なんか妙にバグる。脳みそがバグる。自動補完された変数名が想定と違うものになっていたことに気づかずずっとよくわからない現象に悩まされていた。

 打ち歩詰めの判定を入れ忘れていたことを思い出して、これ合法手生成に入れるの難しいんだよなーって思ってたんだけど、打ち歩詰めになる手は生成してしまって、局面の勝敗判定のところでやってしまうのでいいか。

 対局成績を保存したいのでデータの保存などについて調べる。参考

3月30日

  • 入玉模様のときにバグることが発覚。王手判定で玉への利きを検証するときに、玉から桂馬の利きを生やそうとして盤外へ飛び抜けていた。
  • 宣言勝ちを実装
  • 動かしたときに自動的に思考を走らせる自動検討モードを実装
  • モード切替時などにポップアップを追加 参考
  • 対局成績の記録を追加。アップデート入れたときにファイル内容がどうなるかとか知らないので簡単に消し飛んでしまいそうなところはあるなぁ 参考

 いや、小さいデータの保存はSharedPreferencesを使う方が良いのかもしれない 。修正した。

  • Chainという機能があることを知ったのでちょっと使ってみた 参考
  • Kotlinでクラス変数を作る方法が調べてもよくわからなかったんだけど、companion objectを使えば良かったんだなということに気づいて修正

3月31日

 アイコンを決める(きふわらべ氏が以前描いたものを利用させていただくことに)。そう言えば去年なんか全員分描いてもらっていたんじゃなかったっけと過去ツイートを掘り出したら超カワイイMiacisが出てきて即利用許可をお願いしに行った。快諾してもらい感謝感謝。

  • 投了を追加
  • その他諸々の細かい変更
  • リリース 参考
  • com.exampleは使えないって、まぁそれはそうなんだろうけど、どう命名したら良いのかよくわからないな。適当
  • 適当に画像とか作って、とりあえずリリース完了。審査待ち

4月1日

  • 自動検討モードの切り替えをトグルスイッチで行うように変更
  • 思考部分をCoroutineで実装して別スレッドへ 参考
    • 並列化については結構苦労していろいろ調べたけど、結局公式ページをなんとか読解していくので多分実現できた。自動翻訳なのか知らないけど読みにくい……
    • Dispatcherというのがよくわからないけど、なんかDefaultで良いっぽい 参考

4月2日

  • SFENの入力で文字列の合法性を判定する機能を追加
  • 対局設定のActivityを追加
  • バージョンを上げてリリースし直した。審査まだかなー

4月4日

 評価値グラフを追加

4月5日

  • 自動検討モードのオン/オフを記憶する仕組みを導入したらめちゃくちゃバグりまくった
    • 最初からオンになっていると、起動直後に検討が何回か入って、排他制御が甘いので盤面が壊れて落ちるという現象っぽい。排他制御、本質的に難しい
    • 盤面のコピー機能を追加してなんとかした。まだ落ちるタイミングはあるようだけど

4月7日

 とうとうリリースされた。審査に1週間くらいかかったな……。途中でバージョン2とか3を次々投入したのがダメだったんだろうとは思うが、開発途中だと毎日更新くらいしたくなりそうな気がする(気が短いので完成を待つということができない)。

 とりあえず開発日記としてはここまで了

教師あり学習 + 強化学習

要約

 教師あり学習(300万ステップ)の後に強化学習(10万ステップ)を行うことでR+50.5、さらにもう一度強化学習(10万ステップ)を行うことでR+61.9。floodgateでレート3800ほどになった。

学習の全体

 ランダムパラメータから始めて、以下の3つをこの順に行った。

  1. AobaZeroの棋譜を用いた教師あり学習(300万ステップ)
  2. 強化学習その1(10万ステップ)
  3. 強化学習その2(10万ステップ)

各学習の設定

共通の設定

  • ミニバッチサイズは512
  • 検証データはfloodgate2015年の棋譜

教師あり学習

  • AobaZeroの棋譜11,500,000 ~ 14,300,00を使用(ちょっと古めで宣言勝ちは多めの時期だと思われる)
  • 学習率0.025開始
  • 6万ステップごとに学習率1/10
  • Weight Decay 1e-4
  • 使用GPUは2080ti x1

強化学習1

  • 探索回数は1600回
  • Weight Decayを入れ忘れた
  • 学習率減衰は8万ステップ時点で1/10
  • 学習スレッドのスリープ時間は、生成速度から自動調整して2.744秒(1ステップの間にミニバッチサイズの半分程度を生成)
  • 使用GPUは2080ti x2

 以下実際の設定ファイルを置いておく(自分があとで見る用)

折りたたみ

learn_rate              0.00025
min_learn_rate          0.000025
momentum                0.9
weight_decay            0.0
policy_loss_coeff       1.0
value_loss_coeff        1.0
lambda                  0.75
per_alpha               2.0
Q_dist_temperature      0.01
Q_dist_lambda           1.0
noise_epsilon           0.25
noise_alpha             0.15
C_PUCT                  2.5
use_fp16                0
draw_turn               320
random_turn             320
batch_size              512
thread_num_per_gpu      2
max_step_num            100000
learn_rate_decay_mode   1
learn_rate_decay_step1  80000
learn_rate_decay_step2  200000
learn_rate_decay_step3  200000
learn_rate_decay_step4  200000
learn_rate_decay_period 10000
update_interval         1000
batch_size_per_gen      2
worker_num_per_thread   64
max_stack_size          1048576
first_wait              1048576
data_augmentation       1
Q_search                0
search_limit            1600
search_batch_size       4
output_interval         1
save_interval           2500
validation_interval     2500
sleep_msec              -1
noise_mode              0
wait_sec_per_load       90
use_sam_optim           0
calibration_kifu_path   /root/data/floodgate_kifu/valid

# Shogi
init_buffer_by_kifu     0
train_rate_threshold    2800
valid_rate_threshold    3000
train_kifu_path         /root/data/floodgate_kifu/train
valid_kifu_path         /root/data/floodgate_kifu/valid

# Othello
# init_buffer_by_kifu     0
# train_rate_threshold    2200
# valid_rate_threshold    2290
# train_kifu_path         /root/othello_train_kifu
# valid_kifu_path         /root/othello_valid_kifu

強化学習その2

  • 探索回数は1600回
  • Weight DecayはOFFのまま
  • 学習率減衰はrestartなしの1周期Cosine Annealing
  • 学習スレッドのスリープ時間は、強化学習その1より長くしたかったので決め打ちで5秒
  • リプレイバッファサイズを通常の2倍(221局面)にした
  • 使用GPUは2080ti x2

折りたたみ

learn_rate              0.00025
min_learn_rate          0.0
momentum                0.9
weight_decay            0.0
policy_loss_coeff       1.0
value_loss_coeff        1.0
lambda                  0.75
per_alpha               2.0
Q_dist_temperature      0.01
Q_dist_lambda           1.0
noise_epsilon           0.25
noise_alpha             0.15
C_PUCT                  2.5
use_fp16                0
draw_turn               320
random_turn             320
batch_size              512
thread_num_per_gpu      2
max_step_num            100000
learn_rate_decay_mode   2
learn_rate_decay_step1  80000
learn_rate_decay_step2  200000
learn_rate_decay_step3  200000
learn_rate_decay_step4  200000
learn_rate_decay_period 100000
update_interval         1000
batch_size_per_gen      2
worker_num_per_thread   64
max_stack_size          2097152
first_wait              2097152
data_augmentation       1
Q_search                0
search_limit            1600
search_batch_size       4
output_interval         1
save_interval           5000
validation_interval     5000
sleep_msec              5000
noise_mode              0
wait_sec_per_load       90
use_sam_optim           0
calibration_kifu_path   /root/data/floodgate_kifu/valid

# Shogi
init_buffer_by_kifu     0
train_rate_threshold    2800
valid_rate_threshold    3000
train_kifu_path         /root/data/floodgate_kifu/train
valid_kifu_path         /root/data/floodgate_kifu/valid

# Othello
# init_buffer_by_kifu     0
# train_rate_threshold    2200
# valid_rate_threshold    2290
# train_kifu_path         /root/othello_train_kifu
# valid_kifu_path         /root/othello_valid_kifu

損失推移

教師あり学習

f:id:tokumini:20210325102430p:plainf:id:tokumini:20210325102437p:plain
左:Policy損失 右:Value損失

 特に言うこともなく、今まで何度も目にしてきたようなグラフが得られた。

強化学習その1

f:id:tokumini:20210325103814p:plainf:id:tokumini:20210325103817p:plain
左:Policy損失 右:Value損失

f:id:tokumini:20210325102934p:plainf:id:tokumini:20210325102937p:plain
検証損失のみ拡大(左:Policy損失 右:Value損失)

 特徴的なグラフになった。学習序盤の動きが激しい理由としては、

  1. 教師あり学習で得たValueは-1, 0, 1の3領域のみを使うものだが、強化学習のTD(λ)によってそれら以外の領域も使うようになり、その修正で大きく変わっていくため
  2. 設定ミスでWeight Decayを入れ忘れたため

の2通りが考えられるか。その他ちょこちょこ回してみた結果だと(2)の説が強めかもしれない。

強化学習その2

f:id:tokumini:20210325104322p:plainf:id:tokumini:20210325104325p:plain
左:Policy損失 右:Value損失

f:id:tokumini:20210325104354p:plainf:id:tokumini:20210325104357p:plain
検証損失のみ拡大(左:Policy損失 右:Value損失)

  • 損失の急激な上昇や低下は見られなかった
  • Policyの検証損失はやや増加傾向
  • Valueの検証損失はやや減少傾向

具体的な数値

学習方法 Policy損失 Value損失
教師あり学習終了時 1.917 0.561
強化学習その1終了時 1.791 0.605
強化学習その2終了時 1.774 0.597

学習時間

学習方法 hh:mm:ss
教師あり学習終了時 175:55:07(≒7.3日)
強化学習その1終了時 091:39:56(≒3.8日)
強化学習その2終了時 153:22:36(≒6.3日)
合計 420:57:39(≒17.5日)

 強化学習は2080tiを2枚搭載したマシンで行ったものであり、強化学習の学習時間はほぼGPUの枚数に比例するので、1枚のマシンで行うと約2倍、つまりその1とその2合計で20日程度になるかと思われる。教師あり学習と合わせて28日、約一ヶ月でおそらく再現できると思う。

対局

 YaneuraOu(評価関数パラメータはKristallweizen)との対局を行った。Miacis側1手0.5秒、YaneuraOu側はNodesLimit=800000。

学習方法 勝数 引分数 負数 勝率 相対Eloレート
教師あり学習終了時 479 208 313 58.3% +58.2
強化学習その1終了時 642 19 339 65.1% +108.7
強化学習その2終了時 720 15 265 72.8% +170.6

という結果になった。損失があまり改善されていないことからすると意外に強くなっている。

 強化学習その2後のパラメータでfloodgateに数日放流した結果、レート3840となった。

f:id:tokumini:20210325112523p:plain

 ただし、対戦相手を見るとKristallweizenが多く、単にKristallweizenに対して相性が良いというだけの可能性も捨てきれない。

所感

  • 強化学習を2回に分けているのは本質的じゃないので、上手く設定を見つけて1回にまとめたい
  • Weight Decayを入れるかどうかというのが結構大きく影響してきていそうな印象を受ける。教師あり学習直後の強化学習では勾配クリッピングとかも入れてみた方が良いのかもしれない
  • 1周期Cosine Annealingが雑に使ってそこそこ性能出る印象がある。学習率のスケジュールに悩みたくないのでしばらくはこれでやってみたいか
  • 学習時間もわりと現実的な範囲で、3090x2みたいなマシンが手に入れば結構試行錯誤できるんじゃないか

ランダムパラメータからの強化学習

 TensorRTなどの高速化も導入したので、ランダムパラメータからの強化学習をやり直した。

実験設定

 いつも通り、細かい差異はあれど基本的にはAlphaZeroと同様の設定。

使用パソコン

  • CPU:Intel Core i9-9900K @ 3.6GHz(8core 16thread)
  • メモリ:32GB
  • GPU:RTX 2080ti x2

 以前はドスパラでほぼ同性能のものが60万円で売られていたが、今はなくなっている。

モデルサイズや学習時間など

 256ch 10ブロックのものを使用した。高速化を入れてデータ生成速度は179.6 局面 / 秒となった(左右反転のデータ拡張を含む)。

 1ステップあたりに学習するバッチサイズ(512)の半分程度のデータを生成できるように、学習スレッドについて1ステップごとに1.4秒程度のスリープを挟むようにした。

 合計の学習時間は440時間48分(≒18.4日)となった。生成された棋譜の数は1,566,905であった。

 自分以外にどこまで伝わるかはわからないが、実際に学習で使った設定ファイルを(使ったところだけ抜粋して)折りたたんで置いておく。(これを書いていて気づいたが、int8の設定を有効にしていなかったようだ……。もう少し高速化できたか)

折りたたみ

learn_rate              0.025
momentum                0.9
weight_decay            0.0
policy_loss_coeff       1.0
value_loss_coeff        1.0
lambda                  0.75
per_alpha               2.0
Q_dist_temperature      0.01
Q_dist_lambda           1.0
noise_epsilon           0.25
noise_alpha             0.15
C_PUCT                  2.5
use_fp16                1
draw_turn               320
random_turn             320
batch_size              512
thread_num_per_gpu      2
max_step_num            1000000
learn_rate_decay_mode   1
learn_rate_decay_step1  800000
update_interval         1000
batch_size_per_gen      2
worker_num_per_thread   64
max_stack_size          1048576
first_wait              1048576
data_augmentation       1
search_limit            800
search_batch_size       4
save_interval           50000
validation_interval     50000
sleep_msec              -1
noise_mode              0
use_sam_optim           0
calibration_kifu_path   /root/data/floodgate_kifu/valid
valid_rate_threshold    3000
valid_kifu_path         /root/data/floodgate_kifu/valid

実験結果

損失

 floodgate2015年の棋譜について指し手と最終的な勝敗を予測することで損失を計算した。

f:id:tokumini:20210318101758p:plainf:id:tokumini:20210318101804p:plain
左:Policy損失 右:Value損失

 途中やや乱れるところもあったが全体的に右肩下がりで、まだ収束しきっているかとは言い難い。800Kステップ時点(学習率を1/10にしたタイミング)でValue損失の方は大きく下がったが、Policy損失の方はあまり変わらなかった。

 最終ステップでの損失値をAobaZeroの棋譜を用いた教師あり学習と比較すると次のようになる。

学習方法 Policy損失 Value損失
強化学習(1Mステップ) 1.825 0.621
教師あり学習(3Mステップ) 1.917 0.561

対局結果

 最終ステップのパラメータについてのみYaneuraOu(評価関数パラメータはKristallweizen)との対局を行った。Miacis側1手0.5秒、YaneuraOu側はNodesLimit=800000。

学習方法 勝数 引分数 負数 勝率 相対Eloレート
強化学習(1Mステップ) 493 7 500 49.6% -2.4
教師あり学習(3Mステップ) 479 208 313 58.3% +58.2

 教師あり学習モデルより悪い性能となった。128chなどのモデルでは教師あり学習強化学習でだいたい同じ程度の性能になっていたが、ニューラルネットワークを大きくした影響か、差が出るようになった。強化学習中に生成しているデータの質や量が十分でないため、モデルの表現力ではなくデータによる上限が先に来てしまっているのではないかと考えられる。

 余計な工夫を考えるよりも、単純にデータの生成量を増やし(学習スレッドのスリープ時間を伸ばし)、かつ探索上限を1600回などにすればもう少しは性能が伸びそうな気がするので、計算資源があればそういうところを試してみたい。

Post Training Quantization(PTQ)の導入

要約

 PTQ(要するにINT8演算)の導入でR+30程度

実装

 ニューラルネットワークは基本的に浮動小数点演算(FP32)を用いている。今までは半精度浮動小数点演算(FP16)により高速化を行っていたが、Post Training Quantization(PTQ)という、FP32の範囲を絞ってINT8の演算に変換することで高速化する手法も存在する。TRTorchでもこれがサポートされているのでチュートリアルに従って試した。

 なかなか不安定なところもあったが、

  • Calibrationデータとして取り出すデータのミニバッチサイズを、コンパイルで指定する最適バッチサイズと同じにする
  • Calibrationデータの総数をミニバッチサイズの整数倍にする
  • CalibrationのアルゴリズムにはIInt8MinMaxCalibratorを使用する

あたりに気をつけることで上手くいくようになった感覚がある。ライブラリをそのまま使うだけなので実装自体の難易度は低く、下のように十数行でTorchScript形式のモデルをコンパイル・Calibrationできる。

強化学習で得た128chのモデルについて

損失計測

 floodgate2015年の棋譜を用いてCalibrationを行い、2019年の棋譜に検証損失の計算を行った。

 Calibrationに用いるデータ数(局面数)を変えながらINT8での計測を行った結果が以下となる。

データ数 Policy損失 Value損失
FP16(比較用) 1.8464 0.6429
64 1.8565 0.6450
128 1.8537 0.6435
256 1.8537 0.6442
512 1.8569 0.6437
1024 1.8583 0.6435
2048 1.8584 0.6438

 損失はいくらか悪化している。

 Calibrationに用いるデータ数が多ければ多いほど良いわけではなかった。後の256chでの結果も踏まえて、推論時のバッチサイズ(64)のちょうど2倍のデータ数(128)が最も良い値と見なすことにした。以降のNPSの測定ではそのデータ数でCalibrationを行っている。

NPSの測定

演算精度 初期局面 中盤の局面
fp16 35198 ± 606 30392 ± 1802
INT8 37191 ± 797 34403 ± 1003
(倍率) 1.057倍 1.131倍

 128chのモデルではもともと推論が比較的高速なので、INT8による推論にしたところで恩恵が大きくない。これだと損失の悪化の方が大きいのではないか。1.1倍程度ではまともなレート差になりえないので対局はスキップ。

教師あり学習(AobaZeroの棋譜)で得た256chのモデルについて

損失計測

データ数 Policy損失 Value損失
fp16(比較用) 1.8390 0.5804
64 1.8442 0.5855
128 1.8495 0.5829
256 1.8552 0.5846
512 1.8511 0.5849
1024 1.8501 0.5883
2048 1.8565 0.5896

 128chに比べて全体的に悪化幅が大きくなった。

NPSの測定

演算精度 初期局面 中盤の局面
fp16 18612 ± 188 16278 ± 474
INT8 26790 ± 481 24140 ± 836
(倍率) 1.439倍 1.482倍

 256chだとNPSの向上が大きい。NPS2倍でレート100とすると、1.4倍あれば+50程度は見込める。これなら測定できそうだと思ったため256chモデルについては対局も行った。

対局

 Miacisは1手0.5秒

 対戦相手の条件

  • 探索エンジン:やねうら王
  • 評価関数:Kristallweizen
  • NodesLimit:800000

 結果

演算精度 勝数 引分数 負数 勝率 相対Eloレート
FP16 382 186 432 47.5% -17.4
INT8 426 207 367 52.9% 20.5

 R+37.9となった。+50には届かなかったが、精度の悪化も含めるとこれでも伸びすぎかもしれないと思うくらいではある。

dlshogi(GCT)との比較〜その2〜

要約

 現状のMiacisとdlshogi(GCT:電竜戦ver)とのレート差約300の内訳は

  1. 評価関数の差で約200
  2. 探索速度の差で約100

と予想。

損失計測

 前回とほぼ同様の設定で、floodgateの2015年の棋譜だけでなく2016年から2020年までの棋譜それぞれについて損失を計測した。

 前回からの違いとして、終了ステータスがSENNNICHITEとなっている棋譜は計測対象から除外した。

 得られた局面数はMiacis側でもdlshogiのhcpeに変換するスクリプトでも変わらず、以下のようになった。

データ数
2015 2477935
2016 4049364
2017 4263861
2018 2981430
2019 6086236
2020 11552275

 また前回はMiacisの中でもAobaZeroの棋譜から教師あり学習したパラメータについてのみ比較したが、今回はランダムパラメータから強化学習のみで得られたパラメータとも比較する。これらのパラメータは対局を行うとほぼ同程度の性能である。つまり、性能として見ると「dlshogi(GCT)>>Miacis(教師あり学習)≒Miacis(強化学習)」というような形になっている。

 損失計測結果

f:id:tokumini:20210209172102p:plain

f:id:tokumini:20210209172109p:plain

 ここ2,3年の棋譜にはdlshogi、Miacis、あるいは学習データもとのAobaZeroが指した棋譜も含まれる。その影響か、あるいは単に戦型の流行か、Policy損失は低めに出る傾向が見られる。逆にValue損失は上がっている。

 しかし明確に順位が大きく入れ替わるような年があるわけではなく、概ねどのデータでもそれぞれのモデルの比較結果としては変わらないように思える。

 前回は失念していたが、今までの結果から得られる感覚として、同じ損失値でも教師あり学習で到達する場合と強化学習で到達する場合は対局時の性能が異なる。教師あり学習は(AobaZeroの棋譜からなのでfloodgateの棋譜とはやや違うが)それでも性能のわりに損失が小さくなりがちではある。赤線は正直当てにならないと思いたい。

 問題は強化学習の方(青線)である。これは明らかにValue損失が劣っている。以前の試算では

Value損失が0.01小さくなればレートが86上がる

という仮説を立てた。今回計測した結果では、Miacis(強化学習)とdlshogi(GCT)のValue損失の差は0.050 ~ 0.027なので、素直に当てはめればR差 429.1 ~ 234.8 となる。そこそこ妥当な数値ではあるかもしれない。

対局

 ふかうら王(評価関数:GCT)は3000ノードで水匠30万ノードとほぼ互角というデータが出ている。

 Miacisでも探索ノード数3000で対局を行った。使用したパラメータ上の損失計測でも用いた強化学習で得たパラメータである。

 探索部 : 最新のやねうら王 commit id b0a3a2a4f7565bbefb85999368df15e9c90c621f リンク

 評価関数パラメータ : 水匠3改(20210111) リンク

 対局設定における若干の相違点として

  1. 対局時のスレッド数は複数のまま
  2. 開始局面は互角局面集ではなく初期局面

がある。

 Miacisから見た結果

1000局 231勝 2引分 767敗 勝率23.2% レート差-207.9

 評価関数の差か、3000探索内の効率の差かはわからないが、これだけの差があるようだ。

 個人的にはほぼ同内容のMCTS(PUCT)・かつ3000ノードでそこまで差はつかないと思っているので、これがそのまま評価関数の差として見ても良いのではないか。先の損失計測から見たレート差からするとちょっと小さめではあるが。

NPS

 手元でもdlshogiのusiエンジンをビルドできたのでNPSを測定した。とりあえず面倒なので初期局面だけ。オプションなどはなにもいじっていない。

モデル NPS
Miacis 24580±1614
dlshogi 45638±600

 約2倍となっており、手元の計測では2倍でレート100という感覚ではある。

結論

 というわけでこれらの比較から冒頭の要約で示したような考えを持った。

 ちなみに根本的なレート差が約300であるというのはそれぞれ以下のfloodgateでのレートが根拠。ここからGCTはさらに評価関数が伸びているようなので、評価関数部分にもっと伸びる余地を探りたいところだ。

TensorRTの導入

要約

 TensorRT(TRTorch)を導入したことでNPS約1.3倍、R+70ほど。

実装等

 前回、同じデータについてMiacisとdlshogiの評価関数を比較した結果、そこまで精度に差がないのではないかという結果が得られた。この結果が正しいとすると、棋力の差は探索速度や効率に原因あるのではないかという仮説が立つ。

 GCTの加納さんから情報提供をいただき、少なくとも探索効率以前にNPSで差があることは明らかになった。

 ニューラルネットワークの推論速度についてはTensorRTが速いらしいのでそれを導入することにした。

 MiacisはPyTorch(およびそのC++APIであるLibTorch)を使っている。いくらか調べた結果、そこからTensorRTを用いたグラフのコンパイルをするためのライブラリとしてTRTorchというものが良さそうだったため、これを利用することにした。

 このライブラリではTorchScriptという形式を入力とすることでTensorRTを用いたグラフを構築することができる。というわけでまず、今までのモデルをTorchScriptへと変換する必要がある。C++内でこれを行う手法はよくわからなかったため、多少汚いものになったがPythonスクリプトを実装した。

 C++で定義しているニューラルネットワークと同じ構造のニューラルネットワークPython内で定義し、パラメータを一つ一つ手動で読み込んでくるといったものになっている。

 TorchScriptモデルが出力できれば、あとはTRTorchのチュートリアルが示すとおりに読み込み、コンパイルを行えば良い。以前強化学習によって得たパラメータをTensorRTでコンパイルし、全く同じ検証損失が得られることを確認できた。

NPSの測定

 初期局面と中盤の局面(sfen l2+P4l/7s1/p2ppkngp/9/2p6/PG7/K2PP+r+b1P/1S5P1/L7L w RBGS2N5Pgsn2p 82)の2つで計測を行った。それぞれ10秒の思考を10回行った平均値となる。

モデル NPS(初期局面) NPS(中盤局面)
LibTorch版(ランダムパラメータ) 21260 17862
TRTorch版(ランダムパラメータ) 26937 24745
LibTorch版(学習済みパラメータ) 6626 5922
TRTorch版(学習済みパラメータ) 24580 22299

 LibTorchの学習済みパラメータだけ異様にNPSが低く、妙である。ランダムパラメータで比較した場合TRTorchの導入でNPSは1.27倍だが、学習済みパラメータで比較すると3.71倍となる。

 完全に同じ学習済みパラメータではないが、1年ほど前に同じ関数で計測したNPSをツイートしており、そのときはもっと高かった。

そのときの詳細な結果が残っていたのでそれを見たところ、初期局面:18553, 中盤局面:15120であった。この結果で計算し直すとTRTorch版はLibTorch版の1.32倍であり、ランダムパラメータのときとだいたい同じくらいになる。

 学習済みパラメータを用いたLibTorch版での対局が弱いことはないので、NPSの表示だけがおかしいと見なすのがもっとも収まりがよく思える。この点に関しては要調査。

対局

 強化学習を経て得られたパラメータを用いて対局を行った。対戦相手は探索部:去年の5月時点Yaneuraou、評価関数パラメータ:Kristallweizenとなっている。Miacis側は1手0.25秒、Kristallweizen側はNodesLimit=400000。定跡はオフ。

モデル 勝数 引分数 負数 勝率 レート差
LibTorch版 558 1 441 55.9% 40.8
TRTorch版 655 0 345 65.5% 111.4

 R+70.6という結果になった。NPS約1.3倍にしてはレートの上がり方が大きいように思う。とはいえ伸びたので良し。

余談

 環境を新しくしたことで、最初は間違えて対戦相手のYaneuraouの探索部を最新のバージョンにしてしまっていた。そのときの対局結果が以下の通り。

モデル 勝数 引分数 負数 勝率 レート差
TRTorch版 451 0 549 45.1% -34.2

 先の結果と比べると-145.6であり、やねうら王の探索部は約1年でこれくらい上がっているようだ。

dlshogi(GCT)との損失比較

 同じデータに対して評価関数の性能比較を行った。

 使用データはfloodgate2015年の棋譜(リンク内のwdoor2015.7z)

であり、以下の条件でフィルターをかける。

  • 手数が60手以上
  • 対局両者のレートの大きい方が3000以上
  • 終了状態が%TORYO, %SENNICHITE, %KACHIのいずれかである

 使用する棋譜については1手目から投了直前の局面まで全てを利用する。Policyの教師は実際に指された手をOnehotとし、Valueの教師は最終的な勝敗とする(評価値の情報は使わない)。

 手元のプログラムでもdlshogiのcsa_to_hcp.pyを改造したプログラムでも同じく2,477,935局面を得た。

Miacisのバグ

 CSA形式の棋譜には必ず終了条件が書き込まれている(%から始まる行が絶対にある)という前提で実装していたのだが、どうも時間切れの対局など最後の終了状態が無い場合があるらしい。

 時間切れ負けが起きると優勢なのに負けになり、Valueの教師が局面の形勢とはかけ離れることになる。これはデータとして適当ではないため除外するべきだ。

 この修正により、Miacisが検証データとして取り出す局面数が、2,727,833局面→2,477,935局面となった。9%強が減っており、今までは異常な局面を多く検証データとして利用していたのかもしれない。

 128chで学習したパラメータについて修正前後で検証損失を計測した。

パラメータ Policy損失 Value損失
修正前 1.9499 0.6453
修正後 1.9439 0.5766

 Value損失がとても小さくなった。やはり今までのフィルターでは時間切れ負けなど局面の形勢とかけ離れたValueを示す局面が多かったのではないか。これまで取ってきたデータが全部信用ならなくなった。

dlshogiでの計測

データ整形

 基本的にはcsa_to_hcpをベースとした。

 出力をhcpeにした点以外では、次の

 for i, (move, score) in enumerate(zip(parser.moves, parser.scores)):

で回した後に、そのiを利用して書き込みを行ったり局面数を数えているが

        hcps[:i].tofile(f)
        num_positions += i

これでは書き込む・数えるデータ数が1少なくなっているように思う。意図的な動作なのかはちょっとわからなかったが、今回自分で使うには修正した方がわかりやすかったので直した。

実際に使用したスクリプト(長いので折りたたみ)

import numpy as np
from cshogi import *
import glob
import os.path

MAX_MOVE_COUNT = 512

# process csa
def process_csa(f, csa_file_list, filter_moves, filter_rating, limit_moves):
    board = Board()
    parser = Parser()
    num_games = 0
    num_positions = 0
    hcpes = np.empty(MAX_MOVE_COUNT, dtype=HuffmanCodedPosAndEval)
    for filepath in csa_file_list:
        parser.parse_csa_file(filepath)
        if parser.endgame not in ('%TORYO', '%SENNICHITE', '%KACHI') or len(parser.moves) < filter_moves:
            continue
        cond = (parser.ratings[0] < filter_rating and parser.ratings[1] < filter_rating)
        if filter_rating > 0 and cond:
            continue
        board.set_sfen(parser.sfen)
        assert board.is_ok(), "{}:{}".format(filepath, parser.sfen)
        # gameResult
        skip = False
        for i, move in enumerate(parser.moves):
            if not board.is_legal(move):
                print("skip {}:{}:{}".format(filepath, i, move_to_usi(move)))
                skip = True
                break

            # hcpe
            board.to_hcp(hcpes[i:i+1])
            hcpes[i:i+1]["bestMove16"] = move16(move)
            hcpes[i:i+1]["gameResult"] = parser.win

            board.push(move)

        if skip:
            continue

        # write data
        hcpes[:i + 1].tofile(f)

        num_positions += i + 1
        num_games += 1

    return num_games, num_positions

if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('csa_dir', help='directory stored CSA file')
    parser.add_argument('hcp', help='hcp file')
    parser.add_argument('--filter_moves', type=int, default=60, help='filter by move count')
    parser.add_argument('--filter_rating', type=int, default=3000, help='filter by rating')
    parser.add_argument('--recursive', '-r', action='store_true')
    parser.add_argument('--limit_moves', type=int, default=40000, help='upper limit of move count')
    parser.add_argument('--before_kachi_moves', type=int, help='output the position until N moves before sengen kachi')

    args = parser.parse_args()

    if args.recursive:
        dir = os.path.join(args.csa_dir, '**')
    else:
        dir = args.csa_dir
    csa_file_list = glob.glob(os.path.join(dir, '*.csa'), recursive=args.recursive)

    with open(args.hcp, 'wb') as f:
        num_games, num_positions = process_csa(f, csa_file_list, args.filter_moves, args.filter_rating, args.limit_moves)
        print(f"games : {num_games}")
        print(f"positions : {num_positions}")

損失計測

 GitHubで公開されている、電竜戦で優勝したGCTのパラメータmodel-model-0000167.onnxを使ってonnxのruntimeで呼び出す形式で行った。dlshogiはValueを[0,1]で出力するので、出力と教師を両方2倍してから1引いて[-1,1]の範囲に直し、自乗誤差を取った。

使用スクリプト(長いので折りたたみ)

import numpy as np
import torch
import torch.optim as optim
from dlshogi.common import *
from dlshogi import serializers
import cppshogi
import argparse
import os
import onnx
import onnxruntime as ort
import torch.nn.functional as F


parser = argparse.ArgumentParser()
parser.add_argument('test_data', type=str, help='test data file')
parser.add_argument('--testbatchsize', type=int, default=32, help='Number of positions in each test mini-batch')
args = parser.parse_args()

model_name = "model-0000167.onnx"
ort_session = ort.InferenceSession(model_name)
cross_entropy_loss = torch.nn.CrossEntropyLoss(reduction='none')
mse_loss = torch.nn.MSELoss()

# Init/Resume
test_data = np.fromfile(args.test_data, dtype=HuffmanCodedPosAndEval)

# mini batch
def mini_batch(hcpevec):
    features1 = np.empty((len(hcpevec), FEATURES1_NUM, 9, 9), dtype=np.float32)
    features2 = np.empty((len(hcpevec), FEATURES2_NUM, 9, 9), dtype=np.float32)
    move = np.empty((len(hcpevec)), dtype=np.int32)
    result = np.empty((len(hcpevec)), dtype=np.float32)
    value = np.empty((len(hcpevec)), dtype=np.float32)

    cppshogi.hcpe_decode_with_value(hcpevec, features1, features2, move, result, value)

    z = result.astype(np.float32) - value + 0.5

    return (features1,
            features2,
            move.astype(np.int64),
            result.reshape((len(hcpevec), 1)),
            z,
            value.reshape((len(value), 1))
            )

itr_test = 0
sum_test_loss1 = 0
sum_test_loss2 = 0
sum_test_loss = 0
with torch.no_grad():
    for i in range(0, len(test_data) - args.testbatchsize, args.testbatchsize):
        x1, x2, t1, t2, z, value = mini_batch(test_data[i:i+args.testbatchsize])
        y1, y2 = ort_session.run(None, {'input1': x1, 'input2': x2})

        t1 = torch.tensor(t1)
        t2 = torch.tensor(t2)
        z = torch.tensor(z)
        value = torch.tensor(value)
        y1 = torch.tensor(y1)
        y2 = torch.tensor(y2)

        itr_test += 1
        loss1 = (cross_entropy_loss(y1, t1)).mean()
        loss2 = mse_loss(y2 * 2 - 1, t2 * 2 - 1)
        loss = loss1 + loss2
        sum_test_loss1 += loss1.item()
        sum_test_loss2 += loss2.item()
        sum_test_loss += loss.item()

    print('test_loss = {:.08f}, {:.08f}, {:.08f} = {:.08f}, {:.08f}'.format(
        sum_test_loss1 / itr_test, sum_test_loss2 / itr_test, sum_test_loss / itr_test))

結果

パラメータ Policy損失 Value損失
Miacis 1.9439 0.5766
dlshogi 1.9273 0.5803

 意外にもほとんど変わらない値であり、Value損失ではMiacis(128ch)の方が低くなっている。

 現状のMiacisが2080ti1枚でfloodgate R3600程度であり、

 dlshogitestの4138とは相当差がある。この差がほとんど探索部分の影響というのはにわかには信じがたいところなのだが。

 説としては3通りか。

  • 実装ミスをしている
  • この検証データ・および損失計測方法は棋力との相関が小さい
  • 本当に推論・探索の速度や効率で大きな差がある

Sharpness-Aware Minimizationの検証

 Sharpness-Aware Minimizationという手法が提案されています。

 詳しい人が説明してくれています(僕もこれで知りました)。

 上記事の

パラメータ周辺での最大の損失を求めて、それが下がる方向でパラメータを更新する

というのが基本的なコンセプトでしょう。

 非公式ですが、再現実装も公開されています。

 それほど複雑でもなかったのでMiacisでも実装してみました。

 以下AobaZeroの棋譜を用いて実験して結果を掲載します。

Policy損失

f:id:tokumini:20210129100227p:plain

 最終的な検証損失

手法 時間(hhh:mm:ss)
通常のSGD 1.950
SAM(SGD) 1.864

 はっきりと改善が見られました。1.8台というのは強化学習を最後まで回しきってなんとか見られるかどうかという値であり、1.86というのはとても小さいという印象です。

 train損失は通常のSGDより悪い値となっていますが、SAMの方では2回目の損失、つまり近傍内で悪化する方向に移動してからの損失を表示しているので妥当なのかなと思います。

Value損失

f:id:tokumini:20210129100250p:plain

 最終的な検証損失

手法 時間(hhh:mm:ss)
通常のSGD 0.6453
SAM(SGD) 0.6520

 Value損失は途中までは良かったんですが、最終的な値は通常のSGDよりも悪化してしまいました。

検証対局

Miacis time = 250msec, YaneuraOu time = 250msec, YaneuraOu Threads = 4,NodesLimit=400000
手法 勝数 引分数 負数 勝率 相対レート
通常のSGD 488 117 395 54.6% 32.4
SAM(SGD) 411 109 480 46.6% -24.0

 残念ながら対局では性能が上がってないという結果になりました。個人的な印象ですが、他の実験を見ていてもPolicy損失よりValue損失の方が重要だという印象があります。

学習時間

 SAMは1回損失・勾配を計算して周囲の最も悪いところに移動した後、もう一度損失・勾配を計算するので単純に考えると2倍の時間がかかります。実際の学習時間は

手法 時間(hhh:mm:ss)
通常のSGD 080:44:58
SAM(SGD) 136:37:22

となり、2倍ではないにしろそこそこの増加はありました。しかし強化学習では「データ生成時間 >> 学習時間」なのでこの程度の増加は許容できるでしょう。

 一応Policy損失の方で良さそうな雰囲気は出ているので強化学習でも試してみようと思います。