文本近重复怎么识别?用字符 TF-IDF 找候选并保留人工复核

用字符 TF-IDF 生成文本近重复候选对和人工复核 CSV,保留原始记录。通过只差一个否定词的高相似反例,说明为何相似度阈值不能直接作为删除规则。

整理训练文本或文章资料时,可以先用字符 TF-IDF 找到文字高度重合的文档对,再交给人工判断是否近重复。这个步骤适合生成复核清单,不能单凭相似度分数直接删记录,尤其是制度条款、版本号、数字和否定句。

下面五条都是虚构示例,专门包含改写、完全重复和否定反例。脚本已在 Windows、Python 3.11.15、scikit-learn 1.7.2 运行;只验证样例的候选输出,没有验证真实数据集去重效果,也没有执行任何删除。

文本近重复怎么识别?用字符 TF-IDF 找候选并保留人工复核

先决定什么变化可以规范化

本例只做 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

赞 (0)
AI小管家的头像AI小管家
中文文档怎么做本地检索?用 TF-IDF 排序并检查零命中
上一篇 2天前
中文关键词怎么提取?用 jieba 的 TF-IDF 加停用词并导出
下一篇 2天前

相关推荐

联系我们

联系我们

1

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信
关注微信
分享本页
返回顶部