lm-evaluation-harness + JGLUE で日本語 LLM の改善度合いを測定する

[先日の記事](https://qiita.com/yasusun/items/6418f5558ea9993b725b) では、[open-calm-7b](https://huggingface.co/cyberagent/open-calm-7b) のような大規模言語モデルを少ない VRAM でもファインチューニングできるかどうか試してみました。
今回はそれに引き続き大規模言語モデルの性能評価を試してみたいと思います。

# 大規模言語モデルの性能評価方法

まず、大規模言語モデルの性能評価と一口に言っても色々な指標や方法が考えられます。
おそらく初めに思いつくのは「要求に対して期待通りのテキストが生成される確率」かと思います。他にも「テキストを生成する速度」や「メモリ効率」等が挙げられます。
「期待通りのテキストが生成される確率」にしても中身は様々で、例えば入力（≒タスク）に着目すると「二項分類問題」「多肢選択式問題」「QA」「会話」などのケースが考えられます。また、情報としての「正確性」や、発言の「流調さ」なども指標の候補として挙がります。
さらに「英語」や「日本語」のような「言語」で測定の線引きをしないといけないかもしれません。

というように、工業製品のごとく様々な指標が考えられますが、なにぶん新しい研究分野ですので標準的な指標のセットや、評価ツールも発展途上という状態です。
とはいえ、個人や組織で大規模言語モデルの継続的改善をしていく場合は、その改善が上手くいっているのかいっていないのか判断する必要がありますので、どうしても指標や評価ツールが必要になってきます。

そこで今回は日本語に対応したデータセットである [JGLUE](https://github.com/yahoojapan/JGLUE) と、それを組み込んだ性能評価ツールである [lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness) を調べてみました。
なお、今回は JGLUE の中でも JSQuaD （文脈に基づいた質問にどれだけ正確に回答できるか？）というベンチマークに絞ってテストしています。また今回は評価方法に焦点を当てており、複数のモデルの比較はこの記事には書いていませんのでご了承ください。

# JGLUE について

大規模言語モデルの評価方法は「データセットに基づく自動測定」と「人間の判断に基づく測定」に大きく分けられます。前者の方式、かつ日本語を対象としたデータセットとして **JGLUE** がヤフー株式会社と早稲田大学によって 2022 年に開発されています。

https://github.com/yahoojapan/JGLUE

論文
https://www.jstage.jst.go.jp/article/jnlp/30/1/30_63/_pdf/-char/ja

JGLUE は性能評価のためのデータセット群であり、自動的な測定を行うにはプログラムが別途必要です。
また、JGLUE の主なターゲットは [BERT](https://ja.wikipedia.org/wiki/BERT_(%E8%A8%80%E8%AA%9E%E3%83%A2%E3%83%87%E3%83%AB)) という方式の言語モデルであり、GPT-4、[rinna/japanese-gpt-neox-3.6b](https://huggingface.co/rinna/japanese-gpt-neox-3.6b)、[cyberagent/open-calm-7b](https://huggingface.co/cyberagent/open-calm-7b) のような GPT をベースとしたモデルとは得意領域が違うため、モデルの使用目的に対して適切な評価用データセットにはなっていないかもしれない点は注意が必要です。

JGLUEは [GLUE](https://gluebenchmark.com/) と呼ばれるベンチマークが研究のベースになっていますが、機械翻訳などは行わず一から日本語のデータセットを作っています。（すごい）
データセットは以下のタスク群で構成されています。

## 評価タスク
### MARK-ja » 文章分類

文章を分類するタスクです。Amazon の商品レビューにおける **「レビューテキスト」「5段階評価」「センチメント（ポジティブかネガティブか）」** がデータセットとして含まれています。
例えば、ある商品のレビューである「日本語対応してほしいところ。その外の面では比類なく優れていると思います。」 というテキストを与えたとき、「ポジティブ or ネガティブ」をどのように出力するかが注目ポイントとなります。
ちなみにこのサンプルデータは「★2」「ポジティブ」となっています。私の目から見ても確かに「（やや）ポジティブ」と読めますが、機械が読むと前半の「日本語対応してほしいところ、」を重く見て「ネガティブ」と間違えそうだな、となんとなく思いました。

### JSTS » 文ペアの類似性

文ペアと `0.0` から `5.0` までの類似性の数値がデータセットに含まれています。
例えば「街中の道路を大きなバスが走っています。」と「道路を大きなバスが走っています。」の類似性は `4.4` となっています。

なお、このタスクに関しては性能評価ツールである lm-evaluation-harness に取り込まれていません。（2023年6月時点）

### JNLI » 文ペアの関係を推論

前提文と仮説文からなる文ペアに対し、その関係を推論します。関係は `entailment` （含意）、`contradiction` （矛盾）、`neutral` （中立） の３種です。
……といっても私には意味が良く分からなかったので、データセットから分かりやすそうな例をピックアップしてみました。

| 前提文 | 仮説文 | 関係 |
| - | - | - |
| 雪面を二人のスキーヤーが立っています。 | 雪山で、スキーの格好をした2人がいます。 | 含意 |
| スキー板をつけた人たちが雪道を移動しています。 | スキー板がはずれている。 | 矛盾 |
| 雪山で、スキーの格好をした2人がいます。 | 雪面を二人のスキーヤーが立っています。 | 中立 |

こう並べてみると推論関係というのがなんとなく分かってきますが、じゃあこのデータを作ってくださいと言われると結構間違えそうですね。実際、JGLUEのデータセットを作る際は複数人の多数決によって最終的な推論関係を決めているようです。

### JSQuaD » 文脈に基づいて質問に回答する

このデータセットは日本語の Wikipedia に基づいて作られています。
データセットには「Wikipedia 記事の段落」「質問文」「回答文」が含まれています。
記事がいわゆる文脈（コンテキスト）に該当し、与えられた情報に基づいてきちんと回答できるかがポイントになります。このタスクは GPT のユースケースとして割と近いものではないかと思います。

データのサンプルはこちらです。

| 項目 | 内容 |
| - | - |
| 記事 | 並列計算 [SEP] 並列コンピュータの主記憶は、共有メモリ型（全プロセッサが単一の物理アドレス空間を共有する）と分散メモリ型（各プロセッサがローカルな独自の物理アドレス空間を持つ）に分けられる。分散メモリは、メモリが論理的に分散しているためにそのように呼ばれるが、実際には物理的にも分散していることが多い。分散共有メモリはこの2つの方式を組み合わせたものであり、各プロセッサはローカルなメモリとローカルでないメモリの両方にアクセスできる。この場合、ローカルなメモリへのアクセスはローカルでないメモリへのアクセスよりも一般に高速である。 |
| 質問 | メモリが論理的に分散しているためにそのように呼ばれるものは？ |
| 回答 | 分散メモリ |

なお「回答不能かどうか」という属性がデータセットに含まれているものの、回答不能フラグが立っているデータは１件もありません。

### JCommonsenseQA » 常識的な推論

よく「一般常識」という単語を耳にすることがあるかと思いますが、そういった人間として常識的な反応をできるかどうかという評価になります。例えば「国を守る部隊は？」という問いの回答は「自衛隊」になります。
データセットは多肢選択式になっており「質問文」と「複数の回答選択肢」が含まれています。JSQuaDとは違いWikipedia記事のような文脈はデータセットに含まれていません。

なお、倫理的な問題に関しては入っていないように思われます。
例えば「コンピュータウィルスの作り方」のような質問は ChatGPT 等では答えないようになっていますが、そういった性能評価については別途開発が必要になりそうです。

以上が JGLUE に含まれる要素の説明になります。
大規模言語モデルのデータセットといえば大部分が英語用ですので、日本語用かつ高品質な性能評価用データセットは本当に貴重です。JGLUE はクラウドソーシングを活用しながら高品質なデータセットになるように努めて作られていますので、ありがたく使わせていただくことにします。

# lm-evaluation-harness を使った性能評価

ここまではデータセットの説明でしたが、ここからは性能評価ツールの説明になります。

現在、いくつかの Web サイトで大規模言語モデルの性能を示したリーダーボードが公開されています。
Hugging Face でもリーダーボードが公開されていまして、こちらでは性能評価ツールとして lm-evaluation-harness というツールが使われています。

https://huggingface.co/spaces/HuggingFaceH4/open_llm_leaderboard

https://github.com/EleutherAI/lm-evaluation-harness

こちらの評価ツールは **EleutherAI** という団体が中心になって開発しています。
様々な言語モデルのフォーマットに対応しており、また様々な評価タスクに対応しています。
これを使えば [rinna/japanese-gpt-neox-3.6b](https://huggingface.co/rinna/japanese-gpt-neox-3.6b)、[cyberagent/open-calm-7b](https://huggingface.co/cyberagent/open-calm-7b) のようなモデルがどの程度優れているのか評価することができます。
ただし、**2023年6月時点では JGLUE の評価タスクには対応しておりません。**

……という中、**Stability AI が JGLUE を lm-evaluation-harness に組み込んでくれています。**
こちらを使えば JGLUE によるベンチマークテストを行うことができます。

https://github.com/Stability-AI/lm-evaluation-harness

……が、Stability AI のリポジトリは [8bit 量子化](https://huggingface.co/docs/transformers/main_classes/quantization) モデルを読み込めない問題がある状態でフォークされたものになっています。この問題は [こちらのプルリクエスト](https://github.com/EleutherAI/lm-evaluation-harness/pull/585) で修正されていますので、この修正を取り込んだものがこちらです。

https://github.com/tdc-yamada-ya/lm-evaluation-harness-jp-stable

ということでこちらを使って性能評価をしてみます。
ただし、冒頭でも説明した通り今回テストする評価タスクは **JSQuaD** のみとなりますのでご了承ください。

## lm-evaluation-harness における JSQuaD 評価の挙動

データセットとして JSQuaD を使うとしても、大規模言語モデルにどのような入力をするのか、出力をどのように評価するのかは評価ツールが決めなければなりません。

Stability AI による JGLUE 実装では以下の様に評価を行っています。

### プロンプト（入力）

実装にはいくつかプロンプトを組み立てるパターン（バージョン）が用意されていますが、標準的なパターンは以下の様なプロンプトを組み立てています。

```
[題名]:{タイトル}
[問題]:{コンテキスト}
[質問]:{質問文}
[答え]:
```

先ほどの JSQuaD のサンプルを当てはめると以下の様になります。

```
[題名]:並列計算
[問題]:並列計算 [SEP] 並列コンピュータの主記憶は、共有メモリ型（全プロセッサが単一の物理アドレス空間を共有する）と分散メモリ型（各プロセッサがローカルな独自の物理アドレス空間を持つ）に分けられる。分散メモリは、メモリが論理的に分散しているためにそのように呼ばれるが、実際には物理的にも分散していることが多い。分散共有メモリはこの2つの方式を組み合わせたものであり、各プロセッサはローカルなメモリとローカルでないメモリの両方にアクセスできる。この場合、ローカルなメモリへのアクセスはローカルでないメモリへのアクセスよりも一般に高速である。
[質問]:メモリが論理的に分散しているためにそのように呼ばれるものは？
[答え]:
```

このプロンプトを読み込ませることによって、モデルがその続きを出力します。もしその出力が `分散メモリ` ならばドンピシャで正解ということになります。

### 出力の評価方法

あらゆるケースで大規模言語モデルがドンピシャの回答を示してくれるのならいいのですが、確率の世界ですので細かい違いがどうしても出てしまいます。例えば「分散メモリ」や「分散メモリです。」のように変わるわけですが、もしこれが人間同士の会話だったなら物事を理解する上ではどちらでもいいはずです。そのため、多少のあいまいさを許容する評価方法も必要になります。

Stability AI の実装では `exact_match` （完全一致）と `f1_score` （F値）の２種のスコアを計算します。
SQuaD 用の評価コードがベースになっているようですが日本語向けに少しアレンジされています。

`exact_match` （完全一致）はそのままのとおり、モデルによって得られた出力とデータセットの期待値（回答文）を比較して、文字列が完全に一致している場合は合格（1.0点）とします。ただし半角記号、全角記号、絵文字は除去された状態で比較され、空白文字についても正規化された状態での比較となります。

`f1_score` （F値）の細かい解説は [Wikipedia の記事](https://ja.wikipedia.org/wiki/F%E5%80%A4_(%E8%A9%95%E4%BE%A1%E6%8C%87%E6%A8%99)) に委ねたいと思います。実装の計算式は [ソースコード](https://github.com/tdc-yamada-ya/lm-evaluation-harness-jp-stable/blob/jp-stable/lm_eval/jasquad/evaluate.py#L44-L57) をご覧ください。出力と期待値を分かち書きで分解した後、双方同時に出現した要素数から適合率と再現率を計算しています。
こちらの評価方式は、微妙に違う文であっても類似度合いを比較することができます。ただし、文字一つで意味合いがガラッと変わってしまうものや、肯定文・否定文の違いを見分けるわけではないので、評価タスクによっては注意が必要になります。現に Hugging Face の LLM リーダーボードでは GPT-4 や人による評価も掲載されています。

## lm-evaluation-harness の実行

### 実行環境

実況環境は先日の記事とまったく同じものを使用しています。もちろん Google Colab などでも実行可能です。

- Dell Alienware Aurora R12 (ミドルタワー)
- Intel Core i7-11700F
- 32 GB RAM
- 2 TB HDD
- Windows 11
- [Python 3.10.6](https://www.python.org/downloads/release/python-3106/)
- Git Bash
- Visual Studio Code
  - Jupyter 拡張
- GeForce RTX 3060 (12 GB VRAM)
- [NVIDIA CUDA Runtime 11.7](https://developer.nvidia.com/cuda-downloads)
- [Visual Studio C++](https://visualstudio.microsoft.com/ja/vs/features/cplusplus/)

### 準備

`venv` や `pip` の設定・インストールを順次行っていきます。

```bash
git clone https://github.com/tdc-yamada-ya/lm-evaluation-harness-jp-stable.git
cd lm-evaluation-harness-jp-stable
python -m venv venv
source venv/Scripts/activate
pip install -e ".[ja]"
```

続けて PyTorch や bitsandbytes 等を Windows でも実行できるようにカスタムインストールします。

```bash
pip install -U torch --index-url https://download.pytorch.org/whl/cu117
pip install -U https://github.com/jllllll/bitsandbytes-windows-webui/raw/main/bitsandbytes-0.39.0-py3-none-any.whl
```

### 実行

以下のコマンドで JSQuaD による評価を行うことができます。

```bash
python main.py \
  --model hf-causal-experimental \
  --model_args pretrained=cyberagent/open-calm-7b,load_in_8bit=True,peft=.lora \
  --tasks jsquad-1.1-0.2 \
  --num_fewshot 2 \
  --device cuda \
  --output_path result.json 
```

`--model` には `hf-causal-experimental` を指定します。こちらを指定することで `open-calm-7b` のようなモデルを読み込める上、[Peft](https://github.com/huggingface/peft) による LoRA 読み込みを行えるようになります。

`--model_args` には何のモデルをどのように読み込むか指定します。指定できるパラメータは [ソースコード](https://github.com/tdc-yamada-ya/lm-evaluation-harness-jp-stable/blob/jp-stable/lm_eval/models/huggingface.py#L70-L87) を参照してください。
VRAM 節約のために `load_in_8bit=True` を指定しています。（Stability AI のリポジトリの場合はこれを有効化すると読み込みエラーが発生しました。）
また Peft で LoRA を読み込むために `peft=.lora` を指定しています。`.lora` ディレクトリに Peft でトレーニングした LoRA モデルファイルを格納しておく必要があります。

`--tasks` には評価タスクの名前をカンマ区切りで書きます。今回は `jsquad` のみです。

`--num_fewshot` には Few-Shot の数をタスクごとにカンマ区切りで指定します。今回は `jsquad` のみのため単に `2` と指定しています。Few-Shot とはプロンプトに回答方法のサンプルを含めることで、正確な回答が返ってくる確率を引き上げるプロンプトテクニックのことです。実際、このツールで評価する場合も Few-Shot の有無で大幅に成績が向上します。なお、JSQuaDの場合は、訓練用データセットからランダムでデータを選択し、これを Few-Shot のテキストとして組み立てています。

https://www.promptingguide.ai/jp/techniques/fewshot

`--device` は NVIDIA GeForce を使いたいので `cuda` を指定します。

`--output_path` には評価結果を出力するファイルを指定します。

このコマンドを実行すると概ね３～４時間後に結果が出力されます。結果のサンプルはこちらです。

```json:result.json
{
  "results": {
    "jsquad-1.1-0.2": {
      "exact_match": 45.56506078343089,
      "f1": 59.89948153117292
    }
  },
  "versions": {
    "jsquad-1.1-0.2": 1.1
  },
  "config": {
    "model": "hf-causal-experimental",
    "model_args": "pretrained=cyberagent/open-calm-7b,load_in_8bit=True,peft=.lora",
    "num_fewshot": 2,
    "batch_size": null,
    "device": "cuda",
    "no_cache": false,
    "limit": null,
    "bootstrap_iters": 100000,
    "description_dict": {}
  }
}
```

`exact_match` や `f1` の値を見ることで、モデル間の比較やどの程度改善されたかを測ることができます。

これで評価ツールの実行方法の説明は以上です。
評価に関しても Windows かつ NVIDIA GeForce RTX 3060 で行えることが分かりましたので、これでいろいろなモデルの比較や改善ができそうです。

次回は [text-generation-webui](https://github.com/oobabooga/text-generation-webui) について調査した内容をまとめる予定です。