機械学習周りのプログラミング中心。 イベント情報
ポケモンバトルAI本電子書籍通販中

第5回電竜戦TSEC指定局面戦の結果(2024/06/29)

NPO法人AI電竜戦プロジェクト主催 第5回電竜戦TSEC指定局面戦 に将棋AI「うぉっち・ざ・るーく!」で参加しました。この大会の特徴は、通常の初形からの対局開始ではなく、主催者が指定した局面から対局が開始するという点です。開始局面には、コンピュータ将棋ではあまり登場しない相振り飛車の局面も含んでいます。私は従来「ねね将棋」という名前のソフトで将棋AI大会に参加していましたが、今回は一回限りのネタソフトとして別の名前を付けました。

「うぉっち・ざ・るーく!」は、Appleが発売しているスマートウォッチであるApple Watchで動作する点が特徴で、スマートウォッチによる将棋AIの参加は大会史上初と考えられます。事前の動作検証では、floodgateのレートが3844でした。Apple Watchで将棋AI(やねうら王)を動作させるための技術的なことは過去の記事を参照してください。これに加え、評価関数をnnue-pytorchで学習させたことにより、水匠5よりも若干よい(レート+67)評価関数を得て搭載しました。

select766.hatenablog.com

結果

第1部にのみ参加しました。第2部は長時間耐久戦なのですが、未解決のメモリリークにより数時間でクラッシュすることが分かっているため不参加としました。

11勝13敗で、28チーム中23位となりました。

Etude No.3戦では、開始局面の形勢が傾いていたようで、おそらくかなり棋力に差があるEtude No.3に勝つことができました。

電竜戦 - 棋譜中継

放送では、遠山先生にCalicoCatとの対局を取り上げていただき、「うぉっち・ざ・るーく!は時計だと思ったら強豪だった」とのお褒めの言葉をいただきました。(16:07ごろ)

【2024.06.29】第5回世界将棋AI電竜戦TSEC指定局面戦 午後の部 - YouTube

電竜戦 - 棋譜中継

おわりに

パソコンで普通に対局するよりかなり不安定なので心配でしたが、何とか無事故で対局を終えることができました。さすがにマシンスペックが厳しかったですが、dlshogi *1 と比べれば1/1000未満の重さのマシンでいくつかの勝ち星を拾えただけで十分です。 大会に参加・協力していただいた皆様、ありがとうございました。

*1:NVIDIA DGX A100を使用していると仮定すると、123kg

【コンピュータ将棋】Apple Watchでやねうら王を動かす

Appleが発売しているスマートウォッチApple Watch Series 9にて、将棋AIやねうら王・ふかうら王を動作させることに成功したので報告します。

基本的には、iPhone向けのビルド方法を少し変えれば実現できました。

技術要素

以下の3要素を実現することにより、Apple Watch上で動作するやねうら王をfloodgate上で他の将棋AIと対局させることができます。

  • やねうら王をApple WatchのCPU向けにビルドし、SPM形式で出力
  • watchOSアプリを作成し、SPMを依存関係に追加し、Swift言語からやねうら王を呼び出す
  • Macで、通信仲介用サーバを実行

SPMのビルド

C++言語で実装されたやねうら王を、Apple WatchのCPU(ARMアーキテクチャ)向けにビルドし、Swift Package Manager (SPM)形式で出力します。この形式のライブラリは、Swift言語で実装されたアプリの依存関係としてロードすることができ、C言語の関数をSwift側から呼び出すことができます。

ソースコードはこのブランチにあります。

https://github.com/select766/YaneuraOuiOSSPM/tree/watchos

iPhone向けのSPMビルドに関する記事はこちら。

select766.hatenablog.com

ここからの差分について説明します。やねうら王のmain関数を除去し、Swift言語側から呼び出されて動作するための追加のコードは以下の通りです。

#include "../../YaneuraOu/source/search.h"
#include "../../YaneuraOu/source/thread.h"
#include "../../YaneuraOu/source/tt.h"
#include "../../YaneuraOu/source/usi.h"
#include "../../YaneuraOu/source/misc.h"
#include "../include/yaneuraou.h"

// cin/coutをsocketにリダイレクト
#include <iostream>
#include <unistd.h>
#include <sys/types.h>

std::string nnue_file_path;

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

typedef int (*yaneuraou_usi_read_cb)();
typedef void (*yaneuraou_usi_write_cb)(int ch);
int yaneuraou_ios_main(yaneuraou_usi_read_cb usi_read, yaneuraou_usi_write_cb usi_write, const char* nnue_file_path);

#ifdef __cplusplus
}
#endif /* __cplusplus */

class myoutstreambuf : public std::streambuf {
private:
    yaneuraou_usi_write_cb usi_write;

public:
    myoutstreambuf(yaneuraou_usi_write_cb _usi_write): usi_write(_usi_write) {
    }

protected:
    int overflow(int nCh = EOF) {
        usi_write(nCh);
        return nCh;
    }
};

class myinstreambuf : public std::streambuf {
private:
    yaneuraou_usi_read_cb usi_read;

public:
    myinstreambuf(yaneuraou_usi_read_cb _usi_read): usi_read(_usi_read) {
    }

protected:
    int uflow() {
        return usi_read();
    }

};

static void yaneuraou_ios_thread_main(yaneuraou_usi_read_cb usi_read, yaneuraou_usi_write_cb usi_write) {
    myoutstreambuf outs(usi_write);
    myinstreambuf ins(usi_read);
    auto default_out = std::cout.rdbuf(&outs);
    auto default_in = std::cin.rdbuf(&ins);

    // --- 全体的な初期化
    int argc = 1;
    char argv0[] = "yaneuraou";
    char *argv[] = {argv0};
    CommandLine::init(argc,argv);
    USI::init(Options);
    Bitboards::init();
    Position::init();
    Search::init();

    // エンジンオプションの"Threads"があるとは限らないので…。
    size_t thread_num = Options.count("Threads") ? (size_t)Options["Threads"] : 1;
    Threads.set(thread_num);

    //Search::clear();
    Eval::init();

#if !defined(__EMSCRIPTEN__)
    // USIコマンドの応答部

    USI::loop(argc, argv);

    // 生成して、待機させていたスレッドの停止

    Threads.set(0);
#else
    // yaneuraOu.wasm
    // ここでループしてしまうと、ブラウザのメインスレッドがブロックされてしまうため、コメントアウト
#endif

    std::cout.rdbuf(default_out);
    std::cin.rdbuf(default_in);
    
    // maybe send disconnect message
}

extern "C" int yaneuraou_ios_main(yaneuraou_usi_read_cb usi_read, yaneuraou_usi_write_cb usi_write, const char* nnue_file_path) {
    ::nnue_file_path = nnue_file_path;
    std::thread thread(yaneuraou_ios_thread_main, usi_read, usi_write);
    thread.detach();
    return 0;
}

Swift言語から、yaneuraou_ios_main関数を呼ぶことにより、やねうら王が動作するスレッド(本来のmain関数)を起動する仕組みになっています。引数として、本来標準入出力でUSIコマンドをやり取りするところを、標準入出力を行うとSwift言語側から関数ポインタとして与えられるusi_read, usi_write関数が呼び出されるようにし、Swift言語にコールバックするようになっています。また、watchOSアプリのアセットとしてNNUE評価関数(水匠5)ファイルを組み込むため、動的に決まるパスを受け取ります。やねうら王ではUSIオプションで評価関数ファイル名を受け取るようになっていますが、ここで指定されたパスから常に読み込むよう、やねうら王のソースを一部書き換えます。ソースの書き換えはスクリプトにより自動化しています。 modify-yaneuraou.py ついでに、ハッシュサイズやスレッド数のデフォルトなどを実行環境に合わせて書き換えます。

ビルドには、 ios-cmake というツールを用います。 cmakeのオプション設定により、Apple Watchアーキテクチャに合わせたバイナリがビルドできます。cmakeを呼び出すスクリプト ポイントとして、Macで動作するシミュレータ用のビルドが別途必要で、Apple Silicon用のビルドとIntel Mac用のビルドの両方がないと、対応するMacでのシミュレータでの実行ができません。両方のビルドを行い、バイナリをlipoコマンドで結合します。

watchOSアプリの作成

Swift言語を用いたwatchOSアプリを作成します。ソースコードはこちら。

GitHub - select766/YaneuraOuWatch: やねうら王(将棋AI) on Apple Watch

プロジェクトのPackage Dependencyに、先ほど作成したSPM(のgitリポジトリとブランチ名)を追加することで、 yaneuraou_ios_main 関数がSwift言語から呼べるようになります。

iOSとの重要な違いとして、TCPソケットが使えません。C言語のsocket、SwiftのNWConnectionのいずれも動作しません。また、WebSocketについても限定的な条件下でしか使えません。そのため、外部とのコマンドの送受信はHTTPで行う必要があります。今回は単純に、やねうら王が入出力するUSIコマンド文字列をHTTP POSTで送受信する仕組みを作りました。

やねうら王を呼び出すためのコードは以下の通りです。これは、iOSと変わりません。やねうら王が文字を出力すると、yaneSendBuffer 変数に格納され、改行文字が来ると yaneRecvCallback が呼ばれます。やねうら王に文字列を読ませたいときは、 sendToYaneuraou 関数に与えると、やねうら王が文字を受け取ろうとした際に1文字ずつ送ります。

import Foundation
import YaneuraOuiOSSPM

// やねうら王とのプロセス内通信関係のバッファ(グローバル関数にするしかない)
var yaneRecvBuffer: Data = Data()
let recvSemaphore = DispatchSemaphore(value: 1)
var yaneSendBuffer: Data = Data()
let sendSemaphore = DispatchSemaphore(value: 1)
var yaneRecvCallback: (String) -> Void = {_ in}

func usiWrite(char: Int32) -> Void {
    // 思考スレッドから呼ばれる
    // 1文字ずつくる。
    // 改行が含まれている。複数行の場合もある。
    // USIクライアント->USIサーバへの送信
    if char < 0 {
        print("usiWrite(EOF)")
        return
    }
    
    sendSemaphore.wait()
    
    if char == 0x0a {
        // end of line
        let completeBuffer = yaneSendBuffer
        yaneSendBuffer = Data()
        // 改行文字は含まない
        yaneRecvCallback(String(data: completeBuffer, encoding: .utf8)!)
    } else {
        yaneSendBuffer.append(contentsOf: [UInt8(clamping: char)])
    }
    
    sendSemaphore.signal()
}

func usiRead() -> Int32 {
    // 思考スレッドから呼ばれる
    // USIサーバ->USIクライアントへの受信
    var item: Int32 = 0
    while true {
        recvSemaphore.wait()
        if yaneRecvBuffer.count > 0 {
            item = Int32(yaneRecvBuffer[0])
            // recvBuffer = recvBuffer.dropFirst()
            // を使うと、次回のrecvBuffer[0]のアクセス時になぜかクラッシュする
            yaneRecvBuffer = Data(yaneRecvBuffer[1...])
            recvSemaphore.signal()
            break
        } else {
            recvSemaphore.signal()
            Thread.sleep(forTimeInterval: 0.1)
        }
    }
    
    return item
}


func stringToUnsafeMutableBufferPointer(_ s: String) -> UnsafeMutableBufferPointer<Int8> {
    let count = s.utf8CString.count
    let result: UnsafeMutableBufferPointer<Int8> = UnsafeMutableBufferPointer<Int8>.allocate(capacity: count)
    _ = result.initialize(from: s.utf8CString)
    return result
}

func startYaneuraou(recvCallback: @escaping (String) -> Void) {
    // やねうら王とのプロセス内通信準備
    // recvCallback: やねうら王からメッセージを受信したときに呼ばれる(改行を含まない1行) 例: "bestmove 7g7f"
    yaneRecvCallback = recvCallback

    // assetのnn.binを評価関数ファイルとして渡す
    guard let nnue_eval_path = Bundle.main.path(forResource: "nn", ofType: "bin") else {
        fatalError()
    }
    let nnue_eval_path_p = stringToUnsafeMutableBufferPointer(nnue_eval_path)

    YaneuraOuiOSSPM.yaneuraou_ios_main(usiRead, usiWrite, nnue_eval_path_p.baseAddress!)
}

func sendToYaneuraou(messageWithoutNewLine: String) -> Void {
    // やねうら王にコマンドを送る 例: "usinewgame"
    let d = (messageWithoutNewLine + "\n").data(using: .utf8)!
    recvSemaphore.wait()
    yaneRecvBuffer.append(d)
    recvSemaphore.signal()
}

UIおよび通信部分は以下の通りです。

import SwiftUI

// FIXME: 将棋所が動いているMacのIPアドレスを指定
let baseURL = "http://192.168.3.12:4090"

struct ContentView: View {
    @State var running = false
    @State var lastMessage = "やねうら王 on Apple Watch"
    @State var writeQueue = [String]()
    @State var writeBusy = false

    var body: some View {
        VStack(alignment: .leading) {
            BatteryView()
            Text(lastMessage).frame(maxWidth: .infinity, alignment: .leading)
            if !running {
                Button(action: start) {
                    Text("Connect")
                }.padding()
            }
            Spacer()
        }.frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    func start() {
        if running {
            return
        }
        running = true
        startYaneuraou(recvCallback: { messageFromYane in
            DispatchQueue.main.async {
                doWrite(message: messageFromYane)
            }
        })
        doRead()
        writeNext()
    }
    
    func doWrite(message: String) {
        // ここで直接HTTP POSTすると順序が乱れる場合があるためキューに入れて順番に送信
        writeQueue.append(message)
    }
    
    func writeNext() {
        if writeQueue.count == 0 {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: writeNext)
            return
        }
        
        let message = writeQueue.joined(separator: "\n")
        writeQueue.removeAll()
        lastMessage = "> \(message)"
        let url = URL(string: "\(baseURL)/write")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.httpBody = message.data(using: .utf8)
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            DispatchQueue.main.async {
                writeNext()
            }
        }
        task.resume()
    }
    
    func doRead() {
        let url = URL(string: "\(baseURL)/read")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            guard let data = data else { lastMessage = "Error: " + String(describing: error); return; }
            let messages = String(data: data, encoding: .utf8)!
            
            var nextTime = DispatchTime.now()
            if messages.count > 0 {
                lastMessage = "< \(messages)"
                for message in messages.split(separator: "\n") {
                    sendToYaneuraou(messageWithoutNewLine: String(message))
                }

                nextTime = nextTime + 0.01
            } else {
                nextTime = nextTime + 0.1
            }
            
            DispatchQueue.main.asyncAfter(deadline: nextTime, execute: doRead)
        }
        task.resume()
    }
}

やねうら王から外部への送信は、doWrite関数でやねうら王が出力した文字列をキューに入れ、一定時間間隔で呼び出されるwriteNext関数でキューにある文字列をHTTPサーバに送信します。受信は、一定時間間隔で呼び出されるdoRead関数により、HTTPサーバから取得します。あくまでApple Watchでの動作検証が目的のため、局面の表示機能は搭載せず、Mac上で動作する将棋所と通信するだけの機構としました。

通信仲介用サーバの実装

Mac側で動作し、将棋所とやねうら王を仲介するためのサーバをnode.jsで実装しました。

将棋所から出力されたメッセージ(例: "usinewgame")は、サーバの標準入力に与えられるため、それを読み取って lines 変数にためておきます。やねうら王からHTTPリクエストが来たら、lines変数の内容をレスポンスで返します。やねうら王から出力されたメッセージ(例: "bestmove")がHTTPリクエストで得られたら、console.logで標準出力に出力し、将棋所に読み取らせます。初期の実験では、1回のHTTPリクエストでコマンド1行のみ送信するようにしていましたが、レイテンシが大きすぎました。送信したいメッセージを一度キューにためて、通信時には一括で送る仕組みにする必要がありました。

const express = require('express');
const readline = require("readline");
const app = express()

process.stdin.setEncoding("utf8");

let lines = [];
const reader = readline.createInterface({
  input: process.stdin,
});

reader.on("line", (line) => {
  //改行ごとに"line"イベントが発火される
  lines.push(line); //ここで、lines配列に、標準入力から渡されたデータを入れる
});


// req.bodyにPOSTデータを入れる設定
app.use(express.raw({ type: '*/*', limit: '128kb' }));

const port = 4090;
const server = app.listen(port, () => {
  console.error(`Server listening on port ${port}`);
});

app.post('/write', (req, res)=> {
  let text = req.body.toString('utf-8');
  let lines = text.split('\n');
  for (const line of lines) {
    console.log(line);
  }
  res.send('1');
  res.end();
});

app.post('/read', (req, res)=> {
  const quit = lines.some((v) => v == "quit");
  const line = lines.join('\n');
  lines = [];
  res.send(line);
  res.end();

  if (quit) {
    // クライアントがquitを読み次第、サーバプロセスを終了する
    setTimeout(() => {
      server.close();
    }, 100);
  }
});

reader.on("close", () => {
    // stdinが閉じられたら終了。ただし、将棋所は対局が終わっても閉じてくれない
    console.error('stdin closed');
    server.close();
});

動作結果

floodgateに投入して実験しました。

やねうら王(水匠5評価関数)

NNUE系評価関数を用いるやねうら王の場合の結果です。33局対戦し、レート3873となりました。探索速度は2スレッドで約55万NPSでした。ハッシュサイズは64MBです。

Apple Watch Series 9+やねうら王のfloodgate対局結果

ふかうら王

DL系評価関数を用いるふかうら王も同様にビルドできました。fukauraouブランチにあります。10ブロック(20層)192チャンネルのモデルを動作させました。

探索速度は約450NPSでした。同じモデルでiPhone 15 Proでは約5000NPSとなっています。Apple Watchにもなぜか機械学習用チップのNeural Engineが搭載されているため、機体の大きさの割には高速に動作します。動作するものの、floodgate上でレートが付与されるまで対局を続けることができませんでした。不規則なタイミングでメモリ不足によってクラッシュしてしまう問題があったためです。探索ツリーを保持するメモリ容量が1000ノード=1MB程度必要で、ノード数制限を25000にしておけばメモリが基本的に足りることはわかったのですが、時々これより少ないノード数の時点でクラッシュしてしまいました。探索木を成長させながら逐次的にメモリを確保する仕組みとなっているため、バックグラウンドアプリがメモリを要求したタイミングでメモリ確保に失敗してしまうのかもしれません。これ以上のデバッグが困難なため、ふかうら王の運用は諦めました。やねうら王ではハッシュ表のメモリ全体を事前に確保して動的確保しないため、クラッシュしづらいのではないかと予想しています。

運用ノウハウ

floodgateで対局するにあたり、2点の運用ノウハウが必要でした。

1点目は、Apple Watchをスリープ状態にさせないことです。通常は操作がないと15秒(設定により70秒)で画面が消灯し、アプリも停止してしまいます。しかし、Xcodeからアプリをデバッグ実行している最中は長時間経ってもスリープしないため、この状態でなら手動操作なしにfloodgateでの対局が可能でした。デバッグ実行以外の手段は試せていませんが、Apple Watchで動画を視聴できるアプリがあり、これを試したところ2分以上無操作でも動画が再生され続けたため、何らかの回避策はあるかもしれません。

2点目は冷却です。やねうら王を動作させると発熱し、それが原因で充電器に接続していても充電が止まってしまいます。

Apple Watchの温度が理由で充電は保留されました」の表示

これを回避するため、サーキュレータの上に充電器ごと載せることで、冷却は十分になりました。

サーキュレータで冷却しながら運用する様子

また、バッテリーの設定で充電上限の最適化をオフにすることで、バッテリー残量に関わらず充電されるようになります。

「充電上限の最適化」をオフ

ただし、この設定をしていても対局中にどんどんバッテリーが減ってしまう場合があり、最終的に電池切れになる場合がありました。一方で対局中でも充電残量が上がっていく場合もあり、充電の制御については不明な点が残ります。

残る課題として、HTTP通信が一度失敗してしまうと将棋所との同期がとれなくなり対局続行不可能となる事例がありました。通信部分はよりロバストかつ低レイテンシにすることが望ましいです。単純なHTTPポーリングではなく、ロングポーリング技術を採用することが考えられます。

まとめ

Apple Watch上でやねうら王をどうさせることができました。ビルド手段はiOSと似ていましたが、通信はTCPソケットが使えないため、HTTPで代用する必要がありました。冷却により、floodgate上での連続対局が可能であり、レート3873が付与されました。ふかうら王については動作するものの不安定で、連続対局は困難でした。

動作環境や安定性の面からネタ枠の域を出ませんが、スマートウォッチでも十分強い将棋AIが動作することを示せました。6月の電竜戦TSECは、これで出るかもしれません。

nnue-pytorchの環境構築(将棋AI評価関数学習器)

やねうら王で使えるNNUE型評価関数をGPUを用いて学習できるnnue-pytorchの環境構築に成功したので方法を共有します。

nnue-pytorchはもともとチェス用に開発されましたが、将棋AIたぬきシリーズ作者のnodchipさんにより将棋用にカスタマイズされたバージョンが公開されています。

目標

nnue-pytorchから将棋AI学習用棋譜データshogi_hao_depth9を読み込み、標準NNUEモデルを学習し、やねうら王で動作させる。

必要な環境

  • Windows 11
    • Windows 10でも行けると思います。
    • Linuxは使えません。Parallel Patterns Library (ppl.h)というMSVC専用のライブラリが使われているためです。
  • NVIDIAGPU
    • 検証環境ではRTX 4070を用いました。
    • 最近のPyTorchはCUDAバイナリが同梱されているので、PyTorchのバージョンに合わせたCUDAのインストールは不要と思われます。
  • 1TB以上のディスク
    • 棋譜のダウンロード・加工に必要です。
    • SSDが望ましいです。筆者はNAS上のHDDを使う必要があり、余分に時間がかかりました。

学習用データの準備

ダウンロード

nodchipさんが公開している棋譜データをダウンロードします。約300GBあり、gitの管理用データとあわせてディスク容量が約600GB必要です。

データセットの説明

事前にHugging Faceのユーザ登録・SSH公開鍵登録 https://huggingface.co/settings/keys が必要です。

以下のコマンドで、データセットをダウンロードします。約300GBあるため、回線速度により相当な時間がかかります。

git lfs install
git clone git@hf.co:datasets/nodchip/shogi_hao_depth9

シャッフル

ダウンロードしたデータセットは、 PackedSfenValue という形式で局面とその(深さ9で探索した際の)評価値などが1局面当たり40バイトで記録されています。1ファイルは約300MB、ファイル数は1016個あります。データセットを学習用と評価用に分け、さらにそれぞれをシャッフルします。シャッフルは2つの観点で必要です。1つは、ファイル内の連続する局面は同じ対局から生じたものであるため、学習の安定性のためです。もう1つは、静止探索(qsearch)を適用するためです。探索部が局面Xを評価する場合、もしXが駒の取り合いの最中の局面であった場合、静止探索により駒の取り合いが終わった局面Yに対して評価関数を計算します。そのため、データセット中に局面Xがある場合、それを局面Yに置換し、局面Yと評価値のペアとして評価関数を学習する必要があります。 データセットの説明にある通り、シャッフルも静止探索も適用されていないため、適切な学習のためには単純に40バイトのレコードをランダムにシャッフルするだけではなく、局面を理解したうえで静止探索を適用する機構を持ったシャッフルプログラムが必要です。

セットアップ

シャッフルを行うプログラムも、nodchipさんのリポジトリにあります。注意点として、 tanuki-dr4-learner ブランチを選ぶ必要があります。他のブランチでは、必要な機能が搭載されていません。

github.com

これのビルドに少々難儀したため、「独自ビルド」の章で作成したビルド済みバイナリを置いておきます(オリジナル版に対して修正が入っています)。

github.com

ここでは、 YaneuraOu_NNUE-evallearn-g++-sse42.exe を使う想定でコマンドを記述します。CPUにより、avx2, zen3などを使うとよりパフォーマンスが上がる可能性があります。

静止探索を行うために、評価関数ファイルが必要です。このバイナリでは標準NNUE形式が使われますので、その1つとしてnodchipさんが公開している『Háo』評価関数を用います。

Release tanuki-.halfkp_256x2-32-32.2023-05-08 · nodchip/tanuki- · GitHub

ここから tanuki-.halfkp_256x2-32-32.2023-05-08.7z をダウンロード、展開し、以下のディレクトリ構造になるように配置します。

- YaneuraOu_NNUE-evallearn-g++-sse42.exe
- eval
  - nn.bin

ちなみに、Windows 11では標準機能で7z (7-zip)が解凍できます。

実行

シャッフルプログラムは、1つのフォルダにあるすべてのファイルをシャッフルして結合し、別の1ファイルを出力する仕様になっています。事前に、学習(train)と評価(val)ディレクトリにファイルを分けておきます。ここでは、ファイル名に thread_index=126 とある8個のファイルを評価用に、残りの1008個を学習用にします。(path/toの箇所は適宜置き換えてください)

cd path\to\shogi_hao_depth9
mkdir val
move "kifu.tag=train.depth=9.num_positions=1000000000.start_time=*.thread_index=126.bin" val
mkdir train
move *.bin train

以下のコマンドで、シャッフルを実行します。ディスクの速度によりますが、数時間かかります。筆者は論理16コア、RAM容量32GBのマシンで動かしましたが、容量が小さい場合はバッファサイズの調整が必要かもしれません。

cd path\to\tanuki-
YaneuraOu_NNUE-evallearn-g++-sse42.exe usi , Threads 15 , EvalDir eval , KifuDir path\to\shogi_hao_depth9\val , KifuReaderBufferSize 2147483647 , KifuWriterBufferSize 268435456 , ShuffledKifuDir path\to\shogi_hao_depth9\val_shuffled , ShuffledMinPly 1 , ShuffledMaxPly 9223372036854775807 , ShuffledMinProgress 0.0 , ShuffledMaxProgress 1.0 , ApplyQSearch true , isready , usinewgame , shuffle_kifu , quit
YaneuraOu_NNUE-evallearn-g++-sse42.exe usi , Threads 15 , EvalDir eval , KifuDir path\to\shogi_hao_depth9\train , KifuReaderBufferSize 2147483647 , KifuWriterBufferSize 268435456 , ShuffledKifuDir path\to\shogi_hao_depth9\train_shuffled , ShuffledMinPly 1 , ShuffledMaxPly 9223372036854775807 , ShuffledMinProgress 0.0 , ShuffledMaxProgress 1.0 , ApplyQSearch true , isready , usinewgame , shuffle_kifu , quit

これで、シャッフル済みデータセットファイル( path\to\shogi_hao_depth9\{train,val}_shuffled\shuffled.bin )が得られました。

独自ビルド

シャッフルプログラムには、進行度を推定し、進行度が特定の範囲の局面だけを出力する機能がついているのですが、そのためには進行度推定モデルファイル"progress.bin"が必要でした。公開されているファイルが見当たらなかったため、進行度推定を使用しない場合はこのファイルがなくてもシャッフルを行えるよう改良を行いました。その結果が筆者のリポジトリにあります。 GitHub - select766/tanuki- at tanuki-dr4-learner

上記のビルド済みバイナリを使わず、自前でビルドする際の環境構築を示します。これは、やねうら王リポジトリにあるmake-msys2.ymlを参考にしました。

まず、MSYS2をセットアップします。 https://www.msys2.org/ から、 msys2-x86_64-20240507.exe をダウンロードしてデフォルト設定でインストールします。

コマンドプロンプトを開き、以下のコマンドを実行して必要なライブラリをインストールします。

C:\msys64\usr\bin\bash.exe -lc 'pacman --needed --noconfirm -Syuu'
C:\msys64\usr\bin\bash.exe -lc 'pacman --needed --noconfirm -Syuu'
C:\msys64\usr\bin\bash.exe -lc 'pacman --needed --noconfirm -Syuu pactoys'
C:\msys64\usr\bin\bash.exe -lc 'pacboy --needed --noconfirm -Syuu clang:m lld:m openblas:x openmp:x toolchain:m base-devel:'

PowerShellを開き、以下のコマンドを実行してソースコードをダウンロード・ビルドします。

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
$ENV:Path="C:\msys64;"+$ENV:Path
git clone -b tanuki-dr4-learner https://github.com/select766/tanuki-.git
cd tanuki-
.\script\msys2_build.ps1 -Edition YANEURAOU_ENGINE_NNUE -Compiler g++

ビルドが成功すると、 tanuki-\build\windows\NNUE\YaneuraOu_NNUE-evallearn-g++-sse42.exe などが得られます。

Tips

  • Set-ExecutionPolicy の行は、そのシェルを開いている間だけps1ファイルの実行を許可します。
  • MSVCでのビルドはできませんでした。OpenMPのループ変数にunsigned intが使われている箇所があり、MSVCにとっては文法エラーとなりました。

nnue-pytorch

環境構築

C++コンパイラについては、手元ですでに入っていたものを利用したため、もしかしたらこの手順だとうまくいかないかもしれません。

C++コンパイラを入手するため、Build Tools for Visual Studio 2022をダウンロード・インストールします。(Visual Studio 2022をインストールしている場合は、「C++によるデスクトップ開発」の機能を有効にすれば同等のものが手に入ると思われます)

Visual Studio Tools のダウンロード - Windows、Mac、Linux 用の無料インストール

Build Tools for Visual Studio 2022

cmakeをダウンロード・インストールし、パスが通った状態にします。

Download CMake

Python環境として、Anacondaを用います。

Anaconda Promptで、以下のコマンドを実行して必要なツールをインストールします。

conda create --name nnue python=3.9 
conda activate nnue
conda install pytorch torchvision torchaudio pytorch-cuda=11.8 -c pytorch -c nvidia 
pip install python-chess==0.31.4 pytorch-lightning==1.9.0 cshogi==0.8.5
conda install matplotlib
pip install tensorboard tensorboardX

以後、新しいAnaconda Promptを開くたびに conda activate nnue の実行が必要です。

nnue-pytorchをダウンロードします。 masterブランチはチェス用です

git clone -b shogi.2024-04-20.halfkp_1024x2-8-64 https://github.com/nodchip/nnue-pytorch

コマンドプロンプトを開き、PackedSfenValue形式の棋譜を読むためのネイティブライブラリをビルドします。

cd path\to\nnue-pytorch
compile_data_loader.bat

ネットワーク構造の設定

上記でダウンロードした shogi.2024-04-20.halfkp_1024x2-8-64 ブランチは、nodchipさんが実験のためにモデル構造を変更したものなので、ここで学習したモデルは通常のやねうら王では動かせません。

やねうら王が対応している「標準NNUE」形式を学習する場合は、 model.py の冒頭を次のように書き換えます。標準NNUEは、 halfkp_256x2-32-32 と表記される形式となっています。

# 3 layer fully connected network
L1 = 256
L2 = 32
L3 = 32

学習

以下のコマンドで学習を行います。パスは適宜書き換えてください(最後の2つの引数が学習データ、 --default_root_dir がログの出力ディレクトリ)。

python train.py --features "HalfKP" --batch-size 16384 --max_epochs 1000000 --enable_progress_bar False --default_root_dir logs/20240526_halfkp_256x2-32-32 --threads 8 --lr 0.5 0.05 --num-workers 8 --lambda 1.0 0.5 --label-smoothing-eps 0.001 --accelerator gpu --devices 1 --score-scaling 361 --min-newbob-scale 1e-5 --epoch-size 1000000 --num-epochs-to-adjust-lr 500 --momentum 0.9 --network-save-period 1000 --resume-from-model "" "path\to\shogi_hao_depth9\train_shuffled\shuffled.bin" "path\to\shogi_hao_depth9\val_shuffled\shuffled.bin"

モデルの精度に関するオプションは、nodchipさんがWCSC34用の学習に用いたと説明された値を用いています。

学習のログやチェックポイントファイルは nnue-pytorch\logs\20240526_halfkp_256x2-32-32\lightning_logs\version_0 ディレクトリに吐き出されています。1000epoch(1epoch=1Mサンプル)ごとにチェックポイントを保存するようにしており、検証環境では約1時間ごとに保存されました。max_epochsに大きな値を指定してあるため、適当なタイミングで、Ctrl-Cにより終了する必要があります。

学習状況の確認

上記のコマンドではほぼコンソールに出力がされないので、tensorboardを用いて状況を確認する必要があります。

tensorboard コマンドを実行すると、Webブラウザが開き、学習状況が可視化できます。

tensorboardの画面

モデルをやねうら王の形式に変換する

学習されたモデル(拡張子: ckpt)を、やねうら王で読み込める形式に変換する方法を解説します。

ここでは、先ほどのコマンドで1000epoch学習された時点のチェックポイント( logs\20240526_halfkp_256x2-32-32\lightnin g_logs\version_0\1000.ckpt )を入力として、 for_yaneuraou\nn.nnueに出力します。ディレクトリは自動的に作られます。nn.nnue以外に画像ファイルが出力されるので、カレントディレクトリは避けたほうがよいです。後の処理を考えると、 nn.bin のほうが便利なのですが、拡張子がチェックされるため*.nnueを指定してください。

python serialize.py --features "HalfKP" logs\20240526_halfkp_256x2-32-32\lightnin
g_logs\version_0\1000.ckpt for_yaneuraou\nn.nnue

ここで得られたnn.nnueファイルがやねうら王で使える評価関数ファイルになります。サイズは標準NNUEの場合、64,217,072バイトになっているはずです。

ここからは、やねうら王で動作させる例を示します。 やねうら王V7.6.3のビルド済みバイナリ(YaneuraOu-v7.6.3-windows.7z)をダウンロード・解凍します。

github.com

実行ファイルは、 YaneuraOu-v7.6.3-windows\windows\NNUE\YaneuraOu_NNUE-normal-clang++-sse42.exe を使います。評価関数ファイルを以下のように配置します。

- YaneuraOu_NNUE-normal-clang++-sse2.exe
- eval
  - nn.bin (※nn.nnueからリネーム)

あとは、 YaneuraOu_NNUE-normal-clang++-sse2.exe を将棋所などでエンジン登録すれば対局できます。

1000epoch学習したモデルを用いた対局のスクリーンショットを示します。初手は2六歩を指し、45手でLesserkaiに勝利しています。大会で使えるレベルの強さではないと思われますが、学習パイプラインが無事完成したことを確認できます。

学習したモデルとLesserkaiの対局

モデル構造を変化させる方法

nnue-pytorchでは、様々なモデルの構造(パラメータ数)を試すことができます。前述のように学習の際は"model.py"を変更します。学習結果をやねうら王で使えるようにするには、やねうら王の nnue_architecture.h がモデル構造を切り替えているため、これを書き換えてビルドする必要があります。

https://github.com/yaneurao/YaneuraOu/blob/b327a273c942ad24ee9666c427d92d391da7e4fa/source/eval/nnue/nnue_architecture.h#L54

まとめと今後の課題

Web上で公開されている棋譜データセットを用いて、nnue-pytorchによりNNUE評価関数を学習させ、やねうら王で動作させるまでのパイプラインを示しました。 学習用の棋譜の生成方法まで網羅できれば、完全に独自の評価関数を学習できるようになります。

NNUEについては私も詳しくない箇所が多く、誤りがあるかもしれません。誤りがあればご指摘お願いいたします。

第34回世界コンピュータ将棋選手権の結果(2024/05/03)

2024年5月3日~5日にかけて開催された第34回世界コンピュータ将棋選手権に参加ソフト「ねね将棋」で参加しました。

今回のテーマは、iPhoneだけで対局を行い、DL系エンジンをスマートフォンで動作させた場合の強さを示すことです。第33回ではMac上の将棋所と連携して通信を行っていましたが、今回は完全にiPhoneだけで完結するようにしました。無線LANや携帯電話回線を使えば、バッテリーが持つ範囲では、ケーブルを一切接続せず対局することも可能です。

技術的には、以下の要素の組み合わせです。

  • ふかうら王をiPhone用に移植する
    • 第3回世界将棋AI電竜戦本戦(2022/12/03)で実装済
  • GUIを実装する
    • 第33回世界コンピュータ将棋選手権(2022/05/03)でiPad用に実装したものをiPhone用に変更
  • ふかうら王の指し手(USIプロトコル)を対局サーバで用いるCSAプロトコルに変換する(新規実装)
    • 第33回世界コンピュータ将棋選手権(2022/05/03)でSwift言語で実装した盤面クラスを応用

従来使用していたiPad(第9世代)(2021年発売)から、iPhone 15 Pro(2023年発売)に端末が新しくなったことで、読みの速度が1000NPSから5000NPSに向上しています。floodgateではレート3700程度でした。

設営した状態

戦績

一次予選からの参加で、一次予選は28チーム中5位となりました。二次予選では一次予選から上がった11チームとシード17チームの合計28チームで争い、18位となり、ここで敗退しました。二次予選の上位18チームに与えられる、次回大会のシード権を獲得しました。

なお、二次予選のnshogi-ねね将棋戦は、nshogi側の不具合によりねね将棋の勝利となりました。

感想

長い定跡を使っているチームが多く、序盤の消費時間に大きな差が開くこともありました。本番で普段より強力なハードウェアを利用するチームは定跡のメリットが落ちるようですが、ねね将棋はハードウェアに縛りがあるので定跡は積極的に活用していくべきと思いました。ただし、開発者本人の棋力を活用して調整を行っているチームもあるようで、仮に詳しい手法がわかっても真似するのは難しい場合もあるようです。

対局中の不具合はなく、無事故で終えることができました。iPhoneを使うことは他のチームにない唯一の特徴であるため独自にノウハウを構築する必要がありましたが、過去に起きたトラブルの再発防止を図ったことが有効だったと思います。

select766.hatenablog.com

上位ソフトとの対局では頓死気味の終わり方が多くありました。読みぬけは重要な課題です。今回まででiPhoneでふかうら王を動作させる環境構築が完了したと考えられるので、今後は評価関数の改良などに取り組んでいきたいです。

対局してくださった方、運営の皆様に感謝申し上げます。

リンク

iPhone 15 Proの有線LAN接続検証

世界コンピュータ将棋選手権では、対局に使うコンピュータを会場の有線LAN(Ethernet)に接続する必要があります。

普通のパソコンであれば難なくできる作業ですが、スマートフォンを有線LANに接続するのはあまり一般的な作業ではありません。 私が対局に利用予定のiPhone 15 Proが有線LANに接続できるかどうか検証しました。

iPhoneシリーズはiPhone 15以降、有線コネクタがLightningからUSB-Cに変わりました。 Lightningコネクタを持つiPad (第9世代)についてはこちら。 select766.hatenablog.com

以下の組み合わせで成功しました。iPhoneの設定は特に不要で、機内モードWi-Fiオフにしても正しく通信できました。

  • iPhone 15 Pro
  • USB-C Digital AV Multiportアダプタ (MUF82ZA/A)
    • 2019年8月購入、現行品
  • ETX3-US2 (I-O DATA)
    • 2018年4月購入、生産終了
    • まだ手に入るようです

iPhone15Proを有線LANに接続

iPhoneで将棋AI大会に出るときのヒヤリハット(電竜戦)

第4回世界将棋AI電竜戦本戦(2023年12月2日~3日)に将棋AI「ねね将棋」で出場しました。ほとんどの参加者はパソコンかクラウド計算機上で将棋AIを動作させる中、ねね将棋はiPhone上で動作させるというユニークなソフトです。

ソフトの概要や電竜戦参加の記録はこちらのページから。

select766.hatenablog.com

本記事では、iPhoneで将棋AIを動作させるという特殊な仕組みに起因して大会出場時に起こった・未然に防いだオペレーションミスについて紹介し、再発防止策を検討します。

プロビジョニングプロファイルの有効期限切れ

自作のiOSアプリをiPhoneで動作させる際には「プロビジョニングプロファイル」というものが必要で、有効期限があります。アプリに埋め込まれたプロビジョニングプロファイルの有効期限が切れると、「"ねね将棋"はもう利用できません」というような表示が出てアプリを起動できなくなります。無料でアプリ開発している場合は有効期限が7日しかないため、頻繁にMacでアプリをビルドしなおしてインストールする必要があります。私はこの有効期限について勘違いしており、インストールしなおすたびに有効期限が更新されるものと想定していました。例えば1月1日に初回ビルド(とインストール)、1月5日に2回目のビルドを行った場合、1月12日まで有効ではなく、1月8日まで有効です。有効期間内にビルドしなおしても有効期限が更新されません。私はこの挙動を認識しておらず、大会数日前にビルドしなおしていたにもかかわらず、大会の朝にアプリが起動できなくなり焦りました。

この挙動と対策については以下のサイトが参考になります。

無料の開発者アカウントで iPhone にインストールしたアプリの有効期限を更新する方法 - Neo's World

Apple Developer Program に登録せず、無料の Apple ID アカウントで iOS アプリを実機にインストールした場合、プロビジョニング・プロファイルの有効期限は7日間となる。 「Automatically manage signing」を利用するとプロビジョニング・プロファイルを自動的に作成し、期限が切れてからの再インストール時は自動的に有効期限を更新してはくれる。しかし、7日間経って期限が切れるまでは、プロビジョニング・プロファイルを更新してくれない。

通信仲介用のMacがスリープし、接続切れ

将棋の通信プロトコルの都合で、iPhoneと対局サーバの間にMacを仲介させていました。Macは電源アダプタに接続しているときはスリープしないようにしていたのですが、電源への接続を忘れており一定時間でスリープしてしまい、通信切断が発生しました。接続を忘れた理由は上記のプロビジョニングプロファイルの有効期限切れが発生し、急遽MaciPhoneを優先接続する際に電源ケーブル(USB-C)を流用したためです。テスト対局時に発生したので本番への影響はありませんでしたが、スリープに入るまでの時間によっては本番で失敗していたので危険でした。

対策は対局開始時に電源接続を確認することです。

iPhoneのスリープ

iPhoneは一定時間操作しないとスリープに入り、アプリの動作が停止してしまいます。スリープさせない設定も可能ですが、普段持ち歩いている端末なのでこの設定は不便です。大会前の検証中にスリープが発生したため、事前に対策していました。

対策:アプリ内でスリープを防止するAPIを呼び出す。SwiftUIで以下の処理により対応できる。

UIApplication.shared.isIdleTimerDisabled = true

iPhoneの電源接続忘れ

対局の合間に、将棋AIを止めてLINEなどを確認し、将棋AIを起動しなおした際に電源の接続を忘れ、対局が進みました。電池残量60%程度になったところで気づいて電源を接続し、実害はありませんでした。

対策は、対局前の操作手順を壁に貼るなどして、見落としをなくすことです。また、バッテリー状態を取得するAPIを用いて、画面上に警告を表示することも考えられます。

iPhoneへの電話着信

対局中は他のアプリの通知などが発生しないよう「おやすみモード」を有効にしていましたが、電話は着信する設定になっており、実際に電話が着信しました。その結果、アプリの動作が数十秒停止しました。電話を取らずに呼び出しが終わると動作が再開したため負けにはなりませんでしたが、残り時間がわずかな場合に起こっていれば負けていました。

対策は、「おやすみモード」の中の詳細な設定で電話の着信をしないように設定することや、機内モードを有効にする(WiFiはONにできる)ことです。

以上のようなオペレーションミスがありましたので、次回は対策して臨みたいと思います。

第4回世界将棋AI電竜戦本戦の結果(2023/12/02)

NPO法人AI電竜戦プロジェクト主催 第4回世界将棋AI電竜戦本戦 に将棋AI「ねね将棋」で参加しました。電竜戦は、オンラインのコンピュータ将棋AIの大会です。 将棋AIは通常パソコンやクラウド計算機上で動作させますが、ねね将棋は携帯端末上で動作する点がユニークな点です。前回まではタブレット端末のiPad(第9世代)を用いていましたが、今回は2023年9月発売のスマートフォンであるiPhone 15 Proで動作させました。機種の変更により、局面評価速度が約5倍になりました。私の普段の利用方法であればProではないiPhone 15で十分なのですが、大会でより良い成績を出したかったのでProを買いました。

ソフトウェアの内容

ソフトウェアの内容は、ふかうら王をiOSに移植したものになります。第33回世界コンピュータ将棋選手権で行っていた合議は削除し、DL系思考部単体で動作しています。持ち時間が短めなので、定跡を搭載できるように改良を施し、やねうら王のテラショック定跡を搭載しました。

select766.hatenablog.com

従来はiPadで動作させており冷却の効果はありませんでしたが、iPhoneでは冷却しないと速度が低下するため、スマートフォン用冷却ファンを取り付けて実行しました。

select766.hatenablog.com

大会の結果

予選で6勝8敗で27位(40チーム中)になりました。C級(上位20チーム以外の部門)に進出しました。C級では7勝6敗1分け(7.6点)で8位(16チーム中)となりました。

特筆すべき対局としては、「shotgun」と1勝1敗になりました。shotgunは、2017年に行われた第5回電王トーナメントの準優勝ソフトで、当時の強さを再現する設定で参加しています。第5回電王トーナメントはねね将棋がデビューした大会でもあり、shotgun対たぬきの決勝は大変盛り上がったのを記憶しております。今回、当時ハイエンドのゲーミングPCで動いていた最高峰のソフトと同じレベルの強さがスマートフォン上で実現できることが示せてうれしく思います。2017年当時はとても弱かったDL系ソフトが発展し、スマートフォンのハードウェア面の進歩も合わさってここまで強くなりました。

ねね将棋がshotgunに負けた対局では、一時ねね将棋が優勢だったのですが、23手詰めを受けて負けました。DL系の詰みに弱い特徴がそのまま出た対局でした。

https://golan.sakura.ne.jp/denryusen/dr4_production/dist/#/dr4prd+buoy_blackbid300_dr4c-5-bottom_4_neneshogi_shotgun-600-2F+neneshogi+shotgun+20231203144534/101

【C級】第4回電竜戦本戦 5回裏☗ねね将棋-☖shotgun

この対局については千田先生にも解説いただきました。4時間29分ごろからです。101手目の▲4三歩が人間も指しそうな手ではあるものの敗着とのことでした。

【将棋AI】第4回世界将棋AI電竜戦 決勝午後【解説:千田翔太七段】 - YouTube

iPhoneで将棋AIを動作させたという理由で独創賞を頂きました。電気通信大学エンターテイメントと認知科学研究ステーション様、賞金のご提供ありがとうございました。

大会全体では、NNUE系ソフトである水匠が優勝しました。DL系のdlshogiと優勝争いが繰り広げられ、ハードもソフトも大きく異なる2系統のソフトの棋力が非常に近いというのは技術的にも興味深い結果でした。運営、スポンサー、対局者の皆様ありがとうございました。