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))}

由于这篇文章内容已经比较多了,关于源码的解析另开博文总结。

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