整理训练文本或文章资料时,可以先用字符 TF-IDF 找到文字高度重合的文档对,再交给人工判断是否近重复。这个步骤适合生成复核清单,不能单凭相似度分数直接删记录,尤其是制度条款、版本号、数字和否定句。
下面五条都是虚构示例,专门包含改写、完全重复和否定反例。脚本已在 Windows、Python 3.11.15、scikit-learn 1.7.2 运行;只验证样例的候选输出,没有验证真实数据集去重效果,也没有执行任何删除。

先决定什么变化可以规范化
本例只做 Unicode NFKC 规范化、英文小写和空白去除。NFKC 能统一部分字符表示;处理后相同只表示“按这套规则文字相同”,并不证明来源、许可证或记录身份可以合并。原文和记录编号始终保留。
不要为了抬高相似度删除数字、型号或“不”“不得”等否定词。a 与 c 只相差一个“不”,业务含义却不同。规范化规则应先在实际任务中确认,原始资料不要覆盖。
创建新环境并安装依赖,把后面的脚本保存为 duplicate_demo.py:
python -m venv .venv
.venv\Scripts\python.exe -m pip install "scikit-learn==1.7.2"
输出候选对,不自动合并
官方 TfidfVectorizer 支持字符 n-gram;本例用长度2至3的连续字符表示文本,再按余弦相似度找候选。循环只遍历 i < j,排除自己和同一文档对的反向重复。0.50 是为了观察反例而设的演示阈值,不能照搬成生产删除标准。
from pathlib import Path
import csv, re, unicodedata
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
rows = [
{"id": "a", "text": "报销单提交后需要直属主管审批,批准后才能付款。"},
{"id": "b", "text": "报销单提交后,需要直属主管审批,批准后才可以付款。"},
{"id": "c", "text": "报销单提交后不需要直属主管审批,批准后才能付款。"},
{"id": "d", "text": "视频配音先上传音频,再选择音色并试听效果。"},
{"id": "e", "text": "报销单提交后需要直属主管审批,批准后才能付款。"},
]
def normalize(text):
return re.sub(r"\s+", "", unicodedata.normalize("NFKC", text).lower())
texts = [normalize(row["text"]) for row in rows]
matrix = TfidfVectorizer(analyzer="char", ngram_range=(2, 3)).fit_transform(texts)
scores = cosine_similarity(matrix)
candidates = []
threshold = 0.50 # 演示值,不能直接用于自动删除
for i in range(len(rows)):
for j in range(i + 1, len(rows)): # 去掉自身和反向重复的文档对
if scores[i, j] < threshold:
continue
candidates.append({
"left_id": rows[i]["id"], "right_id": rows[j]["id"],
"score": round(float(scores[i, j]), 4),
"exact_normalized": texts[i] == texts[j],
"left_text": rows[i]["text"], "right_text": rows[j]["text"],
"review_decision": "pending",
})
for pair in candidates:
print(pair["left_id"], pair["right_id"], pair["score"], pair["exact_normalized"])
assert any(x["left_id"] == "a" and x["right_id"] == "c" for x in candidates)
assert any(x["left_id"] == "a" and x["right_id"] == "e"
and x["exact_normalized"] for x in candidates)
with Path("duplicate_candidates.csv").open("w", newline="", encoding="utf-8-sig") as f:
fields = ["left_id", "right_id", "score", "exact_normalized",
"left_text", "right_text", "review_decision"]
writer = csv.DictWriter(f, fieldnames=fields)
writer.writeheader()
writer.writerows(candidates)
print("候选数:", len(candidates), "原始记录仍为:", len(rows))
运行:
.venv\Scripts\python.exe duplicate_demo.py
看反例,确认高分没有替你作决定
固定样例的实际输出:
a b 0.5907 False
a c 0.7967 False
a e 1.0 True
b c 0.5394 False
b e 0.5907 False
c e 0.7967 False
候选数: 6 原始记录仍为: 5
a/e 规范化后相同,分数为1;a/b 是措辞接近的候选。需要特别看 a/c:分数约0.7967,c 却明确增加了“不需要”。这个反例说明:高分能提示文字重合,无法确认两条规则的业务含义相同。
打开 duplicate_candidates.csv,逐行并排看 left_text 和 right_text。review_decision 初始均为 pending,建议人工改为 merge、keep_both 或 needs_context,同时在自己的复核表中补充理由和保留记录编号。脚本只写候选 CSV,原始记录仍为5条。
还要核对没有任何 left_id 等于 right_id、同一对只出现一次、a/e 被标为 exact_normalized=True,以及 a/c 确实进入清单。断言失败时先检查示例是否被改过,不要继续批量处理。
把演示清单改成真正可用的去重流程
- 先分组再比较:按同一产品、语言、日期范围等有业务依据的字段缩小候选,但不要把跨版本差异悄悄丢掉。
- 用一小批人工已判定的“重复/非重复”文档对试阈值;记录漏掉哪些重复、误报了哪些不同内容。阈值变化后重跑这批样本。
- 决定合并时保留旧编号到新编号的映射、来源和审核理由。文字相同但许可、发布状态或来源不同的记录可能仍需各自保留。
- 近重复候选可帮助检查训练与验证之间的文本泄漏,但分数本身不能证明切分已经安全,还需结合用户、时间和文档来源。
本例一次生成全部文档对的相似度矩阵,文档数增加时内存按数量的平方增长;它只适合小样本演示。大库应采用分块计算或候选索引,并继续保留人工确认,不要对海量文本直接运行这个全矩阵版本。
两对都相似,能直接把三篇归为一组吗?
不能仅靠传递关系合并:a 与 b 接近、b 与 c 接近,不保证 a 与 c 是同一条业务信息。复核时应检查整组原文、版本和限制条件,不能只看候选边连在一起。
来源与接口依据
官方资料核对日期:2026年10月1日。本文用到的接口见:字符 TF-IDF 参数;余弦相似度接口;Python Unicode 规范化说明。
Ai菜鸟网。发布者:AI小管家,转载请注明出处:https://www.alyyhw.com/30794.html