TadaoYamaokaの開発日記

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

BERTで日本語の単語埋め込みを試す

京都大学が公開している日本語のWikipediaから学習したBERTのモデルを使って、単語の埋め込みを試した。

Googleが公開しているBERTのextract_features.pyを使って、Juman++v2を使って文を分かち書きして入力すると、文中の単語の埋め込みベクトルが得られる。

以下に、手順と実行結果を示す。

Juman++v2のインストール

先日の記事を参照
Juman++v2をWindowsでビルドする - TadaoYamaokaの開発日記

BERTのソースclone

BERTのソースをgit cloneする。

git clone https://github.com/google-research/bert.git

事前学習済みモデルダウンロード

ku_bert_japanese - KUROHASHI-CHU-MURAWAKI LAB
Japanese_L-12_H-768_A-12_E-30_BPE.zipをダウンロードして、適当な場所に展開する。
以下では、BERTのソースディレクトリのmodelディレクトリに展開したとする。

入力文の作成

入力する文をテキストファイルに記述して、UTF-8で保存する。

sample.txt
機動戦士ガンダムは、サンライズ制作のロボットアニメです。
ザクは、ガンダムシリーズに登場するモビルスーツです。
赤い彗星はシャア専用ザクです。
彗星は、氷や塵などでできています。
赤いイチゴは甘い。

分かち書き

入力文をJuman++v2で分かち書きする。

jumanpp_v2 --config=H:\src\jumanpp-2.0.0-rc2\model\jumandic.conf --segment r:\sample.txt -o r:\sample_s.txt

以下のように分かち書きされる。

機動 戦士 ガンダム は 、 サンライズ 制作 の ロボット アニメ です 。
ザク は 、 ガンダム シリーズ に 登場 する モビルスーツ です 。
赤い 彗星 は シャア 専用 ザク です 。
彗星 は 、 氷 や 塵 など で できて い ます 。
赤い イチゴ は 甘い 。

BERTのソース修正

BERTのソースは、そのままでは日本語の単語が1文字になってしまうため、ここの説明の通りtokenization.pyを修正する。

# text = self._tokenize_chinese_chars(text)

文と単語の埋め込みベクトルを得る

BERTのextract_features.pyを使用して、分かち書きした入力文から、文と単語の埋め込みベクトルを得る。

python extract_features.py --input_file=r:\sampel_s.txt --output_file=r:\output.json --vocab_file=model\vocab.txt --bert_config_file=model\bert_config.json --init_checkpoint=model\bert_model.ckpt --do_lower_case=False --layers=-1

引数の--layers=-1で、最終の中間層のみ出力するようにしている。
この記事によると、最後から4層までのベクトルを連結した場合が、F値が最も高くなるようだが、とりあえず最終層のみとした。
f:id:TadaoYamaoka:20190728162020p:plain

出力結果は、引数の--output_fileで指定したパスにjson形式で保存される。

output.json
{"linex_index": 0, "features": [{"token": "[CLS]", "layers": [{"index": -1, "values": [-0.593731, 0.536862, 1.262189, ...
{"linex_index": 1, "features": [{"token": "[CLS]", "layers": [{"index": -1, "values": [-0.33602, -0.053047, 0.694587, ...
{"linex_index": 2, "features": [{"token": "[CLS]", "layers": [{"index": -1, "values": [1.365805, 0.30302, 0.562358, ...
{"linex_index": 3, "features": [{"token": "[CLS]", "layers": [{"index": -1, "values": [1.169213, -0.522374, -1.460554, ...
{"linex_index": 4, "features": [{"token": "[CLS]", "layers": [{"index": -1, "values": [0.952905, 1.600134, -1.115922, ...

単語の埋め込みベクトルを取り出す

出力されたjsonファイルを読み込む。

with open(r'r:\output.json', 'r') as f:
    lines = f.readlines()

objs = []
for l in lines:
    objs.append(json.loads(l))

変数objsに各行のjsonデータが格納される。

jsonデータから文中の単語と埋め込みベクトルを抽出する。

単語の確認

単語は、jsonデータのfeaturesの単語に対応する位置の['token']に格納されている。
単語の位置の0番目は必ず[CLS]になるため、1番から始まる。

objs[0]['features'][3]['token']
Out: 'ガンダム'
すべての文の各単語埋め込みベクトルを取得

埋め込みベクトルは、featuresの単語に対応する位置の['layers'][0]['values']に格納されている。

import numpy as np

words = []
for o in objs:
    dic = {}
    for feature in o['features']:
        token = feature['token']
        dic[token] = np.array(feature['layers'][0]['values'])
    words.append(dic)

変数wordsに各行の各単語の埋め込みベクトルが格納される。

埋め込みベクトルの表示
words[0]['ガンダム']
Out: array([-9.618200e-01,  3.335200e-02,  1.011132e+00, -6.614870e-01, ...

単語のコサイン類似度の比較

BERTで取得した単語の埋め込みベクトルを使用して、単語同士の類似度を計算する。

コサイン類似度の定義

コサイン類似度を計算する関数を定義する。

def cos_sim(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))


以下に、いくつかの単語同士のコサイン類似度を計算した例を示す。

例1
cos_sim(words[0]['ガンダム'], words[1]['モビルスーツ'])
cos_sim(words[1]['ザク'], words[1]['モビルスーツ'])
cos_sim(words[0]['ロボット'], words[1]['モビルスーツ'])
cos_sim(words[4]['イチゴ'], words[1]['モビルスーツ'])
Out: 0.49544412522207004
Out: 0.5251044756373676
Out: 0.46859541657644055
Out: 0.15821825000954565

関連のある単語の類似度が高くなっている。

例2

BERTは文脈に依存した単語の意味を表現することができるので、同じ単語でも異なる文では埋め込みベクトルも異なる。

cos_sim(words[2]['彗星'], words[2]['シャア'])
cos_sim(words[3]['彗星'], words[2]['シャア'])
Out: 0.45199096516452764
Out: 0.2708140096663009

(0番から数えて)2番目の文の「彗星」の方が期待通り「シャア」と関連が高くなっている。

例3

単語の埋め込みベクトルの演算を試してみる。

cos_sim(words[1]['ザク'] + words[4]['赤い'], words[2]['シャア'])
cos_sim(words[1]['ザク'] + words[4]['甘い'], words[2]['シャア'])
Out: 0.417671520432263
Out: 0.4171861580830552

あまり期待した結果にならなかった。
単語が文脈に依存するので「甘い」が「赤い」と同じような意味になってしまったのかもしれない。
BERTの単語埋め込みベクトルは、単語の演算には適さなさそうだ。

Juman++v2をWindowsでビルドする

BERTの日本語Pretrainedモデルを試してみたくなったので、その準備として、Juman++v2のWindowsでのビルドを行った。
ほぼ公式通りなので、あまり記事にする意味はないが手順をメモしておく。

Juman++v1はWindowsに対応していなかったが、v2は公式でWindowsにも対応している。
現在rc2がリリースされているが、ビルド済みバイナリは配布されていないため自分でビルドする必要がある。

rc2のダウンロード

GitHubのリリースからv2.0.0-rc2をダウンロードして適当なディレクトリに展開する。
jumanpp-2.0.0-rc2.tar.xzの中にソースと学習済みモデルが含まれている。

Visual Studio 2017のインストール

ビルドにはVisual Studio 2017 Communityを使用する。

CMakeのインストール

ビルドツールにCMakeを使用する。CMakeWindows版をインストールして環境変数PATHを設定する。
Visual Studio 2017 CommunityのオプションでCMakeをインストールしている場合は不要)

ビルド

スタートメニューから「VS 2017用 x64 Native Tools コマンドプロンプト」を起動して、jumanpp-2.0.0-rc2.tar.xzを展開したディレクトリで以下の順にコマンドを実行する。

mkdir cmake-build-dir
cd cmake-build-dir
cmake -G "Visual Studio 15 2017 Win64" ..
MSBuild jumanpp.sln /t:build /p:Configuration=RelWithDebInfo;Platform="x64"

src\jumandic\RelWithDebInfoに実行ファイル「jumanpp_v2.exe」が生成される。

テスト

Juman++v2が対応している文字コードUTF-8のため、コマンドプロンプトのコードページをUTF-8に変更してテストする。

chcp 65001
cd src\jumandic\RelWithDebInfo
echo 魅力がたっぷりと詰まっている| jumanpp_v2 --model=..\..\..\..\model\jumandic.jppmdl

以下のように表示される。

魅力 みりょく 魅力 名詞 6 普通名詞 1 * 0 * 0 "代表表記:魅力/みりょく カテゴリ:抽象物"
が が が 助詞 9 格助詞 1 * 0 * 0 NIL
たっぷり たっぷり たっぷりだ 形容詞 3 * 0 ナノ形容詞 22 語幹 1 "代表表記:たっぷりだ/たっぷりだ"
と と と 助詞 9 格助詞 1 * 0 * 0 NIL
詰まって つまって 詰まる 動詞 2 * 0 子音動詞ラ行 10 タ系連用テ形 14 "代表表記:詰まる/つまる ドメイン:料理・食事 自他動詞:他:詰める/つめる"
いる いる いる 接尾辞 14 動詞性接尾辞 7 母音動詞 1 基本形 2 "代表表記:いる/いる"
EOS

別の例

echo 外国人参政権| jumanpp_v2 --model=..\..\..\..\model\jumandic.jppmdl
外国 がいこく 外国 名詞 6 普通名詞 1 * 0 * 0 "代表表記:外国/がいこく ドメイン:政治 カテゴリ:場所-その他"
人 じん 人 名詞 6 普通名詞 1 * 0 * 0 "代表表記:人/じん カテゴリ:人 漢字読み:音"
@ 人 じん 人 名詞 6 普通名詞 1 * 0 * 0 "代表表記:人/ひと カテゴリ:人 漢字読み:訓"
参政 さんせい 参政 名詞 6 サ変名詞 2 * 0 * 0 "代表表記:参政/さんせい ドメイン:政治 カテゴリ:抽象物"
権 けん 権 名詞 6 普通名詞 1 * 0 * 0 "代表表記:権/けん カテゴリ:抽象物 漢字読み:音"
EOS


Juman++v2は、v1に比べて250倍速くなっているようなので、未知語を多く含む場合MeCabに代わる形態素解析として使えそうな気がします。それでもMeCabよりは1桁以上遅いようです。

2019/7/28 追記

モデルは、モデルファイルを直接渡さずに、jumandic.confを引数に指定する方が良い。
jumanpp-2.0.0-rc2.tar.xzに含まれるmodel/jumandic.conf.inをmodel/jumandic.confにリネームして、--modelの行にモデルファイルのパスを設定する。
jumanpp_v2の引数には、

--config=H:\src\jumanpp-2.0.0-rc2\model\jumandic.conf

のようにjumandic.confのパスを指定する。

将棋AIの進捗 その31(cuDNNによるSENetの推論処理の実装)

dlshogiの10ブロックのWideResnetの自己対局による強化学習を続けていましたが、230サイクルほどでほぼ頭打ちになりました。

訓練損失は下がり続けていますが、floodgateの棋譜に対する損失が上昇傾向になっており、技巧2のとの勝利も上がらないため、このモデルでの強化学習は打ち切ることにしました。
技巧2(4コア CPU)との勝率は、GPU2080Ti1枚、1手3秒で32%程です。

訓練損失

f:id:TadaoYamaoka:20190724003127p:plain

floodgateの棋譜に対する損失

f:id:TadaoYamaoka:20190724003418p:plain

floodgateの棋譜に対する正解率

f:id:TadaoYamaoka:20190724003437p:plain

188サイクルあたりで訓練損失が急に下がって、floodgateの棋譜に対する損失が上昇したため、189サイクル目からはエントロピー正則化とL2正則化を加えるようにしました。
少しだけ精度が上がりましたが、すぐにほぼ横ばいになりました。
このあたりが、このモデルの強化学習の限界のようです。

SENetのモデルの学習開始

以前に将棋AIでもSENetが有効であることを実験で確かめたので、SENetのモデルで強化学習を始めることにしました。
今回も、教師ありで事前学習を行ってから、自己対局で強化学習を行う予定です。
教師データはWCSC29のAperyで生成した5億局面を使用します。
WCSC29のAperyは学習局面のフォーマットに手数が追加されていましたが、以前と同じフォーマットで生成しました。
終局近くの手順も学習したいため、対局終了の閾値は、30000としました。
引き分けも生成します。

SENetの実装

ChainerでのSENetの実装は、以前に記事にしたものと同じです。

対局時の推論処理は、cuDNNで実装しているため、cuDNNを使って実装しました。
cuDNNのAPIはChainerよりも使える機能が少ないため、どのAPIで実現できるかを調べるのに結構苦労しました。

global average poolingは、averagePooling2Dのwindowを盤面のサイズにすることで実現しました。
Squeezeの結果とチャンネルごとの積は、cudnnOpTensorのCUDNN_OP_TENSOR_MULモードで実現できました。
(FP16の場合でも計算はfloatなので、係数のalphaとbetaもfloatにする必要があることに気付かず、何時間もデバッグしていましたorz)

cuDNNで実装したい人はほとんどいないと思いますが、ソースを公開しました。
https://github.com/TadaoYamaoka/DeepLearningShogi/blob/master/usi/nn_senet10.cpp

学習結果

Aperyで生成した5億局面で学習した結果、floodgateの棋譜に対する損失と正解率は以下の通りです。
エントロピー正則化とL2正則化は、はじめから加えています。

WideResnet10ブロックをelmoで生成した4.9億局面で学習した際も比較のために載せておきます。

WideResnet SENet
policy loss 0.93350023 0.9108452
value loss 0.58716744 0.57989293
policy accuracy 0.4090266 0.415206
value accuracy 0.68762606 0.68894225

教師データの違いがあるため単純に比較できませんが、精度は上がっています。

技巧2に対する勝率

事前学習したモデルで技巧2(CPU 4コア)に対する勝率を測りました。
1手3秒で100回対局した結果は11-1-88で、勝率11%となりました。

WideResnet10ブロックのモデルで事前学習のみでは技巧2には全く勝てなかったため、技巧2に勝率11%は思ったより良い結果です。

このモデルをベースに、自己対局による強化学習を開始する予定です。
少なくともGPU1枚で技巧2を超えるようになってほしいと思っています。

【書籍】将棋AIで学ぶディープラーニングのJupyter Notebookを公開

【書籍】将棋AIで学ぶディープラーニングのJupyter NotebookをGoogle Colabで公開しました。


Google Colab上で学習を実行して、ssh経由で将棋所から実行できます。

ssh経由で実行する方法は、Notebookにも記載していますが、この記事を参照してください。

イベントの案内

平日で有料ですが、今週水曜日の20時からハンズオンを開催します。
(参加していた勉強会の主催者からの依頼で開催することになりました。)
【初心者〜中級者向け】将棋AIで学ぶディープラーニング入門ハンズオン - connpass

まだ空席がありますので、興味のある方は参加してみてください。

このJupyter Notebookを使用して、解説を行う予定です。
すべてGoogle Colabで実行できるため、ノートPCを持参いただければ実行可能です。

将棋所の実行環境はWindowsを想定していますが、monoなどで将棋所を動かさせればmacbookでも可能です。
将棋所を使わずコマンド実行だけであれば、ブラウザさえあれば実行できます。

Google Colabに鍵ペアを使用してsshでログインする

Google Colabは、基本的にJupyter Notebookから使用します。

しかし、ngrokというサービスを使用することで、sshで接続する方法が知られています。
How can I ssh to google colaboratory VM? - Stack Overflow

上記サイトの方法では、sshでログインする際にパスワードが必要になりますが、鍵ペアを使用してログインしたかったため、試したところ接続することができましたので、方法を載せておきます。

なお、鍵ペアでアクセスしたい理由は、Google Colabで将棋AIのUSIエンジンを実行したかったためです。
将棋所からバッチファイル経由でssh接続する場合、パスワードを入力することができないため、パスワードなしでログインする必要があります。
sshコマンドは、引数からパスワードを渡すことができませんが、鍵ペアを使用するとパスワードなしでログインすることができます。

ngrok経由でsshdを起動

上記のサイトの方法に、鍵ペアでのログインを許可する設定を加えて、sshdを起動します。
加えたのは、

! echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config

の行です。
この時点では、まだクライアントの鍵の設定は行っていません。

#Generate root password
import random, string
password = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(20))

#Download ngrok
! wget -q -c -nc https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip
! unzip -qq -n ngrok-stable-linux-amd64.zip
#Setup sshd
! apt-get install -qq -o=Dpkg::Use-Pty=0 openssh-server pwgen > /dev/null
#Set root password
! echo root:$password | chpasswd
! mkdir -p /var/run/sshd
! echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
! echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config
! echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
! echo "LD_LIBRARY_PATH=/usr/lib64-nvidia" >> /root/.bashrc
! echo "export LD_LIBRARY_PATH" >> /root/.bashrc

#Run sshd
get_ipython().system_raw('/usr/sbin/sshd -D &')

#Ask token
print("Copy authtoken from https://dashboard.ngrok.com/auth")
import getpass
authtoken = getpass.getpass()

#Create tunnel
get_ipython().system_raw('./ngrok authtoken $authtoken && ./ngrok tcp 22 &')
#Print root password
print("Root password: {}".format(password))
#Get public address
! curl -s http://localhost:4040/api/tunnels | python3 -c \
    "import sys, json; print(json.load(sys.stdin)['tunnels'][0]['public_url'])"

実行すると、

Copy authtoken from https://dashboard.ngrok.com/auth

というリンクが表示されるので、リンク先にアクセスしてトークンをクリップボードにコピーして、入力欄に入力します。
初回アクセス時はngrokのサインインが必要です。

成功すると、以下のようにrootのパスワードと、URLが表示されます。パスワードとポートは毎回異なります。

Root password: mwbVRS9XnegtI3AVp3n7
tcp://0.tcp.ngrok.io:12451

クライアントで鍵ペア作成

sshで接続するクライアントで、鍵ペアを作成します。(作成済みの場合は、実行不要です。)
Windows 10 バージョン 1803 以降では OpenSSH クライアントがデフォルトでインストールされています。

ssh-keygen -t rsa

パスフレーズを聞かれますが、そのままEnterで問題ありません。

C:\Users\username\.ssh\に、

が作成されます。id_rsa秘密鍵なので、絶対に外に漏らさないようにしましょう。

公開鍵のアップロード

Google Colabのファイルブラウザ(左ペインからファイルを選択)から、アップロードをクリックして、作成したid_rsa.pubをアップロードします。
フォルダを変更していなければ、/content/にアップロードされます。

パスワードを使用してssh接続して、公開鍵を認証済みに設定する

クライアントから、パスワードを使用してssh接続します。

ssh root@0.tcp.ngrok.io -p 12451

-pオプションで指定するポートは、sshdを起動した際に表示されたポート番号です。
パスワードを聞かれますので、sshdを起動した際に表示されたパスワードを入力します。

rootのホームディレクトリに.sshというディレクトリを作成し、アップロードしたid_rsa.pubを/root/.ssh/authorized_keysに移動します。

mkdir /root/.ssh
mv /content/id_rsa.pub /root/.ssh/authorized_keys

これで、鍵ペアを使用してログインできるようになります。
一度sshをログアウトして、再度ログインする際には、パスワードを聞かれません。

学習中と並列で自己対局

先日の日記で、学習中にも自己対局を行うようにしたことを書いたが、現在dlshogiでは自己対局で生成した局面は過去10サイクル分をすべて学習に使うようにしている。
(AlphaZeroのようにサンプリングしないですべての局面を使うのは、Actor-Criticで敗着となった手(学習したい手)を学習する機会が減りそうという理由からだが、どっちがよいかは分かっていない。)

そのため、学習時間に依存して生成する局面が増えるので、次のサイクルで学習する局面が増えて、それにより学習時間も増える。
そうすると生成する局面も増えるので、どんどん学習時間伸びていくのではないかという素朴な疑問が起きる。

学習する局面に対して、学習中に自己対局で生成する局面は相対的に少ないので、直感的に収束しそうだが、念のため計算してみた。

以下、数学の問題です。高校数学を復習したい方はどうぞ。

収束判定問題

純化のために、過去2サイクル分だけ学習に使うとした場合、Tサイクル目の学習時間t_Tと学習中に生成する局面x_Tは、
 \displaystyle
t_T=a(x_{T-1}+c) \\
x_T=bt_T
となる。ここで、aは学習速度(時間/局面)、bは局面生成速度(局面/時間)、cは1サイクルの学習中以外で生成する局面数(固定値)。

x_{T-1}=bt_{T-1}を、上式のt_{t-1}に代入すると、
 \displaystyle
t_T=a(bt_{T-1}+c)
となる。

よって、T=0のとき、
 \displaystyle
t_0=ac

T=1のとき、
 \displaystyle
\begin{eqnarray}
t_1&=&a(bt_0+c) \\
&=& ac(ab+1)
\end{eqnarray}

T=2のとき、
 \displaystyle
\begin{eqnarray}
t_1&=&a(bt_1+c) \\
&=& ac(ab(ab+1)+1) \\
&=& ac((ab)^2+ab+1)
\end{eqnarray}

T=3のとき、
 \displaystyle
\begin{eqnarray}
t_3&=&a(bt_2+c) \\
&=& ac(ab(ab(ab+1)+1)+1) \\
&=& ac((ab)^3+(ab)^2+ab+1)
\end{eqnarray}

となる。
つまり、
T=nのとき、
 \displaystyle
\begin{eqnarray}
t_n&=&a(bt_{n-1}+c) \\
&=& ac((ab)^n+(ab)^{n-1}+\cdots+ab+1)
\end{eqnarray}
となり、等比級数和の形になっている。

等比級数の和は、高校の数学で習った通り(忘れてたけど)、
 \displaystyle
\frac{1-(ab)^n}{1-ab}
となるので、|ab|<1のときに、
 \displaystyle
\frac{1}{1-ab}
となり、収束する。

dlshogiでは、学習速度aは、3.9 \times 10^{-4}(秒/局面)、学習中の局面生成速度bは、39.6(局面/秒)なので、
 \displaystyle
ab\simeq0.015<1
となり収束する。

10サイクルにした場合も、基本的に同じ問題に落とし込めるので省略する。

WindowsでプロセスにSIGINTを送る

将棋AIの自己対局のプログラムで途中で中断(Ctrl+C)した場合に、それまでに生成した局面を安全にファイルに書き出すためにSIGINTをハンドリングしている。
ただし、今までは、基本的に途中で中断しないで、生成する局面の数を引数として、自己対局完了→学習の順にシーケンシャルに処理していた。

現在1台のPCにGPUを3枚刺して使用しており、学習は1枚のGPUで行っているため、学習中に他の2枚が無駄になっている。
そこで、GPUを有効活用するため、学習中にも自己対局を行い、学習完了時にSIGINTを送信して自己対局を安全に中断するようにした。

なお、自己対局のプログラムが新しいモデルがあれば取りに行くような仕組みの方がエレガントだが、一連の処理をWindowsのバッチファイルで処理するダサい仕組みになっている。

以下、処理方式に関する説明です。

SIGINTの受信処理

C++では、

#include <signal.h>

volatile sig_atomic_t stopflg = false;

void sigint_handler(int signum)
{
	stopflg = true;
}

int main() {
    signal(SIGINT, sigint_handler);
    // ...
}

のようにして、SIGINTをハンドリングできる。

学習と自己対局を並列で実行

バッチファイルでプログラムを並列で実行するために、startコマンドで自己対局を別のコマンドプロンプトで起動してから学習を行うようにした。
ログをリダイレクトで出力していたため、直接自己対局プログラムを起動するのではなく、cmdコマンドを起動してその引数に実行するコマンド行を記述するようにした。

start cmd /c "make_hcpe_by_self_play --mate_depth 25 --mate_nodes 100000 --threashold 1 d:\model\model_rl_val_wideresnet10_selfplay_%M% C:\hcp\roots12.hcp D:\selfplay\selfplay-%N%_org3 100000 1000 1 256 2 256 >> d:\log\%date:~0,4%%date:~5,2%%date:~8,2%_make_hcpe_by_self_play_%N%.txt"

SIGINTの送信

学習完了後に、並列で実行中の自己対局プログラムを終了するために、SIGINTを送信する。
Windowsで標準でSIGINTを送信する方法がないため、windows-killというコマンドを使用した。
なお、標準のプロセスを終了するコマンドとしてtaskkillコマンドがあるが、SIGINTは送信できない。

windows-killコマンドは引数がPIDになっているため、tasklistコマンドでPIDを取得する必要がある。

for /f "tokens=2 delims=, usebackq" %%i in (`tasklist /nh /fo csv /fi "imagename eq make_hcpe_by_self_play.exe"`) do set pid=%%i

これで、変数pidに自己対局プログラムのPIDが設定されるので、

C:\bin\windows-kill\windows-kill.exe -SIGINT %pid%

のようにしてSIGINGを送信する。

その後、自己対局プログラムが終了するのを待つ必要があるため、powershellのWait-Processを使って待つようにした。

powershell -Command Wait-Process -Id %pid%


以上、バッチファイルでプロセスの並列処理を頑張る方法についてでした。
たぶん、バッチファイルで頑張るよりPythonとかで作った方がよいです。