勾配降下党青年局

万国のグラーディエントよ、降下せよ!

半年前に自分で作った機械学習によるオセロAIの復習をする1

 半年前に下記の本を見て、将棋AIについて学んだものの、将棋だとあまりにも実装が難しく、学習時間もかかるのでオセロAIを作ってました。その後画像生成AIとかに興味が移ってしまったのですが、せっかくこんなのを立ち上げたので、忘れないうちに復習しておく。
book.mynavi.jp
人より強い・・・w

第一回はオセロそのものの実装編です。オセロの実装はビットボードというものを使った効率的な方法がありますが、私の目的はそこではないのと、AIに入力するときには結局配列になるので、単純な実装にしています。オセロの実装部分だけC++でやってます。そこまで最適化できてないだろうけどこれでもPythonよりましです。

実装にあたって特に参考にしたものはないのですが、むかーしこの本を読んだので、その記憶をもとにしている可能性が高いです。
https://www.yodobashi.com/product/100000009000200371/

ボードは100次元配列(board)を使って、左上から右下へ並べることで、10×10の表現します。オセロのボードは8×8ですが、端っこに壁があるとすごい分かりやすいです。オセロの左からaマス、上からbマスが十進数abで表現できます。たとえば左から4マス、上から5マス(オセロ的にはd5)はboard(45)と書けます。初期状態ではboard(45)=WHITEですね。

W W W W W W W W W W
W W
W W
W W
W W
W W
W W
W W
W W W W W W W W W W

最初の状態、Wは壁。

使ったライブラリ。pythonのnumpyとの連携のためにpybindやEigenを使ってます。通常の配列ではなくEigenのベクトルをつかうので、[]じゃなくて()を使ったりしています。

#include <string>
#include <iostream>
#include <vector>
#include <stdlib.h>
#include <time.h>

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

#include <Eigen/Dense>
using Eigen::MatrixXd;
using Eigen::VectorXi;
#include <pybind11/eigen.h>

using namespace std;

定数

const int BLACK = 1;
const int BLANK = 0;
const int WHITE = -1;
const int WALL = 2;

const int BOARD_SIZE = 10;

const int RIGHT = 1;
const int LEFT = -1;
const int UP = -BOARD_SIZE;
const int DOWN = BOARD_SIZE;

const int RIGHT_UP = RIGHT+UP;
const int RIGHT_DOWN = RIGHT + DOWN;
const int LEFT_UP = LEFT+UP;
const int LEFT_DOWN = LEFT+DOWN;

const int DIRECTIONS[8] = {RIGHT,LEFT,UP,DOWN,RIGHT_UP,RIGHT_DOWN,LEFT_UP,LEFT_DOWN};

右に移動するには+1、左に移動するには-1、上に移動するには-10、下に移動するには+10といった感じですね。こうすれば右上は右+上で表せられます。

メンバ変数は3つだけです。ボード(100次元配列)と白黒どっちのターンかと、合法手一覧です。

    public:
        VectorXi board = VectorXi::Zero(100);
        int turn;
        std::vector<int> legal_moves;

コンストラクタです。初期配置とターンの設定と合法手の計算(後述)をしています。オセロの先手は黒なんですね。私はしばらくチェスと同じ白だと勘違いしてました。

        Board(){
            turn = BLACK;
            for(int i=0;i<100;i++){
                if(i%10==0 || i%10 == 9 || i/10==0 || i/10 == 9){
                    board(i) = WALL;
                }
                board(44) = WHITE;
                board(55) = WHITE;
                board(45) = BLACK;
                board(54) = BLACK;
            }
            update_legal_moves();
        }

1手指す動きを計算するメソッドです。入力は指す位置のみです。メンバ変数turnがあるので白黒どっちが指したかは分かります。個人的につくったので、p,q,zとか分かりにくい変数ばっかりですね。よくないですねこういうの。

        int move(int z){
            int p,q;
            
            //空白でないならエラーをだす
            if(board(z) != BLANK){
                throw std::logic_error("エラー");
            }

            //上下左右斜めそれぞれに対して、石ひっくり返す動きをする。
            for(int i=0;i<8;i++){
                p = z + DIRECTIONS[i];
                q = 1;

                //隣が敵の石だったら、隣に移動するのという動作繰り返す。
                while(board(p) == -turn){
                    p += DIRECTIONS[i];
                    q += 1;
                }

                //自分の石を見つけたら、
                if(board(p) == turn){
                    //そこまでにあった石を全部自分の石にする。
                    //j=0からスタートなので、指した場所そのものも自分の石になる。同じこと8回やってるけどまあいいや。
                    for(int j=0;j<q;j++){
                        board(z+DIRECTIONS[i]*j) = turn;
                    }
                }
                //コードはないですが、自分の石に会う前に、空白や壁にたどり着いた場合、何も起こりません。
            }
            turn = -turn;
            update_legal_moves();
            if(legal_moves.size()==0){
                turn = -turn; //敵に合法手がない場合、再度自分のターンになる。
                update_legal_moves();
                if(legal_moves.size()==0){
                    return 1; //両方なかったらゲーム終了。
                }
            }
            return 0;
        }

合法手計算です。全てのマスを確認します。

        void update_legal_moves(){

            std::vector<int> moves;
            int p,py,d;

            //すごい単純な全探索アルゴリズムです
            for(int y=1;y<9;y++){
                py = y * 10;
                for(int x=1;x<9;x++){

                    //空白なら非合法手
                    if(board(py+x)!=BLANK){
                        continue;
                    }
                    //全ての方角で
                    for(int i=0;i<8;i++){
                        d = DIRECTIONS[i];
                        p = py + x + d;
                        //隣が敵の石じゃなかったら即アウト!
                        if(board(py+x+d) != -turn){
                            continue;
                        }
                        //隣が敵の石じゃなくなるまで一直線に探索。
                        while(board(p) == -turn){
                            p += d;
                        }
                        //自分の石にたどり着いたら合法、空白や壁だったら違法
                        if(board(p) == turn){
                            moves.push_back(py+x);
                            break;
                        }
                    }
                }
            }
            legal_moves = moves;
        }

これでmove関数を繰り返すことで、ゲームにはなります。
まあこれだけじゃまともにオセロを遊ぶことはできませんが、そこには興味がないので。

その他雑多なもの。Python機械学習するために必要。

        //8×8ボードでの計算用
        int output_move(int move){
            return move%10 + move/10*8 -9;
        }

        //numpy用、機械学習のためにいらない壁を壊す
        vector<MatrixXd> feature(){
            MatrixXd feature1 = MatrixXd::Zero(8,8);
            MatrixXd feature2 = MatrixXd::Zero(8,8);
            vector<MatrixXd> features;

            features.push_back(feature1);
            features.push_back(feature2);

            int t = turn==1 ? 0:1;
            for(int y=0;y<8;y++){
                for(int x=0;x<8;x++){
                    if(board(y*10+x+11)==1)features[t](y,x)=1;
                    if(board(y*10+x+11)==-1)features[1-t](y,x)=1;
                }
            }
            return features;
        }

        //python用
        Board copy(){
            Board board;
            board = *this;
            return board;
        }

        //確認用
        void display(){
            std::string stone[4] = {"○","□","●","■"};
            for(int i=0;i<100;i++){
                if(i%10==0)std::cout << "\n";
                std::cout << stone[board(i)+1];
            }       
        }

};


namespace py = pybind11;
PYBIND11_MODULE(cboard,m) {
    py::class_<Board>(m, "CBoard")
        .def(py::init<>())
        .def("move", &Board::move)
        .def("feature", &Board::feature)
        .def("output_move", &Board::output_move)
        .def("copy", &Board::copy)
        .def_readwrite("legal_moves", &Board::legal_moves)
        .def_readwrite("board", &Board::board)
        .def_readwrite("turn", &Board::turn);
}

最初はPythonで実装していましたが、C++にしたら2倍くらいはやくなりました。
つづく。