Lucene TFIDFSimilarity评分公式详解

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zteny/article/details/57366074

一、预热

TFIDFSimilarity曾经是Lucene/Solr默认评分公式,但是从lucene-6.0开始已经改成BM25Similary了(详见Lucene-6789)。但我们今天看的依然是TFIDFSimilarity,因为它相对简单一些,对我们理解评分过程有好处。

首先假定你知道怎么把一篇文档转化成一个空间向量,并且知道空间向量模型。

接下来先来统一一下术语和记号
q : query,表示一个查询
d : document,表示一篇文档

V(q) : q vec{q} 表示Query的向量
V(d) : d vec{d} 表示Document的向量
|V(q)| : q |vec{q}| 表示Query向量的归一化
|V(d)| : d |vec{d}| 表示Document向量的归一化

在看TFIDFSimilarity之前,我们先看简单复习几个简单的公式。

  1. 余弦定理
    cosine_similarity(q,d)=V(q)V(q)V(q)×V(d) cosine\_similarity(q,d) = frac{V(q) · V(q)}{|V(q)| imes |V(d)|},用余弦定理通过计算两向量的夹角来表示两文本的相似,这是一切的基础。

这里沿*Lucene Docs的写法,cosine_similarity(q,d) cosine\_similarity(q,d)而不是用score(q,d) score(q,d)的原因是相似度不是每个最终得分,相似度只是评分过程比较重要的一个因素而已。*建议你还是先看看TFIDFSimilarty的官方文档,它讲得相当完整也很细**。

  1. tf-idf公式
    tf-idf算法是一种非常常见算法,用来计算文本每个权重的。
    tf-idf算法的原理如果词条在文档出频率越高,则词条权重越高;如词条在越多篇文档出现,而词条的权重越低。具体计算如下:
    tfidf(t)=tf(t)idf(t) tfidf(t) = tf(t) * idf(t)
    tf(t)=frequency tf(t) = sqrt{frequency}
    idf(t)=1+logdoc_count+1doc_freq+1 idf(t) = 1 + log{frac{doc\_count+1}{doc\_freq+1}}
    tfidf(t)=frequency×(1+logdoc_count+1doc_freq+1) tfidf(t) = sqrt{frequency} imes (1 + log{frac{doc\_count+1}{doc\_freq+1}})

对于VSM而言,tf-idf算法并不是必须,甚至权重的引入也不是必须。也就是只需要把每个词转化为一个数值即可,可以用词条的HashCode、词包的下标等等。
当然tf-idf算法也不是计算权重的唯一算法,比tf-idf效果更的还有BM25(BM25Similarity)。
故名思义,TFIDFSimilarity即是用TFIDF作为VSM向量的权重来计算相似度的打分器。

二、开始

原想从两个字符串的相似计算开始来推导我们Lucene的评分公式的,但这样的话篇幅太长太啰嗦太复杂。因此选择从Lucene的公式出发来看全公式每个细节的含义,一步步变化和计算最终推导出实践公式。

2.1 理论公式

我们先看一下,Lucene TFIDFSimilarity给出的理论评分公式:
score(q,d)=coord_factor(q,d)×query_boost(q)×V(q)V(d)V(q)×doc_len_norm(d)×doc_boost(d) score(q,d) = coord\_factor(q,d) imes query\_boost(q) imes frac{V(q)·V(d)}{|V(q)|} imes doc\_len\_norm(d) imes doc\_boost(d)

为什么说它是理论公式呢,因为它由VSM直接推导出来的公式。然则它在实际运用上并不是很方便和高效,因此我们对它进行一系列的变转使得它的计算得到一个简化的式子。

2.1.1 细说相似度部分

前面提到过Lucene相似度是通过VSM来计算(当然*TFIDFSimilarty的官方文档*也是提及的),相似度similarity通过如下公式计算的:cosine_similarity(q,d)=V(q)V(q)V(q)×V(d) cosine\_similarity(q,d) = frac{V(q) · V(q)}{|V(q)| imes |V(d)|}

a. 如何计算俩向量的内积

由于我们研究的是TFIDFSimilarity的评分公式,我们知道TFIDFSimilarity评分过程是采用了tf-idf算法作为向量的权重(weight)。
因此有
q=(w1,w2,...,wn) vec{q}=(w_1, w_2, ..., w_n),且wi=tf(ti)×idf(ti,q) w_i=tf(t_i) imes idf(t_i, q);
d=(d1,d2,...,dn) vec{d}=(d_1, d_2, ..., d_n),且di=tf(ti)×idf(ti,d) d_i=tf(t_i) imes idf(t_i, d)

通常来说每个Query的每个词条的出现次数都是1,因此 tf(t1)=tf(t2)=...=tf(ti)=a,a(0,1] tf(t_1) = tf(t_2) = ... = tf(t_i) = a, ain(0, 1]
V(q)V(d)=qd=t in q(wt×dt) V(q)·V(d) = vec{q} · vec{d} = sum_{t in q}{(w_t imes d_t)}
=t in q(tf(t,q)×idf(t,q)×tf(t,d)×idf(t,d)) = sum_{t in q}{(tf(t,q) imes idf(t,q) imes tf(t,d) imes idf(t,d))}
=tf(t,q)×t in qidf(t,q)×tf(t,d)×idf(t,d)) = tf(t,q) imes sum_{t in q}{idf(t,q) imes tf(t,d) imes idf(t,d))}
=a×t in qidf(t,q)×tf(t,d)×idf(t,d)) = a imes sum_{t in q}{idf(t,q) imes tf(t,d) imes idf(t,d))}

由上式易知a对同一个Query而言是一个常量,因此上式可以进一步简化为
V(q)V(d)=t in qtf(t,d)×idf(t,d)×idf(t,q) V(q)·V(d) = sum_{t in q}{tf(t,d) imes idf(t,d) imes idf(t,q)}

那么又是怎么计算Queryidf(t,q) idf(t,q)呢?
已知道idf(t,d)=1+logdoc_count+1doc_freq+1 idf(t,d) = 1 + log{frac{doc\_count+1}{doc\_freq+1}},同时我们可以把Query也当作一篇文档。因此对于idf(t,q) idf(t,q)doc_countq=doc_countd+1;doc_freqq=doc_freqd+1 doc\_count_q=doc\_count_d + 1; doc\_freq_q = doc\_freq_d+1(即直接把原文档总数以及对应的文档频率都加上1)。整理可得
idf(t,q)=1+log1+(doc_count+1)1+(doc_freq+1) idf(t,q) = 1 + log{frac{1 + (doc\_count+1)}{1 + (doc\_freq+1)}}
当文档数量比较大的时候,我们可以认为idf(t,q)=idf(t,d) idf(t,q) = idf(t,d),因此上式可以进一步简化为
V(q)V(d)=t in qtf(t,d)×idf2(t,q)) V(q)·V(d) = sum_{t in q}{tf(t,d) imes idf^2(t,q))}

中间的计算过程不能理解也没关系,你只需要知道并记住最终结果就可以了。

b. 为什么|V(d)|不见了

先看一下Lucene Docs对V(d) |V(d)|的解释吧。

Normalizing V(d) to the unit vector is known to be problematic in that it removes all document length information. For some documents removing this info is probably ok, e.g. a document made by duplicating a certain paragraph 10 times, especially if that paragraph is made of distinct terms. But for a document which contains no duplicated paragraphs, this might be wrong. To avoid this problem, a different document length normalization factor is used, which normalizes to a vector equal to or larger than the unit vector: doc-len-norm(d).

大意是说|V(d)|是一个单位向量,因此它并不带文档的长度信息。如果文档是由一段文本重复十次组成的,尤其这段文本由完全不一样词条组成时,文档长度信息对于这类文档可能并不会有影响。但是文档由多段不一样文本构成的,那么文档长度信息可能会有影响。为此,使用不同文档长度归一化因子,可实现一个向量大于或等于它的单位向量doc-len-norm(d)。

我猜你没看懂。简单说,如你所知文档长度信息是会影响相似度,即文档长度越大可能更有优势。为了解决这个问题我们引用**doc-len-norm(d)**来代替V(d) |V(d)|

到此为止,并没有解释V(d) |V(d)|为什么可以不要,只是说明了doc-len-norm(d)的作用。

至于为什么不要V(d) |V(d)|呢?虽然V(d) |V(d)|是文档归一化因子,理论上它是可以评价文档的重要性的。但是它计算比较复杂(又有开文又有平方),关键还没什么用。下面是V(d) |V(d)|的计算公式:
V(d)=t in q(tf(t,d)×idf(t,d))2 |V(d)| = sqrt{sum_{t in q}{(tf(t,d) imes idf(t,d))^2}}

上式易知它的计算复杂同时并没有涉及文档的长度信息,因此Lucene选择另一种方式来表示,它能把文档的长度信息考虑进来,这就是我们前面提到doc-len-norm(d)。

c. 什么是 doc-len-norm(d)

故名思义,它是文档长度归一化因子,即是弱化文档长度对最终评分的影响。那我们怎么在搜索过程引用文档的长度对评分的影响呢? 首先lucene文档是可以有多个字段,而且搜索时被指定字段. 由此可以用搜索时指定搜索字段f f的归一化因子来表示文档的归一化因子,所以就把问题转化算字段f f的归一化因子的问题了。设norm(t,d) norm(t,d)表示文档d的搜索字段f f的归一化因子。

doc_len_norm(d)=t in qnorm(t,d) doc\_len\_norm(d) = sum_{t in q}{norm(t,d)}

用什么来表示norm(t,d) norm(t,d)呢?Lucene用了下面的式子来表示:
norm(t,d)=lengthNormf in df.getBoost() norm(t,d) = lengthNorm prod_{f in d}{f.getBoost()}
lengthNorm=1length(f)numOverlap(f) lengthNorm = frac{1}{sqrt{length(f) - numOverlap(f)}}

length : 字段的所有词条的数量
numOverlap : 字段的所有词条中出现过2次及以上词条的数量

lengthNorm是索引时计算好跟其它字段信息一样存储的,因此在搜索时它的“计算”是比较高效的。同时它并不是所有字段类型都会存储的,例如数值类型就不会计算和存储。当然对文本类型也可以关闭norm的计算,同时如果开启norm的计算的话,还能选择是否压缩。为了减小索引的体积,默认情况下是带用压缩的。

这里lengthNorm只是一般情况下的计算公式,并非所有情况都是这个公式

说到这里,我们顺便把norm(d)编码和解码两过程出现误差的情况解释一下吧。
其实前面已经解释过了,即是lucene为了减小索引的体积,对norm进行压缩。即是把原本应该用float表示norm数值,压缩成一个byte造成精度缺失,这就是decodeNormValue(encodeNormValue(norm))不一定等于norm的原因。

Lucene docs对LengthNorm的说明是这样的:

computed when the document is added to the index in accordance with the number of tokens of this field in the document, so that shorter fields contribute more to the score. LengthNorm is computed by the Similarity class in effect at indexing.

d. |V(q)|是什么呢

细心的你可能已经知道了|V(q)|是怎么回事,对的没错它就是queryNorm的别名。看一下QueryNorm的公式
queryNorm(q)=1q.getBoost()2×t in q(idf(t,q)×t.getBoost())2 queryNorm(q)=frac{1}{sqrt{q.getBoost()^2 imes sum_{t in q}{(idf(t,q) imes t.getBoost())}^2}}

我们先假定完全不设boost,即先假定boost=1。因此原式可以变成queryNorm(q)=1t in qidf2(t,q) queryNorm(q)=frac{1}{sqrt{sum_{t in q}{idf^2(t,q)}}}
再回顾一下V(q) |V(q)|的公式,V(q)=1t in qidf2(t,q)×tf2(t,q) |V(q)|=frac{1}{sqrt{sum_{t in q}{idf^2(t,q) imes tf^2(t,q)}}},因为一般情况下Query中每个词出现的频率都是1。因此原式可简写成V(q)=1t in qidf2(t,q) |V(q)|=frac{1}{sqrt{sum_{t in q}{idf^2(t,q)}}},对于这点我们前面解释过了。然后再把各种boost考虑进去,你会发现V(q) |V(q)|的式子跟queryNorm(q) queryNorm(q)一模一样。

我们知道打分是对文档打分,因此是每个被检索到的文档都要套这个公式来计算它的分值。简而言之,评分公式是文档级别的。
  • 1

它使得两个不同Queries的搜索结果的每个文档的得分可以比较,可以比较意思是它们的比较是有意义的。

QueryNorm不影响排名,也就是不影响评分,就是对于同一个Query来说queryNorm是唯一的。好吧,这说法并不准确,应该是对于同一个Index的同一个Query它的queryNorm是唯一的。如果不能理解就看公式吧,作为查询归一化,那么它作用就是缩小同一个Query在不同Index上产生的影响。这使得分布式查询(同一个Query用在不同的Index上并计算文档的分数)的评分变得有意义和有可比性。

不影响排名,也就是不影响评分,有两层意思:

  1. 在单个Index而queryNorm是没意义,不会影响评分和排序。
  2. 对于多个Index,queryNorm实际上是会影响的评分和排序。只是这影响是降低不同的index之间在搜索时的影响,因此可以认为没有影响。

2.2 coord因子

coord相关性,overlap与maxoverlap比值 coord(q,d)=overlapmaxOverlap coord(q,d) = frac{overlap}{maxOverlap}
maxOverlap:是Query中有效的词条数
overlap:表示文档中出现词条数

2.3 boost的作用

boost的作用就是在索引时或搜索时,改变字段或者词条的重要,从而影响文档的最终得分。

  1. 两个时间点:搜索时和索引时,都能设置
  2. 四个级别:Query条件、文档、字段和词条

三、实践公式即将出现

我们先上面推导出来最终式子罗列一下:
V(q)V(d)=t in qtf(t,d)×idf2(t,q)) V(q)·V(d) = sum_{t in q}{tf(t,d) imes idf^2(t,q))}
doc_len_norm(d)=t in qnorm(t,d) doc\_len\_norm(d) = sum_{t in q}{norm(t,d)}
V(q)=queryNorm(q)=1q.getBoost()2×t in q(idf(t,q)×t.getBoost())2 |V(q)| = queryNorm(q) = frac{1}{sqrt{q.getBoost()^2 imes sum_{t in q}{(idf(t,q) imes t.getBoost())}^2}}

把上面三个式子代入下式
score(q,d)=coord(q,d)×queryNorm(q)×t in q(idf2(t,q)×tf(t,d))×doc_len_norm(d) score(q,d) = coord(q,d) imes queryNorm(q) imes sum_{t in q}{(idf^2(t,q) imes tf(t,d))} imes doc\_len\_norm(d)
整理可得
=coord(q,d)×queryNorm(q)×t in q(idf2(t,q)×tf(t,d)×norm(t,d)) = coord(q,d) imes queryNorm(q) imes sum_{t in q}{(idf^2(t,q) imes tf(t,d) imes norm(t,d))}

四、结语

之前在学习lucene的时候就想写这么一篇博客,但是一直没能实现。如今也是能花好几个小时才整理出来,当第一次把所有内容都罗列出来的时候,篇幅是那么那么的长。经过几番裁剪才如今这个样子,肯定是有一些地方没说明白的,若问题可以一起论,这么复杂的博文难免会出错误,若有发现错误望不吝指教

                                </div>

原文地址:https://blog.csdn.net/zteny/article/details/57366074

原文地址:https://www.cnblogs.com/jpfss/p/11395015.html