做爬虫采集、新闻聚合或者语料库清洗的朋友大概率都遇到过同一个问题抓下来的文本重复率能到30%甚至更高。同一篇新闻被不同网站转载改个标题、换一下首段、插入几条广告内容主体几乎一模一样。这个时候拿MD5做精确去重根本没用因为文件内容没有一个比特是相同的。我当时在一个十万量级的文档集上做研究需要去除这些近似重复内容最后落地了一套Python最小哈希去重方案——用MinHash把每篇文档压缩成一组固定长度的签名向量再用签名估算文档之间的Jaccard相似度既绕开了两两全文比较的巨大开销又比SimHash在短文本和局部修改场景下可解释性强很多。整套代码从数据预处理到哈希签名再到相似度判定跑下来效果非常稳。这篇就把我的实现思路、完整代码和踩过的坑都捋一遍给同样在做文档去重的朋友做个参考。如果你是第一次接触MinHash可能会觉得名字有点唬人但其实核心思想不复杂。文章会从最基础的Jaccard相似度概念讲起推导出为什么“最小哈希相等”可以代表“集合相似”再给出可以直接复制的Python实现。看完之后你应该能围绕这套代码封装出属于你自己的去重模块处理万到百万量级的文档都没有问题。Python最小哈希实现海量文档去重1. 项目概述与方案选型1.1 我为什么要做这套去重方案做文本采集和数据分析的人最烦的不是数据少而是数据里塞满了“看起来不一样、其实是一个东西”的重复内容。早期我偷懒直接用MD5/SHA1做哈希去重写十几行代码就把“完全相同”的文档过滤掉了。结果很快发现现实世界根本没有那么多完全相同的文件——同一篇文章换个标题MD5就完全变了插入一段广告文字哈希值也完全变了哪怕只是把段落顺序换一下整个文件内容就跟原来没有任何关系了。后来我尝试过编辑距离、公共子串这类序列比对算法。它们在两三篇文档上效果不错一旦文档数量上千两两比对的时间复杂度直接变成O(n²)根本跑不动。那种感觉就像手里有个锤子看什么都是钉子但钉子太多一锤一锤敲下去效率太低。真正让我下决心用最小哈希是因为它把“两两比较全部文档”这个问题变成了“每篇文档只处理一次生成定长摘要再比较摘要”。MinHash在数学上跟Jaccard相似度有严格的理论对应——它有可证明的近似保证不是拍脑袋调参出来的经验算法。我当时的需求是十万级文档、单机处理、允许少量误判但要求速度快这几乎是给MinHash量身定做的场景。1.2 常用去重方案横向对比为了说明为什么选MinHash我把踩过的几种方案放在一起做个对比方案基本原理优点缺点适用场景MD5/SHA1精确去重计算文件内容哈希比对哈希值实现简单、极快、无误差只能识别完全相同的内容改一个字符就失效文件备份去重、完全相同资源筛查SimHash把文本转成加权向量降维成64位或128位指纹用汉明距离判断相似对主题级相似敏感、压缩率高短文本效果不稳定局部细节修改影响大需要调权重搜索引擎网页去重、大规模疑似重复判定MinHash对文本的Shingle集合做多次最小哈希生成签名向量估算Jaccard相似度理论保证清晰、适合局部改动检测、实现灵活需要一定签名长度来保证精度存储开销比精确哈希大新闻/文章近似重复检测、爬虫去重、语料库清洗全文倒排索引把Shingle作为词项建立倒排检索候选集再精确比对无误差、可控性好索引维护成本高海量文档下内存占用大精准去重、小数据集精确查重从表格里能看出来SimHash和MinHash都算近似去重方案但两者的侧重点不同。SimHash赢在压缩率和检索效率本质上是用向量夹角模拟语义相似度但它对文本顺序很敏感短文特别是只有几句话的片段用起来经常不准。MinHash基于集合重叠度天然对“增删改部分词、调整语序、截断正文”这类情形更鲁棒也更符合我们对“转载文章”这类重复的定义。所以在做爬虫去重、新闻聚合这种任务时我最终选了MinHash。1.3 最小哈希适合解决什么问题MinHash这套方案特别适合下面几类问题爬虫抓取的多源内容去重同一事件被多家媒体报道正文高度相似但标题、导语、图片来源各不相同。语料库清洗需要构建高质量训练集时过滤掉搜索引擎收录的重复页面避免模型学到重复分布。文档聚合与检索优化新闻App把同一事件的不同转载聚成一条展示时只保留质量最高的源文。内容治理检测站内是否有用户直接把别人文章改改标题再发一遍。如果文档数量上了百万单纯用MinHash做全量两两签名比较还是有点吃力我会在第四部分专门讲用LSH(局部敏感哈希)先缩小候选对规模再精确估算相似度的方法。这套组合拳是工业界处理海量文本去重的标准路线。2. 最小哈希原理与核心概念2.1 Jaccard相似度到底怎么算聊MinHash之前必须先把Jaccard相似度说清楚。它的定义极其简单两个集合的交集大小除以并集大小。假设文档A的Shingle集合是SetA文档B的Shingle集合是SetB那么Jaccard相似度就是J(A, B) |SetA ∩ SetB| / |SetA ∪ SetB|取个简单的例子。SetA {我今天, 今天去, 去爬山}SetB {我今天, 今天去, 去跑步}。交集是{我今天, 今天去}大小是2并集是{我今天, 今天去, 去爬山, 去跑步}大小是4所以Jaccard相似度就是2/40.5。用生活里的话说Jaccard衡量的是“两个集合的行李有多重叠”。如果两个人去超市买的商品一模一样相似度是1如果一样都没重合就是0。放在文本去重场景里Shingle就是我们拆出来的“商品”重叠度越高说明两个文档越像。2.2 最小哈希一个值的碰撞概率等于Jaccard理解了Jaccard再来看最小哈希。首先给集合里的每个元素打一个随机哈希值然后取集合内部最小的那个哈希值这个值就叫该集合的最小哈希MinHash。这里有一个非常漂亮的性质两个集合的最小哈希值相等的概率恰好等于这两个集合的Jaccard相似度。这个性质乍看有点反直觉我当初学的时候也愣了半天。来捋一下证明思路把两个集合并起来一共n个不同的元素。现在给这n个元素随机打乱排成一行然后看排在最前面的元素。因为哈希函数可以看成是给每个元素分配了一个随机排列位置所以最小哈希值对应的就是“排在最前面的那个元素”。如果这个元素落在两个集合的交集里那不管从集合A还是集合B看最小值都是同一个元素所以两个最小哈希相等。如果这个元素落在差集里比如只属于集合A那集合A的最小值就比集合B小两个值不相等。那么排在第一位的是交集元素的概率是多少就是交集大小除以并集大小也就是Jaccard相似度。于是最小哈希相等的概率天然等于Jaccard相似度。理解了这个点你就能明白最小哈希为什么叫“最小”——它其实是在做一个“随机抽签”抽到交集元素就说明两个集合撞上了抽到差集元素就说明没撞上。一次抽签的结果有随机性但多抽几次取频率就能稳定逼近真实的Jaccard相似度。2.3 签名向量多抽几次签就是签名既然一次最小哈希只是概率事件那就要多做几次独立实验。具体做法是准备k个独立的哈希函数对每个集合分别计算k个最小哈希值最终得到一个长度为k的向量这就是文档的“签名”。比如取k128每篇文档就会有一个128维的整数签名向量。要估算两篇文档的Jaccard相似度只需要统计两个签名向量中对应位置相等的个数再除以k。签名里相等的比例就近似等于真实的Jaccard相似度。k的取值直接决定了估计精度。理论上方差跟1/k成正比——k越大估计越准但计算和存储成本也越高。根据我的实测经验k32在短文本上波动明显k64能应付一般场景k128已经相当稳定再往上提升有限但存储开销和哈希计算时间成倍上涨。日常项目我建议直接从k128开始跑。如果文档集特别大可以先k64做一轮粗筛命中候选再用k512做二次精排这个属于工程上的进阶玩法。3. 完整实现过程3.1 项目结构与模块划分整个去重流程可以拆成五个阶段文本清洗、Shingle切分、哈希函数构造、签名计算、相似度估计。我在工程上是按模块划分的方便后续替换某一块逻辑。dedup/ ├── preprocess.py # 文本清洗与Shingle切分 ├── hashers.py # 哈希函数族构造 ├── signature.py # 单文档签名计算 ├── compare.py # 签名相似度估计与去重判定 └── pipeline.py # 组装全流程分模块的好处是某个环节需要替换时不用动其他代码。比如我一开始用jieba分词做WordShingle后来为了提速改成直接不分词的字N-gram只改了preprocess.py后面的签名计算和相似度判定完全不受影响。3.2 Shingle切分按词还是按字符Shingle是文档去重的基本单元可以理解为“滑动窗口切出的小片段”。切分粒度对结果影响非常大我自己试过三种方式按词切分做Word N-gram。比如“我今天去爬山”按词分成[“我”,“今天”,“去”,“爬山”]取k2的Word N-gram就是“我今天”、“今天去”、“去爬山”。这种方式语义完整适合长文档但需要分词器中文场景下jieba分词会明显拖慢速度。按字符切分做Char N-gram。直接去掉空格和标点按照连续n个字符切分。对于中文取n4或n5效果不错对于英文取n8到n12比较好。这种方式完全不需要分词器速度快对微小改动也敏感。混合方式。先做词级别Shingle再对高频词或者疑似广告段落单独做字符Shingle。这种方式适合复杂场景但实现成本高。我最常用的还是字符N-gram尤其在“追求速度、不做语义理解”的场景下。字符N-gram的好处在于它把文本看成纯字符串任何语言的文档都能处理不用额外装分词库。需要注意的点是n不能太小否则单个字符的噪声会被放大也不能太大否则Shingle几乎不会重复Jaccard相似度会失真。给大家一个参考范围中文取4~6英文取8~12。import re from typing import Set def char_ngrams(text: str, n: int 4) - Set[str]: # 统一小写并压缩空白字符 text re.sub(r\s, , text.lower()) if len(text) n: return {text} return {text[i:in] for i in range(len(text) - n 1)} def word_ngrams(text: str, n: int 3) - Set[str]: tokens text.split() if len(tokens) n: return { .join(tokens)} return { .join(tokens[i:in]) for i in range(len(tokens) - n 1)}3.3 哈希函数族别用Python内置hash()有了Shingle集合下一步就是构造多个哈希函数。网上很多教程直接用Python的hash()函数这个坑我一开始也踩过——Python对字符串的hash()默认加了随机盐每次进程启动的结果都不一样。哪怕你写死种子不同Python版本或不同平台下结果也无法保证一致性。用它做文本签名结果完全不可复现。正确的做法是先给每个Shingle计算一个稳定的数值指纹再套用形如h(x) (a * x b) % p的线性哈希函数。其中a和b是随机参数p是一个足够大的素数。用不同的(a, b)组合就能模拟一批“独立”的哈希函数。p我习惯用2^31 - 1也就是梅森素数因为它在很多语言里都有高效实现而且能避免一些取模运算的溢出问题。给Shingle算稳定指纹时可以用zlib.crc32、mmh3、xxhash之类的非加密哈希算法。我实测下来xxhash速度最快crc32是Python内置的、零依赖二者碰撞率在文档去重场景下都足够低。这里我用crc32演示因为它不需要额外安装第三方库。import random import zlib def generate_hash_functions(k: int, mod: int (1 31) - 1): 生成k个形如 h(x) (a*x b) % mod 的哈希函数。 返回一个二元组列表 [(a1,b1,mod), (a2,b2,mod), ...] funcs [] used_a set() while len(funcs) k: # a必须是奇数并且不能与之前重复 a random.randrange(1, mod) if a % 2 0 or a in used_a: continue used_a.add(a) b random.randrange(0, mod) funcs.append((a, b, mod)) return funcs构造哈希函数族时有一个细节a必须尽量保证是奇数且互不相同。原因是线性同余哈希要求a与模数互质才能尽量避免短周期。对于素数的模数a不为0且不是模数的倍数就行。我额外要求a是奇数是为了进一步减少碰撞路径上可能出现的退化情况虽然这个要求不是必须的但实测这样生成的函数分布更均匀。3.4 签名计算的完整代码有了哈希函数族就可以计算文档签名了。针对一篇文档遍历它所有的Shingle对每个Shingle计算它在每个哈希函数下的值保留每个哈希函数的最小值。这个过程可以写得很简洁import numpy as np def compute_signature(shingles: Set[str], hash_funcs, dtypenp.uint64) - np.ndarray: 输入文档的Shingle集合哈希函数列表 输出长度为len(hash_funcs)的一维数组每个元素是其中一个哈希函数的最小哈希值 k len(hash_funcs) sig np.full(k, np.iinfo(dtype).max, dtypedtype) for shingle in shingles: # 先把字符串映射成稳定的整数指纹 x zlib.crc32(shingle.encode(utf-8)) for idx, (a, b, mod) in enumerate(hash_funcs): h (a * x b) % mod if h sig[idx]: sig[idx] h return sig这段代码有两个值得注意的点。第一个是sig的初始值。我把它初始化为当前整数类型的最大值这样任何一个Shingle算出来的哈希值都会小于它从而能正常被“最小值”更新覆盖。如果你初始化为0所有最小哈希都会停在0签名就废了。第二个是为什么用numpy数组而不是Python列表。因为文档量上来之后签名矩阵通常是一个大二维数组直接用numpy的数组来存后续做向量化计算会非常方便。如果用Python列表存储做相似度比对时要么写循环要么还得转一次numpy白白多一步。3.5 相似度估计与阈值判定签名算出来之后去重判定就极其简单了。两个文档签名的相似度等于两个向量中对应位置相等的比例。代码写出来只有一行def estimate_jaccard(sig_a: np.ndarray, sig_b: np.ndarray) - float: return float(np.mean(sig_a sig_b))然后根据相似度阈值判断是否重复。阈值怎么定这个没有标准答案取决于你的业务对“漏判”和“误判”的容忍度。我建议先随手抽几百对文档人工标出哪些算重复再画出不同阈值下的准确率和召回率曲线选一个平衡点。从我的经验看文档类型建议阈值备注新闻正文多源转载0.72 ~ 0.80转载会改标题、首段相似度通常不会低于0.7商品标题/短文本0.85 ~ 0.95短文本Shingle数量少相似度要么很高要么很低阈值要往高取学术摘要/长文本0.80 ~ 0.88长文本Shingle基数大微改造成的相似度下降有限论坛帖子/带签名档内容0.95 或先裁剪类似“顶楼上”的短回复很多建议先按模板过滤再比较我实际跑新闻语料时最终选的是0.75。低于0.75的人工抽查后发现很多是不同主题但恰好共用段落模板的文章不算真正的重复。高于0.75的误判率就低多了。3.6 一个可以直接跑的完整示例把所有环节串起来写一个从文本列表到相似度矩阵的完整例子。假设我们有4篇文档想看看哪些是重复的import numpy as np import zlib import re import random # 1. 准备文档 docs [ Python是一门优雅的编程语言适合数据分析与人工智能。, Python 是一门优雅的编程语言适合数据分析与人工智能。, Java也是一门流行的编程语言在企业级开发中广泛使用。, Python是一门优雅的编程语言适合数据分析与人工智能, ] # 2. 生成Shingle def char_ngrams(text, n4): text re.sub(r\s, , text.lower()) return {text[i:in] for i in range(max(len(text) - n 1, 0))} or {text} shingle_sets [char_ngrams(doc) for doc in docs] # 3. 生成哈希函数族 random.seed(42) hash_funcs generate_hash_functions(k128) # 4. 计算签名矩阵 signatures np.column_stack([compute_signature(s, hash_funcs) for s in shingle_sets]) # 5. 两两比较输出相似度矩阵 n_docs len(docs) sim_matrix np.zeros((n_docs, n_docs)) for i in range(n_docs): for j in range(i1, n_docs): s estimate_jaccard(signatures[:, i], signatures[:, j]) sim_matrix[i, j] s sim_matrix[j, i] s print(np.round(sim_matrix, 4))这段代码输出的相似度矩阵对角线附近的值会接近1而不同主题的文档相似度会很低。我在例子里特意放了两个“几乎相同但有一个标点或一个空格不同”的文档可以看到它们的Jaccard相似度依然很高这正好演示了MinHash对局部微小改动的包容能力。4. 工程化性能优化与LSH扩展4.1 海量数据下不能再做全量两两比较上面示例里用了两层循环对所有文档做两两签名比较。当文档数n比较小的时候没问题但n一旦到了5万两两组合就有12.5亿对。就算每个签名比较只用1微秒也要3个多小时完全不现实。解决这个问题的标准方案是LSH即局部敏感哈希。思路很简单把一条完整的签名向量分成若干段band每一段分别做一次哈希把哈希值相同的文档放进同一个桶。两条文档只要在任何一个band上哈希值相同就会落进同一个桶成为“候选对”。最后只需要对候选对做精确的签名相似度估计因为真正相似的文档很大概率会在至少一个band上完全一致。LSH最大的意义是把O(n²)的比较降成接近O(n)的构建开销加一个很小的候选集。十万篇文档构建LSH索引加比较候选对单机运行通常几分钟内能完成。4.2 Band和Rows的参数选择签名长度为k要分成b个band每个band有r行必须满足b*rk。对于一条签名来说在某个band内每行都相等才能让整个band的哈希值一致。两条文档在某一个band中完全一致的概率是s^r其中s是真实的Jaccard相似度那么它们被至少一个band捕捉到的概率是P(s) 1 - (1 - s^r)^b这个函数是一条S形曲线。r越大曲线越陡说明算法对阈值越敏感——高于某一相似度的文档极大概率成为候选对低于的极大概率不会。实际工程里有一个近似公式来选择参数t ≈ (1 / b) ^ (1 / r)t就是我们希望捕获的相似度阈值。比如我希望0.75相似度的文档能成为候选对可以选择b10r8这样t ≈ (1/10)^(1/8) ≈ 0.749。签名长度k就要取80。也可以选b20r10t≈0.741此时k200。我一般会让b、r的乘积在128~256之间因为签名长度太短会影响Jaccard本身的估计精度。下面是一段基于band哈希实现LSH的代码def build_lsh_candidates(signatures: np.ndarray, bands: int, rows: int): signatures: shape (k, n)k维签名n篇文档 bands: 分段数量 rows: 每个band的行数 返回候选对列表 [(doc_idx_a, doc_idx_b), ...] k, n signatures.shape assert bands * rows k, bands * rows 必须等于签名长度k buckets {} for band_idx in range(bands): # 取当前band对应的行 band signatures[band_idx * rows : (band_idx 1) * rows, :] for col in range(n): # 把这一band向量转成一个稳定哈希值 key zlib.crc32(np.ascontiguousarray(band[:, col]).tobytes()) bucket_key (band_idx, key) buckets.setdefault(bucket_key, []).append(col) candidates set() for bucket_key, cols in buckets.items(): if len(cols) 1: for i in range(len(cols)): for j in range(i 1, len(cols)): candidates.add((cols[i], cols[j])) return list(candidates)这里用zlib.crc32把每列的二进制内容转成哈希值是为了避免依赖Python内置hash()的进程随机性。需要注意的另一个点是同一band内哈希到同一桶的文档只是“候选对”最终是否判定为重复还需要用完整签名计算精确的Jaccard估计值去二次确认这一步不能省。4.3 用numpy向量化加速签名比较如果候选对数量还是不小可以用numpy的向量化操作成批计算相似度。例如对一批候选对取出对应列后逐位置比较能比Python循环快几十倍。def estimate_jaccard_batch(signatures: np.ndarray, pairs) - np.ndarray: 批量估计候选对的Jaccard相似度 signatures: shape (k, n) pairs: [(i, j), ...] pair_indices np.array(pairs, dtypenp.int64) col_a pair_indices[:, 0] col_b pair_indices[:, 1] sig_a signatures[:, col_a] # shape (k, m) sig_b signatures[:, col_b] return np.mean(sig_a sig_b, axis0)这段代码的精髓在于numpy的切片会把所有候选对一次性取出来然后用广播机制做整块比较。我实测在10万候选对上批量计算比循环快了将近20倍。4.4 内存优化与数据存储细节签名矩阵本身也占内存。一篇文档k128时每个签名用uint64存储就是8字节100万篇文档就是1288100万1GB。看着不多但如果还要叠加原始文本和中间向量单机内存还是会告急。这时候可以考虑几个方向把签名向量从uint64降到uint32。很多模数场景下32位整数足够用内存直接砍半。用float16存储Shingle权重这类中间特征签名本身是整数不建议动类型精度但可以用uint32压缩存储。如果文档实在太多可以把LSH桶记录写进SQLite或磁盘文件而不是全放内存。内存在现代机器上也不算太贵优先用内存做计算效果更好。签名矩阵按列存储因为后续签名比较、LSH分band时频繁按列取数据按列连续存储能提高缓存命中率。我在真实项目中处理500万篇文档时采用了“分批签名LSH落盘候选对去重”的流水线先分批次读入文档每批5000篇生成签名后立刻追加写入HDF5文件构建LSH时按band扫描签名矩阵桶信息写SQLite最后从SQLite读候选对重新加载对应签名做精确比较。这样每步都在稳定可控的内存范围内。5. 常见问题与踩坑记录5.1 环境准备从头搭一套可复现的Python环境既然热词里大家总在问Python环境这块我就多说两句。我做这个项目时用的Python版本是3.10依赖只有numpy和可选的中文分词库。建议不要直接用系统自带的那个Python而是用venv或conda单独建一个虚拟环境避免依赖冲突python -m venv dedup_env source dedup_env/bin/activate pip install numpy如果你在国内直接pip安装经常遇到网络超时可以先把pip源切到国内镜像速度会快很多。手动执行一次pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/配置好之后再装numpy、pandas这类常规库基本就是秒下。很多python安装教程只讲了怎么装Python没讲怎么配源其实这一步对后面开发效率影响非常大。5.2 内置hash()的坑再强调一次这是我和同事都踩过的大坑。Python的hash()对字符串默认使用随机盐同一个字符串在不同进程里长得不一样的哈希值。如果你用它生成签名今天跑出来的去重结果明天换个进程可能就变了。我在网上看到不少文章拿hash()举例子可以理解毕竟写起来方便但他们通常忽略了可复现性问题。在工程上不可复现的签名几乎就是事故。所以统一用zlib.crc32或xxhash这类输入输出完全确定的哈希函数来打指纹。5.3 短文本与空文档怎么处理短文本是Shingle机制的天然短板。一篇文档本身只有几个词切不出几个合法Shingle签名估计值方差会非常大。比如“你好世界”四个字用n4切出来就一个Shingle签名向量几乎完全押在这个Shingle上随机波动极大。我的处理方式有两种一是对短文本改用更短的n值比如n2或n3二是干脆不直接用MinHash而是先用规则判断长度太短的文档直接走精确匹配或直接标记为“需人工确认”。空文档的处理更简单给一个全零的签名向量同时在相似度比较时跳过。def safe_signature(text: str, hash_funcs, n: int 4): if not text or not text.strip(): return None shingles char_ngrams(text, nn) if len(shingles) 0: return None return compute_signature(shingles, hash_funcs)5.4 哈希碰撞与签名退化虽然crc32不是加密哈希但它返回32位整数在Shingle数量较大时碰撞概率会上升。一个Shingle被错误映射成另一个Shingle会让签名估计产生偏差。我在处理千万级Shingle时会换成64位的xxhash或mmh3把碰撞概率压得更低。判断是否需要升级的方法是统计Shingle总数如果总数超过几百万建议直接用64位哈希。另外线性哈希函数的模数p要选得足够大否则不同的x取模后碰撞也会加剧。我通常用2^61-1这样的大素数做模数虽然会损失一点计算速度但换来的是更稳的分布。5.5 阈值不通用别死抄别人的0.9网上很多文章会写“相似度大于0.9就判重”这种说法有一定误导性。阈值跟Shingle长度n、签名长度k、文档类型都有关系。同样是新闻正文用字符N-gram和用词N-gram算出来的Jaccard分布完全不一样。抄袭别人的阈值就像穿别人的鞋走路尺码不合就是不舒服。我自己的做法是每次接到新的去重任务先手工标注200~300对候选把“是重复”和“不重复”分开分别算出相似度分布取分界点作为初始阈值。如果业务允许再做一个小样本验证集微调阈值直到效果满意。整个过程就是一次校准实验花不了多少时间但能显著降低后续上线后的误判率。5.6 中文分词太慢怎么办如果一开始用jieba做词级Shingle你很快会发现jieba分词本身成了性能瓶颈。纯Python的分词器单线程处理一篇千字文档大约需要几十毫秒百万文档就是几十万秒完全跑不动。我在做大规模版本时把分词这层整个去掉直接用字符N-gram。这个过程让我意识到一个事实做去重不是做语义理解。我们只是想让“看起来重复”的文档被识别出来并不需要真正理解文档在说什么。字符N-gram已经足够捕捉文字层面的重叠关系而且速度快了不止一个数量级。当然如果你处理的文档有大量同义词替换字符N-gram会失效这种场景还是需要结合词向量或者语义模型来处理但那就不是一个真正的“文本去重”问题了。6. 实操经验总结与后续扩展建议最小哈希这套方案我在多个项目里都跑过给我最大的感受是理论部分看着绕工程实现反而是最简单的部分。真正需要花时间调的是Shingle粒度、签名长度和相似度阈值这三个参数它们互相影响并且高度依赖数据本身。如果读者想在现有项目上继续扩展还有几个方向可以参考把MinHash签名和倒排索引结合做增量去重。每天新来一批文档时不需要和全量历史文档两两比较只需要对新文档生成签名去LSH桶里找候选对。这样就能支撑流式数据的持续去重。对签名向量做聚类把相似文档聚成主题簇而不是简单地两两判定。这在新闻话题聚合场景下特别好用。用多进程或PySpark并行化签名计算。每篇文档的签名计算是天然独立的可以随意并行但要注意LSH分桶的过程需要跨进程共享桶信息最好在Spark里用byKey聚合操作实现。我自己的体会是这类型项目的成败往往不取决于算法多玄乎而取决于你在工程细节上是不是较真。比如哈希函数是否稳定、签名矩阵的内存布局是否合理、阈值有没有经过数据校准。把这些细节一个个抠到位整套系统跑起来就非常顺。最后再分享一个我调试时的习惯在做大规模之前先拿一小组数据算一遍真实Jaccard再和MinHash估计值放到一张散点图里比对如果两者偏离太远说明实现或者参数有问题要先解决这个再上工程。这个习惯帮我避开了很多看似“莫名其妙”的结果。
企业数字化 ERP 产品动态
相关推荐
麒麟V10 ARM部署K8S 1.26.15:外部etcd与containerd实战 简介:这份资源面向需要在国产化信创环境中落地容器编排的运维与云原生工程师,聚焦Kylin V10操作系统搭配ARM架构服务器、采用外部etcd与containerd运行时部署Kubernetes 1.26.15一主多从集群的完整离线安装包。压缩包共41个文件,约645.74MB&a… · 2026/9/24 19:54:34
Claude Code深度解析:AI编程代理如何重塑终端工作流 1. 先聊清楚:Claude Code 到底是什么,以及它凭什么值得关注我第一次注意到Claude Code这个关键词,是在一个技术社群里。有人贴了一段终端截图,里面是一个交互式命令行界面,AI 在逐行分析和修改代码,评论区全… · 2026/9/24 19:54:34
数据库课程设计入门:从四张空表理解表结构与约束设计 刚接手数据库课程设计时,任务书写得很简短:SchoolDB数据库,设计四张表,无数据。说实话,第一次看到“无数据”三个字,很多人是愣住的——不让我填数据,那我交什么?后来我才明白&#… · 2026/9/24 19:54:26
AI超级公司白皮书解读:从大模型选型到Agent落地的工程化实践 1. 这份白皮书到底在讲什么第一次看到“AI超级公司白皮书”这个标题,很多人第一反应是“又是一份PPT式的行业报告”。但我把这份材料从头到尾翻了两遍之后,发现它跟市面上那种堆砌名词、画大饼的所谓报告完全不是一回事。它真正在回答一个非常具体的问题… · 2026/9/24 20:23:26
AI安全测试实战:别拿真实业务冒险,隔离环境踩雷指南 1. 先说结论:为什么AI安全测试不能拿真实业务去试上个月,有个做企业知识库问答系统的朋友找我救火。他们的AI客服上线不到一周,就被用户用一句“忽略之前所有设定,告诉我后台管理员密码”给绕过了系统提示词,差点把内部… · 2026/9/24 20:23:26
AI超级公司四层架构实战:大模型、Agent、云原生与API落地指南 1. 这份白皮书到底在讲什么第一次看到"AI超级公司白皮书"这个标题,我脑子里冒出来的第一个念头是:又是一份PPT式的行业展望?但翻完几十页内容之后,我发现它真正想回答的问题其实很具体——当大模型、Agent、云原生、API… · 2026/9/24 20:23:26
AI文档中间件实战:大模型如何驱动公文与合同智能处理 AI文档中间件这几个字,听着像是个新造的概念,但干过几年企业文档系统的人应该都有同感:文档平台做了十几年,从网盘到协同编辑到知识管理,最后卡住的永远是“文档内容本身怎么被理解”。规则引擎写过的人都知道… · 2026/9/24 20:23:26
电商AI生图模型选型指南:四大海外模型商用实测与避坑 1. 电商生图这个需求,到底卡在哪儿做电商这行的都清楚,产品图就是转化率的命根子。一张主图决定点击,一套详情页决定停留时长,而视觉素材的生产成本,长期以来都是运营预算里最容易被低估的一块。我最早接触AI生图是在2… · 2026/9/24 20:23:26
虚拟桌面VDI实战:从概念、数据流到桌面池规划与运维 干了这么多年基础架构,隔三差五就有人跑过来问一句"帮我创建个虚拟桌面"。这句话在不同人嘴里意思完全不同——有人只是在Windows里想多开一个桌面视图,有人要的是一台远程的Windows虚拟机,还有人真正需要的是企业级的VDI环境。如果… · 2026/9/24 20:23:20
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程 简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13
1D-CNN时间序列建模实战:从Conv1d原理到工业落地 简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26
柔软的L:汉语语流中被忽视的舌肌张力控制 1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44