はじめに
エニグモ サーバーサイドエンジニアの @gugu です。
この記事はEnigmo Advent Calendar 2018の15日目です。
日頃はBUYMAの機能改修を行っていますが、弊社では月末のプレミアムフライデーは業務と関係ない開発を行って良い日となっています。
そこで、前から興味のあった機械学習で何か作ってみようと思いました。
Chainerを使って「まるばつゲーム」を学習させてみたので、簡単にやったことを書こうと思います。
github.com
※ちょくちょくリファクタするかもです。
本題に入る前に
私のスペック
仕様など
- みんな知っているまるばつゲームです。
- 学習させるコンピュータは常に先手
- 0〜8の計9マス。どこに打つかを学習させます。(もう既に打たれたマスにも打つことが可能。当然ルール違反なので負け)
| |
0 | 1 | 2
| |
-----+-----+-----
| |
3 | 4 | 5
| |
-----+-----+-----
| |
6 | 7 | 8
| |
- 盤面のデータは誰も打ってないマスは0、○は1、☓は2の値が入ります。
| |
| | ○
| |
-----+-----+-----
| |
☓ | ○ | ○
| |
-----+-----+-----
| |
| ☓ | ☓
| |
↓ ↓ ↓
| |
0 | 0 | 1
| |
-----+-----+-----
| |
2 | 1 | 1
| |
-----+-----+-----
| |
0 | 2 | 2
| |
配列だと[0, 0, 1, 2, 1, 1, 0, 2, 2]
- コンピュータと戦わせて学習させます。(完全ランダムに打つ。打たれているマスには打たない。不毛な戦いになるので。)
- 盤上の状態を入力して、出力結果のうちの最大値のマスに打ちます
| |
| | ○
| |
-----+-----+-----
| |
☓ | ○ | ○
| |
-----+-----+-----
| |
| ☓ | ☓
| |
のとき入力値は[0, 0, 1, 2, 1, 1, 0, 2, 2]
出力結果がもし[0.1, 0.2, 0.4, 0.2, 0.1, 0.3, 0.9, 0.1, 0.5]
だったら、数値が最大のマス6に打ちます。
- 勝敗結果から打ったマスが好手だったのか悪手だったのかを教えます
Chainerの基礎知識
from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L
class MyChain(Chain):
def __init__(self):
super(MyChain, self).__init__(
l1 = L.Linear(9, 20),
l2 = L.Linear(20, 9)
)
def __call__(self, x):
h = F.leaky_relu(self.l1(x))
o = self.l2(h)
return o
初期設定
model = MyChain()
opt = optimizers.SGD()
opt.setup(model)
打つ場所を決める
x = Variable(np.array([input_data], dtype=np.float32))
model.zerograds()
y = model(x)
y.data.argmax()
学習させる
t = Variable(np.array([teacher_data], dtype=np.float32))
loss = F.mean_squared_error(y, t)
loss.backward()
opt.update()
上記の基礎知識を使ってまるばつゲームを学習させていきます。
学習方法
強化学習はよく理解していないので、基礎知識のみで自己流に学習させます。
def learn(self, result):
for i, y in enumerate(models):
teacher = self.teacher(result, marks[i], y.data[0])
t = Variable(np.array([teacher], dtype=np.float32))
loss = F.mean_squared_error(y, t)
loss.backward()
opt.update()
del models[:]
del marks[:]
def teacher(self, result, mark, model):
data = []
if result == Settings.WIN:
for i in range(9):
if i == mark:
data.append(1)
else:
data.append(model[i])
elif result == Settings.LOSE or result == Settings.SAME_PLACE:
for i in range(9):
if i == mark:
data.append(-1)
else:
data.append(model[i])
else:
for i in range(9):
if i == mark:
data.append(0)
else:
data.append(model[i])
return data
学習方法説明
勝った場合
| |
| | ○
| |
-----+-----+-----
| |
☓ | ○ | ○
| |
-----+-----+-----
| |
○ | ☓ | ☓
| |
最後の一手の出力結果が[0.1, 0.2, 0.4, 0.2, 0.1, 0.3, 0.9, 0.1, 0.5]
で、マス6に打った場合、
- 出力結果: [0.1, 0.2, 0.4, 0.2, 0.1, 0.3, 0.9, 0.1, 0.5]
- 教えるデータ: [0.1, 0.2, 0.4, 0.2, 0.1, 0.3, 1, 0.1, 0.5]
とマス6の値を1
にします。
同じように3手目が[0.3, 0.1, 0.9, 0.7, 0.5, 0.3, -0.3, -0.8, 0.5]
で、マス2に打っていた場合
- 出力結果: [0.3, 0.1, 0.9, 0.7, 0.5, 0.3, -0.3, -0.8, 0.5]
- 教えるデータ: [0.3, 0.1, 1, 0.7, 0.5, 0.3, -0.3, -0.8, 0.5]
とマス2の値を1
にします。
※これを1手目まで繰り返します。
※上記は勝った例なので1
にデータを変換しましたが、負けた場合は-1
に、ドローは0
にデータになります。
学習させてみた
※データは最後から100戦の勝率
100戦
win: 0.04
lose: 0.04
draw: 0.0
same_place: 0.92
1,000戦
win: 0.22
lose: 0.01
draw: 0.0
same_place: 0.77
10,000戦
win: 0.61
lose: 0.06
draw: 0.0
same_place: 0.33
100,000戦
win: 0.81
lose: 0.08
draw: 0.02
same_place: 0.09
強くなってる!
VS 人間
実際に100,000戦したコンピュータと戦ってみました。
※コンピュータ:○、 人間:☓
| |
0 | 1 | 2
| |
-----+-----+-----
| |
3 | ○ | 5
| |
-----+-----+-----
| |
6 | 7 | 8
| |
↓ ↓ ↓
| |
0 | 1 | x
| |
-----+-----+-----
| |
○ | ○ | 5
| |
-----+-----+-----
| |
6 | 7 | 8
| |
↓ ↓ ↓
| |
○ | 1 | x
| |
-----+-----+-----
| |
○ | ○ | x
| |
-----+-----+-----
| |
6 | 7 | 8
| |
↓ ↓ ↓
| |
○ | 1 | x
| |
-----+-----+-----
| |
○ | ○ | x
| |
-----+-----+-----
| |
6 | 7 | x
| |
まともに戦えるが、弱い。。
学習方法を変えてみた
どう変えたか?
「既に打たれたマスに打った」で負けた場合は、最後の一手のデータを調整するように変更します。
ソース
def learn(self, result):
for i, y in enumerate(models):
if result == 3 and i == len(models) - 1:
teacher = self.teacher(result, marks[i], y.data[0], True)
else:
teacher = self.teacher(result, marks[i], y.data[0], False)
t = Variable(np.array([teacher], dtype=np.float32))
loss = F.mean_squared_error(y, t)
loss.backward()
opt.update()
del models[:]
del marks[:]
def teacher(self, result, mark, model, last_flg):
data = []
if result == Settings.WIN:
for i in range(9):
if i == mark:
data.append(1)
else:
data.append(model[i])
elif result == Settings.LOSE:
for i in range(9):
if i == mark:
data.append(-1)
else:
data.append(model[i])
elif result == Settings.SAME_PLACE:
if last_flg == True:
for i in range(0, 9):
if i == mark:
data.append(-2)
else:
data.append(model[i])
else:
for i in range(0, 9):
data.append(model[i])
else:
for i in range(9):
if i == mark:
data.append(0)
else:
data.append(model[i])
return data
学習させてみた
10,000戦
win: 0.96
lose: 0.04
draw: 0.0
same_place: 0.0
コンピュータ相手だと9割以上勝てるようになって、打たれているマスには打たなくなりました。
VS 人間 (2)
100,000戦したコンピュータともう一度戦ってみました。
※コンピュータ:○、 人間:☓
| |
0 | 1 | 2
| |
-----+-----+-----
| |
3 | ○ | 5
| |
-----+-----+-----
| |
6 | 7 | 8
| |
| |
0 | ○ | 2
| |
-----+-----+-----
| |
3 | ○ | x
| |
-----+-----+-----
| |
6 | 7 | 8
| |
| |
0 | ○ | ○
| |
-----+-----+-----
| |
3 | ○ | x
| |
-----+-----+-----
| |
6 | x | 8
| |
| |
x | ○ | ○
| |
-----+-----+-----
| |
3 | ○ | x
| |
-----+-----+-----
| |
○ | x | 8
| |
強い!まだ最強とまでは言えませんが、2つリーチを作れることを覚えたようです。
まとめ
機械学習はハードルがものすごく高いイメージでしたが、Chainerの基礎的な関数を駆使すれば初心者の私でも簡単な機械学習を作成できることがわかりました。
今後は強化学習(DQNなど)をちゃんと勉強して、負けないレベルに修正できたらと思います。