1. TF-IDF原理
TF-IDF是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比,但同时会随着它在语料库中出现的频率成反比
- 词频TF(item frequency):某一给定词语在该文本中出现次数。该数字通常会被归一化,以防止它偏向长文本,因为不管该词语重要与否,它在长文件中出现的次数很可能比在短文本中出现的次数更大。
T F ( t ) = 单 词 t 在 文 章 中 出 现 次 数 该 文 章 的 总 词 数 TF(t)=\frac{单词t在文章中出现次数}{该文章的总词数} TF(t)=该文章的总词数单词t在文章中出现次数 - 逆向文件频率IDF(inverse document frequency):一个词语普遍重要性的度量。主要思想是:如果包含词条
t
t
t的文档越少, 则说明词条具有很好的类别区分能力,IDF越大。某一特定词语的IDF,可以由总文件数目除以包含该词语之文件的数目,再将得到的商取对数得到。
I D F ( t ) = l o g ( 语 料 库 文 章 总 数 包 含 单 词 t 的 文 章 数 + 1 ) IDF(t)=log\left(\frac{语料库文章总数}{包含单词t的文章数+1}\right) IDF(t)=log(包含单词t的文章数+1语料库文章总数)
所以可以得到:
T
F
−
I
D
F
(
t
)
=
T
F
(
t
)
∗
I
D
F
(
t
)
TF-IDF(t)= TF(t)*IDF(t)
TF−IDF(t)=TF(t)∗IDF(t)
可以看出TF-IDF与一个词在文档中出现的次数成正比,与该词在整个语料中该出现的次数成反比,TF-IDF倾向于过滤掉常见的词语,保留重要的词语
2. BM25原理介绍
BM25算法,通常用来作搜索相关性评分。一句话概况其主要思想:对Query进行语素解析,生成语素 q i q_i qi ;然后对于每个搜索结果D,计算每个语素 q i q_i qi与D的相关性得分,最后将 q i q_i qi相对于D的相关性得分进行加权求和,从而得到Query与D的相关性得分。
BM25算法的一般性公式如下:
Score
(
Q
,
d
)
=
∑
i
n
W
i
⋅
R
(
q
i
,
d
)
\operatorname{Score}(Q, d)=\sum_{i}^{n} W_{i} \cdot R\left(q_{i}, d\right)
Score(Q,d)=i∑nWi⋅R(qi,d)其中,Q表示Query,
q
i
q_i
qi表示Q解析之后的一个语素(对中文而言,我们可以把对Query的分词作为语素分析,每个词看成语素
q
i
q_i
qi);d表示一个搜索结果文档;
W
i
W_i
Wi表示语素
q
i
q_i
qi的权重;
R
(
q
i
,
d
)
R(q_i, d)
R(qi,d)表示语素
q
i
q_i
qi与文档d的相关性得分。
下面我们来看如何定义
W
i
W_i
Wi。判断一个词与一个文档的相关性的权重,方法有多种,较常用的是IDF。这里以IDF为例,公式如下:
I
D
F
(
q
i
)
=
log
N
−
n
(
q
i
)
+
0.5
n
(
q
i
)
+
0.5
I D F\left(q_{i}\right)=\log \frac{N-n\left(q_{i}\right)+0.5}{n\left(q_{i}\right)+0.5}
IDF(qi)=logn(qi)+0.5N−n(qi)+0.5
其中,N为全部文档数,
n
(
q
i
)
n(q_i)
n(qi)为包含了单词
q
i
q_i
qi 的文档数。
根据IDF的定义可以看出,对于给定的文档集合,包含单词 q i q_i qi的文档数越多, q i q_i qi的权重则越低。也就是说,当很多文档都包含了 q i q_i qi时, q i q_i qi的区分度就不高,因此使用 q i q_i qi来判断相关性时的重要度就较低
再来看语素
q
i
q_i
qi 与文档d的相关性得分
R
(
q
i
,
d
)
R(q_i, d)
R(qi,d)。首先来看BM25中相关性得分的一般形式:
R
(
q
i
,
d
)
=
f
i
⋅
(
k
1
+
1
)
f
i
+
K
⋅
q
f
i
⋅
(
k
2
+
1
)
q
f
i
+
k
2
K
=
k
1
⋅
(
1
−
b
+
b
⋅
d
l
avg
(
d
l
)
)
\begin{gathered} R\left(q_{i}, d\right)=\frac{f_{i} \cdot\left(k_{1}+1\right)}{f_{i}+K} \cdot \frac{q f_{i} \cdot\left(k_{2}+1\right)}{q f_{i}+k_{2}} \\\\ K=k_{1} \cdot\left(1-b+b \cdot \frac{d l}{\operatorname{avg}(d l)}\right) \end{gathered}
R(qi,d)=fi+Kfi⋅(k1+1)⋅qfi+k2qfi⋅(k2+1)K=k1⋅(1−b+b⋅avg(dl)dl)
其中,
k
1
,
k
2
,
b
k_1, k_2, b
k1,k2,b为调节因子,通常根据经验设置,一般
k
1
=
2
,
b
=
0.75
k1=2,b=0.75
k1=2,b=0.75;
f
i
f_i
fi为
q
i
q_i
qi在d中的出现的频次,
q
f
i
qf_i
qfi为
q
i
q_i
qi在Query中的出现频次。
d
l
dl
dl为文档d的长度,
a
v
g
d
l
avgdl
avgdl为所有文档的平均长度。由于绝大部分情况下,
q
i
q_i
qi在Query中只会出现一次,即
q
f
i
=
1
qf_i=1
qfi=1,因此公式可以简化为:
R
(
q
i
,
d
)
=
f
i
⋅
(
k
1
+
1
)
f
i
+
K
R\left(q_{i}, d\right)=\frac{f_{i} \cdot\left(k_{1}+1\right)}{f_{i}+K}
R(qi,d)=fi+Kfi⋅(k1+1)
从K的定义中可以看到,参数b的作用是调整文档长度对相关性影响的大小。b越大,文档长度的对相关性得分的影响越大,反之越小。而文档的相对长度越长,K值将越大,则相关性得分会越小。这可以理解为,当文档较长时,包含 q i q_i qi的机会越大,因此,同等 f i f_i fi的情况下,长文档与 q i q_i qi的相关性应该比短文档与 q i q_i qi的相关性弱。
从BM25的公式可以看到,通过使用不同的语素分析方法、语素权重判定方法,以及语素与文档的相关性判定方法,我们可以衍生出不同的搜索相关性得分计算方法,这就为我们设计算法提供了较大的灵活性。更多bm25算法的变体见论文 Trotmam et al, Improvements to BM25 and Language Models Examined
3. BM25的简单实现
下面的实现是为了能更好的理解上文中的公式,更多bm25及其实现可参考 ddupfun/rank_bm25
import math
import jieba
from nltk import corpus
class BM25(object):
def __init__(self, docs):
self.D = len(docs)
self.avgdl = sum([len(doc)+0.0 for doc in docs]) / self.D
self.docs = docs
self.f = [] # 列表的每一个元素是一个dict,dict存储着一个文档中每个词的出现次数
self.df = {} # 存储每个词及出现了该词的文档数量
self.idf = {} # 存储每个词的idf值
self.k1 = 1.5
self.b = 0.75
self.init()
def init(self):
for doc in self.docs:
tmp = {}
for word in doc:
tmp[word] = tmp.get(word, 0) + 1 # 存储每个文档中每个词的出现次数
self.f.append(tmp)
for k in tmp.keys():
self.df[k] = self.df.get(k, 0) + 1
for k, v in self.df.items():
self.idf[k] = math.log(self.D-v+0.5)-math.log(v+0.5)
def sim(self, doc, index):
score = 0
for word in doc:
if word not in self.f[index]:
continue
d = len(self.docs[index])
score += (self.idf[word]*self.f[index][word]*(self.k1+1)
/ (self.f[index][word]+self.k1*(1-self.b+self.b*d
/ self.avgdl)))
return score
def simall(self, doc):
scores = []
for index in range(self.D):
score = self.sim(doc, index)
scores.append(score)
return scores
if __name__ == '__main__':
corpus = ["自然语言处理是计算机科学领域与人工智能领域中的一个重要方向。",
"它研究能实现人与计算机之间用自然语言进行有效通信的各种理论和方法。",
"自然语言处理是一门融语言学、计算机科学、数学于一体的科学。",
"因此,这一领域的研究将涉及自然语言,即人们日常使用的语言",
"所以它与语言学的研究有着密切的联系,但又有重要的区别。",
"自然语言处理并不是一般地研究自然语言",
"而在于研制能有效地实现自然语言通信的计算机系统",
"特别是其中的软件系统。因而它是计算机科学的一部分。"
]
doc = []
for sent in corpus:
words = list(jieba.cut(sent))
doc.append(words)
s = BM25(doc)
print("相似度:", s.simall(['自然语言', '计算机科学', '领域', '人工智能', '领域']))
输出:
s.f : 每个字的频次
列表的每一个元素是一个dict,dict存储着一个文档中每个词的出现次数
[{'中': 1, '计算机科学': 1, '领域': 2, '一个': 1, '人工智能': 1, '方向': 1, '自然语言': 1},
{'之间': 1, '方法': 1, '理论': 1, '通信': 1, '计算机': 1, '人': 1, '研究': 1, '自然语言': 1},
{'融': 1, '一门': 1, '一体': 1, '数学': 1, '科学': 1, '计算机科学': 1, '语言学': 1, '自然语言': 1},
{},
{'领域': 1, '这一': 1, '涉及': 1, '研究': 1, '自然语言': 1},
{'日常': 1, '语言': 1},
{'语言学': 1, '研究': 1},
{'区别': 1},
{'研究': 1, '自然语言': 2},
{'通信': 1, '计算机系统': 1, '研制': 1, '在于': 1, '自然语言': 1},
{'软件系统': 1, '特别': 1},
{'一部分': 1, '计算机科学': 1}]
s.df
存储每个词及出现了该词的文档数量
{
'在于': 1,
'人工智能': 1,
'语言': 1,
'领域': 2,
'融': 1,
'日常': 1,
'人': 1,
'这一': 1,
'软件系统': 1,
'特别': 1,
'数学': 1,
'通信': 2,
'区别': 1,
'之间': 1,
'计算机科学': 3,
'科学': 1,
'一体': 1,
'方向': 1,
...
}
s.idf
存储每个词的idf值
{
'在于': 2.0368819272610397,
'一部分': 2.0368819272610397,
'一个': 2.0368819272610397,
'语言': 2.0368819272610397,
'领域': 1.4350845252893225,
'融': 2.0368819272610397,
'日常': 2.0368819272610397,
'人': 2.0368819272610397,
'这一': 2.0368819272610397,
'软件系统': 2.0368819272610397,
'特别': 2.0368819272610397,
'数学': 2.0368819272610397,
'通信': 1.4350845252893225,
'区别': 2.0368819272610397,
'之间': 2.0368819272610397,
'一门': 2.0368819272610397,
'科学': 2.0368819272610397,
...
}
- 最后print输出相似度如下
相似度: [3.8602268757499467, -0.830879517415162, -0.49187661205157857, 0.9054721922953262, 0, -1.589034350592673, -1.064792812640545, 0.4705598548557856]