TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

世界コンピュータ将棋選手権でのMulti Ponderのヒット率

先日の世界コンピュータ将棋選手権で、dlshogiはMulti Ponderを実装していた。

相手番で相手局面を探索するStochastic Ponderという手法と組み合わせることで、追加コストゼロで候補手をN手取得できるため、Multi PonderはMCTSと相性がよいクラスタリング手法である。
手法の詳細は、先日の記事に書いたので参照して欲しい。

どれくらい効果があったか把握するため、大会でのPonderのヒット率について集計した。

計測対象

二次予選 9対局、決勝 7対局について調べた。

ヒット率

いずれかにヒット

8台のサーバのいずれかにヒットした割合は以下の通りであった。

局面数 1640
ヒット率 94.817%
候補手ごと

0番目の候補手から7番目の候補手のどれにヒットしたかを分けて計測した際の割合は以下の通り。

0番 1番 2番 3番 4番 5番 6番 7番
65.304% 13.963% 6.280% 3.658% 1.524% 1.646% 1.158% 1.280%

上位4つ(3番まで)で、89.2%がヒットしている。
4台あれば、十分に効果を発揮しそうである。

序盤、中盤、終盤ごと

序盤、中盤、終盤ごとに分析した。
適当だが、序盤は24手まで、中盤25手~150手、終盤は150手以降とした。

序盤 中盤 終盤
局面数 192 982 466
ヒット率 92.708% 98.472% 87.982%

中盤のヒット率が特に高いことがわかった。
序盤は、定跡によって戦型を決めている場合や、NNUE系の序盤とdl系で違いがあるためと考えられる。また、序盤は定跡で抜けているため、Stochastic Ponderの探索時間が短いことも影響していそうである。
終盤が最も低くなっているのは、相手がすでに負けの状況で意味なく手数を伸ばそうとする場合の指し手はほとんど意味を持たないためと考えられる。

まとめ

世界コンピュータ将棋選手権で、Multi Ponderのヒット率は、94.817%であったことが分かった。
dlshogiのStochastic Ponderと組み合わせること、高いヒット率が実現できていた。
特に中盤では、98.472%がヒットしていた。
候補手上位4つで、89.2%がヒットしていたため、サーバは4台でも十分な効果が期待できる。

将棋AIの実験ノート:Fixup Initialization 続き

以前にBatch Normalizationを使用しないFixup Initializationを試したことがある。

その際、Leela Chess Zeroでは、Batch Normalizationの統計情報に関連する問題が報告されていることに言及した。
Pawn promotion issues in T40 · Issue #784 · LeelaChessZero/lc0 · GitHub

将棋で具体的に問題になる局面があるか調べられていなかったが、最新のdlshogiでも水匠がすぐに見つける最善手を見つけるのに時間のかかる局面の指摘をもらったので、Batch Normalizationが問題になる局面かどうか調査を行った。

課題の局面


dlshogiは、▲8三馬が最善手になるのに、50万ノード以上探索が必要。

検証方法

以下の3つのモデルを同一の訓練データで学習して最善手が変わるか確認した。
訓練データには、dlshogiの自己対局で生成した2500万局面を使用した。
モデルサイズは10ブロック192フィルタ、バッチサイズは1024、学習率は0.01とした。

# モデルサイズ Batch Normalization 活性化関数 Fixup Initialization
1 ResNet 10ブロック192フィルタ あり Swish なし
2 ResNet 10ブロック192フィルタ あり ReLU なし
3 ResNet 10ブロック192フィルタ なし ReLU あり

Fixup Initializationを使う場合、活性化関数をSwishにすると学習ができないため、ReLUとしている。
Batch Normalizationを使う場合、活性化関数はSwishの方が精度が高くなるが、活性化関数の影響を排除するためReLUでも比較する。

精度

訓練データを1エポック学習した際の精度は以下の通り。
テストデータは、floodgateのR3500以上の棋譜からサンプリングした856,923局面(重複なし)を使用した。

# 方策損失 価値損失 方策正解率 価値正解率
1 1.8914645 0.5800246 0.4172188 0.6787168
2 1.9333075 0.5880046 0.4127659 0.6734275
3 1.9039329 0.5890524 0.4158906 0.6768700

想定通り、Batch Normalizationありで、活性化関数Swishの場合(#1)が、最も精度が高くなった。
活性化関数がReLUの場合は、Batch Normalizationを使う場合より、Fixup Initializationの方が精度が高い。
ただし、Batch Normalizationの精度はバッチサイズにもよるため、バッチサイズを増やすとBatch Normalizationを使った方がよくなる可能性はある。

課題局面の候補手

Multi PV5で確認した。

# 候補手1 候補手2 候補手3 候補手4 候補手5
1 5f6f P*2b 8i7g 9i9h 6g6f
2 9i9h 5f8c P*2b 2c3b 8i7g
3 5f8c 9i9h 2c3b P*2b 8i7g

Batch Normalizationありで、活性化関数Swishの場合(#1)、5つの候補手に、▲8三馬(5f8c)が現れていない。
Batch Normalizationありで、活性化関数がReLUの場合(#3)は、2番目の候補手になっている。
Fixup Initializationの場合(#3)は、1番目の候補手になっている。

Batch Normalizationだけではなく、活性化関数の影響も大きそうである。

方策の比較

課題局面での方策の値を比較した。

予想に反して、5f8cの値は、Fixup Initializationが一番小さくなっている。
課題の局面の方策ではなく、読み筋上の方策もしくは価値の値が影響しているようだ。

NPS

Fixup Initializationは、スカラー乗数とスカラーバイアスを、各畳み込み、活性化関数の前に追加しているため、必要な演算が増えるためNPSを確認した。
GPU 3090、2スレッドで50万ノード探索時。

# NPS
1 42349
2 45236
3 29547

活性化関数SwishとReLUでNPSの違いは少ない。
Fixup Initializationを使うと、NPSが大幅に低下している。
読み筋に王手の局面があるかによってもNPSが変わるため、初期局面でも比較した。

# NPS
1 62853
2 62969
3 54078

初期局面でもFixup Initializationを使うと遅くなっていた。

まとめ

dlshogiで最善手を見つけるのに時間のかかる局面について、Batch Normalizationの影響を、Fixup Initializationと比較して調査した。
結果、Batch Normalizationだけではなく、活性化関数の影響も大きそうであることがわかった。

また、Fixup Initializationを使うとNPSが大幅に低下することがわかった。
NPSが大幅に低下するので、Fixup Initializationは使えなさそうである。

今までテストデータに対する精度のみを見て、活性化関数をSwishにしていたが、少ない探索数では活性化関数の違いにより候補手が大きく異なり、ReLUの方がよい場合があることがわかった。
テストデータに対する精度の比較だけでなく、強さへの影響も調べなおした方が良さそうである。

cshogiをGitHub ActionsでビルドしてPyPIに公開する

Pythonの将棋ライブラリcshogiPyPIで公開しているが、PyPIに公開するまでの手順が多いため、GitHub Actionsを使って自動化を行った。

前提

cshogiは、CythonとC++を使用しているため、ビルドが必要になる。
WindowsではVisual Studioが必要になる。
Linuxでは複数ディストリビューションに対応したバイナリを作成する必要があるため、manylinuxを使用してビルドが必要である。
また、Pythonのバージョンごとにバイナリを用意する必要がある。

Actions secrets

PyPIにアップロードする際に、アカウント情報が必要になるため、事前にGitHubのActions secretsに登録を行う。
手順は公式のドキュメントを参照した。
https://docs.github.com/ja/actions/security-guides/encrypted-secrets

Repository secretsにユーザ名とパスワードをそれぞれPYPI_USERNAMEとPYPI_PASSWORDとして登録した。

GitHub Actionsの定義作成

.github/workflowsにpublish.ymlを作成し、公式のドキュメントを参照しながら、GitHub Actionsの定義を記述した。
https://docs.github.com/ja/actions/quickstart

リリース時に実行する

GitHubのreleaseに公開した際に実行するように、以下の通り定義した。

name: Build and publish
on:
  release:
    types: [published]
Windows用定義

GitHub Actions Virtual Environmentsは、OSごとに用意されている。
Windowsの向けには、Python3.6も含まれる、windows-2019を使用した。

以下の通りジョブを定義した。
Pythonの複数バージョンに対応するため、matrixを使用している。
Python 3.10のみnumpyのバージョンが異なるため、if:で条件分岐している。

  deploy-windows:
    runs-on: windows-2019
    strategy:
      matrix:
        python-version: [ '3.6', '3.7', '3.8', '3.9', '3.10' ]
    steps:
    - name: Check out repository code
      uses: actions/checkout@v3
    - uses: actions/setup-python@v3
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install setuptools wheel twine Cython
    - name: Build and publish 3_6-3_9
      if: ${{ matrix.python-version != '3.10' }}
      env:
        TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
        TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
      run: |
        pip install numpy==1.19.5
        python setup.py bdist_wheel
        twine upload dist/*
    - name: Build and publish 3_10
      if: ${{ matrix.python-version == '3.10' }}
      env:
        TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
        TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
      run: |
        pip install numpy==1.21.5
        python setup.py sdist bdist_wheel
        twine upload dist/*

Linux用定義

manylinuxには複数のdockerイメージが用意されているが、新しいイメージの方が対応するPythonの新しいバージョンに対応しているが、利用者側でpipの新しいバージョンが必要になる。
できるだけ多くの環境に対応できるように、Python3.6から3.9は、manylinux2010を使い、Python3.10向けにはmanylinux_2_24を使うことにした。

GitHub Actionsで、実行環境にコンテナを指定できるが、ソースをホスト側にチェックアウトしたかったため、docker runコマンドでコンテナを起動するようにした。

manylinux2010とmanylinux_2_24でジョブを分けて、以下の通り定義した。

  deploy-manylinux2010:
    runs-on: ubuntu-20.04
    strategy:
      matrix:
        python-version: [ 'cp36-cp36m', 'cp37-cp37m', 'cp38-cp38', 'cp39-cp39' ]
    steps:
    - name: Check out repository code
      uses: actions/checkout@v3
    - uses: actions/setup-python@v3
      with:
        python-version: '3.x'
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install twine
    - name: Build and publish
      env:
        TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
        TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
      run: |
        docker run --rm -v $(pwd):/work -w /work quay.io/pypa/manylinux2010_x86_64 sh -c "/opt/python/${{ matrix.python-version }}/bin/pip install numpy==1.19.5 Cython && /opt/python/${{ matrix.python-version }}/bin/python setup.py bdist_wheel && cd dist && auditwheel repair *.whl"
        twine upload dist/wheelhouse/*
  deploy-manylinux_2_24:
    runs-on: ubuntu-20.04
    strategy:
      matrix:
        python-version: [ 'cp310-cp310' ]
    steps:
    - name: Check out repository code
      uses: actions/checkout@v3
    - uses: actions/setup-python@v3
      with:
        python-version: '3.x'
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install twine
    - name: Build and publish
      env:
        TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
        TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
      run: |
        docker run --rm -v $(pwd):/work -w /work quay.io/pypa/manylinux_2_24_x86_64 sh -c "/opt/python/${{ matrix.python-version }}/bin/pip install numpy==1.21.5 Cython && /opt/python/${{ matrix.python-version }}/bin/python setup.py bdist_wheel && cd dist && auditwheel repair *.whl"
        twine upload dist/wheelhouse/*

まとめ

今までcshogiのビルドとPyPIへの公開を手動で行っていたが、手順が多く大変だったので、GitHub ActionsでビルドとPyPIへの公開を自動化した。
GitHub Actionsは、mac osにも対応しているので、今後気が向いたときに対応したい。

gMLPを試す

ほぼ個人メモです。

画像認識のモデルで、畳み込みを使わずMLPのみ同等の精度が出せるというMLP Mixerの発展形であるgMLPを試してみた。

gMLP

チャネル方向と、空間方向に分けてMLPを適用する構成は、MLP Mixerと同じだが、Spatial Gating Unit (SGU)という仕組みを使うことで、空間的相互作用をとらえられるようになり、非正方形の畳み込みを学習することができるそうだ。

過学習しやすいため、強い正則化が必要らしい。

ただし、画像認識の精度は最高精度の畳み込みのモデルには及ばないようだ。
自然言語処理にも使えて、タスクによってはBERTと同等の精度になるようだ。

ResNet(ResNet-152)と比べると、gMLP-Sは少ないパラメータ数で同等以上の精度を達成している。

CIFAR-100で試す

CIFAR-100で、ResNetと比較してみた。

実装は、timmの実装を使用した。
timmは、ImageNet用のモデルになっているため、CIFAR-100で試すには一部修正が必要だった。

gMLP

mlp_mixer.pyに以下の定義を追加した。

@register_model
def gmlp_18_64_3_32(pretrained=False, **kwargs):
    model_args = dict(
        img_size=32, patch_size=3, num_blocks=18, embed_dim=64, mlp_ratio=6, block_layer=SpatialGatingBlock,
        mlp_layer=GatedMlp, **kwargs)
    model = _create_mixer('gmlp_18_64_3_32', pretrained=pretrained, **model_args)
    return model

学習のハイパーパラメータは論文の値を参考にした。
以下のコマンドで100epoch学習した。

python train.py cifar100/ --dataset torch/cifar100 --dataset-download --input-size 3 32 32 --num-classes 100 --model gmlp_18_64_3_32 --opt adamw --opt-eps 1e-6 --lr 1e-3 --warmup-lr 1e-6 --weight-decay 0.05 --clip-grad 1 -b 128 --epochs 100 --amp

学習結果は以下の通り。

(略)
Train: 109 [   0/390 (  0%)]  Loss: 1.343 (1.34)  Time: 0.261s,  489.66/s  (0.261s,  489.66/s)  LR: 1.000e-06  Data: 0.166 (0.166)
Train: 109 [  50/390 ( 13%)]  Loss: 1.628 (1.55)  Time: 0.095s, 1346.62/s  (0.098s, 1305.57/s)  LR: 1.000e-06  Data: 0.004 (0.007)
Train: 109 [ 100/390 ( 26%)]  Loss: 1.741 (1.54)  Time: 0.102s, 1251.79/s  (0.097s, 1324.35/s)  LR: 1.000e-06  Data: 0.004 (0.005)
Train: 109 [ 150/390 ( 39%)]  Loss: 1.396 (1.54)  Time: 0.097s, 1318.39/s  (0.096s, 1331.55/s)  LR: 1.000e-06  Data: 0.003 (0.005)
Train: 109 [ 200/390 ( 51%)]  Loss: 1.686 (1.54)  Time: 0.092s, 1396.70/s  (0.096s, 1336.16/s)  LR: 1.000e-06  Data: 0.003 (0.005)
Train: 109 [ 250/390 ( 64%)]  Loss: 1.568 (1.54)  Time: 0.091s, 1408.70/s  (0.095s, 1340.75/s)  LR: 1.000e-06  Data: 0.004 (0.004)
Train: 109 [ 300/390 ( 77%)]  Loss: 1.497 (1.54)  Time: 0.099s, 1298.39/s  (0.095s, 1345.71/s)  LR: 1.000e-06  Data: 0.004 (0.004)
Train: 109 [ 350/390 ( 90%)]  Loss: 1.527 (1.54)  Time: 0.091s, 1410.79/s  (0.095s, 1349.20/s)  LR: 1.000e-06  Data: 0.004 (0.004)
Train: 109 [ 389/390 (100%)]  Loss: 1.280 (1.54)  Time: 0.089s, 1432.73/s  (0.095s, 1348.79/s)  LR: 1.000e-06  Data: 0.000 (0.004)
Test: [   0/78]  Time: 0.083 (0.083)  Loss:  1.3799 (1.3799)  Acc@1: 68.7500 (68.7500)  Acc@5: 85.9375 (85.9375)
Test: [  50/78]  Time: 0.019 (0.022)  Loss:  1.4971 (1.5826)  Acc@1: 64.8438 (62.2089)  Acc@5: 89.0625 (84.5588)
Test: [  78/78]  Time: 0.017 (0.022)  Loss:  1.4111 (1.5565)  Acc@1: 75.0000 (62.4500)  Acc@5: 81.2500 (84.9400)
*** Best metric: 62.64 (epoch 74)

SOTAのaccuracyは96.08なので、それに比べたら精度が低い。
CIFAR-100 Benchmark (Image Classification) | Papers With Code

ResNet18

gMLPは18ブロックとしたので、ResNetも18ブロックのモデルを使用した。
ResNetの実装もImageNet用になっているため、以下の箇所を修正した。

resnet.py

class ResNet(nn.Module):
(略)
    def __init__(
(略)
        # Stem
(略)
            #self.conv1 = nn.Conv2d(in_chans, inplanes, kernel_size=7, stride=2, padding=3, bias=False)
            self.conv1 = nn.Conv2d(in_chans, inplanes, kernel_size=3, stride=1, padding=1, bias=False)

正則化などのハイパーパラメータはデフォルトのままとし、以下のコマンドで100epoch学習した。

python train.py cifar100/ --dataset torch/cifar100 --dataset-download --input-size 3 32 32 --num-classes 100 --model resnet18 --opt adam --lr 1e-3 --warmup-lr 1e-6 -b 128 --epochs 100 --amp

学習結果は以下の通り。

Train: 109 [   0/390 (  0%)]  Loss: 1.708 (1.71)  Time: 0.217s,  590.99/s  (0.217s,  590.99/s)  LR: 1.000e-06  Data: 0.175 (0.175)
Train: 109 [  50/390 ( 13%)]  Loss: 1.447 (1.51)  Time: 0.036s, 3560.15/s  (0.051s, 2486.26/s)  LR: 1.000e-06  Data: 0.004 (0.008)
Train: 109 [ 100/390 ( 26%)]  Loss: 1.480 (1.51)  Time: 0.033s, 3929.15/s  (0.049s, 2620.71/s)  LR: 1.000e-06  Data: 0.004 (0.006)
Train: 109 [ 150/390 ( 39%)]  Loss: 1.588 (1.49)  Time: 0.033s, 3897.49/s  (0.047s, 2704.57/s)  LR: 1.000e-06  Data: 0.004 (0.005)
Train: 109 [ 200/390 ( 51%)]  Loss: 1.453 (1.49)  Time: 0.035s, 3654.12/s  (0.045s, 2867.00/s)  LR: 1.000e-06  Data: 0.003 (0.005)
Train: 109 [ 250/390 ( 64%)]  Loss: 1.550 (1.49)  Time: 0.034s, 3718.97/s  (0.043s, 2951.21/s)  LR: 1.000e-06  Data: 0.004 (0.005)
Train: 109 [ 300/390 ( 77%)]  Loss: 1.513 (1.49)  Time: 0.044s, 2910.11/s  (0.043s, 2997.72/s)  LR: 1.000e-06  Data: 0.004 (0.005)
Train: 109 [ 350/390 ( 90%)]  Loss: 1.556 (1.49)  Time: 0.046s, 2767.55/s  (0.042s, 3026.09/s)  LR: 1.000e-06  Data: 0.004 (0.005)
Train: 109 [ 389/390 (100%)]  Loss: 1.493 (1.49)  Time: 0.031s, 4186.29/s  (0.042s, 3068.63/s)  LR: 1.000e-06  Data: 0.000 (0.005)
Test: [   0/78]  Time: 0.070 (0.070)  Loss:  1.2080 (1.2080)  Acc@1: 75.0000 (75.0000)  Acc@5: 91.4062 (91.4062)
Test: [  50/78]  Time: 0.010 (0.015)  Loss:  1.3047 (1.3169)  Acc@1: 68.7500 (67.1109)  Acc@5: 89.0625 (88.5570)
Test: [  78/78]  Time: 0.007 (0.015)  Loss:  1.3691 (1.3144)  Acc@1: 75.0000 (67.2800)  Acc@5: 81.2500 (88.5800)
*** Best metric: 67.62 (epoch 95)

gMLPより少し良い精度になった。

学習時間とパラメータ数

学習時間は、ResNetはgMLPの1/2くらいになっている。

パラメータ数は、gMLPの方が少ない。(torchsummaryの結果)

gMLP(18ブロック) 871,052
ResNet18 11,220,132

パラメータ数が少ないにも関わらず学習時間がかかっているのは、ResNetは畳み込みにTensorCoreが使用できるためと思われる。
GPUには3090 RTXを使用して、学習にはAMPを使用している。

まとめ

画像認識の新しい手法であるgMLPを試してみた。
少ないパラメータ数でResNetより少し劣るくらいの精度になることが確認できた。
ただし、TensorCoreを使う場合はパラメータ数が少なくてもResNetの方が早く学習できた。

SOTAの精度と比べると遠く及んでいない。
Augumentationはtimmのデフォルトから変更していないため、CIFAR-100に合わせたチューニングが必要かもしれない。

SOTAは、SAMという最新のオプティマイザを使用しているので、それも別途試してみたい。

優勝決定戦について

第32回世界コンピュータ将棋選手権決勝の優勝決定戦について、dlshogi側の評価値は以下のようになっていました。

112手目まではほぼ互角と判断していました。
114手目くらいから徐々に先手優勢になって、逆転となった166手目の手前では、評価値-720でした。

逆転となった166手目

166手目△4九角打で、少し評価値が良くなりましたがまだ先手有利の評価値で、▲6三とでほぼ互角の評価になりました。
これはMCTSでは、探索結果の終端ノードの評価値(勝率)を平均化しているため、勝ちの手順を見つけていても、他の手順も調べているとすぐには評価値に反映されないことが関係しています。

▲6三と以外だったら

166手目のdlshogiの読み筋は▲9七玉となっていましたが、▲9七玉と進んだ局面では、△7五銀が必至になっていましたので勝ちは揺るがなかったようです。
第32回世界コンピュータ将棋選手権の優勝決定戦である、☗二番絞り VS ☖dlshogi with HEROZ の解説記 - あらきっぺの将棋ブログ


なお、dlshogiも▲9七玉と進んだ局面では、△7五銀を発見できるようです。
ただし、USIオプションのUCT_Threadが4だと△9一玉を選んですぐには勝てない手順になりました。UCT_Thread=3だと発見できました。
NPSを上げるため、並列度を上げ過ぎると精度が落ちる場合があること、あらためて認識しました。

感想

自宅で対局中にログの評価値を眺めていて-700くらいになった時点で、見るのがつらくなったので、画面から離れていました。
やはり気になって確認したらなぜか評価値が30000になっていて目を疑いました。
中継サイトでも先手投了になっていたので、dlshogiが勝ったことを認識しました。
逆転の瞬間を見逃してしまって、残念です。
川崎の会場ではだいぶ盛り上がっていたようです。

この写真が好きです。

第32回世界コンピュータ将棋選手権 結果報告

5/3~5/5に開催された第32回世界コンピュータ将棋選手権に参加しました。

HEROZチームとして、「dlshogi with HEROZ」というプログラム名でエントリしました。

大会の概要

世界コンピュータ将棋選手権は、1990年より毎年開催されている歴史のあるコンピュータ将棋の大会です。
今回は32回目の開催で、51チームが参加しました。
第1予選、第2予選を通過した上位8チームで総当たりのリーグ戦で決勝戦が行われました。

大会の結果

決勝では1回も負けることはなく、優勝という結果になりました。
第32回世界コンピュータ将棋選手権 決勝

ソフトの強さは毎年強くなっており、上位ソフトの強さは拮抗しており決勝に残るだけでも難しい状況でした。
優勝の可能性はあると考えていましたが、決勝に残るチームでの勝率の差は10%程度だと考えていたので、1回勝負では運で決まる要素も大きいと思っていました。

優勝できたのは、望外の結果です。

今大会の注目ポイント

従来型の探索方法にニューラルネットワークの評価関数を使ったNNUE系の将棋ソフトと、ディープラーニング系の将棋ソフトのどちらが勝つのか関係者が注目していたポイントになります。

昨年の大会の時点で、ディープラーニング系の将棋ソフトが優勝するのではという予測がありましたが、昨年まではまだNNUE系に及んでいませんでした。
ただし、昨年の世界コンピュータ選手権の前に開催された電竜戦では、ディープラーニング系の将棋ソフトGCT(dlshogiライブラリ)が優勝していました。

世界コンピュータ選手権は、持ち時間が電竜戦より長く、長い持ち時間ではディープラーニング系の将棋ソフトよりNNUE系が有利だったのではという見方がありました。
低レーティング帯では、その傾向があることを確かめています。
ただし、高レーティング帯では持ち時間のその影響は小さくなるため、モデルの精度が勝負だと考えていました。

チームの役割分担

今大会ではHEROZの将棋AI開発者で、一つのチームとして参加しました。
主な目的は、HEROZ社内の計算リソースを分け合わずに集中するためでした。

dlshogiの開発、強化学習、定跡作成は私が行い、チームメンバに計測を手伝っていただいたり、大会に向けた作戦の意見を伺ってそれを反映しました。

今大会の工夫

モデルの精度

モデルの精度が最重要と考えていましたので、秋の電竜戦が終了後まずはモデル精度に注力しました。

dlshogiでは、初期局面集から自己対局を行って教師データを生成していますが、これまでは大量に用意した初期局面集から開始局面を選択していましたが、この方法では序盤よりも中・終盤が多くなるため、序盤の精度が不足していました。

floodgateの棋譜に出現する8手目までの局面から開始局面を選択し、一定の手数までは最善手の評価値(勝率)から閾値の範囲にある手を確率的に選択して自己対局を行うようにしました。
こうすることで序盤の局面ごとの勝率をモデルに反映させることができると考えました。

モデルサイズ

電竜戦ではResNetの15ブロック224フィルタのモデルを使用していましたが、15ブロックのモデルが強くならなくなったため、途中で20ブロックに移行しました。

モデルサイズを大きくすることでNPSが低下するため、精度が上がっても強くならないことが懸念でしたが、自己対局と事前探索による定跡作成では精度が高い方が有利であるため、20ブロックに移行して、大会向けには15ブロックを使用することも考えていました。

結局、20ブロックの方が精度、強さともに15ブロックを上回ったため、大会でも20ブロックのモデルを使用しました。

定跡作成

dlshogiは、アピール文章にある方法で、事前に高ノード数で探索した手を定跡としています。
定跡作成時の相手側指し手は、floodgateの棋譜の出現頻度や、dlshogiで探索した際の子ノードの訪問回数から確率的に選択し、大会での出現確率が高そうな手順をより深く作成するようにしています。

ただし、どの戦型を採用するかランダムプレイありのdlshogiで連続対局を行い、その勝率を見て決めて、自動生成した定跡を手動で数局面だけ修正しています。

dlshogi同士の対局では戦型は角換わりになりますが、上記計測で先手勝率の高かった相掛かりを採用しました。

定跡の並列生成

定跡を幅広く、深く作成するため、複数マシンで並列で生成しました。
相手の指し手を確率的に選択しているため、並列でも別々の局面を検討して作成することができます。
他ノードで作成した情報を共有した方が効率が上がるため、定期的に定跡をマージする機能を実装しました

定跡に欠陥がないか検証

高ノード数で探索しても、何十手も先にあるトラップを発見することはできないため、負ける手順に嵌る場合があります。
そのため、作成した定跡に対して、dlshogiのランダムプレイありで連続対局を行い欠陥がないか検証しました。
欠陥のある手順は、その手順にならないように定跡を手動で修正しました。

修正前の定跡も大会での予備として、残しておきました。
これは大会でも役に立ちました。

Stochastic Multi Ponder

今大会はチームで参加したことにより、大会でマシンリソースをすべて利用できるようになりました。
マシンリソースを有効活用するため、クラスタ構成にすることを検討しました。

MCTSのルート並列化でクラスタ構成にすることを以前に試した際は成果がでなかったため、実装が容易で、ほぼデメリットがないMulti Ponderによるクラスタ構成としました。

Multi Ponderは、第5回電王トーナメントでshotgunが採用した手法で、相手の指し手を複数予測して、並列でPonderを行う手法です。
http://id.nii.ac.jp/1001/00199872/

shotgunで実装されていたMulti Ponderでは、技巧2のMulti PVの結果を利用していましたが、dlshogiでは相手番で相手局面を探索するStochastic Ponderという手法を実装しているため、ほとんどの場合、相手局面でのゲーム木が展開済みで、ルートの子ノードの訪問回数を参照することで、有望な予測手をN手取得することができます(ゲーム木が未展開の場合は、方策ネットワークの推論結果を使用)。

そこで、クラスタの親となるノードで、Stochastic Ponderを行い、その結果を利用してMulti Ponderを行うようにしました。
予測したN手以外の手が指された場合、Stochastic Ponderでも並列に探索を行っているため、Multi Ponderを使用しない場合と遜色のない手を指すことができるため、採用するデメリットはほぼないと考えました。

ただし、探索するノードが分かれることで、前回探索したノードの再利用ができなくなる場合があるため、ponderhitした場合、次の局面の指し手予測の第一候補をそのponderhitしたエンジンに割り当てることで、前回の探索結果を再利用されやすいようにしました。

大会では、親1台と子ノード8台の構成としていました。
正確には集計していませんが、ほとんどの局面でponderhitしており、定跡を抜けた後も0秒指しが続いていました。
相手が時間を使わなかった場合、一定以上は思考するためそこで初めて時間を使うという状況でした。

効果は計測できていませんが、終盤に時間を使えるようになったことで、ディープラーニング系の弱点と言われている終盤で読みが浅くなることによる読み抜けが防げていたと考えています。

まとめ

2017年に開発を始めてから目標としていた世界コンピュータ将棋選手権で優勝できたことで、一旦目標を達成できたという思いです。
dlshogiの改良を続けてきた苦労が報われてよかったです。
今後は、同じ手法の延長線で開発を続けるよりは、人間の将棋の学習に使えるようにすることや、ブレークスルーを起こせるような新たな手法を試すことに注力したいと思っています。

運営の皆様、参加者の皆様、3日間大変お疲れさまでした。

Unityで将棋アプリの開発 その14(実戦寄せ問題)

作成している将棋アプリに実戦寄せ問題を実装した。

アプリを継続利用してもらうには、飽きずに上達につながるコンテンツが必要だと思うので、序盤、中盤、終盤それぞれ棋力を鍛えられる内容を盛り込みたいと思っている。

序盤については、定跡の戦型予想と、AIの戦型指定を実装した。
Unityで将棋アプリの開発 その9(定跡と戦型予想) - TadaoYamaokaの開発日記
Unityで将棋アプリの開発 その10(AIの戦型指定) - TadaoYamaokaの開発日記

中盤については、互角局面集を実装しようと思っている。

今回は、終盤用として、実戦寄せ問題を実装した。
自分は初級者なので、プロの対局などを見ていて、評価値は優勢でもどう寄せれば勝てるか分からなかったりする。
そのため、適度な難易度の問題集で終盤の寄せ方の練習ができるようにしたい。

必至局面の抽出

はじめ、floodgateの棋譜から必至の局面を抽出して、それを問題集にすることを試してみた。

以下の方法で、必至局面を抽出した。

  • 評価値がmateになった局面から1手戻って、王手がかかっていない
  • もう1手戻って王手がかかっていない

上記の条件で候補の局面を抽出した後、1手指した後の相手の局面で、評価値がマイナスのmate(王手はかかっていないので即詰みではない=必至)になるか検証した。

抽出した局面でAIとテスト対局したところ、既に駒割りでかなり差がついている局面が多く、必至をかけなくても簡単に勝ててしまうものがほとんどだった。

この抽出方法は、適切ではなかった。

優勢だがパスすると大きく評価が下がる局面の抽出

次に、mateの局面から2手ずつ戻って、評価値がほぼ勝ちだが、1手パスすると大幅に評価値が下がる局面を抽出するようにした。
これにより、間違わずに寄せれば勝ちだが、適当に指すとすぐに逆転される局面が抽出できる。

実際に自分でテストしてみると、自分の棋力ではミスして逆転される局面も多く、緊張感のある局面が抽出できたように思う。
難しすぎると練習にならないので、パスしたあとの評価値の減り具合で難易度調整したい。

抽出できた局面の例

f:id:TadaoYamaoka:20220405205737p:plain

間違えて逆転された(@´ω`@)
f:id:TadaoYamaoka:20220405205808p:plain

ヒント機能が有効になっているので、答え合わせもできる。
f:id:TadaoYamaoka:20220405205831p:plain

まとめ

初級者が飽きずに練習できるコンテンツとて、実戦寄せ問題を実装した。
はじめfloodgateの棋譜から必至の局面を抽出したが、必至をかけなくても勝てる局面が多く適切な問題集にならなかった。
そこで、優勢だがパスすると形勢が悪くなる局面を抽出したところ、寄せ方を間違えると逆転される局面が抽出できた。

抽出できた局面は簡単に勝てる局面も含まれているので、優勢とする閾値や、パスした後の評価値の閾値についてはもう少し調整してみたい。

5月の世界コンピュータ選手権が近づいてきているので、アプリ開発はペースダウンの予定。