Python最小哈希實現(xiàn)海量文檔去重的具體方案
對百萬級以上的數(shù)據(jù)做相似文檔去重時,往往會面臨性能瓶頸。最小哈希(MinHash)結合局部敏感哈希(LSH)為我們提供了高效的解決方案。
一、 Jaccard相似度:相似性度量的基礎
Jaccard相似度衡量兩個集合的相似程度:
J(A,B) = |A ∩ B| / |A ∪ B|
在文檔去重中,我們首先將文檔轉換為特征集合。常用方法包括:
1. 詞級別的Shingles
對于中文文檔,通常先分詞,然后生成k個連續(xù)詞的片段:
# 示例:文檔"我在學習編程"
分詞后:["我", "在", "學習", "編程"]
3-shingles:{"我 在 學習", "在 學習 編程"}
代碼實現(xiàn):
import jieba
def generate_word_shingles(text, k=3):
"""
生成詞級別的shingles
"""
# 分詞
words = list(jieba.cut(text))
# 生成k個連續(xù)詞的片段
shingles = set()
for i in range(len(words) - k + 1):
shingle = ' '.join(words[i:i + k])
shingles.add(shingle)
return shingles
# 示例
text = "我在學習編程"
shingles = generate_word_shingles(text, k=3)
print("3-shingles:", shingles)
# 輸出: {'我 在 學習', '在 學習 編程'}
2. 字符級別的n-grams
text1的3-grams集合: {"我在學", "在學習", "學習編", "習編程"}
text2的3-grams集合: {"我現(xiàn)在", "現(xiàn)在學", "在學習", "學習編", "習編程"}
交集 = {"在學習", "學習編", "習編程"} = 3個
并集 = {"我在學", "在學習", "學習編", "習編程", "我現(xiàn)在", "現(xiàn)在學"} = 6個
Jaccard相似度 = 3/6 = 0.5
代碼實現(xiàn):
def generate_char_ngrams(text, n=3):
"""
生成字符級別的n-grams
參數(shù):
text: 輸入文本
n: n-gram的大小
返回:
set: n-grams集合
"""
ngrams = set()
for i in range(len(text) - n + 1):
ngram = text[i:i + n]
ngrams.add(ngram)
return ngrams
# 示例
text1 = "我在學習編程"
text2 = "我現(xiàn)在學習編程"
ngrams1 = generate_char_ngrams(text1, n=3)
ngrams2 = generate_char_ngrams(text2, n=3)
print("text1的3-grams集合:", ngrams1)
print("text2的3-grams集合:", ngrams2)
# 計算Jaccard相似度
intersection = ngrams1.intersection(ngrams2)
union = ngrams1.union(ngrams2)
jaccard_sim = len(intersection) / len(union)
print("交集:", intersection)
print("并集:", union)
print("Jaccard相似度:", jaccard_sim)
二、 最小哈希:從集合相似度到簽名相似度
1. 數(shù)學原理
對于一次隨機排列π:
P[min(π(A)) = min(π(B))] = J(A,B)
其中min(π(S))表示集合S在排列π中的最小元素
2. 最小哈希過程
我們使用哈希函數(shù)模擬隨機排列。以下示例展示3個不同哈希函數(shù)的工作過程:
文檔A的shingles: {"我在學", "在學習", "學習編", "習編程"}
文檔B的shingles: {"我現(xiàn)在", "現(xiàn)在學", "在學習", "學習編", "習編程"}
哈希函數(shù)h?:
h?("我在學") = 150
h?("在學習") = 80 ← 兩個文檔的最小值!
h?("學習編") = 200
h?("習編程") = 180
h?("我現(xiàn)在") = 250
h?("現(xiàn)在學") = 300
文檔A的min-hash? = min(150,80,200,180) = 80
文檔B的min-hash? = min(250,300,80,200,180) = 80
結果:相等
哈希函數(shù)h?:
h?("我在學") = 30 ← 文檔A的最小值!
h?("在學習") = 150
h?("學習編") = 200
h?("習編程") = 180
h?("我現(xiàn)在") = 25 ← 文檔B的最小值!
h?("現(xiàn)在學") = 300
文檔A的min-hash? = 30
文檔B的min-hash? = 25
結果:不相等
哈希函數(shù)h?:
h?("我在學") = 400
h?("在學習") = 250
h?("學習編") = 50 ← 兩個文檔的最小值!
h?("習編程") = 150
h?("我現(xiàn)在") = 300
h?("現(xiàn)在學") = 350
文檔A的min-hash? = 50
文檔B的min-hash? = 50
結果:相等
代碼實現(xiàn)(注:與示例數(shù)據(jù)不同):
import hashlib
def hash_shingle(shingle, hash_func_num):
"""
對shingle進行哈希
"""
# 使用不同的哈希函數(shù)模擬隨機排列
# 使用不同的鹽值來模擬不同的哈希函數(shù)
salt = f"salt_{hash_func_num}"
data = (shingle + salt).encode('utf-8')
# 使用MD5哈希并轉換為整數(shù)
hash_hex = hashlib.md5(data).hexdigest()
hash_int = int(hash_hex, 16) % (10 ** 6) # 取模得到合適大小的整數(shù)
return hash_int
def calculate_minhash(shingles_set, num_hash_funcs=3):
"""
計算文檔的最小哈希簽名
"""
# 初始化最小哈希值為無窮大
minhash_signature = [float('inf')] * num_hash_funcs
for shingle in shingles_set:
for i in range(num_hash_funcs):
hash_value = hash_shingle(shingle, i)
if hash_value < minhash_signature[i]:
minhash_signature[i] = hash_value
return minhash_signature
# 文檔A和B的shingles(使用字符級別的3-grams)
docA_shingles = {"我在學", "在學習", "學習編", "習編程"}
docB_shingles = {"我現(xiàn)在", "現(xiàn)在學", "在學習", "學習編", "習編程"}
# 計算最小哈希簽名
minhash_A = calculate_minhash(docA_shingles, num_hash_funcs=300)
minhash_B = calculate_minhash(docB_shingles, num_hash_funcs=300)
print("文檔A的300維哈希簽名:", minhash_A)
print("文檔B的300維哈希簽名:", minhash_B)
# 計算簽名相似度
def signature_similarity(sigA, sigB):
"""計算兩個簽名的Jaccard相似度估計值"""
matches = sum(1 for a, b in zip(sigA, sigB) if a == b)
return matches / len(sigA)
similarity = signature_similarity(minhash_A, minhash_B)
print("簽名相似度估計值:", similarity)
3. N維哈希簽名
N維哈希簽名包含N個哈希函數(shù)對應的N個最小值:
文檔A的3維哈希簽名: [80, 30, 50] 文檔B的3維哈希簽名: [80, 25, 50]
簽名的相似度計算
def signature_similarity(sigA, sigB):
"""計算兩個簽名的Jaccard相似度估計值"""
matches = sum(1 for a, b in zip(sigA, sigB) if a == b)
return matches / len(sigA)
# 示例
similarity = signature_similarity([80, 30, 50], [80, 25, 50])
# 2/3 ≈ 0.667(接近真實值0.5)
三、 為什么需要多個哈希函數(shù)?
1. 統(tǒng)計精度原理
根據(jù)大數(shù)定律,當哈希函數(shù)足夠多時,簽名相似度會收斂到真實Jaccard相似度
2. 不同哈希數(shù)量下的精度
通過計算簽名相似度估計值的置信區(qū)間,展示隨著哈希函數(shù)數(shù)量的增加,最小哈希方法對真實Jaccard相似度的估計精度如何提升。
置信區(qū)間表示在一定的置信水平下(此處為95%),真實相似度落在一個區(qū)間內(nèi)的概率。
- 區(qū)間越窄,表示估計越精確。
- 區(qū)間越寬,表示估計的不確定性越高。
import math
def confidence_interval(n, s=0.5):
"""計算95%置信區(qū)間"""
std = math.sqrt(s * (1 - s) / n)
margin = 1.96 * std
return [max(0, s - margin), min(1, s + margin)]
print(confidence_interval(1, s=0.5))
print(confidence_interval(10, s=0.5))
print(confidence_interval(100, s=0.5))
print(confidence_interval(500, s=0.5))
print(confidence_interval(1000, s=0.5))
[0, 1] [0.19009678930349883, 0.8099032106965012] [0.402, 0.598] [0.4561730676410041, 0.5438269323589959] [0.4690096789303499, 0.5309903210696502]
四、 局部敏感哈希:加速相似搜索
1. LSH核心思想
如果兩個文檔整體相似,那么它們的簽名在某些局部片段很可能完全相同。LSH將長簽名切分成多個片段(bands),只要求某些片段匹配。
2. 簽名分片示例
在實際應用中,我們通常設計bands數(shù)量能被簽名長度整除
假設120維原始簽名: [123, 456, 789, 321, 654, 987, ..., 888] 以切成20個片為例(每片6維): 片 1: [123, 456, 789, ..., ...] 片 2: [..., ..., ..., ..., ...] ... 片 20: [..., ..., ..., ..., 888]
為每個片哈希成一個鍵值
片 1: = hash(tuple(band1)) # 如 0x1a2b3c 片 2: = hash(tuple(band2)) # 如 0x4d5e6f 片 3: = hash(tuple(band3)) # 如 0x7a8b9c 片 4: = hash(tuple(band4)) # 如 0xd1e2f3
3. 數(shù)學原理
設兩個文檔相似度為s,每個片有r行,共有b個片(band):
def lsh_probability(s, r=6, b=20):
"""計算LSH匹配概率"""
prob_one_band = s ** r # 單個band匹配的概率
prob_at_least_one = 1 - (1 - prob_one_band) ** b
return prob_at_least_one
# 示例:相似度0.8的文檔
prob = lsh_probability(0.8, 6, 20) # ≈ 0.9940
# 99.4%的概率會被LSH放到同一個桶中
4. LSH索引構建
| 鍵值 | 片 | 文檔 |
|---|---|---|
| 0x1a2b3c | [123, 456, 789, …, …] | [文檔A, 文檔X, 文檔Y] |
| 0x4d5e6f | […, …, …, …, …] | [文檔B, 文檔C] |
| 0x7a8b9c | […, …, …, …, …] | [文檔D] |
到此,我們便構建完成了LSH哈希與文檔的對應關系,在使用時,只檢查文檔的各個片在哈希表中是否存在,將匹配的文檔再合并為候選集,進一步精確相似度計算。
五、 應用場景
1. 適用場景
搜索引擎去重:去除重復網(wǎng)頁
新聞聚合:識別同一事件的不同報道
論文查重:檢測學術不端
代碼查重:識別抄襲代碼
商品去重:合并相似商品描述
2. 技術限制
短文本效果差:shingles數(shù)量不足時精度下降
語義相似度:對同義替換不敏感
參數(shù)敏感:需要根據(jù)數(shù)據(jù)調(diào)整參數(shù)
近似算法:存在假陽性和假陰性
六、 總結
最小哈希結合局部敏感哈希為大規(guī)模文檔去重提供了高效的解決方案
數(shù)學基礎堅實:基于Jaccard相似度的概率估計
計算高效:將O(n²)復雜度降為近似O(n)
內(nèi)存友好:固定維度簽名大幅減少存儲需求
可擴展性強:支持分布式和流式處理
以上就是Python最小哈希實現(xiàn)海量文檔去重的具體方案的詳細內(nèi)容,更多關于Python最小哈希文檔去重的資料請關注腳本之家其它相關文章!
相關文章
Python+wxPython實現(xiàn)自動生成PPTX文檔程序
這篇文章主要介紹了如何使用 wxPython 模塊和 python-pptx 模塊來編寫一個程序,用于生成包含首頁、內(nèi)容頁和感謝頁的 PPTX 文檔,感興趣的小伙伴可以學習一下2023-08-08
Python hexstring-list-str之間的轉換方法
今天小編就為大家分享一篇Python hexstring-list-str之間的轉換方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-06-06
Python結合多線程與協(xié)程實現(xiàn)高效異步請求處理
在現(xiàn)代Web開發(fā)和數(shù)據(jù)處理中,高效處理HTTP請求是關鍵挑戰(zhàn)之一,本文將結合Python異步IO(asyncio)和多線程技術,探討如何優(yōu)化請求處理邏輯,解決常見的線程事件循環(huán)問題,有需要的小伙伴可以根據(jù)需求進行選擇2025-04-04
python攻防-破解附近局域網(wǎng)WIFI密碼實現(xiàn)上網(wǎng)自由
本文將記錄學習如何通過 Python 腳本實破解附近局域網(wǎng) WIFI 密碼的暴力破解,隨時隨地免費蹭網(wǎng),再也不被WiFi密碼困擾,實現(xiàn)蹭網(wǎng)自由2021-08-08

