将棋AIで学ぶディープラーニング on Mac and Google Colab

AI

[方策ネットワーク]
第7章1〜4

policy.py

フィルター数194個の意味

入力層で考えると入力は9×9の局面図(画像)が104個(=104ch)。
フィルター数は194。
1つ目のフィルターを使い1つの画像に対してフィルターをかけた9×9の1つの画像を得る。
それを104ch数分行い、104個の画像を得る。
104個の画像を1個の画像にまとめる。これが1chとなる。
2つ目のフィルターを使い同様のことを行う。
194個目のフィルターまで同様のことを行う。
これで194個の画像を得る。つまり出力ch数が194chになる。

1×1フィルターの意味

pointwise convolution
次元数の削減が目的
「1画素x1画素x層数」の細長いフィルターのイメージ
https://www.robotech-note.com/entry/2017/12/24/191936
入力:1~192chそれぞれのchのとある1画素、出力:1つの画素値
これを全画素について実施して1画面の画像を得る。
それをパラメータを変えて出力ch回数分実施する。
出力ch数分の出力画像を得る。
結局上の194個のフィルターのフィルターサイズが1×1になっているだけでやってることは同じ。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from chainer import Chain
import chainer.functions as F
import chainer.links as L

from pydlshogi.common import *

ch = 192
class PolicyNetwork(Chain):
# 入力104ch
# 入力は9x9の局面図が104個。
# フィルター数194。
# 入力chそれぞれに対してフィルターをかけた9x9の値を入力ch分加算して1chとして出力、
# それをフィルター数194個行うので出力ch数が194chになる。
    def __init__(self):
        super(PolicyNetwork, self).__init__()
        with self.init_scope():
            self.l1 = L.Convolution2D(in_channels = 104, out_channels = ch, ksize = 3, pad = 1)
            self.l2 = L.Convolution2D(in_channels = ch, out_channels = ch, ksize = 3, pad = 1)
            self.l3 = L.Convolution2D(in_channels = ch, out_channels = ch, ksize = 3, pad = 1)
            self.l4 = L.Convolution2D(in_channels = ch, out_channels = ch, ksize = 3, pad = 1)
            self.l5 = L.Convolution2D(in_channels = ch, out_channels = ch, ksize = 3, pad = 1)
            self.l6 = L.Convolution2D(in_channels = ch, out_channels = ch, ksize = 3, pad = 1)
            self.l7 = L.Convolution2D(in_channels = ch, out_channels = ch, ksize = 3, pad = 1)
            self.l8 = L.Convolution2D(in_channels = ch, out_channels = ch, ksize = 3, pad = 1)
            self.l9 = L.Convolution2D(in_channels = ch, out_channels = ch, ksize = 3, pad = 1)
            self.l10 = L.Convolution2D(in_channels = ch, out_channels = ch, ksize = 3, pad = 1)
            self.l11 = L.Convolution2D(in_channels = ch, out_channels = ch, ksize = 3, pad = 1)
            self.l12 = L.Convolution2D(in_channels = ch, out_channels = ch, ksize = 3, pad = 1)
            self.l13 = L.Convolution2D(in_channels = ch, out_channels = MOVE_DIRECTION_LABEL_NUM,
                                        ksize = 1, nobias = True)
            # フィルターサイズ1x1(ksize=1)の意味
            #  入力:1~192chそれぞれのchのとある1画素、出力:1つの画素値
            #  これを全画素について実施して1画面の画像を得る。
            #  それをパラメータを変えて出力ch回数分実施する。
            #  出力ch数分の出力画像を得る。
            self.l13_bias = L.Bias(shape=(9*9*MOVE_DIRECTION_LABEL_NUM)) 
            # MOVE_DIRECTION_LABEL_NUM=27。移動方向20個と持ち駒7個を示す。

    def __call__(self, x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(h1))
        h3 = F.relu(self.l3(h2))
        h4 = F.relu(self.l4(h3))
        h5 = F.relu(self.l5(h4))
        h6 = F.relu(self.l6(h5))
        h7 = F.relu(self.l7(h6))
        h8 = F.relu(self.l8(h7))
        h9 = F.relu(self.l9(h8))
        h10 = F.relu(self.l10(h9))
        h11 = F.relu(self.l11(h10))
        h12 = F.relu(self.l12(h11))
        h13 = self.l13(h12)
        return self.l13_bias(F.reshape(h13,(-1, 9*9*MOVE_DIRECTION_LABEL_NUM)))
        # 出力にソフトマックス関数を記述していない理由は、
        # 学習するときにF.softmax_cross_entropy関数を使うため。
        # この関数はソフトマックス関数と交差エントロピー誤差の計算を同時に行う。

common.py

bb_rotate180()

引数:81桁の2進数。(piece_bbやoccupiedの1要素、つまりビットボードが入る)
出力:81桁の桁を反転して出力する。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import shogi

# 移動の定数
# UP=0、UP_LEFT=1、、、、UP_RIGHT_PROMOTE=19と定義している。
# 使うときは、例えば〇〇 = UPとすると〇〇という変数に0が代入される。
MOVE_DIRECTION = [
    UP, UP_LEFT, UP_RIGHT, LEFT, RIGHT, DOWN, DOWN_LEFT, DOWN_RIGHT,
    UP2_LEFT, UP2_RIGHT,
    UP_PROMOTE, UP_LEFT_PROMOTE, UP_RIGHT_PROMOTE, LEFT_PROMOTE, RIGHT_PROMOTE,
    DOWN_PROMOTE, DOWN_LEFT_PROMOTE, DOWN_RIGHT_PROMOTE,
    UP2_LEFT_PROMOTE, UP2_RIGHT_PROMOTE
] = range(20)

# 成り変換テーブル
# **_PROMOTEという名前の変数はMOVE_DIRECTIONで定義済み。
# UP_PROMOTE=10,・・・,UP_RIGHT_PROMOTE=19と定義されている。
MOVE_DIRECTION_PROMOTED = [
    UP_PROMOTE, UP_LEFT_PROMOTE, UP_RIGHT_PROMOTE, LEFT_PROMOTE, RIGHT_PROMOTE,
    DOWN_PROMOTE, DOWN_LEFT_PROMOTE, DOWN_RIGHT_PROMOTE,
    UP2_LEFT_PROMOTE, UP2_RIGHT_PROMOTE
]

# 指し手を表すラベルの数
MOVE_DIRECTION_LABEL_NUM = len(MOVE_DIRECTION) + 7 # 7は持ち駒の種類

# rotate 180degree
# shogi.I1は80、・・・、shogi.A9は0。つまりSQUARES_R180 = [80, 79, ・・・, 1, 0]
SQUARES_R180 = [
    shogi.I1, shogi.I2, shogi.I3, shogi.I4, shogi.I5, shogi.I6, shogi.I7, shogi.I8, shogi.I9,
    shogi.H1, shogi.H2, shogi.H3, shogi.H4, shogi.H5, shogi.H6, shogi.H7, shogi.H8, shogi.H9,
    shogi.G1, shogi.G2, shogi.G3, shogi.G4, shogi.G5, shogi.G6, shogi.G7, shogi.G8, shogi.G9,
    shogi.F1, shogi.F2, shogi.F3, shogi.F4, shogi.F5, shogi.F6, shogi.F7, shogi.F8, shogi.F9,
    shogi.E1, shogi.E2, shogi.E3, shogi.E4, shogi.E5, shogi.E6, shogi.E7, shogi.E8, shogi.E9,
    shogi.D1, shogi.D2, shogi.D3, shogi.D4, shogi.D5, shogi.D6, shogi.D7, shogi.D8, shogi.D9,
    shogi.C1, shogi.C2, shogi.C3, shogi.C4, shogi.C5, shogi.C6, shogi.C7, shogi.C8, shogi.C9,
    shogi.B1, shogi.B2, shogi.B3, shogi.B4, shogi.B5, shogi.B6, shogi.B7, shogi.B8, shogi.B9,
    shogi.A1, shogi.A2, shogi.A3, shogi.A4, shogi.A5, shogi.A6, shogi.A7, shogi.A8, shogi.A9,
]

def bb_rotate_180(bb):
# bbに入るのはpiece_bbの1要素やoccupiedの1要素。つまり81桁の2進数。
    bb_r180 = 0
    for pos in shogi.SQUARES: #SQUARESはrange(0, 81)のこと
        if bb & shogi.BB_SQUARES[pos] > 0:
        # BB_SQUARESは[0b000・・・0001, 0b000・・・0010, 0b000・・・0100, ・・・, 0b100・・・0000]。要素81個。
        # &はビット演算子のAND。
            bb_r180 += 1 << SQUARES_R180[pos]
            # a<<bはaのビットを左側にb桁シフトする演算子。
            # なので1をSQUARES_R180[pos]個シフトするという意味。
            # 例えばposが0ならSQUARES_R180[pos]は80なので
            # 0b000000000000000000000000000000000000000000000000000000000000000000000000000000001
            # を80個シフトして
            # 0b100000000000000000000000000000000000000000000000000000000000000000000000000000000
            # になる。

    return bb_r180

features.py

make_input_features()

引数:piece_bb、occupied、pieces_in_hand
出力:先手の駒が有る場所、先手の持ち駒、後手の駒が有る場所、後手の持ち駒、で全部で9×9の行列が104個
[(9×9の行列), ・・・が14個(盤上の駒の種類),
(9×9の行列), ・・・が(18+4+4+4+4+2+2)個,
(9×9の行列), ・・・が14個(盤上の駒の種類),
(9×9の行列), ・・・が(18+4+4+4+4+2+2)個]

make_input_features_from_board()

boardを引数としてpiece_bb, occupied, pieces_in_hand, turnを取り出してmake_input_featuresを行う。

make_output_label()

引数:move(move_fromとmove_to), color(先手か後手)
colorはmove_directionを先手でも後手でも同じものを使用できるようにするために使う。

出力:move_direction(81の桁) + move_to(1の桁)
81進数のイメージ。move_directionが81の桁、move_toが1の桁。
81の桁・・・27個
1の桁・・・9×9個

つまりmoveを一つの数値に変換して出力するということ。
ちなみにこの出力の数値はNNの出力のインデックスにもなっている。NNの出力から所望の要素を取り出したい時に使う。

make_features()

引数:position
positionとはread_kifu()の出力。(「局面図」「指し手」「勝敗」)を局面ごとに算出し一つのリストとして出力したもの。
1局面のデータは下記5要素で構成。
-piece_bb:15要素
-occupied:2要素
-pieces_in_hand:2要素
-move_label:1要素
-win:1要素
最終的な出力は[([15要素], [2要素], [2要素], [1要素], [1要素]), (同じセット), ・・・が手数 x 対局数]

出力 : features, move, winの3つを出力する。
features : positionからpiece_bb, occupied, pieces_in_handを取り出してmake_input_featuresを行ったもの。
先手の駒が有る場所、先手の持ち駒、後手の駒が有る場所、後手の持ち駒、で全部で9×9の行列が104個
[(9×9の行列), ・・・が14個(盤上の駒の種類),
(9×9の行列), ・・・が(18+4+4+4+4+2+2)個,
(9×9の行列), ・・・が14個(盤上の駒の種類),
(9×9の行列), ・・・が(18+4+4+4+4+2+2)個]
move : positionのmove_labelをそのまま出力する
win : positionのwinをそのまま出力する

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import numpy as np
import shogi
import copy

from pydlshogi.common import *

def make_input_features(piece_bb, occupied, pieces_in_hand):
    features = []
    for color in shogi.COLORS:
        # board pieces
        for piece_type in shogi.PIECE_TYPES_WITH_NONE[1:]: #PIECE_TYPES_WITH_NONEはrange(0, 15)のこと
            bb = piece_bb[piece_type] & occupied[color] #手番の側の駒の位置を駒ごとに取得(&はビット演算子)
            feature = np.zeros(9*9)
            for pos in shogi.SQUARES: #SQUARESはrange(0, 81)のこと
                if bb & shogi.BB_SQUARES[pos] > 0: #BB_SQUARESは[0b1, 0b10, 0b100, ・・・, 0b1・・・0]。要素81個。
                    feature[pos] = 1                #bit boardの各bitを要素に分解。例えば1010・・・だったら[1,0,1,0,・・・]に分解。
            features.append(feature.reshape((9, 9))) #bit boardの各bitを要素に分解したものを9x9の行列にして返す。

        # pieces in hand
        for piece_type in range(1, 8):
            for n in range(shogi.MAX_PIECES_IN_HAND[piece_type]):
            #shogi.MAX_PIECES_IN_HANDは持ち駒の数。インデックス1~7はおそらく下記のような感じ。
            #shogi.MAX_PIECES_IN_HAND[1] = 18 :歩
            #shogi.MAX_PIECES_IN_HAND[2] = 4 :香車
            #shogi.MAX_PIECES_IN_HAND[3] = 4 :桂馬
            #shogi.MAX_PIECES_IN_HAND[4] = 4 :銀
            #shogi.MAX_PIECES_IN_HAND[5] = 4 :金
            #shogi.MAX_PIECES_IN_HAND[6] = 2 :角
            #shogi.MAX_PIECES_IN_HAND[7] = 2 :飛車
                if piece_type in pieces_in_hand[color] and n < pieces_in_hand[color][piece_type]:
                    feature = np.ones(9*9)
                else:
                    feature = np.zeros(9*9)
                features.append(feature.reshape((9, 9)))

    return features
    # 出力:先手の駒が有る場所、先手の持ち駒、後手の駒が有る場所、後手の持ち駒、で全部で9x9の行列が104個
    # [(9x9の行列), ・・・が14個(盤上の駒の種類),
    # (9x9の行列), ・・・が(18+4+4+4+4+2+2)個, 
    # (9x9の行列), ・・・が14個(盤上の駒の種類),
    # (9x9の行列), ・・・が(18+4+4+4+4+2+2)個]

def make_input_features_from_board(board): # boardを引数としてmake_input_featuresを行う。
    if board.turn == shogi.BLACK:
        piece_bb = board.piece_bb
        occupied = (board.occupied[shogi.BLACK], board.occupied[shogi.WHITE])
        pieces_in_hand = (board.pieces_in_hand[shogi.BLACK], board.pieces_in_hand[shogi.WHITE])
    else:
        piece_bb = [bb_rotate_180(bb) for bb in board.piece_bb]
        occupied = (bb_rotate_180(board.occupied[shogi.WHITE]), bb_rotate_180(board.occupied[shogi.BLACK]))
        pieces_in_hand = (board.pieces_in_hand[shogi.WHITE], board.pieces_in_hand[shogi.BLACK])

    return make_input_features(piece_bb, occupied, pieces_in_hand)


def make_output_label(move, color):
    move_to = move.to_square
    move_from = move.from_square
                # ■ Moveクラス
                #   from_square変数 :盤面を0~80の数値で表したときの移動元の値。
                #                    9で割ったときの商がy座標、余りがx座標となる。xy座標は0オリジン。
                #   to_square変数   :同上(移動先)。
                #
                #   x座標
                #   0   1   2   3   4   5   6   7   8
                #
                #   0   1   2   3   4   5   6   7   8       0 y座標
                #   9   10  11  12  13  14  15  16  17      1
                #   18  19  20  21  22  23  24  25  26      2
                #   27  28  29  30  31  32  33  34  35      3
                #   36  37  38  39  40  41  42  43  44      4
                #   45  46  47  48  49  50  51  52  53      5
                #   54  55  56  57  58  59  60  61  62      6
                #   63  64  65  66  67  68  69  70  71      7
                #   72  73  74  75  76  77  78  79  80      8

    # 白の場合盤を回転
    if color == shogi.WHITE:
        move_to = SQUARES_R180[move_to]
        if move_from is not None: # 持ち駒ではなく盤上の駒を動かした場合
            move_from = SQUARES_R180[move_from]

    # move direction
    if move_from is not None: # 持ち駒ではなく盤上の駒を動かした場合
        to_y, to_x = divmod(move_to, 9)
        from_y, from_x = divmod(move_from, 9)
        dir_x = to_x - from_x
        dir_y = to_y - from_y
        if dir_y < 0 and dir_x == 0:
            move_direction = UP
        elif dir_y == -2 and dir_x == -1:
            move_direction = UP2_LEFT
        elif dir_y == -2 and dir_x == 1:
            move_direction = UP2_RIGHT
        elif dir_y < 0 and dir_x < 0:
            move_direction = UP_LEFT
        elif dir_y < 0 and dir_x > 0:
            move_direction = UP_RIGHT
        elif dir_y == 0 and dir_x < 0:
            move_direction = LEFT
        elif dir_y == 0 and dir_x > 0:
            move_direction = RIGHT
        elif dir_y > 0 and dir_x == 0:
            move_direction = DOWN
        elif dir_y > 0 and dir_x < 0:
            move_direction = DOWN_LEFT
        elif dir_y > 0 and dir_x > 0:
            move_direction = DOWN_RIGHT

        # promote
        if move.promotion:
            move_direction = MOVE_DIRECTION_PROMOTED[move_direction]
    else:
        # 持ち駒
        # len(MOVE_DIRECTION) は20
        # move.drop_piece_typeは置いた持ち駒の種類?
        # -1は数合わせ?
        move_direction = len(MOVE_DIRECTION) + move.drop_piece_type - 1


    move_label = 9 * 9 * move_direction + move_to
    # 81進数のイメージ。move_directionが81の桁、move_toが1の桁。
    # 81の桁・・・27個
    # 1の桁・・・9x9個

    return move_label

def make_features(position):
    piece_bb, occupied, pieces_in_hand, move, win = position
    features = make_input_features(piece_bb, occupied, pieces_in_hand)

    return(features, move, win)

コメント

タイトルとURLをコピーしました