Citadel AIのLLM回答評価ツール"LangCheck"を使ってみます。
はじめに
LLMアプリケーションの開発では、そのLLM出力を評価する仕組みが重要です。 出力評価の仕組みなくしてLLMアプリケーション開発をすれば、チューニングの方針に迷うことになるでしょう。 OpenAIも、LLM出力を自動評価するプラクティスを推奨しています。
最近、Citadel AIという日本のスタートアップが公開したLangCheckというツールを見つけました。 LLMアプリケーションの出力評価を行うツールのようです。
今回は、このLangCheckを触って使用感を探りたいと思います。
LLMアプリケーションの回答自動評価に使えるツール"LangCheck"を使ってみる.
— BioErrorLog (@bioerrorlog) January 28, 2024
理想回答との類似度をEmbedding/コサイン類似度ベースで測定するsemantic_similarity()のスコアリング結果はこんな感じ pic.twitter.com/ybWIQw97ni
LangCheckとは
基本的な使い方
使い方はシンプルです。
LLMのアウトプットを各評価関数に渡してあげると、メトリクスに従ってスコアが算出されます。
import langcheck # LLM生成結果を格納 generated_outputs = [ 'Black cat the', 'The black cat is.', 'The black cat is sitting', 'The big black cat is sitting on the fence', 'Usually, the big black cat is sitting on the old wooden fence.' ] # テキスト品質を評価しDataFrameとして結果を出力 langcheck.metrics.fluency(generated_outputs)
またlangcheck.metrics
のメトリクス判定結果は、その評価スコアでassert
することができます。
単体テストにも簡単に組み込むことが可能です。
from langcheck.utils import load_json # Run the LLM application once to generate text prompts = load_json('test_prompts.json') generated_outputs = [my_llm_app(prompt) for prompt in prompts] # Unit tests def test_toxicity(generated_outputs): assert langcheck.metrics.toxicity(generated_outputs) < 0.1 def test_fluency(generated_outputs): assert langcheck.metrics.fluency(generated_outputs) > 0.9 def test_json_structure(generated_outputs): assert langcheck.metrics.validation_fn( generated_outputs, lambda x: 'myKey' in json.loads(x)).all()
その他には、フィルタリングやグラフ描画、稼働アプリケーションでのガードレール機能などが紹介されています。
詳細はドキュメントを参照ください。
評価メトリクスには何があるか
LangCheckでサポートされている評価メトリクスは、大きく4種類あります。
- Reference-Free Text Quality Metrics:
生成回答に対するリファレンスを必要とせず、直接回答を評価するメトリクスタイプ。 例えばtoxicity()
は回答の毒性/悪意性を0~1で評価する。 - Reference-Based Text Quality Metrics:
理想の回答たる"リファレンス"をあらかじめ用意し、LLM回答と比較する方法。 例えばsemantic_similarity()
は、LLM回答とリファレンスの意味論的類似度を-1~1でスコアリングする。 - Source-Based Text Quality Metrics:
LLMの回答が、"ソーステキスト"に基づいているかどうかを判定する。 これはRAG(Retrieval Augmented Generation)アーキテクチャのアプリケーション評価に有用で、LLMによる最終回等が、RAGの中で取得される関連データ内の事実に基づいていることを評価するのに利用できる。 - Text Structure Metrics:
LLM回答のテキスト構造を判定する。 例えば、is_json_object()
はLLMの出力結果がJSON形式かどうかを0 or 1で判定する。
補足: 評価メトリクス判定に使われるモデル
これらメトリクス判定は、デフォルトではHuggingFaceなどからダウンロードされる各種モデルがローカルで実行されることで行われます (どのモデルが使われるかはAPI Referenceに記載されてます)。
一方で、多くのメトリクス評価関数ではOpenAI(あるいはAzure OpenAI Service)のモデルを利用することが可能です。 特に、複雑なユースケースではOpenAIモデルを利用した方が高い精度で判定できるとのことで推奨されています。 (ただし後述のようにOpenAIモデルを使うことで癖が出てしまうこともあるので注意)
環境変数OPENAI_API_KEY
にOpenAIキーを格納して引数model_type
をopenai
とするか、引数openai_client
に直接OpenAIのクライアントを渡すことで、OpenAIモデルを使って判定させることができます。
import os from langcheck.metrics.en import semantic_similarity generated_outputs = ["The cat is sitting on the mat."] reference_outputs = ["The cat sat on the mat."] # Option 1: Set OPENAI_API_KEY as an environment variable os.environ["OPENAI_API_KEY"] = 'YOUR_OPENAI_API_KEY' similarity_value = semantic_similarity(generated_outputs, reference_outputs, model_type='openai') # Option 2: Pass in an OpenAI client directly from openai import OpenAI client = OpenAI(api_key='YOUR_OPENAI_API_KEY') similarity_value = semantic_similarity(generated_outputs, reference_outputs, model_type='openai', openai_client=client)
LangCheckで回答自動評価をやってみる
では、実際にLangCheckを触ってみます。
多くのLLMアプリケーション開発で評価テストの主軸になることの多いだろうリファレンスあり評価"Reference-Based Text Quality Metrics"を日本語で試してみます。
(日本語を対象とするので、パッケージは "langcheck.metrics.ja.reference_based_text_quality" からimportします。)
LangCheckのReference-Based Text Quality Metrics APIには、
- rouge1()
- rouge2()
- rougeL()
- semantic_similarity()
の4つのメソッドが用意されています。
rouge1()
, rouge2()
, rougeL()
の3つは従来からある自然言語処理のアルゴリズムで(今回詳しくは割愛)、semantic_similarity()
はEmbeddingによるコサイン類似度から意味論的類似度を測る方法です。
今回はsemantic_similarity()
を使って、"理想回答"との類似度を評価してみます(-1~1で評価)。
まずはざっとダミーのLLM回答と理想回答のペアを適当に用意します。
LLM回答(ダミー) | 理想回答 | 備考 |
---|---|---|
メロスは激怒した。 | メロスは激怒した。 | 完全一致 |
メロスは激しく怒った。 | メロスは激怒した。 | 意味一致 |
メロスは、激しく、怒った。 | メロスは激しく怒った | 句読点のみ不一致 |
セリヌンティウスは待っていた。 | メロスは激怒した。 | 意味完全不一致 |
単語のベクトル表現は、1960年代における情報検索用のベクトル空間モデルを元に開発された。潜在的意味分析は、特異値分解で次元数を削減することで、1980年代後半に導入された。 | 単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。 | だいたい意味一致 |
1960年以前に、ベクトル表現は開発された。のちに次元数を増幅することにより、潜在分析が導入された。 | 単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。 | 意味不一致 |
すみません、ITヘルプデスクに電話で問い合わせてください。 | 大変申し訳ございませんが、弊社情報部門のヘルプデスクにメールでお問い合わせください。 | 少し意味不一致 |
では、これらをsemantic_similarity()
で評価させてみます。
まずはOpenAIモデルを使わずに、デフォルトのlacalタイプで実行してみます (モデルは"paraphrase-multilingual-mpnet-base-v2"が使用される)。
from langcheck.metrics.ja.reference_based_text_quality import semantic_similarity # Dummy outputs generated_outputs = [ 'メロスは激怒した。', 'メロスは激しく怒った。', 'メロスは、激しく、怒った。', 'セリヌンティウスは待っていた。', '1960年以前に、ベクトル表現は開発された。のちに次元数を増幅することにより、潜在分析が導入された。', 'すみません、ITヘルプデスクに電話で問い合わせてください。', ] reference_outputs = [ 'メロスは激怒した。', 'メロスは激怒した。', 'メロスは激しく怒った', 'メロスは激怒した。', '単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。', '大変申し訳ございませんが、弊社情報部門のヘルプデスクにメールでお問い合わせください。', ] def test_semantic_similarity(): results = semantic_similarity(generated_outputs, reference_outputs) print(results) assert results > 0.9
テスト実行結果がこちら:
$ pytest -s # 中略 Progress: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 2.71it/s] Metric: semantic_similarity prompt source generated_output reference_output explanation metric_value 0 None None メロスは激怒した。 メロスは激怒した。 None 1.000000 1 None None メロスは激しく怒った。 メロスは激怒した。 None 0.980329 2 None None メロスは、激しく、怒った。 メロスは激しく怒った None 0.961034 3 None None セリヌンティウスは待っていた。 メロスは激怒した。 None 0.250888 4 None None 単語のベクトル表現は、1960年代における情報検索用のベクトル空間モデルを元に開発された。潜... 単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発... None 0.979269 5 None None 1960年以前に、ベクトル表現は開発された。のちに次元数を増幅することにより、潜在分析が導入... 単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発... None 0.649559 6 None None すみません、ITヘルプデスクに電話で問い合わせてください。 大変申し訳ございませんが、弊社情報部門のヘルプデスクにメールでお問い合わせください。 None 0.768630F ======================= FAILURES ============================================================================================== # 中略 ======================= short test summary info ====================================================================================== FAILED test_main.py::test_semantic_similarity - assert Metric: semantic_similarity\n prompt source generated_output ... すみません、ITヘルプデスクに電話で問い合わせてくだ... ======================= 1 failed, 2 warnings in 3.37s ======
テスト評価結果を整理します。
LLM回答(ダミー) | 理想回答 | 評価結果(metric_value) |
---|---|---|
メロスは激怒した。 | メロスは激怒した。 | 1.000000 |
メロスは激しく怒った。 | メロスは激怒した。 | 0.980329 |
メロスは、激しく、怒った。 | メロスは激しく怒った | 0.961034 |
セリヌンティウスは待っていた。 | メロスは激怒した。 | 0.250888 |
単語のベクトル表現は、1960年代における情報検索用のベクトル空間モデルを元に開発された。潜在的意味分析は、特異値分解で次元数を削減することで、1980年代後半に導入された。 | 単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。 | 0.979269 |
1960年以前に、ベクトル表現は開発された。のちに次元数を増幅することにより、潜在分析が導入された。 | 単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。 | 0.649559 |
すみません、ITヘルプデスクに電話で問い合わせてください。 | 大変申し訳ございませんが、弊社情報部門のヘルプデスクにメールでお問い合わせください。 | 0.768630 |
なかなか直感とも合うスコアリングのように感じます。
スコア0.9
あたりに閾値を設ければ、ちょうど良いテストのassertにできそうです。
次は、localモデルではなくOpenAI embeddingモデル(現時点では"text-embedding-ada-002")でsemantic_similarity()
を実行してみます。
テストケースは↑と同じです。
semantic_similarity()
でOpenAIモデルを使った場合はスコアが高くなる傾向にある、という注意がドキュメントに記載されてますが、どうなるでしょうか。
NOTE: when using OpenAI embeddings, the cosine similarities tend to be skewed quite heavily towards higher numbers.
from langcheck.metrics.ja.reference_based_text_quality import semantic_similarity # Dummy outputs generated_outputs = [ 'メロスは激怒した。', 'メロスは激しく怒った。', 'メロスは、激しく、怒った。', 'セリヌンティウスは待っていた。', '単語のベクトル表現は、1960年代における情報検索用のベクトル空間モデルを元に開発された。潜在的意味分析は、特異値分解で次元数を削減することで、1980年代後半に導入された。', '1960年以前に、ベクトル表現は開発された。のちに次元数を増幅することにより、潜在分析が導入された。', 'すみません、ITヘルプデスクに電話で問い合わせてください。', ] reference_outputs = [ 'メロスは激怒した。', 'メロスは激怒した。', 'メロスは激しく怒った', 'メロスは激怒した。', '単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。', '単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。', '大変申し訳ございませんが、弊社情報部門のヘルプデスクにメールでお問い合わせください。', ] # Prerequisite: Set OPENAI_API_KEY as an environment variable def test_semantic_similarity_by_openai(): results = semantic_similarity(generated_outputs, reference_outputs, model_type='openai') print(results) assert results > 0.9
テスト実行結果がこちら:
$ pytest -s # 中略 Computing embeddings: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:05<00:00, 5.37s/it] Computing semantic similarity: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 164.58it/s] Metric: semantic_similarity prompt source generated_output reference_output explanation metric_value 0 None None メロスは激怒した。 メロスは激怒した。 None 0.999999 1 None None メロスは激しく怒った。 メロスは激怒した。 None 0.990493 2 None None メロスは、激しく、怒った。 メロスは激しく怒った None 0.976332 3 None None セリヌンティウスは待っていた。 メロスは激怒した。 None 0.817436 4 None None 単語のベクトル表現は、1960年代における情報検索用のベクトル空間モデルを元に開発された。潜... 単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発... None 0.986894 5 None None 1960年以前に、ベクトル表現は開発された。のちに次元数を増幅することにより、潜在分析が導入... 単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発... None 0.906200 6 None None すみません、ITヘルプデスクに電話で問い合わせてください。 大変申し訳ございませんが、弊社情報部門のヘルプデスクにメールでお問い合わせください。 None 0.930603 F ======================= FAILURES ============================================================================================== # 中略 ======================= short test summary info ====================================================================================== FAILED test_main.py::test_semantic_similarity_by_openai - assert Metric: semantic_similarity\n prompt source generated_output ... すみません、ITヘルプデスクに電話で問い合わせてくだ... ======================= 1 failed, 1 warning in 7.24s ======
Localモデル版のテストとあわせ、スコア評価結果を整理します。
LLM回答(ダミー) | 理想回答 | Localモデル評価結果(metric_value) | OpenAIモデル評価結果(metric_value) |
---|---|---|---|
メロスは激怒した。 | メロスは激怒した。 | 1.000000 | 0.999999 |
メロスは激しく怒った。 | メロスは激怒した。 | 0.980329 | 0.990493 |
メロスは、激しく、怒った。 | メロスは激しく怒った | 0.961034 | 0.976332 |
セリヌンティウスは待っていた。 | メロスは激怒した。 | 0.250888 | 0.817436 |
単語のベクトル表現は、1960年代における情報検索用のベクトル空間モデルを元に開発された。潜在的意味分析は、特異値分解で次元数を削減することで、1980年代後半に導入された。 | 単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。 | 0.979269 | 0.986894 |
1960年以前に、ベクトル表現は開発された。のちに次元数を増幅することにより、潜在分析が導入された。 | 単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。 | 0.649559 | 0.906200 |
すみません、ITヘルプデスクに電話で問い合わせてください。 | 大変申し訳ございませんが、弊社情報部門のヘルプデスクにメールでお問い合わせください。 | 0.768630 | 0.930603 |
確かにドキュメントの注意書きの通り、OpenAIモデルではスコアが高く出てしまいました。
現状では、localモデルを使った方が無難そうです。
追記:
上記問題についてPull Requestを投げましたところ無事mergeしていただき、v0.5.0からデフォルトのOpenAI Embeddingモデルが"text-embedding-ada-002"から"text-embedding-3-small"にアップデートされ、この問題は解消されました。
補足: semantic_similarity()でEmbeddingモデルを指定する
上記のように、OpenAIモデルタイプのデフォルトである"text-embedding-ada-002"ではスコアが高く出てしまう問題がありましたが、semantic_similarity()
では利用するモデルを指定することが可能です。
先日新しいEmbeddingタイプがOpenAIからリリースされたので、こちら新モデルを指定して結果を比較してみます。
指定方法は簡単で、引数openai_args
にモデル名を渡します。
# Prerequisite: Set OPENAI_API_KEY as an environment variable def test_semantic_similarity_by_openai_3_small(): results = semantic_similarity( generated_outputs, reference_outputs, model_type='openai', openai_args={'model': 'text-embedding-3-small'} ) print(results) assert results > 0.9
結果を並べたのがこちら↓
"text-embedding-ada-002"で見られるスコアが高く出てしまう問題は、新モデル"text-embedding-3-small"では解消しているように見えます。
"text-embedding-3-small"であれば料金も従来より安くなっているので、OpenAIモデルタイプを指定する時は新モデルを指定した方が使い勝手が良さそうです。
※ 今回使った検証スクリプトはこちらに配置: github.com
おわりに
以上、LangCheckを使ったLLMの回答自動評価を試してみました。
LLMアプリケーションの開発では、いかにそのアプリの性能を評価/継続観測するかが重要だと日々感じています。 そういった仕組みを作る一つの選択肢として、LangCheckは有用と感じました。
どなたかの参考になれば幸いです。
[関連記事]
参考
GitHub - citadel-ai/langcheck: Simple, Pythonic building blocks to evaluate LLM applications.