1. 程式人生 > >我是如何用單機實現億級規模題庫去重的?

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

流程 algo 今天 函數 分布 github .com 角度 比例

背景

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

一些嘗試方案

比較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非常必要,這個不難實現。

周期性叠代

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

計算量還大麽?

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

結語

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

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