Python局部敏感哈希实战:论文查重与相似度比对

发布时间:2026/10/11 22:20:54
Python局部敏感哈希实战:论文查重与相似度比对 简介这是一份面向计算机专业学生与毕业设计开发者的Python实战资源围绕局部敏感哈希LSH算法实现论文相似性比对。资源从LSH的(r1,r2,p1,p2)敏感性定义出发讲解相似对象映射为同一哈希值概率高、不相似对象概率低的核心原理并给出可运行的完整代码实现适合需要完成文本查重、相似度检测类课题的读者参考。压缩包共85个文件约340KB其中78个txt为爬取的中文论文语料5个py脚本承担主程序与LSH库逻辑另有1个md说明文档目录结构清晰便于按模块阅读与调试。资源中test.txt由单篇论文拼接其他论文片段构成可直接用于验证比对效果main.py串联起分词、哈希与相似度计算流程lshash第三方库则提供底层支持。目前已有176人学习适合希望快速理解LSH原理并落地论文查重demo的读者。1. 论文查重脚本跑不通先搞懂局部敏感哈希到底在比什么手里攒了上百篇 PDF 论文想快速找出内容高度重合的那几篇用 difflib 逐字比对慢到怀疑人生换词序、改标点就完全失效——这是我第一次做论文相似性比对时踩的坑。后来换成基于 Python 的局部敏感哈希算法才把这件事跑通。它的核心思路不是精确匹配而是把每篇论文切成若干 shingle连续词片段再用多组哈希函数把每个片段映射成签名向量内容越接近的论文签名向量重合度越高。这个资源包解决的正是「批量论文去重与相似度排序」这个具体问题适合正在做毕业设计、需要交一份能跑通代码和实验数据的同学也适合想理解 LSH 工程落地的开发者。它不追求查重率精确到小数点后两位而是用可接受的误差换取数量级的速度提升。2. MinHash 签名矩阵从论文文本到可比对指纹2.1 为什么选 MinHash 而不是直接哈希直接对整篇论文做 MD5只能判断两篇是否完全相同改一个字结果就天差地别。论文相似性比对要的是「差不多也算像」所以需要一种能保留集合相似度的哈希方式。MinHash 的做法是先把文档表示成词片段集合然后用 k 个不同的哈希函数分别作用于集合中每个元素每个哈希函数取最小值组成长度为 k 的签名向量。两个集合的 Jaccard 相似度可以用签名向量对应位置相等的比例来估计。k 越大估计越准但计算量也越大。常见做法是 k 取 100 到 200在毕业设计场景下 128 是个平衡点。这里有个容易混淆的点MinHash 估计的是 Jaccard 相似度不是余弦相似度。如果你拿它去和 TF-IDF 余弦值对比数值对不上是正常的因为两者度量的是不同东西。选型时要先想清楚你要的是「集合重合比例」还是「向量方向接近程度」。论文去重通常更关心前者。2.2 文本预处理与 shingle 切分原始 PDF 抽出来的文本带页眉页脚、参考文献编号、公式乱码直接切 shingle 会引入大量噪声。我一般先做四步清洗去掉非中英文和数字的符号、统一转小写、按标点断句、过滤长度小于 2 的词。然后用滑动窗口切 shingle窗口大小 w 取 3 到 5 个词。w 太小普通短语也会撞车w 太大改几个词就完全匹配不上。下面是我常用的预处理函数import re def clean_text(text): # 只保留中英文和数字其余替换为空格 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) text text.lower() # 合并连续空格 text re.sub(r\s, , text).strip() return text def get_shingles(text, w4): words text.split() if len(words) w: return set() # 滑动窗口生成连续 w 个词的片段 return set( .join(words[i:iw]) for i in range(len(words) - w 1))clean_text里的正则[^\u4e00-\u9fa5a-zA-Z0-9]覆盖了中文区间和英文数字把标点、换行、特殊符号统一替换成空格避免它们影响 shingle 边界。get_shingles用set去重因为同一片段在文中出现多次对集合相似度没有额外贡献。w 默认取 4这是我在中英文混合论文上试出来比较稳的值英文论文可以降到 3中文论文建议升到 5因为中文单字信息量低需要更长窗口才能区分。2.3 构建 MinHash 签名并写入矩阵有了 shingle 集合接下来对每个集合生成签名。这里不自己手写哈希函数直接用datasketch库的MinHash它内部已经用 numpy 做了向量化加速。安装命令是pip install datasketch。核心代码如下from datasketch import MinHash def build_minhash(shingles, num_perm128): m MinHash(num_permnum_perm) for s in shingles: m.update(s.encode(utf-8)) return m # 假设 docs 是 {论文id: 清洗后文本} 的字典 signatures {} for doc_id, text in docs.items(): shingles get_shingles(clean_text(text), w4) signatures[doc_id] build_minhash(shingles, num_perm128)num_perm128就是签名向量长度 k它决定估计精度和内存占用。128 个 32 位整数约 512 字节一万篇论文也就 5MB 左右完全放得下。m.update接收 bytes所以要先encode(utf-8)。如果你要自己实现哈希函数注意别用 Python 内置hash()因为它对字符串有随机盐每次进程启动结果不同会导致签名不可复现——这是新手最容易翻车的地方。3. 相似度查询与批量比对把签名变成可排序的结果3.1 用 LSH 索引加速近邻查询如果只有几十篇论文两两算 Jaccard 也能忍。但上百篇就是上万次比较每次还要遍历签名向量纯 Python 循环会明显变慢。datasketch提供了MinHashLSH它把签名按 band 分组只有落在同一个桶里的候选对才做精确 Jaccard 计算把比较次数降下来。构建索引和查询的代码如下from datasketch import MinHashLSH # threshold 是相似度阈值低于它的对不会被返回 lsh MinHashLSH(threshold0.5, num_perm128) for doc_id, m in signatures.items(): lsh.insert(doc_id, m) def find_similar(doc_id, top_k10): m signatures[doc_id] # query 返回候选集再手动算 Jaccard 排序 candidates lsh.query(m) results [] for cid in candidates: if cid doc_id: continue j m.jaccard(signatures[cid]) results.append((cid, j)) results.sort(keylambda x: x[1], reverseTrue) return results[:top_k]threshold0.5表示只关心相似度超过 0.5 的候选对这个值要根据你的查重严格程度调。毕业设计里如果只是想找出高度重复的论文0.6 到 0.7 更合适如果要做聚类分析可以降到 0.4。lsh.query返回的是候选集不是最终排序结果因为 band 分组会有漏检和误召所以后面还要用m.jaccard精确算一遍再排序。top_k控制返回条数避免结果太多看不过来。3.2 批量比对与结果落盘实际做毕业设计时导师往往要一份完整的相似度矩阵或者 Top-N 列表。我一般会写一个批量处理脚本把所有论文的相似对导出成 CSV方便后续画热力图或写进论文。下面这段代码遍历所有文档用 LSH 查询候选去重后写入文件import csv def batch_compare(signatures, lsh, outputsimilar_pairs.csv, min_jaccard0.5): seen set() rows [] for doc_id in signatures: for cid, j in find_similar(doc_id, top_k20): if j min_jaccard: continue # 无向对去重保证 (a,b) 和 (b,a) 只记一次 pair tuple(sorted([doc_id, cid])) if pair in seen: continue seen.add(pair) rows.append({doc_a: pair[0], doc_b: pair[1], jaccard: round(j, 4)}) rows.sort(keylambda r: r[jaccard], reverseTrue) with open(output, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[doc_a, doc_b, jaccard]) writer.writeheader() writer.writerows(rows) return rowsmin_jaccard是最终输出阈值比 LSH 的 threshold 可以设得更高因为 LSH 阶段宁滥勿缺输出阶段再收紧。tuple(sorted([doc_id, cid]))用来去重无向对否则同一对会出现两次。encodingutf-8-sig是为了 Excel 打开 CSV 不乱码这个细节写论文时很实用。round(j, 4)保留四位小数够用且不会让表格太宽。3.3 参数怎么调一张表说清不同参数对结果和性能的影响差别很大我把常用取值范围整理成表方便你按自己的论文集调整参数含义常用取值调大后果调小后果num_perm签名向量长度64 / 128 / 256精度高内存和计算增加精度下降速度快w (shingle)滑动窗口词数3 / 4 / 5抗改写强但短文本失效易误判普通短语撞车thresholdLSH 召回阈值0.4 / 0.5 / 0.6召回多误报增加漏检增加min_jaccard输出过滤阈值0.5 / 0.6 / 0.7结果少而精结果多而杂我的习惯是先用 num_perm128、w4、threshold0.5 跑一遍看结果分布再决定往哪个方向调。如果发现明显相似的论文没被召回先降 threshold如果结果里一堆不相关的先升 min_jaccard。4. 避坑与排查那些让结果失真的细节4.1 现象同一篇论文两次运行相似度不一样原因用了 Python 内置hash()或者没固定随机种子。内置哈希对字符串有进程级随机盐每次启动结果不同。解决统一用datasketch的 MinHash或者自己实现时用hashlib.md5这类确定性哈希。如果用了 numpy 随机数记得np.random.seed(42)。4.2 现象短论文之间相似度虚高原因shingle 集合太小Jaccard 分母小随便几个共同短语就能把比例拉高。解决对长度低于阈值的文档直接跳过或单独处理比如词数少于 200 的论文不参与比对或者在结果里标注「短文本仅供参考」。4.3 现象PDF 抽取文本全是乱码shingle 全是噪声原因PDF 里嵌入了非标准字体或扫描件pdfminer、PyPDF2抽出来是乱码。解决先用pdfplumber试抽一页看效果扫描件必须走 OCR如pytesseract否则后面所有步骤都是白费。这一步不做后面调参调到天亮也没用。4.4 现象LSH 查询返回空但明明有相似论文原因threshold 设太高或者 num_perm 太小导致 band 分组太粗。解决先把 threshold 降到 0.3 验证流程是否通再逐步升回去。另外检查lsh.insert时用的 MinHash 对象和查询时是不是同一批重新构建过签名就要重新建索引。4.5 现象内存爆掉进程被 kill原因一次性把所有论文的 shingle 集合都留在内存里。解决shingle 集合用完即弃只保留 MinHash 签名或者分批处理每批构建签名后写入磁盘最后统一建索引。一万篇论文的签名也就几 MB真正占内存的是原始文本和 shingle 集合。5. 进阶技巧把相似度结果变成论文里能用的图跑出 CSV 只是第一步毕业设计里通常还要一张相似度热力图或者聚类树。我的做法是用scipy的层次聚类把 Jaccard 距离转成距离矩阵再画树状图。Jaccard 距离就是1 - jaccard注意要保证矩阵对称且对角线为 0。下面这段代码从签名直接算距离矩阵并聚类import numpy as np from scipy.cluster.hierarchy import linkage, dendrogram import matplotlib.pyplot as plt def cluster_docs(signatures, doc_ids): n len(doc_ids) dist np.zeros((n, n)) for i in range(n): for j in range(i1, n): jac signatures[doc_ids[i]].jaccard(signatures[doc_ids[j]]) dist[i][j] dist[j][i] 1 - jac # condensed 距离向量linkage 要求上三角展开 condensed dist[np.triu_indices(n, k1)] Z linkage(condensed, methodaverage) dendrogram(Z, labelsdoc_ids, leaf_rotation90) plt.tight_layout() plt.savefig(dendrogram.png, dpi150) return Znp.triu_indices(n, k1)取上三角索引因为linkage只接受压缩后的距离向量传完整矩阵会报错。methodaverage是平均连接法对论文聚类比较稳single容易产生链状效应complete又太保守。dpi150保证图放进论文里不糊。如果论文数量超过 50树状图标签会挤成一团这时候改用热力图更合适用seaborn.heatmap配mask遮住下三角即可。还有一个实用技巧把 Jaccard 相似度转成「重复率」写进论文时别直接说「相似度 0.73」而是说「按 4-gram shingle 和 128 位 MinHash 估计Jaccard 相似度为 0.73」。把参数写清楚答辩时老师问起来你能答得上也显得实验可复现。我吃过亏第一次答辩只写了个相似度数字被追问「怎么算的、参数多少」时卡住了。从那以后我每次导出结果都强制把 num_perm、w、threshold 三个参数写进 CSV 表头或者实验记录里再也没被问倒过。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询