上一篇文章讲了自己关于如何结合使用tfidf和embedding的一点想法。这篇文章就具体来讲讲怎么实现。
对于embedding来说,我们不在多做操作,直接调用接口获得每个单词的embedding向量就行了。主要是tfidf的获取方法。我们要获得每一个单词对应的tfidf值,然后对每一个句子形成一个tfidf值组成的向量。还需要着重考虑的是,我们使用的语料库体积是很大的问题。
网上搜索了一下,主要有三种方法计算tfidf值。使用sklearn的接口,gensim接口,python手写。
下面我们说一下每种方法的使用方式,并对每种方式举一个简单的例子帮助理解。然后针对我们的需要选择一种方式计算我们项目中使用的tfidf值。
因为tfidf的原理很简单,公式不复杂,所以我们可以选择自己根据公式直接计算。原理可以参考这篇博客。
这种自己手写的优势就是灵活性很高,你想要什么样的数据形式都可以相对轻松的得到。所以一开始我使用的方法也是自己手写。但是在实际操作中,自己手写的方法耗时非常多,相比于sklearn接口直接调用,慢了有三倍,这个速度是不能接受的。,所以我放弃了这个方法。
其实在手写之前就已经意识到速度会很慢了,因为在计算的时候会计算单词在全部字典中的出现频率,字典很大的情况下速度肯定不高。仍然选择尝试一下的原因就是灵活度的问题,调用接口获得数据格式不是我想要的。没办法,只能选择尝试使用接口的方法,然后将接口获得的数据进行改变了。
下面写手写计算tfidf值的代码实现:
语料库:
corpus = [
'this is the first document',
'this is the second second document',
'and the third one',
'is this the first document'
]
对语料进行分词:
[输入]:
word_list = []
for i in range(len(corpus)):
word_list.append(corpus[i].split(' '))
print(word_list)
[输出]:
[['this', 'is', 'the', 'first', 'document'],
['this', 'is', 'the', 'second', 'second', 'document'],
['and', 'the', 'third', 'one'],
['is', 'this', 'the', 'first', 'document']]
统计词频:
[输入]:
countlist = []
for i in range(len(word_list)):
count = Counter(word_list[i])
countlist.append(count)
countlist
[输出]:
[Counter({'document': 1, 'first': 1, 'is': 1, 'the': 1, 'this': 1}),
Counter({'document': 1, 'is': 1, 'second': 2, 'the': 1, 'this': 1}),
Counter({'and': 1, 'one': 1, 'the': 1, 'third': 1}),
Counter({'document': 1, 'first': 1, 'is': 1, 'the': 1, 'this': 1})]
定义公式:
# word可以通过count得到,count可以通过countlist得到
# count[word]可以得到每个单词的词频, sum(count.values())得到整个句子的单词总数
def tf(word, count):
return count[word] / sum(count.values())
# 统计的是含有该单词的句子数
def n_containing(word, count_list):
return sum(1 for count in count_list if word in count)
# len(count_list)是指句子的总数,n_containing(word, count_list)是指含有该单词的句子的总数,加1是为了防止分母为0
def idf(word, count_list):
return math.log(len(count_list) / (1 + n_containing(word, count_list)))
# 将tf和idf相乘
def tfidf(word, count, count_list):
return tf(word, count) * idf(word, count_list)
计算tfidf:
[输入]:
import math
for i, count in enumerate(countlist):
print("Top words in document {}".format(i + 1))
scores = {word: tfidf(word, count, countlist) for word in count}
sorted_words = sorted(scores.items(), key=lambda x: x[1], reverse=True)
for word, score in sorted_words[:]:
print("\tWord: {}, TF-IDF: {}".format(word, round(score, 5)))
[输出]:
Top words in document 1
Word: first, TF-IDF: 0.05754
Word: this, TF-IDF: 0.0
Word: is, TF-IDF: 0.0
Word: document, TF-IDF: 0.0
Word: the, TF-IDF: -0.04463
Top words in document 2
Word: second, TF-IDF: 0.23105
Word: this, TF-IDF: 0.0
Word: is, TF-IDF: 0.0
Word: document, TF-IDF: 0.0
Word: the, TF-IDF: -0.03719
Top words in document 3
Word: and, TF-IDF: 0.17329
Word: third, TF-IDF: 0.17329
Word: one, TF-IDF: 0.17329
Word: the, TF-IDF: -0.05579
Top words in document 4
Word: first, TF-IDF: 0.05754
Word: is, TF-IDF: 0.0
Word: this, TF-IDF: 0.0
Word: document, TF-IDF: 0.0
Word: the, TF-IDF: -0.04463
sklearn提供了TfidfVectorizer这个接口来计算tfidf值。这个接口会返回一个存储tfidf值的稀疏矩阵。从这个稀疏矩阵中直接取tfidf值是不太好弄的,我也没太仔细研究。如果将这个稀疏矩阵转化成稠密矩阵或者array的话,就会占用内存过大,也不可取。所以不使用sklearn的方法。
下面写一下sklearn接口的使用:
corpus = [
'this is the first document',
'this is the second second document',
'and the third one',
'is this the first document'
]
[输入]:
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf_vec = TfidfVectorizer()
tfidf_matrix = tfidf_vec.fit_transform(corpus)
# 得到语料库所有不重复的词
print(tfidf_vec.get_feature_names())
# 得到每个单词对应的id值
print(tfidf_vec.vocabulary_)
# 得到每个句子所对应的向量
# 向量里数字的顺序是按照词语的id顺序来的
print(tfidf_matrix.toarray())
[输出]:
['and', 'document', 'first', 'is', 'one', 'second', 'the', 'third', 'this']
{'this': 8, 'is': 3, 'the': 6, 'first': 2, 'document': 1, 'second': 5, 'and': 0, 'third': 7, 'one': 4}
[[0. 0.43877674 0.54197657 0.43877674 0. 0.
0.35872874 0. 0.43877674]
[0. 0.27230147 0. 0.27230147 0. 0.85322574
0.22262429 0. 0.27230147]
[0.55280532 0. 0. 0. 0.55280532 0.
0.28847675 0.55280532 0. ]
[0. 0.43877674 0.54197657 0.43877674 0. 0.
0.35872874 0. 0.43877674]]
这个接口的好处是返回的数据格式比较贴近我想要的形式的。他会将每个句子对应到一个列表,每个列表里是句长个数的元组,每个元组是单词id和tfidf值的组合。id是通过gensim里面的一个接口获得的。有一点需要处理的是,每个句子对应的列表里元组出现的顺序并不是原始句子中单词出现的顺序,而是按照id的大小排序的。而我们想要得到的形式是tfidf的值出现的顺序就是单词在句中出现的顺序,所以得到gensim形式的列表后我们还要处理一下。
下面我们先来看一下gensim提供接口的一个使用小例子。然后我们重点讲一下我们是怎么处理数据得到我们想要的数据形式的。
corpus = [
'this is the first document',
'this is the second second document',
'and the third one',
'is this the first document'
]
分词处理:
[输入]:
word_list = []
for i in range(len(corpus)):
word_list.append(corpus[i].split(' '))
print(word_list)
[输出]:
[['this', 'is', 'the', 'first', 'document'],
['this', 'is', 'the', 'second', 'second', 'document'],
['and', 'the', 'third', 'one'],
['is', 'this', 'the', 'first', 'document']]
[输入]:
from gensim import corpora
# 赋给语料库中每个词(不重复的词)一个整数id
dictionary = corpora.Dictionary(word_list)
new_corpus = [dictionary.doc2bow(text) for text in word_list]
print(new_corpus)
# 元组中第一个元素是词语在词典中对应的id,第二个元素是词语在文档中出现的次数
[输出]:
[[(0, 1), (1, 1), (2, 1), (3, 1), (4, 1)],
[(0, 1), (2, 1), (3, 1), (4, 1), (5, 2)],
[(3, 1), (6, 1), (7, 1), (8, 1)],
[(0, 1), (1, 1), (2, 1), (3, 1), (4, 1)]]
[输入]:
# 通过下面的方法可以看到语料库中每个词对应的id
print(dictionary.token2id)
[输出]:
{'document': 0, 'first': 1, 'is': 2, 'the': 3, 'this': 4, 'second': 5, 'and': 6,
'one': 7, 'third': 8}
[输入]:
# 训练模型并保存
from gensim import models
tfidf = models.TfidfModel(new_corpus)
tfidf.save("my_model.tfidf")
# 载入模型
tfidf = models.TfidfModel.load("my_model.tfidf")
# 使用这个训练好的模型得到单词的tfidf值
tfidf_vec = []
for i in range(len(corpus)):
string = corpus[i]
string_bow = dictionary.doc2bow(string.lower().split())
string_tfidf = tfidf[string_bow]
tfidf_vec.append(string_tfidf)
print(tfidf_vec)
[输出]:
[[(0, 0.33699829595119235),
(1, 0.8119707171924228),
(2, 0.33699829595119235),
(4, 0.33699829595119235)],
[(0, 0.10212329019650272),
(2, 0.10212329019650272),
(4, 0.10212329019650272),
(5, 0.9842319344536239)],
[(6, 0.5773502691896258), (7, 0.5773502691896258), (8, 0.5773502691896258)],
[(0, 0.33699829595119235),
(1, 0.8119707171924228),
(2, 0.33699829595119235),
(4, 0.33699829595119235)]]
另外,使用gensim还要注意几件事情:
gensim训练出来的tf-idf值左边是词的id,右边是词的tfidf值
gensim有自动去除停用词的功能,比如the
gensim会自动去除单个字母,比如i
gensim会去除没有被训练到的词,比如name
所以通过gensim并不能计算每个单词的tfidf值
好了。下面重点描述一下我们是如何计算得到自己项目中想要的tfidf形式的。
我们想要的tfidf形式是这样的,一个句子中的单词被表示成tfidf值,这样一个句子就是几个tfidf值组成的向量形式。并且,新形成的向量中元素的顺序保持原始句子中单词的顺序。这一点很重要,因为我们知道文本的一部分信息是保存在语句的顺序中的。
先来看一下原始数据的形式:
X_train = train_df["question_text"].fillna("na").values
# ['how did quebec nationalists see their province as a nation in the ####s ? '
# 'do you have an adopted dog , how would you encourage people to adopt and not shop ? '
# 'why does velocity affect time ? does velocity affect space geometry ? ']
我们可以看到整个训练集是一个列表,每个句子是一个列表中的元素。下一步我们进行分词处理,将每个句子变成一个列表。
X_train_word_list = []
# [['how', 'did', 'quebec', 'nationalists', 'see', 'their', 'province', 'as', 'a', 'nation', 'in', 'the', '####s', '?', ''], ['do', 'you', 'have', 'an', 'adopted', 'dog', ',', '', 'how', 'would', 'you', 'encourage', 'people', 'to', 'adopt', 'and', 'not', 'shop', '?', ''], ['why', 'does', 'velocity', 'affect', 'time', '?', '', 'does', 'velocity', 'affect', 'space', 'geometry', '?', '']]
for i in range(len(X_train)):
X_train_word_list.append(X_train[i].split(' '))
接下来,调用gensim接口。先将训练集中的单词拿出来形成一个Wordlist,并使用corpora形成一个单词的字典,然后dictionary.doc2bow生成词袋。其中每个句子形成一个列表,列表由句长个元组组成,每个元组的第一个元素的单词id,第二个元素是单词在句中的词频
from gensim import corpora
# 赋给语料库中每个词(不重复的词)一个整数id
word_list = X_train_word_list+X_test_word_list
dictionary = corpora.Dictionary(word_list)
corpus = [dictionary.doc2bow(text) for text in word_list]
# [[(0, 1), (1, 1), (2, 1), (3, 1), (4, 1), (5, 1), (6, 1), (7, 1), (8, 1), (9, 1), (10, 1), (11, 1), (12, 1), (13, 1), (14, 1)]]
# 元组中第一个元素是词语在词典对应的id,第二个元素是词语在句子中出现的次数
from gensim import models
tfidf = models.TfidfModel(corpus)
上面的tfidf模型我们可以从中得到tfidf值,不过得到的形式是单词id和对应的tfidf值组成的元组,并且这时候的句子由这些元组组成,但是顺序是按照id的大小排序的。为了得到按句子单词出现顺序形成的tfidf向量,我们先把句子表示成由单词id按单词出现顺序组成的向量。下面是具体的操作:
X_train_id = [] # 将句子表示成单词在词典中id的形式
# [[6, 5, 11, 9, 12, 14, 10, 4, 3, 8, 7, 13, 1, 2, 0], [20, 29, 23, 18, 17, 21, 15, 0, 6, 28, 29, 22, 25, 27, 16, 19, 24, 26, 2, 0], [36, 31, 35, 30, 34, 2, 0, 31, 35, 30, 33, 32, 2, 0]]
word_id_dict = dictionary.token2id
for i in range(len(X_train_word_list)):
sen_id = []
word_sen = X_train_word_list[i]
for j in range(len(word_sen)):
id = word_id_dict.get(word_sen[j])
if id is None:
id = 0
sen_id.append(id)
X_train_id.append(sen_id)
可以看到,现在的句子是用单词id表示成的,并且顺序是原始句子单词出现的顺序。
然后,为了方便我们将tfidf值顺序进行调整,我们将id和tfidf值对应的形式存储到python的dict里,每一个句子都形成一个dict
X_train_tfidf_vec = [] # 每个句子是一个字典,key是单词的ID,value是单词对应的tfidf值
# {1: 0.3575884680878971, 2: 1.138526028042638e-05, 3: 0.06784206283888641, 4: 0.1578771299789853, 5: 0.17261014334370672, 6: 0.07546735698247294, 7: 0.06744570199911137, 8: 0.33649935246876767, 9: 0.4503446034778049, 10: 0.43789085413253603, 11: 0.4463286505421934, 12: 0.2444398022602915, 13: 0.046399522873083514, 14: 0.1834687282725944}
for i in range(len(X_train)):
temp = {}
string = X_train[i]
string_bow = dictionary.doc2bow(string.lower().split())
string_tfidf = tfidf[string_bow]
# 每个句子是一个list,句中的每个单词表示为一个元组,元组的第一个元素是单词的ID,第二个元素是tfidf值
# print(string_tfidf) # [(1, 0.3575884680878971), (2, 1.138526028042638e-05), (3, 0.06784206283888641), (4, 0.1578771299789853), (5, 0.17261014334370672), (6, 0.07546735698247294), (7, 0.06744570199911137), (8, 0.33649935246876767), (9, 0.4503446034778049), (10, 0.43789085413253603), (11, 0.4463286505421934), (12, 0.2444398022602915), (13, 0.046399522873083514), (14, 0.1834687282725944)]
for j in range(len(string_tfidf)):
# print(string_tfidf[j][0])
temp[string_tfidf[j][0]] = string_tfidf[j][1]
# print(temp)
X_train_tfidf_vec.append(temp)
最后一步,我们将id表示的句子映射到tfidf值对应的句子就行了。
X_train_tfidf = [] # tfidf值形成的句子。每个句子是一个list
for i in range(len(X_train_id)):
sen_id = X_train_id[i]
sen_id_tfidf = X_train_tfidf_vec[i]
sen = []
for j in range(len(sen_id)):
word_id = sen_id[j]
word_tfidf = sen_id_tfidf.get(word_id)
if word_tfidf is None:
word_tfidf = 0
sen.append(word_tfidf)
X_train_tfidf.append(sen)
如果需要,还可以把tfidf形成的句子pad到相同的长度
x_train_tfidf = sequence.pad_sequences(X_train_tfidf, maxlen=maxlen,dtype='float64')