NLP自然语言处理实战(二):词中的数学

在(一)中我们已经收集了一些词(词条),对这些词进行了计数,并将它们归并成词干或者词元,下一步我们要探索将其转换成连续值,而并非只表示词出现数目的离散整数,也不只是表示特定词出现与否的二值位向量。

1.词袋–词出现频率或词频向量

之前我们使用了每个词的独热向量,然后将所有这些向量用二进制OR运算组合以创建文本的向量表示。
接下来我们考虑一个更有用的向量表示方法,它计算词在给定文本中的出现次数或者频率。这里引入第一个近似假设,假设一个词在文档中出现的次数越多,那么该词对文档的意义的贡献就越大。
下面给出一个统计词出现次数很有用的例子

from nltk.tokenize import TreebankWordTokenizer
sentence = """The faster Harry got to the store,the faster Harry,the faster,would get home."""
tokenizer = TreebankWordTokenizer()
tokens = tokenizer.tokenize(sentence.lower())
print(tokens)
from collections import Counter # collections.Counter对象是一个无序的集合(collection),也称为袋(bag)或者多重集合(multiset)
bag_of_words = Counter(tokens)
print(bag_of_words)
# 在默认情况下,most_common()会按照频率从高到低列出所有词条,这里只给出频率最高的4个词条
print(bag_of_words.most_common(4))

Out[1]:[‘the’, ‘faster’, ‘harry’, ‘got’, ‘to’, ‘the’, ‘store’, ‘,’, ‘the’, ‘faster’, ‘harry’, ‘,’, ‘the’, ‘faster’, ‘,’, ‘would’, ‘get’, ‘home’, ‘.’]
Out[2]:Counter({‘the’: 4, ‘faster’: 3, ‘,’: 3, ‘harry’: 2, ‘got’: 1, ‘to’: 1, ‘store’: 1, ‘would’: 1, ‘get’: 1, ‘home’: 1, ‘.’: 1})


具体来说,某个词在给定文档中出现的次数称为词项频率,通常简写为TF。在某些例子中可以将某个词的出现频率初一文档中的词项总数从而得到归一化的此项频率结果。(注意:归一化的词项频率实际上是概率,因此可能不应该称为频率。
接下来我们计算上述文档中“harry”的词频

times_harry_appears = bag_of_words['harry']
# 原始语句中的独立词条数
num_unique_words = len(bag_of_words)
tf = times_harry_appears / num_unique_words
print(round(tf,4))

Out[1]:0.1818
这里先暂停一下,我们更深入了解一下归一化词项频率这个术语。它是经过文档长度“调和”后的词频。但是为什么要“调和”呢?考虑词“dog”在文档A中出现3词,在文档B中出现100次。显然“dog”似乎对文档B更重要,但是等等!这里的文档A只是一封写给兽医的30个词的电子邮件,而文档B却是包含大约580000个词的长篇巨著《战争与和平》!因此,我们一开始的分析结果应该正好反过来,即“dog”对文档A更重要。
现在,我们可以看到描述关于两篇文档的一些东西,以及这两篇文档和词“dog”的关系和两篇文档之间的关系。因此,我们不使用原始的词频来描述语料库中的文档,而使用归一化词项频率。
下面考虑一个更长的文本片段,它来自维基百科中有关风筝(kite)的文章的前几个段落

from collections import Counter
from nltk.tokenize import TreebankWordTokenizer
tokenizer = TreebankWordTokenizer()
from nlpia.data.loaders import kite_text
tokens = tokenizer.tokenize(kite_text.lower())
token_counts = Counter(tokens)
print(token_counts)
# 去掉停用词
import nltk
nltk.download('stopwords', quiet=True)
stopwords = nltk.corpus.stopwords.words('english')
tokens = [x for x in tokens if x not in stopwords]
kite_counts = Counter(tokens)
print(kite_counts)

Out[1]:Counter({‘the’: 26, ‘a’: 20, ‘kite’: 16, ‘,’: 15, ‘and’: 10, ‘of’: 10, ‘kites’: 8, ‘is’: 7, ‘in’: 7,…})
Out[2]:Counter({‘kite’: 16, ‘,’: 15, ‘kites’: 8, ‘wing’: 5, ‘lift’: 4, ‘may’: 4, ‘also’: 3, ‘kiting’: 3,…})


2.向量化

我们已经将文本转换为基本的数值。虽然仍然只是把它们存储在字典中,但我们已经从基于文本的世界中走出一步,而进入了数学王国。接下来我们不使用频率字典来描述文档,而是构建词频向量。对上面的代码继续进行处理:

document_vector = []
doc_length = len(tokens)
for key, value in kite_counts.most_common():
    document_vector.append(value/doc_length)
print(document_vector)

Out[1]:[0.07207207207207207, 0.06756756756756757, 0.036036036036036036, 0.02252252252252252, 0.018018018018018018, 0.018018018018018018,…]
对于上述列表或者向量,我们可以直接对它们进行数学运算。


如果只处理一个元素,那么在数学上没什么意思。只有一篇文档对应一个向量是不够的,我们可以获取更对的文档,并为每篇文档创建其对应的向量。但是每个向量内部的值必须都要相对于某个在所有向量上一致性结果进行计算(即所有文档上有个通用的东西,大家都要对它来计算)。
如果要对这些向量进行计算,那么需要相对于一些一致的东西,在公共空间中表示一个位置。向量之间需要有相同的原点,在每个维度上都有相同的表示尺度或者“单位”。这个过程的第一步是计算归一化词项频率,而不是计算文档中的原始词频,第二步是将所有向量都转换到标准长度或维度上去。
此外,我们还希望每个文档向量同一维上的元素值代表同一个词,我们会在每篇文档中找到独立的词,然后将这些词集合求并集后从中找到每个独立的词。词汇表中的这些词集通常称为词库(lexicon)

from nltk.tokenize import TreebankWordTokenizer
tokenizer = TreebankWordTokenizer()
docs = ["The faster Harry got to the store,the faster and faster Harry would get home."]
docs.append("Harry is hairy and faster than Jill.")
docs.append("Jill is not as hairy as Harry.")
# 首先,我们来看看包含三篇文档的语料库的词库:
doc_tokens = []
for doc in docs:
    doc_tokens += [sorted(tokenizer.tokenize((doc.lower())))]
print(doc_tokens)
print(len(doc_tokens[0]))
all_doc_tokens = sum(doc_tokens, [])
print(all_doc_tokens)
print(len(all_doc_tokens))
lexicon = sorted(set(all_doc_tokens))
print(lexicon)
print(len(lexicon))

Out[1]:
[[‘,’, ‘.’, ‘and’, ‘faster’, ‘faster’, ‘faster’, ‘get’, ‘got’, ‘harry’, ‘harry’, ‘home’, ‘store’, ‘the’, ‘the’, ‘the’, ‘to’, ‘would’]
[‘.’, ‘and’, ‘faster’, ‘hairy’, ‘harry’, ‘is’, ‘jill’, ‘than’]
[‘.’, ‘as’, ‘as’, ‘hairy’, ‘harry’, ‘is’, ‘jill’, ‘not’]]
Out[2]:17
Out[3]:[‘,’, ‘.’, ‘and’, ‘faster’, ‘faster’, ‘faster’, ‘get’, ‘got’, ‘harry’, ‘harry’, ‘home’, ‘store’, ‘the’, ‘the’, ‘the’, ‘to’, ‘would’, ‘.’, ‘and’, ‘faster’, ‘hairy’, ‘harry’, ‘is’, ‘jill’, ‘than’, ‘.’, ‘as’, ‘as’, ‘hairy’, ‘harry’, ‘is’, ‘jill’, ‘not’]
Out[4]:33
Out[5]:[‘,’, ‘.’, ‘and’, ‘as’, ‘faster’, ‘get’, ‘got’, ‘hairy’, ‘harry’, ‘home’, ‘is’, ‘jill’, ‘not’, ‘store’, ‘than’, ‘the’, ‘to’, ‘would’]
Out[6]:18
尽管有些文档并不包含词库中所有的18个词,但是上面3篇文档的每个文档向量都会包含18个值。每个词条都会被分配向量中的一个槽位(slot),对应的是它词库中的位置。向量中某些词条的频率会是0。


我们先创建一个全为0的基本向量,然后对基本向量进行复制,更新每篇文档的向量值,然后将它们存储到数组中。

from collections import OrderedDict
zero_vector = OrderedDict((token, 0) for token in lexicon)
print(zero_vector)
import copy
from collections import Counter
doc_vectors = []
for doc in docs:
    vec = copy.copy(zero_vector) #  copy.copy()构建了完全独立的副本,即0向量的一个独立的实例,而非复用一个指针指向原始对象的内存位置,否则,就会在每次循环中用新值重写相同的zero_vector,从而导致每次循环都没有使用新的零向量
    tokens = tokenizer.tokenize(doc.lower())
    token_counts = Counter(tokens)
    for key, value in token_counts.items():
        vec[key] = value / len(lexicon)
    doc_vectors.append(vec)
print(doc_vectors)

Out[1]:OrderedDict([(‘,’, 0), (‘.’, 0), (‘and’, 0), (‘as’, 0), (‘faster’, 0), (‘get’, 0), (‘got’, 0), (‘hairy’, 0), (‘harry’, 0), (‘home’, 0), (‘is’, 0), (‘jill’, 0), (‘not’, 0), (‘store’, 0), (‘than’, 0), (‘the’, 0), (‘to’, 0), (‘would’, 0)])
Out[2]:
[OrderedDict([(‘,’, 0.05555555555555555), (‘.’, 0.05555555555555555), (‘and’, 0.05555555555555555), (‘as’, 0), (‘faster’, 0.16666666666666666), (‘get’, 0.05555555555555555), (‘got’, 0.05555555555555555), (‘hairy’, 0), (‘harry’, 0.1111111111111111), (‘home’, 0.05555555555555555), (‘is’, 0), (‘jill’, 0), (‘not’, 0), (‘store’, 0.05555555555555555), (‘than’, 0), (‘the’, 0.16666666666666666), (‘to’, 0.05555555555555555), (‘would’, 0.05555555555555555)]),
OrderedDict([(‘,’, 0), (‘.’, 0.05555555555555555), (‘and’, 0.05555555555555555), (‘as’, 0), (‘faster’, 0.05555555555555555), (‘get’, 0), (‘got’, 0), (‘hairy’, 0.05555555555555555), (‘harry’, 0.05555555555555555), (‘home’, 0), (‘is’, 0.05555555555555555), (‘jill’, 0.05555555555555555), (‘not’, 0), (‘store’, 0), (‘than’, 0.05555555555555555), (‘the’, 0), (‘to’, 0), (‘would’, 0)]),
OrderedDict([(‘,’, 0), (‘.’, 0.05555555555555555), (‘and’, 0), (‘as’, 0.1111111111111111), (‘faster’, 0), (‘get’, 0), (‘got’, 0), (‘hairy’, 0.05555555555555555), (‘harry’, 0.05555555555555555), (‘home’, 0), (‘is’, 0.05555555555555555), (‘jill’, 0.05555555555555555), (‘not’, 0.05555555555555555), (‘store’, 0), (‘than’, 0), (‘the’, 0), (‘to’, 0), (‘would’, 0)])]


3.向量空间

向量是线性代数或向量代数的主要组成部分。它是一个有序的数值列表,或者说这些数值是向量空间中的坐标。它描述了空间中的一个位置,或者它也可以用来确定空间中一个特定的方向和大小或距离。空间是所有可能出现在这个空间中的向量的集合。
对于自然语言文档向量空间,向量空间的维数是整个语料库中出现的不同词的数量。对于TF,有时我们会用一个大写字母K,称他为K维空间。上述在语料库中不同的词的数量也正好是语料库的词汇量的规模,因此在学术论文中,它通常被称为|V|。然后可以用这个K维空间中的一个K维向量来描述每篇文档。
我们可以通过向量加减,然后计算结果向量的大小来得到两个向量之间的欧几里得距离,也成为2范数距离。但是欧几里得距离对词频向量来说不是一个好办法。因为在高维的情况下,如果两个文本的相似度的长度差距很大,但内容相近,如果使用词频或词向量作为特征,它们在特征空间中的欧式距离通常很大,如果使用余弦相似度,它们的夹角可能很小,因而相似度高。
两个向量间的余弦值可以通过使用欧几里得点积公式求出:
在这里插入图片描述
Python中的余弦相似度计算

import math
def consin_sim(vec1, vec2):
    vec1 = [val for val in vec1.values()]
    vec2 = [val for val in vec2.values()]
    dot_prod = 0
    for i, v in enumerate(vec1):
        dot_prod += v * vec2[i]
    mag_1 = math.sqrt(sum([x ** 2 for x in vec1]))
    mag_2 = math.sqrt(sum([x ** 2 for x in vec2]))
    return dot_prod / (mag_1 * mag_2)

sim_1_2 = consin_sim(doc_vectors[0], doc_vectors[1])
print(sim_1_2)

Out[1]:0.4445004445006667
余弦相似度为1表示两个归一化向量完全相同,它们在所有维度上都指向完全相同的方向。此时,两个向量的长度或大小可能不一样,但是它们指向的方向相同。
余弦相似度为0表示两个向量之间完全没有共享任何分量。它们是正交的,在所有维度上都互相垂直。
余弦相似度为-1表示两个向量是反相似的,即完全相反,也就是两个向量指向完全相反的方向。


4.齐普夫定律

齐普夫定律(Zipf’s Law)指出,在给定的自然语言语料库中,任何一个词的频率与它在频率表中的排名成反比。

import nltk
nltk.download('brown')
from nltk.corpus import brown
print(len(brown.words()))
from collections import Counter
puncs = {',', '.', '--', '-', '!', '?', ':', ';', '``', "''", '(', ')', '[', ']'}
word_list = (x.lower() for x in brown.words() if x not in puncs)
token_counts = Counter(word_list)
print(token_counts.most_common(20))

Out[1]:1161192
Out[2]:[(‘the’, 69971), (‘of’, 36412), (‘and’, 28853), (‘to’, 26158), (‘a’, 23195), (‘in’, 21337), (‘that’, 10594), (‘is’, 10109), (‘was’, 9815), (‘he’, 9548), (‘for’, 9489), (‘it’, 8760), (‘with’, 7289), (‘as’, 7253), (‘his’, 6996), (‘on’, 6741), (‘be’, 6377), (‘at’, 5372), (‘by’, 5306), (‘i’, 5164)]
快速浏览一下上述结果,布朗语料库中的词频符合齐普夫预测的对数线性关系。“The”(词项频率最高)出现的频率大约是“of”(此项频率次高)的2倍,大约是“and”(词项频率第三高)的3倍。


5.主题建模

首先,我们得到语料库中的每篇文档(即intro_doc和history_doc)的总词频:

from nlpia.data.loaders import kite_text, kite_history
from nltk.tokenize import TreebankWordTokenizer
from collections import Counter
kite_intro = kite_text.lower()
tokenizer = TreebankWordTokenizer()
intro_tokens = tokenizer.tokenize(kite_intro)
kite_history = kite_history.lower()
history_tokens = tokenizer.tokenize(kite_history)
intro_total = len(intro_tokens)
history_total = len(history_tokens)
print(intro_total)
print(history_total)

Out[1]:363
Out[2]:297


现在,有两篇分词后的kite文档在手,我们看看“kite”在每篇文档中的词项频率。我们将词项频率存储到两个字典中,其中每一个字典对应一篇文档:

intro_tf = {}
history_tf = {}
intro_counts = Counter(intro_tokens)
intro_tf['kite'] = intro_counts['kite'] / intro_total
history_counts = Counter(history_tokens)
history_tf['kite'] = history_counts['kite'] / history_total
print('Term Frequency of "kite" in intro is:{:.4f}'.format(intro_tf['kite']))
print('Term Frequency of "kite" in history is:{:.4f}'.format(history_tf['kite']))

Out[1]:Term Frequency of “kite” in intro is:0.0441
Out[2]:Term Frequency of “kite” in history is:0.0202
好了,我们看到了两个数字,即“kite”在两篇文档中的词项频率,其中一个数字是另一个数字的两倍。那是不是说就与“kite”的相关度而言,intro部分就是history部分的两倍呢?不,不见得。因此,我们进一步挖掘一下,我们先看看其他一些词如“and”的词项频率数字:

intro_tf['and'] = intro_counts['and'] / intro_total
history_tf['and'] = history_counts['and'] / history_total
print('Term Frequency of "and" in intro is:{:.4f}'.format(intro_tf['and']))
print('Term Frequency of "and" in history is:{:.4f}'.format(intro_tf['and']))

Out[1]:Term Frequency of “and” in intro is:0.0275
Out[2]:Term Frequency of “and” in history is:0.0275
太棒了!我们发现,这两篇文档和“and”的相关度,与它们和“kite”的相关度相差不大。这似乎没有什么用吧。在这个例子中,“and”也被认为与文档高度相关。即使轻轻一瞥,我们也能看出这不具启发意义。
考虑词项逆文档频率的一个好方法是,这个词条在此文档中有多稀缺?如果一个词项在某篇文档中出现很多次,但是却很少出现在语料库的其他文档中,那么就可以假设它对当前文档很重要。这是我们迈向主题分析的第一步!
词项的IDF仅仅是文档总数与该词项出现的文档数之比。在上述例子中的“and”和“kite”,它们的IDF是相同的:

  • 文档总数 / 出现“and”的文档数 = 2/2 = 1
  • 文档总数 / 出现“kite”的文档数 = 2/2 = 1
  • 上面两个词项的计算结果意义不大,我们看看另一个词“China”
  • 文档总数 / 出现“China”的文档数 = 2/1 = 2
    好了,这下出现了一个不同的结果,下面使用这种稀缺度指标来对词项频率加权:
num_docs_containing_and = 0
for doc in [intro_tokens, history_tokens]:
    if 'and' in doc:
        num_docs_containing_and += 1

接下来获取“china”在两篇文档中的词项频率值:

intro_tf['china'] = intro_counts['china'] / intro_total
history_tf['china'] = history_counts['china'] / history_total

最后,计算3个词的IDF,我们就像存储词项频率一样把IDF存储在每篇文档的字典中:

num_docs = 2
intro_idf = {}
history_idf = {}
intro_idf['and'] = num_docs / num_docs_containing_and
history_idf['and'] = num_docs / num_docs_containing_and
intro_idf['kite'] = num_docs / num_docs_containing_kite
history_idf['kite'] = num_docs / num_docs_containing_kite
intro_idf['china'] = num_docs / num_docs_containing_china
history_idf['china'] = num_docs / num_docs_containing_china

然后,对文档intro有:

intro_tfidf = {}
intro_tfidf['and'] = intro_tf['and'] * intro_idf['and']
intro_tfidf['kite'] = intro_tf['kite'] * intro_idf['kite']
intro_tfidf['china'] = intro_tf['china'] * intro_idf['china']
print(intro_tfidf['and'])
print(intro_tfidf['kite'])
print(intro_tfidf['china'])

Out[1]:0.027548209366391185
Out[2]:0.0440771349862259
Out[3]:0.0
对文档history有:

history_tfidf = {}
history_tfidf['and'] = history_tf['and'] * history_idf['and']
history_tfidf['kite'] = history_tf['kite'] * history_idf['kite']
history_tfidf['china'] = history_tf['china'] * history_idf['china']
print(history_tfidf['and'])
print(history_tfidf['kite'])
print(history_tfidf['china'])

Out[1]:0.030303030303030304
Out[2]:0.020202020202020204
Out[3]:0.020202020202020204


5.1回到齐普夫定律

假设我们已经拥有了一个包含100万篇文档的语料库,有人搜索“cat”这个词,在上述100万篇文档中,只有一篇文档包含“cat”,那么这个词的原始或原生IDF为:1000000 / 1 = 1000000
如果有10篇文档包含“dog”,那么“dog”的IDF为:1000000 / 10 = 100000
上述两个结果显著不同。齐普夫会说上面的差距太大了,因为这种差距肯会经常出现。齐普夫定律表明,当比较两个词,如“cat”和“dog”的词频时,即使它们出现的次数类似,更频繁出现的词的词频也将指数级的高于较不频繁出现的词的词频。因此,齐普夫定律建议使用对数log()(exp()的逆函数)来对词频(和文档频率)进行尺度的缩放处理。这就能够确保像“cat”和“dog”这样的词,即使它们出现的次数类似,在最后的词频计算结果上也不会出现指数级的差异。此外,这种词频的分布将确保TF-IDF分数更加均匀分布。因此我们应该将IDF重新定义为词出现在某篇文档中原始概率的对数。对于词项频率,我们也会进行对数处理。

例如我们用一个以10为底的对数函数,我们会得到:
search:cat
idf = lg(1000000 / 1) = 6
search:dog
idf= lg(1000000 / 10) = 5
所以现在要根据它们在语言中总体出现的次数,对每一个TF结果进行适当的加权。
最终,对于语料库D中给定的文档d里的词项t,有:
tf(t,d) = (t在d中出现的次数) / (d的长度)
idf(t, D) = lg[(文档数) / (包含t的文档数)]
tfidf(t,d,D) = tf(t,d) * idf(t, D)

因此,一个词在文档中出现的次数越多,它在文档中的TF(进而TF-IDF)就会越高。与此同时,随着包含该词的文档数增加,该词的IDF(进而TF-IDF)将下降。


5.2相关度排序

现在,向量将更全面地反映文档的含义或主题,如下面这个Harry示例所示:

import copy
from collections import Counter
from nltk.tokenize import TreebankWordTokenizer
tokenizer = TreebankWordTokenizer()
docs = ["The faster Harry got to the store,the faster and faster Harry would get home."]
docs.append("Harry is hairy and faster than Jill.")
docs.append("Jill is not as hairy as Harry.")
doc_tokens = []
for doc in docs:
    doc_tokens += [sorted(tokenizer.tokenize(doc.lower()))]
all_doc_tokens = sum(doc_tokens, [])
lexicon = sorted(set(all_doc_tokens))
from collections import OrderedDict
zero_vector = OrderedDict((token, 0) for token in lexicon)
document_tfidf_vectors = []
for doc in docs:
    vec = copy.copy(zero_vector)
    tokens = tokenizer.tokenize(doc.lower())
    token_counts = Counter(tokens)

    for key, value in token_counts.items():
        docs_containing_key = 0
        for _doc in docs:
            if key in _doc:
                docs_containing_key += 1
        tf = value / len(lexicon)
        if docs_containing_key:
            idf = len(docs) / docs_containing_key
        else:
            idf = 0
        vec[key] = tf * idf
    document_tfidf_vectors.append(vec)
print([vec for vec in document_tfidf_vectors])

Out[1]:
[OrderedDict([(‘,’, 0.16666666666666666), (‘.’, 0.05555555555555555), (‘and’, 0.08333333333333333), (‘as’, 0), (‘faster’, 0.25), (‘get’, 0.16666666666666666), (‘got’, 0.16666666666666666), (‘hairy’, 0), (‘harry’, 0.0), (‘home’, 0.16666666666666666), (‘is’, 0), (‘jill’, 0), (‘not’, 0), (‘store’, 0.16666666666666666), (‘than’, 0), (‘the’, 0.5), (‘to’, 0.16666666666666666), (‘would’, 0.16666666666666666)]), OrderedDict([(‘,’, 0), (‘.’, 0.05555555555555555), (‘and’, 0.08333333333333333), (‘as’, 0), (‘faster’, 0.08333333333333333), (‘get’, 0), (‘got’, 0), (‘hairy’, 0.08333333333333333), (‘harry’, 0.0), (‘home’, 0), (‘is’, 0.08333333333333333), (‘jill’, 0.0), (‘not’, 0), (‘store’, 0), (‘than’, 0.16666666666666666), (‘the’, 0), (‘to’, 0), (‘would’, 0)]),
OrderedDict([(‘,’, 0), (‘.’, 0.05555555555555555), (‘and’, 0), (‘as’, 0.1111111111111111), (‘faster’, 0), (‘get’, 0), (‘got’, 0), (‘hairy’, 0.08333333333333333), (‘harry’, 0.0), (‘home’, 0), (‘is’, 0.08333333333333333), (‘jill’, 0.0), (‘not’, 0.16666666666666666), (‘store’, 0), (‘than’, 0), (‘the’, 0), (‘to’, 0), (‘would’, 0)])]
在上述设置下,我们就得到了语料库中每篇文档的K维向量表示。在给定的向量空间中,如果两个向量有相似的角度,可以说它们是相似的。想象一下,每个向量从原点出发,到达它规定的距离和方向,那些以相同角度到达的向量是相似的,即使它们没有到达相同的距离。
如果两个向量的余弦相似度很高,那么它们就被认为是相似的。
现在,我们已经有了进行基本TF-IDF搜索的所有东西。我们可以将搜索查询本身视为文档,从而获得它的基于TF-IDF的向量表示。最后一步是找到与查询余弦相似度最高的向量的文档,并将这些文档作为搜索结果返回。
如果我们的语料库由关于Harry的3篇文档组成,而查询是"How long does it take to get to the store?",如下所示:

query = "How long does it take to get to the store?"
query_vec = copy.copy(zero_vector)
tokens = tokenizer.tokenize(query.lower())
token_counts = Counter(tokens)
for key, value in token_counts.items():
    docs_containing_key = 0
    for _doc in docs:
        if key in _doc.lower():
            docs_containing_key += 1
        if docs_containing_key == 0:
            continue
        tf = value / len(tokens)
        idf = len(docs) / docs_containing_key
        query_vec[key] = tf * idf


import math
def consin_sim(vec1, vec2):
    vec1 = [val for val in vec1.values()]
    vec2 = [val for val in vec2.values()]
    dot_prod = 0
    for i, v in enumerate(vec1):
        dot_prod += v * vec2[i]
    mag_1 = math.sqrt(sum([x ** 2 for x in vec1]))
    mag_2 = math.sqrt(sum([x ** 2 for x in vec2]))
    return dot_prod / (mag_1 * mag_2)

print(consin_sim(query_vec, document_tfidf_vectors[0]))
print(consin_sim(query_vec, document_tfidf_vectors[1]))
print(consin_sim(query_vec, document_tfidf_vectors[2]))

Out[1]:0.6132857433407973
Out[1]:0.0
Out[1]:0.0
我们可以负责的说,对于当前查询,文档0的相关度最高!
在前面的代码中,我们去掉了词库中没有找到的键,以避免除零错误。但是更好的办法是每次计算IDF时分母都加1,这样可以确保分母不为0。事实上,这种称为加法平滑(拉普拉斯平滑)的方法通常会改进基于TF-IDF关键词搜索的搜索结果。


5.3其他工具

from sklearn.feature_extraction.text import TfidfVectorizer
docs = ["The faster Harry got to the store,the faster and faster Harry would get home."]
docs.append("Harry is hairy and faster than Jill.")
docs.append("Jill is not as hairy as Harry.")
corpus = docs
vectorizer = TfidfVectorizer(min_df=1)
# 为方便查看, .todense()方法将稀疏矩阵转换回常规的numpy矩阵(用0填充空格)
model = vectorizer.fit_transform(corpus)
print(model.todense().round(2))

Out[1]:
[[0.16 0. 0.48 0.21 0.21 0. 0.25 0.21 0. 0. 0. 0.21 0. 0.64
0.21 0.21]
[0.37 0. 0.37 0. 0. 0.37 0.29 0. 0.37 0.37 0. 0. 0.49 0.
0. 0. ]
[0. 0.75 0. 0. 0. 0.29 0.22 0. 0.29 0.29 0.38 0. 0. 0.
0. 0. ]]
利用scikit-learn,我们在上面的4行代码中创建了一个由3个文档组成的矩阵,以及词库中每个词项的逆文档频率。因为分词方式不同,而且去掉了标点符号(原文有一个逗号和句号),所以词库中只有16个词项。对大规模文本而言,这种或其他一些预优化的TF-IDF模型将为我们省去大量工作。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值