1. 程式人生 > >淺談基於simhash的文字去重原理

淺談基於simhash的文字去重原理

題外話

最近更新文章的頻率比較低,所以抓緊抽時間更新一波,要不然有人取關了,啊哈哈。

近日比較開心的一件事情是偶然的機會在開發者頭條分享了一篇文章,然後這篇文章目前排在7日熱度文章第二,看了下點贊近40、收藏數近200、閱讀量近2w,所以更堅定了要寫下去和大家一起分享學習的想法。

之前一直在系列輸出Redis面試熱點相關的文章,本來準備的部分還沒看完無法成文,因此本次就暫且跳過了。

今天結合筆者日常工作和大家一起來學習一些偏工程的演算法,都是大家很熟悉的場景,想必會有共鳴,開始今天的學習吧!

,通過本文你將瞭解到以下內容:

  1. 資訊爆炸的日常生活
  2. 網頁去重和區域性敏感雜湊演算法
  3. simhash演算法基本原理和過程分析
  4. 工程中的去重和聚類實現建議

資訊爆炸

從2010年之後移動網際網路如火如荼,筆者在2011年的時候還在用只能打電話發簡訊的那種手機,然而現在幾乎每個人手機裡的app起碼有10-20款,以至於經常有種資訊爆炸到頭暈的感覺,回顧一下匆匆十年手機裡的變化:

以筆者目前正在從事的資訊流領域來說,有今日頭條、百度App、搜狗搜尋app、一點資訊、趣頭條等feed軟體。

很多時候都是自媒體作者會同時在多個平臺釋出相同的文章,然後會出現非常多的洗稿文章、抄襲文章等,我們無法杜絕和制止這種行為,但是很多時候需要我們使用技術手段來進行識別並處理,讓使用者看到最好的形式的文章和資訊。

資訊爆炸時代,我們需要一個好的文字去重演算法。

網頁去重

前面是以資訊流為例來說的,但是更早的文字去重場景是網頁去重,像谷歌、百度、搜狗這種大型的搜尋引擎,必須有一套高效的去重演算法,要不然網路蜘蛛將做非常多的無用功,時效性等都無法得到保證,更重要的是使用者體驗也不好。

研究表明:網際網路上近似重複的網頁的數量佔網頁總數量的比例高達29%,完全相同的網頁大約佔網頁總數量的22%。

實際中搜索引擎的去重和排序都非常複雜,本文字著簡化的思路來闡述其中的一些要點,無法全面深入,對此表示歉意。

谷歌出品,必屬精品,我們來看看地表最強搜尋引擎是如果做網頁去重呢?

這裡就引出了今天要講的主要內容simhash演算法,本質上文字去重演算法有很多種,每種演算法都有各自的優劣勢,本文並不做橫向對比,而是直接引出simhash演算法進行闡述,對於橫向對比感興趣的讀者可以自行查閱相關資料。

區域性性敏感雜湊

說到hash可能我們第一個想到的是md5這種資訊摘要演算法,可能兩篇文字只有一個標點符號的差距,但是兩篇文字A和B的md5值差異就非常大,感興趣的可以試驗一下看看,Linux環境下直接md5sum即可計算。

有時候我們希望的是原本相同的文章做了微小改動之後的雜湊值也是相似的,這種雜湊演算法稱為區域性敏感雜湊LSH(Locality Sensitive Hashing),這樣我們就能從雜湊值來推斷相似的文章。

區域性敏感雜湊演算法使得在原來空間相似的樣本集合,進行相關運算對映到特定範圍空間時仍然是相似的,這樣還不夠,還需要保證原來不相似的雜湊之後仍然極大概率不相似,這種雙向保證才讓LSH的應用成為可能。

筆者個人認為LSH常用的用途是判重和聚類,其實這兩個作用很相似,比如在資訊流中我們在識別到文章相似之後無法拒絕入庫,這時候就會做聚類,然後用一個id來串起來很多相似的id,從而實現相似文章的把控和管理。

simhash的基本過程

降維壓縮對映
simhash演算法可以將一個文字生成為一個64bit的二進位制數,這裡提一句simhash演算法最初貌似並不是谷歌提出來的,而是谷歌應用推廣的,所以本文出現的simhash相關的資料也都是基於工程中谷歌提出的simhash網頁去重展開的。

谷歌2007年關於simhash的論文:https://www2007.org/papers/paper215.pdf

64bit文字容量
simhash值每個位可以是1或者0,這樣就有2^64種可能了,這個有多大呢?

想必聰明的讀者一定知道棋盤爆炸理論的故事:

傳說西塔發明了國際象棋而使國王十分高興,他決定要重賞西塔,西塔說:我不要你的重賞,陛下,只要你在我的棋盤上賞一些麥子就行了。
在棋盤的第1個格子裡放1粒,在第2個格子裡放2粒,在第3個格子裡放4粒,在第4個格子裡放8粒,依此類推,以後每一個格子裡放的麥粒數都是前一個格子裡放的麥粒數的2倍,直到放滿第64個格子就行了。

看似樸實但是如果真放滿64格需要多少麥粒呢?

筆者在網上看了一些相關的資料和大家分享一下:

圖片來自網路

總計需要1844.67億億顆麥粒,以每顆麥粒0.015g計算大約是2767億噸,在當今糧食產量的水平下大約需要300多年才能種出來。

冪次爆炸的影響力絕非臆想所能企及的程度的,這個哥們後來不知道是不是被治了欺君之罪了,要是在我國古代那肯定懸了。

說這麼多的目的是想表達simhash使用64bit的空間足夠,不用有太多的擔心。

雜湊指紋生成過程分析

我們如何將一個文字轉換為64bit資料呢?

主要步驟:在新拿到文字之後需要先進行分詞,這是因為需要挑出TopN個詞來表徵這篇文字,並且分詞的權重不一樣,可以使用相應資料集的tf-idf值作為分詞的權重,這樣就分成了帶權重的分詞結果。

之後對所有分詞進行雜湊運算獲取二值化的hash結果,再將權重與雜湊值相乘,獲得帶權重的雜湊值,最後進行累加以及二值化處理,先不要暈,看一張圖來大致瞭解下:

接下來舉例詳細說明的判重過程,假如我們需要處理的短文字如下(本質上長文字也是一樣的,相反長文字的判定比短文字更準確):

12306出現伺服器故障:車次載入失敗、購買不了票或卡在候補訂單支付介面等問題。官方給到消費者的建議是:解除安裝或重灌APP,並切換網路耐心等待。
  • 分詞:
    使用分詞手段將文字分割成關鍵詞的特徵向量,分詞方法有很多一般都是實詞,也就是把停用詞等都去掉之後的部分,使用者可以根據自己的需求選擇,假設分割後的特徵實詞如下:
12306 伺服器 故障 車次 載入失敗 購買 候補訂單 支付 官方 消費者 建議 解除安裝 重灌 切換網路 耐心 等待

目前的詞只是進行了分割,但是詞與詞含有的資訊量是不一樣的,比如12306 伺服器 故障 這三個詞就比 支付 解除安裝 重灌更能表達文字的主旨含義,這也就是所謂資訊熵的概念。

為此我們還需要設定特徵詞的權重,簡單一點的可以使用絕對詞頻來表示,也就是某個關鍵詞出現的次數,但是事實上出現次數少的所含有的資訊量可能更多,這就是TF-IDF逆文件頻率概念,來看下維基百科對tf-idf的解釋:

tf-idf(term frequency–inverse document frequency)是一種用於資訊檢索與文字挖掘的常用加權技術。
tf-idf是一種統計方法,用以評估一字詞對於一個檔案集或一個語料庫中的其中一份檔案的重要程度。
字詞的重要性隨著它在檔案中出現的次數成正比增加,但同時會隨著它在語料庫中出現的頻率成反比下降。
tf-idf加權的各種形式常被搜尋引擎應用,作為檔案與使用者查詢之間相關程度的度量或評級。

總之需要選擇一種加權方法,否則效果會打折扣。

  • 雜湊計算和權重化
    前面我們使用分詞方法和權重分配將文字就分割成若干個帶權重的實詞,比如權重使用1-5的數字表示,1最低5最高,這樣我們就把原文字處理成如下的樣式:
12306(5) 伺服器(4) 故障(4) 車次(4) 載入失敗(3) 購買(2) 候補訂單(4) 支付(2) 官方(2) 消費者(3) 建議(1) 解除安裝(3) 重灌(3) 切換網路(2) 耐心(1) 等待(1)

我們對各個特徵詞進行二值化雜湊值計算,為了簡化問題這裡設定雜湊值長度為8bit 並非實踐使用,舉例如下:

12306 10011100 
伺服器 01110101
故障 00110011
車次 11001010
….

這樣我們就把特徵詞都全部二值化為8bit,這裡各個位的1代表+1,0代表-1,依次進行權重相乘,得到新的結果:

12306 10011100 --> 5 -5 -5 5 5 5 -5 -5
伺服器 01110101 --> -4 4 4 4 -4 4 -4 4
故障 00110011 --> -4 -4 4 4 -4 -4 4 4 
車次 11001010 --> 4 4 -4 -4 4 -4 4 -4
….
  • 特徵詞帶權重雜湊值累加和二值化降維
    經過前面的幾步 我們已經將文字轉換為帶權重的雜湊值了,接下來將所有的雜湊值累加,最後將累加結果二值化,如下:
12306的帶權重雜湊值為5 -5 -5 5 5 5 -5 -5
伺服器的帶權重雜湊值為-4 4 4 4 -4 4 -4 4
二者累加為 1 -1 -1 9 1 9 -9 -1

依次累加所有的帶權重雜湊值,假定最終結果為 18 9 -6 -9 22 -35 12 -5
再按照正數1負數0的規則將上述結果二值化為:11001010

至此當收到一個新的文字時經過分詞、雜湊、加權、累加、二值化幾個步驟就將一個文字對映到一定長度的二進位制空間內,之後所有的判重和聚類操作都會基於這個降維數值進行,最後貼一張網上非常經典的圖,展示一個文字進行simhash的主要步驟和細節,如圖所示:

判重檢索實現

前面講述瞭如何生成一個文字的simhash值,接下來思考一個更重要的問題:如果對比確定相似呢?

別急,先來看看維基百科關於漢明距離的一些基礎吧,後面要用到這個理論。

漢明距離

在資訊理論中,兩個等長字串之間的漢明距離(英語:Hamming distance)是兩個字串對應位置的不同字元的個數。換句話說,它就是將一個字串變換成另外一個字串所需要替換的字元個數。
漢明重量是字串相對於同樣長度的零字串的漢明距離,也就是說,它是字串中非零的元素個數:對於二進位制字串來說,就是1的個數,所以11101的漢明重量是4。
對於二進位制字串a與b來說,它等於a 異或b後所得二進位制字串中“1”的個數。
漢明距離是以理查德·衛斯里·漢明的名字命名的,漢明在誤差檢測與校正碼的基礎性論文中首次引入這個概念。
在通訊中累計定長二進位制字中發生翻轉的錯誤資料位,所以它也被稱為訊號距離。漢明重量分析在包括資訊理論、編碼理論、密碼學等領域都有應用。但是,如果要比較兩個不同長度的字串,不僅要進行替換,而且要進行插入與刪除的運算,在這種場合下,通常使用更加複雜的編輯距離等演算法。

理論發明者理查德·衛斯里·漢明簡介

理查德·衛斯里·漢明(Richard Wesley Hamming,1915年2月11日-1998年1月7日),美國數學家,主要貢獻在電腦科學和電訊。
1937年芝加哥大學學士學位畢業,1939年內布拉斯加大學碩士學位畢業,1942年伊利諾伊大學香檳分校博士學位畢業,博士論文為《一些線性微分方程邊界值理論上的問題》。
二戰期間在路易斯維爾大學當教授,1945年參加曼哈頓計劃,負責編寫計算機程式,計算物理學家所提供方程的解。該程式是判斷引爆核彈會否燃燒大氣層,結果是不會,於是核彈便開始試驗。
1946至76年在貝爾實驗室工作。他曾和約翰·懷爾德·杜奇、克勞德·艾爾伍德·夏農合作。1956年他參與了IBM 650的程式語言發展工作。1976年7月23日起在美國海軍研究院擔當兼任教授,1997年為名譽教授。他是美國電腦協會ACM的創立人之一,曾任該組織的主席。
相關獎項和榮譽:1968年ACM圖靈獎、1968年IEEE院士、1979年EmanuelR.Piore獎、1980年美國國家工程學院院士、1981年賓夕法尼亞大學Harold Pender獎、1988年IEEE理查·衛斯里·漢明獎

本文就不深究漢明距離的數學原理以及證明過程了,直接使用結論,谷歌經過工程驗證認為當兩個64bit的二值化simhash值的漢明距離超過3則認為不相似,所以判重問題就轉換為求兩個雜湊值的漢明距離問題。

谷歌漢明距離的權衡

谷歌對於漢明距離的選取,筆者查詢了相關論文後瞭解到,谷歌使用80億的資料將漢明距離從1-10進行試驗,我們可以知道當漢明距離越大意味著判重越不嚴格,漢明距離越小則判重更加嚴格,對此谷歌給出的是一個權衡值:

海量資料匹配問題

看到這裡,我們又更近一步了,可以判斷兩個文字是否相似了,但是網頁去重是面對海量資料的,我們如何對比所有資料確定相似呢?

假如庫存10億條資料的simhash值,每新來一個文字生成simhash值之後就要對庫存10億資料進行O(n)遍歷嗎?這個時間消耗有些大,為此我們需要進行一些工程加速,理論才能在工程中展示威力。

暴力破解思路

先看下暴力方法是如何實現的呢?
我們不知道3位以內的變化究竟會是哪些,所以這是個非精確匹配問題,計算機是無法像人一樣去模糊思考的,從兩個角度去分析暴力破解的情況:

  • 生成64bit雜湊值的3位以內的變化組合
    這是個排列組合問題,相當於從64bit中挑選3bit及其以內資料作為突變點,此種情況生成組合總共有43744種組合,看個圖:

  • 儲存時生成每個原始simhash值的所有3位內的變化組合
    也就是說儲存1個simhash要輔助儲存43744個組合變化,空間大了4.37w倍,彷彿聽到心在顫抖…

暴力破解就是典型的空間換時間,在資料量不大且不會持續增長時還好,對於海量資料無法接受,所以還得繼續想辦法!

一種演算法優化:鴿巢原理

高中在做概率題目的時候經常有這種場景:問yyy事件的概率是多少?

一般對於這種問題正面求解的計算量會比較龐大,大的計算量也意味著出錯,所以常用的套路是轉換為其對立面xxx事件的概率,然後1-xxx事件的概率就是yyy的概率,然而xxx事件的概率比較好計算,採用對立迂迴的戰術同樣解決了問題。

再看看我們的simhash相似判定的問題,之前一直關注於有至少有61位相同,現在轉換為看最多有3位不同要怎麼分佈,或許有驚喜!

考慮一個問題:假如現在有3只鴿子,給你4個鴿子窩,每個鴿子窩可以有任意只鴿子,所以放鴿子的時候會出現什麼情況呢?思考3分鐘......

答案:無論怎麼放最少會有1個鴿子窩是空的。

將鴿巢原理遷移到simhash相似度問題上來看,假如我們把64bit的雜湊值均分為4份(鴿子窩),那麼兩文字相似假如有3位不一樣(3只鴿子),那麼也必然最少有一份16bit長度是完全一樣的(最少有1個鴿子窩是空的)。

將鴿巢原理應用到simhash去重問題

我們在儲存時將64bit雜湊值平均分為4份每份16bit長,然後使用每一份作為key,value是64bit雜湊值陣列,原來是單個雜湊值儲存,彼此沒有關係,現在每個雜湊值都劃分為4份,由整體儲存轉換為分散複用儲存,複用的就是劃分的key,從概率上來說超過2^16個數據集後必然存在key的相同資料,再貼一張筆者畫的圖:

注:value可以有其他形式,但是型別是個列表,因為會有很多資料的某16bit的key是一樣的,對應的value中v1...vn就是其4等份中的某一份與當前key相同。

鴿巢原理應用詳細說明
在圖中對於一個64bit的雜湊值S均分為16位長度四個部分ABCD,在儲存時分別以ABCD為key進行儲存,如果當前key已經存在,那麼就將S追加到key對應的value列表中,也就是說value中儲存的都是四等份中存在key的雜湊值。

當新來一個文字生成雜湊值S'之後,按照相同的規則生成abcd四部分,之後逐個進行雜湊對比,這個時間複雜度是O(1):

  • 如果abcd四個作為key都不存在,那麼可以認為S'沒有相似的文字;
  • 如果abcd四個key中有命中,那麼就開始遍歷對應key的value,檢視是否滿足<=3的漢明距離確定相似性;
  • 如果上一個命中的key未找到相似文字,則繼續遍歷剩下的key,重複相同的過程,直至所有的key全部遍歷完或者命中相似文字,則結束。

以庫存10億資料為例複雜度分析
10億資料大約是2^30,key的長度為16bit,由於資料量比較大所以理論上key分佈均勻,儲存中約有2^16(65536)個key,肯定不會多於65536的。

每個key對應value的list長度最大是(2^14)*4=65546個,因為key在雜湊值中的位置可能是1/2/3/4,這樣理論上第1位是key的數量是2^30/2^16=2^14,同理第2/3/4位置上也是這樣的,所以value最大長度是(2^14)*4。

最壞情況待匹配哈數值S'的四個key,ABCD均命中了但是依次遍歷之後都相似度判斷失敗了,這種情況的理論最大匹配次數是4*65536=262144次。

犯嘀咕

寫到這裡筆者心裡有點犯嘀咕,因為網上雖然有些介紹simhash的但是有的計算存和儲存的思路也有一些差異,工程使用中設計方案也不同,因此上述的計算主要表達了從億級降低到萬級的優化思想。

水平所限筆者現在的思路可能存在問題或者不是最優解,如果有讀者發現問題,可以私信我哈!

一些工程實踐優化

前面重點闡述了simhash的原理和網頁去重應用過程,結合筆者自身在做資訊流文章判重和聚類的相關經驗來看,很多新聞類文章的時效性區分比較明顯,也就是2019年12月20號的文章跟2018年12月20號的文章相似的概率很低,這種場景下儲存海量資料可能產生浪費,因為大部分資料都是不相似的,所以在實際使用中旨在理解simhash的核心思想,然後採用適合自己的判重和聚類演算法。

一般來說進行判重和聚類離不開分詞,在實踐中筆者使用JieBa分詞的C++版本封裝了一個分詞服務,支援批量和多種分詞方式,不過結巴的詞庫貌似是使用人民日報的語料訓練的tf-idf,並不適用所有場景,所以我們採用自己的數千萬文章資料建立了自己的tf-idf表,實際效果比預設的tf-idf好一些。

沒有萬能的方法 只有萬能的思想。

掌握核心要點就可以自己展開設計調整,效果不一定比網上那些成功範例差甚至更好,這也是學習和實踐的樂趣吧!

巨人的肩膀

  1. https://zhuanlan.zhihu.com/p/55372480
  2. https://wizardforcel.gitbooks.io/the-art-of-programming-by-july/content/06.03.html
  3. https://cloud.tencent.com/developer/article/1189493
  4. https://www2007.org/papers/paper215.pdf

往期精彩

    • 系列文章
      【決戰西二旗】|理解Sort演算法
      【決戰西二旗】|快速排序的優化
      【決戰西二旗】|你真的懂快速排序?
      以上這三篇文章是圍繞快速排序從基礎概念、基礎實現、到優化實現、效能分析、最後到工程應用展開的,雖然不全面但是也不算沒有深度,感興趣的讀者可以翻閱。
      【決戰西二旗】|理解標準模板庫STL(一)
      【決戰西二旗】|理解STL Map使用和原理
      【決戰西二旗】|理解STL vector原理
      【決戰西二旗】|理解STL list原理
      以上三篇文章是對STL進行了一些比較淺顯的介紹,僅做拋磚引玉之用,可能深度不夠理想,後面筆者一定會捲土重來寫個深度版本的STL系列。
      【決戰西二旗】|Redis面試熱點之底層實現篇
      【決戰西二旗】|Redis面試熱點之工程架構篇
      【決戰西二旗】|Redis面試熱點之工程架構篇[2]
      【決戰西二旗】|Redis面試熱點之底層實現篇(續)
      以上四篇文章圍繞Redis面試中常見的熱點問題展開的,目前還沒有結束,還在更新。
    • 資料結構和演算法
      白話分散式系統中的一致性雜湊演算法
      深入理解跳躍連結串列[一]
      面試必知必會|堆和優先佇列
      面試必知必會|理解堆和堆排序
      二叉樹及其四大遍歷
      白話布隆過濾器BloomFilter
      以上幾篇文章都是資料結構和演算法相關的,筆者並沒有選取太多非常基礎的結構說起,而是選擇了一些工程中廣泛使用的資料結構為切入點,實用第一,感興趣可以直接戳進去。
    • 資料儲存
      淺談叢集版Redis和Gossip協議
      深入理解跳錶在Redis中的應用
      理解Redis單執行緒執行模式
      理解Redis的反應堆模式
      淺析Redis 4.0新特性之LazyFree
      理解快取系統的三個問題
      理解Redis持久化
      以上幾篇基本上都是圍繞Redis寫的,可能有些不符合資料儲存的大標題了,後續會逐漸增加相關文章,敬請期待吧!
    • 服務開發
      深入理解IO複用之epoll
      幾種高效能網路模型
      淺談Linux下Socket選項設定
      淺談生活中的短網址和短ID
      聊聊後端面試中的一些問題和思考
      這一塊產出比較少,不過epoll和網路模型這兩篇還是湊合的,可以戳進去看看。
    • 程式語言
      淺析CPython的全域性解釋鎖GIL
      面試必知必會|理解C++虛擬函式
      這一塊產出也比較少,後續會補充大量C/C++、Python、Golang的文章,還是一如既往地期待吧。