読み書きプログラミング

日常のプログラミングで気づいたことを綴っています

AndroidでTensorFlow Lite

Android版「囲碁の師匠」の検討の着手をしました。

最初のマイルストーンは、色々なAndroid用SoCで搭載されつつあるNPUをニューラルネットワークの評価に利用できるかどうかです。
Kirin 990搭載のMatePad Proを購入して挑戦しています。
このページはその備忘録です。随時更新します。

  • TensorFlow LiteのC++ APIでNPUを利用するにはNNAPIを使う。このためはtflte::InterpreterのUseNNAPIメソッドでtrueを設定する。デフォルトはfalseなので明示的に呼ぶ必要あり。

システムログを見る

Androidのシステムログを見るコマンドは、リモートのパソコンにて、

adb logcat

NNAPIのログを出力するには、Android側で事前に

setprop debug.nn.vlog 1

引き数1は色々なオプションあり。詳しくはNeural Networks API  |  Android NDK  |  Android Developersを参照してください。

システムログからTensorFlow Liteに関係するものを抽出するにはキーワードでフィルターする。
tflite, Manager, AndroidNN, ModelBuilder, TypeManager, Memory, GraphDump, HiTraceC, CompilationBuilder, ExecutionPlan, HwHiShowManagerService, DPMS_DeviceStatusMonitorという感じ。

ノウハウ

  • NNAPIを使うには明示的にInterpreterインストタンスのUserNNAPI(true)をコールする。
  • ARM GPUを使うにはTfLiteGpuDelegateV2Createを使う方法とArm NNを使う方法の2通りあるらしい。
  • float16モデルはNNAPIは(他のドライバは動かず)nnapi-referenceにフォールバックする。
  • float32モデルでSetAllowFp16PrecisionForFp32(true)ならliteadapter(NPUドライバと思われる)が動くが、出力が複数あるとCPUにフォールバックするパターンがある。また初回出力がおかしい。
  • float32モデルでSetAllowFp16PrecisionForFp32(false)だとarmnnにフォールバックする。

結論として、float32で出力が1つのモデルは、初回推論以外NPUで正しく計算される。

iPadOSでポートレートとランドスケープで異なるレイアウトを採用する

iPadのサイズクラスはポートレートでもランドスケープでもwR hRなのでサイズクラスでは表題の件を実現できません。
これについてググると、traitCollectionをoverrideする方法が出てきます。wR hRのサイズクラスを縦横比に従ってwC hRもしくはhCに変えてしまおうというアイデアです。
(wR hCではなくhCなのは、wC hCがiPhone 4などコンパクトな機種でのランドスケープだからです)

ところが、iOS 13/iPadOS 13ではtraitCollectionのoverrideがnot supportedになりました。
なんとかしないといけません。

色々方法はありますが、拡張性を重視して以下の方法を採用しました。

  1. wC hR, hCについてストーリボードで制約を設計
  2. wR hRについてポートレートランドスケープそれぞれについてストーリボードで制約を設計。両方installedにすると衝突するので、メインと思われる片方のみinstalledにする。
  3. ルートビューコントローラにisPortraitというインスタンス変数を用意する
  4. ルートビューコントローラのupdateVIewConstraintsをoverrideしてwR hRの場合、isPortraitに応じて2で設計した制約のどちらかだけをactivateする。(wR hRでない場合には両方deactivateする)
  5. ルートビューコントローラのviewWillTransition(to:with:)をoverrideしてwR hRのときの縦横比からisPortraitの条件が変わった場合、isPortraitを設定してview.setNeedsUpdateConstraintsを呼び出す

これで色々な状況に対応できました。
色々とは、念頭にある回転だけでなく、例えば、アプリがフォワグランドに復帰するときとか、スプリットビューとか。

これだけ書くと当たり前っぽい処理ですが、ここに至る前にupdateVIewConstraintsをoverrideせずにviewWillTransition(to:with:)内で制約を切り替える方法を実装して嵌りました。updateVIewConstraintsはデフォルトでは色々な状況でストーリボードの制約を読み直す処理が入っているようで、viewWillTransition(to:with:)で設定しても有効にならないケースが複数ありました。

以上、ご参考になれば幸いです。
また、これに関してもっと常識のイデオムがあれば教えていただきたく。

囲碁AIアプリの作り方4

追記(2019/02/19)
猛烈に勘違いしていました。
AlphaZeroは、バリューネットワークの出力には[-1,1]レンジを使っていますが、MCTSでは[0,1]レンジだそうです。
talkchess.com
今までのいくつかの謎が氷解しました。
これに合わせてコードのコメントとオリジナルコードのバグを修正しました。

1年前大騒ぎになったAlphaZeroの論文が正式に出版されました。
擬似コードも公開されたので、今日はそれを読みましょう。
コメントを挿入しました。

# Python2のコードでしたが、type hintを通すためにPython3に直しました。
"""Pseudocode description of the AlphaZero algorithm."""


#from __future__ import google_type_annotations
#from __future__ import division

import math
import numpy
import tensorflow as tf
from typing import List

##########################
####### Helpers ##########


class AlphaZeroConfig(object):

  def __init__(self):
    ### Self-Play

    self.num_actors = 5000
    # 5,000台のTPUを使って自己対戦をさせてます。

    self.num_sampling_moves = 30
    # 30手まではMCTSプレイアウト数に比例した確率で着手を選択します。

    self.max_moves = 512  # for chess and shogi, 722 for Go.
    # 手数が長い場合、チェス,将棋は引き分け、囲碁はTromp-Taylorルールスコアに従います。
    # なので囲碁の場合、無勝負形はコミとアゲハマの差で決まる?それとも無勝負は別途判定?

    self.num_simulations = 800

    # Root prior exploration noise.
    self.root_dirichlet_alpha = 0.3  # for chess, 0.03 for Go and 0.15 for shogi.
    self.root_exploration_fraction = 0.25
    # ルートノードではポリシー確率に事実上1点ランダムな着手を確率0.25増しにしてMCTSを行います。
    # 事実上というのはalphaがこのように1より小さい場合原点付近の非常に鋭利な関数になるからです。
    # AlphaGo Leeが第4局で負けた教訓で、ポリシー確率の低い手を打たれてもMCTSがそれを咎める手を探せるように訓練します。

    # UCB formula
    self.pb_c_base = 19652
    self.pb_c_init = 1.25
    # AlphaZeroではCpuctがプレイアウト数に依存する関数になりました。
    # 定数だとプレイアウト数がある程度大きくなるとこの項の効果がなくなってしまい、
    # プレイアウトを増やしても手があまり変わらなくなる性質を抑制するためと思われます。

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

    self.weight_decay = 1e-4
    self.momentum = 0.9
    # Schedule for chess and shogi, Go starts at 2e-2 immediately.
    self.learning_rate_schedule = {
        0: 2e-1,
        100e3: 2e-2,
        300e3: 2e-3,
        500e3: 2e-4
    }


class Node(object):

  def __init__(self, prior: float):
    self.visit_count = 0
    self.to_play = -1
    self.prior = prior
    self.value_sum = 0
    self.children = {}

  def expanded(self):
    return len(self.children) > 0

  def value(self):
    if self.visit_count == 0:
      return 0
    return self.value_sum / self.visit_count


class Game(object):

  def __init__(self, history=None):
    self.history = history or []
    self.child_visits = []
    self.num_actions = 4672  # action space size for chess; 11259 for shogi, 362 for Go

  def terminal(self):
    # Game specific termination rules.
    pass

  def terminal_value(self, to_play):
    # Game specific value.
    pass

  def legal_actions(self):
    # Game specific calculation of legal actions.
    return []

  def clone(self):
    return Game(list(self.history))

  def apply(self, action):
    self.history.append(action)

  def store_search_statistics(self, root):
    sum_visits = sum(child.visit_count for child in root.children.values())
    self.child_visits.append([
        root.children[a].visit_count / sum_visits if a in root.children else 0
        for a in range(self.num_actions)
    ])

  def make_image(self, state_index: int):
    # Game specific feature planes.
    return []

  def make_target(self, state_index: int):
    return (self.terminal_value(state_index % 2),
            self.child_visits[state_index])

  def to_play(self):
    return len(self.history) % 2


class ReplayBuffer(object):

  def __init__(self, config: AlphaZeroConfig):
    self.window_size = config.window_size
    self.batch_size = config.batch_size
    self.buffer = []

  def save_game(self, game):
    if len(self.buffer) > self.window_size:
      self.buffer.pop(0)
    self.buffer.append(game)

  def sample_batch(self):
    # Sample uniformly across positions.
    move_sum = float(sum(len(g.history) for g in self.buffer))
    games = numpy.random.choice(
        self.buffer,
        size=self.batch_size,
        p=[len(g.history) / move_sum for g in self.buffer])
    game_pos = [(g, numpy.random.randint(len(g.history))) for g in games]
    return [(g.make_image(i), g.make_target(i)) for (g, i) in game_pos]


class Network(object):

  def inference(self, image):
    return (-1, {})  # Value, Policy

  """
  http://talkchess.com/forum3/viewtopic.php?f=2&t=69175&start=70&sid=8eb37b9c943011e51c0c3a88b427b745
  matthewlai san said,
  "All the values in the search are [0, 1].
  We store them as [-1, 1] only for network training, to have training targets centered around 0.
  At play time, when network evaluations come back, we shift them to [0, 1] before doing anything with them.
  Yes, all values are initialized to loss value."
  """
  def inference_0to1value(self, image):
    value, policy = self.inference(image)
    value = (value + 1) / 2
    return value, policy

  def get_weights(self):
    # Returns the weights of this network.
    return []


class SharedStorage(object):

  def __init__(self):
    self._networks = {}

  def latest_network(self) -> Network:
    if self._networks:
      return self._networks[max(self._networks.keys())]
    else:
      return make_uniform_network()  # policy -> uniform, value -> 0.5

  def save_network(self, step: int, network: Network):
    self._networks[step] = network


##### End Helpers ########
##########################


# AlphaZero training is split into two independent parts: Network training and
# self-play data generation.
# These two parts only communicate by transferring the latest network checkpoint
# from the training to the self-play, and the finished games from the self-play
# to the training.
def alphazero(config: AlphaZeroConfig):
  storage = SharedStorage()
  replay_buffer = ReplayBuffer(config)

  for i in range(config.num_actors):
    launch_job(run_selfplay, config, storage, replay_buffer)

  train_network(config, storage, replay_buffer)

  return storage.latest_network()


##################################
####### Part 1: Self-Play ########


# Each self-play job is independent of all others; it takes the latest network
# snapshot, produces a game and makes it available to the training job by
# writing it to a shared replay buffer.
def run_selfplay(config: AlphaZeroConfig, storage: SharedStorage,
                 replay_buffer: ReplayBuffer):
  while True:
    network = storage.latest_network()
    game = play_game(config, network)
    replay_buffer.save_game(game)


# Each game is produced by starting at the initial board position, then
# repeatedly executing a Monte Carlo Tree Search to generate moves until the end
# of the game is reached.
def play_game(config: AlphaZeroConfig, network: Network):
  game = Game()
  while not game.terminal() and len(game.history) < config.max_moves:
    action, root = run_mcts(config, game, network)
    game.apply(action)
    game.store_search_statistics(root)
  return game


# Core Monte Carlo Tree Search algorithm.
# To decide on an action, we run N simulations, always starting at the root of
# the search tree and traversing the tree according to the UCB formula until we
# reach a leaf node.
def run_mcts(config: AlphaZeroConfig, game: Game, network: Network):
  root = Node(0)
  evaluate(root, game, network)
  add_exploration_noise(config, root)

  for _ in range(config.num_simulations):
    node = root
    scratch_game = game.clone()
    search_path = [node]

    while node.expanded():
      action, node = select_child(config, node)
      scratch_game.apply(action)
      search_path.append(node)

    value = evaluate(node, scratch_game, network)
    backpropagate(search_path, value, scratch_game.to_play())
    # 論文ではエッジを更新するアルゴリズムですが、ノードを更新しています。
    # 結果として論文ではWは親ノードのバリューでの初期化、コードではW=0と違ったアルゴリズムを実装しています。
    # WはMCTS内部では[-1,1]ではなく[0,1]です。なのですべてのエッジは勝率0のバリューで初期化されます。

  return select_action(config, game, root), root


def select_action(config: AlphaZeroConfig, game: Game, root: Node):
  visit_counts = [(child.visit_count, action)
                  for action, child in root.children.items()]
  if len(game.history) < config.num_sampling_moves:
    _, action = softmax_sample(visit_counts)
  else:
    _, action = max(visit_counts)
  return action


# Select the child with the highest UCB score.
def select_child(config: AlphaZeroConfig, node: Node):
  _, action, child = max((ucb_score(config, node, child), action, child)
                         for action, child in node.children.items())
  return action, child


# The score for a node is based on its value, plus an exploration bonus based on
# the prior.
def ucb_score(config: AlphaZeroConfig, parent: Node, child: Node):
  pb_c = math.log((parent.visit_count + config.pb_c_base + 1) /
                  config.pb_c_base) + config.pb_c_init
  pb_c *= math.sqrt(parent.visit_count) / (child.visit_count + 1)

  prior_score = pb_c * child.prior
  value_score = child.value()
  return prior_score + value_score


# We use the neural network to obtain a value and policy prediction.
def evaluate(node: Node, game: Game, network: Network):
  value, policy_logits = network.inference_0to1value(game.make_image(-1))

  # Expand the node.
  node.to_play = game.to_play()
  policy = {a: math.exp(policy_logits[a]) for a in game.legal_actions()}
  policy_sum = sum(policy.values())
  for action, p in policy.items():
    node.children[action] = Node(p / policy_sum)
  return value


# At the end of a simulation, we propagate the evaluation all the way up the
# tree to the root.
def backpropagate(search_path: List[Node], value: float, to_play):
  for node in search_path:
    node.value_sum += value if node.to_play == to_play else (1 - value)
    node.visit_count += 1


# At the start of each search, we add dirichlet noise to the prior of the root
# to encourage the search to explore new actions.
def add_exploration_noise(config: AlphaZeroConfig, node: Node):
  actions = node.children.keys()
  noise = numpy.random.gamma(config.root_dirichlet_alpha, 1, len(actions))
  frac = config.root_exploration_fraction
  for a, n in zip(actions, noise):
    node.children[a].prior = node.children[a].prior * (1 - frac) + n * frac


######### End Self-Play ##########
##################################

##################################
####### Part 2: Training #########


def train_network(config: AlphaZeroConfig, storage: SharedStorage,
                  replay_buffer: ReplayBuffer):
  network = Network()
  optimizer = tf.train.MomentumOptimizer(config.learning_rate_schedule,
                                         config.momentum)
  for i in range(config.training_steps):
    if i % config.checkpoint_interval == 0:
      storage.save_network(i, network)
    batch = replay_buffer.sample_batch()
    update_weights(optimizer, network, batch, config.weight_decay)
  storage.save_network(config.training_steps, network)


def update_weights(optimizer: tf.train.Optimizer, network: Network, batch,
                   weight_decay: float):
  loss = 0
  for image, (target_value, target_policy) in batch:
    value, policy_logits = network.inference(image)
    loss += (
        tf.losses.mean_squared_error(value, target_value) +
        tf.nn.softmax_cross_entropy_with_logits(
            logits=policy_logits, labels=target_policy))

  for weights in network.get_weights():
    loss += weight_decay * tf.nn.l2_loss(weights)

  optimizer.minimize(loss)


######### End Training ###########
##################################

################################################################################
############################# End of pseudocode ################################
################################################################################


# Stubs to make the typechecker happy, should not be included in pseudocode
# for the paper.
def softmax_sample(d):
  return 0, 0


def launch_job(f, *args):
  f(*args)


def make_uniform_network():
  return Network()

囲碁AIアプリの作り方4

今日は碁盤描画周りです。

間口の一番広いウェブアプリを対象にします。
JavaScriptの碁盤描画のライブラリをまずご紹介します。

  • eidogo: HTML5以前では事実上のデファクトスタンダードだったオープンソースのウェブアプリです。DOMで描画します。耳赤サイトでも現役で活躍しています。
  • jGoBoard: canvasを使った美しい碁盤ライブラリです。ついー碁や慎始(どちらも私の自作ウェブアプリです)で利用させていただいています。AZ.jsの碁盤もこれです。有料の商用の場合、ライセンスの確認が必要です。
  • WGo.js: 同じくcanvasを使った碁盤ライブラリです。AlphaGo Teachで使われています。実装に特徴があって市松模様上に描画したcanvasを2枚重ねることで長いラベルにも対応しています。
  • Sabaki: Electronを使った碁盤アプリです。ElectronなのでJavaScriptで書かれておりウェブアプリ版もあります。Leela Zeroにも対応し、今も積極的に開発が続いているこころ強いソフトです。DOMで描画しています。

個人的にはHTML5が安定した現在にあっても碁盤描画はcanvas描画ではなくDOMで構成するのが筋がいいと思っています。
ですが、ちょっと遊ぼうと思うとcanvasが自由ですね。

碁盤になる自家製描画ライブラリをご紹介します。
まずはどんなものかライブラリを使ったウェブアプリから。

https://new3rs.github.io/allelo-intro/index.html

(Google Chromeで見てください。Safariだと肝心な部分の描画が欠けます。)

描画に思いっきり特長があるので、あえて囲碁と呼ばずに「アレロ」と呼んでいます。
わかる人にわかると思いますが、ルールは囲碁のTromp-Taylerルールと同値です。Tromp-Taylerルールは純碁から地埋めをなくしたルールでわかれば一番シンプルなのですが、文章で読むと難しい感じがします。
そういう理解のための障壁をなくすため、出来うる限り可視化に努めました。

石と石がくっついたり、呼吸点がなくなったりする様子がわかりますし、なぜ石が盤上から消えるのかもうまく説明できたと思っています。「アレロ」はこれから色々展開していく予定です。乞うご期待。

さて、このウェブアプリの描画ライブラリが、allelo-boardです。

github.com

コードはまだ整理されて居ないのですが、HTML template, customElement, WebGLSVGHTML5の次の技術を駆使しています^^
描画の実装のご参考になれば幸いです。

(商標やデザインなど今後権利化していく可能性があります。)

今日はこの辺で。

囲碁AIアプリの作り方3

今日はちょっと変わったところで、SGFファイルを扱う方法です。

SGFというのは、様々なプラットフォーム上で販売されているSmartGoというアプリケーションのファイルフォーマットです。Smart Game Formatの略で、囲碁以外のボードゲームにも対応可能なフォーマットです。
囲碁のファイルフォーマットはアプリケーション毎に他にも色々ありますが、仕様がドキュメント化されているこのSGFが事実上標準フォーマットという感じです。

テキストフォーマットなので中身を見たことのある人も多いかと思います。こんな感じ。

(;FF[4]GM[1]CA[UTF-8]AP[囲碁の師匠:1.1.2]SZ[19]HA[2]AB[pd][dp];W[dd])

見ただけでなんとなく意味もわかる、わかりやすいフォーマットですね。

こういうフォーマットを扱うには、扱うプログラムを直接書くのではなく、パーサジェネレータというものを使うと楽ができます。
パーサジェネレータというのは、フォーマットの規則を与えると、その規則に則ってフォーマットに従った文字列からデータ構造を抽出するコード(パーサ)を生成してくれるものです。

パーサジェネレータを使って作った自作のパーサをいくつかご紹介します。

  • jssgf: JavaScript用SGFパーサ。Jisonというパーサジェネレータを使いました。
  • rust-sgf: Rust用SGFパーサ。rust-pegというパーサジェネレータを使いました。
  • swift-SGF: Swift用SGFパーサ。citronというパーサジェネレータを使いました。


なんでいくつも作っているのかというと、それだけ作るのが簡単だからです^^

jssgfを作った時のSGFフォーマットを記述を見てみましょう。

/lex

%% /* language grammar */

output
	: collection EOF
        { return $1; }
	;

collection
	: gametree
		{ $$ = [$1]; }
	| collection gametree
		{ $1.push($2); $$ = $1; }
    ;

gametree
    : '(' sequence ')'
		{ $$ = $2; }
    | '(' sequence gametrees ')'
        { $$ = addGameTrees($2, $3); }
    ;

gametrees
	: gametree
		{ $$ = [$1]; }
	| gametrees gametree
		{ $1.push($2); $$ = $1; }
	;

sequence
	: node
		{ $$ = $1; }
	| node sequence
		{ $1._children.push($2); $$ = $1; }
	;

node
	: ';'
		{ $$ = {_children: []}; }
	| node property
		{
            if (typeof $1[$2[0]] !== 'undefined') {
                if (strict) {
                    throw new Error('double properties');
                }
            } else {
                $1[$2[0]] = $2[1];
                $$ = $1;
            }
        }
	;

property
    : PROPIDENT propvalues
        { $$ = [$1, $2]; }
    ;

propvalues
    : propvalue
		{ $$ = $1; }
	| propvalues propvalue
		{ var a; if ($1 instanceof Array) a = $1; else a = [$1]; $$ = a.concat($2); }
	;

propvalue
    :  /* empty */
        { $$ = ''; }
	| CVALUETYPE
		{ $$ = decodeValue($1); }
	;

%%

こんな風に、コレクションはどういうもの?ゲームツリーはどういうもの?シークエンスはどういうもの?ノードはどういうもの?という規則を書くと、JavaScriptで書かれたパーサを生成してくれます。

実はこの規則、SGFの仕様書に書かれたものをほぼ書き写したものです。以下はSGFの仕様書より。

Collection = GameTree { GameTree }
GameTree   = "(" Sequence { GameTree } ")"
Sequence   = Node { Node }
Node       = ";" { Property }
Property   = PropIdent PropValue { PropValue }
PropIdent  = UcLetter { UcLetter }
PropValue  = "[" CValueType "]"
CValueType = (ValueType | Compose)
ValueType  = (None | Number | Real | Double | Color | SimpleText |
		Text | Point  | Move | Stone)

似てるでしょ?
なので、新しいプログラミング言語に取り組んだときも、その言語のパーサジェネレータがあれば、その言語用にSGFパーサを用意することは簡単なのです。

今日はこの辺で。

囲碁AIアプリの作り方2

今日はニューラルネットワーク周りのご紹介です。
前回同様、技術的な話ではないです。どういう材料が利用可能かご紹介します。

AlphaGo Zeroは、MCTSニューラルネットワーク強化学習という組み合わせで、0から人類を遥かに超えて学習するAIです。
「莫大な演算力があれば」の話ですが。

演算のほとんどはニューラルネットワーク部分に費やされます。
なので実は、前回、ご紹介したAZ.jsのメインの部分であるMCTSのコードは、ぶっちゃけそんなに頑張ってチューンする必要は最初はないのです。
探索エンジンというと、CやC++で書かないとスピードが出なくて意味がないという印象を持たれている方も居られるかも知れません。
私も以前はそうでした。
でも、AlphaGo方式が主流になって、MCTSボトルネックとは言えなくなったので、ニューラルネットワークを速く計算することがAIを強くするために重要な部分となりました。


まず、ニューラルネットワークそのものとも言えるウェイトをいかに入手するかについてです。
ウェイトというのはニューラルネットワークの中の係数データです。
性能の高いウェイトを生成するのに上記の「莫大な演算力」が必要になります。
幸いに、フリーでいくつかのウェイトが公開されているのでご紹介します

  • Leela Zero: 世界中の協力者のコンピュータを借りてAlphaGo Zeroの追実験を行っています。Hashに紐づけされているリンクをクリックするとウェイトをダウンロードすることができます。
  • ELF OpenGo: FacebookAlphaGo Zeroの追実験を行った結果が公開されています。pretrained-go-19x19-v1.binが最新版のウェイトです。
  • PhoenixGo: Tencentが作ったAlphaGo Zeroクローンです。
  • Minigo: Googleが(DeepMindとは別に)AlphaGo Zeroの追実験を行っています。リンク先はウェイトファイルの置き場なので、プロジェクト本体はgithubを参照ください。
  • AQ: 山口さんが開発された囲碁AIです。最初のAlphaGo論文ベースのものかと思います。
  • Ray: 小林さんが開発されたMCTS碁 Rayに松崎さんがニューラルネットワークを追加されたものです。

皆さん、相当のリソースを費やされているはずで、公開に感謝致します。


さて、ウェイトを入手したら今度はそれを動かしたいプラットフォーム上で動かすべくフレームワークを探します。

例えば、AZ.jsの場合、言語はJavaScriptですが、ブラウザで利用可能な演算力としてWebGL, WebGPUがあります。これは元々グラフィックス用のAPIですが、昨今のニューラルネットワークGPU上で計算されていることから容易に推測できるようにこれらのAPIを使ってJavaScriptよりも速くニューラルネットワークの計算をすることができます。

そのためのフレームワークがWebDNNです。

github.com


その他のプラットフォームの例としては、iOSなら Core ML、AndroidならNeural Networks API、パソコンの場合色々ありますが、TensorFlowがメジャーですか。


Leela ZeroのウェイトをWebDNNで動かすために変換するには、AZ.js内にスクリプトを用意しています。
Leela Zeroのウェイトは独自のテキストフォーマットなので、

  1. 独自フォーマットをTensorFlowで読み込む
  2. TensorFlowモデルをWebDNNフォーマットに変換

ということをしています。なので、TensorFlowをインストールして変換するということになります。

なんだか人のお役に立ちそうにない文章になりましたが、今日はこの辺で。

囲碁AIアプリの作り方1

1日遅れですが、一人アドベントカレンダーを始めます。
(ご協力いただける方、お声がけください)


題して「囲碁AIアプリの作り方」です。
念のため、「囲碁AIの作り方」ではありません。囲碁AI自体を作りたい方はAlphaGo論文などを読んで頑張ってください。


とは言っても、アプリのコアになるのは囲碁AIです。なので初日の今日は、お手製AI、「AZ.js」のご紹介です。
まずは以下のURLからAZ.jsで遊んでみてください。

https://new3rs.github.io/AZ.js/index.ja.html



このアプリ、ウェブアプリながら、ELF OpenGoと同じニューラルネットワークを搭載しています。時間さえ与えれば、ELF OpenGoと同じ強さを発揮するはずです。(その時間が問題なのですが)
探索アルゴリズムPyaqJavaScriptに移植したものです。


AZ.jsのコードは以下で見ることが出来ます。

github.com

中身はほぼPyaqですが、多少コードを整理してドキュメンテーションも充実させてあります。
Pyaqは自己対戦から学習するところまで含めてコードが用意されていますが、AZ.jsはエンジンのみです。
なので、ニューラルネットワークの学習部分にも取り組みたければPyaq、エンジンだけみたいという人はAZ.jsという感じでおすすめします。

拙作「囲碁の師匠」のAIエンジンもこのAZ.jsをSwiftに移植することから開発を始めました。

MCTSの部分はAlphaGo Zero論文の素直な実装です。シングルスレッド版なので、マルチスレッドにするにはスレッドの言語的な部分とともにアルゴリズム上バーチャルロスを導入する必要が出てきます。
同じアルゴリズムで並列に走らせると、スレッドが同じノードを探索してしまうのでそれを避ける工夫がバーチャルロスです。探索中の枝は仮にちょっと分が悪い枝に見せることで他のスレッドに別の枝を探索させます。


他にもAlphaGoの論文では明らかにされていない部分がいくつかあって、そういう部分を見つけて工夫してみるのも面白いと思います。
ただ工夫の検証が大変なので…(400局ぐらい対戦させて勝率何%上がるかという評価作業が必要になります)


参考に、「囲碁の師匠」では、Leela Zero界隈でFPUと呼ばれる部分に独自の工夫を入れました。
MCTSを使ったアルゴリズムは元局面がバリュー以上に悪くて一手進めると評価を落とすような局面に出会うと、候補手すべてを探索し始めます。Lizzieをお使いの方は、画面全体が勝率表示で埋まる現象を見たことがあるかと思います。
思ったよりよくないからと言って、生きている石の内側まで探索するのもなんなんで、そういう現象を抑えるようなFPUを実装しました。
意図した局面でも探索の効率を上げることが出来たので「強くなった」と思うのですが、こういう局面の効率を上げても仕方がないようで、勝率はあまり変わらないようです^^;


囲碁AIを動かしたい環境(例えばAndoroidとか)がある方は、まずAZ.jsをその環境に適した言語(AndroidならKotlinとか)に移植してみるといかもしれません。

本日はこのぐらいで。