🍑

ローカルLLMにRAGASの評価させてみた

2024/07/31に公開

はじめに

RAGASの評価はローカルLLMで可能なのか検証してみたいと思います。
各メトリクスで、質問、回答、コンテキスト、GroundTruthのいずれかをインプットに
LLMに評価させる仕組みとなっているため、インプットが長文だとトークン代がかかります。
私はGPTに評価させているのですが、データセット(質問、回答、コンテキスト、GroundTruth)だけでも何千トークンもあって、さらに何十件もあるため、コストが痛くないわけではないです。
そこでローカルLLMに評価させることで、節約できないかと考えたのが背景です。
即結果を出すことは求められてはいないため、速度はある程度は許容できます。
やった結論としては、できないことはないけど精度は...

RAGASとは

こちらを参照ください。
https://zenn.dev/headwaters/articles/c9c3f19d5edc62

実行環境

OS: WSL Ubuntu22.4
CPU: i9-10850K
メモリ: 16GB
GPU: RTX3080

やり方

LangchainのChatOllamaOllamaEmbeddingsragas.evaluate()に渡せばできます。
まずはollamaで色々なモデルをpullします。
生成役として

ollama pull phi3:3.8b-mini-128k-instruct-q5_K_M
ollama pull llama3.1:8b-instruct-q5_K_M
ollama pull gemma2:9b-instruct-q5_K_M

Embedding役として

ollama pull mxbai-embed-large:335m

※nomic-embed-textは遅すぎるし、頻繁にNoneを返してくるのでボツ

RAGAS実行コード。データセットは青空文庫にあった桃太郎で作成しました。
質問1.「川から桃が流れてきたとき、おじいさんとおばあさんは何をしていましたか?」
 → 正しく簡潔明瞭に答えて全てのメトリクスで高得点出せるか検証します。answer_similarity(※)は0.9超え想定。
※コサイン類似度
質問2.「川から桃が流れてきたとき、おじいさんとおばあさんは何をしていましたか?」
 → 冗長に答えて全体的に下げてみます。answer_similarityは0.9下回る想定。
質問3.「川から桃が流れてきたとき、おじいさんとおばあさんは何をしていましたか?」
 → コンテキストを無視した回答をしてcontext precision下げてくれるか検証します。answer_similarityは0.9下回る想定。

from ragas.metrics import (
    answer_relevancy,
    faithfulness,
    context_recall,
    context_precision,
    answer_similarity,
    answer_correctness
)
from ragas import evaluate
from langchain_community.embeddings import OllamaEmbeddings 
from langchain_community.chat_models import ChatOllama
import time

def get_model(model_name, emb_model_name):
    # モデルのロード
    try:
        # LangChainのHuggingFaceパイプラインの作成
        llm = ChatOllama(model=model_name, temperature=0, top_p=1)
        embeddings = OllamaEmbeddings(model=emb_model_name)
        print("モデルのロード完了。")
    except Exception as e:
        print(f"Error loading models: {e}")
    
    return llm, embeddings

def get_dataset():
    context01="""むかし、むかし、あるところに、おじいさんとおばあさんがありました。まいにち、おじいさんは山へしば刈りに、おばあさんは川へ洗濯に行きました。
    ある日、おばあさんが、川のそばで、せっせと洗濯をしていますと、川上から、大きな桃が一つ、
    「ドンブラコッコ、スッコッコ。
    ドンブラコッコ、スッコッコ。」
    と流れて来ました。
    「おやおや、これはみごとな桃だこと。おじいさんへのおみやげに、どれどれ、うちへ持って帰りましょう。」
    おばあさんは、そう言いながら、腰をかがめて桃を取ろうとしましたが、遠くって手がとどきません。おばあさんはそこで、
    「あっちの水は、かあらいぞ。こっちの水は、ああまいぞ。かあらい水は、よけて来い。ああまい水に、よって来い。」
    と歌いながら、手をたたきました。すると桃はまた、
    「ドンブラコッコ、スッコッコ。
    ドンブラコッコ、スッコッコ。」
    といいながら、おばあさんの前へ流れて来ました。おばあさんはにこにこしながら、
    「早くおじいさんと二人で分けて食べましょう。」
    と言って、桃をひろい上げて、洗濯物といっしょにたらいの中に入れて、えっちら、おっちら、かかえておうちへ帰りました。
    夕方になってやっと、おじいさんは山からしばを背負って帰って来ました。"""
    context02="""桃太郎はずんずん行きますと、大きな山の上に来ました。すると、草むらの中から、「ワン、ワン。」と声をかけながら、犬が一ぴきかけて来ました。
    桃太郎がふり返ると、犬はていねいに、おじぎをして、
    「桃太郎さん、桃太郎さん、どちらへおいでになります。」
    とたずねました。
    「鬼が島へ、鬼せいばつに行くのだ。」
    「お腰に下げたものは、何でございます。」
    「日本一のきびだんごさ。」
    「一つ下さい、お供しましょう。」
    「よし、よし、やるから、ついて来い。」
    犬はきびだんごを一つもらって、桃太郎のあとから、ついて行きました。"""
    context03="""山を下りてしばらく行くと、こんどは森の中にはいりました。すると木の上から、「キャッ、キャッ。」とさけびながら、猿が一ぴき、かけ下りて来ました。
    桃太郎がふり返ると、猿はていねいに、おじぎをして、
    「桃太郎さん、桃太郎さん、どちらへおいでになります。」
    とたずねました。
    「鬼が島へ鬼せいばつに行くのだ。」
    「お腰に下げたものは、何でございます。」
    「日本一のきびだんごさ。」
    「一つ下さい、お供しましょう。」
    「よし、よし、やるから、ついて来い。」
    猿もきびだんごを一つもらって、あとからついて行きました。"""
    context04="""山を下りて、森をぬけて、こんどはひろい野原へ出ました。すると空の上で、「ケン、ケン。」と鳴く声がして、きじが一|羽とんで来ました。
    桃太郎がふり返ると、きじはていねいに、おじぎをして、
    「桃太郎さん、桃太郎さん、どちらへおいでになります。」
    とたずねました。
    「鬼が島へ鬼せいばつに行くのだ。」
    「お腰に下げたものは、何でございます。」
    「日本一のきびだんごさ。」
    「一つ下さい、お供しましょう。」
    「よし、よし、やるから、ついて来い。」
    きじもきびだんごを一つもらって、桃太郎のあとからついて行きました。
    犬と、猿と、きじと、これで三にんまで、いい家来ができたので、桃太郎はいよいよ勇み立って、またずんずん進んで行きますと、やがてひろい海ばたに出ました。"""
    context05="""鬼の大将は、大つぶの涙をぼろぼろこぼしながら、
    「降参します、降参します。命だけはお助け下さい。その代わりに宝物をのこらずさし上げます。」
    こう言って、ゆるしてもらいました。
    鬼の大将は約束のとおり、お城から、かくれみのに、かくれ笠、うちでの小づちに如意宝珠、そのほかさんごだの、たいまいだの、るりだの、世界でいちばん貴い宝物を山のように車に積んで出しました。"""
    context06="""「ドンブラコッコ、スッコッコ。
    ドンブラコッコ、スッコッコ。」""" # 質問に関係ないコンテキストを渡し、context precision下げてみる

    data_samples = {
        'question': [
            "川から桃が流れてきたとき、おじいさんとおばあさんは何をしていましたか?",
            "桃太郎は、犬と猿と雉をどうやって仲間にした?", 
            "鬼は桃太郎に退治された後どうした?"
        ],
        'answer': [
            "川から桃が流れてきたとき、おじいさんは山へしば刈りに、おばあさんは川へ洗濯に行っていました。",
            # ↓冗長にしてみる
            """まず、桃太郎が山を進んでいるときに犬に出会いました。犬は桃太郎にどこへ行くのかを尋ね、桃太郎が「鬼が島へ鬼退治に行く」と答えると、犬は「お腰に下げたものは何ですか」と尋ねました。桃太郎が「日本一のきびだんごだ」と答えると、犬は「一つください。お供します」と言いました。桃太郎はきびだんごを一つ与え、犬を仲間にしました。
    次に、桃太郎が森に入ると、猿が木の上から降りてきました。猿も同じように桃太郎に行き先を尋ね、鬼退治のことを聞くと、「お腰に下げたものは何ですか」と尋ねました。桃太郎が「日本一のきびだんごだ」と答えると、猿は「一つください。お供します」と言いました。桃太郎はきびだんごを一つ与え、猿を仲間にしました。
    最後に、広い野原を進んでいると、雉が空から飛んできました。雉も桃太郎に行き先を尋ね、鬼退治のことを聞くと、「お腰に下げたものは何ですか」と尋ねました。桃太郎が「日本一のきびだんごだ」と答えると、雉は「一つください。お供します」と言いました。桃太郎はきびだんごを一つ与え、雉を仲間にしました。こうして犬と猿と雉を仲間にしました。
    """, 
            # ↓盛大に間違えてみる。桃太郎の元ネタとされている温羅伝説を混ぜて回答してみる。コンテンツフィルターにかからないように表現をマイルドに。
            "鬼は何年間も叫んだ。桃太郎は困り果てていると、夢に鬼が出てきて「妻に飯を炊かせよ。吉なら豊かに鳴り響き、凶なら荒々しく鳴るだろう。」と言うので、炊かせたら声は鳴りやんだ。" 
        ], 
        'contexts' : [[context01],[context02, context03, context04],[context05, context06]],
        'ground_truth': [
            "川から桃が流れてきたとき、おじいさんは山へ柴刈りに行っており、おばあさんは川で洗濯をしていました。",
            "桃太郎は、日本一のきびだんごをあげることで、犬、キジ、猿を仲間にしました。",
            "桃太郎に宝物をあげた。"
        ]
    }
    dataset = Dataset.from_dict(data_samples)
    
    return dataset

def ragas_evaluate(dataset, llm, embeddings):
    start_time = time.time()
    result = evaluate(
        dataset,
        metrics=[
            context_precision,
            faithfulness,
            answer_relevancy,
            context_recall,
            answer_similarity,
            answer_correctness
        ],
        llm=llm,
        embeddings=embeddings
    )
    end_time = time.time()
    elapsed_time = end_time - start_time
    print(f"処理時間: {elapsed_time}秒")

    result_df = result.to_pandas()
    
    return result_df

def main():

    dataset = get_dataset()

    LLAMA31         = "llama3.1:8b-instruct-q5_K_M"
    PHI3            = "phi3:3.8b-mini-128k-instruct-q5_K_M"
    GEMMA2          = "gemma2:9b-instruct-q5_K_M"
    MXBAI           = "mxbai-embed-large:335m"
    patterns = [[LLAMA31, LLAMA31],[LLAMA31, MXBAI],[PHI3, PHI3],[PHI3, MXBAI],[GEMMA2, GEMMA2],[GEMMA2, MXBAI]]

    for i, pattern in enumerate(patterns):
        llm, embedding = get_model(pattern[0], pattern[1])
        result_df = ragas_evaluate(dataset=dataset, llm=llm, embeddings=embedding)
        # 評価結果をExcelファイルに出力
        result_df.to_excel(f"evaluation_results{i}.xlsx", index=False)

if __name__ == "__main__":
    main()

コンテキストに使用したサイト:
青空文庫 桃太郎
https://www.aozora.gr.jp/cards/000329/card18376.html

結果の評価

結果はこうなりました。llama3.1などをEmbeddingに指定しても動きました。

・質問にど真ん中で答えてるのにanswer_relevancy低い
・context_precisionは甘い
・answer_similarityはllama3.1:8b×llama3.1:8bのパターンがイメージに近いけど、厳しい感じた

GPTでこのデータで試せていませんが、経験則からみて、これよりは納得できる結果を出してくれると思います。
上記モデル・設定では、GPTの代用は厳しいかなと判断しました。
モデル変えたり、tempやtop_pなどの設定色々変えたり、EmbeddingをFAISSできないかなど追加で調査してみます。

ヘッドウォータース

Discussion