mahiyama/quiz-works
mahiyama/quiz-works クイズワークス (https://quiz-works.com/) のクイズ Q&A を日本語版 Wikipedia パッセージで根拠付けした Open-domain QA データセットに、本リポ内で Hard Negative Mining と Cross-Encoder 蒸留スコア付与を実施した日本語 retrieval / 蒸留学習用データセット。 本リポに供給される (query, positive) ペアは、クイズワークス のクイズ問題文を query、 その短い正答 (例: 「青年海外協力隊」) を 日本語版 Wikipedia 全 passage コレクションへ 引き当てて取得した 根拠 passage 本文 を positive としたもの。短答テキストから passage 本文への引き当て (hydration) は別パイプラインで済んだ状態で入力されており、本リポでは それを起点に Hard Negative Mining と Cross-Encoder… See the full description on the dataset page: https://huggingface.co/datasets/mahiyama/quiz-works.
mahiyama/quiz-works
クイズワークス (https://quiz-works.com/) のクイズ Q&A を日本語版 Wikipedia パッセージで根拠付けした Open-domain QA データセットに、本リポ内で Hard Negative Mining と Cross-Encoder 蒸留スコア付与を実施した日本語 retrieval / 蒸留学習用データセット。
本リポに供給される (query, positive) ペアは、クイズワークス のクイズ問題文を query、 その短い正答 (例: 「青年海外協力隊」) を 日本語版 Wikipedia 全 passage コレクションへ 引き当てて取得した 根拠 passage 本文 を positive としたもの。短答テキストから passage 本文への引き当て (hydration) は別パイプラインで済んだ状態で入力されており、本リポでは それを起点に Hard Negative Mining と Cross-Encoder 蒸留スコア付け、quality_score フィルタを行って pairs / triplets / n-tuples / n-tuples-filtered の 4 形式に 組み替えている。
Configs
すべて split は train。triplets / n-tuples は全行 (フィルタ未適用)、n-tuples-filtered は valid 行のみ。
label カラムの形式 (n-tuples / n-tuples-filtered 共通)
長さ 6 の List[float] で [score(query, positive), score(query, neg1), …, score(query, neg5)]。 label[0] が positive、label[1:] が negative1 〜 negative5 に 1:1 で順序通り対応する。 スコアは Cross-Encoder の生 logit (sigmoid 適用前)。 本データの観測 score の絶対値最大は約 12.59。
上流データの形式について
本リポに入ってくる (query, positive) ペアは「クイズ問題文 × 短答ベタ書き」ではなく、 「クイズ問題文 × その短答を Wikipedia 全 passage コレクションへ引き当てて hydrate した 根拠 passage 本文」になっている。具体例:
短答テキスト (例: 「青年海外協力隊」) 自体は本リポには含まれない。短答が必要なユース ケースでは、原典 (クイズワークス) もしくは passage 引き当ての元になった public な オープン Wikipedia retrieval リソースから query (anchor) キーで join し直して 取得すること。
Hard Negative 選定の二段階フロー
各 (query, positive) 行について、negative1 〜 negative5 を以下の二段階で選出する。 2 段階の役割を分離することで、Stage 1 (dense retriever) で「意味的に近い候補を広く拾い」、 Stage 2 (cross-encoder) で「真に紛らわしい hard negative だけを残す」という構造になっている。
Stage 1: dense retriever による候補集めの粗探索 (top-15)
- cl-nagoya/ruri-v3-310m で query / 全ユニーク positive (15,313 件) を 768 次元にエンコードする (prompt = 「検索クエリ:」 / 「検索文書:」, L2 正規化済み)。
- faiss-cpu の IndexFlatIP (cosine 同等) で query 毎に top-16 を取得。
- top-16 中の gold positive (query の正解 positive 本体) を除外し、 残り 15 件を「negative 候補プール」とする。
- この時点では各候補が真に negative であるかは未確認 (近義 passage や同義文を含む可能性あり)。
Stage 2: cross-encoder による精選 (top-15 → top-5)
- cl-nagoya/ruri-v3-reranker-310m で 各 (query, candidate) の生 logit を計算する (max_length=512, FP16, sigmoid OFF / Identity activation)。
- positive についても同じ cross-encoder で生 logit を計算し pos_score とする。
- 各候補に対し positive-aware filter (NV-Retriever TopK-PercPos) を適用: candidatescore < posscore × 0.95 を満たすものを PASS、そうでないものを NG。
- PASS 候補を score 降順にソートし、上位 5 件を negative1 〜 negative5 として確定。
- PASS が 5 件に満たない場合は不足分を NG 候補から score 降順でフォールバック採用する (NV-Retriever / SFR レシピ準拠の fallback 規則)。
- triplets の negative は同じ手順で得た PASS のうち最高スコア 1 件 (= n-tuples の negative_1 と同一エントリ)。
なお、本データセットは規模が中規模 (16,996 行) のため、Stage 1 出力の top-5 を機械的に 除外する手順 (大規模データで採用される false-positive 緩和) は実施していない。 偽 positive の混入回避は Stage 2 の positive-aware filter に完全に任せる設計。
選定統計
蒸留スコア付けの方法
n-tuples / n-tuples-filtered の label カラムは、教師モデル cl-nagoya/ruri-v3-reranker-310m (Japanese ModernBERT Cross-Encoder) の 生 logit を Cross-Encoder スコアとして 各 (query, doc) ペア (positive 1 件 + negative 5 件) に付与したもの。 Stage 2 で計算した logit をそのまま label に保存している (再推論はしていない)。
スコア付けの設定
KL 蒸留損失 (SparseDistillKLDivLoss / DistillKLDivLoss 等) は softmax(label / T) を target distribution として使うため、 sigmoid 後の [0, 1] 値だと softmax target が一様分布に潰れて ranking 情報が失われる。 これを防ぐため、教師の最終活性化を明示的に Identity に置き換えてスコアを出力した:
import torch.nn as nn
from sentence_transformers import CrossEncoder
teacher = CrossEncoder(
"cl-nagoya/ruri-v3-reranker-310m",
max_length=512,
device="cuda",
trust_remote_code=True,
)
teacher.model.eval()
teacher.model.half() # FP16
teacher.default_activation_function = nn.Identity() # sigmoid を外す
scores = teacher.predict(
[("query A", "passage A"), ("query A", "passage B")],
batch_size=256,
convert_to_numpy=True,
activation_fct=nn.Identity(), # 念のため predict() でも明示
)
# scores: array([10.32, -3.56], dtype=float32)
assert abs(scores).max() > 1.0 # |score|max > 1 で sigmoid 混入チェックlogit のスケール感:
quality_score: n-tuples-filtered の抽出指標
n-tuples-filtered は label から計算した qualityscore の valid 行のみを含む。 qualityscore は学習価値が高い行を以下の 3 軸で定義する:
- positive が信頼できる — 教師が positive を高 logit に置く
- 偽 negative がない — max(neg) が positive を超えない
- negative が硬い — negative の logit がそこそこ高く、margin が中程度
定義 (実装通り)
import numpy as np
POS_MIN = 2.0 # σ(2.0) ≈ 0.88: 確信のある positive のみ採用
MARGIN_MIN = 0.5 # 誤差レベルの分離は雑音と見なす
MARGIN_PENALTY = 0.1 # 易しすぎる行を弱く後回しにする係数
def quality_score_vec(labels: np.ndarray) -> np.ndarray:
"""labels: (N, 6) float32 raw logits [p, n1, n2, n3, n4, n5]. Returns (N,) float, -inf if invalid."""
p = labels[:, 0]
neg = labels[:, 1:]
max_n = neg.max(axis=1)
mean_n = neg.mean(axis=1)
margin = p - max_n
fn_mask = margin <= 0 # 偽 negative リーク
weak_mask = (~fn_mask) & (p < POS_MIN) # positive 自体が弱い
border_mask = (~fn_mask) & (~weak_mask) & (margin < MARGIN_MIN) # 雑音疑い (境界)
valid_mask = (~fn_mask) & (~weak_mask) & (~border_mask)
raw = mean_n - MARGIN_PENALTY * margin
return np.where(valid_mask, raw, -np.inf)各フィルタの意味
ランキング (quality_score 本体)
mean(neg) - 0.1 × margin の 降順。本リポでは n-tuples-filtered は qualityscore の -inf 落ち (validmask 通過しない) 行のみを除外しており、 順序は元の n-tuples を保持 (top-K 抽出は行わない)。 top-K 抽出が必要な場合は下記の selecttopk を使うこと:
def select_top_k(ds, k: int):
labels = np.asarray(ds["label"], dtype=np.float32)
scores = quality_score_vec(labels)
n_valid = int(np.isfinite(scores).sum())
take = min(k, n_valid)
order = np.argsort(-scores, kind="stable")[:take]
return ds.select(order.tolist())フィルタ統計
KL 蒸留損失への組み込み (PyTorch 例)
import torch.nn.functional as F
def kl_distill_loss(student_logits, teacher_label, T=2.0):
"""
student_logits: (B, 6) student の (query, doc_i) スコア (生 logit)
teacher_label: (B, 6) label カラムをそのまま (生 logit)
T: distillation temperature (推奨 1.0〜4.0; 観測レンジ ±13 なので 2.0 前後)
"""
log_p_student = F.log_softmax(student_logits / T, dim=-1)
p_teacher = F.softmax(teacher_label / T, dim=-1)
return F.kl_div(log_p_student, p_teacher, reduction="batchmean") * (T * T)sentence-transformers の SparseDistillKLDivLoss / DistillKLDivLoss を使う場合は、 label カラムをそのまま teacher score として渡せばよい。
データソース
License
CC BY-SA 4.0
- 元クイズ問題文 (query): クイズワークス の二次利用自由ポリシーに基づく。
- Passage 本文 (positive / negative_*): 日本語 Wikipedia に由来し、CC BY-SA 4.0 を継承する。再配布時は Wikipedia の表示・継承条件に従うこと。
- label カラム: 本リポで cl-nagoya/ruri-v3-reranker-310m を用いて算出した派生スコア。
- データセット全体としては、Wikipedia 由来の passage を含むため CC BY-SA 4.0 (ShareAlike) で配布する。
構築パイプライン (再現性)
pairs (16,996) <- 上流で短答を Wikipedia passage に引き当てて hydrate 済みの (query, positive)
├─ unique positive 抽出 → 15,313 件
├─ Stage 1 (dense retriever)
│ ├─ cl-nagoya/ruri-v3-310m で query / positive を埋め込み (L2 正規化)
│ ├─ faiss-cpu IndexFlatIP で top-16 検索
│ └─ gold positive を除外 → 各 query につき 15 件の negative 候補
└─ Stage 2 (cross-encoder)
├─ cl-nagoya/ruri-v3-reranker-310m で (query, doc) ペアの生 logit を計算
└─ NV-Retriever / SFR レシピで上位 5 件を確定
├─ triplets: 1 negative (filter PASS の最高スコア)
├─ n-tuples: 5 negatives + label (filter PASS 優先、不足分はフォールバック)
└─ n-tuples-filtered: n-tuples のうち quality_score valid 行のみAcknowledgements
- 元クイズデータ: クイズワークス (https://quiz-works.com/)
- HNM パイプライン設計: NV-Retriever (Moreira et al.) / SFR-Embedding-Mistral レシピ
- 蒸留教師: cl-nagoya/ruri-v3-reranker-310m
- 埋め込みモデル: cl-nagoya/ruri-v3-310m
