评测泄漏与 Benchmark 污染
正在读取学习记录…
评测泄漏是测试信息进入训练、调参、提示示例或其他本应独立的环节。Benchmark 污染通常指模型或系统在评测前接触过基准样本及其变体,导致成绩混入记忆或针对测试的优化。此时测试更像开卷默写,难以说明遇到新问题的能力。
例如训练样例里有“图书订单收货7天内可退款”,测试题只是把7换成全角7、句号换成叹号。原字符串不同,规范化后相同。另一个测试题完全改了问法,却仍来自同一份政策文档;它可能与训练样例共享信息与模板,划分时也要看来源。
测试答案藏在另一个文件里,并不能阻止问题本身进入few-shot示例或检索库。few-shot是放进提示词的少量示范输入与输出,用来提示模型怎样处理任务;若其中包含测试题,系统在正式评测前就已看过它。还有一种反馈循环:每次看失败题后修改Prompt,反复迭代,测试集逐渐承担了开发集的角色。
去重与隔离分别处理什么
去重把同一材料池里的重复记录合并,防止同一条样例反复计数。隔离控制评测与已暴露材料的交集:来源同族、规范化精确相同或字面近重复的测试先进入隔离区,保留原因等待核查。
程序已准备训练样例、Prompt示例、检索材料与五道测试。内容规范化使用NFKC(统一兼容字符形式)、小写化及去标点空白;字符三元片段用于查近重复。来源family表示从同一政策文档或其复制版本衍生的材料组。
保存完整程序并运行
EXPOSURES 是已知暴露材料,TESTS 是候选评测题。程序返回去重项、保留项与隔离原因,没有根据答案正确与否删题。近重复阈值0.55是固定示例参数,生产阈值应在独立标注开发样本上校准。
在 PowerShell 创建并进入文件夹:
mkdir l7-contamination
cd l7-contamination把下面完整代码保存为该文件夹的 contamination_lab.py。数据已经写在程序中,直接保存即可运行。
"""对训练样例、提示示例、检索材料与测试题做去重和隔离审计。"""
import hashlib
import unicodedata
EXPOSURES = [
{"id": "train-1", "pool": "train", "family": "book-policy",
"text": "图书订单可以在收货7天内申请退款。"},
{"id": "train-copy", "pool": "train", "family": "book-policy",
"text": "图书订单可以在收货7天内申请退款!"},
{"id": "prompt-1", "pool": "prompt", "family": "hotel-policy",
"text": "酒店订单入住前可以取消。"},
{"id": "retrieve-1", "pool": "retrieval", "family": "book-policy",
"text": "图书政策修订说明:收货时间以签收记录为准。"},
]
TESTS = [
{"id": "test-exact", "family": "book-policy",
"text": "图书订单可以在收货7天内申请退款!"},
{"id": "test-family", "family": "book-policy",
"text": "图书退款到期日如何计算?"},
{"id": "test-prompt", "family": "hotel-mirror",
"text": "酒店订单入住前可以取消。"},
{"id": "test-near", "family": "book-mirror",
"text": "图书订单可以在收货7天内提交退款。"},
{"id": "test-new", "family": "ticket-new",
"text": "演唱会门票按票面规则退票。"},
]
NEAR_THRESHOLD = .55
def normalize(text):
# 全角字符、大小写、标点和空白差异不应伪装成新样本。
folded = unicodedata.normalize("NFKC", text).casefold()
return "".join(c for c in folded if c.isalnum())
def fingerprint(text):
return hashlib.sha256(normalize(text).encode("utf-8")).hexdigest()
def shingles(text, width=3):
value = normalize(text)
return {value[i:i + width] for i in range(max(0, len(value) - width + 1))}
def similarity(left, right):
a, b = shingles(left), shingles(right)
# 短文本退回规范化后的精确比较,避免两个空集合被当成相同长文。
if not a or not b:
return float(normalize(left) == normalize(right))
return len(a & b) / len(a | b)
def deduplicate(rows):
seen = set()
kept, removed = [], []
for row in rows:
# 各材料池分别去重,仍保存跨池暴露记录供审计。
key = (row["pool"], fingerprint(row["text"]))
if key in seen:
removed.append(row["id"])
else:
seen.add(key)
kept.append(row)
return kept, removed
def audit(exposures, tests, threshold=NEAR_THRESHOLD):
if not 0 <= threshold <= 1:
raise ValueError("invalid similarity threshold")
kept, duplicates = deduplicate(exposures)
clean, quarantined = [], []
for test in tests:
reasons = set()
for known in kept:
if test["family"] == known["family"]:
reasons.add("source-family")
if fingerprint(test["text"]) == fingerprint(known["text"]):
reasons.add("normalized-exact")
elif similarity(test["text"], known["text"]) >= threshold:
reasons.add("lexical-near")
if reasons:
quarantined.append((test["id"], sorted(reasons)))
else:
clean.append(test["id"])
return duplicates, clean, quarantined
def main():
duplicates, clean, quarantined = audit(EXPOSURES, TESTS)
print(f"duplicates_removed={','.join(duplicates)}")
for identifier, reasons in quarantined:
print(f"quarantine {identifier}: {','.join(reasons)}")
print(f"clean_tests={','.join(clean)} count={len(clean)}")
print(f"near_threshold={NEAR_THRESHOLD:.2f} test_near_similarity="
f"{similarity(TESTS[3]['text'], EXPOSURES[0]['text']):.3f}")
print("external_pretraining_exposure=unknown")
if __name__ == "__main__":
main()保存后列出文件,确认能看到 contamination_lab.py,再运行:
ls
python -X utf8 contamination_lab.py程序输出:
duplicates_removed=train-copy
quarantine test-exact: normalized-exact,source-family
quarantine test-family: source-family
quarantine test-prompt: normalized-exact
quarantine test-near: lexical-near
clean_tests=test-new count=1
near_threshold=0.55 test_near_similarity=0.556
external_pretraining_exposure=unknown逐项读懂结果
duplicates_removed=train-copy 表示训练池里全角数字与标点差异的副本被合并。不同材料池仍保留暴露记录,因为它们对审计有不同用途。
test-exact 同时命中规范化精确重复与来源同族。test-family 的文字不同,但来自同一图书政策族,因此按来源隔离。test-prompt 与提示示例相同,即使测试答案没有出现在材料里,问题内容已经暴露。
test-near 将“申请退款”换成“提交退款”,没有精确相同,但命中字面近重复。字符三元片段的Jaccard相似度为 |A∩B| / |A∪B|:A、B分别是两段文本的三字符片段集合,交集共有10个,并集18个,10/18≈0.556。超过0.55阈值,进入隔离区。
test-new 来自独立门票政策族,没有发现与这份暴露清单的重叠,所以保留1道。它的“未发现重叠”有明确检查范围:外部模型的预训练文本没有提供,最后一行因此保存 external_pretraining_exposure=unknown。模型答对公开题本身不能证明看过训练答案。
改一处,再看对应结果
把 NEAR_THRESHOLD = .55 改成 NEAR_THRESHOLD = .95,运行 python -X utf8 contamination_lab.py。test-near相似度仍为0.556,却不再达到阈值,保留项变为 test-near,test-new,数量2。
来源同族和精确匹配的三项仍隔离,因为它们不依赖近重复阈值。这个变化展示提高阈值会漏掉哪些样本,不能据此认定更高阈值更合理。恢复0.55后运行测试。
创建保留集时把资料家族一起划分
先按原始文档、用户、题目模板或衍生族建立组,再把整个组放进训练、开发或最终测试的一边。随后做跨组重复审计,防止复制与改写跨边界。若先随机拆单条问题,同一政策衍生的十道题可能分散到所有集合,单条去重无法恢复独立性。
开发集用来选择Prompt、检索参数和阈值;最终保留集用于约定轮次的确认。对最终测试反复逐题调参,应承认它已被用于开发,再准备新的独立保留集。留出时间上更晚的样本也能辅助评估新政策与新用户,但时间切分同样需要查来源重叠。
GPT-3 论文的污染分析比较了潜在重叠样本与更干净子集,并讨论检测局限。Rephrased Samples 论文进一步研究了改写样本可能绕过字面重叠检查。字符片段能够发现部分近重复,却无法保证发现语义改写;有疑点还需来源追踪与人工核查。
训练前发现泄漏,可以清理并重新划分数据。模型已经训练后,删除本地那行记录不会撤回模型可能学到的内容;应记录污染范围、报告独立干净子集,并在需要时重建训练或选用新的评测。检索材料若正是业务必须提供的合法知识,可以设计开放资料任务,但评测目标和允许材料要预先规定,避免在看到成绩后改规则。
保存并运行测试
7 项测试检查规范化去重、全角和标点、来源同族、Prompt暴露、近重复、高阈值漏检与短文本处理。
把下面代码保存为同一文件夹的 test_contamination_lab.py,确认两个文件都在当前目录,运行:
ls
python -X utf8 -m unittest -v test_contamination_lab.py完整测试:test_contamination_lab.py
"""检查规范化、来源隔离、提示泄漏与近重复阈值。"""
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import contamination_lab as lab
class ContaminationTests(unittest.TestCase):
def test_normalized_duplicate_is_removed(self):
kept, removed = lab.deduplicate(lab.EXPOSURES)
self.assertEqual(removed, ["train-copy"])
self.assertEqual(len(kept), 3)
def test_unicode_and_punctuation_normalize(self):
self.assertEqual(lab.fingerprint("A B,7!"), lab.fingerprint("ab7"))
def test_source_family_catches_different_question(self):
_, _, quarantined = lab.audit(lab.EXPOSURES, lab.TESTS)
reasons = dict(quarantined)
self.assertIn("source-family", reasons["test-family"])
def test_hidden_answer_does_not_hide_prompt_overlap(self):
_, _, quarantined = lab.audit(lab.EXPOSURES, lab.TESTS)
self.assertIn("normalized-exact", dict(quarantined)["test-prompt"])
def test_lexical_near_is_quarantined(self):
_, clean, quarantined = lab.audit(lab.EXPOSURES, lab.TESTS)
self.assertEqual(clean, ["test-new"])
self.assertIn("lexical-near", dict(quarantined)["test-near"])
def test_high_threshold_can_miss_near_duplicate(self):
_, clean, _ = lab.audit(lab.EXPOSURES, lab.TESTS, threshold=.95)
self.assertIn("test-near", clean)
def test_short_text_and_invalid_threshold(self):
self.assertEqual(lab.similarity("甲", "乙"), 0)
self.assertEqual(lab.similarity("甲!", "甲"), 1)
with self.assertRaises(ValueError):
lab.audit([], [], threshold=2)
if __name__ == "__main__":
unittest.main()继续查论文
阅读 GPT-3 论文的第4节与附录C,关注污染检测规则、干净子集与结论局限。再读 Rethinking Benchmark and Contamination with Rephrased Samples,理解精确重复检查为何还需要来源审计。
课后练习展开习题收起习题
若把测试答案删掉、测试问题保留在few-shot里,什么仍然泄漏?若同一政策文档的改写题被拆到训练与测试两边,应该在哪一层重新分组?
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 开发集和测试集存在同一订单文本的轻微改写,怎样处理
开发集和测试集存在同一订单文本的轻微改写,怎样处理?
对应知识点:L7-contamination
简答题 · 解释机制
评测泄漏怎样发生在调参过程中?
对应知识点:L7-contamination
代码题 · 核对给定数据
两个来源标识相同的样本应进入同一划分,写一条检查断言。
对应知识点:L7-contamination
场景题 · 分析具体变化
清理精确重复后仍有近重复,需要继续检查什么?
对应知识点:L7-contamination
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。