麻雀の危険牌を予測する part1 ~解釈性を添えて~
久々に麻雀AI関連のネタです。 続編を(脳内では)用意してるのでpart1としています。
ちょっとした背景
将棋や囲碁ほどではないですが、麻雀界で麻雀AIが最近少しずつ知名度が上がっています。 有名どころでは「爆打」や「NAGA」、「Suphx」あたりでしょうか。
特にMicrosoft製である「Suphx」はネット麻雀の天鳳で10段という、かなりの上位成績を残しており、最近注目されています。 Arxivに論文も投稿されており、深層強化学習ベースで、学習に対しての工夫も面白いものでした。
私自身、過去に麻雀AIを作っていて作業量の多さから無期限休止してたのですが、仕事やKaggleで得た経験を元に、あらためて何かできないかなと思いやってみました。
「麻雀の危険牌予測」とは?
麻雀では、「手牌から何を切るか?」「立直するか?」「チー・ポンなどの鳴きをするか?」といった判断をプレイヤーが行います。
そしてその判断をするためには
- どの牌が山にどれくらいありそうか?
- 立直してない相手がテンパイしてそうか?
- 立直した/テンパイしてそうな相手にとって何が危険な牌か?
等々の読みを行うことが重要です。 そしてその読みの精度が麻雀の実力における大事な要素とも言えるでしょう。
今回は、そんな読みの中でも、「立直した相手にとって何が危険な牌なのか?」について、機械学習で取り扱ってみます。
問題設定
危険牌の予測について、以下のようにシンプルな問題設定としました。
- 立直者以外の1人のプレイヤー(以降ではプレイヤーAとします)の視点から、立直している人の当たり牌を予測する
- 立直宣言牌が場に出た時点での予測とする
- プレイヤーAから見える牌としては以下の通り。
- 4人の捨て牌
- 自身の手牌
- ドラ表示牌
- それぞれの牌がツモ切りかどうかは見ない
なお、他プレイヤーの副露情報および副露時に晒される手牌(鳴いた牌以外の2枚)については、今回は見えていないという前提にしています。 (ツモ切りかどうかと合わせて本当は大事なのですが、実装の都合上面倒だったので今回は削りました。ごめんなさい)
データの用意
今回は天鳳の鳳凰卓(四麻)4年分(2016~2019)の牌譜データを学習・検証に用いました。
1つの局を1レコードとして441万個レコードあります。
改めてこうして過去分のデータを無料で公開していただいているのは、ありがたいですね。
牌譜データは、mjlogという独自形式であり、パースするのがちょっと面倒だったので下記リポジトリのソースコードを参考にさせていただきました。
モデル設計
今回予測する麻雀の当たり牌は以下の特徴を持ちます。
- 待ちは34種類の牌の中に存在
- 待ちには種類がある(単騎待ち、両面待ち、カンチャン待ちなど)
- 同時に複数枚の待ちがありえて、何種類あるかは分からない。
- 例:単騎待ち、カンチャン/ペンチャン待ちでは1枚、両面待ちでは2枚、国士無双13面待ちでは13枚等
- 特定の組み合わせの待ちのパターン(1-4待ちなど)が多いが、組み合わせは多数ある(シャンポン待ち、清一色多面待ちなど)
また今回、教師あり学習のモデルとしてはLightGBMを用います。 LightGBMを用いる理由としては、後半でも出てきますが、精度面と解釈性などを考慮した結果です。
LightGBMで今回の問題を解こうとした場合、いくつか方法が考えられますが、今回はそれぞれの牌34種類ごとにモデルを作ります。
1萬が当たり牌かどうかのモデル、2萬が当たり牌かどうかのモデル、、、、という形です。
特徴量エンジニアリング
さぁKagglerの腕の見せ所です...!!
と言いたいところではありますが、今回は34個のモデルを学習させないといけません。 ということで、学習時間的にもあまり作り過ぎず、自分のドメイン知識を活かしつつ以下のような特徴量合計144個を作りました。
- 場況情報(各プレイヤーの点棒、場風、自風、本場、ドラ等)
- 見えている牌の情報(それぞれの牌が場に何枚見えているか)
- 立直プレイヤーの捨て牌情報
学習結果
比較用のベースライン
今回1つのモデルしか作っていないため、超簡易なベースラインも作成しました。 ベースラインとしては、安牌と分かっている牌を除く牌が平均1.8枚均等に当たるとした場合のスコアを用いました。 1.8枚としたのは、今回のデータ全体で、待ちの枚数の平均が約1.8枚だったためです。
例えば、立直した人が萬子と字牌のみを全て切っている場合、残りの牌であるソウズとピンズ計18種類それぞれの牌の確率は、
P(1ピン) = P(2ピン) = ..... = P(9ソウ) = 1.0 * 1.8 / 18 = 0.1
P(1マン) = P(2マン) = .... = P(中) = 0.0
となります。
さすがにこれよりは良いスコアが出ないと、何も学習できていないことになります。
評価指標・バリデーション
全データのうち、ランダムに選択した20%の局を評価データ、残りを学習データとして学習を行いました。 麻雀の立直においては、局単位で互いに独立とほぼほぼ言えるはずなので、時間軸は無視してます。
評価指標としては、MultiLoglossを用いています。 MulitiLoglossは多クラス分類かつ、複数の正解があるケースでもよく使われる一般的な指標です。 値が小さいほど、正しく当てられていることになり、0が最も良い精度ということになります。
結果
条件 | MultiLogloss |
---|---|
ベースライン | 6.31628 |
LightGBM | 5.665 |
さすがにベースラインよりは、良い精度が出ています。 とはいえ、あまり大きく変わらない気も。。。そもそもこの数字見ても、どれだけ当てられているかよく分からないですよね。
ということで他の視点でも見てみましょう。
Feature Importance
Feature Importanceとは、どの特徴量がモデルの予測に役だったかを示すものです。
2つの牌で見てみましょう。
1萬
feature | gain |
---|---|
萬子のスジ14をきったか | 428518 |
1萬をきったか | 246415 |
最初に切られた萬子の牌 | 53528 |
立直宣言牌 | 42191.8 |
捨て牌に含まれる筋の数 | 40264.3 |
2萬を切ったか | 35750 |
最初の6枚に萬子が含まれる割合 | 29590.7 |
4萬を切ったか | 29198 |
捨て牌の萬子の割合 | 18617.2 |
3萬を切ったか | 18063.7 |
1萬が何枚見えているか | 12186.1 |
2萬が何枚見えているか | 9478.43 |
2番目に切られた萬子の牌 | 9232.62 |
ヤオチュウ牌の割合 | 7794.8 |
最初の6枚に4,5,6が含まれる割合 | 6770.15 |
立直者の点数 | 6616.69 |
萬子で最後に切られた牌 | 6178.87 |
捨て牌の数 | 5963.82 |
立直宣言牌の数字 | 5644.4 |
萬子の最初の牌と次の牌の数字の差 | 5305.09 |
gainというのが重要度のスコアを表しており、大きいほど重要となります。
当然ながら1萬を切ったかどうか、ということを表現している特徴量は上位に来ています。 それ以外には、最初に切った萬子の牌、2萬を切ったかどうか等の特徴量が上位に来ているのは非常に納得感がありますね。 他にも萬子の1~4あたりに関する特徴量が多いのも納得感がありますね。
「立直者の点数」が上位に入っているのも興味深いですね。 自分の点棒が少ないときはタンヤオ狙いで1萬待ちが減るということでしょうか。
南
feature | gain |
---|---|
南が場に何枚見えているか | 359076 |
南を切ったか | 84561 |
捨て牌に含まれる筋の数 | 19852.3 |
捨て牌の数 | 8081.86 |
場風 | 6031.25 |
最初の3枚でヤオチュウ牌の以外割合 | 4031.93 |
立直した人の点数 | 2902.47 |
字牌で最初に切った牌 | 2767.41 |
最初の6枚で字牌の割合 | 2365.44 |
ドラを切った数 | 2241.72 |
立直宣言牌 | 2138.46 |
立直した人の上家の点数 | 1929.55 |
立直した人の下家の点数 | 1928.33 |
宣言牌が何枚切られているか | 1693.06 |
捨て牌のヤオチュウ牌の割合 | 1466.89 |
立直した人の対面の点数 | 1435.81 |
字牌の割合 | 1189.01 |
ソウズの割合 | 1181.95 |
1萬が見えている数 | 1167.43 |
マンズの割合 | 1109.77 |
こちらも南が切られたかどうかが上位に来ているのは当然ですね。
「場風」が上位に来ているのが、良いですね。 東場よりも南場のほうが南であがると得点があがるので、場風は非常に大事です。
あとは、字牌をたくさん切っていると、数字の待ちの可能性が高くなるし、字牌があまり切られていないと字牌を含んだ変則待ちの可能性が高いはずなので、 ヤオチュウ牌の割合を使った特徴も上位に入っているのも納得です。
個別での分析
やはり、実際の捨て牌でどう予測したかを見ないと、何とも実感ないですよね。 ということでいくつかの場面での予測結果を見てみます。
ここではヒントが多そうな場面にしぼり、 シンプルに立直した人の捨て牌と、それに対する予測確率だけを表示します。
(他の人の捨て牌と合わせて、ノーチャンスの牌などが分からなくなってしまいますが、ご了承ください。)
せっかくなので、(元天鳳9段の)私自身の捨て牌を見た上での見解を述べて、予測結果と比較したいと思います。
場面1
私の見解
- マンズは全体的に安全そう。とはいえ、5-8マンは全然ありうる。
- ピンズは最初に5ピンが切られて、かつ上の方の牌だけ切られていることから、1-4ピンだけ危なく、6ピン, 8ピンは割と大丈夫そう
- ソウズは最初に2ソウ, 3ソウと切られているので1-4ソウはマシそう。4-7ソウ、5-8ソウはかなり危険。
- 特に宣言牌が9ソウなことから、「779」「889」「799」「899」からの9切りによる、7ソウ、8ソウの待ちもありかなり危険
予測結果
- 既に切っている牌はほぼ0になっている
- 7ソウが最も危険と予測しており、私の見解とも合っている
- 危険度合いが4-7ソウ、5-8ソウ > 1-4ピン > 5-8マンというのも感覚と合う
正解
4-7ソウでした。 ちゃんと4-7ソウの危険度が高く、合っていますね。
場面2
私の見解
- マンズは1-4マン、2-5マン、5-8マンどれも危ない。宣言牌周りの1-4, 2-5のほうが危険そう
- ピンズは序盤の2, 9, 7ピン切りから、3-6ピンだけ危険そう。
- ソウズは最後の5ソウ周りの3-6ソウ危険、1-4ソウも危なそう
予測結果
- 5マンの危険度が最も高い。2-5マンと5-8マンの筋どちらもありうるため納得
- 次に3-6ピンが危険。これも納得。
- 1-4マンの危険度が想像より低い。
- ソウズが見えていない筋も少し低め。
→他の人の捨て牌を見ると、 - 2マンが4枚見えているため、1-4マンがノーチャンスであり、低いことがわかりました。(それにしては高い気もしますが) - 1ソウ4枚、4ソウ2枚見えており、1-4ソウは薄い待ち(上がりにくいので立直しにくい)であることがわかりました。
これらを考慮すると納得いく予測だと言えそうです。
正解
3-6ピンでした。 これも危険度を高めに予測できていますね。
場面3
私の見解
いかにも怪しい捨て牌ですね。 序盤から真ん中の牌がどんどん切られています。
ピンフ系には見えません。
また1-9が序盤から切られていること、どの色も切られていることから、染め手やチャンタの線も薄そうです。
チートイツでしょうか。 無筋の牌を警戒しつつも、ヤオチュウ牌も危険という感じでしょうか。
予測結果
- 危険度が高いものTop5としては、「1マン」「4マン」「5ソウ」「1ソウ」「4ソウ」
- たしかにTop5は危なそうなので納得
- 7マンが通っているわけでもないのに、4マンより1マンのほうが危ないと出ているのは、変則待ちの可能性があると判断している?
正解
正解は5ピンの単騎待ちでした。 これはなかなかの難題でした。
モデルの予測としては、大きく外れても当たってもいないという感じですね。
解釈性
モデルの予測結果を見ることで、そこそこの精度では予測できてそうなことが分かりました。
ここで少し話は変わりますが、仮に今回作ったモデルが精度が良かった場合のこのモデルの使い道についても少し考えてみたいと思います。
Suphxのような強いAIを作る上でのパーツとして利用し最強のAIを目指す道も考えられますが、個人的にはもう一つ有力な使い道として、人間への指導ツールとしての価値もあると思っています。
自分が麻雀を打っているときに「この場面では(自分より強い)AIはこういった予測・選択をしている」というのが分かれば、自分が考えていなかったことや考え方の偏りに気付けるかもしれません。
今回はそういった指導ツールとしての使い道も考える上で重要な「解釈性」についても考慮してみます。
解釈性とは簡単に言うと、「なぜAIがその選択をしたかがどれだけ分かるか?」というものです。
今回の危険牌予測で解釈性があるということは、なぜこの牌が危険だと思ったのか?AIの考えが分かるということであり、それがあるほうが指導ツールとしても使えるかもしれません。
解釈性は、しばしばAIをビジネスで活用する上でも重要であり、「xAI (ExplainableAI: 説明可能な AI)」と呼ばれる分野として研究も盛んです。
参考:ドワンゴ製NAGAの牌譜解析レポート
ドワンゴ製のNAGAでこの点を最も考慮して作られているように感じます。 ちょうど先日まで行われていたNAGAが参加する大会では参加者視点でNAGAがどう考えているかのレポートを公開しています。
各局面での期待順位や手牌それぞれの危険度、切ると期待値が高い牌を教えてくれます。
何より、誰でも使えるようなUIになっているのが良いですね。
今回試してみること
今回用いているLightGBMは、深層学習と比べると比較的解釈性は高いと言われています。 実際、特徴量の重要度の算出も簡単に出来ます。
しかし、それはあくまで全体の傾向であり、一つ一つ予測する局面毎での解釈ではありません。
今回は一つ一つの予測で解釈性を持たせる方法として「SHAP」を用いました。
SHAPとは
SHAPとは、協力ゲーム理論のShaply Valueを元にしたアルゴリズムで、各説明変数が目的変数の予測にどれだけ寄与したのかを算出します。
特徴としては、モデル自体がブラックボックスでも取り扱えることです。
アルゴリズムの詳細は割愛しますが、下記の記事が非常に分かり易いです。
SHAPによる実例
上とはまた別の1つの例でみてみます。
SHAP
SHAP Valueはモデル毎(牌の種類毎)に出せます。
今回は捨て牌的に気になる2マンについてみていきたいと思います。
簡単に説明すると、
- 下に書かれた文字が特徴量(都合により英数字での表記です)
- 赤色のものがスコアを上げている特徴量
- 青色のものがスコアを下げている特徴量
- 幅が広いものほど、より寄与している
です。 ここでは特に重要と思われる特徴量だけが表示されています。
この例では「discord_2」が最も幅が大きく寄与しています。discord_2は「2マンが捨て牌に含まれているかどうか」という意味の特徴量です。
これが最も大きくなるのは当然ですよね。
他の特徴量については下記の通りです。
feature | 説明 |
---|---|
manzu_suji14 | 1-4マンのどちらかを切ったか |
manzu_first_hai | マンズで最初にどの牌を切ったのか |
can_see_count_2 | 2マンが場に何枚見えているか |
sengen_hai | 立直宣言牌が何か |
manzu_suji25 | マンズの筋25を切っているかどうか |
discord_5 | 5マンを切っているかどうか |
これらをまとめて簡単に要約すると、 「2マンを切られていなくて、宣言牌や2マンの見えている枚数、萬子の最初に切られた牌からして危険度が上がっている。ただし5マンが切られていることで危険度が少し下がっている」
といった感じでしょうか。
納得感ありますね。
面白いのが、「can_see_count_2」や「sengen_hai」はFeatureImportanceではそこまで上位ではないですが、今回の例では寄与度が高くなっていると言うことです。 すなわち、今回の場面だからこそ重要な特徴量とも捉えられます。
can_see_count_2について
特に納得できるのが「can_see_count_2(2マンが場に何枚見えているか)」です。
5マンが切られていることから、2マンで当たる形としては「カンチャン待ち」か「シャンポン待ち」、「単騎待ち」となります。 この中で特に「シャンポン待ち」は場に2マンが3枚以上見えていたらありえないですし、2枚でも山に0枚の待ちになるので、その選択をする可能性は低いです。 さらに単騎やカンチャンも同じですが、場に見えている枚数が少ない方が一般的には上がりやすいので、待ちになりやすいです。
このことから「can_see_count_2」が1以下かどうかが大事ということになります。
また、5マンが切られていない場合は、2が場にたくさん見えていても5が場に見えていなければ2-5待ちでの立直の可能性は全然あるので「can_see_count_2」はそこまで重要ではないでしょう。
ちなみに今回のcan_see_count_2は1でした。
解釈性についての考察
SHAPを使うことで、FeatureImportanceだけでは分からない各場面での特徴量の寄与度が分かりました。 これがあるだけで、「なぜその予測をしたのか?」が格段に分かりやすくなり、指導ツールとしての使える可能性も大きく広がったと思います。
とはいえ、実際に一通りやってみた上での課題や考慮すべきことも見つかりました。
特徴量の作り方次第で解釈性は大きく変わる
SHAPで分かるのは特徴量の寄与度のみであり、その特徴量自体が解釈できないものであれば解釈性があるとは言えません。
例えば今回はやっていませんが、もっと精度をあげようと思ったら以下のような特徴量エンジニアリングも考えられます
- 34種類の牌が切られた枚数からPCAやTruncatedSVDなどで特徴圧縮
- 各牌を一つの単語とみなし、Word2Vecで学習。Word2Vecでえられた各牌のベクトルの統計量を用いる
仮にこういった特徴量を入れていて、それが上位に来ても何のことかさっぱり分かりません。
また、ここまで複雑でなくても、他の人がパッと理解できるような特徴量にしないと使いづらくなってしまいます。
寄与度から、いかに人間が解釈できてかつ精度も出る特徴量エンジニアリングが出来るかが大事だと改めて感じました。
安牌のデータは学習から外すべきだった
今回、それぞれの牌について使えるデータは全て使いましたが、安牌のケースは学習から外した方が良いと感じました。
例えば、1マンの危険度を学習するときは、1マンが捨て牌にあるケースを学習データから外すといった形です。 そして、そういったケースはルールベースで危険度を0に変更するといった形です。
精度的にもその方がいいのかもしれませんが、解釈性をあげるためにもそうすべきだったと思いました。
今回のやり方では上の例でもそうですが、その牌を切ったかどうかが一番寄与するという結果にどの例でもなります。 これは当たり前のことであり、モデルに予測してもらわなくてもいいですし、その分他に寄与した特徴量の情報が分かりづらくなってしまいます。
麻雀AI以外のビジネスで活用する場合でも、解釈性としてSHAPのようなツールを使う場合、明らかにルールベースで何とかなるものは除いた上でモデリングをしたほうが良いかもしれません。
待ちの種類や手牌についても予測した方が解釈性は上がる
今回の例では危険度をそのまま予測しましたが、解釈性や指導ツールとしての使い道を考えると、「両面待ちか?」「染めていそうか?」といったことも予測し、それに対しての寄与度があった方が価値がありそうです。
麻雀の待ちを予測すると言うのは人間の思考でいくと、「相手の捨て牌から相手が何を手牌に持っていそうか?」、「どこがまだ完成していなさそうか?」、「どういう待ちを選択しそうか?」といった要素的な予測があった上で成り立っています。
強さを求めるだけの麻雀AIでは精度が全てですが、解釈性をあげるとなると出来るだけ人間の思考に近づけたモデルを用意してあげた方が良さそうです。
例えば上の2マンの例では、「立直宣言牌が何か?」が大きく寄与しているという結果でしたが、それだけではなく例えば「2マンが手牌に含まれているかどうかのモデル」があったとして、 「5マンを切った上で立直宣言牌が1マンだから2マンを持っている可能性が高い」という解釈がSHAPで出来ると、とても麻雀の勉強になりそうです。
もちろん無限にモデルを作るわけにはいかないですが、最終的に予測したいものでなくても人間の思考に合わせた目的変数の設計をすることが非常に大事だと分かりました。
その他改善すべきポイント
解釈性としては上に挙げたとおりですが、精度面においても、特徴量の工夫はもちろん下記のようなことをすればもっと良くなると思います(時間の都合上断念しました)。
- フリテンのデータを削除(これが入っていた影響で切っている牌も確率が0にならない予測があったと考えられます)
- 牌の色を入れ替えたDataAugmentation
- プレイヤーの視点(上家、対面、下家)それぞれの視点でのデータを含める
- 待ちの種類や役を予測するモデルとスタッキング
また、実際の麻雀では振り込む確率だけでなく、振り込んだときの得点も込みした期待値が大事なので、打点の予測などもした方が良さそうです。
まとめ
今回はLightGBMとSHAPを使った危険牌予測を行いました。
精度的にはそこそこ出ていそうです。 あと、実際に使ってみることで解釈性としての使い道も少し見えました。
次回は、危険牌予測version2としてTransformerを用いた予測モデルの構築をしてみる予定です!
長文にお付き合いいただきありがとうございました。
ソースコード
散らかっていますが、下記に学習や可視化に使ったソースコードを置いています。