redditを眺めていたら気になる投稿があった。「What Happens When LLMs Play Chess? And the Implications for AGI」というタイトルだった。LLMにチェスをプレイさせるとどうなるか、という内容のようだ。著者のブログにはStockfishと複数のLLMのELOレーティングが掲載されていて、LLMとStockfishとのレーティング差は、ざっくりR-400となっている。ということはStockfish相手に10回に1回は勝てるということか?
Stockfishは将棋AIのやねうら王も参考にするぐらいのチェスの最強ソフトで、そんな強い相手にLLMはチェスで勝てるのか?? いやそれ以前にLLMはチェスをプレイできるのか???
おもしろそうなので検証してみることにした。
主なソース
GitHubにソースが公開されている。
作者のブログ。こちらに上記のELOレーティングのグラフがある。
作者はチェス愛好家で、プログラムを開発した動機について以下のように述べている。
(Google翻訳)
私はここ数年、生成 AI 技術にかなり取り組んできたので、LLM がチェス ゲームでどのように活躍するかを見てみたかったのです。
LLM 間でチェス競技会を開催するというアイデアは、2 つの理由で興味深いと思いました。
- まず、汎用 LLM はチェスで優秀になるように特別に訓練されていないため、チェスを使用して、彼らの出現する戦術的および戦略的計画能力をテストするのは良いことだと思いました。明らかに、LLM は事前トレーニングの段階でインターネットで利用できるチェス ゲームをいくつか記憶しています。しかし、ここで注目すべきは、汎用 LLM がチェスで優秀になるようにするための目的関数がないことです。これにより、LLM の出現する思考および計画能力をテストする可能性が開かれます (少なくともチェスに関しては)。
- 2 番目に、チェス競技会は LLM を評価し、LLM の戦術的および戦略的計画能力に関する新しい種類のリーダーボードを作成するための良い方法でもあると思いました。
テスト環境構築
1)テスト専用のPython環境をvenvで作成する
$ puthon3 -m venv Chess
2)テスト環境を立ち上げて、必要なプログラムを導入する
$ source Chess/bin/activate (Chess) $ sudo apt install stockfish (Chess) $ git clone https://github.com/fsndzomga/chess_tournament_nebius_dspy.git (Chess) $ cd chess_tournament_nebius_dspy (Chess) $ pip install -r requirements.txt
3)自分の環境に合わせてソースを修正する
作者はOpenAIのサービスでGPT-4oなど、Nebius AI studioというサービスでLlama3.1などのLLMを利用している。私はローカルでLLMを動作させたいので、ollamaを使うように変更した。
使用するモデルをmodels.pyで指定する。使用モデルはLlama3.1-8BとLlama3.2-3Bとした。当初はnemotron-70Bを使おうとしたが、70Bクラスでは1手ごとに長考するため、まずは動作確認として軽めのモデルを使用した。
テスト結果
main.pyでゲーム数とトーナメントモードを指定する。ゲーム数の指定は3回、トーナメントモードは「vs_stockfish」(Stockfishとの対抗戦形式)とした。Llama3.1-8B vs Stockfishのゲームが先後入れ替えで計6回、Llama3.2-3B vs Stockfishのゲームも先後入れ替えで計6回行われる。
ゲーム内容はチェスライブラリの機能によって、端末に以下のように表示される。最初のゲームはLlama3.2-3B vs Stockfishで、23手でStockfishの圧勝となった。
(Chess) $ python main.py Game 1 between stockfish (White) and meta-llama/Meta-Llama-3.2-3B (Black) stockfish plays: e2e4 r n b q k b n r p p p p p p p p . . . . . . . . . . . . . . . . . . . . P . . . . . . . . . . . P P P P . P P P R N B Q K B N R Stockfish: [Move.from_uci('c7c5'), Move.from_uci('c7c6'), Move.from_uci('e7e5')], LLM: g8h6, Score: 152 Centipawn loss: 184 meta-llama/Meta-Llama-3.2-3B plays: Nh6 r n b q k b . r p p p p p p p p . . . . . . . n . . . . . . . . . . . . P . . . . . . . . . . . P P P P . P P P R N B Q K B N R stockfish plays: d2d4 r n b q k b . r p p p p p p p p . . . . . . . n . . . . . . . . . . . P P . . . . . . . . . . . P P P . . P P P R N B Q K B N R Stockfish: [Move.from_uci('c7c6'), Move.from_uci('d7d5'), Move.from_uci('e7e6')], LLM: h6f5, Score: 858 Centipawn loss: 1012 meta-llama/Meta-Llama-3.2-3B plays: Nf5 r n b q k b . r p p p p p p p p . . . . . . . . . . . . . n . . . . . P P . . . . . . . . . . . P P P . . P P P R N B Q K B N R stockfish plays: e4f5 r n b q k b . r p p p p p p p p . . . . . . . . . . . . . P . . . . . P . . . . . . . . . . . . P P P . . P P P R N B Q K B N R Stockfish: [Move.from_uci('d7d5'), Move.from_uci('b8c6'), Move.from_uci('e7e5')], LLM: b8c6, Score: 969 Centipawn loss: 1822 meta-llama/Meta-Llama-3.2-3B plays: Nc6 r . b q k b . r p p p p p p p p . . n . . . . . . . . . . P . . . . . P . . . . . . . . . . . . P P P . . P P P R N B Q K B N R stockfish plays: f1d3 r . b q k b . r p p p p p p p p . . n . . . . . . . . . . P . . . . . P . . . . . . . B . . . . P P P . . P P P R N B Q K . N R Stockfish: [Move.from_uci('c6d4'), Move.from_uci('d7d5'), Move.from_uci('e7e5')], LLM: c6b4, Score: 938 Centipawn loss: 1802 meta-llama/Meta-Llama-3.2-3B plays: Nb4 r . b q k b . r p p p p p p p p . . . . . . . . . . . . . P . . . n . P . . . . . . . B . . . . P P P . . P P P R N B Q K . N R stockfish plays: b1c3 r . b q k b . r p p p p p p p p . . . . . . . . . . . . . P . . . n . P . . . . . . N B . . . . P P P . . P P P R . B Q K . N R Stockfish: [Move.from_uci('d7d5'), Move.from_uci('g7g6'), Move.from_uci('b4d3')], LLM: b4d5, Score: 1096 Centipawn loss: 1990 meta-llama/Meta-Llama-3.2-3B plays: Nxd5 r . b q k b . r p p p p p p p p . . . . . . . . . . . n . P . . . . . P . . . . . . N B . . . . P P P . . P P P R . B Q K . N R stockfish plays: c3d5 r . b q k b . r p p p p p p p p . . . . . . . . . . . N . P . . . . . P . . . . . . . B . . . . P P P . . P P P R . B Q K . N R Stockfish: [Move.from_uci('e7e6'), Move.from_uci('c7c6'), Move.from_uci('d7d6')], LLM: a7a6, Score: 1228 Centipawn loss: 2324 meta-llama/Meta-Llama-3.2-3B plays: a6 r . b q k b . r . p p p p p p p p . . . . . . . . . . N . P . . . . . P . . . . . . . B . . . . P P P . . P P P R . B Q K . N R stockfish plays: g1e2 r . b q k b . r . p p p p p p p p . . . . . . . . . . N . P . . . . . P . . . . . . . B . . . . P P P . N P P P R . B Q K . . R Stockfish: [Move.from_uci('e7e6'), Move.from_uci('g7g6'), Move.from_uci('e7e5')], LLM: d7d6, Score: 1192 Centipawn loss: 2281 meta-llama/Meta-Llama-3.2-3B plays: d6 r . b q k b . r . p p . p p p p p . . p . . . . . . . N . P . . . . . P . . . . . . . B . . . . P P P . N P P P R . B Q K . . R stockfish plays: e2g3 r . b q k b . r . p p . p p p p p . . p . . . . . . . N . P . . . . . P . . . . . . . B . . N . P P P . . P P P R . B Q K . . R Stockfish: [Move.from_uci('c7c6'), Move.from_uci('h7h5'), Move.from_uci('e7e5')], LLM: c8f5, Score: 1309 Centipawn loss: 2428 meta-llama/Meta-Llama-3.2-3B plays: Bxf5 r . . q k b . r . p p . p p p p p . . p . . . . . . . N . b . . . . . P . . . . . . . B . . N . P P P . . P P P R . B Q K . . R stockfish plays: d3f5 r . . q k b . r . p p . p p p p p . . p . . . . . . . N . B . . . . . P . . . . . . . . . . N . P P P . . P P P R . B Q K . . R Stockfish: [Move.from_uci('c7c6'), Move.from_uci('e7e6'), Move.from_uci('g7g6')], LLM: d8c8, Score: 1799 Centipawn loss: 3108 meta-llama/Meta-Llama-3.2-3B plays: Qc8 r . q . k b . r . p p . p p p p p . . p . . . . . . . N . B . . . . . P . . . . . . . . . . N . P P P . . P P P R . B Q K . . R stockfish plays: f5c8 r . B . k b . r . p p . p p p p p . . p . . . . . . . N . . . . . . . P . . . . . . . . . . N . P P P . . P P P R . B Q K . . R Stockfish: [Move.from_uci('a8c8'), Move.from_uci('e7e6'), Move.from_uci('e8d8')], LLM: a8a7, Score: None Centipawn loss: 0 meta-llama/Meta-Llama-3.2-3B plays: Ra7 . . B . k b . r r p p . p p p p p . . p . . . . . . . N . . . . . . . P . . . . . . . . . . N . P P P . . P P P R . B Q K . . R stockfish plays: d1g4 . . B . k b . r r p p . p p p p p . . p . . . . . . . N . . . . . . . P . . Q . . . . . . . N . P P P . . P P P R . B . K . . R Stockfish: [Move.from_uci('e7e6'), Move.from_uci('f7f5'), Move.from_uci('f7f6')], LLM: a6a5, Score: None Centipawn loss: 0 meta-llama/Meta-Llama-3.2-3B plays: a5 . . B . k b . r r p p . p p p p . . . p . . . . p . . N . . . . . . . P . . Q . . . . . . . N . P P P . . P P P R . B . K . . R stockfish plays: g4d7 . . B . k b . r r p p Q p p p p . . . p . . . . p . . N . . . . . . . P . . . . . . . . . . N . P P P . . P P P R . B . K . . R stockfish rating: 1500.0, meta-llama/Meta-Llama-3.2-3B rating: 1500.0 meta-llama/Meta-Llama-3.2-3B rating: 1228.784, stockfish rating: 1771.216 writing to csv {'game_number': 1, 'white': 'stockfish', 'black': 'meta-llama/Meta-Llama-3.2-3B', 'winner': 'stockfish', 'white_rating': 1500, 'black_rating': 1500}
全てゲームが終わると結果が表示された。
Final Results: meta-llama/Meta-Llama-3.1-8B: Wins=0, Losses=6, Draws=0, Rating=1500.00 meta-llama/Meta-Llama-3.2-3B: Wins=0, Losses=6, Draws=0, Rating=1500.00 stockfish: Wins=12, Losses=0, Draws=0, Rating=1500.00
LLMの全敗である。これは当然の結果だろう。それよりもLLMが普通にチェスのゲームをしていることにまず感動する。これはどういう仕組みなのだろう。ソースのchess_model.pyの中でLLMに対して以下のようにプロンプトを投げている。
def get_raw_response(self, board_state, legal_moves, history, feedback): if self.provider == 'stockfish': board = chess.Board(board_state) result = self.client.play(board, chess.engine.Limit(time=0.1)) return result.move.uci() prompt = f""" You are a chess grandmaster. Given the current state of the chess board: {board_state} Legal moves: {legal_moves} History of moves so far: {history} Feedback on the previous move: {feedback} Generate the next move and explain your reasoning concisely. The move should be in a <move> tag """ while True: if self.provider == 'openai': self.client = dspy.LM(f"openai/{self.model_id}") response = self.client( messages=[{"role": "user", "content": f"{prompt}"}] ) return response[0] response = self.client.chat.completions.create( model=self.model_id, messages=[{"role": "user", "content": f"{prompt}"}] ) try: response.choices[0].message.content break except Exception as e: print(e) continue print(prompt) print(response.choices[0].message.content) return response.choices[0].message.content
プロンプトの内容を翻訳すると以下のようになる。
あなたはチェスのグランドマスターです。チェス盤の現在の状態は次のとおりです: {board_state} 有効な動き: {legal_moves} これまでの動きの履歴: {history} 前回の動きのフィードバック: {feedback} 次の動きを生成し、その理由を簡潔に説明してください。動きは <move> タグ内に記述する必要があります。
プリント文を入れて内容を覗いてみた。Llama3.1-8Bが先手である。
FEN形式でボードの状態が渡され、先手の合法手がLegal movesで渡されている。
自分の手を決定し、その理由を述べている部分がこちら。
(Google翻訳)
私はNc3から始めます。これは、キングのナイトで中央をコントロールする堅実な中央ポーンプッシュです。この動きは、他の駒の潜在的な展開に備え、ポジションを安定させるのに役立ちます。
実際の手は「Nh3」として、説明は「Nc3」言っているのは変だが、説明はもっともらしいことを言っている。
3手目の説明はさらに本格的である。
(Google翻訳)
理由: 現在のポーン構造は、いくぶん対称的で、やや「ベノーニ型」の位置を示しています。私は白でプレイしています。相手の駒は d7-d5 でクイーンサイドに反撃するのに適した位置にいるので、キングサイドの展開に焦点を当てるのが賢明と思われます。私は、黒の f 列のコントロールに挑戦し、より速い集中化をもたらす可能性のある小さな駒の交換に備えることを目指しています。Nf4 はこれに役立つだけでなく、e4 にもプレッシャーをかけ、白にいくらかの柔軟性をもたらします。
私はチェスには全く詳しくないので、このLLMの解説が正しいのか分からない。「ベノーニ型」という専門用語は確かにあるようだが、この戦型が「ベノーニ型」なのかも分からない。しかし、将棋で言えば「相手が矢倉で囲ってくるようなのでこちらは・・・」という本格的な解説を聞いているような気がする。
本日のまとめ
8BクラスのLLMのチェスの説明はプロ並みであるが、Stockfishには勝てそうにない。次は70BクラスのLLMで試してみたい。10回ゲームさせるのに我が1000ドルPCでは多分数時間かかるだろう。
GroqのOpenAIサーバがうまく機能すれば、Groqの爆速のLlama3.1-70Bが使えるはずなので、それも試してみたい。
チェスライブラリを将棋ライブラリに入れ替え、SFEN形式で棋譜を渡せば、将棋の対局もできる気がする。しかも数本のPythonのプログラムだけでできてしまう。そこまで自分で試してみたい気もするが、試すのが怖い気もする。なぜなら私は将棋が強くないので、LLMに負ける可能性がある(笑)。将棋AIが人間より10万倍強いことは理解している。しかし将棋の学習をしていない、評価関数も持っていないLLMに将棋で負けるとしたら意味合いが違ってくる。作者の言うところの「戦術的および戦略的計画能力」でLLMより劣っていることが証明されてしまうのだ。
まあ、そうなったらそうなったで、割り切ってLLMに将棋を教えてもらうのもいいのかもしれない。70Bクラスの結果については、検証でき次第本ブログで報告したい。