Llama.cpp + WandBで始める日本語AI評価：Gemmaモデル（gemma-2-9b-it-Q4_K_M.gguf）のELYZA-tasks-100パフォーマンス分析

## はじめに

こんにちは！この記事では、Google Colab上でGemmaモデル（gemma-2-9b-it-Q4_K_M.gguf）を使用して、ELYZA-tasks-100データセットの評価を行う方法をご紹介します。この記事は、大規模言語モデルの評価に興味がある初心者の方々向けに書かれています。

この記事を読むことで、以下のことが学べます：

1. llama.cppの環境構築方法
2. Gemmaモデル（gemma-2-9b-it-Q4_K_M.gguf）のダウンロードと設定
3. llama.cppサーバーの起動方法
4. ELYZA-tasks-100データセットを使用したモデル評価の実装
5. Weights and Biases (WandB)を使用した評価結果の記録方法

それでは、順を追って説明していきましょう！

## 1. 環境構築

まずは、必要なライブラリやツールをインストールします。Google Colabを開いて、以下のコードを順番に実行してください。

```python
# Google Driveをマウントします。これにより、Driveのファイルにアクセスできるようになります。
from google.colab import drive
drive.mount('/content/drive')

# プロジェクトのルートディレクトリを設定します。
root_dir = "/content/drive/MyDrive/Prj/ELYZA-tasks-100"
# 必要なディレクトリを作成します。
!mkdir -p $root_dir/content/
# 作業ディレクトリを変更します。
%cd $root_dir/content/
```

次に、必要なライブラリをインストールします。

```python
# 必要なライブラリをインストールします。
# litellm: 様々な言語モデルAPIを統一的に扱うためのライブラリ
# mlflow: 機械学習の実験管理ツール
# wandb: 機械学習の実験追跡と可視化ツール
# loguru: Pythonのロギングライブラリ
!pip install -qU litellm mlflow wandb loguru
```

Weights and Biases (WandB)にログインします。

```python
# WandBの認証ファイルを削除します。
!rm -rf /root/.netrc
# Google ColabのユーザーデータからAPIキーを取得します。
from google.colab import userdata
import wandb
WANDB_API_KEY = userdata.get('WANDB_API_KEY')
# WandBにログインします。
!wandb login $WANDB_API_KEY
```

Google API Keyを環境変数に設定します。

```python
# Google API Keyを環境変数に設定します。
import os
from google.colab import userdata
os.environ['GEMINI_API_KEY'] = userdata.get('GOOGLE_API_KEY')
```

LiteLLMを使用してGeminiモデルにリクエストを送信するテストを行います。

```python
from litellm import completion

# 使用するモデルを指定します。
LITELLM_MODEL = "gemini/gemini-1.5-pro-latest"

# LiteLLMを使用して外部モデル（ここではGemini）にリクエストを送信します。
response = completion(
    model=LITELLM_MODEL,
    messages=[{"role": "user", "content": "今日のきぶんはどう？"}]
)
content = response.get('choices', [{}])[0].get('message', {}).get('content', '')
print(content)
```

llama.cppリポジトリをクローンします。

```python
# llama.cppリポジトリをクローンします。
!git clone https://github.com/ggerganov/llama.cpp.git
```

ELYZA-tasks-100データセットをクローンします。

```python
%cd $root_dir/content/
# ELYZA-tasks-100データセットをクローンします。
# このデータセットは、日本語のタスク評価用のデータセットです。
!git clone https://huggingface.co/datasets/elyza/ELYZA-tasks-100/
```

llama.cppをビルドします。

```python
# 作業ディレクトリをllama.cppに移動
%cd $root_dir/content/llama.cpp

# 既存のビルドディレクトリを削除して、新しく作成
!rm -rf build
!mkdir build

# ビルドディレクトリに移動
%cd build

# CMakeでビルド設定を行い、CUDAサポートを有効化
!cmake .. -DGGML_CUDA=ON

# CMakeで生成されたビルドファイルを使ってビルドを実行
!cmake --build . --config Release

# ビルドされたバイナリファイルをllama.cppディレクトリにコピー
!cp bin/* ..

# 作業ディレクトリをllama.cppに戻す
%cd ..

print("llama.cppのリビルドが完了しました。")
```

## 2. モデルのダウンロード

次に、Gemmaモデル（gemma-2-9b-it-Q4_K_M.gguf）をダウンロードします。

```python
# Gemmaモデル（gemma-2-9b-it-Q4_K_M.gguf）をダウンロードします。
# このモデルは、Hugging Faceのリポジトリからダウンロードされます。

%cd $root_dir/content
REPO = "bartowski/gemma-2-9b-it-GGUF"
MODEL_NAME = "gemma-2-9b-it-Q4_K_M.gguf"
!huggingface-cli download $REPO $MODEL_NAME --local-dir .
```

## 3. llama.cppサーバーの起動

モデルのダウンロードが完了したら、llama.cppサーバーを起動します。

```python
# ダウンロードしたファイルを確認します。
!ls -al -h
```

```python
# llama.cppサーバーをバックグラウンドで起動します。
%cd $root_dir/content/
# すべてのファイルに実行権限を付与します。
!chmod 777 -R $root_dir/**/*
# サーバーを起動します。
!nohup ./llama.cpp/llama-server -m $MODEL_NAME --port 8181 --n-gpu-layers 43 > output1.log &
```

サーバーが正常に起動したかテストします。

```python
%%time
# llama.cppサーバーにリクエストテスト
import requests
import json

# テスト用の入力テキスト
input_text = "今日の気分はどう？"

# プロンプトを作成します。
prompt = (
    f"<bos><start_of_turn>user"
    f"{input_text} <end_of_turn> <start_of_turn>model"
)

# サーバーにリクエストを送信します。
r = requests.post(
    "http://127.0.0.1:8181/completions",
    data=json.dumps({
        "prompt": prompt,
        "n_predict": 1024,
        "temperature": 0.3,
    }),
    headers={"Content-Type": "application/json"}
)
# レスポンスの内容を表示します。
print(json.loads(r.content)["content"])
```

## 4. 評価スクリプトの作成

次に、ELYZA-tasks-100を使用してモデルを評価し、結果をWandBに送信するスクリプトを作成します。

```python
# 必要なライブラリをインポートします。
import csv
import json
import os
import requests
from litellm import completion
import wandb
from transformers import AutoTokenizer
from tqdm import tqdm
import loguru
import time
from tenacity import retry, stop_after_attempt, wait_fixed

# ロガーの設定
logger = loguru.logger

# パラメータ設定
EXPERIMENT_NAME = f"elyza-tasks-100-{MODEL_NAME}-6"
EVAL_DATASET_PATH = f"{root_dir}/content/ELYZA-tasks-100/test.csv"
TEMPLATE_PROMPT_PATH = "/content/drive/MyDrive/Bench/prompt_eval_llamacpp.txt"
USE_WANDB = True
LITELLM_MODEL = "gemini/gemini-1.5-pro-latest"
START_INDEX = 30  # 評価を開始するインデックスを設定します。

# 評価データセットを読み込む関数
def load_eval_dataset(path):
    with open(path, "r") as f:
        csv_table = list(csv.reader(f))[1:]
        return [{"input_text": r[0], "output_text": r[1], "eval_aspect": r[2]} for r in csv_table]

# プロンプトテンプレートを読み込む関数
def load_template_prompt(path):
    with open(path, encoding="utf-8") as f:
        return f.read()

# ローカルモデルクラス
class LocalModel:
    def predict(self, input_text: str) -> str:
        # モデルに対する入力プロンプトを作成します。
        prompt = (
            f"<bos><start_of_turn>user"
            f"{input_text} <end_of_turn> <start_of_turn>model"
        )

        # llama.cppサーバーにリクエストを送信します。
        r = requests.post(
            "http://127.0.0.1:8181/completions",
            data=json.dumps({
                "prompt": prompt,
                "n_predict": 1024,
                "temperature": 0.3,
            }),
            headers={"Content-Type": "application/json"}
        )
        return json.loads(r.content)["content"]

# LiteLLMリクエスト送信関数（リトライ機能付き）
@retry(stop=stop_after_attempt(3), wait=wait_fixed(60))
def send_litellm_request(litellm_model, prompt):
    try:
        # LiteLLMを使用して外部モデル（ここではGemini）にリクエストを送信します。
        response = completion(
            model=litellm_model,
            messages=[{"role": "user", "content": prompt}]
        )
        content = response.get('choices', [{}])[0].get('message', {}).get('content', '')
        return content
    except Exception as e:
        print(f"エラーが発生しました: {e}")
        print("1分後に再試行します...")
        raise  # 例外を再度発生させて、retryデコレータがキャッチできるようにします

# ELYZA-tasks-100のスコアリング関数
def elyza_tasks_100_score(input_text, output_text, eval_aspect, model_output, template_prompt, litellm_model):
    # スコアリング用のプロンプトを作成します。
    prompt = template_prompt.format(
        input_text=input_text,
        output_text=output_text,
        eval_aspect=eval_aspect,
        pred=model_output,
    )

    try:
        content = send_litellm_request(litellm_model, prompt)
        print("!!! 成功 !!!")
    except Exception as e:
        print(f"3回の試行後も失敗しました: {e}")
        raise

    # レスポンスから有効なスコアを抽出します。
    numbers = []
    for line in content.split('\n'):
        try:
            num = int(line.strip())
            if 1 <= num <= 5:
                numbers.append(num)
        except ValueError:
            logger.warning(f"Invalid line in response: {line}")
            continue

    if numbers:
        return numbers[0]
    else:
        logger.error(f"No valid score found in the response. Full content: {content}")
        return 1

# 評価実行関数
def evaluate(model, eval_dataset, template_prompt, litellm_model):
    for i, row in tqdm(enumerate(eval_dataset[START_INDEX:], start=START_INDEX), total=len(eval_dataset) - START_INDEX):
        input_text = row["input_text"]
        output_text = row["output_text"]
        eval_aspect = row["eval_aspect"]
        logger.info(f"Task {i} predict start ...")
        model_output = model.predict(input_text)
        logger.info(f"Task {i} evaluate start ...")
        score = elyza_tasks_100_score(input_text, output_text, eval_aspect, model_output, template_prompt, litellm_model)

        # 評価結果をログに出力します。
        logger.info(f"Input Text (Q):\n {input_text}")
        logger.info(f"Model Output Text (A):\n {model_output}")
        logger.info(f"Output Text (Answer):\n {output_text}")
        logger.info(f"Evaluation Aspect (Scoring criteria):\n {eval_aspect}")
        logger.info(f">>>> Score: {score}")

        # WandBを使用している場合、結果をWandBに記録します。
        if USE_WANDB:
            wandb.init(project=EXPERIMENT_NAME, name=f"Task{i:03d}")
            wandb.log({"score": score, "input_text": input_text, "output_text": output_text, "eval_aspect": eval_aspect, "model_output": model_output})
            wandb.finish()

# メイン関数（続き）
def main():
    # 評価データセットとプロンプトテンプレートを読み込みます。
    eval_dataset = load_eval_dataset(EVAL_DATASET_PATH)
    template_prompt = load_template_prompt(TEMPLATE_PROMPT_PATH)

    # モデルとトークナイザーを初期化します。
    model = LocalModel()

    logger.info(f"Starting evaluation from index {START_INDEX}")
    evaluate(model, eval_dataset, template_prompt, LITELLM_MODEL)

# Google Colab用のユーザーデータからWandB APIキーを取得し、環境変数に設定します。
from google.colab import userdata
os.environ['WANDB_API_KEY'] = userdata.get('WANDB_API_KEY')

# メイン関数を実行します。
if __name__ == "__main__":
    main()
```

このスクリプトは以下の機能を持っています：

1. 必要なライブラリとモジュールのインポート
2. 評価データセットとプロンプトテンプレートの読み込み
3. ローカルで起動したllama.cppサーバーへのリクエスト送信
4. ELYZA-tasks-100の評価基準に基づくスコアリング
5. 評価結果のWandBへの送信

各関数や処理にはコメントを付けて、初心者の方でも理解しやすいようにしています。

## 5. 評価の実行とWandBへの結果送信

最後に、作成したスクリプトを実行して評価を行い、結果をWandBに送信します。上記のコードブロックの最後の部分で、`main()`関数を呼び出すことで評価が実行されます。

評価が完了したら、WandBのダッシュボードにアクセスして結果を確認することができます。

## 注意点とTips

評価プロセスをスムーズに進めるために、いくつかの注意点とTipsをご紹介します。

1. **Google Colab環境について**
   - Google Colabのセッションは一定時間経過すると切断されることがあります。長時間の評価を行う場合は、定期的に操作を行うか、有料版のColab Proの使用を検討してください。

2. **メモリ管理**
   - 大規模なモデルを扱うため、メモリ使用量に注意が必要です。評価中にメモリ不足エラーが発生した場合は、`torch.cuda.empty_cache()`を実行してGPUメモリをクリアしてみてください。

3. **エラーハンドリング**
   - 評価中に予期せぬエラーが発生する可能性があります。`try-except`文を使用して適切にエラーをキャッチし、ログに記録することをお勧めします。

4. **中断と再開**
   - 評価が途中で中断された場合に備えて、定期的に進捗状況を保存しておくと良いでしょう。`START_INDEX`変数を使用して、中断したポイントから再開することができます。

5. **WandBの活用**
   - WandBのダッシュボードを活用して、評価結果をリアルタイムで可視化することができます。グラフや表を使って結果を分析することで、モデルの性能を直感的に理解できます。

## まとめ

以上で、Google Colab上でGemmaモデル（gemma-2-9b-it-Q4_K_M.gguf）を使って、ELYZA-tasks-100データセットの評価を行う方法をご紹介しました。この記事では、以下の内容を学びました：

1. llama.cppの環境構築方法
2. Gemmaモデル（gemma-2-9b-it-Q4_K_M.gguf）のダウンロードと設定
3. llama.cppサーバーの起動方法
4. ELYZA-tasks-100を使用したモデル評価の実装
5. WandBを使用した評価結果の記録方法

これらの手順を踏むことで、大規模言語モデルの評価と結果の可視化が可能になります。モデルの性能向上や比較分析に役立てることができるでしょう。

初心者の方々も、この記事を参考に実際に試してみることで、大規模言語モデルの評価プロセスについて理解を深めることができるでしょう。

## 発展的な取り組み

本記事で紹介した評価方法をマスターしたら、以下のような発展的な取り組みにチャレンジしてみるのも良いでしょう。

1. **複数モデルの比較**
   - Gemmaモデル以外の日本語モデルでも同様の評価を行い、性能を比較してみましょう。

2. **プロンプトエンジニアリング**
   - モデルへの入力プロンプトを工夫することで、性能がどのように変化するか実験してみてください。

3. **カスタムデータセットの作成**
   - ELYZA-tasks-100以外にも、自分で作成したカスタムデータセットで評価を行ってみましょう。特定のドメインや用途に特化したデータセットを使用することで、より実践的な評価ができます。

4. **評価指標の拡張**
   - 現在のスクリプトでは単一のスコアのみを使用していますが、複数の評価指標を組み合わせてより総合的な評価を行うことも可能です。

5. **自動化とパイプライン化**
   - 評価プロセス全体を自動化し、継続的にモデルの性能をモニタリングするパイプラインを構築してみましょう。

## おわりに

本記事では、Google Colab上でGemmaモデル（gemma-2-9b-it-Q4_K_M.gguf）を使ってELYZA-tasks-100データセットの評価を行う方法を詳しく解説しました。大規模言語モデルの評価は、モデルの性能を客観的に把握し、改善点を見出すための重要なプロセスです。

初心者の方々にとっては、ここで紹介した手順を1つずつ丁寧に実践していくことで、モデル評価の基礎を身につけることができるでしょう。また、経験豊富な方々にとっても、この評価フレームワークをベースにさらに高度な評価手法を開発するきっかけになるかもしれません。

評価結果を分析し、モデルの強みや弱みを理解することで、より効果的なモデルの選択や改善が可能になります。ぜひ、この記事を参考に実際に評価を行い、日本語大規模言語モデルの可能性を探ってみてください。

最後に、本記事で紹介した手法はあくまで一例です。常に新しい評価手法や指標が提案されていますので、最新の研究動向にも注目しながら、自身のニーズに合った評価方法を探求し続けることをお勧めします。

モデル評価の世界は奥深く、まだまだ発見や改善の余地がたくさんあります。皆さんの挑戦が、日本語自然言語処理の発展に貢献することを願っています。頑張ってください！