TadaoYamaokaの開発日記

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

pocketfftを使ってみる

先日、Whisperで音声認識する処理を実装した際に、FFTの処理でフレームサイズが2のべき乗でない場合にどうやって処理するか戸惑った。

Whisperではフレームサイズが400となっており、NAudioのFFTでは、フレームサイズが2のべき乗という制約があり使用できなかった。

FFTアルゴリズムは、Nが2のべき乗でなくても素因数分解できれば高速に計算できるが、2のべき乗のみを実装しているライブラリが多い。
C#で2のべき乗以外にも対応しているライブラリを探したところ、Math.NET Numericsが対応していたので使用した。

WhisperをC++でも使いたいと考えており、C++で、2のべき乗以外に対応しているライブラリだと何があるのか調べてみた。
なお、私が開発しているスマホアプリでは、FFTの実装に、大浦版FFTを使用しているが、これも2のべき乗である必要がある。

C++で2のべき乗以外に対応しているライブラリ

FFTWは、GPLライセンスのため、あまり使いたくない。
FFTPACKはFortranの実装のラッパーになっている。
pocketfftは、FFTPACKを改良してC++でヘッダーのみで実装されており、Numpyでも採用されている。ライセンスもBSDで使いやすい。

この中では、pocketfftが一番良さそうである。

pocketfftの使い方

説明がほとんどないため、pocketfft_demo.ccのコードを参考にした。

入力が実数で、出力が複素数の場合のコード例

サンプリング周波数16kHzで、800Hz、5000Hzの正弦波と20000Hzの余弦波を合成した波形にFFTする処理である。
フレームサイズは400とする。

#include <complex>
#include <cmath>
#include <iostream>
#include "pocketfft_hdronly.h"

using namespace pocketfft;

constexpr double PI = 3.14159;
constexpr double SAMPLE_RATE = 160000;
constexpr int N_FFT = 400;

int main()
{
	double data[N_FFT];
	std::complex<double> res[N_FFT / 2 + 1];

	for (int i = 0; i < N_FFT; i++) {
		data[i] = sin(2.0 * PI * 800.0 * (double)i / SAMPLE_RATE) + sin(2.0 * PI * 5000.0 * (double)i / SAMPLE_RATE) + cos(2.0 * PI * 20000.0 * (double)i / SAMPLE_RATE);
	}

	shape_t shape{ N_FFT };
	stride_t stride_in(shape.size());
	stride_t stride_out(shape.size());
	size_t tmp_in = sizeof(double);
	size_t tmp_out = sizeof(std::complex<double>);
	for (int i = shape.size() - 1; i >= 0; --i)
	{
		stride_in[i] = tmp_in;
		tmp_in *= shape[i];
		stride_out[i] = tmp_out;
		tmp_out *= shape[i];
	}
	shape_t axes;
	for (size_t i = 0; i < shape.size(); ++i)
		axes.push_back(i);

	r2c(shape, stride_in, stride_out, axes, FORWARD, data, res, 1.);

	std::cout << "index\treal\timag\tabs\n";
	for (int i = 0; i < N_FFT / 2 + 1; i++)
		std::cout << i << "\t" << res[i].real() << "\t" << res[i].imag() << "\t" << std::abs(res[i]) << "\n";

	return 0;
}

stride_in、stride_outの設定はfor文になっているが、入力が1次元の場合はfor文にする必要はないが、サンプルコードのまま記載した。

出力の絶対値をグラフにすると、以下のようになる。

3つのピークが確認できる。

torch.stftと比較

Whisperで使用されているPyTorchのtorch.stftと結果を比較した。

import torch
import math

SAMPLE_RATE = 160000

d = torch.zeros(400)
for i in range(400):
    d[i] = math.sin(2*math.pi*800*i/SAMPLE_RATE) + math.sin(2*math.pi*5000*i/SAMPLE_RATE) + math.cos(2*math.pi*20000*i/SAMPLE_RATE)

stft = torch.stft(d, 400, center=False, return_complex=True)
pd.DataFrame(stft.abs().squeeze().numpy()).plot()


同じ結果が得られた。

まとめ

フレームサイズが2のべき乗以外に対応しているC++FFTライブラリを調べた。
pocketfftが、Numpyでも採用されており、ヘッダーのみで、ライブラリもBSDで扱いことがわかった。
pocketfftを使用して、実数入力複素数出力でFFTするコードを実装して試してみた。

Whisperでリアルタイムに文字起こしするアプリ

前回の記事で、WhisperのモデルをONNXにして文字起こしができるようになったので、Windowsスタンドアロンで実行できるアプリを作成した。

C#WPFを使用して開発したので、実行できるのはWindowsのみである。

GitHubのReleaseからダウンロードできるようにした。
GitHub - TadaoYamaoka/RealtimeTranscribe: real-time transcription application

実行環境

.NET 6の.NET デスクトップ ランタイムが必要である。

実行方法

ReleaseからダウンロードしたRealtimeTranscribe.zipを解凍して、「RealtimeTranscribe.exe」をダブルクリックすると起動できる。

ラジオボタンで、「マイク」を選択すると音声をマイクから入力し、「ループバック」を選択するとPCで再生している音声から入力する。

余談

ONNX RuntimeのDirectML版を試したが、CPUより遅かったため、CPUで実行するようにしている。
4コアのノートPCでもほぼ処理落ちなしに実行できることを確認した。

起動時に、ONNX Runtimeでモデルの最適化を行うので30秒くらい待たされる。
3秒ごとに認識しているので、3秒以上の遅延がある。
認識のタイミングが音声の途中になると精度が落ちてしまう問題がある。
実行速度の問題があり、前の音声とつなげて認識し直す処理は実装していない。

実用性はあまりないと思うが、Whisperをスタンドアロンで実行できるデモとして作成した。

WhisperのモデルをONNXにする その2

前回、WhisperのモデルをONNXにする方法について記述した。

Whisperのモデルは、単体では音声認識はできず、音声をメルスペクトログラムにする前処理と、トークンをデコードして文字列にする後処理が必要になる。
今回は、前処理と後処理をC#で実装する方法について記述する。

音声認識の流れ

以下のような流れで音声をテキストに変換する。

  1. 音声を16kHzにリサンプリングする
  2. 対数メルスペクトログラムに変換する
  3. ONNXモデルで推論する
  4. 推論結果をトークン列にする
  5. トークン列をデコードしてUTF8の文字列にする

以下ではそれぞれの処理の実装方法について記述する。

音声を16kHzにリサンプリングする

Whisperでは、ffmpeg-pythonを使用してリサンプリングしている。
.NETでffmpegを扱う良いライブラリがなかったので、.NETのオーディオライブラリのNAudioを使用して、音声をリサンプリングした。

        using (var reader = new AudioFileReader("a.wav"))
        {
            var resampler = new WdlResamplingSampleProvider(reader, SAMPLE_RATE).ToMono();
            resampler.Read(audio, 0, audio.Length);
        }

対数メルスペクトログラムに変換する

メルスペクトログラムは、音声をフーリエ変換したスペクトルを2乗した、パワースペクトルメル尺度を適用したものである。

WhisperのPython実装では、フーリエ変換にPyTorchのtorch.stftを使用しているが、C#では別の方法で実装が必要である。
NAudioにもFFTのメソッドがあるが、フレームのサンプル数が2のべき乗という制約がある。Whisperは1フレームが400サンプルのため使用できない。
そのため、.NETの数値ライブラリのMathNet.Numericsを使用した。

窓関数

Whisperでは、窓関数には、ハン窓が使用されている。
MathNet.Numericsの、Window.Hannで実装した。

パディング

時刻0をフレームの中心にして、スライディングウィンドウで、フレームを切り出す際に、両端でパディングが必要になる。
Whisperでは、パディングの方法には、reflectが使用されている。
ライブラリがないため、スクラッチで実装した。

フーリエ変換

MathNet.NumericsのFourier.Forwardを使用して実装した。
サンプル数でスケーリングは行わない。

Fourier.Forward(frame, FourierOptions.NoScaling);
メル尺度の適用

メル尺度の係数は、whisper/assets/mel_filters.npzに保存されている。
C#で読み取れるように、PythonのNumpyでRawデータにしてバイナリファイルにしておき、それを読み込むようにした。

対数変換

対数にした後に、スケールの変換を行っているので、Whisperの実装と同じに実装した。

        logSpec = logSpec.PointwiseMaximum(1e-10).PointwiseLog10();
        logSpec = logSpec.PointwiseMaximum(logSpec.Enumerate().Max() - 8.0);
        logSpec = (logSpec + 4.0) / 4.0;

前処理は、以上である。

推論

ONNXRuntimeを使用して、推論を行う。
推論の箇所は、言語の自動判定と、メインループの2か所にある。

メインループでは、推論結果に対して、いくつか特定のトークンを抑止するためのマスク処理がある。
Pythonの実装をそのまま移植して実装した。

推論結果をトークン列にする

推論結果は、トークンIDごとの確率として出力されるので、確率が最大となるトークンIDを見つけて、トークン列とする。

トークン列は、そのままでは可読可能な文字列ではないため、デコード処理が必要である。

トークンのデコード

Whisperでは、文字列のトークン化に、バイトレベルBPEという方法が使われている。
その処理には、transformersライブラリのGPT2TokenizerFastが使用されている。

デコード処理の内部は、Rustで実装されているため、Rustのコードを読み解いてC#で同様の処理を実装した。

処理は単純で、

  1. UTF8の各バイトを1文字に変換するルールに従った対応表
  2. トークンIDから対応表のキー列となる文字列に変換する対応表

の2つの対応表を使って、トークン列からUFT8のバイト列に変換を行う。
1.の対応表は、Rustの実装C#に移植した。
2.の対応表は、whisper/assets/multilingual/vocab.jsonに保存されているjsonを使う。

以上で、音声からテキストに変換できる。

テスト

C#で実装した処理で、音声ファイルをテキストに変換できるか確認した。

「これはテストです。」とマイクで録音した音声ファイルを使って、言語が正しく認識されて、日本語テキストに変換できることを確認した。

>dotnet run a.wav
ja
これはテストです。

Whisperのtestsにあるjfk.flacでも試した。

>dotnet run jfk.flac
en
 And so my fellow Americans ask not what your country can do for you, ask what you can do for your country.

まとめ

C#で、ONNXにしたWhisperのモデルを使用して、音声をテキストに変換する処理の実装方法について記述した。
Pythonなしで実装できたことで、スタンドアロンのアプリに組み込みしやすくなる。

まだモデルの最適化を行っていないため、FP16化や、プルーニング(枝刈り)も試してみたい。

また、ONNXRuntimeはWebAssemblyでも動かせるため、ブラウザで動くデモを作成してみたい。

続き
tadaoyamaoka.hatenablog.com

2023/6/3 追記

ONNXに変換するソースを公開した。
github.com

WhisperのモデルをONNXにする

WhisperのモデルをONNXに変換する方法について記述する。

Whisperのモデル

WhisperのモデルはPyTorchを使ってPythonで実装されている。
そのため、実行にはPyTorchをインストールしたPython環境が必要になる。
環境構築なしでスタンドアロンで利用できると用途が広がる。
また、アプリへの組み込みも行いやすくなる。

ONNXモデル

ONNXは、ニューラルネットワークの標準ファイルフォーマットである。
モデルをONNXにすると、ONNX Runtimeなどの推論用のライブラリを使って推論できる。
推論用のライブラリは、組み込みで使うことを意図しているので、スタンドアロンのアプリに組み込むことができる。

ONNXへの変換

WhisperのモデルからONNXへの変換は、pytorch.onnxを使って行う。
ただし、Whisperは、デコーダのループ処理で、前の演算結果を再利用する処理で、kv_cacheという辞書型のオブジェクトを使用しているため単純には変換できない。
kv_cacheをTensor型として入力するように変更が必要になる。

実装方法は、以下の記事を参考にした。
音声認識AIのWhisperをUnreal Engineでリアルタイムに動かすためにやったこと
この記事に書かれているコードは、torch.catの部分がうまく動かなかったため、kv_cacheの領域をあらかじめ確保する方法で実装した。

エンコーダの変更

エンコーダのモデルは、そのままpytorch.onnxで変換できるが、デコーダ側の効率化のために、エンコーダで変換した特徴マップをそのまま出力するのではなく、特徴マップを、デコーダ側のクロスアテンションで使用するkeyとvalueにあらかじめ変換して返すようにする。

        n_layer_cross_k_list = []
        n_layer_cross_v_list = []
        for block in self.textDecoder.blocks:
            n_layer_cross_k_list.append(block.cross_attn.key(audio_features))
            n_layer_cross_v_list.append(block.cross_attn.value(audio_features))

        return torch.stack(n_layer_cross_k_list), torch.stack(n_layer_cross_v_list)
デコーダの入力変更

デコーダの入力を以下のように変更する。

    def forward(self, tokens: Tensor,
                n_layer_self_k_cache: Tensor,
                n_layer_self_v_cache: Tensor,
                n_layer_cross_k: Tensor,
                n_layer_cross_v: Tensor,
                offset: Tensor,
                ):

n_layer_self_k_cacheと、n_layer_self_v_cacheは、それぞれセルフアテンションで使うkeyとvalueで、ループのたびに末尾にkeyとvalueを追加する。
呼び出し側で、n_text_ctxの分だけ領域をあらかじめ確保して入力する。

ResidualAttentionBlockに渡す前には、スライスして渡す。

            self_k_cache = n_layer_self_k_cache[i,:,:offset[0] + tokens.shape[-1],:]
            self_v_cache = n_layer_self_v_cache[i,:,:offset[0] + tokens.shape[-1],:]
セルフアテンションの変更

セルフアテンションでは、k_cacheとv_cacheの末尾に、keyとvalueを追加する。

        k_cache[:,-k.shape[1]:,:] = k
        v_cache[:,-v.shape[1]:,:] = v
クロスアテンションの変更

クロスアテンションのkeyとvalueは、エンコーダで事前に計算したkeyとvalueを使う。
それぞれ、引数n_layer_cross_kとn_layer_cross_vでエンコーダの出力から受け取る。

ONNXへ変換

以上の変更を行い。
エンコーダ、デコーダを別々にpytorch.onnxで変換する。

torch.onnx.export(
    encoder,
    mel,
    "encoder.onnx",
    verbose=True,
    input_names=['mel'],
    output_names=['n_layer_cross_k', 'n_layer_cross_v'])
torch.onnx.export(
    decoder,
    (tokens, n_layer_self_k_cache, n_layer_self_v_cache, n_layer_cross_k, n_layer_cross_v, offset),
    "decoder.onnx",
    verbose=True,
    input_names=['tokens', 'in_n_layer_self_k_cache', 'in_n_layer_self_v_cache', 'n_layer_cross_k', 'n_layer_cross_v', 'offset'],
    output_names=['logits', 'out_n_layer_self_k_cache', 'out_n_layer_self_v_cache'],
    dynamic_axes={
                'tokens' : { 0: 'n_audio', 1 : 'n_tokens' },
                'in_n_layer_self_k_cache' : { 1: 'n_audio' },
                'in_n_layer_self_v_cache' : { 1: 'n_audio' },
                })

テスト

変換したONNXで推論できるかテストする。
Whisperで音声からテキストにする処理は、モデルの推論のみだけではなく、音声をメルスペクトログラムにする処理や、モデルの出力を埋め込みから単語に変換する処理なども実装が必要である。

モデルの推論のみをテストするため、Whisperのdecoding.pyを改造して、ONNXによる推論に書き換え、改造前の結果と一致するかを確認する。

モデルの推論処理は、_get_audio_featuresと、_detect_language、_main_loopの3か所にある。

_get_audio_featuresの変更

_get_audio_featuresを以下のように変更する。

        io_binding = self.encoder_session.io_binding()
        io_binding.bind_input('mel', device_type='cuda', device_id=0, element_type=np.float32, shape=mel.shape, buffer_ptr=mel.data_ptr())
        io_binding.bind_output('n_layer_cross_k', device_type='cuda')
        io_binding.bind_output('n_layer_cross_v', device_type='cuda')

        self.encoder_session.run_with_iobinding(io_binding)

        n_layer_cross_k, n_layer_cross_v = io_binding.get_outputs()

        return n_layer_cross_k, n_layer_cross_v
_detect_languageの変更
        logits = self.model.logits(x, mel)[:, 0]

を以下のように変更する。

        n_layer_self_k_cache = onnxruntime.OrtValue.ortvalue_from_shape_and_type((len(self.model.decoder.blocks), n_audio, self.model.dims.n_text_ctx, self.model.dims.n_text_state), element_type=np.float32, device_type='cuda', device_id=0)
        n_layer_self_v_cache = onnxruntime.OrtValue.ortvalue_from_shape_and_type((len(self.model.decoder.blocks), n_audio, self.model.dims.n_text_ctx, self.model.dims.n_text_state), element_type=np.float32, device_type='cuda', device_id=0)
        offset = torch.zeros(1, dtype=torch.int64)

        io_binding = self.decoder_session.io_binding()
        io_binding.bind_input('tokens', device_type='cuda', device_id=0, element_type=np.int64, shape=x.shape, buffer_ptr=x.data_ptr())
        io_binding.bind_ortvalue_input('in_n_layer_self_k_cache', n_layer_self_k_cache)
        io_binding.bind_ortvalue_input('in_n_layer_self_v_cache', n_layer_self_v_cache)
        io_binding.bind_ortvalue_input('n_layer_cross_k', n_layer_cross_k)
        io_binding.bind_ortvalue_input('n_layer_cross_v', n_layer_cross_v)
        io_binding.bind_cpu_input('offset', offset.numpy())
        io_binding.bind_output('logits')
        io_binding.bind_output('out_n_layer_self_k_cache')
        io_binding.bind_output('out_n_layer_self_v_cache')

        self.decoder_session.run_with_iobinding(io_binding)

        logits_onnx, n_layer_self_k_cache, n_layer_self_v_cache = io_binding.get_outputs()
        logits = torch.from_numpy(logits_onnx.numpy()[:, 0])
_main_loopの変更
                logits = self.inference.logits(tokens, audio_features)

を以下のように変更する。

                if tokens.shape[-1] > self.inference.initial_token_length:
                    # only need to use the last token except in the first forward pass
                    offset = np.array([tokens.shape[1] - 1], np.int64)
                    tokens_onnx = tokens[:, -1:]
                else:
                    offset = np.zeros(1, np.int64)
                    tokens_onnx = tokens

                io_binding = self.decoder_session.io_binding()
                io_binding.bind_input('tokens', device_type='cuda', device_id=0, element_type=np.int64, shape=tokens_onnx.shape, buffer_ptr=tokens_onnx.data_ptr())
                io_binding.bind_ortvalue_input('in_n_layer_self_k_cache', n_layer_self_k_cache)
                io_binding.bind_ortvalue_input('in_n_layer_self_v_cache', n_layer_self_v_cache)
                io_binding.bind_ortvalue_input('n_layer_cross_k', n_layer_cross_k)
                io_binding.bind_ortvalue_input('n_layer_cross_v', n_layer_cross_v)
                io_binding.bind_cpu_input('offset', offset)
                io_binding.bind_output('logits')
                io_binding.bind_output('out_n_layer_self_k_cache')
                io_binding.bind_output('out_n_layer_self_v_cache')

                self.decoder_session.run_with_iobinding(io_binding)

                logits_onnx, n_layer_self_k_cache, n_layer_self_v_cache = io_binding.get_outputs()
                logits = torch.from_numpy(logits_onnx.numpy()).to(tokens.device)

テストを実行した結果、変更前と同一の音声で同じ結果になることが確認できた。

まとめ

WhisperのモデルをONNXにする方法について記述した。

PyTorchとONNXでtorch.catの動作が異なるため、試行錯誤が必要だった。
また、スライスしたTensorを更新する場合も、PyTorchはinplaceで行われるが、ONNXだと新規メモリ割り当てが行われるという違いがあり、デバッグに時間がかかった。
ONNXの内部では、デバッガが使えないため、エラーメッセージから原因を推測して、トライ&エラーを繰り返す必要があった。

ONNXに変換できたので、スタンドアロンでリアルタイムに音声をテキストに変換するツールを作成してみたい。
また、ONNXモデルの最適化も試したい。

続き
tadaoyamaoka.hatenablog.com

続:どの駒が評価値に寄与しているかを可視化する

先日記事にした駒の働きを可視化するツール(feature_importance.py)に、SVGを出力する機能を追加した。

前回は数値で出力して、Excelの条件付き書式のカラースケールで、色に変換してから、ShogiGUIに画像として重ね合わせるということを行っていたが、手間がかかるので、SVG画像を出力することで加工なしで確認できるようにした。

ソースコード

前回はGitHub Gistで公開していたが、dlshogiのutilsに追加した。
DeepLearningShogi/feature_importance.py at master · TadaoYamaoka/DeepLearningShogi · GitHub

実行例

オプション「--svg」にSVGファイルのパスを指定する。

python -m dlshogi.utils.feature_importance model.onnx "l2r3nl/1P1+Rsk3/2nPppgp1/p5p1p/1pP2P3/PSp1s1P1P/4P4/1KG2G3/LN6L b 2B2Pgsn 85" --svg a.svg

実行には、cshogiの最新版(v0.4.9)が必要。

表示結果

SVG画像は、ブラウザで開いて確認できる。

floodgateで任意のソフト同士で対局する方法

floodgateは、30分置きに自動で対局が始まるコンピュータ将棋の対局サイトだが、任意のタイミングで2つのソフトを対局させることができる。

CSAサーバプロトコル

CSAサーバプロトコルでは、%%GAMEでgamenameを指定することで、対局条件が同じソフト同士の対局が開始する。

gamenameは、test-600-10Fのように「名前」-「持ち時間」-「1手ごと加算時間」を指定する。
1手ごと加算時間は、末尾にFを付けるとフィッシャークロックルールになる。

手番を指定する場合は、gamenameの末尾にスペースの後に「+」か「-」を指定する。

shogi-serverで対局する方法

shogi-server改造したバージョンを使用して、対局する方法について記述する。

shogi-serverのスクリプトを使用する場合は、PASSWORDで、gamenameを指定する。
,区切りの1番目がgamename、2番目がパスワードになる。

先手後手は、+-ではなく、-の後に「b」か「w」で指定する。

名前がtest、持ち時間300秒、10秒加算、先手番、パスワードfoobar1の場合の例:
test-300-10F-b,foobar1

ID、PASSWORDを環境変数で渡す場合の、スクリプトのコマンド例は以下の通り。

ID=test1 PASSWORD=test-300-10F-b,foobar1 ./usiToCsa.rb --host wdoor.c.u-tokyo.ac.jp --floodgate --ponder --keep-alive=60 --options USI_Ponder=true /work/bin/usi

同様に後手番も以下のように指定して実行すると対局が始まる。

ID=test2 PASSWORD=test-300-10F-w,foobar2 ./usiToCsa.rb --host wdoor.c.u-tokyo.ac.jp --floodgate --ponder --keep-alive=60 --options USI_Ponder=true /work/bin/usi

観戦

コンピュータ将棋対局場"の観戦ー>対局リスト: 全てのリンクで、現在対局中の一覧が表示されるので、その中に条件を指定して起動した対局も表示される。

【論文】Adversarial Policies Beat Superhuman Go AIs

[2211.00241] Adversarial Policies Beat Superhuman Go AIs

最先端のコンピュータ囲碁ソフトのKataGoの脆弱性を攻撃することで、高い勝率を上げるエージェントを訓練する方法について書かれた論文。

囲碁のルールの設定が公平でないとRedditで批判されているので、どれほど信頼できるかは不明だが、エクスプロイターの作成の参考になる。
[N] Adversarial Policies Beat Professional-Level Go AIs : MachineLearning

提案手法

  • KataGoの脆弱性を攻撃することを目的とした敵対的ポリシーを学習する
  • KataGoとの対局時に、MCTSの相手側のポリシーにKagaGoのポリシーを使う
  • エクスプロイターの手番のみを学習する

ランダムから学習した場合でも、探索なしの条件では、KataGoに99%勝利し、探索あり(2048回)で77%以上の勝率になる。

感想

RedditでKataGoの作者が、KataGoでも学習していない自己対局では現れないまれな局面(out-of-distribution)では、人間のプレーヤーによって悪用されることがあると言っている。
KataGoは、「Mi Yuting's flying dagger」のような知られている定石は、手動で訓練に追加して対策しているようだ。

ただし、十分に探索を行うことで、未知の局面でもほぼ悪用できなくなることが多いので、論文で採用されている探索回数が少なすぎではないかというようなことを言っている。

しかし、重要なのは、最先端の囲碁AIでも、超人的なパフォーマンスを発揮するのは自己対局で現れた局面に「似ている」局面であって、指数関数的な状態空間を持つ囲碁では、そのサブセットが空間の重要な部分をすべてうまくカバーしているとは限らないことだ。

将棋AIに関して

dlshogiでも、水匠との長時間思考して対局させると、中終盤で評価を誤る局面がある。
学習していない局面というよりも、何手も先で詰みが絡むような局面が多い。
自己対局ではプレイアウト数が比較的少ないため、そのような局面の最善手を学習するのは難しい。

現状有効な対策は、水匠と対局して現れたdlshogiが判断を誤る局面を抽出して、学習に追加することだと考えている。
それで学習できるのは、将棋の状態空間の中のごく一部だが、コンピュータ将棋同士の対局ではそれで十分かもしれない。