TadaoYamaokaの開発日記

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

Ubuntu 16.04 LTSにChainerをインストールする

以前に機械学習用にPCを購入したとき、Ubuntuをインストールしようとしましたが、GPUが認識できずあきらめてWindowsをインストールしていました。

Ubuntu Desktopをインストールしていましたが、Ubuntu Serverをインストールして試したら無事動かすことができたので手順を残しておきます。

Ubuntu Desktopでうまくインストールできなかった原因は、CUDAのInstallation Guide for Linuxに記載されていましたが、ドライバインストール前は、カーネルパラメータにnomodesetを付けて起動しないとコンソールモードにできないためでした。
UbuntuGUIで使う必要がなければ、Ubuntu Serverからインストールした方が簡単です。
後からGUIをインストールすることもできます。

Nouveau kernel driver をdisableにする

/etc/modprobe.d/blacklist-nouveau.confを作成して、以下のように編集します。

$ sudo vim /etc/modprobe.d/blacklist-nouveau.conf
blacklist nouveau
options nouveau modeset=0

反映してリブートします。

$ sudo update-initramfs -u
$ sudo reboot

ドライバダウンロード

以前失敗したときは、GeForceのドライバをPPAからインストールしていましたが、今回はNvidiaのサイトからダウンロードしました。
http://www.nvidia.com/Download/index.aspx?lang=en-us

f:id:TadaoYamaoka:20180105221236p:plain
図のように設定して、SEARCHをクリックします。
f:id:TadaoYamaoka:20180105221325p:plain
DOWNLOADをクリックします。
NVIDIA-Linux-x86_64-384.98.run」がダウンロードされます。

インストール

ビルドツールがないとエラーになるので、build-essentialをインストールしておきます。

$ sudo apt-get install build-essential

gccのバージョンが5.4であることを確認します。

$ gcc --version
gcc (Ubuntu 5.4.0-6ubuntu1~16.04.5) 5.4.0 20160609
Copyright (C) 2015 Free Software Foundation, Inc.

ドライバをインストールします。

$ sudo ./NVIDIA-Linux-x86_64-384.98.run

リブートします。(不要かも)

$ sudo reboot

ドライバが認識できていることを確認します。

$ sudo lspci | grep -i nvidia
65:00.0 VGA compatible controller: NVIDIA Corporation Device 1b06 (rev a1)
65:00.1 Audio device: NVIDIA Corporation Device 10ef (rev a1)
$ nvidia-smi
Fri Jan  5 20:58:17 2018       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 384.90                 Driver Version: 384.90                    |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce GTX 108...  Off  | 00000000:65:00.0 Off |                  N/A |
|  0%   44C    P5    28W / 250W |      0MiB / 11171MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

CUDAダウンロード

NvidiaのサイトからCUDAをダウンロードします。
https://developer.nvidia.com/cuda-toolkit

TensorFlowも使う予定なので、CUDA8をダウンロードします。
「Download Now」をクリックした後、ページの下の方にある「Legacy Releases」をクリックします。
f:id:TadaoYamaoka:20180105222616p:plain

「CUDA Toolkit 8.0 GA2 (Feb 2017)」をクリックします。
f:id:TadaoYamaoka:20180105222741p:plain

Select Target Platformを下図のように選択します。
f:id:TadaoYamaoka:20180105222817p:plain

Base InstallerをDownloadします。
「cuda-repo-ubuntu1604-8-0-local-ga2_8.0.61-1_amd64.deb」がダウンロードされます。

インストール手順は、「Installation Guide for Linux」のリンクから参照できます。
f:id:TadaoYamaoka:20180105223111p:plain

CUDAのインストール

Installation Guide for Linuxの通りにインストールしました。

ダウンロードした.debパッケージをインストールします。

$ sudo dpkg -i cuda-repo-ubuntu1604-8-0-local-ga2_8.0.61-1_amd64.deb

インストールされるのはレポジトリのメタデータだけなので、以下のコマンドでCUDAをインストールします。

$ sudo apt-get update
$ sudo apt-get install cuda

環境変数の設定

.bashrcに以下の行を追記します。

$ vi .bashrc
export PATH=/usr/local/cuda-8.0/bin${PATH:+:${PATH}}
export LD_LIBRARY_PATH=/usr/local/cuda-8.0/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}

再起動します。

$ sudo reboot

再起動後、GUIのログイン画面が表示されるようになったので、CUIに戻しました。

$ sudo systemctl set-default multi-user.target
$ sudo reboot

cuDNNのダウンロード

NvidiaのサイトからcuDNNをダウンロードします。
https://developer.nvidia.com/cudnn

「Download」をクリックします。
「I Agree To the Terms of the cuDNN Software License Agreement」にチェックします。
TensorFlowが対応しているcuDNN v6.0をダウンロードします。
f:id:TadaoYamaoka:20180105224006p:plain
「Runtime Library」と「Developer Library」の2つをダウンロードします。
「libcudnn6_6.0.21-1+cuda8.0_amd64.deb」と「libcudnn6-dev_6.0.21-1+cuda8.0_amd64.deb」がダウンロードされます。

インストール手順は、「Installation Guide」のリンクから参照できます。

cuDNNのインストール

Installation Guideの通りインストールしました。

$ sudo dpkg -i libcudnn6_6.0.21-1+cuda8.0_amd64.deb
$ sudo dpkg -i libcudnn6-dev_6.0.21-1+cuda8.0_amd64.deb

Anacondaのインストール

pyenvを使ってAnacondaをインストールします。

pyenvをインストールします。

$ git clone git://github.com/yyuu/pyenv.git ~/.pyenv

.bashrcに環境変数を追加します。

$ echo 'export PYENV_ROOT="${HOME}/.pyenv"' >> ~/.bashrc
$ echo 'export PATH="${PYENV_ROOT}/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(pyenv init -)"' >> ~/.bashrc

再ログインします。

インストール可能なPythonの一覧を表示します。

$ pyenv install --list

Anacondaをインストールします。問題の起きなそうなPython 3.5がインストールされるanaconda3-4.2.0をインストールします。

$ pyenv install anaconda3-4.2.0

インストールされたPythonの一覧を確認します。

$ pyenv versions
* system (set by /home/xxx/.pyenv/version)
  anaconda3-4.2.0

anaconda3-4.2.0をデフォルトにします。

$ pyenv global anaconda3-4.2.0

anaconda3-4.2.0がデフォルトになったことを確認します。

$ pyenv versions
  system
* anaconda3-4.2.0 (set by /home/xxx/.pyenv/version)

Chainerのインストール

先にcupyをインストールします。

pip install cupy

Chainerをインストールします。

pip install chainer

MNISTサンプル実行

MNISTサンプルを実行します。

$ git clone https://github.com/chainer/chainer.git
$ cd chainer
$ python examples/mnist/train_mnist.py -g 0

以下のエラーが発生しました。

Traceback (most recent call last):
  File "examples/mnist/train_mnist.py", line 127, in <module>
    main()
  File "examples/mnist/train_mnist.py", line 78, in main
    updater = training.updaters.StandardUpdater(
AttributeError: module 'chainer.training.updaters' has no attribute 'StandardUpdater'

サンプルがChainerのリリース版の最新に対応していないようです。(tagを指定してgit cloneすればよかったかも)
examples/mnist/train_mnist.pyの78行目の

training.updaters.StandardUpdater

training.StandardUpdater

に修正します。

再度実行します。

$ python examples/mnist/train_mnist.py -g 0
QXcbConnection: Could not connect to display 
中止 (コアダンプ)

実行中にコアダンプが出力されました。
X windowが有効でないときに、matplotlibがエラーを吐くようです。
参考:QXcbConnection: Could not connect to display のエラー対処 - Qiita

train_mnist.pyのソースを確認すると、「--noplot」というオプションがあるようなので、「--noplot」を付けて実行します。

$ python examples/mnist/train_mnist.py -g 0 --noplot
GPU: 0
# unit: 1000
# Minibatch-size: 100
# epoch: 20

epoch       main/loss   validation/main/loss  main/accuracy  validation/main/accuracy  elapsed_time
(略)
20          0.00584211  0.0890107             0.99795        0.9846                    35.6723       
     total [##################################################] 100.00%
this epoch [..................................................]  0.00%
     12000 iter, 20 epoch / 20 epochs
    348.04 iters/sec. Estimated time to finish: 0:00:00.

実行できました。

参考までに、同じPCのWindows上で実行すると、

(略)
20          0.0119292   0.0924681             0.996449       0.9859                    45.5555

となりました。
elapsed_timeを比較すると、Ubuntuが35.6723で、Windowsが45.5555となっており、Ubuntuの方が27.7%ほど速いです。

将棋でディープラーニングする その51(ディリクレノイズ)

電王トーナメント版のdlshogiでは、Policyの読み漏れを回避するために、自分の手番の局面だけ、Policyの予測するそれぞれの手について1/1000の確率で値を1.5倍にするということを行っていた。
自分の手番の局面だけにしたのは、相手の局面にもノイズを入れると、相手番で楽観的な手を選びやすくなり頓死しやすくなる傾向があったためである。
MCTSは並列で探索するため探索する各スレッドの探索する手をばらけさせる効果もあり、ノイズなしより少しノイズを入れた方が強くなることを確認していた。

その後、ルート局面のみ1/500の確率でランダムでノイズを付加して、
(1-\epsilon)*p(a) + \epsilon * 1.0, \epsilon=0.5
という式で、ランダムで全ての手が選ばれやすくすることを行っていた。
これもGPSfish相手に勝率を上げる効果があった。

AlphaGo Zeroでは、ルート局面のみにディリクレノイズを加えることを行っている。
そこで、ルート局面のノイズの付加方法をディリクレノイズに変更して、GPSfish相手に勝率が上がるか試してみた。

変更前

前に発見したブートストラップ項のバグを改修したバージョンで、elmoで生成した5.8億局面を学習したモデルで、GPSfishと1手3秒50回対局で勝率47%となっている。

対局数50 先手勝ち17(36%) 後手勝ち29(63%) 引き分け4
dlshogi
勝ち22(47%) 先手勝ち7(33%) 後手勝ち15(60%)
GPSfish 0.2.1+r2837 gcc 4.8.1 osl wordsize 32 gcc 4.8.1 64bit
勝ち24(52%) 先手勝ち10(40%) 後手勝ち14(66%)

ディリクレノイズに変更後

定数にAlphaZeroの論文と同じ値を使用し、
P(x,a)=(1-\epsilon)p_a + \epsilon \eta_a, \eta \sim Dir(0.15), \epsilon=0.25
として、GPSfishと対局させた結果は以下の通り。

対局数17 先手勝ち10(62%) 後手勝ち6(37%) 引き分け1
dlshogi
勝ち4(25%) 先手勝ち3(37%) 後手勝ち1(12%)
GPSfish 0.2.1+r2837 gcc 4.8.1 osl wordsize 32 gcc 4.8.1 64bit
勝ち12(75%) 先手勝ち7(87%) 後手勝ち5(62%)

明らかに弱くなったので、対局を途中で打ち切った。

次に、\epsilon=0.1として、対局を行った結果は以下の通り。

対局数20 先手勝ち9(47%) 後手勝ち10(52%) 引き分け1
dlshogi
勝ち10(52%) 先手勝ち5(50%) 後手勝ち5(55%)
GPSfish 0.2.1+r2837 gcc 4.8.1 osl wordsize 32 gcc 4.8.1 64bit
勝ち9(47%) 先手勝ち4(44%) 後手勝ち5(50%)

20回対局で、GPSfishに52%の勝率となった。

今度は、\epsilon=0にして対局を行った結果は以下の通り。

対局数20 先手勝ち10(52%) 後手勝ち9(47%) 引き分け1
dlshogi
勝ち5(26%) 先手勝ち3(30%) 後手勝ち2(22%)
GPSfish 0.2.1+r2837 gcc 4.8.1 osl wordsize 32 gcc 4.8.1 64bit
勝ち14(73%) 先手勝ち7(77%) 後手勝ち7(70%)

勝率が下がった。

考察

\epsilonを増やしすぎても、減らしすぎても勝率は低くなっている。
AlphaZeroの論文と同じ値だと勝率は低くなった。
1手3秒という試行時間では、ノイズにより探索の幅が広がったのに対して深さが足りなくなったためと思われる。
これは、「探索と活用のジレンマ」という問題で説明できる。
幅と深さはどちらかを増やせば、どちらかが減る関係にある。
UCTアルゴリズムでは理論的には適切なバランスに調整されるはずだが、現実的な思考時間では、深さを優先した探索が必要になる。
PUCTアルゴリズムではPolicyの遷移確率に従って優先度を調整する。
Policyの精度は完全ではないため、浅いトラップのあるゲームでは読み漏れがあると、すぐに頓死する。
ノイズを加えることで、ある程度読み漏れを防ぐことができる。
ノイズを増やしすぎると幅に対して深さが足りなくなるため、行動価値の予測精度が落ちる。
思考時間に応じた適切なバランスを見つける必要がある。

今回の結果で、モデルの精度だけではなく探索のハイパーパラメータの調整によっても勝率が大きく変わることが分かったので、今後調整していきたい。
ベイズ最適化まで行おうとすると相当な対局数が必要になるので、気長にやっていくことにする。

C++でディリクレ分布による乱数生成

C++にディリクレ分布で乱数生成する標準関数は用意されていない。

ガンマ分布で乱数生成する標準関数std::gamma_distributionが用意されているので、
Dirichlet distribution - Wikipedia
に書かれている方法を使って、ガンマ分布で乱数y1,...,yKを生成し、
x_i=\frac{y_i}{\sum_{j=1}^K y_j}
で、ディリクレ分布の乱数に変換できる。

これを実装した。

#include <iostream>
#include <random>
#include <vector>
#include <algorithm>

void random_dirichlet(std::mt19937_64 &mt, std::vector<double> &x, const double alpha) {
	std::gamma_distribution<double> gamma(alpha, 1.0);
	
	double sum_y = 0;
	for (int i = 0; i < x.size(); i++) {
		double y = gamma(mt);
		sum_y += y;
		x[i] = y;
	}
	std::for_each(x.begin(), x.end(), [sum_y](double &v) { v /= sum_y; });
}

int main() {
	std::random_device rd;
	std::mt19937_64 mt(rd());
	const int K = 5;
	const double alpha = 0.15;

	for (int i = 0; i < 10; i++) {
		std::vector<double> x(K);
		random_dirichlet(mt, x, alpha);

		for (int j = 0; j < x.size(); j++) {
			std::cout << x[j];
			if (j < x.size() - 1) std::cout << ", ";
		}
		std::cout << std::endl;
	}
}
実行結果

K=5, alpha=0.15の対称ディリクレ分布

0.00820617, 1.6067e-08, 1.82496e-06, 0.991569, 0.000223472
0.100336, 0.104018, 0.027749, 0.0115689, 0.756328
0.965557, 0.00401107, 7.88541e-18, 0.0285327, 0.00189949
0.721145, 0.0584562, 0.00153585, 2.04515e-05, 0.218842
0.834343, 0.00332245, 5.07e-09, 0.00101873, 0.161316
0.00615545, 0.993583, 0.000253227, 8.34935e-06, 3.51258e-07
0.893519, 0.0714379, 0.000327648, 0.00228227, 0.0324336
0.000104421, 0.00124077, 0.99865, 5.90869e-07, 4.54077e-06
0.0158551, 0.00067078, 0.0668421, 0.882654, 0.033978
8.98511e-09, 0.016774, 7.59551e-05, 0.0137781, 0.969372

ディリクレ分布の可視化

AlphaZeroのMCTSのルートノードではディリクレノイズを加えることで、全ての手をランダムで選ばれやすくしている。
P(x,a)=(1-\epsilon)p_a + \epsilon \eta_a, \eta \sim Dir(0.03)

以前の記事で、2次元のディリクレ分布を可視化したが、3次元の場合の可視化ができないか調べていたら、以下のページを見つけたので試してみた。
Visualizing Dirichlet Distributions with Matplotlib
x, y, zを2次元の正3角形に投影して、確率密度を色で表示している。
本当は、x, y, zを3角形の平面に投影して、確率密度を高さの軸にして3Dのグラフにしたかったが、やり方がわからなかった。

上記のページは、python2用のコードになっていたので、python3で動かすには、

import functools

を追加して、
reduceをfunctools.reduceに置換する。

[0.03, 0.03, 0.03]のディリクレ分布を可視化するコードは以下の通り。

%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.tri as tri

corners = np.array([[0, 0], [1, 0], [0.5, 0.75**0.5]])
triangle = tri.Triangulation(corners[:, 0], corners[:, 1])

# Mid-points of triangle sides opposite of each corner
midpoints = [(corners[(i + 1) % 3] + corners[(i + 2) % 3]) / 2.0 \
             for i in range(3)]
def xy2bc(xy, tol=1.e-3):
    '''Converts 2D Cartesian coordinates to barycentric.'''
    s = [(corners[i] - midpoints[i]).dot(xy - midpoints[i]) / 0.75 \
         for i in range(3)]
    return np.clip(s, tol, 1.0 - tol)

class Dirichlet(object):
    def __init__(self, alpha):
        from math import gamma
        from operator import mul
        self._alpha = np.array(alpha)
        self._coef = gamma(np.sum(self._alpha)) / \
                     functools.reduce(mul, [gamma(a) for a in self._alpha])
    def pdf(self, x):
        '''Returns pdf value for `x`.'''
        from operator import mul
        return self._coef * functools.reduce(mul, [xx ** (aa - 1)
                                         for (xx, aa)in zip(x, self._alpha)])

def draw_pdf_contours(dist, nlevels=200, subdiv=8, **kwargs):
    import math

    refiner = tri.UniformTriRefiner(triangle)
    trimesh = refiner.refine_triangulation(subdiv=subdiv)
    pvals = [dist.pdf(xy2bc(xy)) for xy in zip(trimesh.x, trimesh.y)]

    plt.tricontourf(trimesh, pvals, nlevels, **kwargs)
    plt.axis('equal')
    plt.xlim(0, 1)
    plt.ylim(0, 0.75**0.5)
    plt.axis('off')

draw_pdf_contours(Dirichlet([0.03, 0.03, 0.03]))
実行結果

f:id:TadaoYamaoka:20171209223116p:plain

よく見ないとわからないが、角の部分の色が赤色(=高い値)になっている。
それぞれの角は(1, 0, 0)、(0, 1, 0)、(0, 0, 1)を意味する。
つまり、いずれかの手が選ばれやすくなる。

[0.03, 0.03, 0.03]だと見にくいので、[0.9, 0.9, 0.9]とすると、

draw_pdf_contours(Dirichlet([0.9, 0.9, 0.9]))

f:id:TadaoYamaoka:20171209223400p:plain
角の値の色が変わっているのが分かりやすくなった。

alphaが正の値の場合、

draw_pdf_contours(Dirichlet([5, 5, 5]))

f:id:TadaoYamaoka:20171209223735p:plain
中央の値が選ばれやすくなる。

乱数生成

Pythonでディリクレ分布に従って乱数を生成するには、numpy.random.dirichletを使用する。

import numpy as np
[np.random.dirichlet([0.03, 0.03, 0.03]) for _ in range(10)]
[array([  9.30062319e-13,   2.57427053e-04,   9.99742573e-01]),
 array([  1.00000000e+00,   2.17381455e-19,   6.85158127e-35]),
 array([  1.00000000e+00,   1.16336290e-15,   3.83232435e-19]),
 array([  8.76967918e-01,   1.15399381e-36,   1.23032082e-01]),
 array([  1.94448447e-45,   5.91371213e-01,   4.08628787e-01]),
 array([  5.51018131e-56,   1.00000000e+00,   1.32521447e-17]),
 array([  2.68526987e-21,   5.73712429e-01,   4.26287571e-01]),
 array([  1.00000000e+00,   7.11978680e-14,   4.22528386e-24]),
 array([  3.84802928e-58,   5.40350337e-20,   1.00000000e+00]),
 array([  1.56164809e-01,   8.43835191e-01,   2.39362055e-11])]

どれか一つが選ばれやすくなっているのが分かる。

AlphaZero Chess/Shogiの論文を読む その3

前回までに個人的に気になった点はだいたい書いたので、今回は残った部分で気になったところを拾って書きます。

スケーラビリティ

思考時間を増やした場合、αβ探索よりもレーティングの伸びが良い。
これはAlphaZeroのMCTSがαβ探索より思考時間を短縮できるということを示しており、αβ探索の方がこの分野で優れているという信念が疑問視された。

引用されていた文献には、チェスにおけるMCTSの欠点として、トラップを発見するためにより長い試行時間が必要ということが挙げられていました。
ただし、十分に正確なシミュレーション戦略が見つかれば対処できるとも書かれていたので、今回それが精度の高いPolicyによって実現されたということでしょう。

先行研究

チェスの研究はたくさん上げられていますが、将棋の先行研究としては、ボナンザによる機械学習が取り上げられていました。

なぜうまく動くか

ニューラルネットワークによる非線形関数近似で局面評価をすると、強い表現力があるが疑わしい近似誤差を起こすことがある。
MCTSは、これらの近似誤差を平均するため相殺する傾向がある。
対照的に、αβ探索は近似誤差をルートに伝播させる。
MCTSによって、ニューラルネットワークを効果的に適用できるようになる。

ドメイン知識

使用したドメイン知識は以下の通り。

  1. 入力特徴と出力特徴を表現するために、盤がグリッド構造であることを使用
  2. シミュレーション、ゲーム終了のために、ゲームのルールを使用
  3. 千日手、移動、成り、持ち駒からの打ち手のルールを入力特徴と出力特徴に使用
  4. 上限手数で引き分けとする

以上のドメイン知識以外は使用していない。

考察

スケーラビリティ

将棋はチェス以上に浅いトラップを持つゲームであることが知られています。
将棋におけるモンテカルロ木探索の特性の解明
浅いトラップの存在がMCTSがMini-Max探索よりも弱い原因の一つとされていました。
AlphaZeroの手法でこの欠点が克服できることが示されたと思います。

先行研究

コンピュータ将棋でブレイクスルーとなったボナンザはしっかり取り上げられてますね。

なぜうまく動くか

非線形関数はMCTSと組み合わせることでうまく動くということです。
Ponanza方式のαβ探索とディープラーニングの組み合わせはこれとは異なるアプローチですが、近似誤差の平均化という点では狙いは同じだと思います。
AlphaZero方式はドメイン知識を用いない汎用性のある方法という主張もあるので目指すベクトルは異なりますが、将棋を強くするという目的であれば他にもアプローチはあると思います。

ドメイン知識

千日手を入力特徴に加えていますが、囲碁のコウと異なり、千日手が絡む手はそれほど重要ではないため、私は千日手は探索中にチェックしてもよいと思っていますが、ニューラルネットワークに学習させたかったようです。
AlphaGoZeroの論文の記事でも書きましたが、将棋を強くするという目的であれば効果的なドメイン知識は使って学習を効率化させたいところです。

今回で終了します。

AlphaZero Chess/Shogiの論文を読む その2(AlphaGo Zeroとの差分)

AlphaZero Chee/Shogiの論文についての続きです。
今回はAlphaGo Zeroとの差分について書きます。

AlphaGo Zeroの論文については、以前に書いた記事を参照ください。

ネットワーク構成

ニューラルネットワークの構成は、AlphaGo Zeroと同じ、PolicyとValueを同時出力する20ブロックのResNetです。
前回書いた通り入力特徴と出力ラベルが異なります。

探索アルゴリズム

基本的にはAlphaGo Zeroと同じ、モンテカルロ木探索にPolicyとValueを使ったPUCTアルゴリズムです。

以下の点が異なります。

  • AlphaGo Zeroでは探索のハイパーパラメータは、ベイジアン最適化で調整したが、チェス、将棋のゲーム固有の調整は行わず同じパラメータを使用
  • AlphaGo Zeroニューラルネットワークに対称、回転の8局面からランダムに選んで入力していたが、局面は固定とする
  • AlphaGo Zeroと同様に探索を確実にするためにルート局面にディリクレノイズDir(α)を加えるが、合法手の数に反比例して、チェス、将棋、囲碁で、α={0.3, 0.15, 0.03}にスケーリングする

AlphaGo Zeroと同様に自己対局では、30手まではルートノードの訪問回数に応じた分布に従って確率で選択して、それ以降は最大訪問回数の手を選ぶグリーディー戦略としていましたが、チェス、将棋の場合は何手目まで確率的にしていたかは書かれていません。

elmoとの対局では、グリーディー戦略を使っています。

自己対局パイプライン

AlphaGo Zeroでは自己対局1,000回ごとにチェックポイントを設けて、評価のステップで、55%以上の勝率の場合にそれを最良のネットワークとし、その後の自己対局で使用していましたが、評価のステップは省略されました。
常に最新のネットワークが自己対局に使用されるようになっています。

囲碁は固定手数で終局しますが、将棋では上限手数を超えると引き分けとしています。

学習

以下はAlphaGo Zeroと同じ。

以下の点が異なる。

  • ミニバッチサイズ:4,096(AlphaGoZeroは2,048)
  • 自己対局時のシミュレーション回数は800回(AlphaGoZeroは1,600回)
  • 自己対局数は、将棋では2.4千万(AlphaGoZeroは4.9千万)
  • 学習率は0.2から0.02、0.002、0.0002の順に段階的に下げる。(AlphaGoZeroは0.01、0.001、0.0001)

考察

ネットワーク構成については、前回日記で書いたので省略。

探索アルゴリズム

AlphaGo Zeroでは、対称、回転を使うことでData Augmentationを行って、探索時もそれらをランダムに選ぶことで評価を平均化すると共にランダム性が加わっていました。
チェス、将棋では局面が固定されるので、ランダム性を加えるのはルート局面のディリクレノイズのみとなっています。
自己対局では、序盤については訪問回数に応じて確率的に手を選択するのでそこで局面のバリエーションを作っています。
何手まで確率的に指しているのかは書かれていませんでした。

AlphaGo Zeroの論文には、対局時に手を選択する方法が書かれていませんでしたが、elmoとの対局時はグリーディー戦略と書かれていましたので、AlphaGo Zeroも対局時はグリーディー戦略を使っていたと思われます。

自己対局パイプライン

評価のステップを省略しても学習できたようです。
これは意外な結果です。
強化学習は学習が発散することがあって学習が難しいことが知られています。
そのため、過去のネットワークに対して強くなっていることを確認するステップが必要と思っていました。
DQNのような方策オフの学習に比べて、AlphaGoZeroのような方策オンの場合は、学習が安定するということでしょうか。

学習

自己対局時のシミュレーション回数が半分になっています。
800回のシミュレーションではとても弱いですが、その回数でも強化学習できるようです。
AlphaGoのRL Policyの強化学習ではシミュレーション0回(Policyのみ)でしたので、それに比べたらまともな方策が学習できるということでしょう。

800回のシミュレーションで1手80msと書かれているので、1局平均150手とすると、1局の自己対局で12秒です。
並列化しない場合、2.4千万局の対局には、547.9年かかります。
GPUを使うと、処理速度を落とさず並列にゲームを進行できるので、これを短縮しています。
自己対局には、5,000個の第一世代のTPUを使ったと書かれています。
第一世代のTPUが何TFLOPSか不明ですが、個人レベルで試すのは不可能な規模であることは想像できます。

学習率は、AlphaGo Zeroよりも大きい値が使われています。
0.2と大きめの学習率から始めても学習できるようです。
Batch Normalizationを使用しているので、大きめの学習率が使用でき収束が速くできたということでしょう。

続く

AlphaZero Chess/Shogiの論文を読む

DeepMindからAlphaGo Zeroと同じ方法で、チェスと将棋でトップレベルを上回ったという論文が発表されました。
[1712.01815] Mastering Chess and Shogi by Self-Play with a General Reinforcement Learning Algorithm

ドメイン知識を用いないスクラッチから強化学習のみで達成しています。
将棋やチェスはモンテカルロ木探索(MCTS)は向かず、Mini-Max探索の方が良いというのが常識のようになっていましたが、将棋やチェスでもディープラーニングと組み合わせることでMCTSが有効であることが示されたというのも大きな成果だと思います。

まだ全部読み切れていませんが、気になる個所から読んだ内容を書いていきます。

個人的に一番気になるのは、入力特徴と出力ラベルの表現方法です。
チェスについても書かれていますが、将棋のみについて書きます。

入力特徴

  • 先手の駒の配置×14種類
  • 後手の駒の配置×14種類
  • 繰り返し回数(3まで)(千日手用)
  • 先手の持ち駒の枚数×7種類
  • 後手の持ち駒の枚数×7種類

これを8手の履歴局面分。
これに、先手か後手かの特徴(1面)と合計手数(1面)を加えて合計で(14+14+3+7+7)×8+2=362面。

出力ラベル

  • 桂馬以外の駒の位置×方向(8)×距離(8) = 64
  • 桂馬の位置×方向(2)
  • 桂馬以外の駒が成る場合(64)
  • 桂馬が成る場合(2)
  • 持ち駒から打つ位置×7種類

合計で、64+2+64+2+7=139面

他の表現方法も考えられるが、これで合理的な手を学習できたとのこと。

考察

入力特徴

私が試している将棋AIでは入力特徴は、

  • 先手の駒の配置×14種類
  • 後手の駒の配置×14種類
  • 先手の持ち駒×7種類×枚数
  • 先手の持ち駒×7種類×枚数
  • 駒の効き×14種類
  • 位置ごとの効き数(1)
  • 王手かどうか(1)

としていましたが、これとの比較で言うと、
まず、千日手を判定する情報が加わっています。
AlphaGo Zeroでは、コウを識別するために履歴局面を使っていましたが、入力特徴として与えています。

持ち駒は種類ごとの枚数で、7面となっています。
ニューラルネットワークの入力は0~1で正規化する場合が多いですが、枚数をどのような値で入力しているかは書かれていません。
AlphaGoでは整数はOneHotエンコーディングで複数枚で入力していましたが、整数をそのまま数値で入力して問題ないのでしょうか。
※「the number of captured prisoners of each type」で7面を、枚数なしで種類のみと解釈しましたが、コメントを頂いたので修正しました。

効きや王手の情報は入っていません。

8手の履歴局面を入力していますが、私が以前に実験したところでは履歴は効果ありませんでした。教師ありではなく自己対局による強化学習では必要なのかもしれません。

先手後手の特徴がありますが、将棋の場合は局面の価値は、先手でも後手でも変わらないと思いますが、必要なのでしょうか。あっても影響はないと思いますが。

合計手数が1面のみで表現されていますが、具体的にどのように値を入力するかが書かれていません。例えば256手を0~1の範囲の実数として入力するのでしょうか。
また、手数が同じでも局面の進み具合は異なるので、そのまま手数を入力してしまってよいか疑問です。
技巧などは、局面の進み具体を手番ではなく進行度という尺度で表現しています。

出力ラベル

私が試している将棋AIでは、

  • 移動先の座標
  • 移動方向(8+2(桂馬の動き))

として表現しています。
それとの比較で言うと、
座標は移動先ではなく、移動元で表現しています。
持ち駒から打つ場合は、移動先なのだから、移動先でそろえた方がよいと思っていますが、あまり違いはないかもしれません。

移動方向に加えて、移動距離も表現しています。
移動距離がなくても移動する駒は一意に特定できるので、必要かどうかは疑問です。
私の実験では、出力ラベルは必要最低限にした方がよいです。
私は以前は、移動する駒にもラベルを割り当てていましたが、ない方が良いことが分かったので、移動方向のみにしました。
AlphaZeroでも移動する駒は表現していません。

AlphaGoZeroの囲碁の入力特徴と出力ラベルの表現をベースにしていることがうかがえます。
必ずしも将棋でベストな表現ではないと思いますが、マシンパワーがあれば、結果は同じということでしょうか。

個人レベルで試すなら、学習効率の良い入力特徴と出力ラベルの表現を模索することも必要だと思います。

続く。。。
AlphaZero Chess/Shogiの論文を読む その2(AlphaGo Zeroとの差分) - TadaoYamaokaの開発日記
AlphaZero Chess/Shogiの論文を読む その3 - TadaoYamaokaの開発日記