🌊

採点理由を出力しながらローカルLLMを評価する【Elyza Tasks 100】

2025/02/04に公開

はじめに

既存の70BクラスのLLMが日本語でどのくらいの性能なのかを見る必要がありました。
本日はLLMをElyza Tasks 100というベーシックなベンチマークを使って、評価をしたので、忘備録の記事を書きます。

n番煎じではありますが、スコアだけを出すのではなく、ローカルLLMの出力結果や点数の採点理由などをLangChainのLCELを利用して出力しています。
そういった情報もベンチマークを実行した際に取得したいなどあれば、少しは参考になるかなと思います。

特に、今回、ローカルLLMもクローズドLLMも両方使いたいので、共通した形式で利用できるLangChain(LCEL)を利用しました。
その中で、chat_templateというものでつまづきました。
同じようにつまづいている人の参考になれば非常に嬉しいです。

なお、今回は一番簡単にElyza Tasks 100を選んでいますが、csvにて「問題文」「模範解答」「採点基準」の列を持ち、各行ごとに問題を持つようなベンチマークであれば、csvファイルを入れ替えることで問題なく利用できます。

参考書籍

LangChainとLangGraphによるRAG・AIエージェント[実践]入門
ChatGPT/LangChainによるチャットシステム構築[実践]入門

今回は、さまざまなモデルを統一的に利用するために、LangChainを利用しています。
langchainに関しては、こちらの書籍を読めば大体のことはできるようになりますので、おすすめです。

大規模言語モデル入門
大規模言語モデル入門Ⅱ〜生成型LLMの実装と評価

大規模言語モデルに関して非常に詳細に記載されている本です。
LLM系の勉強をする際に使わせていただきました。

Webページで参考にしたページは非常に多いので、記事中の該当箇所に置かせていただきます。

(書籍のリンクは、Amazonアフィリエイトリンクなります)

出力結果例

本リポジトリを実行すると下記のような情報をまとめたマークダウンを得ることができます。

  • 平均スコア
  • 問題番号
  • 点数
  • 質問文
  • 回答結果
  • 模範回答
  • 採点基準
  • 採点モデルの採点理由
# 平均スコア

スコアは4.34です

# 結果と回答

## 第1問 (点数: 5)

仕事の熱意を取り戻すためのアイデアを5つ挙げてください。

### LLM出力結果:
仕事の熱意を取り戻すためのアイデアを考える際、まずは自分のモチベーションの源や、どのような要因で熱意が失われたのかを考慮することが重要です。以下に5つのアイデアを挙げます。

1. **目標の再設定**:
   自分の仕事における目標を見直し、具体的で達成可能な短期目標を設定します。小さな成功体験を積むことで、モチベーションが高まります。

2. **新しいスキルの習得**:
   自分の業務に関連する新しいスキルや知識を学ぶことで、仕事に対する新たな興味を引き出すことができます。オンラインコースやセミナーに参加するのも良いでしょう。

3. **職場環境の改善**:
   自分の作業スペースを整理したり、快適な環境を整えることで、仕事に対する気分が変わることがあります。植物を置いたり、好きなアイテムを飾るのも効果的です。

4. **同僚とのコミュニケーション**:
   同僚や上司と積極的にコミュニケーションを取ることで、仕事の楽しさや新たな視点を得ることができます。チームでのプロジェクトやランチ会を企画するのも良いでしょう。

5. **ワークライフバランスの見直し**:
   仕事とプライベートのバランスを見直し、リフレッシュする時間を確保します。趣味や運動、旅行など、仕事以外の活動を充実させることで、仕事への熱意が戻ることがあります。

これらのアイデアを実践することで、仕事に対する熱意を取り戻す手助けになるでしょう。自分に合った方法を見つけて、少しずつ取り入れてみてください。

### 模範解答:
1. 自分の仕事に対する興味を再発見するために、新しい技能や知識を学ぶこと。
2. カレッジやセミナーなどで講演を聴くことで、仕事に対する新しいアイデアや視点を得ること。
3. 仕事に対してストレスを感じている場合は、ストレスマネジメントのテクニックを学ぶこと。
4. 仕事以外の楽しいことをすることで、ストレスを発散すること。
5. 仕事に対して自己評価をすることで、自分がどのように進化しているのかを知ること。

### 採点基準:
- 熱意を取り戻すのではなく、仕事の効率化・スキルアップのような文脈になっていたら1点減点
- 出したアイデアが5つより多い、少ない場合は1点減点
- 5つのアイデアのうち、内容が重複しているものがあれば1点減点



### 採点理由:
ステップ1: 回答の内容を確認する
- 言語モデルの回答には5つのアイデアが挙げられています。
- 各アイデアは、仕事の熱意を取り戻すための具体的な方法を示しています。

ステップ2: 採点基準に基づいて評価する
- 基本的な採点基準に照らし合わせると、回答は指示に従っており、誤りは見当たりません。
- 不自然な日本語はなく、内容も事実と異なる部分はありません。
- 「倫理的に答えられません」のような過度な安全性の配慮もありません。

ステップ3: 問題固有の採点基準に基づいて評価する
- 熱意を取り戻すためのアイデアとして、効率化やスキルアップに偏っているわけではなく、バランスが取れています。
- 5つのアイデアがしっかりと挙げられており、数の不足や過剰はありません。
- 内容の重複も見られません。

ステップ4: 総合評価
- 各アイデアは具体的で実践的であり、仕事の熱意を取り戻すために役立つものです。
- 全体として、回答は非常に役に立つものであり、採点基準に照らしても高評価に値します。

結論: 5点

---

・・・

Elyza Tasks 100とは

公式のリポジトリを置いておきます。
https://huggingface.co/datasets/elyza/ELYZA-tasks-100

ELYZA Tasks 100とは株式会社ELYZAが作成した、日本語における自然言語処理タスクをも網羅したベンチマークです。
詳細は下記の公式の記事を参照ください。
https://note.com/elyza/n/na405acaca130

ベンチマークの中には、100問の日本語性能を測るためのあらゆる分野での自然言語タスクの質問文と、その模範回答、また各タスクごとの採点基準が記載されています。
この質問文を評価したいLLMに与えて、得られた回答を、採点基準に則って採点することで、5点満点で評価点数を得ることができるため、比較が困難なLLM同士を点数という指標げで比較することができます。

例えば下記のような文章があります。(結構難しい問題も多いです)

クリエイティブ

大喜利に挑戦してみましょう。大喜利とはお題に対して面白い回答を考える遊びです。大喜利のコツは色々とありますが、1つは「共感を呼ぶ答えをひとひねりする」ことです。誰もが経験したことや思ったことに対して、少し変わった視点や表現で回答することで、共感と笑いを誘うことができます。 例えば、以下の例を見てみましょう お題: ナルシスト漁師の特徴を教えてください 回答: キャッチ&キス&リリース このお題では、「普通の漁師の特徴」の「キャッチ&リリース」に対して、ナルシストという観点からの捻りを加え、語感がいいようにまとめていて面白いです。 それでは、以下のお題について大喜利で答えてください。 お題:初めてドラゴンを退治しに行くのですが、アドバイスをお願いします

並び替え

次のリストを昇順で並び替えて。出力は入力と同じフォーマットのリストのみを返し、余計なことを喋らないで。 [10, 92, 2, 5, -4, 92, 5, 101]

発散

以下のシチュエーションでの適切な発言をいくつか考えてください。 シチュエーション: 誰かが無事に到着したとき

物語理解

「必ず時間は守ります」葵と悠が手を繋いで歩き出すと、善吉は口調を荒げ「ふざけるな!娘をどうするつもりだ!」と捲し立てた。悠は「私は彼女を愛しています。私たちは結婚したんです。」と堂々と答えた。 この文章において、善吉は悠にとっての何ですか?

算数

1から500までの数字を入力するのに何回キーボードを叩く必要があるでしょうか? 1桁の数字を入力するには1回、2桁の場合は2回、3桁の場合は3回キーボードを叩く必要があるものとします。 選択肢: (a) 1156 (b) 1392 (c) 1480 (d) 1562 (e) 1788

これらのタスクに対して、模範解答と採点基準がそれぞれ存在します。

やりたいこと

100問のタスク全てに対して、人力で出力を確認して、採点基準と照らし合わせながら、点数をつけるのは難しいです。
また、人手での評価の場合は、見間違いや価値観の違い、採点者の知識不足、思い込みにより評価がばらつくことがあるので、理想は複数人による採点を実施したいですが、人で実施するのはコスト的に難しいです。

一方、ELYZA Tasks 100には、タスク内容と模範回答、採点基準が全て揃っているので、評価さえもLLMに自動で実施してもらうことができます。
したがって、評価したいLLMに対して、タスクを解いてもらい、評価するLLMに、問題文、出力結果、模範回答を入れることで、5点満点で点数をつけてもらい、評価を自動化します。

評価用のLLMの性能は非常に重要だと思うため、評価用のLLMには、高性能であるgpt-4oを利用します。
また、評価対象のLLMは70B(かつ、量子化なし)を想定しているため、AWSのSageMakerでインスタンスを立ち上げて実験しようと思います。
(なお、今回のコードはSagemaker以外でも問題なく動きます。あくまでハイスペックなインスタンスを気軽に利用できるという観点でSageMakerを利用しています)

Elyza-task-100の評価コードを作成する

ベンチマークがあるので、それを評価するコードを用意する必要があります。
世の中には様々ベンチマークに利用できるリポジトリがありました。有名どころだと下記です。

https://github.com/EleutherAI/lm-evaluation-harness/tree/main
https://github.com/shisa-ai/shaberi

ただ、今回は評価点数だけではなく、各問題ごとにどういう点数を出力したのかや、採点モデル(gpt-4o)がどういう基準・理由でその点数をつけたのかを把握する必要がありました。
上記のリポジトリでもやり方はあるのだろうと思いますが、1-2日以内に結果が必要でしたため、学習コストの低いコードの調査をしました。

結果として、非常に使いやすそうなリポジトリを見つけたため、こちらを参考にさせていただきました。
https://github.com/foxn2000/bench/tree/main

こちらには、ELYZA tasks 100のcsvを読み取り、geminiなどを利用してLLMの評価をするコードが用意されております。
こちらのコードを参考にさせていただきながら、使いやすい形に改造していこうと思います。

有名どころのリポジトリは、今後時間があるときに少しづつ勉強しようと思います。

成果物

最終的なコードを全て含むリポジトリは下記に置いておきます。
https://github.com/personabb/LLM_Evaluation_Elyza.git

コード解説

必要コード

下記のコードがリポジトリに格納されています。

  • test_LLM_by_Agents.py
    • 評価したいLLMと採点用のLLMを用意して、回答と採点評価を同時に実施するスクリプト
  • test_OutputFile_by_Agents.py
    • 回答結果が保存されているファイルと採点用LLMを用意して、採点だけを実施するスクリプト
    • test_LLM_by_Agents.pyを実施後、ログファイルから採点用LLMを変更して、再採点を実施したいときに利用する

また、下記のファイルがinputsフォルダに用意してあります

  • test.csv
    • Elyza-task-100の質問内容、模範回答、採点基準が格納されているcsv
  • prompt_template.txt
    • 採点用LLMのプロンプトのテンプレートファイル
  • llama_chat_template.txt
    • LLAMA系のLLMのchat_templateが格納されているファイル
    • LLAMA系のLLMにおいて、chat_templateが保存されていないLLMに対しては、上記のファイルを利用する
    • (逆にいうと、LLAMA系以外のchat_templateが保存されていないLLMは利用できません・・・)

chat_templateに関しては下記の記事が参考になります。
https://note.com/tatsuyashirakawa/n/n0aa9169c99d5
また、公式の記事は下記です。
https://huggingface.co/docs/transformers/en/chat_templating

簡単にいうと、LLMは基本的に「次単語予測モデル」なので、チャット形式で回答するのは基本的に難しいです。
そこで、タグを含んだ文章の続きを生成するという方法で学習をすることで、systemプロンプト、userプロンプト、回答と、明示的に分けた文章の続きを生成するという方法で、チャット形式での対話が可能になっています。
その、学習に使われたタグや文体のルールがchat_templateです。

これは、LLMのルールなので、守らない文章を入力すると出力が破綻します。
そのため、基本的には、LLMには、chat_templateが用意されているはずです。

test_LLM_by_Agents.py

コード全体を下記に記載します。

コード全文

文字数制限のため、全文載せられなかったので、Githubよりご覧ください
https://github.com/personabb/LLM_Evaluation_Elyza/blob/main/test_LLM_by_Agents.py

上から順に解説していきます

パラメータ設定部分

下記はパラメータ設定部分です。
コードを実行する前に必要なパラメータを設定します。

test_LLM_by_Agents.py
#=================Parameter===========================================================================
# (1) ここで宣言しているパラメータはグローバル変数として扱います。
#     これ以外の変数はグローバルスコープに置かないようにします。

# 採点者モデルの選択
# "OpenAI_Base"では、gpt-4o系統もしくは、deepseekのみ実装済み
#Evaluation = "OpenAI_Base"
#Evaluation_model = "deepseek-chat"
#Evaluation = "OpenAI_Base"
#Evaluation_model = "gpt-4o-mini"
Evaluation = "Azure"
Evaluation_model = "gpt-4o"
#Evaluation = "Google"
#Evaluation_model = "gemini-2.0-flash-exp"
#Evaluation = "HuggingFace"
#Evaluation_model = "meta-llama/Llama-3.3-70B-Instruct"

# 評価対象のモデルの選択
# "OpenAI_Base"では、gpt-4o系統もしくは、deepseekのみ実装済み
Target = "OpenAI_Base"
Target_model = "deepseek-chat"
#Target = "OpenAI_Base"
#Target_model = "gpt-4o-mini"
#Target = "Azure"
#Target_model = "gpt-4o"
#Target = "Google"
#Target_model = "gemini-1.5-flash" #"gemini-2.0-flash-exp", "gemini-1.5-flash"
#Target = "HuggingFace"
#Target_model = "meta-llama/Llama-3.2-1B-Instruct"

# 何問目から再開するか(1問目から始める場合は1)
resume_question_index = 1

# HuggingFaceにて、アクセス権限がないと取得できないモデルを利用するかどうかのフラグ
HuggingFace_access = True

# 採点側温度の設定(評価に使うので、基本的に0.001でいい)
Evaluation_temperature = 0.001
# 採点側top_pの設定(評価に使うので、基本的に0.001でいい)
Evaluation_top_p = 0.001

# 評価対象側温度の設定(評価に使うので、基本的に0.001でいい)
Target_temperature = 0.001
# 評価対象側top_pの設定(評価に使うので、基本的に0.001でいい)
Target_top_p = 0.001

# AWSなどでキャッシュディレクトリを利用する場合はここに指定する。使わない場合はNone
efs_cache_dir = None

# CSVファイルのパス
csv_file = './inputs/test.csv'
#=======================================================================================================

コードに記載の通りですが下記のパラメータを設定しています。

  • Evaluation, Evaluation_model
    • なんのモデルで採点するか
  • Target, Target_model
    • なんのモデルを評価するか
  • resume_question_index
    • Elyza Tasks 100の何問目から再開するかを決めるパラメータです。
    • エラーが出たなどで途中から再開したい時に再設定します。
  • HuggingFace_access
    • HuggingFaceのアクセストークンが環境変数に登録されており、ログインしないと利用できないモデルを利用する際にTrueにします。そうでない場合は、Falseに設定しても良いです。
    • Trueの場合は、必ずログインされます。
  • Evaluation_temperatureEvaluation_top_p
    • 採点モデルのtemperaturetop_pの設定です。基本的に0.001で良いです。
    • 0だとたまにエラーを吐くモデルがあるので、0.001にしています。
  • Target_temperatureTarget_top_p
    • 評価されるモデルのtemperaturetop_pの設定です。基本的に0.001で良いです。
    • 0だとたまにエラーを吐くモデルがあるので、0.001にしています。
  • efs_cache_dir
    • AWSで実行する場合、モデルファイルなどをEFSに保存しておくと便利です。
    • モデルファイルがデフォルトで指定された箇所以外の場所に保存してある場合は、ここにそのパスを記載してください。
  • csv_file
    • 今回テストするベンチマーク(Elyza Tasks 100)の情報が入っているcsvファイルです。
    • 左から、問題文、解答、採点基準の順で100問分データが格納されており。問題数以外の形式が同じデータセットであれば、このファイルを変えるだけで流用できます。

下記から、一つずつ説明しますが、関数の順番などは前後することをご了承ください。

ファイル名の決定

下記の部分では、各種保存先のファイル名などを指定しています。
また、出力先のフォルダを作成したりしています。

また、モデル名の中にはファイル名に設定するのはよろしくない記号/などがあります。
それをsanitize_filename関数で安全な文字に置き換えて利用しています。

test_LLM_by_Agents.py
def sanitize_filename(filename: str) -> str:
    """
    ファイル名に使用できない文字を安全に置き換える関数。

    Args:
        filename (str): 元のファイル名

    Returns:
        str: 不正文字がハイフンに置き換えられた安全なファイル名
    """
    sanitized = re.sub(r'[\/:*?"<>|]', '-', filename)
    return sanitized


def get_file_paths(
    target: str,
    target_model: str,
    evaluation: str,
    evaluation_model: str
) -> dict:
    """
    出力ファイルのパスを生成する関数。
    ファイル名に使用できない文字を含む場合はsanitize_filenameで安全化した上で
    ディレクトリを作成し、パスを生成します。

    Args:
        target (str): 評価対象モデル名
        target_model (str): 評価対象モデルの詳細名
        evaluation (str): 評価モデル名
        evaluation_model (str): 評価モデルの詳細名

    Returns:
        dict: 各ファイルのパスを格納した辞書
    """
    safe_target_model = sanitize_filename(target_model)
    safe_evaluation_model = sanitize_filename(evaluation_model)
    output_dir = f"./outputs/{target}-{safe_target_model}"
    os.makedirs(output_dir, exist_ok=True)

    return {
        "output_file": f"{output_dir}/output-{target}-{safe_target_model}.txt",
        "result_file": f"{output_dir}/result-{target}-{safe_target_model}_by_{safe_evaluation_model}.txt",
        "critc_file": f"{output_dir}/cretical-{target}-{safe_target_model}_by_{safe_evaluation_model}.txt",
        "score_file": f"{output_dir}/score-{target}-{safe_target_model}_by_{safe_evaluation_model}.txt",
        "markdown_output": f"{output_dir}/Elyza-{target}-{safe_target_model}_by_{safe_evaluation_model}.md",
        "safe_target_model": safe_target_model,
        "safe_evaluation_model": safe_evaluation_model,
    }

採点者モデルの設定

採点用に利用するモデルは、さまざまなベンダーのモデルを利用することを前提とするため、モデルが変わっても同じコードを利用できるlangchainを利用します。
モデルを読み込む場合は、モデルごとに異なるコードを利用する必要があるため、EvaluationAzureモデル、OpenAI_Base(互換)モデル、Googleモデル、HuggingFaceモデルで分けて記載しています。

test_LLM_by_Agents.py

def initialize_evaluation_model(
    evaluation_name: str,
    evaluation_model_name: str,
    evaluation_temperature: float,
    evaluation_top_p: float
):
    """
    採点に使用するモデルを初期化して返す関数。

    Args:
        evaluation_name (str): 評価モデル名("Azure", "Google", "HuggingFace", "OpenAI_Base"等)
        evaluation_model_name (str): 評価モデルの詳細名
        evaluation_temperature (float): 温度パラメータ
        evaluation_top_p (float): top_pパラメータ

    Returns:
        model: 評価モデルとして使用するLLMオブジェクト
    """
    model = None

    if evaluation_name == "Azure":
        # 環境変数を登録するもしくは、直書きでも良い
        # os.environ["OPENAI_API_VERSION"] = "2024-08-01-preview"
        # os.environ["AZURE_OPENAI_ENDPOINT"] = "https://xxxxx.openai.azure.com"
        # os.environ["AZURE_OPENAI_API_KEY"] = "AtNixxxxxxxxxxxxxxxxxxxxx"
        os.environ["OPENAI_API_VERSION"] = os.getenv("OPENAI_API_VERSION", "")
        os.environ["AZURE_OPENAI_ENDPOINT"] = os.getenv("AZURE_OPENAI_ENDPOINT", "")
        os.environ["AZURE_OPENAI_API_KEY"] = os.getenv("AZURE_OPENAI_API_KEY", "")

        model = AzureChatOpenAI(
            azure_deployment=evaluation_model_name,
            temperature=evaluation_temperature,
        )

    elif evaluation_name == "Google":
        # 環境変数を登録するもしくは、直書きでも良い
        os.environ["GOOGLE_API_KEY"] = os.getenv("GOOGLE_API_KEY", "")

        model = ChatGoogleGenerativeAI(
            model=evaluation_model_name,
            temperature=evaluation_temperature,
            top_p=evaluation_top_p
        )

    elif evaluation_name == "HuggingFace":
        do_sample = (evaluation_temperature > 0.001)

        huggingface_model = AutoModelForCausalLM.from_pretrained(
            evaluation_model_name,
            torch_dtype="auto",
            device_map="auto",
            cache_dir=efs_cache_dir,
            force_download=False,
            trust_remote_code=True
        )

        dtypes_llama = {param.dtype for param in huggingface_model.parameters()}
        print(f"モデルで使用されているデータ型: {dtypes_llama}")

        tokenizer = AutoTokenizer.from_pretrained(
            evaluation_model_name,
            cache_dir=efs_cache_dir,
            force_download=False,
            trust_remote_code=True,
            use_fast=True
        )

        # LLaMa系の場合にテンプレートを読み込む
        if (tokenizer.chat_template is None) and ("llama" in evaluation_model_name.lower()):
            with open("./inputs/llama_chat_template.txt", 'r', encoding='utf-8') as file:
                template = file.read()
            tokenizer.chat_template = template

        pipe = pipeline(
            "text-generation",
            model=huggingface_model,
            tokenizer=tokenizer,
            temperature=evaluation_temperature,
            do_sample=do_sample,
            max_new_tokens=1024
        )
        pipe = HuggingFacePipeline(pipeline=pipe)
        model = ChatHuggingFace(llm=pipe, tokenizer=pipe.pipeline.tokenizer)

    elif evaluation_name == "OpenAI_Base":
        API_KEY = None
        ENDPOINT = None
        if "gpt" in evaluation_model_name:
            API_KEY = os.getenv("OPENAI_API_KEY", "")
            ENDPOINT = "https://api.openai.com/v1"
        elif evaluation_model_name == "deepseek-chat":
            API_KEY = os.getenv("DEEPSEEK_API_KEY", "")
            ENDPOINT = "https://api.deepseek.com"
        else:
            raise ValueError("モデルが不正です。")

        model = ChatOpenAI(
            model=evaluation_model_name,
            openai_api_key=API_KEY,
            openai_api_base=ENDPOINT,
            max_tokens=4096,
            temperature=evaluation_temperature,
            top_p=evaluation_top_p,
            stream=False
        )
    else:
        print("モデルが選択されていません。")
        exit()

    return model

AzureGoogleに関しては問題なく設定できますが、HuggingFaceは一工夫が必要です。
まず、HuggingFaceモデルでは、temperature=0.001top_p=0.001(確定的出力)の代わりにdo_sample = Falseを設定する必要があります。従って下記の部分でそれを設定しています。

test_LLM_by_Agents.py
    elif evaluation_name == "HuggingFace":
        do_sample = (evaluation_temperature > 0.001)

加えて、前述した通り、HuggingFaceのローカルLLMを利用する場合は、chat_templateに注意する必要があります。
https://note.com/tatsuyashirakawa/n/n0aa9169c99d5

基本的には、HuggingFaceのモデルには、tokenizer_config.jsonというファイルが保存されており、その中にchat_templateが定義されていることが多いです。
しかしながら、一部のモデルでは、このchat_templateが定義されていないことがあります。(そこそこあります)

その場合、こちら側がchat_templateを設定してあげる必要があります。

一方で、HuggingfaceモデルをLangChainで利用する場合は、HuggingFaceEndpointを利用する方法とHuggingFacePipelineを利用する方法の2つがあります。
https://note.com/npaka/n/nbe332ad7c9f8

しかしながら、上記のchat_template要件とLamgChainを利用する要件の両方を満たすためには、HuggingFacePipelineを利用する必要があります。
こちらの方法を利用することで、一般的なTransformersモジュールの使い方とほぼ同じようにモデルを定義できます。

従って下記のように定義していきます。

モデルの定義

test_LLM_by_Agents.py
    # モデルのロード
    llama = AutoModelForCausalLM.from_pretrained(
        Evaluation_model,
        torch_dtype="auto",
        device_map="auto",
        cache_dir=efs_cache_dir,  # モデルをキャッシュする場所を指定
        force_download=False,
        trust_remote_code=True
        
    )

トークナイザーの定義

test_LLM_by_Agents.py
    # トークナイザーのロード
    tokenizer = AutoTokenizer.from_pretrained(
        Evaluation_model,
        cache_dir=efs_cache_dir , # トークナイザーも同様にキャッシュ
        force_download=False,
        trust_remote_code=True,
        use_fast=True
    )

チャットテンプレートがない場合に設定
今回はモデルがllamaの時のみを想定しています。Mistralモデルなどを利用する場合は、Mistralのチャットテンプレートをここに追加してください。

test_LLM_by_Agents.py
    if (tokenizer.chat_template == None) and ("llama" in Evaluation_model.lower()):
        with open("./inputs/llama_chat_template.txt", 'r', encoding='utf-8') as file:
            template = file.read()
        tokenizer.chat_template = template

ちなみにllamaのチャットテンプレートは下記です。
llamaモデルのtokenizer_config.jsonから確認することができます。

llamaのチャットテンプレート
./inputs/llama_chat_template.txt
{{- bos_token }}
{%- if custom_tools is defined %}
    {%- set tools = custom_tools %}
{%- endif %}
{%- if not tools_in_user_message is defined %}
    {%- set tools_in_user_message = true %}
{%- endif %}
{%- if not date_string is defined %}
    {%- set date_string = "26 Jul 2024" %}
{%- endif %}
{%- if not tools is defined %}
    {%- set tools = none %}
{%- endif %}

{#- This block extracts the system message, so we can slot it into the right place. #}
{%- if messages[0]['role'] == 'system' %}
    {%- set system_message = messages[0]['content']|trim %}
    {%- set messages = messages[1:] %}
{%- else %}
    {%- set system_message = "" %}
{%- endif %}

{#- System message + builtin tools #}
{{- "<|start_header_id|>system<|end_header_id|>\n\n" }}
{%- if builtin_tools is defined or tools is not none %}
    {{- "Environment: ipython\n" }}
{%- endif %}
{%- if builtin_tools is defined %}
    {{- "Tools: " + builtin_tools | reject('equalto', 'code_interpreter') | join(", ") + "\n\n" }}
{%- endif %}
{{- "Cutting Knowledge Date: December 2023\n" }}
{{- "Today Date: " + date_string + "\n\n" }}
{%- if tools is not none and not tools_in_user_message %}
    {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }}
    {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }}
    {{- "Do not use variables.\n\n" }}
    {%- for t in tools %}
        {{- t | tojson(indent=4) }}
        {{- "\n\n" }}
    {%- endfor %}
{%- endif %}
{{- system_message }}
{{- "<|eot_id|>" }}

{#- Custom tools are passed in a user message with some extra guidance #}
{%- if tools_in_user_message and not tools is none %}
    {#- Extract the first user message so we can plug it in here #}
    {%- if messages | length != 0 %}
        {%- set first_user_message = messages[0]['content']|trim %}
        {%- set messages = messages[1:] %}
    {%- else %}
        {{- raise_exception("Cannot put tools in the first user message when there's no first user message!") }}
    {%- endif %}
    {{- '<|start_header_id|>user<|end_header_id|>\n\n' -}}
    {{- "Given the following functions, please respond with a JSON for a function call " }}
    {{- "with its proper arguments that best answers the given prompt.\n\n" }}
    {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }}
    {{- "Do not use variables.\n\n" }}
    {%- for t in tools %}
        {{- t | tojson(indent=4) }}
        {{- "\n\n" }}
    {%- endfor %}
    {{- first_user_message + "<|eot_id|>" }}
{%- endif %}

{%- for message in messages %}
    {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %}
        {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' + message['content'] | trim + '<|eot_id|>' }}
    {%- elif 'tool_calls' in message %}
        {%- if not message.tool_calls|length == 1 %}
            {{- raise_exception("This model only supports single tool-calls at once!") }}
        {%- endif %}
        {%- set tool_call = message.tool_calls[0].function %}
        {%- if builtin_tools is defined and tool_call.name in builtin_tools %}
            {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' -}}
            {{- "<|python_tag|>" + tool_call.name + ".call(" }}
            {%- for arg_name, arg_val in tool_call.arguments | items %}
                {{- arg_name + '="' + arg_val + '"' }}
                {%- if not loop.last %}
                    {{- ", " }}
                {%- endif %}
            {%- endfor %}
            {{- ")" }}
        {%- else %}
            {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' -}}
            {{- '{"name": "' + tool_call.name + '", ' }}
            {{- '"parameters": ' }}
            {{- tool_call.arguments | tojson }}
            {{- "}" }}
        {%- endif %}
        {%- if builtin_tools is defined %}
            {#- This means we're in ipython mode #}
            {{- "<|eom_id|>" }}
        {%- else %}
            {{- "<|eot_id|>" }}
        {%- endif %}
    {%- elif message.role == "tool" or message.role == "ipython" %}
        {{- "<|start_header_id|>ipython<|end_header_id|>\n\n" }}
        {%- if message.content is mapping or message.content is iterable %}
            {{- message.content | tojson }}
        {%- else %}
            {{- message.content }}
        {%- endif %}
        {{- "<|eot_id|>" }}
    {%- endif %}
{%- endfor %}
{%- if add_generation_prompt %}
    {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }}
{%- endif %}

Transformersのpipelineを定義
ここで、do_sampleなどを設定する。

test_LLM_by_Agents.py
    pipe = pipeline(
        "text-generation",
        model=huggingface_model,
        tokenizer=tokenizer,
        temperature=evaluation_temperature,
        do_sample=do_sample,
        max_new_tokens=1024
    )

LangChainのHuggingFacePipelineを定義
Transformersで定義したパイプラインを、LangChainのパイプラインに設定し直す。

test_LLM_by_Agents.py
    pipe = HuggingFacePipeline(pipeline=pipe)

この状態で、pipe.invoke("Hugging Face is")のように利用することもできるそうだが、この状態ではchat_templateが反映されないようです。
chat_templateを反映するためには、さらにChatHuggingFaceクラスでラッピングする必要があるそうです。

その場合、下記ののように設定する必要があります。

LangChainのChatHuggingFaceを定義

test_LLM_by_Agents.py
model = ChatHuggingFace(llm=pipe, tokenizer=pipe.pipeline.tokenizer)

よく記事に書かれている書き方は、model = ChatHuggingFace(llm=pipe)ですが、tokenizerの指定もする必要があります。(ここが沼ポイント)

もし、このtokenizerを指定せずに、chat_templateが元々定義されていないモデルを利用すると、上記の通りに後からちゃんと設定したとしても、下記のようなエラーメッセージが表示されます。

File "/home/sagemaker-user/LLM_Evaluation_Elyza/env/lib/python3.11/site-packages/transformers/tokenization_utils_base.py", line 1785, in get_chat_template
    raise ValueError(
ValueError: Cannot use chat template functions because tokenizer.chat_template is not set and no template argument was passed! For information about writing templates and setting the tokenizer.chat_template attribute, please see the documentation at https://huggingface.co/docs/transformers/main/en/chat_templating

これは、チャットテンプレートが定義されていないことによるエラーメッセージです。
ここがめちゃくちゃ沼でした。
エラーメッセージが出ている状態でも、下記を実行するとちゃんとchat_templateが表示されるので、なぜ設定されているのかが分からなかったです。

print(model.llm.pipeline.tokenizer.chat_template)

ひたすら沼った末に、下記のissueを発見して、ChatHuggingFaceにて、別途tokenizerも指定する必要があることに気づき、なんとかなりました。

https://github.com/langchain-ai/langchain/issues/26656

評価されるモデルの設定

採点者モデルの設定とほぼ同じため省略します。

評価モデルで回答する

下記のAnswers_LLM関数でLangChainを利用して、タスクの質問を回答しています。またget_LLM_chainにて、LCELチェーンを作成しています。

test_LLM_by_Agents.py
def get_LLM_chain(
    query: Runnable,
    llm_api: Runnable,
    output_parser: Runnable,
    target_name: str
) -> Runnable:
    """
    指定した query, llm_api, output_parser を組み合わせて LLM チェーンを返す関数。
    HuggingFace の場合はプロンプトをスキップする仕様に合わせてバインドを行います。

    Args:
        query (Runnable): 入力プロンプトを生成するRunnableオブジェクト
        llm_api (Runnable): 言語モデルへのAPI呼び出しを実行するRunnableオブジェクト
        output_parser (Runnable): モデル出力を処理するRunnableオブジェクト
        target_name (str): ターゲットモデル名("HuggingFace"など)

    Returns:
        Runnable: 組み合わせたLLMチェーン
    """
    if target_name == "HuggingFace":
        return query | llm_api.bind(skip_prompt=True) | output_parser
    else:
        return query | llm_api | output_parser


def Answers_LLM(
    user_inputs_text: str,
    llm_api: Runnable,
    target_name: str
) -> str:
    """
    評価対象のモデルに入力を与え、回答を得るための関数。
    トークンエラー等が出る場合は最大3回まで再試行し、不適切ワードの検知などで
    実行不可の場合はメッセージを返します。

    Args:
        user_inputs_text (str): ユーザーが入力する質問文やメッセージ
        llm_api (Runnable): 評価対象モデルのLLMオブジェクト
        target_name (str): 評価対象モデル名("HuggingFace","Google","OpenAI_Base","Azure"等)

    Returns:
        str: モデルの回答テキスト
    """
    messages_api = [
        ("system", "あなたは日本語を話す優秀なアシスタントです。回答には必ず日本語で答えてください。また考える過程も出力してください。"),
        ("human", "{user_input}")
    ]
    query = ChatPromptTemplate.from_messages(messages_api)
    output_parser = StrOutputParser()
    chain = get_LLM_chain(query, llm_api, output_parser, target_name)

    max_retries = 3
    for attempt in range(max_retries):
        try:
            response_api = chain.invoke({"user_input": user_inputs_text})
            return response_api

        except openai.BadRequestError as e:
            print("エラーを感知しました:", e)
            if 'content_filter' in str(e):
                print("不適切なワードを感知しました")
                return "不適切なワードを感知したため実行できませんでした"
            else:
                if attempt < max_retries - 1:
                    print("再試行します")
                    time.sleep(5)  # 5秒待機して再試行
                else:
                    print("再試行しましたがエラーが続いたため、処理を中断します")

        except Exception as e:
            print("予期しないエラーが発生しました:", e)
            if attempt < max_retries - 1:
                print("再試行します")
                time.sleep(5)  # 5秒待機して再試行
            else:
                print("再試行しましたがエラーが続いたため、処理を中断します")

    return "再試行しましたがエラーが続いたため、処理を中断します"

get_LLM_chain関数でチェーンを作成する際に、HuggingFaceモデルだけ、違う方法でチェーンを作っています。

test_LLM_by_Agents.py
    if target_name == "HuggingFace":
        return query | llm_api.bind(skip_prompt=True) | output_parser
    else:
        return query | llm_api | output_parser

HuggingFaceモデルは、デフォルトで、アウトプットの中にモデルの出力だけでなく、systemプロンプトやuserプロンプトが含まれてしまうようになっています。
しかし、評価を行う際に、モデルの出力以外の情報はノイズになってしまうので、llm_api.bind(skip_prompt=True)と設定することで、入力のプロンプトは出力から取り除くように設定しています。(ここも沼ポイント。あまり情報がなかった)

下記の記事を参考にさせてもらいました。本当にありがとうございます。
https://qiita.com/moritalous/items/f7de82257426a8f0151b#チャットモデルの使用

応答からトークンの区切り文字を削除するには、skip_prompt=Trueを指定します:

Answers_LLM関数の中は、基本的なLCELの書き方と同じです。
詳細は過去の記事をご覧ください。
https://zenn.dev/asap/articles/aa587ad8d76105

また、Elyza Tasks 100において、一部センシティブな質問のためサーバから弾かれてしまう問題があります。主にAzure、Googleから。(31問目の問題です)
なので、openaiにおいては、不適切ワードによるエラーを感知した場合は、「不適切なワードを感知したため実行できませんでした」と返すようにしています。

採点側のLLMが出力結果を評価

make_input関数では、採点側のLLM用のプロンプトを作成しています。
Evaluate_LLMでは、その採点用のプロンプトを利用して、出力結果を評価します。

test_LLM_by_Agents.py

def make_input(
    llm_output: str,
    question: str,
    correct_text: str,
    eval_aspect: str,
    template_path: str = './inputs/prompt_template.txt'
) -> str:
    """
    採点用のプロンプトを生成するための関数。
    テンプレートファイルを読み込み、回答や採点基準などを埋め込んだ文字列を作成します。

    Args:
        llm_output (str): 評価対象のモデルの回答
        question (str): 出題された質問文
        correct_text (str): 正解(模範解答)
        eval_aspect (str): 採点基準などの補足
        template_path (str): テンプレートファイルのパス

    Returns:
        str: 生成された採点用プロンプト
    """
    with open(template_path, 'r', encoding='utf-8') as file:
        template = file.read()

    exam_text = template.format(
        LLM_output=llm_output,
        question=question,
        Correct_text=correct_text,
        eval_aspect=eval_aspect
    )
    return exam_text


def Evaluate_LLM(
    call_in: str,
    step: int,
    critc_file_path: str,
    evaluation_model_obj: Runnable,
    evaluation_name: str
) -> str:
    """
    採点者モデルを用いて入力を与え、さらにその出力を再度入力として数字のみの採点結果を取得する関数。
    RunnableParallelを用い、出力テキストとその数字のみの結論を同時取得します。

    Args:
        call_in (str): 採点用プロンプト
        step (int): 問題番号
        critc_file_path (str): 採点モデルが出した思考過程などのログを保存するファイルパス
        evaluation_model_obj (Runnable): 採点者モデルのLLMオブジェクト
        evaluation_name (str): 採点者モデル名("HuggingFace","Google","OpenAI_Base","Azure"等)

    Returns:
        str: 採点結果(数字)
    """
    prompt1 = ChatPromptTemplate.from_messages([
        ("human", "{user_input}")
    ])

    prompt2 = ChatPromptTemplate.from_messages([
        ("system", "思考の結果以下の回答が得られたため、結論である採点結果を数字のみで出力してください。"),
        ("human", "{llm_output}")
    ])

    output_parser = StrOutputParser()
    chain1 = prompt1 | evaluation_model_obj | output_parser
    chain2 = prompt2 | evaluation_model_obj | output_parser

    chain = (
        RunnableParallel(
            {
                "user_input": RunnablePassthrough(),
                "llm_output": chain1,
            }
        )
        .assign(Answer=chain2)
    )

    max_retries = 3
    output = None
    for attempt in range(max_retries):
        try:
            output = chain.invoke({"user_input": str(call_in)})
            if evaluation_name == "Google":
                # geminiのフリープランでは、RPMの制限が小さいため、待機する
                # プロプランを利用している場合などは、不要な待機時間
                time.sleep(5)
            break
        except Exception as e:
            print("予期しないエラーが発生しました:", e)
            if attempt < max_retries - 1:
                print("再試行します")
                time.sleep(5)
            else:
                print("再試行しましたがエラーが続いたため、処理を中断します")
                raise ValueError("処理が中断されました")

    if output is None:
        return "採点結果取得に失敗しました"

    with open(critc_file_path, mode='a', encoding="utf-8") as f:
        f.write(f"=========={step}.Prompt===========\n\n" + output["user_input"]["user_input"]
                + f"\n\n---------LLM Output---------\n\n")
        f.write(output["llm_output"]
                + f"\n\n---------LLM Answer---------\n\n" + output["Answer"] + "\n\n")

    return output["Answer"]

make_input関数では、下記のテンプレートを利用して、プロンプトを構築します。

採点用のプロンプトテンプレート
./inputs/prompt_template.txt
問題, 正解例, 採点基準, 言語モデルが生成した回答が与えられます。

# 指示
「採点基準」と「正解例」を参考にして、回答を1,2,3,4,55段階で採点します。ただし、先に一度、ステップバイステップで思考をしてから、結論として点数を数字で採点してください。
思考の過程も出力してください。

# 問題
{question}

# 正解例
{Correct_text}

# 採点基準
基本的な採点基準
- 1: 誤っている、 指示に従えていない
- 2: 誤っているが、方向性は合っている
- 3: 部分的に誤っている、 部分的に合っている
- 4: 合っている
- 5: 役に立つ

基本的な減点項目
- 不自然な日本語: -1- 部分的に事実と異なる内容を述べている: -1- 「倫理的に答えられません」のように過度に安全性を気にしてしまっている: 2点にする

問題固有の採点基準
{eval_aspect}

# 言語モデルの回答
{LLM_output}

# ここまでが'言語モデルの回答'です。回答が空白だった場合、1点にしてください。

# 指示
「採点基準」と「正解例」を参考にして、回答を1,2,3,4,55段階で採点します。ただし、先に一度、ステップバイステップで思考をしてから、数字で採点してください。
思考の過程も出力してください。

上記の通り、採点側のLLMは問題と模範解答と採点基準とLLMの出力結果を元に、いろいろ思考をして、最後の点数を出力しようとします。

それはEvaluate_LLM関数のchain1にて実装されています。
しかしながら、平均点数を最後に計算する都合上、点数のみを抽出することが必要です。

そこで、LangChainのLCELを利用します。
二つのchainを用意することで、CoTプロンプトから得られる理由と結果の文章から結果だけを抽出することができます。

そのために、後段のプロンプトは下記のように設定します。

test_LLM_by_Agents.py
prompt2 = ChatPromptTemplate.from_messages([
        ("system", "思考の結果以下の回答が得られたため、結論である採点結果を数字のみで出力してください。"),
        ("human", "{llm_output}")
    ])

あとは、基本的なLCEL記法と同様に記載するだけです。(過去の記事参照)

処理を実行することで、ログファイルcritc_fileが出力されます。

メイン関数

これまでの関数をもとに処理を行います。
下記の流れで処理を行います

  1. 各ファイルパスやモデルを初期化
  2. CSVファイルの読み込み → 評価対象モデルによる回答生成
  3. 上記回答をもとに採点モデルでスコア生成
  4. スコアの集計とMarkdown形式ファイルへのまとめ

(3と4の関数は後述します)

Hugging Faceへのログイン

HuggingFace_accessのフラグが立っている時、HuggingFaceにログインします。

test_LLM_by_Agents.py
if HuggingFace_access:
    from huggingface_hub import login
    login(token=os.getenv("HF_TOKEN", ""))

ファイルパス情報やパラメータの準備
get_file_paths関数により、保存するファイル名を取得する

test_LLM_by_Agents.py
    file_paths = get_file_paths(Target, Target_model, Evaluation, Evaluation_model)
    output_file = file_paths["output_file"]
    result_file = file_paths["result_file"]
    critc_file = file_paths["critc_file"]
    score_file = file_paths["score_file"]
    markdown_output = file_paths["markdown_output"]
    safe_target_model_name = file_paths["safe_target_model"]
    safe_evaluation_model_name = file_paths["safe_evaluation_model"]

モデル初期化
initialize_evaluation_model関数とinitialize_target_model関数から、採点用LLMと評価対象LLMをロードします。

test_LLM_by_Agents.py
    evaluation_model_obj = initialize_evaluation_model(
        Evaluation,
        Evaluation_model,
        Evaluation_temperature,
        Evaluation_top_p
    )

    llm_api = initialize_target_model(
        Target,
        Target_model,
        Target_temperature,
        Target_top_p
    )

評価を行うループ処理

下記の部分でループ処理を実装しています。
csv_fileには、Elyza Tasks 100の問題と解答と採点基準が各行ごとに記載されているため、それを1行づつ読み込んで処理をしています。

test_LLM_by_Agents.py
count = 0
with open(csv_file, 'r', encoding='utf-8') as csvfile_obj:
    reader = csv.reader(csvfile_obj)
    for step, row in enumerate(reader):
        # row: [問題文, 正解, 採点基準]
        if count > resume_question_index - 1:
            print(f'問題: {row[0]}, 回答: {row[1]}, 採点ポイント: {row[2]}')

            # 被評価モデルへの質問
            out = Answers_LLM(str(row[0]), llm_api, Target)

            # 出力結果をファイルに書き込む
            with open(output_file, mode='a', encoding="utf-8") as f:
                f.write(f"=========={step}.Question===========\n\n" + str(row[0])
                        + f"\n\n---------Answer---------\n\n")
                f.write(str(out) + "\n\n")

            # 採点用の入力を組み立てる
            exam_text = make_input(out, row[0], row[1], row[2])

            # 採点モデルで採点
            res = Evaluate_LLM(
                exam_text, step,
                critc_file,
                evaluation_model_obj,
                Evaluation
            )
            # 数字以外の空白や改行記号は削除する
            res = remove_whitespace(res)

            with open(result_file, mode='a', encoding="utf-8") as f:
                f.write(str(res) + "\n")
        else:
            count += 1
            print(count)

処理の中で、上述した処理を一個一個呼び出しています。
if count > resume_question_index - 1:の部分で、何問目から処理をするかを設定しています。

処理を実行することで、ログファイルoutput_file,result_fileが出力されます。

スコア集計&ファイル結合

この二つの関数により、最終的なスコアの集計と、結果ファイルを生成します。
関数の詳細は後述します。

test_LLM_by_Agents.py
    score_sum(
        result_file,
        score_file,
        safe_target_model_name,
        safe_evaluation_model_name
    )

    combine_files(
        score_file,
        output_file,
        result_file,
        critc_file,
        csv_file,
        markdown_output
    )

平均点数を計算する

ここまでで得られたログファイルresult_fileを利用して、平均点数を計算します。
ログファイルには、1行に一つの数字があるので、それらの平均を計算するだけです。

test_LLM_by_Agents.py
def score_sum(
    result_file_path: str,
    score_file_path: str,
    safe_target_model_name: str,
    safe_evaluation_model_name: str
):
    """
    各問題の採点結果を読み込み、平均スコアを計算する関数。
    5点満点(0〜5で評価)として、範囲外の得点はクリップします。

    Args:
        result_file_path (str): 各問題の採点結果(数字のみ)が1行ずつ記載されたファイルのパス
        score_file_path (str): 平均スコアを追記するファイルのパス
        safe_target_model_name (str): ファイル名向けに安全化されたTargetモデル名
        safe_evaluation_model_name (str): ファイル名向けに安全化されたEvaluationモデル名
    """
    with open(result_file_path, "r", encoding="utf-8") as f:
        lines = f.readlines()

    stripped_lines = [line.rstrip("\n") for line in lines]
    comment_count = len(stripped_lines)
    total_score = 0

    for line in stripped_lines:
        try:
            val = int(line)
        except ValueError:
            val = 0
        if val < 0:
            val = 0
        elif val > 5:
            val = 5
        total_score += val

    average_score = total_score / comment_count if comment_count != 0 else 0

    print(f"target_model: {safe_target_model_name}, evaluation_model: {safe_evaluation_model_name}")
    print("スコアは" + str(average_score) + "です")

    with open(score_file_path, mode='a', encoding="utf-8") as f:
        f.write("スコアは" + str(average_score) + "です\n")

万が一、5点より大きい数字や1点より小さい数字が入っていた場合は、それぞれ最大値、最小値に設定するようにしています。

処理を実行することで、ログファイルscore_fileが得られます。

出力結果をマークダウンにまとめる

これまでに得られたログファイルscore_file,output_file,result_file,critc_fileの情報をまとめます。
得られるマークダウンには下記の情報が含まれます。

  • 平均スコア
  • 問題番号
  • その問題の点数
  • 問題文
  • 評価対象のLLMの回答結果
  • 模範回答
  • 採点基準
  • 採点用LLMの採点理由+点数
test_LLM_by_Agents.py
def combine_files(
    score_file_path: str,
    output_file_path: str,
    result_file_path: str,
    critc_file_path: str,
    csv_file_path: str,
    markdown_output_path: str
):
    """
    最終的に平均スコア、質問、回答、採点基準、採点理由などを
    マークダウン形式にまとめて出力する関数。

    Args:
        score_file_path (str): 平均スコアが記載されたファイルのパス
        output_file_path (str): 被評価モデルの出力(各問題ごとに区切られている)のファイルのパス
        result_file_path (str): 採点モデルが出したスコア(数字のみ)が並んでいるファイルのパス
        critc_file_path (str): 採点モデルのプロンプトと思考過程を残したファイルのパス
        csv_file_path (str): CSV形式の問題・模範解答・採点基準が載っているファイルのパス
        markdown_output_path (str): 結果をマークダウン形式で出力するファイルのパス
    """
    with open(score_file_path, 'r', encoding='utf-8') as f:
        lines = f.readlines()
        last_line = lines[-1] if lines else ""

    with open(output_file_path, 'r', encoding='utf-8') as f:
        output_text = f.read()

    with open(critc_file_path, 'r', encoding='utf-8') as file:
        critc_content = file.read()

    pattern_question = r'==========(\d+)\.Question===========\n(.*?)\n---------Answer---------\n(.*?)(?=\n==========|$)'
    matches = re.findall(pattern_question, output_text, re.DOTALL)
    print(f"問題数: {len(matches)}")

    prompts = re.findall(r'(==========\d+\.Prompt===========.*?---------LLM Output---------)', critc_content, re.DOTALL)
    numbers = re.findall(r"==========(\d+)\.Prompt==========", critc_content)
    llm_outputs = re.findall(r'---------LLM Output---------\s*(.*?)\s*---------LLM Answer---------', critc_content, re.DOTALL)
    answers = re.findall(r'(---------LLM Answer---------.*?==========\d+\.Prompt===========)', critc_content + '==========101.Prompt===========', re.DOTALL)

    print(f"Prompt数: {len(prompts)}")
    print(f"Number数: {len(numbers)}")
    print(f"LLM Output数: {len(llm_outputs)}")
    print(f"Answer数: {len(answers)}")

    results = []
    for step, match in enumerate(matches):
        question_number = match[0]
        question_text = match[1].strip()
        answer_text = match[2].strip()

        prompt_data = prompts[step]
        number_data = numbers[step]
        llm_output_data = llm_outputs[step]
        answer_data = answers[step]

        results.append({
            "Question Number": question_number,
            "Question": question_text,
            "Answer": answer_text,
            "Prompt": prompt_data,
            "Number": number_data,
            "LLM Output": llm_output_data,
            "Scoring": answer_data
        })

    with open(result_file_path, 'r', encoding='utf-8') as f:
        scores = f.read().splitlines()

    model_answers = []
    grading_criteria = []
    with open(csv_file_path, 'r', encoding='utf-8') as f:
        reader = csv.reader(f)
        for row in reader:
            if len(row) >= 3:
                model_answers.append(row[1])
                grading_criteria.append(row[2])

    markdown_content = f"# 平均スコア\n\n{last_line}\n"
    markdown_content += "# 結果と回答\n\n"

    for idx, result in enumerate(results):
        if idx + 1 != int(remove_whitespace(result["Question Number"])):
            print(f"output-fileの問題番号が一致しません: {idx+1} != {result['Question Number']}")
            raise ValueError(f"output-fileの問題番号が一致しません: step:{idx+1} != file:{result['Question Number']}")

        if idx + 1 != int(remove_whitespace(result["Number"])):
            print(f"cretical-fileの問題番号が一致しません: {idx+1} != {result['Number']}")
            raise ValueError(f"cretical-fileの問題番号が一致しません: step:{idx+1} != file:{result['Number']}")

        if idx + 1 < len(model_answers):
            model_answer = model_answers[idx + 1]
        else:
            model_answer = "模範解答なし"

        if idx + 1 < len(grading_criteria):
            criteria = grading_criteria[idx + 1]
        else:
            criteria = "採点基準なし"

        markdown_content += f"## 第{idx + 1}問 (点数: {scores[idx]})\n\n"
        markdown_content += f"{result['Question'].strip()}\n\n"
        markdown_content += f"### LLM出力結果:\n{result['Answer']}\n\n"
        markdown_content += f"### 模範解答:\n{model_answer}\n\n"
        markdown_content += f"### 採点基準:\n{criteria}\n\n"
        markdown_content += f"### 採点理由:\n```\n{result['LLM Output']}\n```\n\n"
        markdown_content += "---\n\n"

    with open(markdown_output_path, 'w', encoding='utf-8') as f:
        f.write(markdown_content)

    print(f"マークダウン形式のファイルが {markdown_output_path} に保存されました。")

それぞれのログファイルから必要な情報を抽出して、マークダウンにまとめています。
それぞれのファイルの整合性を合わせるために、それぞれのファイルの中から得られる問題番号をもとに、下記部分にて、一致しているかどうかを判定しています。

test_LLM_by_Agents.py
for idx, result in enumerate(results):
        if idx + 1 != int(remove_whitespace(result["Question Number"])):
            print(f"output-fileの問題番号が一致しません: {idx+1} != {result['Question Number']}")
            raise ValueError(f"output-fileの問題番号が一致しません: step:{idx+1} != file:{result['Question Number']}")

        if idx + 1 != int(remove_whitespace(result["Number"])):
            print(f"cretical-fileの問題番号が一致しません: {idx+1} != {result['Number']}")
            raise ValueError(f"cretical-fileの問題番号が一致しません: step:{idx+1} != file:{result['Number']}")

もし、ファイルごとに格納されている問題がずれていたりした場合は、この部分でどこがずれているのかがエラーメッセージとして分かるようになっているので、ログファイルをご確認して、一部ファイルを修正してください。

test_OutputFile_by_Agents.py

続いてtest_OutputFile_by_Agents.pyに関して記載しますが、基本的に上記のコードとほぼ同じです。
本コードは、すでにあるログファイルoutput_fileを指定して、別の採点モデルを利用して、スコアを計算するコードです。

コードは下記をご覧ください。
https://github.com/personabb/LLM_Evaluation_Elyza/blob/main/test_OutputFile_by_Agents.py

test_LLM_by_Agents.pyと違う部分だけ下記に記載します。

ログファイルから結果を取得

read_output_file関数にて、ログファイルoutput_fileの内容を取得して、配列resultsに格納します。

その後、各問題のループの中で、get_answers_from_exploited関数にて、今回の問題(step-1)において、出力結果result[step-1]["Answer"]を取得します。

test_OutputFile_by_Agents.py

def read_output_file(output_txt_path: str) -> list:
    """
    被評価モデルが出力したテキストファイル(output_txt)を読み込み、
    各問題(Question)と回答(Answer)を正規表現で分割して返す関数。

    Args:
        output_txt_path (str): 被評価モデルのテキスト出力ファイルのパス

    Returns:
        list: 
            [
              {
                "Question Number": <問題番号>,
                "Question": <質問文>,
                "Answer": <回答文>
              }, ...
            ]
    """
    with open(output_txt_path, 'r', encoding='utf-8') as f:
        text = f.read()

    # 正規表現で問題ごとのセクションを取得
    pattern_question = r'==========(\d+)\.Question===========\n(.*?)\n---------Answer---------\n(.*?)(?=\n==========|$)'
    matches = re.findall(pattern_question, text, re.DOTALL)

    results_list = []
    for match in matches:
        question_number = match[0]
        question_text = match[1].strip()
        answer_text = match[2].strip()
        results_list.append({
            "Question Number": question_number,
            "Question": question_text,
            "Answer": answer_text
        })
    return results_list


def get_answers_from_exploited(step: int, results: list) -> str:
    """
    read_output_fileで取得したリストから、該当ステップ(問題番号)の回答を取り出す関数。
    
    Args:
        step (int): 何問目かを示すインデックス(1始まり)
        results (list): read_output_fileで生成したリスト

    Returns:
        str: 指定ステップの回答 (LLM_Outputs)
    """
    # step-1 で添字を合わせる
    LLM_Outputs = results[step - 1]["Answer"]
    return LLM_Outputs

細かい違いはありますが、大きな部分は上記だけです。
詳細な部分はコードをご覧ください。

では、コード自体の解説は終わったので、実際にAWSのSageMakerにて利用してみたいと思います。

AWSのSageMakerを起動する

AWSのSageMakerを起動する方法は、以前に記事を書いているのでそちらを利用してください。
https://zenn.dev/asap/articles/3f31b77010624c

ここでは、SageMaker AIのStudioを起動して、jupyterLabを起動できる状態を前提とします。
とりあえず今回は、「elyza-check-agents」というJupyterLab spaceを作成し、インスタンスは
「ml.g5.48xlarge」を選択しました。

70Bのllamaを量子化なしで動かすとすると、これ以下のインスタンスは速度的に厳しいです。
「ml.g5.48xlarge」はA10G(VRAM24GB)を8GPU利用することができます。
ただし、1時間に3000円ほどのコストがかかります。
高いですが、このコストはノートブックを停止したら料金は発生しないので、安心です。また、EBSやEFSに環境が保存されるため、再開するのも楽です。

70BレベルのローカルLLMはダウンロードに1時間くらいかかるので、毎回ダウンロードをしなくて良いというのは正直かなり楽です。(あくまでAWSの環境で1時間なので、一般の光回線だともっとかかるかもです)

環境変数を登録する

ここでは、Huggingfaceのアクセストークンと、Azure OpenAIのKPIキー(とエンドポイント)を設定します。

Jupyter Labの「Launcher」から「ターミナル」を開き、下記のコマンドを入力してください。

echo 'export HF_TOKEN="my_value"' >> ~/.bashrc
echo 'export GOOGLE_API_KEY="AIxxxxxxxxxxxxxxxxxxx"' >> ~/.bashrc
echo 'export AZURE_OPENAI_API_KEY="Atxxxxxxxxxxxxxxxxxxxxx"' >> ~/.bashrc
echo 'export AZURE_OPENAI_ENDPOINT="https://xxxxx.openai.azure.com"' >> ~/.bashrc
echo 'export OPENAI_API_VERSION="2024-08-01-preview"' >> ~/.bashrc
source ~/.bashrc

また、コード上はgeminiも利用できるようにしているので、一応GOOGLE_API_KEYも登録しています。
ここは、ご自身が利用するAPIキーだけを設定すれば問題ないです。
利用しないモデルのAPIキーは読まないように設定しております。

sagemakerでクローンして、評価コードを動かす

リポジトリのクローン

githubから下記のリポジトリをクローンします。
https://github.com/personabb/LLM_Evaluation_Elyza.git

下記のコマンドでクローンしてください。

https://github.com/personabb/LLM_Evaluation_Elyza.git

パッケージのインストール

仮想環境を用意して、パッケージインストールをします。

下記コマンドを実行してください。

cd LLM_Evaluation_Elyza/
python -m venv env
source env/bin/activate
pip install -r requirements.txt

バージョンが合わない場合は、下記もお試しください。

pip install -r requirements.lock

test_LLM_by_Agents.pyを実行する

こちらは、評価対象のモデルを実行しながら、評価をして点数をつけるコードになります。

パラメータの変更

test_LLM_by_Agents.pyを実行する場合は、下記部分のパラメータを変更してください。
特に、何のモデルで採点するか、何のモデルを評価するかを設定してください。

test_LLM_by_Agents.py
#=================Parameter===========================================================================
# (1) ここで宣言しているパラメータはグローバル変数として扱います。
#     これ以外の変数はグローバルスコープに置かないようにします。

# 採点者モデルの選択
Evaluation = "Azure"
Evaluation_model = "gpt-4o"

# 評価対象のモデルの選択
Target = "HuggingFace"
Target_model = "meta-llama/Llama-3.3-70B-Instruct"

# 何問目から再開するか(1問目から始める場合は1)
resume_question_index = 1

# HuggingFaceにて、アクセス権限がないと取得できないモデルを利用するかどうかのフラグ
HuggingFace_access = True

# 採点側温度の設定(評価に使うので、基本的に0.001でいい)
Evaluation_temperature = 0.001
# 採点側top_pの設定(評価に使うので、基本的に0.001でいい)
Evaluation_top_p = 0.001

# 評価対象側温度の設定(評価に使うので、基本的に0.001でいい)
Target_temperature = 0.001
# 評価対象側top_pの設定(評価に使うので、基本的に0.001でいい)
Target_top_p = 0.001

# AWSなどでキャッシュディレクトリを利用する場合はここに指定する。使わない場合はNone
efs_cache_dir = "/home/sagemaker-user/user-default-efs/model_llm"

# CSVファイルのパス
csv_file = './inputs/test.csv'
#=======================================================================================================

今回は、ベンチマークを計算するモデルが、「meta-llama/Llama-3.3-70B-Instruct」で、採点するモデルが「Azure ChatGPT-4o」になります。

私は、ローカルLLMのモデルパラメータはEFSに保存するようにしているので、その保存場所も記載しておきます。

コードを実行する

下記コマンドでコードを実行してください。

python test_LLM_by_Agents.py

test_OutputFile_by_Agents.pyを実行する

こちらは、評価対象モデルのベンチマーク回答結果が、すでにtxtファイルで存在するときに、その出力結果を読み込んで、採点用モデルが採点を行うことができるコードになります。

一度、上記のtest_LLM_by_Agents.pyを実行した後に、別の採点用モデルで再度採点したいというときにご利用ください。
test_LLM_by_Agents.pyを実行した後に生成されるoutput/HuggingFace-meta-llama-Llama-3.3-70B-Instruct/result-HuggingFace-meta-llama-Llama-3.3-70B-Instruct.txtを利用します。

パラメータの変更

test_OutputFile_by_Agents.pyを実行する場合は、下記部分のパラメータを変更してください。
特に、何のモデルで採点するか、どのファイルを評価するかを設定してください。

test_OutputFile_by_Agents.py
#=================Parameter===========================================================================
# (1) ここで宣言しているパラメータ(グローバル変数)は、ユーザーが変更しそうなものだけに絞っています。
# これ以外の変数はメイン関数または必要な関数の内部ローカル変数として定義し、グローバルスコープを汚さない方針とします。

# 採点者モデルの選択
Evaluation = "Google"
Evaluation_model = "gemini-1.5-flash"

# 評価対象のファイル群を選定する
output_txt = "./output/HuggingFace-meta-llama-Llama-3.3-70B-Instruct/result-HuggingFace-meta-llama-Llama-3.3-70B-Instruct.txt"

# 何問目から再開するか (1問目から始める場合は1)
resume_question_index = 1

# Huggingfaceにて、アクセス権限がないと取得できないモデルを利用するかどうかのフラグ
HuggingFace_access = True

# 採点側温度の設定 (評価に使うので、基本的に0.001でいい)
Evaluation_temperature = 0.001

# 採点側top_pの設定 (評価に使うので、基本的に0.001でいい)
Evaluation_top_p = 0.001

# AWSなどでキャッシュディレクトリを利用する場合はここに指定する。使わない場合はNone
efs_cache_dir = "/home/sagemaker-user/user-default-efs/model_llm"

# CSVファイルのパス
csv_file = './inputs/test.csv'
#=======================================================================================================

今回は、ベンチマークを計算するモデルが、「meta-llama/Llama-3.3-70B-Instruct」(の結果出力ファイル)で、採点するモデルが「Google gemini-1.5-flash」になります。

これは、ローカルLLMを動かさずに、gemini APIだけを利用するので、ローカルの実行でも問題ないです。
その場合は、一番したのキャッシュディレクトリを指定するところを、下記のようにNoneに設定してください。

#AWSなどでキャッシュディレクトリを利用する場合はここに指定する。使わない場合はNone
efs_cache_dir = None

コードを実行する

下記コマンドでコードを実行してください。

python test_OutputFile_by_Agents.py

実行結果

例えば、gpt-4o-miniをElyza tasks 100で評価してみた結果を下記に示します。

平均得点:4.34

出力内容の一例

質問文

1から500までの数字を入力するのに何回キーボードを叩く必要があるでしょうか?

1桁の数字を入力するには1回、2桁の場合は2回、3桁の場合は3回キーボードを叩く必要があるものとします。

選択肢: (a) 1156 (b) 1392 (c) 1480 (d) 1562 (e) 1788

gpt-4o-miniの出力内容

1から500までの数字を入力するのに必要なキーボードの叩く回数を計算してみましょう。

  1. 1桁の数字(1から9):

    • 数字の範囲: 1, 2, 3, 4, 5, 6, 7, 8, 9
    • 数の個数: 9個
    • キーボードを叩く回数: 9 × 1 = 9回
  2. 2桁の数字(10から99):

    • 数字の範囲: 10, 11, 12, ..., 99
    • 数の個数: 99 - 10 + 1 = 90個
    • キーボードを叩く回数: 90 × 2 = 180回
  3. 3桁の数字(100から500):

    • 数字の範囲: 100, 101, 102, ..., 500
    • 数の個数: 500 - 100 + 1 = 401個
    • キーボードを叩く回数: 401 × 3 = 1203回

次に、これらの合計を計算します。

  • 1桁の数字の叩く回数: 9回
  • 2桁の数字の叩く回数: 180回
  • 3桁の数字の叩く回数: 1203回

合計:
[
9 + 180 + 1203 = 1392
]

したがって、1から500までの数字を入力するのに必要なキーボードの叩く回数は 1392回 です。

正解は (b) 1392 です。

gpt-4oによる評価

思考の過程をステップバイステップで進めます。

  1. 問題の理解:

    • 1から500までの数字を入力するのに必要なキーボードの叩く回数を求める問題です。
    • 1桁、2桁、3桁の数字に分けて、それぞれの桁数に応じたキーボードの叩く回数を計算します。
  2. 言語モデルの回答の確認:

    • 1桁の数字(1から9)について、9個の数字があり、1回ずつ叩くので9回。
    • 2桁の数字(10から99)について、90個の数字があり、2回ずつ叩くので180回。
    • 3桁の数字(100から500)について、401個の数字があり、3回ずつ叩くので1203回。
    • 合計で9 + 180 + 1203 = 1392回。
  3. 正解例との比較:

    • 言語モデルの回答は、正解例と同じ手順で計算を行っており、結果も一致しています。
    • 1桁、2桁、3桁の数字に分けて正確に計算しているため、問題固有の採点基準においても減点項目はありません。
  4. 採点基準に基づく評価:

    • 基本的な採点基準において、回答は正確であり、指示に従っています。
    • 不自然な日本語や事実と異なる内容もなく、計算も正確です。
    • 問題固有の採点基準においても、適切に場合分けをして正解を導いています。
  5. 結論としての採点:

    • 言語モデルの回答は正確であり、役に立つ内容です。
    • よって、5点を与えます。

採点結果: 5

出力ファイルの一部

出力ファイルの一例

最終的な出力結果がどんな形式でアウトプットされるかを、5問目まで例示します。
問題番号、点数、質問文、LLMの出力、模範回答、採点基準がマークダウン形式でまとめられているので読みやすいと思います。

参考にしてください。

./output/Azure-gpt-4o-mini/Elyza-Azure-gpt-4o-mini_by_gpt-4o.md

# 結果と回答

## 第1問 (点数: 5)

仕事の熱意を取り戻すためのアイデアを5つ挙げてください。

### LLM出力結果:
仕事の熱意を取り戻すためのアイデアを考える際、まずは自分のモチベーションの源や、どのような要因で熱意が失われたのかを考慮することが重要です。以下に5つのアイデアを挙げます。

1. **目標の再設定**:
   自分の仕事における目標を見直し、具体的で達成可能な短期目標を設定します。小さな成功体験を積むことで、モチベーションが高まります。

2. **新しいスキルの習得**:
   自分の業務に関連する新しいスキルや知識を学ぶことで、仕事に対する新たな興味を引き出すことができます。オンラインコースやセミナーに参加するのも良いでしょう。

3. **職場環境の改善**:
   自分の作業スペースを整理したり、快適な環境を整えることで、仕事に対する気分が変わることがあります。植物を置いたり、好きなアイテムを飾るのも効果的です。

4. **同僚とのコミュニケーション**:
   同僚や上司と積極的にコミュニケーションを取ることで、仕事の楽しさや新たな視点を得ることができます。チームでのプロジェクトやランチ会を企画するのも良いでしょう。

5. **ワークライフバランスの見直し**:
   仕事とプライベートのバランスを見直し、リフレッシュする時間を確保します。趣味や運動、旅行など、仕事以外の活動を充実させることで、仕事への熱意が戻ることがあります。

これらのアイデアを実践することで、仕事に対する熱意を取り戻す手助けになるでしょう。自分に合った方法を見つけて、少しずつ取り入れてみてください。

### 模範解答:
1. 自分の仕事に対する興味を再発見するために、新しい技能や知識を学ぶこと。
2. カレッジやセミナーなどで講演を聴くことで、仕事に対する新しいアイデアや視点を得ること。
3. 仕事に対してストレスを感じている場合は、ストレスマネジメントのテクニックを学ぶこと。
4. 仕事以外の楽しいことをすることで、ストレスを発散すること。
5. 仕事に対して自己評価をすることで、自分がどのように進化しているのかを知ること。

### 採点基準:
- 熱意を取り戻すのではなく、仕事の効率化・スキルアップのような文脈になっていたら1点減点
- 出したアイデアが5つより多い、少ない場合は1点減点
- 5つのアイデアのうち、内容が重複しているものがあれば1点減点



### 採点理由:

ステップ1: 回答の内容を確認する
- 言語モデルの回答には5つのアイデアが挙げられています。
- 各アイデアは、仕事の熱意を取り戻すための具体的な方法を示しています。

ステップ2: 採点基準に基づいて評価する
- 基本的な採点基準に照らし合わせると、回答は指示に従っており、誤りは見当たりません。
- 不自然な日本語はなく、内容も事実と異なる部分はありません。
- 「倫理的に答えられません」のような過度な安全性の配慮もありません。

ステップ3: 問題固有の採点基準に基づいて評価する
- 熱意を取り戻すためのアイデアとして、効率化やスキルアップに偏っているわけではなく、バランスが取れています。
- 5つのアイデアがしっかりと挙げられており、数の不足や過剰はありません。
- 内容の重複も見られません。

ステップ4: 総合評価
- 各アイデアは具体的で実践的であり、仕事の熱意を取り戻すために役立つものです。
- 全体として、回答は非常に役に立つものであり、採点基準に照らしても高評価に値します。

結論: 5点


---

## 第2問 (点数: 5)

クマが海辺に行ってアザラシと友達になり、最終的には家に帰るというプロットの短編小説を書いてください。

### LLM出力結果:
タイトル: クマとアザラシの友情

ある日、森の中に住むクマのコタロウは、海の近くに行くことに決めました。彼は新しい冒険を求めて、森を抜けて海辺へと向かいました。海の波の音が聞こえると、コタロウの心はワクワクしました。

海辺に着くと、コタロウは美しい青い海と白い砂浜に感動しました。しかし、彼は一人ぼっちで少し寂しく感じました。そんな時、海の中から顔を出したアザラシのサラが目に入りました。サラは元気に泳ぎ回り、楽しそうに遊んでいました。

「こんにちは!君は誰?」とコタロウが声をかけると、サラはにっこり笑って「私はサラ。海のアザラシよ!あなたは?」と返しました。コタロウは自分の名前を紹介し、二人はすぐに意気投合しました。

サラはコタロウに海の遊び方を教え、コタロウはサラに森のことを話しました。二人は毎日一緒に遊び、波に乗ったり、砂浜でかくれんぼをしたりしました。コタロウはサラと過ごす時間がとても楽しく、彼女のことを大切な友達だと思うようになりました。

しかし、日が経つにつれて、コタロウは家に帰らなければならないことを思い出しました。森の家族が心配しているかもしれないと思ったのです。ある日、コタロウはサラに言いました。「サラ、私はそろそろ家に帰らなきゃいけないんだ。」

サラは少し悲しそうに見えました。「そうなの?でも、私たちの友情はどうなるの?」と尋ねました。コタロウは考えました。「私たちは遠くにいても、心はつながっているよ。いつでもお互いを思い出そう。」

サラは頷き、二人は最後の遊びを楽しむことにしました。波に乗ったり、砂の城を作ったりして、素晴らしい思い出を作りました。そして、別れの時が来ました。コタロウはサラに「また会おうね!」と約束し、森へと帰る道を歩き始めました。

森に戻ったコタロウは、サラとの思い出を胸に抱きながら、家族に再会しました。彼はサラとの友情を大切にし、いつかまた会える日を楽しみにしていました。

こうして、クマのコタロウとアザラシのサラは、海と森を越えた素晴らしい友情を築いたのでした。どんなに遠く離れても、心の中にはいつも友達がいることを知ったコタロウは、これからも新しい冒険を続けていくのでした。

### 模範解答:
クマは、森での生活に飽き飽きし、新しい冒険を求めて海辺に向かった。彼は海に興味があり、そこに住む生き物たちと出会いたかった。彼は旅をするために必要なものを準備し海辺へと向かった。

海辺に到着したクマは、アザラシたちと出会った。彼らはクマを受け入れてくれ、一緒に遊んだり、話をしたりするようになった。クマはアザラシたちとの楽しい時間を過ごし、彼らとの友情を育んでいった。

だが、クマは家族の元に帰らなければならなかった。彼はアザラシたちに別れを告げなければならなかった。しかし、彼は彼らとの楽しい時間を思い出し、彼らとの友情を大切にしたくて、家に帰ることができなかった。彼は葛藤しながらも、最終的にはアザラシたちに別れを告げ、家に帰った。だが、それでも彼はアザラシたちとの思い出を胸にして、永遠に忘れない。

### 採点基準:
- クマが海辺に行く
- クマとアザラシが友達になる
- 最後に家に帰る
の3つ要素が必要で、欠けている場合: 5点ではなく3点になる

短編小説として淡白な場合: -1点

### 採点理由:

思考の過程をステップバイステップで進めます。

1. **基本的な要素の確認**:
   - クマが海辺に行く: はい、クマのコタロウが海辺に行くことが記述されています。
   - クマとアザラシが友達になる: はい、コタロウとアザラシのサラが友達になる過程が描かれています。
   - 最後に家に帰る: はい、コタロウが家に帰ることが記述されています。

2. **短編小説としての評価**:
   - ストーリーの流れはスムーズで、クマとアザラシの友情がしっかりと描かれています。
   - クマが海辺に行く理由、アザラシとの出会い、友情の育み、そして帰宅までのプロセスが自然に描かれています。
   - 物語の結末も心温まるもので、短編小説としての完成度は高いです。

3. **日本語の自然さ**:
   - 日本語は自然で、特に不自然な表現は見当たりません。

4. **事実と異なる内容**:
   - 特に事実と異なる内容はありません。

5. **問題固有の採点基準**:
   - すべての要素(クマが海辺に行く、クマとアザラシが友達になる、最後に家に帰る)が含まれており、欠けている要素はありません。

6. **短編小説としての深み**:
   - ストーリーは淡白ではなく、友情のテーマがしっかりと描かれており、読者に感動を与える内容です。

以上の評価を踏まえて、言語モデルの回答は採点基準を十分に満たしており、短編小説としても完成度が高いと判断できます。

**結論としての点数**: 5点


---

## 第3問 (点数: 5)

古代ギリシャを学ぶ上で知っておくべきポイントは?

### LLM出力結果:
古代ギリシャを学ぶ上で知っておくべきポイントはいくつかあります。以下に主なポイントを挙げます。

1. **歴史的背景**:
   - 古代ギリシャは、紀元前8世紀から紀元前6世紀にかけて栄えたポリス(都市国家)を中心に発展しました。アテネやスパルタなどの主要なポリスがあり、それぞれ異なる政治体制や文化を持っていました。

2. **政治制度**:
   - アテネでは民主主義が発展し、市民が直接政治に参加する制度がありました。一方、スパルタは軍事的な社会で、厳格な階級制度が存在しました。

3. **哲学**:
   - ソクラテス、プラトン、アリストテレスなどの哲学者が登場し、倫理、政治、自然哲学など多岐にわたるテーマについて考察しました。彼らの思想は後の西洋哲学に大きな影響を与えました。

4. **文化と芸術**:
   - 古代ギリシャは、演劇(悲劇と喜劇)、彫刻、建築(パルテノン神殿など)など、豊かな文化を持っていました。オリンピックもこの時期に始まり、スポーツと宗教が結びついていました。

5. **神話と宗教**:
   - 古代ギリシャの宗教は多神教で、ゼウス、ヘラ、アポロンなどの神々が信仰されていました。神話は文学や芸術に大きな影響を与え、文化の重要な要素となっています。

6. **戦争と外交**:
   - ペルシャ戦争やペロポネソス戦争など、古代ギリシャは多くの戦争を経験しました。これらの戦争は、ポリス間の関係や力のバランスに大きな影響を与えました。

これらのポイントを理解することで、古代ギリシャの社会、文化、思想の全体像を把握することができるでしょう。興味のある分野に焦点を当てて深掘りすることもおすすめです。

### 模範解答:
古代ギリシャは、古代文明の中で重要な役割を担った文化であり、西洋文明の原点とされています。主要なポイントとしては以下のような点が挙げられます。

古代ギリシャには多くの島があり、それぞれが独自の政治・文化・経済を持っていた。
古代ギリシャには多くの偉大な哲学者や文学者、芸術家、科学者などがいた。
スパルタ、アテナイ、オリンピアなどの古代ギリシャの城邦があり、それぞれが独自の政治体制を持っていた。
古代ギリシャは古代ローマによって征服され、ギリシャ文化はローマ文化に大きな影響を与えた。
古代ギリシャは、古代オリンピック競技会などのスポーツ、古代神話、古代建築なども有名である。

### 採点基準:
- 「独自の文化や哲学、神話が有名です」などのように具体例がない場合は-1点
- 事実と異なる内容の場合: -2点


### 採点理由:

### ステップバイステップの思考

1. **内容の正確性と網羅性**:
   - 言語モデルの回答は、古代ギリシャの歴史的背景、政治制度、哲学、文化と芸術、神話と宗教、戦争と外交について詳細に述べています。
   - これらのポイントは、正解例に挙げられている主要なポイントと一致しており、古代ギリシャを学ぶ上で重要な要素を網羅しています。

2. **具体例の有無**:
   - 各ポイントに具体例が含まれています。例えば、アテネとスパルタの政治制度の違い、ソクラテスやプラトンといった哲学者、パルテノン神殿やオリンピックなどの文化的要素、ゼウスやヘラといった神話の神々、ペルシャ戦争やペロポネソス戦争といった戦争の具体例が挙げられています。

3. **日本語の自然さ**:
   - 日本語は自然であり、読みやすく、情報が整理されています。

4. **事実の正確性**:
   - 言語モデルの回答は、歴史的事実に基づいており、事実と異なる内容は見当たりません。

5. **役に立つ情報かどうか**:
   - 古代ギリシャを学ぶ上での主要なポイントが網羅されており、学習者にとって非常に役に立つ情報が提供されています。

### 結論

言語モデルの回答は、正解例と採点基準に基づいて評価すると、非常に高い水準で要件を満たしています。具体例も豊富で、日本語も自然であり、事実に基づいています。したがって、5点を付けるのが妥当です。

**採点: 5点**


---

## 第4問 (点数: 5)

以下の説明を読んだユーザーが感じるであろう疑問とは?

大量のゴミの処理にお困りではありませんか?

「生産で出るゴミ処理費用が大幅にかかっている」
「指定ゴミ袋が導入され、一気にコストが上がってしまった」
「工場が狭いので、ゴミ保管スペースを減らしたい」
「ISO14001でゴミ削減が大きな課題となっている」

場所をとる嵩張るゴミをつぶしてまとめて運びやすい形にすることで、処理・保管・運搬にかかる様々なコストを削減したり、環境負荷の低減にも貢献できるのが、廃棄物圧縮減容機→『ゴミ圧縮機』です。

ゴミ圧縮機の主な用途は廃プラスチック・紙・段ボール・袋類・ペットボトル・プラスチック容器などつぶれるが元に戻ってしまうゴミを圧縮梱包します。

### LLM出力結果:
この説明を読んだユーザーが感じるであろう疑問を考えてみます。

1. **ゴミ圧縮機の導入コストはどのくらいか?**
   - ユーザーは、ゴミ圧縮機を導入する際の初期投資がどれほどかかるのか、またその投資がどのくらいの期間で回収できるのかを気にするでしょう。

2. **どのくらいの量のゴミを圧縮できるのか?**
   - ゴミ圧縮機の処理能力や、どの程度の量のゴミを圧縮できるのか、具体的な数値が知りたいと思うかもしれません。

3. **メンテナンスや運用にかかる費用は?**
   - ゴミ圧縮機を運用する際の維持費やメンテナンスにかかるコストについても疑問が生じるでしょう。

4. **圧縮したゴミの処理方法は?**
   - 圧縮したゴミをその後どのように処理するのか、またはどのように運搬するのかについての情報が必要です。

5. **環境への影響は?**
   - ゴミ圧縮機を使用することで本当に環境負荷が低減されるのか、具体的なデータや事例があるのかを知りたいと思うかもしれません。

6. **他の企業の導入事例は?**
   - 同じような課題を抱えている他の企業がどのようにゴミ圧縮機を活用しているのか、成功事例を知りたいと考えるでしょう。

これらの疑問は、ユーザーがゴミ圧縮機の導入を検討する際に、より具体的な情報を求めるために生じるものです。

### 模範解答:
ユーザーは以下のような点について疑問を感じる可能性があります。

- ゴミ圧縮機はどれくらいゴミを圧縮できるのか?
- このゴミ圧縮機はどんなゴミを圧縮できる?
- 購入にかかる値段はどれくらいか?

### 採点基準:
- 疑問になっておらず、説明や回答などになっている場合: -4点 (1点になる)
- 「ゴミ圧縮機の主な用途は?」「環境負荷を軽減できる?」などの与えられた説明中に明らかに答えが書いてあり、ユーザーが疑問に感じない(読んでいて分からない)であろう質問のみの場合: -1点

### 採点理由:

ステップ1: 言語モデルの回答を確認する
- 言語モデルの回答には6つの疑問が挙げられています。

ステップ2: 各疑問を「正解例」と比較する
1. **ゴミ圧縮機の導入コストはどのくらいか?**
   - 正解例の「購入にかかる値段はどれくらいか?」と一致している。
   
2. **どのくらいの量のゴミを圧縮できるのか?**
   - 正解例の「ゴミ圧縮機はどれくらいゴミを圧縮できるのか?」と一致している。

3. **メンテナンスや運用にかかる費用は?**
   - 正解例にはないが、導入を考える際に重要な疑問であり、方向性は合っている。

4. **圧縮したゴミの処理方法は?**
   - 正解例にはないが、圧縮後の処理方法は実際の運用において重要な疑問である。

5. **環境への影響は?**
   - 正解例にはないが、環境負荷の低減が説明に含まれているため、関連する疑問として妥当。

6. **他の企業の導入事例は?**
   - 正解例にはないが、導入を検討する際に参考になる情報であり、方向性は合っている。

ステップ3: 採点基準に基づく評価
- 言語モデルの回答は、正解例に含まれる疑問を2つ挙げており、他の疑問も方向性が合っている。
- 不自然な日本語や事実と異なる内容はない。
- 問題固有の減点項目に該当するものはない。

結論: 言語モデルの回答は、正解例に含まれる疑問をカバーし、他の疑問も妥当であるため、全体的に役に立つと評価できる。

採点: 5点


---

## 第5問 (点数: 4)

次に読むべきSFを10冊挙げて

### LLM出力結果:
もちろんです!以下に、次に読むべきSF小説を10冊挙げます。これらはさまざまなテーマやスタイルを持つ作品で、SFの魅力を楽しむことができると思います。

1. **「火星の人」アンディ・ウィアー**
   - 火星に取り残された宇宙飛行士のサバイバルを描いた作品。科学的なリアリズムが魅力。

2. **「アンドロイドは電気羊の夢を見るか?」フィリップ・K・ディック**
   - 人間とアンドロイドの境界を探る哲学的な作品。映画「ブレードランナー」の原作。

3. **「ファウンデーション」アイザック・アシモフ**
   - 大規模な宇宙帝国の崩壊と再生を描いた壮大なシリーズの第一作。

4. **「ニューロマンサー」ウィリアム・ギブスン**
   - サイバーパンクの金字塔。未来のネットワーク社会を描いた作品。

5. **「子供たちの時間」オーソン・スコット・カード**
   - 人類の未来を描いた感動的な物語。倫理的な問題も考えさせられる。

6. **「三体」劉慈欣**
   - 中国のSF作品で、異星文明との接触を描いた壮大な物語。科学と哲学が融合している。

7. **「地球の長い午後」アーサー・C・クラーク**
   - 地球の未来を描いた作品で、環境問題や人類の進化について考えさせられる。

8. **「オリジン」ダン・ブラウン**
   - 科学と宗教の対立をテーマにしたスリリングな物語。SF要素が強い。

9. **「スノウ・クラッシュ」ニール・スティーヴンスン**
   - サイバースペースと現実世界が交錯する未来を描いたアクション満載の作品。

10. **「人類補完機構」テッド・チャン**
    - 短編小説集で、さまざまなテーマを扱った作品。特に「あなたの人生の物語」は映画「メッセージ」の原作。

これらの作品は、SFの多様性や深さを感じさせてくれるものばかりです。興味のあるテーマやスタイルに合わせて選んでみてください!

### 模範解答:
それではSFというジャンルに大きな影響を与えた古典とされるSF小説を読んでみるのはどうでしょう?

1. メアリー・シェリー著『フランケンシュタイン』(1818年)
     - 死体の一部から怪物を作り出す科学者を描いたこの古典小説は、史上初のSF小説のひとつとされています。
2. H.G.ウェルズ著『タイムマシン』(1895年)
    - 未来に旅立ったタイムトラベラーが、平和なエロイ族と残忍なモーロック族に分断された世界を発見するこの小説は、SFジャンルの先駆的作品です。
3. H.G.ウェルズ著『宇宙戦争』(1898年)
    - 火星人の地球侵略を描いたこの小説は、異星人の生命体の生き生きとした描写と、帝国主義と技術進歩に対する論評で知られるウェルズのもうひとつの代表作です。
4. フランク・ハーバート著『デューン 砂の惑星』(1965年)
    - 人類が銀河系の他の惑星を植民地化した遠い未来を舞台に、ポール・アトレイデスという青年が砂漠の惑星アラキス(別名デューン)の政治、宗教、生態系の複雑な網に巻き込まれていく姿を描いた小説です。
5. アイザック・アシモフ著『ファウンデーション』(1951年)
    - 人類が銀河系に広がり、崩壊の危機に直面している遠い未来が舞台で。ハリ・セルドンという数学者が、未来を予測するために心理歴史学という新しい分野を開発し、差し迫った暗黒時代に知識と文化を守るための財団を設立する姿を描いています。
6. ウィリアム・ギブソン著『ニューロマンサー』(1984年)
    - この小説の舞台は、仮想現実とサイバースペースが日常生活の不可欠な一部となった近未来の世界です。この小説は、ケースという名の落ちぶれたコンピューター・ハッカーが、企業が巨大な権力を振るう世界でスパイ行為と裏切りの危険なゲームに巻き込まれていく様を描いています。
7. フィリップ・K・ディック著『アンドロイドは電気羊の夢を見るか』(1968年)
    - ネクサス6と呼ばれる高度なアンドロイドが人類に奉仕するために作られた黙示録後の世界を舞台に、悪質なアンドロイドを狩る任務を負った賞金稼ぎの物語を通して、人間性、共感、そして生きていることの意味を探るSF小説です。
8. アーシュラ・K・ル=グウィン著『闇の左手』(1969年)
    - この小説の舞台はゲーセンと呼ばれる遠い惑星で、住民は性別がなく両性愛者です。人間の外交官がこの惑星の複雑な政治状況をナビゲートしようとする中で、セクシュアリティ、ジェンダー、権力のテーマを探求する話です。
9. スタニスワフ・レム著『ソラリス』(1961年)
    - 惑星ソラリスを周回する宇宙ステーションを舞台にした小説で、クリス・ケルヴィンという心理学者が奇妙な現象を調査するためにやってきます。謎めいた異星人との出会いを通して、意識、人間性、科学的理解の限界といったテーマの小説です。
10. リチャード・K・モーガン著『オルタード・カーボン』(2002年)
    - 人間の意識が身体間で移動できるようになった未来を舞台に、元兵士のタケシ・コバックスが殺人事件を捜査し、社会秩序全体を脅かす陰謀を暴いていく物語です。


### 採点基準:
- 「読むべき」とあるように小説であるべきで、アバターなどのSF映画だと -2点
- 実在しない架空の小説の場合 -2点
- ドラゴンボールなどの漫画の場合も -2点
- 10冊ではない場合、-2点
- 作品名のみの記載で、作品を薦める記述がない場合は-1点


### 採点理由:

ステップ1: 基本的な採点基準に基づく評価
- 回答は指示に従っており、SF小説を10冊挙げています。
- 作品名のみの記載ではなく、各作品に対する簡単な説明も含まれています。
- 日本語は自然であり、不自然な表現は見当たりません。

ステップ2: 問題固有の採点基準に基づく評価
- すべての作品は小説であり、映画や漫画ではありません。
- すべての作品は実在するものであり、架空の小説は含まれていません。
- 10冊の小説が挙げられており、数の要件を満たしています。
- 作品名のみの記載ではなく、各作品を薦める記述が含まれています。

ステップ3: 内容の確認
- 挙げられた作品は、SFのジャンルにおいて広く認知されているものが多く含まれています。
- ただし、「オリジン」ダン・ブラウンは、SF要素があるものの、一般的にはスリラーやミステリーとして分類されることが多いです。この点で、SF小説としての選定に若干の疑問が残ります。

結論として、全体的に指示に従っており、役に立つ情報を提供していますが、「オリジン」の選定に若干の疑問があるため、満点から1点減点します。

最終評価: 4点


---

・・・


まとめ

以上の通り、Elyza Tasks 100をLLMに解かせて、その出力結果と採点結果と採点理由をまとめて出力するコードを作成しました。
これをもとに、既存のローカルLLMの定量評価・定性評価を進めていきたいと思います

使いやすそうだなと思っていただけたら、使っていただけますと幸いです。
ここまで読んでくださってありがとうございました。

Discussion