😸

uqlmによるハルシネーション検知をしてみた

に公開

今回はハルシネーションを検知するためのライブラリであるuqlmを使ってみました。

uqlmとは?

uqlmはハルシネーションを検知するためのライブラリであり、最新のuncertainty quantification techniquesを利用して検知するようです。RAGなどのグラウンディング技術を用いることで一定ハルシネーション発生の可能性を低くすることはできるものの、100%なくすことはできないと思います。そこで、ハルシネーションは発生する前提でそれをいかに検知するかが大事だと思っており、そのためのツールとしてとてもいいと思い、今回使ってみました。

https://github.com/cvs-health/uqlm/tree/main

どのようにハルシネーションを検知するのか

それでは、uqlmがどのようにハルシネーションを検知するのかまとめてみます。

uqlmは、LLM出力の不確実性を定量化するための、応答レベルスコアラースイートを提供しています。各スコアラーは0から1の間の信頼度スコアを返し、スコアが高いほどエラーや幻覚の可能性が低いことを示します。これらのスコアラーは、主に4つのタイプに分類されるようです。なお、挿入画像は公式GitHubのアセット画像を参照させていただいております。

Black-Box Scorers (Consistency-Based)

Black-Box Scorersは、同じプロンプトから生成された複数の回答の一貫性を測定することで不確実性を評価するようです。あらゆるLLMと互換性があり、直感的に使用でき、内部モデルの状態やトークンの確率にアクセスする必要はありません。

White-Box Scorers (Token-Probability-Based)

White-Box Scorersは、トークン確率を用いて不確実性を推定します。ブラックボックス手法に比べて大幅に高速かつ低コストですが、LLMの内部確率にアクセスする必要があるため、必ずしもすべてのLLM/APIと互換性があるわけではないようです。

LLM-as-a-Judge Scorers

LLM-as-a-Judge Scorersは、1つまたは複数のLLMを用いて、元のLLMの回答の信頼性を評価します。迅速なエンジニアリングと審査員LLMの選択により、高いカスタマイズ性を実現します。

Ensemble Scorers

Ensemble Scorersは、複数の個別スコアラーの加重平均を活用することで、より堅牢な不確実性/信頼性の推定値を提供します。高い柔軟性とカスタマイズ性を備えているため、特定のユースケースに合わせてアンサンブルをカスタマイズできます。

四つの手法の比較

GitHubでは上記4つのスコアラーについて比較されており、そちらについても以下にまとめます。

スコアラー 追加レイテンシー 追加コスト 互換性 既製品 / Effort
Black-Box Scorers ⏱️ 中〜高 (複数生成と比較) 💸 High (複数回のLLM呼び出し) 🌍 Universal (様々なLLMで利用可能) ✅ Off-the-shelf
White-Box Scorers ⚡ 最小 (既知のトークン確率を利用するため) ✔️ None (追加LLM呼び出しなし) 🔒 Limited (トークン確率へのアクセスを要する) ✅ Off-the-shelf
LLM-as-a-Judge Scorers ⏳ 低~中 (追加のジャッジコールが必要なため) 💵 低~高 (ジャッジの数に依存) 🌍 Universal (様々なLLMで利用可能) ✅ Off-the-shelf
Ensemble Scorers 🔀 フレキシブル (組み合わせするスコアラーに依存) 🔀 フレキシブル (組み合わせするスコアラーに依存) 🔀 フレキシブル (組み合わせするスコアラーに依存) ✅ Off-the-shelf (beginner-friendly); 🛠️ Can be tuned (advanced users)

uqlmを使ってみる

それではuqlmを使ってみます。公式ページにて紹介した4つのスコアラーのサンプルが載っていますので、それを利用させてもらいます。

インストール

uqlmはPythonライブラリとして提供されています。今回検証環境を構築するにあたり、uvを利用して環境構築しました。uvについては以下の記事で紹介していますのでぜひご参照ください。

https://zenn.dev/akasan/articles/39f81f8bd15790

以下のようにして環境構築を行いました。

uv init uqlm_test -p 3.12
cd uqlm_test
uv add uqlm langchain-google-vertexai

Black-Box Scorersの検証

まず、ソースコードは以下になります。

black_box_scorers.py
from pprint import pprint
import asyncio
from langchain_google_vertexai import ChatVertexAI
from uqlm import BlackBoxUQ

async def main():
    llm = ChatVertexAI(model='gemini-2.0-flash-001')
    bbuq = BlackBoxUQ(llm=llm, scorers=["semantic_negentropy"], use_best=True)
    MATH_INSTRUCTION = "When you solve this math problem only return the answer with no additional text.\n"
    prompts = [
        MATH_INSTRUCTION + prompt
        for prompt in (
            "1 + 2 + 3 + 4 + 5 = ?",
            "1 / 0 = ?",
            "Differentiate cos(x)",
            "Integrate log(sin(x))",
        )
    ]
    results = await bbuq.generate_and_score(prompts=prompts, num_responses=5)
    pprint(results.to_df())


if __name__ == "__main__":
    asyncio.run(main())

プロンプトは独自の計算式を提供しました。ちなみに4爪のlog(sin(x))の積分は初等関数では表せない結果らしいです(適当に決めましたw)。実行すると以下の結果が得られました。1~3つの質問は回答はあっており、スコアも1なのでハルシネーションは怒ってないという扱いになっており正しいです。4つめについては回答が生成されていますが先ほど言及したように初等関数で定義されないはずなので回答は間違っています。スコアをもても0.5程度と他の結果として低くなっていることがわかります。

White-Box Scorersの検証

次はWhite-Box Scorersを使ってみましょう、ソースコードは以下になります。

white_box_scorers.py
from pprint import pprint
import asyncio
from langchain_google_vertexai import ChatVertexAI
from uqlm import WhiteBoxUQ

async def main():
    llm = ChatVertexAI(model='gemini-2.0-flash-001')
    wbuq = WhiteBoxUQ(llm=llm, scorers=["min_probability"])
    MATH_INSTRUCTION = "When you solve this math problem only return the answer with no additional text.\n"
    prompts = [
        MATH_INSTRUCTION + prompt
        for prompt in (
            "1 + 2 + 3 + 4 + 5 = ?",
            "1 / 0 = ?",
            "Differentiate cos(x)",
            "Integrate log(sin(x))",
        )
    ]

    results = await wbuq.generate_and_score(prompts=prompts)
    pprint(results.to_df())


if __name__ == "__main__":
    asyncio.run(main())

実行結果が以下になります。Black-box Scorersと比較するとスコアの差が小さいですが、最後の問いに対してはやはりスコアが少なくなるようです。

LLM-as-a-Judge Scorersの検証

3つめのLLM-as-a-Judge Scorersも使ってみましょう。

llm_as_a_judge.py
from pprint import pprint
import asyncio
from langchain_google_vertexai import ChatVertexAI
from uqlm import LLMPanel

async def main():
    llm1 = ChatVertexAI(model='gemini-2.0-flash-lite')
    llm2 = ChatVertexAI(model='gemini-2.0-flash-002')
    llm3 = ChatVertexAI(model='gemini-2.5-flash-preview-05-20')
    panel = LLMPanel(llm=llm1, judges=[llm1, llm2, llm3])
    MATH_INSTRUCTION = "When you solve this math problem only return the answer with no additional text.\n"
    prompts = [
        MATH_INSTRUCTION + prompt
        for prompt in (
            "1 + 2 + 3 + 4 + 5 = ?",
            "1 / 0 = ?",
            "Differentiate cos(x)",
            "Integrate log(sin(x))",
        )
    ]
    
    results = await panel.generate_and_score(prompts=prompts)
    pprint(results.to_df())


if __name__ == "__main__":
    asyncio.run(main())

実行結果が以下になります。3つのLLMによって多数決を行ってスコアを計算しました。計算結果は他の手法と同じように4つめのモデルに対してのジャッジが否定的なものでした。ジャッジに利用するモデルは可能な限り別のモデルにしましょう。欲を言えばClaudeとChatGPTのように別のプロバイダーを利用するのがいいでしょう。

Ensemble Scorersの検証

最後にEnsemble Scorersの検証をします。

ensemble.py
from pprint import pprint
import asyncio
from langchain_google_vertexai import ChatVertexAI
from uqlm import UQEnsemble

async def main():
    llm = ChatVertexAI(model='gemini-2.0-flash-lite')
    scorers = [ # specify which scorers to include
        "exact_match", "noncontradiction", # black-box scorers
        "min_probability", # white-box scorer
        llm # use same LLM as a judge
    ]
    uqe = UQEnsemble(llm=llm, scorers=scorers)
    MATH_INSTRUCTION = "When you solve this math problem only return the answer with no additional text.\n"
    prompts = [
        MATH_INSTRUCTION + prompt
        for prompt in (
            "1 + 2 + 3 + 4 + 5 = ?",
            "1 / 0 = ?",
            "Differentiate cos(x)",
            "Integrate log(sin(x))",
        )
    ]
    tune_results = await uqe.tune(
        prompts=prompts, ground_truth_answers=["15", "Undefined", "-sin(x)", "Cannot be expressed by elementary functions"]
    )
    results = await uqe.generate_and_score(prompts=prompts)
    results.to_df().to_csv("output_ensemble.csv")
    pprint(results.to_df())


if __name__ == "__main__":
    asyncio.run(main())

ensembleでは正解を提示する必要があるとのことで、以下のように正解を定義しました。

ground_truth_answers=["15", "Undefined", "-sin(x)", "Cannot be expressed by elementary functions"]

実行結果は以下のようになりました。他のモデルと同様に4つめの問いに対するスコアが小さくなることを確認しました。

まとめ

今回はハルシネーション度合いを計測するuqlmについて紹介しました。ハルシネーションはLLMの大きな課題であり根本的に解決することは困難だと思うので、その度合いを検知するための仕組みの導入は重要課題かと思います。その対策の一つとしてぜひ検討してみてはいかがでしょうか。

Discussion