TadaoYamaokaの開発日記

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

C++の引数解析ライブラリ

Pythonではargparseという引数解析ライブラリが標準で使用できる。
バッテリー同梱なだけはある。

C++にはそのような便利なライブラリは用意されていないため、同じような引数解析を行おうとする煩雑な処理を実装する必要がある。
有名な引数解析ライブラリとしては、gflagsやboost::Program_optionsがあるが、ビルドに手間がかかったりする。
ヘッダー一つで使用できるライブラリがないか探したところ、cxxoptsがヘッダーオンリーでboost::Program_optionsライクな使い方ができて良さそうだった。

試した結果

試しに使ってみると、

  • 必須の引数が指定できない
  • 位置引数(positional arguments)のヘルプがオプションと区別できない

という点が望んでいたものとは違った。

必須の引数については、Issuesに上がっていたもののライブラリではサポートする気がないという回答だった。
自前でチェックが必要になるので、毎回同じようなチェック処理を書かないといけない。

2点目の位置引数のヘルプ表示については、以下のようにfileとoutputを位置引数にしている場合、

options.add_options()
	("file", "File name", cxxopts::value<std::string>(file))
	("output", "output file", cxxopts::value<std::string>())
	("d,debug", "Enable debugging")
	("h,help", "Print help")
	;
options.parse_positional({ "file", "output" });

ヘルプは以下のように表示される。

Usage:
  cxxopts_test [OPTION...] positional parameters

      --file arg    File name
      --output arg  output file
  -h, --help        Print help

これは、期待していた表示とは異なる。

ライブラリの修正

そこで、以上の2点を満たすように、ライブラリを修正した。
修正後、以下のように表示できるようになった。

usage:
  cxxopts_test [OPTION...] file output

positional arguments:
  file    File name
  output  output file

options:
  -h, --help   Print help

位置引数が不足している場合は、以下のように表示される。

usage:
  cxxopts_test [OPTION...] file output
Option 'file' is required but not present

使い方は、元のライブラリとほとんど変わっていない。
ヘルプ表示に多少コードが必要だが、argparseと似たような使い方ができる。

#include <iostream>
#include "cxxopts.hpp"

int main(int argc, char** argv)
{
	cxxopts::Options options("cxxopts_test");
	try {
		std::string file;

		options.add_options()
			("file", "File name", cxxopts::value<std::string>(file))
			("output", "output file", cxxopts::value<std::string>())
			("d,debug", "Enable debugging")
			("h,help", "Print help")
			;
		options.parse_positional({ "file", "output" });

		auto result = options.parse(argc, argv);

		if (result.count("help"))
		{
			std::cout << options.help({}) << std::endl;
			return 0;
		}

		std::cout << file << std::endl;
		std::string output = result["output"].as<std::string>();
		std::cout << output << std::endl;
	}
	catch (cxxopts::OptionException &e) {
		std::cout << options.usage() << std::endl;
		std::cerr << e.what() << std::endl;
	}

	return 0;
}

GitHubにフォークして修正したソースを置いたので、使いたい方はご自由にどうぞ。
cxxopts/cxxopts.hpp at master · TadaoYamaoka/cxxopts · GitHub

optunaで探索パラメータの最適化

optunaを使って将棋プログラムの探索パラメータの最適化を行うツールを作成した。
Pythonで実装しており、任意のUSIエンジンの間で指定した回数対局を行い、勝率が最大となるように探索パラメータを最適化する。

https://github.com/TadaoYamaoka/DeepLearningShogi/blob/master/utils/mcts_params_optimizer.py

最適化するパラメータ

探索パラメータは、USIオプションで変更可能としておく。
パラメータの値の範囲などを設定ファイルで設定可能にすることも考えたが、汎用性はあまり必要ないのでスクリプトに直接記述することにした。
パラメータを最適化するのは、1つのエンジン(オプションのcommand1)のみである。

optuna

パラメータの最適化には、optunaを使用した。
hyperoptという選択もあったが、試したところ、hyperoptよりも収束が速く、見込みがない試行を枝刈りする機能もあるのでoptunaにした。
optunaは、現バージョンでは最小化にしか対応していないため、勝率に-1を掛けて負の値を返す必要がある。

回数の指定

勝率の測定は、デフォルトで100回(optunaの単位ではステップ)対局して行う。
それを、デフォルトで100回試行して最適化を行う。
それぞれ、オプションで回数を指定できる。

枝刈り

optunaで枝刈りを有効にすると、各試行の同じステップでの中央値を基準として枝刈りが行われる。
デフォルトでは、0ステップから比較が行われるため、たまたま初めの2局連続で負けるとすぐに枝刈りになってしまうため、オプションで枝刈り開始を20ステップ目以降とした。

対局条件

秒読み(デフォルト1秒)のみで、時間切れ負けの判定は行っていない。
最大手数(デフォルト256)に達すると、引き分けとする。

勝敗の条件

終局まで対局すると時間がかかるため、どちらかのエンジンが詰みを見つけた時点で相手を強制的に投了させるようにした。
エンジンの詰みの判定が誤っていると正しく動作しないため、エンジンにバグがない前提である。
また、エンジン自体に投了の閾値の設定がある場合は、設定を行う(スクリプトに直書きしてある)。

開始局面

sfenファイルから、ランダムに開始局面を選んで、先後入れ替えて対局できるようにした。
やねうら互角局面集などが使用できる。

ログ

対局中の情報やoptunaの最適化の結果は、ログに出力する。
デフォルトではコンソールに出力し、オプションを指定した場合はファイルに出力する。

棋譜の保存

後で棋譜を確認できるように1対局ごとに、.kifファイルで保存するようにした。
(.kifフォーマットは、人には見やすいが、プログラムには優しくなかった・・・)

使用例

python mcts_params_optimizer.py --command1 "C:\DeepLearningShogi\x64\Release\usi.exe" --command2 "C:\gpsfish\gpsfish.exe" --byoyomi 1000 --model "D:\model\model_rl_val_wideresnet10_selfplay_078" --batch_size 168 --initial_positions "D:\shogi\gokaku\records2016_10818.sfen" --kifu_dir D:\kifu\param_opt --log D:\log\param_opt.txt

※オプションmodel、batch_sizeは、dlshogiに特化したオプションなので他のエンジンには不要

出力例

2019/01/06 20:49:38	INFO	Finished a trial resulted in value: -0.3854166666666667. Current best value is -0.3854166666666667 with parameters: {'C_init': 128, 'C_base': 38360}.

※パラメータC_initと、C_baseを最適化している。valueは-1を掛けた勝率となっている。


ということで、optunaで探索パラメータを最適化したい方は、パラメータに応じた変更が必要ですが、ご自由にどうぞ。

optunaを使ってみる

昨日試したhyperoptと同じことをoptunaで試してみた。

探索する関数の形

hyperoptで試したものと同じ、2つの説明変数で、極大値が複数ある関数

Z = 10**(-(X-0.1)**2)*10**(-Y**2)*np.sin(5*X)*np.sin(3*Y)

f:id:TadaoYamaoka:20181217233040p:plain

optunaによる最適化

optunaでは以下のようにしてパラメータの最適化を行う。

import numpy as np
import optuna

def objective(trial):
    x = trial.suggest_uniform('x', -1, 1)
    y = trial.suggest_uniform('y', -1, 1)
    return -10**(-(x-0.1)**2)*10**(-y**2)*np.sin(5*x)*np.sin(3*y)

study = optuna.create_study()
study.optimize(objective, n_trials=100)

結果は、以下のように出力される。

[I 2018-12-18 22:22:38,234] Finished a trial resulted in value: -0.26608404873345176. Current best value is -0.26608404873345176 with parameters: {'x': -0.4372236826385516, 'y': -0.42153119135705275}.
(略)
[I 2018-12-18 22:22:39,082] Finished a trial resulted in value: 0.23048800674448708. Current best value is -0.5864926930433219 with parameters: {'x': 0.284010530868015, 'y': 0.3078011551597313}.

試行1回に付きログが1行出力される。

hyperoptと比較するため結果を並べてみる。

hyperopt
試行回数 x y
20 0.39556414532440365 0.20875085952399286
40 0.3202657168213312 0.42527827006887997
60 -0.24148461859260986 -0.36080178232006715
80 0.37880403950167435 0.29472084021122946
100 0.2611536044253473 0.39531968048635835
optuna
試行回数 x y
20 -0.4372236826385516 -0.42153119135705275
40 0.2758835033984871 0.18587125262622836
60 0.284010530868015 0.3078011551597313
80 0.284010530868015 0.3078011551597313
100 0.284010530868015 0.3078011551597313

optunaの方が少ない試行で近づいている。正解はx = 0.28、y = 0.36付近だが、近い値になっている。

収束傾向

optunaは一度収束するとそれ以降同じ値になっている。
hyperoptの結果は、max_evalsを変更して測定し直しているので、途中で収束しているかは確認できていない。
そこで、Trialsを使って途中経過を確認してみた。

from hyperopt import Trials
trials = Trials()
fmin(objective,
    space,
    algo=tpe.suggest,
    max_evals=100,
    trials=trials)

trials.miscsに途中結果が保存されるので、途中結果を確認してみた。

試行回数 x y
20 0.5420640699743933 0.5315515733903343
40 -0.5098469891667613 -0.438424969493291
60 0.03469969785010063 -0.040788916295743854
80 0.608071890102492 0.006584448401573517
100 0.30918481652031465 -0.1562913234242519

hyperoptは途中で収束することなく、新しい値を探索するようである。
試行回数が多いと正解に近づく可能性はあるが、optunaよりも収束が遅い傾向がありそうである。
逆に、optunaは収束すると新しい値は探索しないので、探索空間を狭めてさらに探索するとよさそうである。

追試

単純な関数でも試してみた。

Z = 10**(-(X-0.5)**2)*10**(-(Y-0.2)**2)

f:id:TadaoYamaoka:20181217234418p:plain
この関数は、x=0.5、y=0.2で最大となる。

hyperopt
試行回数 x y
20 0.3443597966317844 0.23786682969927742
40 0.38271437610741593 0.14961016637448996
60 0.49909182531107105 0.16338851488579612
80 0.47974941506728047 0.16023365584782412
100 0.5242348907635725 0.19484332282113948
optuna
試行回数 x y
20 0.45310859794747144 -0.04581641718039608
40 0.5736411512986137 0.28240726860174903
60 0.4378516591044338 0.21435567472136136
80 0.4378516591044338 0.21435567472136136
100 0.4378516591044338 0.21435567472136136

1つ目の関数と同じ傾向で、optunaは早く正解に近づいている。

hyperoptを使ってみる

ほぼ自分用のメモです。

前回ベイズ最適化で探索パラメータの最適化を試したが、ぽんぽこなどが使っているhyperoptも試してみた。

最適化する目的変数

テスト用に、説明変数が2つで、極大値が複数ある少々複雑な関数を用意した。

Z = 10**(-(X-0.1)**2)*10**(-Y**2)*np.sin(5*X)*np.sin(3*Y)

グラフにすると、以下のような形になる。

from mpl_toolkits.mplot3d import Axes3D

import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter
import numpy as np

fig = plt.figure()
ax = fig.gca(projection='3d')

X = np.arange(-1, 1, 0.01)
Y = np.arange(-1, 1, 0.01)
X, Y = np.meshgrid(X, Y)
Z = 10**(-(X-0.1)**2)*10**(-Y**2)*np.sin(5*X)*np.sin(3*Y)

surf = ax.plot_surface(X, Y, Z, cmap=cm.coolwarm,
                       linewidth=0, antialiased=False)

ax.set_zlim(-1.0, 1.0)
ax.zaxis.set_major_locator(LinearLocator(10))
ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))

fig.colorbar(surf, shrink=0.5, aspect=5)

plt.show()

f:id:TadaoYamaoka:20181217233040p:plain

この関数は、x = 0.28、y = 0.36付近で最大となる。

Z.argmax() # 27328
X.flatten()[27328] # 0.28000000000000114
Y.flatten()[27328] # 0.3600000000000012

hyperoptで最適化

hyperoptでは、以下のようにして目的変数が最大となるパラメータの探索を行う。

from hyperopt import fmin, tpe, hp

def objective(args):
    x, y = args
    return -10**(-(x-0.1)**2)*10**(-y**2)*np.sin(5*x)*np.sin(3*y)

space = [hp.uniform('x', -1, 1), hp.uniform('y', -1, 1)]

best = fmin(objective,
    space,
    algo=tpe.suggest,
    max_evals=100)

print(best)

hyperoptは最小値を探索するので、objectiveのreturnをマイナスにしている。

探索空間は、spaceにx、yそれぞれ[-1, 1]の範囲と定義している。hp.uniformは、一様ランダムにサンプリングした点から探索を行うことを意味する。

実際の探索の処理は、fminで行っている。引数のalgoにtpe.suggestを指定することで、「Tree of Parzen Estimators (TPE)」というアルゴリズムで探索が行われる(TPEがGPと比べてどうなのかといった理論的な話はよくわかっていない)。

実行結果

max_evalsを20ずつ増やしていった結果は、以下のようになった。

max_evals x y
20 0.39556414532440365 0.20875085952399286
40 0.3202657168213312 0.42527827006887997
60 -0.24148461859260986 -0.36080178232006715
80 0.37880403950167435 0.29472084021122946
100 0.2611536044253473 0.39531968048635835

100の試行でだいたい正解に近づいている。

追試

もっと単純な関数でも試してみた。

Z = 10**(-(X-0.5)**2)*10**(-(Y-0.2)**2)

f:id:TadaoYamaoka:20181217234418p:plain
この関数は、x=0.5、y=0.2で最大となる。

max_evalsを20ずつ増やしていった結果は、以下のようになった。

max_evals x y
20 0.3443597966317844 0.23786682969927742
40 0.38271437610741593 0.14961016637448996
60 0.49909182531107105 0.16338851488579612
80 0.47974941506728047 0.16023365584782412
100 0.5242348907635725 0.19484332282113948

今度は、60回でだいたい正解に近づいている。

hyperoptでは、サンプリングの手法としてuniform以外にも正規分布に基づくnormなども指定できるので、大雑把に調べてから正規分布で調べた方が効率的に探索できるかもしれない。

追試2

比較のため、ランダムサーチでも測定してみた。

from hyperopt import rand

best = fmin(objective,
    space,
    algo=rand.suggest,
    max_evals=100)
1つ目の関数
max_evals x y
20 0.3385365523415862 0.07397186373359554
40 -0.23970049764740975 -0.33954484250904127
60 0.27495652472338383 0.3548873522022511
80 0.28471503714642177 0.563064953585914
100 0.35214511978226337 0.2684189656435647
2つ目の関数
max_evals x y
20 0.3742602732502782 0.31675423601984165
40 0.6009004093795294 0.14050923510882107
60 0.716162366548053 0.11912978009135555
80 0.34036595940517334 0.3499254601398716
100 0.5909165567905033 0.022329294838111302

1つめの関数の60回で正解に近くなっているが、それ以外は近づいていない。
2つ目の関数も正解に近づいていない。
実験した2つの関数では、TPEの方が効率的に探索できていると言える。

PUCTの定数のベイズ最適化

AlphaZeroの論文では、PUCTの定数C_{PUCT}を以下の式で、親ノードの訪問回数に応じて動的に調整を行っている。

U(s,a)=C(s)P(s,a)\frac{\sqrt{N(s)}}{1+N(s,a)} \\
C(s)=\log{\frac{1+N(s) + C_{base}}{C_{base}}}+C_{init}
この式で現れる定数C_{base}C_{init}は、疑似コードでは以下のように定義されている。

    # UCB formula
    self.pb_c_base = 19652
    self.pb_c_init = 1.25

私が実験しているdlshogiでも上記の式を適用することで効果があるか試してみた。

報酬によるスケーリング

AlphaZeroは、報酬(負けと勝ち)を-1、1で与えているが、dlshogiでは、0、1で与えているため、スケールを変更する必要がある。
定数C_{PUCT}の理論値は、報酬が[-1,1]の場合は2で、[0,1]の場合は1となる。
したがって、およそ半分になるように調整すればよい。

測定結果

以下の通り、手動でいくつかの組み合わせで、GPSfishと1手3秒で100回対局した勝率を測定した。

c_base c_init 勝率
変更前(C_PUCT=1) 54%
25000 0.8 65%
20000 0.7 62%
30000 0.85 56%
20000 0.8 57%
30000 0.8 66%

考察

いずれの組み合わせでも、変更前のC_{PUCT}が固定値1の場合より、勝率が高くなっている。
勝率が最大の組み合わせは、c_base=30000、c_init=0.8であった。
C(s)をグラフにすると以下のようになる。
f:id:TadaoYamaoka:20181213001907p:plain

自己対局でモデルの学習を長時間行っても勝率は少しずつしか変わらなかったが、探索のパラメータを変えるだけで10%以上勝率が上がることがわかった。
今までは探索パラメータを手動で適当に決めていたが、強くするには探索のパラメータの調整も重要であると気づかされた。

そこで、ベイズ最適化を使って、探索パラメータをちゃんと調整することにした。

ベイズ最適化

ベイズ最適化を使うことで、既知の説明変数と目的変数から、確率的に目的変数が最大となる説明変数の組み合わせ候補を求めることができる。
ベイズ最適化の理論を正確に理解して実装するのは大変なので、こちらのページにあったPythonのコードを使用させてもらった。

以下のようにして、次の実験候補を求めることができる。

import numpy as np

from bayesianoptimization import bayesianoptimization

X = np.random.rand(100, 2) * [1, 3] + [0.5, 1]
X_train = np.array([[0.8, 2.5], [0.7, 2.0], [0.85, 3.0], [0.8, 2.0], [0.8, 3.0]])
y_train = np.array([0.65, 0.62, 0.56, 0.57, 0.66])

selected_candidate_number, selected_X_candidate, cumulative_variance = bayesianoptimization(X_train, y_train, X, 2)

selected_X_candidateに説明変数の候補が格納される。

array([0.70598003, 2.04039803])

候補となった条件で、実験を行い、その結果を追加して、同様に次の候補を求めて実験すればよい。

変数の正規化

c_baseの値の桁がc_initに比べて大きいため、適切に正規化しておく必要がある。
上記のコードでは、c_baseを1/10000にしている。勝率も%ではなく、[0,1]の範囲で与えている。
Xは、ランダムにサンプリングした仮想的な候補だが、サンプリングの範囲は、c_baseを[1.0,4.0]、c_initを[0.5,1.5]の範囲でサンプリングしている。

ベイズ最適化で求めた候補の実験結果

実験した結果は以下の通りとなった。

c_base c_init 勝率
20403.9803 0.70598003 59%

予測はしていたが、1回の実験では最大にはならなかった。
そもそも100回の対局では誤差も大きく、確率的な事象であるため、1回で最適な値は求まらない。

繰り返し実験を行う必要があるが、今後は自動的に調整できるような仕組みを構築したい。

AlphaZeroの論文

Science誌に掲載された論文は、新しい対局条件での結果と棋譜の公開がメインで技術的な内容は、昨年のarXivで公開された論文とほとんど差分はありませんでした。
DeepMindのページのリンクからダウンロードできるOpen Access versionのMethodsでは、技術的な内容が追加されており、興味深い内容も記載されていました。

Prior Work on Computer Chess and Shogi

TD Gammonwasについて追加されています。

Domain Knowledge

引き分けの条件がチェスと将棋では512手、囲碁では722手であることが追加されています。
定跡と終盤データベースを使用していないことが追加されています。

Search

MCTSアルゴリズムの説明が追加されています。
以前のAlphaGoからPUCTの定数が変更されており、親ノードの訪問回数応じた関数になっていました。

以前

U(s,a)=C_{PUCT}P(s,a)\frac{\sqrt{N(s)}}{1+N(s,a)}

今回

U(s,a)=C(s)P(s,a)\frac{\sqrt{N(s)}}{1+N(s,a)} \\
C(s)=\log{\frac{1+N(s) + C_{base}}{C_{base}}}+C_{init}

C(s)は探索の割合(exploration rate)で、探索の時間とともにゆっくり増加し、探索が高速な場合は、本質的には一定であると説明されています。

定数の値は、公開された疑似コードをみれば分かります。(Additional FilesのData S1に含まれるpseudocode.py)

    # UCB formula
    self.pb_c_base = 19652
    self.pb_c_init = 1.25

と定義されています。

グラフにすると以下のようになります。
f:id:TadaoYamaoka:20181208181048p:plain

訪問回数が少ない内は探索結果を重視して、訪問回数が増えるとボーナス項の重みが大きくなり、未知の手が選択されやすくなると解釈できます。

Representation

ポリシーの予測した手について、合法でない手は確率を0にして、残りの合法手で再正規化するという説明が追加されています。

Architecture

ネットワーク構成の説明が追加されています。

  • AlphaGo Zeroでは、ポリシーの出力に全結合層が使われていましたが、チェスと将棋では、畳み込み層であるという説明が追加(ポリシーの出力層の畳み込みのカーネルサイズについては記載されていません。)

チェスと将棋でポリシーの出力が畳み込み層になっていることから、AlphaGo Zeroでポリシーの出力が全結合層になっていた理由は、パスを表現するためであったと解釈できます。(個人的に知りたかった内容でした。)

バリューの出力層は、1フィルター、カーネルサイズ1×1の畳み込み層の後に、256ユニットの全結合層と1ユニットの全結合層で、tanhで出力しています。
チェスと将棋でも、畳み込み層は、1フィルターになっていました。
駒の種類が多いため、フィルタ数は多くしていると予測していましたが、そうではなかったようです。
バリューに関してはフィルターを1つにして、重みを共有した方がよいのかもしれません。

Configuration

学習率について詳細が追加されています。
学習率を0.2から0.02、0.002、0.0002に落とす条件が、チェスや将棋の場合は100、300、500,000ステップ後で、囲碁の場合は0、300、500,000ステップ後という条件が追記されています。

Opponents

  • 対局相手のソフトの条件の記載が追加

定跡の仕様有無について一部で疑問が上がっていましたが、将棋は、やねうら王の標準定跡を使用していることが記載されています。
ハードウェアなどの詳細な条件が記載されていましたが割愛します。

Match conditions

対局条件について内容が追加されています。
初期局面から対局を行い、将棋の場合は2017年WCSCと同じ持ち時間10分、1手10秒加算の条件です。
AlphaZeroの時間制御は、単純に残り時間の1/20のようです。
ポンダーは、思考時間の評価を行うため、双方未使用の設定です。
投了の条件は、elmoは-4500が10回連続の場合、AlphaZeroはバリューが-0.9になった場合です。
1,000試合で勝率を評価しています。

Elo ratings

Eloレーティングの測定方法について内容が追加されています。
1手1秒の対局結果から評価したことが書かれています。

感想

ポリシーの出力層の構成が知りたかったことの一つだったので、有益な情報が得られたと思います。
PUCTの定数が訪問回数による関数に代わっていたことは、dlshogiでもどれくらい効果があるか試そうと思います。
疑似コードがPythonで実装されていたので、AlphaZeroもPythonで実装されているのか気になりました。
まだ全部読めていませんが、疑似コードを読むと自己対局から訓練の流れや、データの格納方法とかが推測できて楽しめます。

2018/12/9 追記

疑似コードを見ると、学習のハイパーパラメータがわかります。
自己対局して学習に使う局面数は、700,000ステップ(1ステップは4,096局面)と記載されていましたが、過去何局からサンプリングしているかは記載されていませんでした。(AlphaGo Zeroの論文には、most recent 500,000 gamesと記載されていました。)
疑似コードには、

    ### Training
    self.training_steps = int(700e3)
    self.checkpoint_interval = int(1e3)
    self.window_size = int(1e6)
    self.batch_size = 4096

と定義されており、ReplayBuffer.save_gameで、保存するゲームの数の上限がwindow_sizeになっていることから、直近1,000,000局からサンプリングしていることがわかります。

タスクマネージャーのメモリ使用量

将棋AIの学習のためにGPUを増やしたところ、まったく速度が上がらないどころか低下するという事象が起きて、ここ数日原因を調べていました。
メモリスワップが起きていたことが原因で、わかってしまえば単純な話でしたが、Windowsのタスクマネージャーの仕様の理解が不足していたため、数日悩むことになりました。

物理メモリを食いつぶしていたので、Windows 10のタスクマネージャーの詳細タブでプロセスのメモリ使用量を見ると、プロセスを起動してから、メモリ(プライベートワーキングセット)の数字が徐々に上がっていたため、メモリリークを起こしているのではないかと疑いました。

プライベートワーキングセットとは

実は、タスクマネージャー表示されるメモリ(プライベートワーキングセット)は、プロセスに割りあてられたページの物理メモリのサイズを示しており、C++のnewで確保したメモリは、仮想メモリが割り当てられるだけで、実際にアクセスするまではOSに物理メモリが割り当てられないということが今回調べてわかりました。
詳しい人によっては、常識レベルの話で、自分も仮想メモリの動作は知識としては知っていたはずですが、タスクマネージャーの数値の意味を理解していませんでした。

なお、仮想メモリのサイズを見るにはタスクマネージャーで、列の選択で「コミット サイズ」を表示する必要があります。


f:id:TadaoYamaoka:20181121210733p:plain


ということで、徐々に上がる=メモリリークと勘違いしたため、コードを変えたりして原因を探してもリークしている箇所が見つからず数日時間を浪費しましたorz。
しかし、おかげでタスクマネージャーと仮想メモリについて理解が深まったので良いとします。

プログラムの改良

自己対局プログラムは、1つのGPUごとにエージェント複数割り当てて、それぞれのエージェントに別々のハッシュを割り当てていたので、メモリが大量に必要となる構成になっていました。
以前に、エージェントを全て別スレッドで動かしていたときの構成を引きずって、そのような構成になっていましたが、現在はシングルスレッドで直列に動かしているため、ハッシュを共有することで、メモリ使用量を減らすように修正しました。
メモリスワップが起こらないサイズに調整したことで、安定した速度で自己対局が行えるようになりました。