HUOXIU

Llama-2 vs. Llama-3: ミニベンチマーク(三目並べ)を使った大規模モデルの評価

編集者注:大規模言語モデルの異なるバージョンをより適切に評価・比較するにはどうすればよいでしょうか?従来の学術的なベンチマークは重要ですが、実世界のアプリケーションにおけるモデルのパフォーマンスを十分に反映していないことがよくあります。こうした背景から、本稿の著者らは、Llama-2モデルとLlama-3モデルを三目並べゲームで競わせることで、モデル評価に関する斬新で興味深い視点を独創的に提示しています。

本論文は、モデル比較のための革新的な手法を提示するだけでなく、一見単純な空間論理タスクを処理する際に、現在の大規模言語モデルが直面する課題も明らかにします。これにより、異なるパラメータスケールを持つモデル間のパフォーマンスの違い、そして新世代モデルが前世代モデルと比較してどのような進歩と欠点を持っているかを垣間見ることができます。

特に注目すべきは、70バイトのパラメータを持つ大規模モデルであっても、三目並べのような基本的なタスクを実行すると予期せぬエラーが発生することです。この発見は、大規模言語モデルの能力限界を定義するための新たな基準を提供するだけでなく、将来のモデル最適化への道筋も示しています。

この記事が読者の皆様に、AIモデルを評価する際に、一見単純そうに見えてもモデルの認知能力を深く検証できるタスクに重点を置くべきではないか、そして同時に、「三目並べ」に似た「ミニベンチマーク」をもっと設計し、より包括的かつ直感的にモデルのパフォーマンスを評価するにはどうすれば良いか、という問いへの考察を促すきっかけになれば幸いです。

著者 | ドミトリー・エリウセエフ

編纂者:岳陽

画像提供:Solstice Hannan、Unsplash( https://unsplash.com/@darkersolstice)

この記事を書く約1週間前、Metaは新しいオープンソースモデルLlama-3[1]をリリースしました。彼らはこれを「8Bおよび70Bパラメータ範囲で現在入手可能な最高のモデル」と謳っていました。HuggingFaceプラットフォームのモデルページ[2]に記載されているように、Llama-3 8BはMMLU(Massive Multitask Language Understanding)ベンチマークにおいて、Llama-2 7Bの45.7に対して66.6というスコアを記録し、Llama-2 7Bを上回りました。CommonSense QA(常識的な質問回答のためのデータセット)評価でも、Llama-3はそれぞれ72.6と57.6というスコアで競合他社を上回り、トップに立ちました。特に注目すべきは、特別な命令チューニングが施されたLlama-3 8Bモデルで、数学ベンチマークにおけるパフォーマンススコアは3.8から30.0へと飛躍的に向上しました。これは目覚ましい向上です。

学術的なベンチマークを通して大規模モデルを評価することは重要ですが、実世界におけるパフォーマンスを直接確認する方が直感的で興味深いのではないでしょうか。答えはイエスです。そして、この体験はしばしば魅力的です。2つのモデルを古典的な三目並べゲームに投入すると想像してみてください。どちらが勝つでしょうか?次のゲームセクションでは、パラメータ仕様を7B、8B、70Bとしたモデルを徹底的にテストします。同時に、モデルのパフォーマンス指標とシステム構成要件も記録します。

さっそく出発して調べてみましょう!

01 モデルの読み込み

これらのモデルを完全にテストするために、PythonライブラリLlama-cpp[3]を使用することにしました。このツールの利点の一つは、CPU環境とGPU環境の両方に適応し、GPU上で効率的に実行できることです。2つのLLMを並列に実行する必要があります。幸いなことに、Google Colabの16GB GPU環境では、7Bモデルと8Bモデルの両方がスムーズに実行できます。しかし、70Bパラメータを持つ巨大なモデルに直面した際には、CPUを使用してテストするしかありませんでした。最上位のNVIDIA A100グラフィックカードでさえ、このような巨大なモデルを2つ同時に実行するためのメモリが不足していたためです。

まず、Llama-cppをインストールし、7Bおよび8Bパラメータレベルモデルをダウンロードする必要があります。70Bパラメータレベルモデルのダウンロード手順は基本的に同じで、ダウンロードリンクが異なるだけです。

 !CMAKE_ARGS="-DLLAMA_CUBLAS=on" FORCE_CMAKE=1 pip3 で llama-cpp-python をインストール -U
!pip3 インストール huggingface-hub hf-transfer sentence-transformers
!エクスポート HF_HUB_ENABLE_HF_TRANSFER="1" && huggingface-cli ダウンロード TheBloke/Llama-2-7B-Chat-GGUF llama-2-7b-chat.Q4_K_M.gguf --local-dir /content --local-dir-use-symlinks False
!エクスポート HF_HUB_ENABLE_HF_TRANSFER="1" && huggingface-cli ダウンロード QuantFactory/Meta-Llama-3-8B-Instruct-GGUF Meta-Llama-3-8B-Instruct.Q4_K_M.gguf --local-dir /content --local-dir-use-symlinks False 

モデルをダウンロードしたら、次のステップは正式に起動することです。

 llama_cpp からインポート Llamallama2 = Llama(
model_path="/content/llama-2-7b-chat.Q4_K_M.gguf",
n_gpu_layers=-1、
n_ctx=1024,
      echo=False)llama3 = ラマ(
model_path="/content/Meta-Llama-3-8B-Instruct.Q4_K_M.gguf",
n_gpu_layers=-1、
n_ctx=1024,
エコー=False) 

次に、さまざまなプロンプト単語を処理して実行する関数を作成します。

 def llm_make_move(モデル: Llama, プロンプト: str) -> str:
""" プロンプトでモデルを呼び出します """
res = model(prompt, stream=False, max_tokens=1024, temperature=0.8) 戻り値 res["choices"][0]["text"] 

02 プロンプト

それでは、三目並べゲームを実装するコードを書いてみましょう。ボード上に「X」と「O」を交互に配置します。最初に縦、横、または斜めに2つの「X」をつなげたプレイヤーが勝ちます。

画像出典:Wikipedia( https://en.wikipedia.org/wiki/Tic-tac-toe)

これまで見てきたように、このゲームは人間にとっては非常に単純ですが、言語モデルにとっては非常に難しい場合があります。正しい動きをするには、ボードスペース、オブジェクト間の関係、さらには基本的な数学の知識を理解する必要があります。

まず、チェス盤を2次元配列としてエンコードします。また、チェス盤を文字列に変換する関数も作成します。

ボード = [["E", "E", "E"],
["E", "E", "E"],
["E", "E", "E"]]def board_to_string(board_data: リスト) -> str:
""" ボードを文字列表現に変換します """
"\n".join([" ".join(x) for x in board_data]) を返します

出力は次のようになります。

イーイーイーイーイー

これでモデルプロンプトを作成できます。

 sys_prompt1 = """三目並べゲームをプレイします。Xを置くことで動きます。
対戦相手はOを置いてプレイします。空のセルはマークされます
Eと組み合わせると、Xは空いているマスにのみ置くことができます。"""sys_prompt2 = """三目並べのゲームをします。Oを置くことで動き、
対戦相手はXを置くことでプレイします。空のセルはマークされます
E と一緒に。O は空のセルにのみ配置できます。"""game_prompt = """次の動きは何ですか? 段階的に考えてください。
各行と各列は1..3の範囲にある必要があります。
回答は JSON で {"ROW": ROW, "COLUMN": COLUMN} となります。""" 

ここでは、モデル1とモデル2それぞれに2つのプロンプトを作成しました。2つの文はほぼ同じであることがわかります。唯一の違いは、最初のモデルでは「チェス盤」に「X」を配置し、2番目のモデルでは「O」を配置することです。

Llama-2 と Llama-3 のプロンプト ワードの形式は異なります。

 template_llama2 = f"""<s>[INST]<<SYS>>{sys_prompt1</SYS>>
ボードの画像は次のとおりです。
__ボード__\n
{ゲームプロンプト}
[/INST]"""template_llama3 = f"""<|begin_of_text|>
<|start_header_id|>システム<|end_header_id|>{sys_prompt2} <|eot_id|>
<|開始ヘッダーID|>ユーザー<|終了ヘッダーID|>
ボードの画像は次のとおりです。
__ボード__\n
{ゲームプロンプト}
<|eot_id|>
<|start_header_id|>アシスタント<|end_header_id|>""" 

もちろん、これらのプロンプトを利用するために、Llama-2 用と Llama-3 用の 2 つの関数メソッドを作成することもできます。

 def make_prompt_llama2(ボード: リスト) -> str:
「ラマ2プロンプトを作成」
template_llama2.replace("__BOARD__", board_to_string(board)) を返します。make_prompt_llama3(board: List) -> str:
「ラマ3プロンプトを作成」
template_llama3.replace("__BOARD__", board_to_string(board)) を返します。 

03 ゲームのコーディング

この三目並べゲームを構築するための手がかりはすべて準備できました。いよいよコーディングフェーズに移りましょう。ある手がかりに対して、モデルにJSON形式で応答を返すように指示します。実際には、モデルは次の質問に答えることができます。

次は、セル (3, 1) の右上隅に X を配置します。{"ROW": 3,"COLUMN": 1} 

それでは、このタイプの文字列から JSON データを抽出する関数の設計を始めましょう。

 def extract_json(レスポンス: str) -> オプション[dict]:
""" 応答文字列から辞書を抽出します """
try: # モデルは時々間違いを起こします、修正: {ROW: 1, COLUMN: 2} => {"ROW": 1, "COLUMN": 2}
response = response.replace('ROW:', '"ROW":').replace('COLUMN:', '"COLUMN":') # レスポンスからjsonを抽出
pos_end = response.rfind("}")
pos_start = response.rfind("{") 例外を除いて json.loads(response[pos_start:pos_end+1]) を返します。
print(f"extract_json::cannot parse output: {exp}") 戻り値 None 

結果によると、LLaMA-2 が生成したモデルレスポンスは必ずしも有効な JSON 形式ではなく、「{ROW: 3, COLUMN: 3}」のようなモデルレスポンスが頻繁に生成されていました。上記のコードブロックに示すように、このような場合は、引用符が不足している文字列をパディングして、正しいフォーマットになるようにしました。

チェス盤の行と列の数を取得したら、それを更新できます。

 def make_move(board_data: リスト、move: 省略可能[dict]、symb: str):
「新しいシンボルでボードを更新」
row, col = int(move["ROW"]), int(move["COLUMN"]) 1 <= row <= 3 かつ 1 <= col <= 3 の場合: board_data[row - 1][col - 1] == "E" の場合:
board_data[行 - 1][列 - 1] = シンボル それ以外の場合:
print(f"間違った動き: セル {row}:{col} が空ではありません") else:
print("間違った動き: インデックスが正しくありません") 

ボードの状態を更新した後、次のステップはゲームが終了条件に達したかどうかを判断することです。

 def check_for_end_game(board_data: List) -> bool:
""" 空のセルがないかどうかを確認します """
戻り値 board_to_string(board_data).find("E") == -1def check_for_win(board_data: List) -> bool:
「ゲームオーバーかどうか確認する」
# 水平線と垂直線を確認する
indがrange(3)の場合: board_data[ind][0] == board_data[ind][1] == board_data[ind][2]かつboard_data[ind][0] != "E"の場合:
print(f"{board_data[ind][0]} 勝ち!") Trueを返す
board_data[0][ind] == board_data[1][ind] == board_data[2][ind] かつ board_data[0][ind] != "E" の場合:
print(f"{board_data[0][ind]} win!") Trueを返す
# 対角線をチェック
board_data[0][0] == board_data[1][1] == board_data[2][2] かつ board_data[1][1] != "E" または \
board_data[2][0] == board_data[1][1] == board_data[0][2] かつ board_data[1][1] != "E":
print(f"{board_data[1][1]} 勝ち!") Trueを返す
Falseを返す

このコードロジックは、チェス盤の水平線、垂直線、対角線を繰り返しチェックし、勝利側が出現したかどうかを判断します。より単純な解決策が存在する可能性もありますが、現在の要件を満たすにはこの方法で十分です。

これで必要なコンポーネントはすべて準備できました。次に、これらのコンポーネントを統合します。

 num_wins1、num_wins2 = 0、0times_1、times_2 = []、[]def run_game():
「2つのモデル間でゲームを実行する」
ボード = [["E", "E", "E"],
["E", "E", "E"],
["E", "E", "E"]]
移動制限 = 20
範囲内のステップ(moves_limit):
print(f"Step {step+1}") # 移動: モデル-1
t_start = time.monotonic()
プロンプト = make_prompt_llama2(ボード)
result_str = llm_make_move(llama2, プロンプト)
times_1.append(time.monotonic() - t_start)
new_data = extract_json(result_str)、new_dataがNoneでない場合:
make_move(board, new_data, symb="X") で、check_for_win(board):
print('**モデル1が勝利**')
                num_wins1 += 1
壊す
check_for_end_game(ボード): break
# 移動: モデル2
t_start = time.monotonic()
プロンプト = make_prompt_llama3(ボード)
result_str = llm_make_move(llama3, プロンプト)
times_2.append(time.monotonic() - t_start)
new_data = extract_json(result_str)、new_dataがNoneでない場合:
make_move(board, new_data, symb="O") の場合 check_for_win(board):
print('**モデル2が勝利**')
勝利数2 += 1
壊す
check_for_end_game(ボード): break
印刷() 

さらに、各ステップの実行時間と、ゲームで各モデルが勝利した回数も記録しました。

04件の結果

実験結果は実に興味深いものでした。

パラメータサイズが7Bと8Bのモデルでは、三目並べゲームを完了するのは非常に困難に思えます。7B Llama-2モデルはゲームのルールをある程度理解しているものの、ボードの座標の意味を正確に解釈できず、「X」マーカーの配置が不適切になることがよくあります。

以下に、プロンプトとモデル応答の具体的な例を示します。

上記の応答に基づいて、このモデル応答にはいくつかの誤りがあることがわかります。まず、チェス盤にはまだ多くの空きスペースがあります。次に、座標 (2,1) はチェス盤の中心ではありません。そして最後に、与えられた終盤では、初期位置には既に駒があり、空ではありません。

比較すると、パラメータスケール70BのLLaMA-2モデルは全体的に優れたパフォーマンスを示していますが、欠陥がないわけではなく、依然として多くのエラーが含まれています。以下は、プロンプトとモデルの応答の非常に代表的な例です。

ご覧の通り、パラメータ指定70Bのモデルは「中央」の位置を検出しましたが、関連する「駒」♟の配置決定は間違っていました。モデルは中央のマスが既に占有されていることに気づいていないようです。モデルはチェス盤の再描画も試みましたが、この「修正」も誤りでした。

皮肉なことに、ChatGPT 3.5も同じ問題に対して誤った解を出し、中心位置は{"ROW": 2, "COLUMN": 2}であると結論付けていました。しかし、LLaMA-3 70Bはこの落とし穴をうまく回避しました。それでも、同様のミスを犯しており、既にピースがあるマスに別のピースを配置してしまうことがあります。残念ながら、各モデルの具体的なエラー数はカウントしていませんが、これは今後の改善に向けて取り組む価値のある領域です。

棒グラフを使用してデータを表示すると、7B モデルと 8B モデルのパフォーマンス データはおおよそ次のようになります。

7Bおよび8Bモデルのゲームスコア、画像は著者による

結果は明らかでした。Llama-3 が 10-0 で勝利しました。

同時に、16 GB の NVIDIA T4 GPU を搭載したデバイスで 2 つのモデルの推論時間を観察できます。

7Bおよび8Bモデルの推論時間、画像提供:著者

Llama-3の唯一の欠点は、前モデルよりも動作が少し遅いことです(2.5秒対4.3秒)。しかし、実際には、ほとんどのケースでストリーミング処理が行われており、ユーザーが即時の応答を期待していないことを考えると、4.3秒という応答時間はすでに十分に優れています。

パラメータ仕様が70BのLlama-2モデルは、2回勝利するなど、より優れたパフォーマンスを発揮しました。しかし、それでもLlama-3はほとんどのケースで優位に立っていました。最終的に、推論速度に関してはLlama-3が8対2のスコアで勝利しました。

70Bモデルのゲームスコア、画像は著者による

大規模モデルの推論操作に CPU を使用する場合、CPU の計算能力と並列処理能力が比較的限られているため、当然推論速度は速くなりません。

推論時間、著者による画像

10個のゲームをクリアするのに約1時間かかります。この速度は本番環境には理想的ではありませんが、テスト環境としては許容範囲内です。興味深いことに、Llama-cppはメモリマップドファイル方式を使用してモデルをロードすることで、70Bパラメータのモデルを2つ同時に処理する場合でも、メモリ使用量を12GB以下に抑えています。これは、16GBのRAMしか搭載していないPCでも70Bパラメータのモデルを2つテストできることを示しています(残念ながら、GPUでは動作しません)。

05 結論

本論文では、2つの言語モデルグループをユニークな三目並べゲームで競わせました。興味深いことに、この一見単純な「ベンチマークテスト」は、実際には非常に難しいものでした。このテストでは、モデルがゲームのルールをどれだけ習得しているかだけでなく、座標系を使いこなし、「空間」と「抽象的思考」を文字列で表現して2次元のチェス盤をシミュレートする能力もテストされます。

結果から判断すると、LLaMA-3が明らかに勝者です。このモデルは明らかに優れたパフォーマンスを発揮しましたが、両モデルともゲーム中に多くのミスを犯したことを認めざるを得ません。この現象は示唆に富んでおり、現在の大規模言語モデルでさえ、この小規模ながらも非公式な「ベンチマーク」では苦戦する可能性があることを示唆しています。本論文で提案された「三目並べバトル」ベンチマークは、将来的に他の大規模モデルをテストするための参考資料となることは間違いありません。

記事内のリンク

[1]https://ai.meta.com/blog/meta-llama-3/

[2]https://huggingface.co/meta-llama/Meta-Llama-3-70B-Instruct

[3]https://github.com/abetlen/llama-cpp-python

読んでくれてありがとう!

------------

ドミトリー・エリウセエフ

Python/IoT 開発者、データ エンジニア、データ サイエンスとエレクトロニクス愛好家 https://www.linkedin.com/in/dmitrii-eliuseev/

オリジナルリンク: https://towardsdatascience.com/llama-2-vs-llama-3-a-tic-tac-toe-battle-between-models-7301962ca65d