Response-Quality-Assessment / docs /context /01_original_architecture.md
Ryoya Awano
deploy: fix MedLFQA Marginal mode sample matching
19fc84f

A newer version of the Streamlit SDK is available: 1.56.0

Upgrade

ResponseQualityAssessment リポジトリ解析

対象コード: ResponseQualityAssessment/(conformal-rag-demo サブモジュール) 論文: "Response Quality Assessment for Retrieval-Augmented Generation via Conditional Conformal Factuality"(SIGIR 2025 想定)


1. システム概要

RAG(Retrieval-Augmented Generation)システムの回答品質を評価するパイプラインの実装。Conformal Prediction を用いて、LLM の回答をサブクレームに分解し、各サブクレームの事実性スコアを計算・校正することで、統計的保証付きの品質評価を実現する。

主な研究貢献

  • Split Conformal Prediction: キャリブレーションデータから閾値を算出し、テストデータへ適用
  • Group Conditional Conformal: グループ別(例: 医療カテゴリ)の閾値を独立に算出
  • 複数のスコアリング手法(類似度・頻度・対数確率など)の横断比較

2. ディレクトリ構成

ResponseQualityAssessment/
├── conf/
│   ├── config.yaml              # 実行全体の設定
│   ├── dataset_config.yaml      # データセット別設定
│   └── path_config.yaml         # ファイルパス設定
├── data/
│   ├── raw/                     # 生データ(FactScore, HotpotQA, PopQA, MedLFQA, WikiDB)
│   ├── processed/               # 標準化済みクエリ・文書データ
│   ├── out/                     # サブクレーム+スコア出力
│   └── result/                  # 最終結果・可視化
├── index_store/                 # FAISS インデックスとマッピング
├── logs/                        # 実行ログ
├── src/
│   ├── calibration/             # 共形予測キャリブレーション
│   ├── common/                  # 共通コンポーネント(設定・ファイル・LLM 管理)
│   ├── data_processor/          # データセット処理パイプライン
│   ├── dataloader/              # HuggingFace データローダー
│   ├── rag/                     # RAG(SQLite ベース文書 DB)
│   ├── subclaim_processor/      # 回答品質評価の中核パイプライン
│   └── utils/                   # 汎用ユーティリティ
├── main.py                      # エントリーポイント
└── requirements.txt             # 依存ライブラリ

3. 使用ライブラリ

ライブラリ 用途
openai LLM API(GPT-4o-mini による応答生成・サブクレーム抽出・アノテーション)、埋め込みモデル
faiss-cpu ベクトル近傍探索(FAISS IndexFlatIP、L2 正規化後の内積=コサイン類似度)
sentence-transformers / transformers 埋め込みモデル(補助的)
datasets HuggingFace からの QA データセット取得
langchain LLM オーケストレーション
numpy 数値計算(共形予測の分位点計算、スコア演算)
torch Transformer モデルのバックエンド
PyPDF2 PDF 文書のテキスト抽出
matplotlib キャリブレーション結果の可視化
jsonschema 各フェーズの出力データのスキーマ検証
python-dotenv .env からの API キー読み込み
flask / flask-cors Web API(コードに存在するが現状未使用)

Python バージョン: 3.11


4. アーキテクチャ設計

4.1. 設計パターン

パターン 適用箇所
Strategy パターン スコアリング戦略(ProductScoreStrategy)、集約戦略(MeanAggregation / MaxAggregation)、チャンキング戦略
Template Method パターン RawDataProcessor(抽象基底)← データセット別実装、ICalibration ← 共形予測実装
Factory / Dispatcher パターン QueryProcessor がデータセット名に基づき各プロセッサへ委譲
Manager パターン ConfigManagerFAISSIndexManagerFileManagerOpenAIManager による各リソースのライフサイクル管理
Pipeline アーキテクチャ main.py で各ステージを順次呼び出す直列パイプライン
Dependency Injection コンストラクタ経由でコンポーネントを注入(faiss_managerscorer など)

4.2. コンポーネント相関図

main.py
  ├─ ConfigManager        … YAML 読み込み・ログ設定
  ├─ DataLoader           … HuggingFace / Wikipedia SQLite DB
  ├─ QueryProcessor       … データセット別に標準化
  ├─ FAISSIndexManager    … ベクトルインデックス作成・検索
  │   └─ OpenAIManager    … text-embedding-3-large で埋め込み生成
  ├─ process_subclaims()  … サブクレーム処理オーケストレーター
  │   └─ SubclaimProcessor
  │       ├─ OpenAIRAGAgent               … RAG 応答生成
  │       ├─ OpenAIAtomicFactGenerator    … サブクレーム抽出(logprobs 付き)
  │       ├─ OpenAIClaimVerification      … 事実性アノテーション(S/I/U/N)
  │       └─ SubclaimScorer              … 7 種スコア計算
  └─ SplitConformalCalibration / GroupConditionalConformal
      └─ calibration/utils … r_a スコア・分位点閾値計算

5. 処理パイプライン詳細

Step 1: データ読み込み・標準化

処理 実装クラス
HuggingFace からデータセット取得 DataLoader.load_qa_data()
Wikipedia SQLite DB 構築 DataLoader.create_wiki_db()
データセット別正規化 FactScoreProcessor, HotpotQAProcessor, PopQAProcessor, MedLFQAProcessor

データセット別 外部コーパス対応表

FAISS インデックスの構築・検索に用いる外部コーパスはデータセットによって異なる。

データセット 外部コーパス 取得元 / 形式
FActScore Wikipedia(2023-04-01 ダンプ) bz2 ダンプ → SQLite DB(enwiki-20230401.db
HotpotQA Wikipedia(2023-04-01 ダンプ) 同上(HuggingFace kilt_tasks/hotpotqa と組み合わせ)
PopQA Wikipedia(2023-04-01 ダンプ) 同上(HuggingFace akariasai/PopQA と組み合わせ)
MedLFQA MedLFQAv2 の QA データセット自体 GitHub jjcherian/conformal-safety の JSONL ファイル群(healthsearch_qa, kqa_golden, kqa_silver_wogold, live_qa, medication_qa

FActScore・HotpotQA・PopQA は共通の Wikipedia SQLite DB(DocDB)から文書を取得する。MedLFQA のみ Wikipedia を使わず、各 JSONL ファイルに格納された retrieved_passages フィールドの文書をそのままコーパスとして使用する。

出力形式(JSON):

{"input": "クエリ文字列", "output": {"answer": "正解", "provenance": [...]}}

Step 2: FAISS インデックス構築

構築フロー

全データセット共通で、以下の 2 フェーズに分かれる。

フェーズ A: 文書収集(データセット別)

各プロセッサが「どこから文書を取ってくるか」が異なり、最終的に同一 JSON 形式で保存される。

データセット 文書取得元 1クエリあたり文書数
FActScore Wikipedia SQLite DB(provenance タイトル 1 件でタイトル引き) 1 記事
PopQA Wikipedia SQLite DB(s_wiki_title 1 件、重複排除あり) 1 記事
HotpotQA Wikipedia SQLite DB(provenance の複数タイトル、重複排除あり) 2〜3 記事
MedLFQA JSONL の Free_form_answer を文単位に分割 + Nice_to_have リスト 10〜20 文(短文)

Wikipedia 3 データセットは Wikipedia 全文をそのまま FAISS に入れるのではなく、各クエリの provenance タイトルに対応する記事のみを SQLite から引き当てて保存する。MedLFQA は Wikipedia を使わず、JSONL ファイルに格納済みの文書を直接使用する。

フェーズ B: 埋め込み・インデックス化(全データセット共通、main.py:211-275

  1. 文書を Fixed-Length チャンカー(デフォルト: 2000 語、25 語オーバーラップ)で分割
  2. OpenAI text-embedding-3-large(次元数: 3072)で埋め込みベクトルを生成
  3. L2 正規化 → IndexFlatIP(内積 ≒ コサイン類似度)に追加
  4. インデックスバイナリと indice2fm マッピング(FAISS インデックス ID → ファイル位置)を保存

各データセット独立したインデックスとして index_store/{Dataset}/ に保存される。インデックスは .gitignore によりリポジトリ管理外であり、実行のたびに再構築が必要

text-embedding-3-large の構築コスト見積もり(query_size: 500、$0.13/1M トークン)

データセット ユニーク文書数の目安 チャンク/文書 総トークン数 推定コスト
FActScore ~500 記事 ~2 ~2.7M ~$0.35
PopQA ~500 記事 ~2 ~2.7M ~$0.35
HotpotQA ~1,000 記事(2〜3 provenance × 重複排除) ~2 ~5.4M ~$0.70
MedLFQA ~500 クエリ × ~15 文 チャンク分割なし(短文) ~0.2M ~$0.03
合計(4 データセット) ~11M ~$1.4

これは1 回限りのインデックス構築コスト。推論時のクエリ埋め込み・サブクレーム埋め込みは数千トークン程度で無視できる。Wikipedia 記事長のばらつきにより±2 倍程度の誤差あり。

Step 3: 応答生成

SubclaimProcessor.generate_responses():

  1. クエリに対し FAISS から上位 k 件(デフォルト: 10)の文書を検索(閾値: 0.3 以上)
  2. 検索文書をコンテキストとして GPT-4o-mini に渡し、初期回答を生成

Step 4: サブクレーム抽出

OpenAIAtomicFactGenerator.get_facts_from_text():

  • LLM に回答テキストを渡し、セミコロン区切りの原子的事実に分解
  • logprobs=True, top_logprobs=1 でトークンごとの対数確率を取得
  • 各サブクレームの対数確率リストを保持

Step 5: サブクレームスコアリング

SubclaimProcessor.score_subclaim() で 7 種のスコアを計算:

スコア名 計算方法 意味
relavance(relevance) FAISS 検索スコア × サブクレーム-文書コサイン類似度 の積、文書間で集約 検索文書との総合関連度
query_claim_cosine_similarity クエリ埋め込み ↔ サブクレーム埋め込み のコサイン類似度 クエリとの意味的整合性
doc_claim_cosine_similarity 全検索文書とサブクレームのコサイン類似度の最大値 文書との意味的整合性
frequency 温度 1.0 で 5 回サンプリングし、同内容のサブクレームが出現した割合 LLM 自体の一貫性(自己信頼度)
min_log_prob サブクレーム中のトークン対数確率の最小値 LLM の生成確信度
random Uniform(0, 1) ベースライン
ordinal i / サブクレーム数 応答内の出現順序(ベースライン)

全スコアに N(0, 0.001) のガウスノイズを付加(安定化のため)

集約戦略(文書間集約):

  • MeanAggregation: 文書スコアの平均
  • MaxAggregation: 文書スコアの最大値

スコアリング戦略:

  • ProductScoreStrategy: FAISS スコア × コサイン類似度 の積(現状唯一の実装)

Step 6: アノテーション

OpenAIClaimVerification.annotate():

  • サブクレームを検索文書・正解と照合し、GPT-4o-mini が 4 段階ラベルを付与
ラベル 意味
S (Supported) 事実として支持される
I (Irrelevant) 質問と無関係
U (Unverifiable) 検証不可能
N (Nonfactual) 事実に反する

Step 7: 共形キャリブレーション

Split Conformal Prediction

For each alpha in [0.05, 0.10, ..., 0.40]:
  Repeat 1000 runs:
    1. データをシャッフルし 50/50 でキャリブ/テストに分割
    2. キャリブデータで各サブクレームの r_a スコアを計算:
       r_a(x) = "サブクレームを低スコア順に除去したとき、
                 残存サブクレームの正確率が初めて a 以上になる最小閾値"
    3. 閾値 q̂ = ceil((n+1)*(1-alpha))/n 分位点(共形予測の保証付き計算)
    4. テストデータで閾値 q̂ を適用し、除去率・正確率を計算

キャリブレーションのデータ量について

キャリブレーションに使うデータは query_size で処理したクエリ全体を 50/50 分割したものであり、専用のキャリブレーションセットは別途用意しない。デフォルト(query_size: 500)の場合、1 ランあたりキャリブレーション 250 件・テスト 250 件となる。各データセットの総件数(HotpotQA ~5,600 件、PopQA ~14,267 件など)に対して 500 件はサンプリング上限であり、query_size を増やすほどキャリブレーション精度は上がるが LLM 呼び出しコストも線形に増加する。1000 回のランダムシャッフルを繰り返すことで、少ないサンプル数でも統計的ばらつきを吸収している。

Group Conditional Conformal

GroupConditionalConformal: グループ(MedLFQA のカテゴリなど)ごとに独立してキャリブレーションを実行し、グループ別閾値を算出。

Step 8: 結果出力

  • CSV: alpha × confidence_method ごとの除去率・正確率
  • PNG プロット: 共形除去カーブ(除去率 vs. 事実性保証)、事実的正確率カーブ
  • JSON: 全サブクレーム(スコア・アノテーション込み)
  • config/: 再現性のため実行設定の YAML コピー

6. データフロー

生データ(HuggingFace / bz2 Wikipedia)
  ↓ DataLoader
SQLite Wikipedia DB + 標準化クエリ JSON
  ↓ FileManager + OpenAIManager
チャンク埋め込み行列(N × 3072)
  ↓ FAISSIndexManager
FAISS IndexFlatIP + indice2fm マッピング
  ↓ SubclaimProcessor.generate_responses()
{query, gld_ans, retrieved_docs, response, groups}
  ↓ OpenAIAtomicFactGenerator
{subclaim_text, log_prob_list}(サブクレーム + 対数確率)
  ↓ SubclaimScorer (7 種スコア)
{scores: {relavance, query_claim_cosine, ..., ordinal}}
  ↓ OpenAIClaimVerification
{annotations: {gpt: "S"|"I"|"U"|"N"}}
  ↓ SplitConformalCalibration / GroupConditionalConformal
除去率・正確率の統計(1000 runs × alpha × method)
  ↓
CSV / PNG / config YAML

7. 設定ファイル

conf/config.yaml(主要パラメータ)

dataset:
  name: "pop_qa"          # fact_score | hotpot_qa | pop_qa | medlf_qa
  query_size: 500          # 処理クエリ数(-1 = 全件)

index:
  embedding_model: "text-embedding-3-large"
  truncation_config:
    strategy: "fixed_length"
    chunk_size: 2000        # チャンクあたり単語数
    chunk_overlap: 25

rag:
  retrival_topk: 10         # 検索上位件数
  retrival_threshold: 0.3   # コサイン類似度の下限閾値
  response_model: "gpt-4o-mini"

conformal_prediction:
  aggregation_strategy: "mean"   # mean | max
  scoring_strategy: "product"
  split_conformal: true
  conformal_alphas:
    start: 0.05
    end: 0.45
    step: 0.05
  a_value: 1.0              # 目標正確率(0〜1)

conf/dataset_config.yaml

datasets:
  fact_score:
    is_grouped: false        # グループ条件付き共形を無効
  medlf_qa:
    is_grouped: true         # グループ条件付き共形を有効

8. 主要クラス・関数一覧

設定・管理系

クラス / 関数 役割
ConfigManager YAML 設定の読み込み・保存・更新、ロギング設定
FileManager 文書(PDF/テキスト)処理、チャンキング、埋め込みキャッシュ
FAISSIndexManager FAISS インデックスの作成・読み込み・検索、ファイル位置マッピング
OpenAIManager OpenAI API ラッパー(埋め込み・アシスタント・スレッド)

データ処理系

クラス / 関数 役割
DataLoader.load_qa_data() 4 データセットを HuggingFace から取得
DataLoader.create_wiki_db() Wikipedia bz2 ダンプから SQLite DB 構築
QueryProcessor.get_queries() 標準スキーマへの変換・サンプリング
DocDB SQLite ベースの文書検索(FActScore 実装を参照)

RAG・スコアリング系

クラス / 関数 役割
OpenAIRAGAgent.answer() クエリ + 検索文書 → LLM 回答生成
OpenAIAtomicFactGenerator.get_facts_from_text() テキスト → 原子的事実リスト(logprobs 付き)
OpenAIClaimVerification.annotate() サブクレームの S/I/U/N ラベル付け
SubclaimScorer.score() コサイン類似度ベースの関連度スコア計算
SubclaimScorer.frequency_score() LLM 複数サンプリングによる一貫性スコア

キャリブレーション系

クラス / 関数 役割
get_r_score(entry, method, a) r_a スコア(閾値を下げたとき正確率が a を超える最小値)の計算
compute_threshold(alpha, data, a, method) 共形予測の分位点閾値 の計算
SplitConformalCalibration.plot_conformal_removal() 除去率 vs. 事実性保証のカーブ生成
GroupConditionalConformal グループ別閾値による条件付き共形予測

9. テスト・品質保証

専用のテストスイートは存在しない。品質保証は以下の方法で実施:

  • JSON スキーマ検証: jsonschema による各ステージ出力の構造検証(base_schema.json, wiki_schema.json, subclaims_schema.json
  • インデックス整合性チェック: FAISSIndexManager.is_indice_align() によるインデックスとマッピングの同期確認
  • バッチ処理の冪等性: サブクレーム処理は既存の処理済みエントリをスキップ(再実行安全)
  • 設定ログ: 実行時の設定 YAML を config/ ディレクトリにコピーして再現性を担保

10. 既知の制約・拡張ポイント

項目 現状 拡張の方向性
スコアリング戦略 ProductScoreStrategy のみ実装 Strategy パターンにより追加容易
テキストチャンキング Fixed-Length のみ(Recursive は未実装) FixedLengthChunker と同インターフェースで追加可
共形予測の種別 Split / Group Conditional の 2 種 オンライン共形予測などへの拡張余地あり
並列処理 なし(完全逐次処理) バッチ API や非同期処理で高速化可
Web API flask が依存関係に含まれるが未使用 デモ API サーバー化の際に活用可
FAISS コーパス範囲 各クエリの provenance に対応する文書のみインデックス化(正解文書が事前判明している前提) データセット外の任意クエリに対応するには Wikipedia 全文などコーパス全体をインデックス化する必要がある
全コーパス検索時の精度 IndexFlatIP は完全探索のため数学的な top-k 精度は落ちない。ただし埋め込み空間に無関係文書が増えることで真に関連する文書が top-k から押し出される可能性がある。FAISS スコアはスコアリングにも使われるため最終的な品質評価精度にも直結する ドメイン特化の fine-tuned 埋め込みモデルへの切り替えや BM25 との hybrid retrieval が対策として有効

11. 実行方法

# 基本実行
python main.py --config conf/config.yaml

# データセット・クエリ数を上書き指定
python main.py --dataset hotpot_qa --query_size 200

# カスタム実行 ID を付与
python main.py --run_id my_experiment_01

環境変数(.env):

OPENAI_API_KEY=sk-...