TadaoYamaokaの日記

山岡忠夫Homeで公開しているプログラムの開発ネタを中心に書いていきます。

Google ColabでAlphaZero Shogiのモデルを教師あり学習する

Google ColabでAlphaZero Shogiのモデルを論文に通り定義して、テストのために教師ありで学習してみました。
TPUでも学習して学習時間の比較もしてみました。

教師データには、elmoで生成したhcpe形式のデータを使用し、入力特徴量と正解ラベルの加工には、先日作成したPythonの将棋ライブラリ(cshogi)を使用しました。

モデルの定義

AlphaZeroの論文の通り、ResNetで、policyとvalueの2つの出力を持つネットワークを定義します。
ブロック数とフィルタ数、全結合層のユニット数はパラメータにしています。
policyの出力の畳み込みの後の位置ごとのバイアスはカスタムレイヤーを定義しています。

import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Layer, Input, Dense, Conv2D, BatchNormalization, Activation, Flatten, Add

class Bias(Layer):
    def __init__(self, **kwargs):
        super(Bias, self).__init__(**kwargs)

    def build(self, input_shape):
        self.b = self.add_weight(name='b',
                                 shape=(input_shape[1:]),
                                 initializer='zeros',
                                 trainable=True)
        super(Bias, self).build(input_shape)

    def call(self, x):
        return x + self.b

def conv_layer(inputs,
               filters,
               activation='relu',
               use_bias=True):

    x =  Conv2D(filters,
                kernel_size=3,
                strides=1,
                padding='same',
                data_format='channels_first',
                kernel_initializer='he_normal',
                use_bias=use_bias)(inputs)
    x = BatchNormalization(axis=1)(x)
    if activation is not None:
        x = Activation(activation)(x)
    return x

def ResNet(input_planes=45,
           res_blocks=20,
           filters=256,
           fcl_units=256,
           policy_planes=139):

    inputs = Input(shape=(input_planes, 9, 9))
    x = conv_layer(inputs, filters=filters, use_bias=False)

    for res_block in range(res_blocks):
        # bottleneck residual unit
        y = conv_layer(x, filters=filters, use_bias=False)
        y = conv_layer(y, filters=filters, use_bias=False, activation=None)
        x = Add()([x, y])
        x = Activation('relu')(x)

    # Add policy output
    policy_x = Conv2D(policy_planes,
                      kernel_size=1,
                      strides=1,
                      padding='same',
                      data_format='channels_first',
                      kernel_initializer='he_normal',
                      use_bias=False)(x)
    policy_x = Flatten()(policy_x)
    policy_y = Bias(name='policy')(policy_x)

    # Add value output
    value_x = conv_layer(x, filters=1)
    value_x = Flatten()(value_x)
    value_x = Dense(fcl_units,
                    activation='relu',
                    kernel_initializer='he_normal')(value_x)
    value_y = Dense(1,
                    activation='tanh',
                    kernel_initializer='he_normal',
                    name='value')(value_x)

    # Instantiate model
    return Model(inputs=inputs, outputs=[policy_y, value_y])

教師データ

教師データには、elmoを使用して、hcpe形式で生成したデータを使用します。
hcpe形式のデータには、

hcp ハフマン符号で圧縮した局面
bestMove16 指し手
gameResult 勝敗結果

が含まれます。

訓練データの局面数は、100万局面です。テストデータは10万局面です。

入力特徴量と正解データ

AlphaZeroの論文の通り、局面を入力特徴量に変換します。
ただし、hcpeには、局面の繰り返し数と手数が含まれないため、値を0にしています。
また、局面の繰り返し数は、単に繰り返しがあるかどうかだけ判定し、特徴を1面だけとしました。(今回は常に0)

持ち駒の数(prisoner count)は0から1の範囲の正規化を行わず、そのまま整数の値を設定しています。

policyの正解ラベルは、sparse_categorical_crossentropyを使用するので、one-hotベクトル化を行わず、整数で表しています。
valueの正解となる勝敗は、出力の活性関数にtanhを使うので、-1(負け),0(引き分け),1(勝ち)で表します。

履歴局面はなしです。

cshogiを使うと、以下のように記述できます。

board = Board()
def mini_batch(hcpes):
    features = np.zeros((len(hcpes), 45, 81), dtype=np.float32)
    action_labels = np.empty(len(hcpes), dtype=np.int)
    game_outcomes = np.empty(len(hcpes), dtype=np.float32)

    for i, hcpe in enumerate(hcpes):
        # Input features
        #   P1 piece 14
        #   P2 piece 14
        #   Repetitions 1
        #   P1 prisoner count 7
        #   P2 prisoner count 7
        #   Colour 1
        #   Total move count 1
        board.set_hcp(hcpe['hcp'])
        # piece
        pieces = board.pieces
        for sq in SQUARES:
            piece = pieces[sq]
            if piece != NONE:
                if piece >= WPAWN:
                    piece = piece - 2
                features[i][piece - 1][sq] = 1
        # repetition
        if board.is_draw() == REPETITION_DRAW:
            features[i][28].fill(1)
        # prisoner count
        pieces_in_hand = board.pieces_in_hand
        for c, hands in enumerate(pieces_in_hand):
            for hp, num in enumerate(hands):
                features[i][29 + c * 7 + hp].fill(num)
        # Colour
        if board.turn == WHITE:
            features[i][43].fill(1)
        # Total move count
        # not implement for learning from hcpe

        # Action representation
        #   Queen moves 64
        #   Knight moves 2
        #   Promoting queen moves 64
        #   Promoting knight moves 2
        #   Drop 7
        move = hcpe['bestMove16']
        if not move_is_drop(move):
            from_sq = move_from(move)
            to_sq = move_to(move)
            from_file, from_rank = divmod(from_sq, 9)
            to_file, to_rank = divmod(to_sq, 9)
            diff_file = to_file - from_file
            diff_rank = to_rank - from_rank
            if abs(diff_file) != 1 or abs(diff_rank) != 2:
                # Queen moves
                if diff_file < 0:
                    if diff_rank < 0:
                        move_dd = -diff_file - 1
                    elif diff_rank > 0:
                        move_dd = 8 - diff_file - 1
                    else:
                        move_dd = 16 - diff_file - 1
                elif diff_file > 0:
                    if diff_rank < 0:
                        move_dd = 24 + diff_file - 1
                    elif diff_rank > 0:
                        move_dd = 32 + diff_file - 1
                    else:
                        move_dd = 40 + diff_file - 1
                else:
                    if diff_rank < 0:
                        move_dd = 48 - diff_rank - 1
                    else:
                        move_dd = 56 + diff_rank - 1
            else:
                # Knight moves
                if diff_file < 0:
                    move_dd = 64
                else:
                    move_dd = 65

            promotion = 1 if move_is_promotion(move) else 0

            action_labels[i] = (promotion * 66 + move_dd) * 81 + from_sq
        else:
            # drop
            to_sq = move_to(move)
            hp = move_drop_hand_piece(move)
            action_labels[i] = (132 + hp) * 81 + to_sq

        # game outcome
        #   z: −1 for a loss, 0 for a draw, and +1 for a win
        gameResult = hcpe['gameResult']
        if board.turn == BLACK:
            if gameResult == BLACK_WIN:
                game_outcomes[i] = 1
            if gameResult == WHITE_WIN:
                game_outcomes[i] = -1
            else:
                game_outcomes[i] = 0
        else:
            if gameResult == BLACK_WIN:
                game_outcomes[i] = -1
            if gameResult == WHITE_WIN:
                game_outcomes[i] = 1
            else:
                game_outcomes[i] = 0

    return (features.reshape((len(hcpes), 45, 9, 9)), { 'policy': action_labels, 'value': game_outcomes })

Googleドライブのマウント

学習データは、Googleドライブにアップロードして、Google Colabからマウントしてアクセスします。

from google.colab import drive
drive.mount('/content/drive')

Googleドライブのファイルは、「drive/My Drive」からアクセスできます。

学習

hcpeデータはnumpyのfromfileでデータ形式を指定して読み込みます。
hcpeのデータ形式HuffmanCodedPosAndEvalはcshogiで定義しています。
入力特徴量はデータ量が大きいため、fit_generatorでミニバッチごとに入力特徴量と正解データを作成しながら学習します。

モデルのサイズは、10ブロック、192フィルタとしています。

import tensorflow as tf
from tensorflow.keras.optimizers import SGD
import numpy as np
import os
from cshogi import *

train_hcpe_path = 'drive/My Drive/hcpe/elmo_teacher_depth8_uniq-001-01'
test_hcpe_path = 'drive/My Drive/hcpe/elmo_teacher_depth8_uniq-test-01'
batchsize = 256
epochs = 1
weight_decay = 1e-4
use_tpu = True

model = ResNet(res_blocks=10, filters=192)

train_hcpes = np.fromfile(train_hcpe_path, dtype=HuffmanCodedPosAndEval)
test_hcpes = np.fromfile(test_hcpe_path, dtype=HuffmanCodedPosAndEval)

def datagen(hcpes, batchsize):
    while True:
        np.random.shuffle(hcpes)
        for i in range(0, len(hcpes) - batchsize, batchsize):
            yield mini_batch(hcpes[i:i+batchsize])

def categorical_crossentropy(y_true, y_pred):
    return tf.keras.backend.sparse_categorical_crossentropy(y_true, y_pred, from_logits=True)

def categorical_accuracy(y_true, y_pred):
    return tf.keras.metrics.sparse_categorical_accuracy(y_true, tf.nn.softmax(y_pred))

def binary_accuracy(y_true, y_pred):
    return tf.keras.metrics.binary_accuracy(tf.keras.backend.round((y_true + 1) / 2), y_pred, threshold=0)

# add weight decay
for layer in model.layers:
    if isinstance(layer, tf.keras.layers.Conv2D) or isinstance(layer, tf.keras.layers.Dense):
        layer.add_loss(tf.keras.regularizers.l2(weight_decay)(layer.kernel))

model.compile(optimizer=tf.train.MomentumOptimizer(learning_rate=0.01, momentum=0.9),
              loss={'policy': categorical_crossentropy, 'value': 'mse'},
              metrics={'policy': categorical_accuracy, 'value': binary_accuracy})

# TPU
if use_tpu:
    model = tf.contrib.tpu.keras_to_tpu_model(
        model,
        strategy=tf.contrib.tpu.TPUDistributionStrategy(
            tf.contrib.cluster_resolver.TPUClusterResolver(tpu='grpc://' + os.environ['COLAB_TPU_ADDR'])
        )
    )

model.fit_generator(datagen(train_hcpes, batchsize), len(train_hcpes) // batchsize,
                    epochs=epochs,
                    validation_data=datagen(test_hcpes, batchsize), validation_steps=len(test_hcpes) // batchsize)

学習結果

TPUでの学習結果
INFO:tensorflow:Querying Tensorflow master (grpc://10.26.203.146:8470) for TPU system metadata.
INFO:tensorflow:Found TPU system:
INFO:tensorflow:*** Num TPU Cores: 8
INFO:tensorflow:*** Num TPU Workers: 1
INFO:tensorflow:*** Num TPU Cores Per Worker: 8
INFO:tensorflow:*** Available Device: _DeviceAttributes(/job:worker/replica:0/task:0/device:CPU:0, CPU, -1, 2190986923747320621)
INFO:tensorflow:*** Available Device: _DeviceAttributes(/job:worker/replica:0/task:0/device:XLA_CPU:0, XLA_CPU, 17179869184, 12416730924266005929)
INFO:tensorflow:*** Available Device: _DeviceAttributes(/job:worker/replica:0/task:0/device:TPU:0, TPU, 17179869184, 13813975489927709994)
INFO:tensorflow:*** Available Device: _DeviceAttributes(/job:worker/replica:0/task:0/device:TPU:1, TPU, 17179869184, 8536274068396247395)
INFO:tensorflow:*** Available Device: _DeviceAttributes(/job:worker/replica:0/task:0/device:TPU:2, TPU, 17179869184, 3398334431816235909)
INFO:tensorflow:*** Available Device: _DeviceAttributes(/job:worker/replica:0/task:0/device:TPU:3, TPU, 17179869184, 590228271416283825)
INFO:tensorflow:*** Available Device: _DeviceAttributes(/job:worker/replica:0/task:0/device:TPU:4, TPU, 17179869184, 15798692355657786920)
INFO:tensorflow:*** Available Device: _DeviceAttributes(/job:worker/replica:0/task:0/device:TPU:5, TPU, 17179869184, 1954696542070558698)
INFO:tensorflow:*** Available Device: _DeviceAttributes(/job:worker/replica:0/task:0/device:TPU:6, TPU, 17179869184, 898663154261538184)
INFO:tensorflow:*** Available Device: _DeviceAttributes(/job:worker/replica:0/task:0/device:TPU:7, TPU, 17179869184, 4503259854332690989)
INFO:tensorflow:*** Available Device: _DeviceAttributes(/job:worker/replica:0/task:0/device:TPU_SYSTEM:0, TPU_SYSTEM, 17179869184, 10060441942931197921)
WARNING:tensorflow:tpu_model (from tensorflow.contrib.tpu.python.tpu.keras_support) is experimental and may change or be removed at any time, and without warning.
INFO:tensorflow:New input shapes; (re-)compiling: mode=train (# of cores 8), [TensorSpec(shape=(32,), dtype=tf.int32, name='core_id_40'), TensorSpec(shape=(32, 9, 9, 45), dtype=tf.float32, name='input_4_10'), TensorSpec(shape=(32, 1), dtype=tf.float32, name='policy_target_120'), TensorSpec(shape=(32, 1), dtype=tf.float32, name='value_target_120')]
INFO:tensorflow:Overriding default placeholder.
INFO:tensorflow:Remapping placeholder for input_4
INFO:tensorflow:Started compiling
INFO:tensorflow:Finished compiling. Time elapsed: 38.74543237686157 secs
INFO:tensorflow:Setting weights on TPU model.
3905/3906 [============================>.] - ETA: 0s - loss: 4.4481 - policy_loss: 1.7086 - value_loss: 1.7085 - policy_categorical_accuracy: 1.7085 - value_binary_accuracy: 1.7084INFO:tensorflow:New input shapes; (re-)compiling: mode=eval (# of cores 8), [TensorSpec(shape=(32,), dtype=tf.int32, name='core_id_50'), TensorSpec(shape=(32, 9, 9, 45), dtype=tf.float32, name='input_4_10'), TensorSpec(shape=(32, 1), dtype=tf.float32, name='policy_target_120'), TensorSpec(shape=(32, 1), dtype=tf.float32, name='value_target_120')]
INFO:tensorflow:Overriding default placeholder.
INFO:tensorflow:Remapping placeholder for input_4
INFO:tensorflow:Started compiling
INFO:tensorflow:Finished compiling. Time elapsed: 17.765841245651245 secs
390/390 [==============================] - 43s 110ms/step - loss: 3.7269 - policy_loss: 1.4889 - value_loss: 1.4889 - policy_categorical_accuracy: 1.4878 - value_binary_accuracy: 1.4872
3906/3906 [==============================] - 644s 165ms/step - loss: 4.4478 - policy_loss: 1.7085 - value_loss: 1.7084 - policy_categorical_accuracy: 1.7084 - value_binary_accuracy: 1.7083 - val_loss: 3.7269 - val_policy_loss: 1.4889 - val_value_loss: 1.4889 - val_policy_categorical_accuracy: 1.4878 - val_value_binary_accuracy: 1.4872
<tensorflow.python.keras.callbacks.History at 0x7f841a0954a8>

TPUでは、lossは正しく表示されますが、accuracyが正しく表示されませんでした。
ネットワークの出力が2つの場合に発生するようです。
まだKerasのTPUサポートは正式版ではないので、バグの可能性があります。

Google ColabのGPU(Tesla K80)での学習結果
WARNING:tensorflow:From /usr/local/lib/python3.6/dist-packages/tensorflow/python/ops/math_ops.py:3066: to_int32 (from tensorflow.python.ops.math_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use tf.cast instead.
390/390 [==============================] - 62s 159ms/step - loss: 4.4728 - policy_loss: 3.4411 - value_loss: 0.2205 - policy_categorical_accuracy: 0.2172 - value_binary_accuracy: 0.7430
3906/3906 [==============================] - 1803s 462ms/step - loss: 5.2603 - policy_loss: 4.1786 - value_loss: 0.2296 - policy_categorical_accuracy: 0.1737 - value_binary_accuracy: 0.7433 - val_loss: 4.4728 - val_policy_loss: 3.4411 - val_value_loss: 0.2205 - val_policy_categorical_accuracy: 0.2172 - val_value_binary_accuracy: 0.7430
<tensorflow.python.keras.callbacks.History at 0x7f61b7ee5780>
ローカルPCのGPU(1080 Ti)での学習結果
WARNING:tensorflow:From C:\Anaconda3\lib\site-packages\tensorflow\python\ops\resource_variable_ops.py:435: colocate_with (from tensorflow.python.framework.ops) is deprecated and will be removed in a future version.
Instructions for updating:
Colocations handled automatically by placer.
WARNING:tensorflow:From C:\Anaconda3\lib\site-packages\tensorflow\python\keras\utils\losses_utils.py:170: to_float (from tensorflow.python.ops.math_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use tf.cast instead.
WARNING:tensorflow:From C:\Anaconda3\lib\site-packages\tensorflow\python\ops\math_ops.py:3066: to_int32 (from tensorflow.python.ops.math_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use tf.cast instead.
2019-02-17 16:51:40.039612: I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2
2019-02-17 16:51:40.216090: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1433] Found device 0 with properties:
name: GeForce GTX 1080 Ti major: 6 minor: 1 memoryClockRate(GHz): 1.6705
pciBusID: 0000:01:00.0
totalMemory: 11.00GiB freeMemory: 9.10GiB
2019-02-17 16:51:40.224327: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1512] Adding visible gpu devices: 0
2019-02-17 16:51:40.638740: I tensorflow/core/common_runtime/gpu/gpu_device.cc:984] Device interconnect StreamExecutor with strength 1 edge matrix:
2019-02-17 16:51:40.642624: I tensorflow/core/common_runtime/gpu/gpu_device.cc:990]      0
2019-02-17 16:51:40.645263: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1003] 0:   N
2019-02-17 16:51:40.648587: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1115] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 8780 MB memory) -> physical GPU (device: 0, name: GeForce GTX 1080 Ti, pci bus id: 0000:01:00.0, compute capability: 6.1)
2019-02-17 16:51:45.858150: I tensorflow/stream_executor/dso_loader.cc:152] successfully opened CUDA library cublas64_100.dll locally
390/390 [==============================] - 16s 42ms/step - loss: 4.4274 - policy_loss: 3.3871 - value_loss: 0.2285 - policy_categorical_accuracy: 0.2272 - value_binary_accuracy: 0.7431
3906/3906 [==============================] - 451s 116ms/step - loss: 5.2415 - policy_loss: 4.1575 - value_loss: 0.2314 - policy_categorical_accuracy: 0.1760 - value_binary_accuracy: 0.7424 - val_loss: 4.4274 - val_policy_loss: 3.3871 - val_value_loss: 0.2285 - val_policy_categorical_accuracy: 0.2272 - val_value_binary_accuracy: 0.7431

GPUでは、accuracyも正しく表示されています。

比較

TPUとGPUで学習時間を比較した結果は以下の通りでした。

TPU 644s
Google ColabのGPU(Tesla K80) 1803s
ローカルPCのGPU(1080Ti) 451s

バッチサイズを2048に増やすと以下の通りになりました。(Google ColabのGPUは遅いので測定から外しています。)

TPU 806s
ローカルPCのGPU(1080Ti) 516s

20ブロック、256フィルタ、バッチサイズ256では以下の通りでした。

TPU 1448s
ローカルPCのGPU(1080Ti) 1267s

いずれの条件でも、ローカルPCのGPU(1080Ti)が早いという結果になりました。
条件を変えると変わってくるかもしれません。

Google Colabを使う場合は、TPUの方がGPUより圧倒的に速いです。
ただし、TPUはまだKerasでの動作が安定していないため、安心して使えるのはTensorFlow 2.0で正式サポートされてからになりそうです。