理论讲解
tf-idf可以帮助我们了解文档中每个词的重要程度。tf代表词频,表示当前文档每个词出现的次数;idf代表逆文档频率,表示在整个文档集中,该词在几个文档中出现过。其公式如下所示:
t
f
i
d
f
(
w
,
d
,
D
)
=
t
f
(
w
,
d
)
∗
i
d
f
(
w
,
D
)
tfidf(w, d, D) = tf(w, d) * idf(w, D)
tfidf(w,d,D)=tf(w,d)∗idf(w,D)
其中:
- tf(w, d) = (词w在文档d中出现的次数) / (文档d中所有词的总数)
- idf(w, D) = log(文档集D的文档总数 / 包含词w的文档数) + 1
如果只考虑tf,很多频繁出现并且无意义的词会变得很重要,加入idf后可以缓解这种情况,因为那些无意义的词通常会出现在多个文档中,idf值就会很小,相乘后总值会变小,但是tfidf通常还是会倾向于那些出现次数高的词。
注1:idf值后面加1是防止某个词在所有文档中都出现过,导致分子分母相等,log值为0,从而无法与tf值加权。
注2:有些idf计算公式会在分母处加1,以防止某个词出现次数为0而导致分母为0报错。在首次输入文档集计算idf值时,会首先遍历文档集,得到文档集出现过的词汇表,然后计算这些词的idf值,所以每个词至少在一个文档中出现过,不存在分母为0的情况发生。而建立好idf表之后,对新的文本计算tfidf向量的时候,这时候就有可能存在超出词表之外的情况,这时候就需要分母+1平滑处理了。但对于词表之外的词可以直接略过,或取一个常值代替idf值,视不同情况而定。(个人认为,请求指正)
注2补充:分母加1后,如果某个词在所有文档都出现过,那分母就会比分子大1,分子/分母<1,取log后会小于0,乘上tf值就无法反映出该词真实的重要程度了。
注3:有些idf计算公式会在分子和分母处同时加1,为了平滑idf值,看起来还较为合理。
sklearn库实现
引入sklearn的tfidf模块并初始化
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf_vec = TfidfVectorizer()
初始化时可以指定一些参数,这里只介绍一些常用并且关键的参数
- use_idf:默认为True,指定是否计算idf值,若为False,返回值只是tf值,若为True,返回的是tf * idf;
- smooth_idf:默认为True,平滑idf值,指定计算idf时是否需要分子分母同时加1;
- norm:默认为l2标准化,指定标准化的方式。tfidf计算后,会针对每个文档返回一个向量,向量的维度是整个文档集词汇表的大小,向量包含该文档中每个词的tfidf值,为了更好的计算不同文档之间的相似度,这里需要将其标准化。l2标准化代表每个值的平方和为1,若指定为l1则代表绝对值和为1,若指定为None则不进行标准化。
下面将sklearn的结果与手动计算的结果进行对比:
- 首先设置use_idf=False,不计算idf,并取消标准化
from sklearn.feature_extraction.text import TfidfVectorizer
corpus = [
"查下 明天 天气",
"查下 今天 天气",
]
tfidf_vec = TfidfVectorizer(use_idf=False, smooth_idf=False, norm=None)
tfidf_matrix = tfidf_vec.fit_transform(corpus)
print('输出每个单词对应的 id 值:', tfidf_vec.vocabulary_)
# 输出每个单词对应的 id 值: {'查下': 3, '明天': 2, '天气': 1, '今天': 0}
print('返回tf值:', tfidf_matrix.toarray())
# 返回tf值: [[0. 1. 1. 1.], [1. 1. 0. 1.]]
词汇表 | 文档1 | 文档2 |
---|---|---|
查看 | 1 | 1 |
明天 | 1 | 0 |
今天 | 0 | 1 |
天气 | 1 | 1 |
tfidf_matrix.toarray()返回的维度为(2,4),2代表有2个文档,4对应词汇表的大小,例如第一行[0, 1, 1, 1]代表文档1中没有出现 ‘今天’,‘查下’、‘明天’、'天气’各出现了一次。
这里返回的tf值并没有除以该文档的总词数,因为默认是需要标准化的,而不管除不除以该文档的总词数,标准化后的结果都是一样的。
将参数设置为norm='l2'后结果变为
返回tf值:
[[0. 0.57735027 0.57735027 0.57735027]
[0.57735027 0.57735027 0. 0.57735027]]
- 设置use_idf=True,将idf参与计算
from sklearn.feature_extraction.text import TfidfVectorizer
corpus = [
"查下 明天 天气",
"查下 今天 天气",
]
tfidf_vec = TfidfVectorizer(use_idf=True, smooth_idf=False, norm=None)
tfidf_matrix = tfidf_vec.fit_transform(corpus)
print('输出每个单词对应的 id 值:', tfidf_vec.vocabulary_)
# 输出每个单词对应的 id 值: {'查下': 3, '明天': 2, '天气': 1, '今天': 0}
print('返回idf值:', tfidf_vec.idf_)
# 返回idf值: [1.69314718 1. 1.69314718 1. ]
print('返回tf值:', tfidf_matrix.toarray())
# 返回tfidf值: [[0. 1. 1.69314718 1. ] [1.69314718 1. 0. 1. ]]
根据公式log(文档集D的文档总数 / 包含词w的文档数) + 1得到下表结果,这里log以e为底。
如 ‘查看’:ln(2/2) + 1=1;‘明天’:ln(2/1) + 1=1.693;
词汇表 | 出现文档次数 | idf |
---|---|---|
查看 | 2 | 1 |
明天 | 1 | 1.693 |
今天 | 1 | 1.693 |
天气 | 2 | 1 |
最后得到的tfidf结果也是两者相乘,若设置标准化,也同样会对每个文档的tfidf向量进行标准化。
将参数设置为norm='l2'后结果变为
返回tf值:
[[0. 0.45329466 0.76749457 0.45329466]
[0.76749457 0.45329466 0. 0.45329466]]
个人简易实现
这里首先遍历文档集建立词汇表,然后建立一个大小为(文档数,词汇表长度)的数组,用来存储各文档的tfidf值。首先遍历各文档得到tf值,然后计算每个词对应的idf值,将二者相乘得到文档的tfidf向量,最后再将其标准化,得到的输出与sklearn库中相同。
对于新文档,还可以用类中的方法快速计算出该文档的向量,以便后续计算相似度。
class Tfidf:
def __init__(self, documents, use_idf=True, norm='l2'):
self.documents = documents
self.vocab = defaultdict(int)
# 获取词表
# defaultdict(<class 'int'>, {'查下': 0, '明天': 1, '天气': 2, '今天': 3})
for text in documents:
for t in text.split():
self.vocab[t] = self.vocab.get(t, len(self.vocab))
# 获得每个文档的tf向量
# [[1. 1. 1. 0.]
# [1. 0. 1. 1.]]
self.docment_vector = np.zeros((len(documents), len(self.vocab)))
for i in range(len(documents)):
for word in documents[i].split():
self.docment_vector[i][self.vocab[word]] += 1
if use_idf:
self.idf = defaultdict(int)
# 计算所有词汇的idf值, idf值在不同文档中是相等的
self.idf_vector = np.zeros((len(self.vocab)))
for k in self.vocab.keys():
for document in documents:
if k in document.split():
self.idf[k] += 1
# 先计算出现在文档集中的次数, 再取log
# idf_vector: [1. 0. 1. 1.69314718]]
for k, v in self.idf.items():
self.idf[k] = np.log(len(documents) / v) + 1
self.idf_vector[self.vocab[k]] = self.idf[k]
# 得到最终的tf * idf
self.docment_vector *= self.idf_vector
if norm:
# 这里只实现了l2标准化
for i in range(len(self.docment_vector)):
norm = np.sqrt(sum(self.docment_vector[i] ** 2))
self.docment_vector[i] /= norm
def search_similar(self, text):
vec = np.zeros((len(self.vocab)))
# 这里略过了词表之外的词
for t in text.split():
if t in self.vocab:
vec[self.vocab[t]] += 1
vec *= self.idf_vector
vec /= np.sqrt(sum(vec ** 2))
return vec
corpus = [
"查下 明天 天气",
"查下 今天 天气",
]
target = '今天 天气 怎么样'
my_tfidf = Tfidf(corpus)
print('输出每个单词对应的 id 值:', my_tfidf.vocab)
# 输出每个单词对应的 id 值: defaultdict(<class 'int'>, {'查下': 0, '明天': 1, '天气': 2, '今天': 3})
print('返回idf值:', my_tfidf.idf)
# 返回idf值: defaultdict(<class 'int'>, {'查下': 1.0, '明天': 1.6931471805599454, '天气': 1.0, '今天': 1.6931471805599454})
print('返回各文档的向量:', my_tfidf.docment_vector)
# 返回各文档的向量: [[0.45329466 0.76749457 0.45329466 0. ] [0.45329466 0. 0.45329466 0.76749457]]
print('指定文本的向量:', my_tfidf.search_similar(target))
# 指定文本的向量: [0. 0. 0.50854232 0.861037 ]