火眼金睛算法,教你海量短文本場景下去重

本文由QQ大數據發表算法

最樸素的作法數據庫

在大多數狀況下,大量的重複文本通常不會是什麼好事情,好比互相抄襲的新聞,羣發的垃圾短信,鋪天蓋地的廣告文案等,這些都會形成網絡內容的同質化並加劇數據庫的存儲負擔,更糟糕的是下降了文本內容的質量。所以須要一種準確而高效率的文本去重算法。而最樸素的作法就是將全部文本進行兩兩比較,簡單易理解,最符合人類的直覺,對於少許文原本說,實現起來也很方便,可是對於海量文原本說,這明顯是行不通的,由於它的時間複雜度是,針對億級別的文本去重時,時間消耗可能就要以年爲單位,此路不通。網絡

另外,咱們講到去重,實際上暗含了兩個方面的內容,第一是用什麼方式去比較更爲高效,第二是比較的時候去重標準是什麼。這裏的去重標準在文本領域來講,就是如何度量兩個文本的類似性,一般包含編輯距離,Jaccard距離,cosine距離,歐氏距離,語義距離等等,在不一樣領域和場景下選用不一樣的類似性度量方法,這裏不是本文的重點,因此按下不表,下面着重解決如何進行高效率比較的問題。app

核心思想框架

下降時間複雜度的關鍵: > 盡力將潛在的類似文本聚合到一塊,從而大大縮小須要比較的範圍分佈式

simHash算法工具

海量文本去重算法裏面,最爲知名的就是simHash算法,是谷歌提出來的一套算法,並被應用到實際的網頁去重中。 simHash算法的最大特色是:將文本映射爲一個01串,而且類似文本之間獲得的01串也是類似的,只在少數幾個位置上的0和1不同。爲了表徵原始文本的類似度,能夠計算兩個01串之間在多少個位置上不一樣,這即是漢明距離,用來表徵simHash算法下兩個文本之間的類似度,一般來講,越類似的文本,對應simHash映射獲得的01串之間的漢明距離越小。大數據

爲了讓這個過程更爲清晰,這裏舉個簡單的例子。ui

t1 = "媽媽喊你來吃飯" t2 = "媽媽叫你來吃飯"blog

能夠看到,上面這兩個字符串雖然只有一個字不一樣,可是經過簡單的Hash算法獲得的hash值可能就徹底不同了,於是沒法利用獲得的hash值來表徵原始文本的類似性。然而經過simHash算法的映射後,獲得的simHash值即是以下這樣:

SH1 = "1000010010101101[1]1111110000010101101000[0]00111110000100101[1]001011" SH2 = "1000010010101101[0]1111110000010101101000[1]00111110000100101[0]001011"

仔細觀察,上面的兩個simHash值只有三個地方不同(不同的地方用"[]"標出),所以原始文本之間的漢明距離即是3。一般來講,用於類似文本檢測中的漢明距離判斷標準就是3,也就是說,當兩個文本對應的simHash之間的漢明距離小於或等於3,則認爲這兩個文本爲類似,若是是要去重的話,就只能留下其中一個。

simHash算法的去重過程思路很簡單,首先有一個關鍵點: > 假如類似文本判斷標準爲漢明距離3,在一個待去重語料集中存在兩個類似文本,那也就是說這兩個類似文本之間的漢明距離最大值爲3(對應hash值最多有3個地方不一樣),若是simHash爲64位,能夠將這個64位的hash值從高位到低位,劃分紅四個連續的16位,那麼這3個不一樣的位置最多隻能填滿4箇中的任意3個區間(能夠反過來想,若是這4個區間都填滿了,那就變成漢明距離爲4了)。也就是說兩個類似文本一定在其中的一個連續16位上徹底一致。

想明白了這個關鍵點以後,就能夠對整個待去重文本都進行一次simHash映射(本文中使用64位舉例),接着將這些01串從高位到低位均分紅四段,按照上面的討論,兩個類似的文本必定會有其中一段同樣,仍用上面的例子,分紅的四段以下所示:

t1 = "媽媽喊你來吃飯" SH1 = "1000010010101101[1]1111110000010101101000[0]00111110000100101[1]001011" SH1_1 = "1000010010101101" #第一段 SH1_2 = "[1]111111000001010" #第二段 SH1_3 = "1101000[0]00111110" #第三段 SH1_4 = "000100101[1]001011" #第四段 t2 = "媽媽叫你來吃飯" SH2 = "1000010010101101[0]1111110000010101101000[1]00111110000100101[0]001011" SH2_1 = "1000010010101101" #第一段 SH2_2 = "[0]111111000001010" #第二段 SH2_3 = "1101000[1]00111110" #第三段 SH2_4 = "000100101[0]001011" #第四段

這一步作完以後,接下來就是索引的創建。按照上面的討論,每個simHash都從高位到低位均分紅4段,每一段都是16位。在創建倒排索引的過程當中,這些截取出來的16位01串的片斷,分別做爲索引的key值,並將對應位置上具備這個片斷的全部文本添加到這個索引的value域中。 直觀上理解,首先有四個大桶,分別是1,2,3,4號(對應的是64位hash值中的第1、2、3、四段),在每個大桶中,又分別有個小桶,這些小桶的編號從0000000000000000到1111111111111111.在創建索引時,每個文本獲得對應的simHash值後,分別去考察每一段(肯定是1,2,3和4中的哪一個大桶),再根據該段中的16位hash值,將文本放置到對應大桶中對應編號的小桶中。 索引創建好後,因爲類似文本必定會存在於某一個16位hash值的桶中,所以針對這些分段的全部桶進行去重(能夠並行作),即可以將文本集合中的全部類似文本去掉。

整個利用simHash進行去重的過程以下圖所示:

img

總結一下,整個simHash去重的步驟主要是三個: 1. 針對每個待去重文本進行simHash映射; 2. 將simHash值分段創建倒排索引; 3. 在每個分段的hash值中並行化去重操做。

利用simHash進行去重有兩個點很是關鍵: - simHash映射後仍然保持了原始文本的類似性; - 分而治之的思想大大下降了沒必要要的比較次數。

所以,有了這兩點作保證,對於長文本下的simHash算法以及使用漢明距離來度量文本之間的類似性,能夠極大下降算法的時間複雜度,而且也能取得很好的去重效果。可是在短文本場景下,這種度量方法的效果將會變得不好,一般狀況下,用來度量長文本類似的漢明距離閾值爲3,可是短文本中,類似文本之間的漢明距離一般是大於3的,而且該算法中,基於漢明距離的類似性閾值選取的越高,該算法的時間複雜度也會越高,此時漢明距離沒法繼續做爲短文本類似性的度量標準應用到短文本去重中。

基於文本局部信息的去重算法

基於文本局部信息的去重過程,其基本思想和simHash相似,只不過不是利用hash值,而是直接利用文本的一個子串做爲key,而後凡是擁有這個子串的文本都會被放入到這個子串對應的桶中。 這裏隱含了一個前提: > 任意兩個可斷定的類似文本,一定在一個或多個子串上是徹底一致的。

此外,子串的產生,能夠經過相似於n-grams(若是是詞和字層面的,對應shingles)的方法,直接從原始文本上滑動窗口截取,也能夠去掉停用詞後在剩下的有序詞組合中截取,還能夠對原始文本進行摘要生成後再截取,總之只要是基於原始文本或可接受範圍內的有損文本,均可以利用相似的思想來產生這些做爲索引的子串。

整個去重算法分爲五個大的框架,分別包括:文本預處理,倒排索引的創建,並行化分治,去重算法的實現,文本歸併等。

文本預處理

文本預處理根據所選用的具體子串截取方法的不一樣,而有所不一樣。若是子串是由詞組合造成的,則須要對文本進行分詞,若是須要去掉停用詞,那麼這也是文本預處理的工做。爲了簡化過程的分析,這裏主要以原始文本直接截取子串爲例,所以預處理的工做相對偏少一些。

倒排索引的創建

假定潛在的兩個類似文本(要求去重後其中一個被去掉)分別是t1和t2,兩者之間徹底一致的最大連續子文本串有k個,它們組成一個集合,將其定義爲S = {s1,s2,...,sk},這些子文本串的長度也對應一個集合L = {l1,l2,...,lk},針對該特定的兩個文本進行去重時,所選擇的截取子文本串長度不能超過某一個閾值,由於若是截取長度超過了該閾值,這兩個文本便再也不會擁有同一個子文本串的索引,於是算法自始至終都不會去比較這兩個文本,從而沒法達到去重的目的。這個閾值便被定義爲這兩個文本上的最大可去重長度,有:

img

在全部的全局文本上去重的話,相應的也有一個全局去重長度m,它表徵了若是要將這部分全局文本中的類似文本進行去重的話,針對每個文本須要選取一個合適的截取長度。通常來講,全局去重長度的選擇跟去重率和算法的時間複雜度相關,實際選擇的時候,都是去重率和時間複雜度的折中考慮。全局去重長度選擇的越小,文本的去重效果越好(去重率會增大),但相應的時間複雜度也越高。全局去重長度選擇越大,類似文本去重的效果變差(部分類似文本不會獲得比較),但時間複雜度會下降。這裏的緣由是:若是全局去重長度選擇的太高,就會大於不少類似文本的最大可去重長度,於是這些類似文本便再也不會斷定爲類似文本,去重率於是會降低,但也正是由於比較次數減小,時間複雜度會下降。相反,隨着全局去重長度的減少,更多的類似文本會劃分到同一個索引下,通過類似度計算以後,相應的類似文本也會被去除掉,於是全局的去重率會上升,可是因爲比較次數增多,時間複雜度會增大。

假定有一個從真實文本中抽樣出來的類似文本集C,能夠根據這個樣例集來決定全局去重長度m,實際狀況代表,一般來講當m>=4(通常對應兩個中文詞的長度),算法並行計算的時候,時間複雜度已經下降到能夠接受的範圍,所以能夠獲得:

img

假定某個待去重的文本t,其長度爲n。定義S爲截取的m-gram子串的集合,根據m和n的大小關係,有下列兩種狀況: (1)當n>=m時,能夠按照m的大小截取出一些m-gram子串的集合,該集合的大小爲n-m+1,用符號表示爲S = {s1,s2,...,sn-m+1}; (2)當n<m時,沒法截取長度爲m的子串,所以將整個文本做爲一個總體加入到子串集合當中,所以有S={t}. 每個待去重文本的m-gram子串集合生成以後,針對每一個文本t,遍歷對應集合中的元素,將該集合中的每個子串做爲key,原始文本t做爲對應value組合成一個key-value對。全部文本的m-gram子串集合遍歷結束後,即可以獲得每個文本與其n-m+1個m-gram子串的倒排索引。 接下來,按照索引key值的不一樣,能夠將同一個索引key值下的全部文本進行聚合,以便進行去重邏輯的實現。

img

算法的並行框架

這裏的並行框架主要依託於Spark來實現,原始的文本集合以HDFS的形式存儲在集羣的各個節點上,將這些文本按照上面所講的方法將每個文本劃分到對應的索引下以後,以每個索引做爲key進行hash,並根據hash值將全部待去重文本分配到相應的機器節點(下圖中的Server),分佈式集羣中的每個工做節點只需負責本機器下的去重工做。基於Spark的分佈式框架以下,每個Server即是一個工做節點,Driver負責分發和調配,將以HDFS存儲形式的文本集合分發到這些節點上,至關於將潛在的可能重複文本進行一次粗粒度的各自聚合,不重複的文本已經被徹底分割開,於是每一個Server只須要負責該節點上的去重工做便可,最終每一個Server中留下的即是初次去重以後的文本。

img

去重的實現

並行化框架創建後,能夠針對劃分到每個索引下的文本進行兩兩比較(如上一個圖所示,每個Server有可能處理多個索引對應的文本),從而作到文本去重。根據1中的分析,任意兩個可斷定的類似文本t1和t2,一定在一個或多個子文本串上是徹底一致的。根據3.1.1中的設定,這些徹底一致的最大連續子串組成了一個集合S = {s1,s2,...,sk},針對t1和t2劃分m-gram子串的過程當中,假定能夠分別獲得m-gram子串的集合爲S1和S2,不妨假設S中有一個子串爲si,它的長度|si|大於全局去重長度m,那麼必定能夠將該子串si劃分爲|si|-m+1個m-gram子串,而且這些子串必定會既存在於S1中,也會存在於S2中。更進一步,t1和t2都會同時出如今以這|si|-m+1個m-gram子串爲key的倒排索引中。

去重的時候,針對每個索引下的全部文本,能夠計算兩兩之間的類似性。具體的作法是,動態維護一個結果集,初始狀態下隨機從該索引下的文本中選取一條做爲種子文本,隨後遍歷該索引下的待去重文本,嘗試將遍歷到的每一條文本加入結果集中,在添加的過程當中,計算該遍歷到的文本與結果集中的每一條文本是否能夠斷定爲類似(利用類似性度量的閾值),若是與結果集中某條文本達到了類似的條件,則退出結果集的遍歷,若是結果集中徹底遍歷仍未觸發類似條件,則代表這次待去重的文本和已知結果集中沒有任何重複,所以將該文本添加到結果集中,並開始待去重文本的下一次遍歷。 去重的時候,兩個文本之間的類似性度量很是關鍵,直接影響到去重的效果。可使用的方法包括編輯距離、Jaccard類似度等等。在實際使用時,Jaccard類似度的計算通常要求將待比較的文本進行分詞,假定兩個待比較的文本分詞後的集合分別爲A和B,那麼按照Jaccard類似度的定義能夠獲得這兩個文本的類似度 顯然,兩個徹底不一致的文本其Jaccard類似度爲0,相反兩個徹底同樣的文本其Jaccard類似度爲1,所以Jaccard類似度是一個介於0和1之間的數,去重的時候,能夠根據實際須要決定一個合適的閾值,大於該閾值的都將被斷定爲類似文本從而被去掉。

整個的去重實現僞代碼以下:

初始狀態: 文本集合T = {t_1,t_2,...,t_n} 去重結果R = {} 類似度閾值sim_th 輸出結果: 去重結果R 算法過程: for i in T: flag = true for j in R: if( similarity(i,j) < sim_th ) flag = false break -> next i else continue -> next j if( flag ) R.append(i) #表示i文本和當前結果集中的任意文本都不重複,則將i添加到結果集中

文本歸併去重

這一個步驟的主要目的是將分處在各個不一樣機器節點上的文本按照預先編排好的id,從新進行一次普通的hash去重,由於根據上一步的過程當中,可能在不一樣子串對應的桶中會留下同一個文本,這一步通過hash去重後,便將這些重複的id去除掉。 最終獲得的結果即是,在整個文本集上,全部的重複文本都只保留了一條,完成了去重的目的。整個的去重流程以下圖所示:

img

和simHash進行比較

這裏提出來的去重算法與simHash比較,分別從時間複雜度和去重準確度上來講,

首先,時間複雜度大大下降 - 分桶的個數根據文本量的大小動態變化,大約爲文本數的2倍,平均單個桶內不到一條文本,桶內的計算複雜度大大下降;而simHash算法中,桶的個數是固定的4*216=26萬個 - 通常來講,只有類似文本纔有類似的詞組合,因此某個特定的詞組合下類似文本佔大多數,單個桶內的去重時間複雜度傾向於O(N);相應的,simHash單個桶內依然有不少不類似文本,去重時間複雜度傾向於O(N^2)

其次,類似性的度量更爲精準: - 可使用更爲精準的類似性度量工具,可是simHash的漢明距離在短文本里面行不通,召回過低,不少類似文本並不知足漢明距離小於3的條件

總結

這裏提出的基於文本局部信息的去重算法,是在短文本場景下simHash等去重算法沒法知足去重目的而提出的,實際上,一樣也能夠應用於長文本下的去重要求,理論上,時間複雜度能夠比simHash低不少,效果可以和simHash差很少,惟一的缺點是存儲空間會大一些,由於算法要求存儲不少個文本的副本,但在存儲這些文本的副本時候,可使用全局惟一的id來代替,因此存儲壓力並不會提高不少,相比時間複雜度的大大下降,這點空間存儲壓力是徹底能夠承擔的。

此文已由做者受權騰訊雲+社區發佈,更多原文請點擊

搜索關注公衆號「雲加社區」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!

相關文章
相關標籤/搜索