minimind/dataset/search_shortqa_dataset.py

139 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import json
import os
from typing import Any, Dict, List
from torch.utils.data import Dataset
try:
from datasets import load_dataset
except ImportError:
load_dataset = None
os.environ["TOKENIZERS_PARALLELISM"] = "false"
SEARCH_SHORTQA_SYSTEM = (
"你是一个搜索增强短问答助手。你只能根据给定检索片段回答问题;"
"如果证据不足,回答“不确定”。输出必须是JSON,格式为"
"{\"answer\":\"...\", \"citations\":[\"D1\"]}。answer必须简短,citations只能引用给定片段ID。"
)
def _as_list(value):
if value is None:
return []
if isinstance(value, list):
return value
return [value]
def normalize_contexts(sample: Dict[str, Any]) -> List[Dict[str, str]]:
contexts = (
sample.get("contexts")
or sample.get("documents")
or sample.get("ctxs")
or sample.get("passages")
or sample.get("snippets")
or sample.get("search_results")
or []
)
normalized = []
for i, item in enumerate(contexts):
if isinstance(item, str):
normalized.append({"id": f"D{i + 1}", "title": "", "text": item})
continue
doc_id = str(item.get("id") or item.get("doc_id") or item.get("source") or f"D{i + 1}")
title = str(item.get("title") or item.get("name") or "")
text = str(item.get("text") or item.get("content") or item.get("snippet") or "")
normalized.append({"id": doc_id, "title": title, "text": text})
return normalized
def normalize_answers(sample: Dict[str, Any]) -> List[str]:
answers = sample.get("answers")
if answers is None:
answers = sample.get("answer")
if answers is None:
answers = sample.get("short_answers")
if answers is None:
answers = sample.get("output")
return [str(a) for a in _as_list(answers) if str(a).strip()]
def normalize_gold_doc_ids(sample: Dict[str, Any]) -> List[str]:
doc_ids = sample.get("gold_doc_ids") or sample.get("citations") or sample.get("supporting_doc_ids")
return [str(x) for x in _as_list(doc_ids)]
def select_contexts(sample: Dict[str, Any], max_contexts=6) -> List[Dict[str, str]]:
contexts = normalize_contexts(sample)
if max_contexts <= 0 or len(contexts) <= max_contexts:
return contexts
gold_ids = set(normalize_gold_doc_ids(sample))
gold_contexts = [doc for doc in contexts if doc["id"] in gold_ids]
other_contexts = [doc for doc in contexts if doc["id"] not in gold_ids]
return (gold_contexts + other_contexts)[:max_contexts]
def format_search_prompt(sample: Dict[str, Any], max_contexts=6, max_chars_per_context=700) -> str:
question = str(sample.get("question") or sample.get("query") or sample.get("prompt") or "")
contexts = select_contexts(sample, max_contexts)
lines = ["请基于以下检索片段回答问题。", "", f"问题:{question}", "", "检索片段:"]
for doc in contexts:
title = f"|{doc['title']}" if doc["title"] else ""
text = doc["text"].replace("\n", " ").strip()[:max_chars_per_context]
lines.append(f"[{doc['id']}]{title} {text}")
lines.extend(["", "要求:输出JSON;answer简短;citations只填写真正支撑答案的片段ID。"])
return "\n".join(lines)
def build_search_shortqa_messages(sample: Dict[str, Any], max_contexts=6, max_chars_per_context=700):
return [
{"role": "system", "content": SEARCH_SHORTQA_SYSTEM},
{"role": "user", "content": format_search_prompt(sample, max_contexts, max_chars_per_context)},
]
def build_sft_conversation(sample: Dict[str, Any], max_contexts=6, max_chars_per_context=700):
answers = normalize_answers(sample)
gold_doc_ids = normalize_gold_doc_ids(sample)
answer = answers[0] if answers else "不确定"
assistant = {
"answer": answer,
"citations": gold_doc_ids[:3],
}
return build_search_shortqa_messages(sample, max_contexts, max_chars_per_context) + [
{"role": "assistant", "content": json.dumps(assistant, ensure_ascii=False)}
]
class SearchShortQARLDataset(Dataset):
def __init__(self, jsonl_path, tokenizer, max_length=1024, max_contexts=6, max_chars_per_context=700):
super().__init__()
if load_dataset is None:
raise ImportError("SearchShortQARLDataset requires the 'datasets' package. Install project requirements before training.")
self.tokenizer = tokenizer
self.max_length = max_length
self.max_contexts = max_contexts
self.max_chars_per_context = max_chars_per_context
self.samples = load_dataset("json", data_files=jsonl_path, split="train")
def __len__(self):
return len(self.samples)
def __getitem__(self, index):
sample = dict(self.samples[index])
messages = build_search_shortqa_messages(sample, self.max_contexts, self.max_chars_per_context)
prompt = self.tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
open_thinking=False,
)
return {
"prompt": prompt,
"question": str(sample.get("question") or sample.get("query") or ""),
"answers": normalize_answers(sample),
"gold_doc_ids": normalize_gold_doc_ids(sample),
"contexts": select_contexts(sample, self.max_contexts),
}