我是如何用單機實現億級規模題庫去重的?

haolujun發表於2018-02-09

背景

最近工作中遇到了一個問題:如何對大規模題庫去重?公司經過多年的積累,有著近億道題目的題庫,但是由於題目來源不一導致題庫中有很多重複的題目,這些重複的題目在檢索時,除了增加搜尋引擎的計算量外,並不會提高準確率。此外由於題目過多,搜尋引擎往往採取了截斷策略,只對一部分題目進行計算,這導致了某些正確的題目反而得不到計算,拍搜準確率甚至不增反降。所以對於一個搜尋引擎來說,雖然初期增加題目數量往往可以大幅提高拍搜準確率,但是當題目量大到一定程度時,反而會由於計算量跟不上導致準確率下降。如何儘可能的去除重複題目顯得尤為重要。

一些嘗試方案

比較MD5值

對每道題目計算其MD5值作為簽名,這樣在新增題目時,只要判斷題庫中是否有相同的MD5值即可。

這種方案只適用於兩道題目一模一樣的情況,而現實中題目往往不只是這樣。

  • “A比B大10"與"B比A小10”
  • “小紅買10本書”與“小明買10本書”
  • “今天空氣溫度為10度”與“今天的空氣溫度為10度”
    這些應該是重複題,但是MD5值不同,沒法去重。

利用最長公共子序列和最小編輯距離演算法

利用最長公共子序列演算法與最小編輯距離演算法計算兩個題目的相似度,如果相似度大於一定比例,例如大於90%,就認為是重複的題目。

這個方法理論上可行,但是計算量太大。假如文件數為N,平均文件長度為M,那麼計算量大致為:$ O(N^2*M^2) $ 。

假設N=1000萬,M=200,則計算量約為 $ 4*10^{18} $ ,筆者線下可用機器有限,沒有這麼大的計算能力。但是如果能夠把相似的題目歸攏到一起,然後去比較這一小撮題目中兩兩相似程度,這個還是可行的。

Jaccard相似度

為此,我特意看了兩本書:《資訊檢索導論》的19.6章節以及《大資料-網際網路大規模資料探勘與分散式處理》的3.2與3.3節。這裡面講述瞭如何計算兩個集合的Jaccard相似度:$ \frac{|A \cap B|}{|A \cup B|} $ 。這個公式對於去重來說沒什麼卵用,因為計算量還是那麼大。所以這兩本書還特意介紹了與其等價的演算法:轉換成隨機全排列,基於概率演算法去計算Jaccard的近似值。這個轉換的證明本文不贅述,有興趣的小夥伴直接去看這兩本書。但是這裡面有一個有意思的問題也是計算Jaccard相似度最關鍵的一步:如何對一個超級大的N生成一個0~N-1隨機全排列?我這裡給出一個近似演算法,學過初等數論的小夥伴應該對下面的定理不陌生。

  • 定理: $ y = (a*x+b) \mod n $ ,如果a與n互質(即a與n的最大公約數為1),當x取遍0~n-1時,y取遍0~n-1。

證明:假如存在兩個數 $ x_1 $ 和 $ x_2 $,使得 $ y_1 = (a * x_1 + b) \mod n = y_2 = (a * x_2 + b) \mod n $ ,則 $ (a * x_1 + b) \% n = (a * x_2 + b) \% n $ ,得出 $ (a * x_1 + b - a * x_2 - b) \% n = 0 $ ,繼而得到 $ a * (x_1 - x_2) \% n = 0 $。由於a與n互質,最大公約數為1,所以得出 $ x_1 - x_2 = k * n $ ,即 $ x_1 = x_2 + k * n $。當 $ x_1 $ 和 $ x_2 $ 都小於n時,k只能等於0,即 $ x_1 = x_2 $。這就說明當x取遍0~n-1時,其餘數肯定不重複,由於餘數的取值範圍也是0~n-1,所以結論得證。

這樣,當我們知道n時,只要找到與n互質的100或者200個數就行,甚至可以找到小於n的100個或者200個素數(素數篩法大家自行百度),然後再隨機生成100次到200次b,就能構造出一批這樣的函式。
例如,a = 3,b = 4, n = 8

x = 0 y = 4
x = 1 y = 7
x = 2 y = 2
x = 3 y = 5
x = 4 y = 0
x = 5 y = 3
x = 6 y = 6
x = 7 y = 1

雖然這個概率演算法能夠降低一些計算量,但是我還是不能夠接受。因為我們現在的關鍵問題是找出相似的一小撮,並在這一小撮中進行更精細化的判斷策略,怎麼找到這一小撮咧?

利用線上拍搜日誌進行挖掘

正所謂具體情況具體分析,不能一味追求高科技卻忽略現實條件。比如百度也有去重策略,但是其最後應用到線上的並不是Jaccard相似度,而是找文件中最長的幾個句子,根據這幾個句子是否一樣判斷兩個文件是否重複,而且準確率出奇的好。所以,我們也要具體問題具體分析。

觀察一下拍搜流程,檢索日誌中會記錄每次搜尋結果中幾個匹配程度最高的文件id,那麼我就可以認為這幾個文件是一個小簇,沒有必要再重新聚簇。此外由於拍搜的優化策略極多,準確率極高,這比我自己再重新發明一個聚簇演算法要省事並且效果好。有這麼好的日誌在手,就要充分利用起來。接下來我就詳細說說我是如何實現去重策略的。

日誌格式如下:

[[1380777178,0.306],[1589879284,0.303],[1590076048,0.303],[1590131395,0.303],[1333790406,0.303],[1421645703,0.303],[1677567837,0.303],[1323001959,0.303],[1440815753,0.303],[1446379010,0.303]]

這是一個json陣列,每個陣列中有題目的ID和其得分。

日誌選取

選取題目ID得分比較高的日誌作為候選日誌。這麼選取是因為線上的影象識別不能保證百分百準確,如果圖片質量特別差,那麼根據識別內容檢索到的題目之間差別較大,可能根本不是一類。

聚簇

初始集合建立

對於每條日誌,由排在第一位的ID作為簇標識,其它元素作為簇中的元素。

集合求並

看如下樣例:

A -> B,C,D
E -> C,D,F

由於兩個集合中有相同的ID,我們推測這兩個集合其實屬於一個簇,如何實現兩個集合的並?利用並查集演算法(自行百度之,參加過程式設計競賽的小夥伴應該都不陌生,我寫的一個樣例程式碼:https://github.com/haolujun/Algorithm/tree/master/union_find_set ),並查集能夠出色的完成集合合併操作。例如,可以利用並查集的join操作完成兩條日誌的合併。

union_find_set.join(A,B)
union_find_set.join(A,C)
union_find_set.join(A,D)

union_find_set.join(E,C)
union_find_set.join(E,D)
union_find_set.join(E,F)

呼叫完操作後,我們會發現A,B,C,D,E,F都屬於同一個集合。

集合元素限制

在實際測試時發現,某些集合中的題目數量可能會達到百萬,這種情況出現是因為聚類過程中的計算偏差導致的。比如:A與B相似,B與C相似,我們會把A,B,C放到一個簇中,但是實際上A和C可能不相似,這是聚類過程中非常容易出現的問題。簇過大會加大後面的精細計算的計算量,這是一個比在大題庫中去重稍簡單的問題,但是也非常難。考慮到題庫中重複題目不會太多,可以對每個集合大小設定上限元素數目,如果兩個將要合併的集合元素總數大於上限,則不將這兩個集合合併,這個利用並查集也非常容易實現。

精細計算

如何判斷兩個題目是否重複

現在得到的簇是一個經過拍搜的結果聚合的,但是拍搜有一個問題就是檢索使用的文字是由OCR識別生成的,其中難免會有識別錯誤,搜尋引擎為了能容忍這種錯誤,加入了一定的模糊策略,這導致簇中的結果並不完全相似,所以精細計算是必要的。那麼如何比較兩個題目是否是重複的呢?特別是對於數學題這種數字和運算子、漢字混合的題目,該如何辦?經過長時間分析發現,不能夠把數字、字母與漢字同等比較。數字、字母如果不相等,那麼八成這兩道題是不同的;如果數字、字母相同那麼漢字描述部分可以允許一些差異,但是差異也不要太大。這就得到了我最後的精細去重策略:分別提取題目的漢字和數字、字母、運算子,數字、字母、運算子完全相等並且漢字部分的相似度(可以使用最小編輯距離或者最長公共子序列)大於80%,就可以認為兩道題目相同。

“A比B大10"與"B比A小10”                                                 數字與字母組成的字串不相等,不認為重複
“小紅買10本書”與“小明買10本書”                                     數字字母相同,漢字相似度大於80%,認為重複
“今天空氣溫度為10度”與“今天的空氣溫度為10度”            數字字母相同,漢字相似度大於80%,認為重複

雖然這個策略不能百分百去重所有重複題,但是能確保它能去重大部分重複題。

保留哪些題目,去除哪些題目?

考慮到搜尋引擎在儲存倒排是按照題目ID大小進行排序的(存放ID與ID之間的差值),所以留下小的ID去掉大的ID非常必要,這個不難實現。

週期性迭代

我們的去重演算法是基於日誌進行的去重,那麼可以每次去重一部分,上線後再撈取一段時間內的日誌進行去重,這樣不斷的迭代進行。

計算量還大麼?

根據單機的計算量,一次撈取一定數量的日誌進行去重,單機就可以完成,不需要叢集,不需要分散式。

結語

聰明的小夥伴可能發現,我投機取巧了。我並沒有直接對題庫去蠻力去重,而是從拍搜日誌下手,增量的一步步的實現題庫去重,只要迭代次數足夠,可以最終去重所有題目,並且每次去重可以實實在在看到效果,可以更方便調整策略細節。所以,在面對一個問題時,換一個角度可能會有更簡單的做法。

相關文章