以前、私は定跡生成スクリプトをPythonで書いた。書いているうちに気づいたのだが、先手なのに1手損すると後手番になるのだ。(当たり前か) そうすると、局面は後手番の定跡に突入する。つまり、せっかくそれぞれの局面を探索して定跡局面を作成しているのだから、その得られた定跡局面から盤面を180°回転させた局面は、調べたくないわけである。
この「盤面を180°回転させる(+ 手駒は先手のものと後手のものとが入れ替わる)」ことをいまflipと呼ぶことにする。
定跡を掘っていくときに、flipした局面が定跡DBにすでに登録されているなら、それは調べないという処理を書きたいわけだ。
いまPythonで将棋の定跡を掘っていて、Pythonから使える将棋の盤面操作ライブラリとしてcshogi(dlshogiの山岡さんが作っている)がある。私のスクリプトは、このcshogiを用いているのだが、cshogiには、このflipした局面を求めるような関数は用意されていない。
cshogiにプルリクして追加してもらっても良いのだが、プルリクしてもマージしてもらえるとは限らないし、cshogiは盤面操作自体は、C++で書かれたAperyという将棋ソフトに処理を丸投げしている。つまりは、flipしたければ、Aperyの方のコードを修正しないといけない。この修正は骨が折れそうである。
しかしよく考えたら、Pythonでflipするのはそんなに難しくはない。
いまどきの将棋AIではUSIプロトコルを採用しているので、将棋の局面(盤面+手駒+手番)は、SFEN文字列で表される。そこで、このSFEN文字列を与えて、flipした局面のSFEN文字列を返す関数を作れば良い。
今回はこれをChatGPT先生のお力を借りて書いていこう。
まずはUSIプロトコルの復習から。
USIプロトコル : http://shogidokoro.starfree.jp/usi.html
例えば、平手の初期局面は、SFEN文字列ではこの⇓ように表される。
lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b – 1
見てわかるように盤面の左上から順番に駒をアルファベットで書いてある。l = lance(香)、n = knight(桂)、s = silver(銀)、g = gold(金)、k = king(玉)、r = rook(飛車)、b = bishop(角)と言った具合である。駒は小文字が後手、大文字が先手である。5という数字は5升の空きがあることを意味する。/ は段の区切りで、そこで次の段に行くことを意味する。末尾の b – 1は、手番がb(=black=先手)で、- (=持ち駒なし)で 1(1手目)の局面であることを意味する。
まず、盤面部分と手番部分、手駒部分、手数部分を取り出すプログラムをPythonで書こう。
sfen = “lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b – 1”
board , turn , hands , ply = sfen.split()
これは問題ない。ChatGPT先生に尋ねるまでもない。次に盤面部分である。ここをflipさせよう。
flipさせるには、この文字列を逆順にして大文字と小文字を入れ替えれば良い。
文字列sを逆順にするのにs[::-1]とslice構文を用いているところ、よくわかっている。大文字と小文字を入れ替えるのにswapcase()を用いているところもよく知ってるなという感じ。
このようにして、わずか10秒程度で盤面をflipするところまで書けた。
board = board[::-1].swapcase()
手番はb(black = 先手)ならw(white = 後手)、wならbにしたいだけなので、
turn = ‘w’ if turn == ‘b’ else ‘b’
と書いた。この程度、ChatGPT先生の手を煩わせるまでもない。退屈な仕事は人間が率先してさせていただいて、難しい仕事だけをChatGPT先生にこなしていただくのだ。
次に手駒、これがわりとややこしい。
さきほどのUSIプロトコルの説明によると、「先手側が銀1枚歩2枚、後手側が角1枚歩3枚であれば、S2Pb3pと表記されます。」だそうだ。つまり、1枚の時の1は省略できる。先手の手駒は大文字、後手の手駒は小文字で表記される。
「S2Pb3p」これをflipすると「B3Ps2p」となる。
これ、文字列を前から見ていき、大文字から小文字に切り替わるところで分割して、その前半と後半を入れ替えてswapcase()すればいいと思うかも知れないが、手駒の枚数の数字が挟まることがある。また数字は1桁とは限らず、2桁でありうる。(手駒の歩は最大で18枚)
人間でもこの処理をどうすればエレガントに書けるのか悩むところだろう。
このプログラムをChatGPT先生はいつまで経っても書けない。
仕方がないので、ChatGPTに比べてたいへんにかしこいのやねさん(⇦賢くなさそう)が重大なヒントを与えることにしよう。
この問題は文字列を後ろから見ていくと良いのだ。後ろから見ていき、最初に大文字を見つけたら、そこで区切り、前半と後半を入れ替えてswapcase()するだけなのだ。(なぜこれでうまくいくのかは読者の皆さんは自分で考えてみてね!)
ChatGPTの生成したコード。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def transform_and_swap_advanced(s): # Finding the index 'm' from the end where the first uppercase letter appears m = next((i for i in range(len(s) - 1, -1, -1) if s[i].isupper()), None) # Splitting the string into two parts: B and W B = s[:m+1] W = s[m+1:] # Swapping the case and concatenating W and B return (W + B).swapcase() # Testing the function with the provided string transform_and_swap_advanced("S2P2b3p") |
next()とrange()を使っているところ、よくわかっている。人間でもここをこうやってささっと書ける人ばかりではないだろう。ChatGPT4、現時点でも平均的なPythonプログラマの域ははるかに超えていると感じる。
ChatGPTが出力した真上⇑のコードは、大文字が見つからない時に、m == Noneになってしまうのでちょっと良くないが、これは Noneではなく -1となるようにすればいい。ここまで出来たので、少し修正してくっつければ完成である。
1 2 3 4 5 6 7 8 9 10 |
def flip_sfen(sfen:str)->str: board , turn , hands , ply = sfen.split() board = board[::-1].swapcase() turn = 'w' if turn == 'b' else 'b' m = next((i for i in range(len(hands) - 1, -1, -1) if hands[i].isupper()), -1) hands = (hands[m+1:] + hands[:m+1]).swapcase() return f"{board} {turn} {hands} {ply}" print(flip_sfen("lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1")) |
ここまでわずか数分。このプログラム、皆さんは独力で書けるだろうか?数分で?
今回用いたChatGPT4は、本記事のようにヒントを少し与えないと書けないことはあるものの、平均的なプログラマの能力をすでに上回っていると思うし、10秒程度で書いてくれるのマジで神だし、まして自分がよく知らないプログラミング言語やプログラミング環境のことであるなら、大きな助けになってくれると感じている。
※ 本記事に掲載したプログラムはMIT Licenseとします。ご自由にお使いください。
追記 2023/12/20 21:30
盤上の駒に関して、成り駒は+pのように駒名の前に’+’記号を入れるのですが、これが逆順にしただけではp+のようになってしまっていました。(成り駒のことを忘れてました。コメント欄でご指摘いただきました。)
この部分を修正したロジックは以下のようになります。(ChatGPTはなかなか書いてくれなかったので私が書きました。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
def flip_sfen(sfen:str)->str: board , turn , hands , ply = sfen.split() # 成り駒は'+'が駒名の前につく。逆順にしているので、'+b' になるべきところが 'b+'になってしまう。 # そこで、逆順にしたあと、"b+"を"+b"に修正する。 # 逆順にする前にやるとboard[i]を見てboard[i+1]とswapすると、次のループでまたそれがswapされてまずい。 l = list(board[::-1].swapcase()) for i in range(1, len(l)): if l[i] == '+': l[i], l[i - 1] = l[i - 1], l[i] board = ''.join(l) turn = 'w' if turn == 'b' else 'b' m = next((i for i in range(len(hands) - 1, -1, -1) if hands[i].isupper()), -1) hands = (hands[m+1:] + hands[:m+1]).swapcase() return f"{board} {turn} {hands} {ply}" print(flip_sfen("l+nsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1")) |
はじめまして。
私も定跡の自動生成を行っており、
やねうら王、Ayane、cshogiを使わせていただき、Pythonで開発をしています。
私も開発当初は今回の記事のように、水平・垂直・180度反転を同一局面を見做して一つの局面をキーとして登録したかったのですが、主に2つの問題に直面しました。
・循環を含む手順をどうやってminmaxでrootへ伝播させればよいか。
・連続王手の千日手はどう扱えばよいか。
私の拙い技術ではこれを考えていたらいつまで経ってもプログラムを動作させる所までたどり着けないため、結局合流と循環は考慮せず手順をキーとして登録しています。
かれこれ採掘を続けて2年になりますが、案の定組み合わせ爆発により角換わりの駒組み手前程度までしか掘れていません
これは詰将棋解答プログラムにおいてよく問題として挙げられるGHI問題に近いものなのでしょうか。
もしそうであるならば定跡の自動生成は私には手に余る代物のようです…。
> 連続王手の千日手はどう扱えばよいか。
そんな終盤まで定跡掘らないのでまず出てこないですね…。(考慮しなくともいいような…)
> 循環を含む手順をどうやってminmaxでrootへ伝播させればよいか。
やねうら王のペタショックコマンドがそれをやっていますね。
後退解析をしてます。疑似コードで書くと以下のようにするだけですね。
MAX_PLY回繰り返す:
for node in nodes:
v = nodeのなかで一番良い指し手の評価値
for parent in node.parents:
parentからnodeに行く指し手の評価値 = v
nodesは定跡DB上のすべての定跡局面を意味します。
node.parentは、このnodeに遷移できる親nodeのlistです。
また、子nodeを持っている指し手の評価値は0(千日手スコア)で初期化されているものとします。
ありがとうございます。
こんなに簡単にできるものだったんですね…不勉強ですみません
早速改良に着手します。
独自の将棋AIを作っているのですが、gct用の既存の棋譜を元に学習させていたら局面の評価値の値が全部プラス(プラスマイナス1.0に適当な値をかけてから整数に変換してる)になるようになってしまったのですが、どういう原因が考えられますか?
最後に7エポックほど学習させた後からこういう状態になっているらしく、以前はプラスマイナスにばらけてたと思います。
詳しくはわかりませんが、学習率が高すぎてオーバーフィッティングしてるとか?
良かったら将棋AI開発者のDiscordに招待するのでメールくだされ。
なお、本記事に関係のないコメントは手動的に削除されるだす。。。
送り先のメールアドレスはどこに書いてありますか?
ABOUTに書いてありましたね。メール送りました。
この実装ではl4Ssnl/7k1/p1np1g1p1/1rp1PPp1p/3P5/2P2+bPPP/P5BK1/6S2/LN2RG2L b GSPgn3p 77のような成駒が含まれている配置の時、+が相手の駒にかかってしまい、正しく反転できない場合があるのでは?
ほんまや!!(゚д゚)
気づいてませんでした。あとで、修正致します…。ご指摘ありがとうございます。
修正案)
board = board[::-1].swapcase()
l = list(board)
for i in range(1, len(l)):
if l[i] == ‘+’:
l[i], l[i – 1] = l[i – 1], l[i]
board = ”.join(l)
ダサいっすかね…。
boardの一番最後に’+’が来ることはないので、
board = board[::-1].swapcase()
board = ”.join([y + x if y == ‘+’ else ” if x == ‘+’ else x for x, y in zip(board, board[1:])])
はどうでしょう。ちょっと見づらいですかね…
も少しトリッキーに書くと、
board = board[::-1].swapcase()
board = ”.join([(y+x)[y!=’+’:(x!=’+’)+1] for x, y in zip(board, board[1:])])
でもいけそうですね(可読性皆無)。
このロジック、内包表記で書くの、無理があるですなー。
cpt4でカスタム指示か何かのカスタマイズしてるんだろうなぁ
ここで紹介したChatGPTへの質問はほんとこのままでカスタマイズなしです。ChatGPT4、たいへん賢いですね。