麻雀AIの昨今
ツモ!リーチ一発ツモウラしてますか?
私は去年から麻雀を遊び始めて、日々精進しています。麻雀は囲碁や将棋とは異なり、プレイヤーから多くの情報が隠されている「不完全情報ゲーム」に属しており、これが私およびAIの予測を困難にしているそうです。
しかし、その中でも2019年にマイクロソフトが開発したAI、Suphxがオンライン麻雀プラットフォームの天鳳で十段(最高段位の一つ手前)を達成し、トッププレイヤーに匹敵する実力を示しました。
参考:麻雀 AI Microsoft Suphx が人間のトッププレイヤーに匹敵する成績を達成
Suphexでは、報酬(局収支や最終的な順位)を獲得しながら方策(一回一回の打牌)を模索するような強化学習を使っているそうです。
また、近年ではドワンゴが開発したAI、NAGAも話題になっています。NAGAはSuphexと同じく天鳳で十段を達成した後、NAGAによる牌譜添削サービスがリリースされました。
参考:深層学習麻雀AI「NAGA」
私の打牌がNAGAに怒られている図です。夢見るのはやめろとのことです。はぁい。
NAGAはSuphexとは異なり、プレイヤー(NAGA)から見えている卓上の情報だけを入力として、CNN(畳み込みニューラルネットワーク)を使って打牌を決定します。卓上を絵のように捉えて、似た絵を探して打牌を決める、といったところでしょうか。学習データには天鳳の上位プレイヤーの打牌を使っているそうです。
同じ好成績の麻雀AIでもアプローチが異なるのは面白いですね。
麻雀AIを作ってみよう
手役推測モデル
CNNくらいなら私にも作れるかもしれません。
あがり系から手役を推測するモデルを作ってみようと思います。最初は点数計算をするモデルを作りたかったのですがややこしすぎて諦めました。私も点数計算できないので、しょうがないね。
- 環境:Google colaboratory
- 機械学習フレームワーク:Pytorch
コード全体は最後にまとめました。
インプットデータ
天鳳の対局データからあがりの形を抽出します。天鳳は膨大なデータを提供しているだけでなく、上記のようなAIによるbotの参戦も認めています。こういったプラットフォームがあるのはとってもありがたいですね。
今回はこちらの記事からデータを利用させていただきました。
【麻雀AI】天鳳鳳凰卓の9年分のプレイをCNNを用いたNNモデルに学習させてみた
中を見ると、あがりのデータはこんな感じ。
<AGARI ba="1,1" hai="8,9,10,132,134" m="22641,17929,28682" machi="10" ten="40,12000,2" yaku="28,2,52,4" doraHai="131,129" who="3" fromWho="1" sc="188,0,240,-123,296,0,266,133" />
m="22641,17929,28682"と表現されている副露のデータがわかりにくいですが、ビット演算で表現されていることを頭において解読していきます。ポンと加槓、暗槓と大明槓の扱いが一緒なのが不思議な感じですね。
参考:天鳳牌譜 mjlog形式について・天鳳の牌譜形式を解析する(2)
def furo_to_hai(furo):
furo = int(furo)
if (furo & 0x0004): # 順子の場合
pattern = (furo & 0xFC00)>>10; #「順子のパターン」を取得
pattern = int(pattern / 3);
color_id = int(pattern / 7);
number = pattern % 7 + 1;
mentsu_num = [number, number+1, number+2];
soezi = [(furo & 0x0018)>>3, (furo & 0x0060)>>5, (furo & 0x0180)>>7]; # 牌添字1〜3を取得
mentsu = [color_id*9*4+(i-1)*4 + j for i, j in zip(mentsu_num, soezi)]
elif (furo & 0x0018): # 刻子、加槓の場合
pattern = (furo & 0xFE00)>>9;
pattern = int(pattern / 3);
color_id = int(pattern / 9);
number = pattern % 9 + 1;
soezi = [0, 1, 2, 3]
if (furo & 0x0008): # 刻子
mentsu_num = [number, number, number];
soezi_exclude = (furo & 0x0060)>>5
soezi.remove(soezi_exclude)
mentsu = [color_id*9*4+(i-1)*4 + j for i, j in zip(mentsu_num, soezi)]
else: # 加槓
mentsu_num = [number, number, number, number];
mentsu = [color_id*9*4+(i-1)*4 + j for i, j in zip(mentsu_num, soezi)]
else: # 暗槓、大明槓の場合
pattern = (furo & 0xFF00)>>8;
pattern = int(pattern / 4);
color_id = int(pattern / 9);
number = pattern % 9 + 1;
soezi = [0, 1, 2, 3]
mentsu_num = [number, number, number, number];
if (furo & 0x0003): # 大明槓
mentsu = [color_id*9*4+(i-1)*4 + j for i, j in zip(mentsu_num, soezi)]
else: # 暗槓
mentsu = [color_id*9*4+(i-1)*4 + j for i, j in zip(mentsu_num, soezi)]
mentsu_str = [str(i) for i in mentsu]
return mentsu_str;
あとはこんな感じのデータフレームに整形していきます。実際のデータセットにはtehai_c_intとyaku_labelを使います。
yaku_label | yaku_label_str | tehai_str | tehai_c_int | tehai_im |
---|---|---|---|---|
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | [役牌 白] | [三, 四, 五, 六, 七, 八, 九, 九, ④, ⑤, ⑥, 白, 白, 白] | [2, 3, 4, 5, 6, 7, 8, 8, 12, 13, 14, 31, 31, 31] | 🀉🀊🀋🀌🀍🀎🀏🀏🀜🀝🀞🀆🀆🀆 |
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | [役牌 中] | [三, 三, 三, 五, 六, 七, 2, 2, 6, 7, 8, 中, 中, 中] | [2, 2, 2, 4, 5, 6, 19, 19, 23, 24, 25, 33, 33, 33] | 🀉🀉🀉🀋🀌🀍🀑🀑🀕🀖🀗🀄🀄🀄 |
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | [役牌 發] | [③, ④, ⑤, ⑤, ⑤, 2, 3, 4, 西, 西, 西, 發, 發, 發] | [11, 12, 13, 13, 13, 19, 20, 21, 29, 29, 29, 32, 32, 32] | 🀛🀜🀝🀝🀝🀑🀒🀓🀂🀂🀂🀅🀅🀅 |
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | [役牌 中] | [八, 八, ①, ②, ③, 7, 8, 9, 南, 南, 南, 中, 中, 中] | [7, 7, 9, 10, 11, 24, 25, 26, 28, 28, 28, 33, 33, 33] | 🀎🀎🀙🀚🀛🀖🀗🀘🀁🀁🀁🀄🀄🀄 |
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | [断幺九] | [五, 六, 七, ②, ③, ④, ⑧, ⑧, 3, 3, 3, 4, 4, 4] | [4, 5, 6, 10, 11, 12, 16, 16, 20, 20, 20, 21, 21, 21] | 🀋🀌🀍🀚🀛🀜🀠🀠🀒🀒🀒🀓🀓🀓 |
ちなみにサンプルサイズ111049個に対し、ユニークなあがりの形は93033個でした。意外とあるもんですね。
また先述の記事を参考にさせていただいて、あがった手牌を二次元情報にします。
タンヤオや三色同順が推定しやすくなるのではないかと思って、マンズ、ピンズ、ソウズは同じ数字(例えば一、1、①)が直線上にくるように並べてみました。
アウトプットデータ
麻雀の点数計算をしてくれるライブラリmahjongを参考に、手牌から推測できない役は除くことにします。
参考:Pythonライブラリの「麻雀(mahjong)」って??
つまり、リーチ、一発、嶺上開花、槍槓、海底……
……
手牌から推測できない役、多すぎッ!
さらに門前限定の手を除いて、役の対象は以下とします。
yakuuse = [
# //// 一飜
"断幺九",
"役牌 白","役牌 發","役牌 中",
# //// 二飜
"混全帯幺九","一気通貫","三色同順",
"三色同刻","三槓子","対々和","三暗刻","小三元","混老頭",
# //// 三飜
"二盃口","純全帯幺九","混一色",
# //// 六飜
"清一色",]
役は複合しうるのでマルチラベル分類になりますね。損失関数はマルチラベルで一般的なBCEWithLogitsLossです。
モデルの概形
一般的なCNNのコードを参考にさせていただき、てきとうに畳み込みます。
参考:pyTorchでCNNsを徹底解説
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.relu = nn.ReLU()
self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=2, padding=1) # 第1引数はその入力のチャネル数,第2引数は畳み込み後のチャネル数,第3引数は畳み込みをするための正方形フィルタ(カーネルとも言う)の1辺のサイズ
self.conv2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=2, padding=1)
self.fc1 = nn.Linear(6336, 120) # ここのin_featuresはここまでのカーネルサイズなどに左右される
self.fc2 = nn.Linear(120, LABEL_NUM)
def forward(self, x):
x = self.conv1(x)
x = self.relu(x)
x = self.conv2(x)
x = self.relu(x)
x = x.view(x.size()[0], -1)
# print(x.size()) # fc1のin_featuresを調べる
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
麻雀は3枚1組が大事なのでカーネルはやっぱり3がいいんじゃないかと思いましたが、別にそんなことはなかったです。
1個目の全結合層の入力値はpytorchドキュメントの数式を参考に計算していくか、直前のサイズをprintして確認します。
初っ端から正解率がかなり高くなっていますが、True Negative(含まれていない手役を含まれていないと判断できる)がほとんどだからですね。ここからじわじわと正解率を上げていきます。
yaku_label_str_preが予測したラベル、yaku_label_strが実際のラベルです。
大体予測できるようになりました!清一色はまだわかってなさそうですね。
yaku_label_str | yaku_label_str_pre | tehai_str | tehai_im |
---|---|---|---|
[三色同順] | [三色同順] | [一, 二, 三, 六, 六, ①, ②, ③, ⑥, ⑦, ⑧, 1, 2, 3] | 🀇🀈🀉🀌🀌🀙🀚🀛🀞🀟🀠🀐🀑🀒 |
[断幺九] | [断幺九] | [三, 四, 五, ③, ④, ⑤, ⑥, ⑦, ⑧, 3, 3, 5, 6, 7] | 🀉🀊🀋🀛🀜🀝🀞🀟🀠🀒🀒🀔🀕🀖 |
[断幺九] | [断幺九] | [二, 三, 四, 四, 五, 五, 六, 六, 七, ③, ④, ⑤, 6, 6] | 🀈🀉🀊🀊🀋🀋🀌🀌🀍🀛🀜🀝🀕🀕 |
[役牌 中] | [役牌 中] | [一, 二, 三, ②, ②, 2, 3, 4, 4, 5, 6, 中, 中, 中] | 🀇🀈🀉🀚🀚🀑🀒🀓🀓🀔🀕🀄🀄🀄 |
[清一色] | [混一色] | [一, 一, 二, 二, 三, 三, 三, 四, 五, 五, 六, 七, 九, 九] | 🀇🀇🀈🀈🀉🀉🀉🀊🀋🀋🀌🀍🀏🀏 |
自分で手を動かしたことで麻雀AIが身近になりました。これからもNAGAにお世話になって麻雀の勉強をがんばります。