LoginSignup
20
18

More than 5 years have passed since last update.

Pylearn2で三目並べのAIをつくってみる

Last updated at Posted at 2016-06-19

はじめに

pylearn2でDeep Learningしてみたい。でもチュートリアルの画像判別やってるだけだとよく分からない。ということで、三目並べ(いわゆる○×ゲーム)のAIを作ってみようと思います。
本当はオセロのAIを作ってみたかったのですが、pylearn2を使用するのが初めてなので、より簡単なゲームを選びました。
ちなみに機械学習、pylearn2初心者です。間違いなどあればご指摘ください。
pylearn2、およびnumpyなどの必要なパッケージのインストール手順は省きます。

ニューラルネットの設計

三目並べのAIなので、まずは盤面上の状態を表すために、3x3のボードのマスに番号を振ります。
3x3.png

3x3の盤面のとある状態を入力とし、次の手の位置が出力となるようにします。そこで以下のようなネットワークを考えます。
n-net.png

入力と出力の丸の肩にある番号は盤面上のマスの番号です。
9つの入力、つまり9マスの状態を入力とします。隠れ層を一層とし、活性化関数をシグモイド関数とします。出力は9つのクラスに分けるので、ソフトマックス関数を使用します。
ここで、「9つのクラスに分ける」とは、ある入力(盤面の状態)に対して、次の一手をどのマスに打つかでクラス分けする、という意味になります。つまり、0にクラス分けされた場合は、0のマスに次の一手を打つ、となります。

教示データの作成

今回作成するAIは強さを追求しません。教示データからルールを逸脱せずにゲームを進めてくれれば御の字、と割りきっています。そのため、最善手を打っている対戦データだけを集めるのではなく、単にルールに則って対戦しているデータを大量に用意します。
大量の対戦データの生成をするために、先手、後手ともにランダムに置ける場所に置いていくだけのRubyスクリプトを作成しました。なぜpythonで書かないんだと言われそうですが、最近pythonばっかり書いていて飽きた、というだけで深い意味はありません。
先手が〇、後手が×です。
引数にLoop回数を指定して実行すると、棋譜が作成できます。これを教示データとします。
数字は置いたマスの番号、末尾のwin,loseは先攻の勝敗を示しています。

tic_tac_toe.rb
#!/usr/bin/env ruby
#
# tic tac toe
#
# 0 : O (first)
# 1 : X (second)
# 2 : none
#

def show_board(array)
  p array[0][0].to_s + "," + array[0][1].to_s + "," + array[0][2].to_s
  p array[1][0].to_s + "," + array[1][1].to_s + "," + array[1][2].to_s
  p array[2][0].to_s + "," + array[2][1].to_s + "," + array[2][2].to_s
  p ""
end

def judge(array)
  ret = 2
  for stone in [0, 1] do
    for i in [0 ,1, 2] do
      if (array[i][0]==stone && array[i][1]==stone && array[i][2]==stone) ||
         (array[0][i]==stone && array[1][i]==stone && array[2][i]==stone)   then
        ret = stone
      end
    end

    if (array[0][0]==stone && array[1][1]==stone && array[2][2]==stone) ||
       (array[0][2]==stone && array[1][1]==stone && array[2][0]==stone)   then
      ret = stone
    end
  end
  return ret
end

loop_max = ARGV[0].to_i
#p "loop max=" + loop_max.to_s
cell_array = [] 
stone_array = [] 

loop_cnt = 0
until loop_cnt >= loop_max do
  cell_array = Array.new(9)
  stone_array = Array.new(3).map { Array.new(3, 2) }
  9.times do |num|
    cell_array[num] = num
  end

  i = 10
  history = []
  9.times do |num|
    #p i
    rnd = rand(i) - 1
    if num % 2 == 0
      stone_array[cell_array[rnd].divmod(3)[0]][cell_array[rnd].divmod(3)[1]] = 0
    else
      stone_array[cell_array[rnd].divmod(3)[0]][cell_array[rnd].divmod(3)[1]] = 1
    end
    history.push(cell_array[rnd])
    #show_board(stone_array)
    ret = judge(stone_array)
    if ret == 0 then
      history.push("win")    # "O" is winner.
      break
    elsif ret == 1 then
      history.push("lose")   # "O" is loser.
      break
    end

    cell_array.delete_at(rnd)

    i -= 1
  end
  p history.join(",")
  loop_cnt += 1
end
$ ruby tic_tac_toe.rb 500 | tee tic-tac-toe_records.log
"6,8,3,4,2,0,lose"
"7,1,4,5,0,2,8,win"
"6,2,3,0,5,8,1,7,4,win"
"3,8,4,2,6,5,lose"
"1,8,2,7,3,0,6,4,lose"
"8,0,3,6,7,4,1,2,lose"
"6,8,3,5,2,7,4,win"
"2,1,7,4,3,5,8,6,0"
"4,8,6,7,1,3,2,win"
"6,1,3,0,8,7,5,4,lose"
"8,2,7,1,4,3,0,win"
"8,6,1,2,7,0,3,4,lose"
"4,3,8,1,2,6,7,0,lose"
"8,6,3,4,1,5,7,2,lose"
"1,2,0,4,7,8,5,6,lose"
"0,5,2,3,6,7,8,4,lose"
"7,1,2,6,4,5,0,3,8,win"
"2,1,0,8,3,5,7,4,6,win"
"2,0,8,5,6,7,4,win"
...

この棋譜データを入力として使用しやすいようにcsvに加工しておきます。今回は後攻の手を決めるAIを作るため後攻が勝利したものだけ抜き出しています。

$ awk '{gsub("\"","");print $0;}' tic-tac-toe_records.log | grep lose | tee tic-tac-toe_records_lose.csv

加工後はこうなります。

tic-tac-toe_records_lose.csv
6,8,3,4,2,0,lose
3,8,4,2,6,5,lose
1,8,2,7,3,0,6,4,lose
8,0,3,6,7,4,1,2,lose
...

これで準備は完了です。

pylearn2を使用したMLPモデル

まずソースコードを示します。

tic_tac_toe.py
#!/usr/bin/env python
# -*- cording: utf-8 -*-

import theano
from pylearn2.models import mlp
from pylearn2.training_algorithms import sgd
from pylearn2.termination_criteria import EpochCounter
from pylearn2.datasets.dense_design_matrix import DenseDesignMatrix
import numpy as np
import csv

class TicTacToe(DenseDesignMatrix):
    def __init__(self):
        X = []
        y = []
        X_temp = [0,0,0,0,0,0,0,0,0]  # 3x3 board
        y_temp = [0,0,0,0,0,0,0,0,0]  # 3x3 board

        # (1)
        self.class_names = ['0', '3']
        f = open("tic-tac-toe_records_lose.csv", "r")
        reader = csv.reader(f)

        # (2)
        for row in reader:
            for i, cell_index in enumerate(row):
                if cell_index == "win" or cell_index == "lose":
                    X_temp = [0,0,0,0,0,0,0,0,0]
                elif i % 2 == 0:
                    temp = []
                    X_temp[int(cell_index)] = 1
                    for x in X_temp:
                        temp.append(x)

                    #print "  temp = " + str(temp)
                    X.append(temp)
                else:
                    X_temp[int(cell_index)] = 2
                    y_temp[int(cell_index)] = 3
                    #print "y_temp = " + str(y_temp)
                    y.append(y_temp)
                    y_temp = [0,0,0,0,0,0,0,0,0]

        X = np.array(X)
        y = np.array(y)
        super(TicTacToe, self).__init__(X=X, y=y)

# (3)
data_set = TicTacToe()
h0 = mlp.Sigmoid(layer_name='h0', dim=9, irange=.1, init_bias=1.)
out = mlp.Softmax(layer_name='out', n_classes=9, irange=0.)
trainer = sgd.SGD(learning_rate=.05, batch_size=200, termination_criterion=EpochCounter(5000))
layers = [h0, out]

ann = mlp.MLP(layers, nvis=9)
trainer.setup(ann, data_set)

# (4)
while True:
    trainer.train(dataset=data_set)
    ann.monitor.report_epoch()
    ann.monitor()
    if trainer.continue_learning(ann) == False:
        break

# (5)-1
next_move = [0,0,0,0,0,0,0,0,0]
inputs = np.array([[0,0,1,0,0,0,0,0,0]])
output = ann.fprop(theano.shared(inputs, name='inputs')).eval()
print output[0]
for i in range(0,9):
    if max(output[0]) == output[0][i]:
        next_move[i] = 3

print next_move

# (5)-2
next_move = [0,0,0,0,0,0,0,0,0]
inputs = np.array([[1,0,2,1,0,0,0,1,2]])
output = ann.fprop(theano.shared(inputs, name='inputs')).eval()
print output[0]
for i in range(0,9):
    if max(output[0]) == output[0][i]:
        next_move[i] = 3

print next_move

少し解説していきます。コード上の(1), (2)...と対応させて読んでください。
前提として、〇を"1", ×を"2", 何も置いていないマスを"0"としています。
(教示データの作成に使用したrubyのコードとは異なります。分かりにくくてすいません。)

(1) 出力の値の設定
出力の値としてとる値。入力に0,1,2を使用しているので、出力には3を使用することにしました。
(2) 棋譜の変換
読み込んだ棋譜をニューラルネットの入力と出力に対応させます。
例えば、棋譜の入力が"6,8,3,4,2,0,lose"の場合、以下のような展開になります。

棋譜 : 6,8,3,4,2,0,lose
X[n]   = [0,0,0,0,0,0,1,0,0]  : "6"の位置に"〇"、つまり"1"を入力
y[n]   = [0,0,0,0,0,0,0,0,3]  : 次の手は"8"の位置に"×"なので、"8"の位置に"3"が出力
X[n+1] = [0,0,0,1,0,0,1,0,2]  : "8"の位置に"×"、つまり"2", 次は"3"の位置に"〇"、つまり"1"を入力
y[n+1] = [0,0,0,0,3,0,0,0,0]  : 次の手は"4"の位置に"×"なので、"4"の位置に"3"が出力
X[n+2] = [0,0,1,1,2,0,1,0,2]  : "4"の位置に"×"、つまり"2", 次は"2"の位置に"〇"、つまり"1"を入力
y[n+2] = [3,0,0,0,0,0,0,0,0]  : 次の手は"0"の位置に"×"なので、"0"の位置に"3"が出力

(3) MLP構造記述
隠れ層h0をシグモイド関数を活性化関数として、dim(次元)=9(マスの数)で生成します。
irange, init_biasは適当です。
出力層outをソフトマックス関数を活性化関数として、n_classes(クラス分け数)=9(マスの数)で生成します。
irangeは適当です。
確率的勾配降下法でトレーニングします。
learning_rate, batch_sizeは適当です。termination_criterionはいつトレーニングを終了するかを指定します。

ann = mlp.MLP(layers, nvis=9)

上記ではMLPの構造を指定しています。nvisは入力の次元=9(マスの数)です。
(4) トレーニング
トレーニングのループです。途中経過のモニターも出しています。
(5) テスト
ある入力(inputs)に対する出力がどうなるかテストします。
9つのクラスの確率(output[0])の最も大きい個所を次の手(next_move)とします。

プログラムを実行したときの出力は以下のようになります。

$ python tic_tac_toe.py
Parameter and initial learning rate summary:
    h0_W: 0.05
    h0_b: 0.05
    softmax_b: 0.05
    softmax_W: 0.05
Compiling sgd_update...
Compiling sgd_update done. Time elapsed: 0.203131 seconds
compiling begin_record_entry...
compiling begin_record_entry done. Time elapsed: 0.003911 seconds
Monitored channels: 
Compiling accum...
Compiling accum done. Time elapsed: 0.000039 seconds
Monitoring step:
    Epochs seen: 1
    Batches seen: 3
    Examples seen: 542
Monitoring step:
    Epochs seen: 2
    Batches seen: 6
    Examples seen: 1084

...

Monitoring step:
    Epochs seen: 5000
    Batches seen: 15000
    Examples seen: 2710000
Monitoring step:
    Epochs seen: 5001
    Batches seen: 15003
    Examples seen: 2710542
[ 0.07985083  0.10700001  0.00255253  0.15781951  0.08504663  0.16470689
  0.11459433  0.12293593  0.16549335]
[0, 0, 0, 0, 0, 0, 0, 0, 3]
[  2.56981722e-03   1.25923571e-01   2.05250923e-04   6.14268028e-04
   1.85819252e-02   2.43921569e-02   8.27328217e-01   3.84348076e-04
   4.45556802e-07]
[0, 0, 0, 0, 0, 0, 3, 0, 0]

以上から、
入力が [0,0,1,0,0,0,0,0,0] の場合の次の手は [0,0,0,0,0,0,0,0,3] です。つまり以下の図のようにAIは打ちます。
example01.png

入力が [1,0,2,1,0,0,0,1,2] の場合の次の手は [0,0,0,0,0,0,3,0,0] です。つまり以下の図のようにAIは打ちます。
example02.png

うーん、"5"の位置に"X"を打てば勝利して終了なのですが、"O"のリーチを止めに行ってますね。まぁ、ランダムな棋譜を元にしている割には、まぁまぁでしょうか。

毎度ソースコードをいじらないといけないので、ちょっと面倒ですが、任意の入力に対する次の一手を得られました。

今後は一度のトレーニング結果を元に、任意の入力に対する次の一手を簡単に得られるようにして、三目並べのゲームに実装してみようと思います。まずは三目並べのゲームプログラムを作らねば。
つくりました。以下の記事をご覧ください。
Pylearn2で三目並べのAIをつくってみる - モデルの保存と読み込み -

参考

http://www.arngarden.com/2013/07/29/neural-network-example-using-pylearn2/
http://sinhrks.hatenablog.com/entry/2014/11/30/085119
https://www.safaribooksonline.com/blog/2014/02/10/pylearn2-regression-3rd-party-data/

20
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
18