Lucene打分公式詳解(TFIDFSimilarity)

本文章基於Lucene5.5,對其默認使用的打分公式(TFIDFSimilarity)進行解析

一、餘弦相似度算法

由於網絡上有很多關於VSM(向量空間模型)的解釋,這裏就不花費篇章做基本理論的描述了,只總結一下算法即可。給定兩個文本,按照餘弦相似度算法進行相似度度量一般需要以下步驟:

1、提取詞條(term)進行向量化。也就是將文本進行分詞,每個不同的詞都表示爲一個term(也就是維度)。

2、處理詞條。比如去除“無用”的詞,像標點符號、常用語氣詞等沒有太大“意義”的詞,一般我們把這種詞叫做停用詞(stop word),在實際應用中一般都是可以配置的,直接將其忽略;大小寫轉化之類的操作。

3、整理詞條集合。詞條集合應該爲兩個文本分詞集合(除去停用詞)的並集。

4、計算權重。簡單的做法就是以詞頻作爲權重:該詞條在文本分詞集合中出現了幾次,權重就是幾(當然Lucene中不是這麼簡單的計算權重的哈)。

5、根據公式計算餘弦值(公式這裏就不解釋了哈後面會結合Lucene的實現詳細說明):

6、餘弦值的大小就表徵了相似性或相異性。

注:我們知道餘弦函數的取值範圍爲:[-1,1]。而我們進行文本相似度度量時,常用於正空間,因此範圍應該是:[0,1],0表示“完全不同”,1表示“完全相同”。

現在按照上面的步驟我們舉例子進行說明,提供兩個文本:

文本1:我今天去打籃球,然後去打乒乓球。

文本2:我明天去吃牛排,然後去打籃球。

1、我們先使用分詞器將兩個文本進行詞條提取,也就是分詞(分詞算法本身並不死板,需要的話可以自己實現想要的算法),比如分詞的結果如下:

分詞1:我  今天  去  打  籃球  ,  然後  去  打  乒乓球  。

分詞2:我  明天  去  吃  牛排  ,  然後  去  打  籃球  。

2、處理詞條,去除停用詞(假設只有標點爲停用詞):

分詞1:我  今天  去  打  籃球  然後  去  打  乒乓球

分詞2:我  明天  去  吃  牛排  然後  去  打  籃球

3、詞條集合爲上述分詞集合的並集(假設已經做了排序):

詞條集合:我  今天  明天  去  打  吃  牛排  然後  乒乓球  籃球 

4、計算權重,我們以詞頻作爲權重,結合第三步的特徵集合和第二步的分詞集合:

  今天 明天 牛排 然後 乒乓球 籃球
weight1 1 1 0 2 2 0 0 1 1 1
weight2 1 0 1 2 1 1 1 1 0 1

5、計算餘弦值。現在我們得到了向量:

          A=(1,1,0,2,2,0,0,1,1,1)

          B=(1,0,1,2,1,1,1,1,0,1)

所以,\cos\theta =\frac{\sum_{i=1}^{n}{A_i}\times {B_i}}{\sqrt{\sum_{i=1}^{n}{A_i} ^{2}} \times\sqrt{\sum_{i=1}^{n}{B_i} ^{2}} }

                    =\frac{1\times1 + 1\times0 + 0\times1+2\times2+2\times1+0\times1+0\times1+1\times1+1\times0+1\times1}{\sqrt{1^{2}+1^{2}+0^{2}+2^{2}+2^{2}+0^{2}+0^{2}+1^{2}+1^{2}+1^{2}}\times\sqrt{1^{2}+0^{2}+1^{2}+2^{2}+1^{2}+1^{2}+1^{2}+1^{2}+0^{2}+1^{2}}}

                    =\frac{9}{\sqrt{13}\times\sqrt{11}}

                    \approx 0.75

所以我們能得出結論,這兩文本有75%的相似程度,這就是餘弦相似度的簡單表現形式,Lucene在運用的時候根據實際場景做出了自己的優化。

二、TF-IDF

上面我們提到了餘弦相似度算法,其中有個很關鍵的一個步驟:計算權重。在我們的例子中,權重是簡單的以詞頻來度量的。但是很多時候,我們不會簡單的將詞頻直接當做權重來使用。TF-IDF就是一種常用的加權算法,下面我們簡單的介紹一下。

TF: 詞頻(Term Frequency),表示詞條在文檔d中出現的頻率,這個比較簡單。

IDF: 逆文本頻率指數(Inverse Document Frequency),它表徵一個詞條的“普遍重要性”,也就是:在整個文檔庫中,包含該詞的文檔數量越多,則表示這個詞越“普遍”,也就是越不“重要”。比如讓你描述一個人的長相,我們根據你的描述去找人。如果你說:這個人有一張嘴巴,那麼對於我們來說則相當於沒有說,因爲有一張嘴巴是一個很普遍的“特徵”,正常人都是這樣。但是如果你說:這個人右手有6根手指,那麼這就能爲我們提供很重要的線索,因爲這個特徵不那麼普遍。

IDF可以由文檔庫中的總文檔數(numDocs)除以包含該詞條的文檔數量(docFreq),再將得到的商取以10爲底的對數得到,即:

                                                                 IDF=lg{\frac{numDocs}{docFreq}}

但是考慮到一個詞條可能並沒有出現在文檔庫中,這時候docFreq爲0,而0是不能作爲分母的。所以我們通常使用 1+docFreq作爲分母,即:

                                                                IDF=lg{\frac{numDocs}{docFreq+1}}

TF-IDF最終的計算公式即:

                                                                   TF-IDF=TF * IDF

三、TFIDFSimilarity的概念評分公式

注:接下來的公式展示是通過CSDN的公式編輯器編輯的,其中的“-”符號看起來可能會更像減號。同時我們設查詢爲:q,文檔爲:d。

Lucene在運用VSM的時候,就採用TF-IDF進行加權,同時在細節上又做出了一些自己的“優化”和“改進”,這些點在不同的版本可能會有些許差異,我們接下來按照5.5的概念評分公式來做解析(官網地址)。請注意,這個公式僅僅是概念評分公式,實際使用的公式會在後面解析:

再解釋上述公式之前,我們先來看“正常”的餘弦相似度計算:

                                                    cosine-similarity(q,d)=\frac{V(q) \cdot V(d)}{|V(q)| \times |V(d)|}

根據向量點積的含義,該公式其實可以看作是帶權向量歸一化之後的點積,結果是標量,表徵的兩個向量夾角的餘弦。將其歸一化之後其實夾角是不變的,比如在二維中的以下向量,aba'b':

1、doc-len-norm(d)

在上面的提到的公式中,站在這個角度看的話,V(d)其實最終通過除以其模長被歸一化爲了單位向量。但是我們知道,將向量歸一化爲單位向量之後,其實是已經消除了其長度的屬性。在一些情況下,這沒有什麼不妥,比如某個文檔是由一個段落複製N次形成的,而且該段落由不同的詞條組成。但是對於那些不包含重複段落的文檔,文檔長度本來是會影響最終相似度得分的,但是我們消除了長度影響,這明顯就不那麼靠譜了,所以我們不能單純的將V(d)歸一化爲單位矢量。

Lucene爲了避免這個問題,引入了一個新的文檔長度歸一化因子doc-len-norm(d),將其歸一化爲大於或等於單位向量的結果(也是一個向量)。

所以公式成了(|V(d)|也就不需要留在這裏了):

                                               score(q,d) =\frac{V(q) \cdot V(d)}{|V(q)|} \times doc-len-norm(d)

2、doc-boost(d)

同時,我們可能會認爲文檔庫中的一些文檔相對於其它文檔來說是“更加重要”的。基於這個需求,Lucene爲我們提供了一個手段來提升這些文檔的得分,這個手段就是:doc-boost(d)。有了它之後,每個文檔的得分會和他相乘,在索引時指定(但是現在已經不支持直接給Document設置boost了)。而Document是基於Field的,可以給每個Field都設置boost。這樣我們就能影響得分了。加入doc-boost(d)之後的公式:

                               score(q,d) = \frac{V(q) \cdot V(d)}{|V(q)|} \times doc-len-norm(d) \times doc-boost(d)

3、query-boost(q)

另外,對於每個查詢(query)、子查詢(subQuery)以及查詢的每個詞條(term),Lucene都加入了分數“提升因子”,作用“類似於”前面提到的doc-boost(d)(一個針對於document,一個針對query)。它可以設定指定項的“重要性”,影響最終得分。這個“提升因子”就是:query-boost(q)。所以我們的公式中需要加入它:

                                    score(q,d) =query-boost(q) \times \frac{V(q) \cdot V(d)}{|V(q)|} \times doc-len-norm(d) \times doc-boost(d)

4、coord-factor(q,d)

在某些情況下,一些文檔可能會匹配一個複合查詢,但是該文檔又不包含該查詢的所有詞項(雖然我沒有遇到過),可以使用一個“協調因子”,同時我們可以通過這個“協調因子”提升匹配到更多查詢詞項的文檔的得分。這個協調因子就是:coord-factor(q,d)。通常情況下,q對應d匹配的詞項越多,其值也就越大。最終就得出了TFIDFSimilarity的概念評分公式:

                        score(q,d) =coord-factor(q,d) \times query-boost(q) \times \frac{V(q) \cdot V(d)}{|V(q)|} \times doc-len-norm(d) \times doc-boost(d)

四、TFIDFSimilarity的實際使用評分公式

上面我們提到了TFIDFSimilarity的概念評分公式,而實際上使用的公式其實就是上面概念模型的一個實踐。這裏還是先給出公式,然後我們再看Lucene是如何“實踐”的,爲了方便比較,我們這裏同時貼出概念公式。

概念公式:

實際使用公式:

上面兩張圖都來自於官網,而官網也比較貼心,爲了方便我們比較,使用不同的顏色對公式中的各個部分做了對應,讓我們能夠有個比較直觀的瞭解。

對於查詢:q和文檔:d,假設他們的帶權向量爲:

\vec{q}=(q_1,q_2,q_3,...,q_n)

\vec{d}=(d_1,d_2,d_3,...,d_n)

其中q_nd_n分別爲對應維度(這裏也就是term,參考本文第一小節的內容)的權重,我們上面也說了,權重是通過TF-IDF計算的,所以有:

q_n=tf(t_n \ in \ q) \times idf(t_n)

d_n=tf(t_n \ in\ d)\times idf(t_n)

下面解釋公式中的各個部分

1、V(q) \cdot V(d)

將上面的定義帶入公式得:

V(q) \cdot V(d)

= \sum_{t\ in\ q}{\vec{q} \cdot \vec{d}}

= \sum_{t\ in\ q}{(tf(t \ in \ q) \times idf(t) \times tf(t \ in\ d)\times idf(t))}

= \sum_{t\ in\ q}{(tf(t \ in \ q) \times tf(t \ in\ d)\times idf(t)^2)}

但是在用Lucene作查詢的時候,tf(t \ in \ q)都默認認爲是 1 的,也就是認爲查詢語句的每個詞項在查詢中只出現一次。但是如果出現了多次呢?比如我們提供個給Lucene的查詢詞項列表爲:晚上 晚上 晚上 晚上。我們發現,“晚上”這個詞出現了四次。實際上,這時候會產生四個相同詞項的打分,顯然結果也是正確的,雖然這樣做效率並不高,所以通常情況下,我們在查詢的時候都會對分詞結果進行“去重”(注:如果查詢語句包含多個不同的詞項,部分重複的詞項是可能會“影響”到最終的得分排序的,後面分析源碼的時候會特別說明)。

有了這個理論,tf(t \ in \ q)==1,可以在公式中直接拿掉,就成了:

                                                            \sum_{t\ in\ q}{tf(t \ in\ d)\times idf(t)^2)}

Lucene是如何計算tf和idf的呢?

在上面的概念模型中我們已經提到了,tf表徵的是詞在文檔中出現頻率,頻率越高則表示該詞越重要,默認的計算公式如下:

                                                          tf(t \ in \ q) = \sqrt{frequency}

idf表示逆文檔頻率,頻率越低則表示該詞越重要,默認的計算公式如下(我們發現idf公式在這裏有了變種):

                                                        idf(t)=1+log(\frac{docCount}{docFreq+1})

2、|V(q)|queryNorm(q)

概念模型中的 |V(q)|是獨立於當前參與評分的文檔的,所以可以在搜索開始的時候就計算好。而且它並不會影響到文檔的最終排序,因爲所有的得分都會和該參數進行運算。爲什麼要使用它呢?Lucene給出的解釋如下:

  • Recall that Cosine Similarity can be used find how similar two documents are. One can use Lucene for e.g. clustering, and use a document as a query to compute its similarity to other documents. In this use case it is important that the score of document d3 for query d1 is comparable to the score of document d3 for query d2. In other words, scores of a document for two distinct queries should be comparable. There are other applications that may require this. And this is exactly what normalizing the query vector V(q) provides: comparability (to a certain extent) of two or more queries.
  • Applying query normalization on the scores helps to keep the scores around the unit vector, hence preventing loss of score data because of floating point precision limitations.

Lucene在實現的時候,將其抽象爲一個歸一化因子,也就是 queryNorm(q),而Lucene的默認實現也是使用的歐式範數

這個sumOfSquaredWeights是由具體的Weight(每個term都會有一個Weight,源碼解析的時候會詳細解釋)計算的。不同的Weight可以有不同的實現。

3、coord-factor(q,d)coord(q,d)

概念公式中的coord-factor(q,d)在實際公式中的表現就是:coord(q,d)。它的計算公式如下:
                                                                             coord(q,d)=\frac{overlap}{maxOverlap}

其中,overlap表示:文檔中匹配的詞條的數量;maxOverlap表示:查詢中所有詞條的數量。所以公式成了:

                                        score(q,d)=coord(q,d)\times queryBoost(q) \times \sum_{t\ in\ q}{(tf(t \ in\ d)\times idf(t)^2)}\times doc-len-norm(d) \times doc-boost(d)

4、query-boostt.getBoost

query-boost(q)我們前面已經提過了,它用於影響“詞條”的重要性,所以它應該反映到每個term上,所以有:

                     score(q,d)=coord(q,d)\times \sum_{t\ in\ q}{(tf(t \ in\ d)\times idf(t)^2\times t.getBoost())} \times doc-len-norm(d) \times doc-boost(d)

5、doc-boost(d)doc-len-norm(d)

上面提到doc-boost(d)doc-len-norm(d)是和文檔(Document)相關的。在實際應用中,他們會參與到每個詞條(term)的計算中。Lucene將他們進行了封裝,封裝的結果就是norm(t,d)(上面已經提到過,現在Document不支持直接設置boost)。

這裏要引入一個Field Boost。Lucene將文檔中出現的該field的boost相乘:
                                                     fieldBoost=\prod_{field\ f\ in\ d\ named\ as\ t} {f.boost()}

在概念公式中,doc-len-norm(d)作爲文檔長度歸一化因子,在實際公式中體現爲lengthNorm,在文檔添加到索引中的時候根據該字段在文檔中出現次數計算,默認的計算方式如下:

                                    lengthNorm=f.getBoost() \times \frac{1}{\sqrt{numTerms}}

其中,f.getBoost()表示doc-boost和所有具有相同字段(Field)名稱的字段Boost的乘積;numTerms則表示該Field的詞條數量。所以有:

                                norm(t,d)=lengthNorm\times \prod_{field\ f\ in\ d\ named\ as\ t} {f.boost()}

經過我們上面的步驟,我們就得出了最終的實際公式:

score(q,d)=coord(q,d)\times \sum_{t\ in\ q}{(tf(t \ in\ d)\times idf(t)^2\times t.getBoost()\times norm(t,d))}

由於這篇文章內容已經比較多了,關於源碼的解析另開博文總結。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章