最近はますます小型LLMの開発競争が熾烈さを増している。何故か?というと今まで散々書いてきた通り、チャットベクターや進化的マージでLLMの性能が引き出せることが分かったし、それらの操作には計算資源はほとんど不要だから、個人でも参入できると分かったからだ。

Twitter上の目ぼしいメンツはみんな進化的マージに着手してるまである。当然だろう。敢えてやらない理由が見つからない。

そういう中で、「LLM開発者ごとに自称する性能がチグハグすぎるぞーっ!もっと公平に評価できる環境を用意しろ!」というような声も上がり始めてる。
まあ、一理あるなとは思う。私だってもっと適切なLLM評価の方法は色々探っていたのだが、LLMの評価というのは本腰入れてやろうとすれば、それだけで一つの職業になるくらい手間ヒマがかかる。

で、今回はShisa-AIのlhlさんが公開してるShaberiベンチマークを試してみたいと思う。このベンチマークは元々はLightBlueが公開してたjapanese_llm_evalリポジトリをlhlさんがフォークしてカスタムしたものだ。

shisa-ai/shaberi: Lightblue LLM Eval Framework: tengu, elyza100, ja-mtbench, rakuda (github.com)

Shaberiベンチマークでは以下の4つのベンチマークをまとめて評価できる
①ElyzaのElyzaTasks100。LLMに様々なジャンルの質問して、その回答を1~5点で評価する。問題は100問。模範解答や採点基準が詳細に作り込まれている。個人的には結果を信用してるベンチマーク
②LightBlueのTenguBench。LLMに様々なジャンルの質問して回答を1~10点で評価する。問題は120問。模範解答はあったりなかったりする。採点基準が作り込まれてる。
japanese-mt-bench-oneshot。lmsysが提供するMT-Benchを日本語化して、さらにマルチターンからシングルターン回答に簡易化させたもの。LLMに様々なカテゴリの質問して回答を1~10点で評価する。問題は60問。
④YuzuAIのrakuda-questions。LLMに様々なカテゴリの質問して回答を1~10点で評価する。問題は40問。

何故これらのベンチマークが採用されてるのか?というと、LLMに自由回答させてその回答を評価する形式の日本語ベンチマークが集められてる。このようなベンチマークでなければLLMのチャット能力は測れない。LLMの喋り能力を評価できるからShaberiベンチマークというわけだ。

ShaberiベンチマークはGPT-4Tで回答を採点する想定で作られているが、合計320問の回答をGPT-4Tで採点してたらAPI代が500円以上かかると思う。たけー。
LLMでジャッジするならGPT-4T一択!みたいな声もあるが、誰が何と言おうが私はLLMの評価なんかにイチイチ金を払いたくない。GPT-4Tの採点が見たいなら見たい人が自分でベンチ評価取っていただきたい。

私はこれまでもっぱらLlama3-70Bで採点してきた。というのもローカルで採点するのにベストなモデルだったからだ。しかし、今ならGemini1.5FlashのAPIがタダでいっぱい叩ける。
だから今回はGemini1.5Flashを採用してみよう。

で、ShaberiベンチマークでGemini1.5Flashによる評価ができるように改造したのが以下のリポジトリだ↓

umiyuki/shaberi: Lightblue LLM Eval Framework: tengu, elyza100, ja-mtbench, rakuda (github.com)

そういうわけで、ここからは改修したShaberiベンチマークの使い方を説明する。
まず、環境はwindowsだが、vLLMも使うのでWSLを使用する。

とりま、condaとかで仮想環境を作る。
conda create -n shaberi python=3.11
conda activate shaberi

リポジトリをクローンする
git clone https://github.com/umiyuki/shaberi.git
cd shaberi

pytorch、google-generativeai、requirements.txtなどインスコする。これでLiteLLMやvLLMも入る。
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
pip install -q -U google-generativeai
pip install -r requirements.txt

環境変数にGOOGLE_API_KEYとしてGeminiのAPIキーを設定する。色々方法はあるが、とりまここでは.profileに追記する。まずnanoで.profileを開く。
nano ~/.profile

.profileの末尾にAPIキーを追記する。↓
export GOOGLE_API_KEY=<apiキー>

Ctrl+Oで上書き保存して、Ctrl+Xでnanoを閉じる。
.profileを適用する。↓
source ~/.profile

で、次は評価したいLLMをOpenAIの互換サーバとして起動する。例えばvLLMで互換サーバを起動するにはこんな感じ↓
python -m vllm.entrypoints.openai.api_server \
--model THUDM/glm-4-9b-chat \
--chat-template glm-4-9b-chat.jinja \
--trust-remote-code \
--gpu-memory-utilization 0.9 \
--max-model-len 4096

さて、–modelとして評価したいモデルをhuggingfaceのモデル名で指定するのはいいんだが、問題は–chat-templateとしてjinjaテンプレートを与えてる部分だ。イチイチ評価したいモデル毎にワケ分からん記法のjinjaテンプレートを書くというクソ面倒な作業が必要になる。まあしゃーない。

まず、大抵のモデルはhuggingfaceでチャットテンプレートが書かれてる。モデルのtokenizer_config.jsonを開いて、下の方に”chat_template”があれば、その文字列をコピペしてファイルに保存すればいい。

だがtokenizer_config.jsonにチャットテンプレートが無いケースもある。例えばTHUDM/glm-4-9b-chatには無い。こうなるとダルいのだが、こういう場合はchat_templates/get_chattemplate.pyを参考にチャットテンプレートを抽出して欲しい。↓

これを実行するとglm-4-9b-chatのチャットテンプレートが出力される↓

上の方法が使えない場合は、残念ながら自前でチャットテンプレートを書くしかない。まあAIに書いてもらえばいいかもしれないが、その場合も最終的に出力されるプロンプトが正しいかどうか確認して欲しい。chat_templates/jinja_test.pyが使用できる↓
python jinja_test.py ChatNTQ.jinja

これで、以下のように出力されて正しいプロンプトが確認できる↓

jinjaテンプレートの用意はこれでできたとして、vLLMでOpenAI互換サーバも立てた。
次はShaberiベンチマークのLLM回答を生成する。サーバを起動したターミナルはそのままにして、新しいターミナルを開いてこうする↓
python generate_answers.py --model_name THUDM/glm-4-9b-chat -fp 0.5

–model_nameにはVLLMサーバの引数と同じものを指定する。-fpというのは繰り返しペナルティだが、0.5がちょうどいいらしいのでそうする。
これで320問の問題に対してガンガン生成が始まる。

ところで、vLLMは高速推論できるのはいいんだが、量子化しないとVRAMに収まらないモデルや量子化してもVRAMに収まらない大型モデルを評価したい時はどうする?そういう時はLlama.cppサーバを使えばいい。Llama.cppサーバにもOpenAI互換性はある。
こういう感じでサーバ立てればいいらしい↓
./server -ngl 99 -c 8192 -m shisa-v1-llama3-70b.Q4_K_M.gguf --chat-template llama3 --host 0.0.0.0 --port 8000 -a shisa-v1-llama3-70b.q4_k_m

Llama.cppサーバでは–chat-template引数でチャットテンプレートが与えられるが、あらかじめ用意されてるいくつかのパターンしか指定できない。それ以外のテンプレートを使いたければ、Shaberiのスクリプトを弄ってサーバに入力するプロンプト自体を加工するしかないかもしれない。
https://github.com/ggerganov/llama.cpp/wiki/Templates-supported-by-llama_chat_apply_template

ちなみにOllamaサーバにもOpenAI互換性があるので、base_url = ‘http://localhost:11434/v1’と指定すれば行けるだろう。

さて、ここまででLLMの回答の生成が終わったとする。もうサーバは閉じていい。data/model_answers以下にそれぞれの問題の回答がjsonとして記録されてるハズだ。

では次はGemini1.5Flashで回答を採点する↓
python judge_answers.py -m THUDM/glm-4-9b-chat -e gemini/gemini-1.5-flash

環境にもよるが、回答の生成と採点、合わせて30分ほどかかるといったところか。
採点が終わったらdata/judgements/judge_gemini__gemini-1.5-flash以下に採点の結果がjsonで保存されている。ところでdata/judgements/judge_gpt-4-turbo-previewには既存の評価結果が保存されているが、これがあると紛らわしいので削除しておいていい。

では評価結果を以下で集計する↓
python ./totals_csv.py

これを実行すると集計結果がresults/totals.csvとして出力される。

あと、以下で設問ごとの出力、スコアがresults以下にモデル別にcsvとして出力される↓
python ./results_to_csv.py

まあこういう具合に表示できる↓

まあShaberiベンチマークの使い方はこんなところだ。

それでは手元でいくつかのモデルを評価してみたので集計結果を見てみる。↓

各モデルの名前と、ベンチマーク別のスコアとmean、weighted_meanというのが表示されている。
meanというのは4つのベンチマークの平均点だ。じゃあ結局はこのmeanを総合点と考えて良さそうに見えるが、だが待ってほしい。単純にベンチごとの平均点を使うのは偏りがあると私は思う。
というのも、Rakudaベンチは40問しかないのに対してTenguベンチは120問あるのだ。meanだとRakudaの1問当たりの評価がTenguの1問より3倍も重く評価されてしまう事になる。
そこで、weighted_meanという重み付けされた平均値も算出する事にした。これは全ての設問の平均値という事になる。以下のような重み付けで算出した。
weighted_mean = (Rakuda*40 + Tengu*120 + MT-Bench*60 + ELYZA*100) / 320
こちらのweighted_meanを総合点として捉える方が妥当だと私は考える。

で、あらためて上の結果を見てみよう。こうして見ると私のGleipnir-7BはElyzaTasks100においてはKUJIRAにちょっと負けている。さらにMT-Benchでも負けてる。しかしRakudaとTenguでは勝利しているので、結果的にはmeanとweighted_meanで勝っている。

ホーダチさんのGLM4-9B微調整モデルはたしかにスコアでGleipnirを圧倒している。それにElyzaTasks100において元モデルのGLM4-9Bから若干向上してるのもたしかに正しいようだ。とは言え、MT-BenchとRakudaでは元モデルに負けており、結局meanとweighted_meanでは元モデルより下で、劣化してしまってるという事になりそうだ。

ただし、そのような見解が言えるとしたら、今回のベンチ結果を留保なく額面通りに受け取った場合の話である。実際にはまだまだ留保しないといけないことだらけだ。Shaberiベンチで評価できたからと言って、その結果が妥当かどうかは別の話だ。

まず、Gemini1.5Flashによる採点は本当に妥当なのか?今回初めて試したのでまだまだ未知数である。それから、Shaberiベンチマーク自体、妥当なのか?それもまだ未知数だ。ElyzaTasks100のスコアが体感性能によく合致する事は今までの経験から分かっているが、他のTenguとかRakudaのスコアがどうかはまだ分からない。
ベンチのスコアと体感性能が一致していなければ、ベンチのスコアが高かろうが低かろうが何の参考にもならない。
ベンチの妥当性というのは、もっと沢山色んなモデルの評価を繰り返して、体感とマッチするか時間をかけて検証していくしかない。今までずっとElyzaTasks100をLlama3-70Bで自動評価していた分には検証できていたが、今回評価環境を刷新すればまたイチから全部やり直しだ。

まあ、とにかくShaberiベンチを導入して今後妥当性が検証されていけば本格的に採用していこうと思ってる。