自然语言处理实战第二版(MEAP)(二)(1)https://developer.aliyun.com/article/1517845
3.2 计数 n-grams
在上一章中你已经学到如何从语料库的标记中创建 n-gram。现在,是时候将它们用于创建更好的文档表示了。对你来说幸运的是,你可以使用你已经熟悉的相同工具,只需稍微调整参数即可。
首先,让我们在我们的语料库中添加另一句话,这将说明为什么 n-gram 向量有时比计数向量更有用。
>>> import copy >>> question = "What is algorithmic bias?" >>> ngram_docs = copy.copy(docs) >>> ngram_docs.append(question)
如果你使用我们在 3.2 小节训练的相同的向量化器计算这个新句子(问题)的词频向量,你会发现它与第二个句子的表示完全相等:
>>> question_vec = vectorizer.transform([new_sentence]) >>> question_vec <1x240 sparse matrix of type '<class 'numpy.int64'>' with 3 stored elements in Compressed Sparse Row format>
稀疏矩阵是存储标记计数的高效方法,但为了增强对正在发生的情况的直观理解,或者调试代码,你会希望将向量稠密化。你可以使用.toarray()
方法将稀疏向量(稀疏矩阵的行)转换为 numpy 数组或 Pandas 系列。
>>> question_vec.to_array() array([[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, ... ]])
你可能猜到了问题中显示在计数向量的第 8 个位置(维度)上的单词是哪个。记住,这是由CountVectorizer
计算的词汇表中的第 8 个词,并且在运行.fit()
时它会按字典序对其词汇表进行排序。你可以将计数向量与 PandasSeries
一起配对,以查看计数向量中的内容。
>>> vocab = list(zip(*sorted((i, tok) for tok, i in ... vectorizer.vocabulary_.items())))[1] >>> pd.Series(question_vec.to_array()[0], index=vocab).head(8) 2018 0 ability 0 accurately 0 across 0 addressed 0 advanced 0 algorithm 0 algorithmic 1
现在,计算问题向量与你的句子向量"知识库"中所有其他向量之间的余弦相似度。这就是搜索引擎或数据库全文搜索用来查找问题答案的方法。
>>> cosine_similarity(count_vectors, question_vector) array([[0.23570226], [0.12451456], [0.24743583], [0.4330127 ], [0.12909944], ...
最相似的是语料库中的第四个句子。它与question_vector
的余弦相似度为 0.433。检查一下你的句子知识库中的第四个句子,看看它是否能很好地匹配这个问题。
>>> docs[3] The study of algorithmic bias is most concerned with algorithms that reflect "systematic and unfair" discrimination.
不错!那个句子可能是一个不错的开头。然而,维基百科文章的第一句可能更适合这个问题的算法偏见的定义。想一想如何改进向量化流水线,使得你的搜索返回第一句而不是第四句。
要找出 2-grams 是否有帮助,请执行与几页前使用 CountVectorizer
进行的相同向量化过程,但是将 n-gram
超参数 设置为计算 2-grams 而不是单个令牌(1-grams)。超参数只是一个函数名称、参数值或任何你可能想要调整以改善 NLP 流水线的东西。找到最佳超参数称为超参数调整。因此开始调整 ngram_range
参数,看看是否有帮助。
>>> ngram_vectorizer = CountVectorizer(ngram_range=(1, 2)) >>> ngram_vectors = ngram_vectorizer.fit_transform(corpus) >>> ngram_vectors <16x616 sparse matrix of type '<class 'numpy.int64'>' with 772 stored elements in Compressed Sparse Row format>
查看新计数向量的维数,你可能注意到这些向量要长得多。唯一的 2-grams(单词对)总是比唯一的令牌多。检查一下对你的问题非常重要的“算法偏差”2-gram 的 ngram-计数。
>>> vocab = list(zip(*sorted((i, tok) for tok, i in ... ngram_vectorizer.vocabulary_.items())))[1] >>> pd.DataFrame(ngram_vectors.toarray(), ... columns=vocab)['algorithmic bias']
现在,第一句话可能更符合你的查询。值得注意的是,词袋-n-gram 方法也有自己的挑战。在大型文本和语料库中,n-gram 的数量呈指数增长,导致了我们之前提到的“维度灾难”问题。然而,正如你在本节中看到的,可能会有一些情况,你会选择使用它而不是单个令牌计数。
3.2.1 分析这个
即使到目前为止我们只处理了词令牌的 n-grams,字符的 n-grams 也是有用的。例如,它们可以用于语言检测或作者归属(确定在分析的文档集中谁是作者)。让我们使用字符 n-grams 和你刚学会如何使用的 CountVectorizer
类来解决一个谜题。
我们将从导入一个名为 this
的小而有趣的 Python 包开始,并检查其中一些常量:
>>> from this import s >>> print(s) Gur Mra bs Clguba, ol Gvz Crgref Ornhgvshy vf orggre guna htyl. Rkcyvpvg vf orggre guna vzcyvpvg. Fvzcyr vf orggre guna pbzcyrk. ... Nygubhtu arire vf bsgra orggre guna *evtug* abj. Vs gur vzcyrzragngvba vf uneq gb rkcynva, vg'f n onq vqrn. Vs gur vzcyrzragngvba vf rnfl gb rkcynva, vg znl or n tbbq vqrn. Anzrfcnprf ner bar ubaxvat terng vqrn -- yrg'f qb zber bs gubfr!
这些奇怪的词是什么?用什么语言写的?H.P. Lovecraft 的粉丝可能会想到用来召唤死神克苏鲁的古老语言。^([9]) 但即使对他们来说,这个消息也将是难以理解的。
为了弄清楚这段神秘文字的意思,你将使用你刚学到的方法 - 频率分析(计数令牌)。但这一次,一只小鸟告诉你,也许从字符令牌而不是单词令牌开始可能会更有价值!幸运的是,CountVectorizer
在这里也能为你提供帮助。你可以在图 3.4a 中看到列出的结果 3.5 。
列表 3.5 CountVectorizer 直方图
>>> char_vectorizer = CountVectorizer( ... ngram_range=(1,1), analyzer='char') # #1 >>> s_char_frequencies = char_vectorizer.fit_transform(s) >>> generate_histogram( ... s_char_frequencies, s_char_vectorizer) # #2
嗯。 不太确定你可以用这些频率计数做什么。 但再说一遍,你甚至还没有看到其他文本的频率计数。 让我们选择一些大型文档 - 例如,机器学习的维基百科文章,^([10]) 并尝试进行相同的分析(查看图 3.4b 中的结果):
>>> DATA_DIR = ('https://gitlab.com/tangibleai/nlpia/' ... '-/raw/master/src/nlpia/data') >>> url = DATA_DIR + '/machine_learning_full_article.txt' >>> ml_text = requests.get(url).content.decode() >>> ml_char_frequencies = char_vectorizer.fit_transform(ml_text) >>> generate_histogram(s_char_frequencies, s_char_vectorizer)
现在看起来很有趣!如果你仔细观察两个频率直方图,你可能会注意到一个模式。直方图的峰值和谷值似乎以相同的顺序排列。如果你之前曾经处理过频率谱,这可能会有意义。字符频率峰值和谷值的模式是相似的,但是偏移了。
要确定你的眼睛是否看到了一个真实的模式,你需要检查峰值和谷值的变化是否一致。这种信号处理方法被称为频谱分析。你可以通过将每个信号的最高点的位置相互减去来计算峰值的相对位置。
你可以使用几个内置的 Python 函数,ord()
和 chr()
,来在整数和字符之间进行转换。幸运的是,这些整数和字符的映射是按字母顺序排列的,“ABC…”。
>>> peak_distance = ord('R') - ord('E') >>> peak_distance 13 >>> chr(ord('v') - peak_distance) # #1 'I' >>> chr(ord('n') - peak_distance) # #2 'A'
所以,如果你想解码这个秘密信息中的字母"R",你应该从它的ordinal(ord
)值中减去 13,以得到字母"E"——英语中最常用的字母。同样,要解码字母"V",你可以将它替换为"I"——第二个最常用的字母。前三个最常用的字母已经被同样的peak_distance
(13)移动,以创建编码消息。并且这个距离在最不常用的字母之间也被保持:
>>> chr(ord('W') - peak_distance) 'J'
到这个点为止,你可能已经通过 MetaGered(搜索网络)查找了有关这个谜题的信息。^([11])也许你发现了这个秘密信息很可能是使用 ROT13 密码(编码)进行编码的。^([12]) ROT13 算法将字符串中的每个字母向字母表的前面旋转 13 个位置。要解码一个据说是用 ROT13 编码的秘密信息,你只需要应用逆算法,将你的字母表向后旋转 13 个位置。你可能可以在一行代码中创建编码器和解码器函数。或者你可以使用 Python 的内置codecs
包来揭示这一切是关于什么的:
>>> import codecs >>> print(codecs.decode(s, 'rot-13')) The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those!
现在你知道了 Python 的禅意!这些智慧之言是由 Python 族长之一的 Tim Peters 在 1999 年写的。由于这首诗已经放入了公共领域,被谱曲,([13])甚至被拙劣模仿。([14])Python 的禅意已经帮助本书作者编写了更干净、更易读和可重用的代码。由于基于字符的CountVectorizer
,你能够解码这些智慧之言。
3.3 锡普夫定律
现在我们来到了我们的主题——社会学。好吧,不是,但你将会快速地进入人数和字词计数的世界,你将会学到一个看似普遍适用的规则来统计大多数事物。事实证明,在语言中,像大多数涉及到生物的事物一样,模式是丰富多彩的。
在 20 世纪初,法国速记员让-巴蒂斯特·埃斯图普(Jean-Baptiste Estoup)注意到他费力手动计算的许多文件中单词频率的模式(感谢计算机和Python
)。20 世纪 30 年代,美国语言学家乔治·金斯利·齐普夫试图正式化埃斯图普的观察,并最终这种关系以齐普夫的名字命名。
Zipf 定律指出,在给定自然语言话语语料库的情况下,任何单词的频率与其在频率表中的排名成反比。
—— 维基百科
Zipf 定律 en.wikipedia.org/wiki/Zipfs_law
具体而言,反比例 指的是在排名列表中,项目将以与其在列表中的排名直接相关的频率出现。例如,排名列表中的第一项将出现两次,比第二项多三倍,依此类推。您可以对任何语料库或文档进行的一个快速处理是绘制单词使用频率相对于其(频率)排名的图表。如果在对数-对数图中看到不符合直线的任何异常值,可能值得调查。
作为 Zipf 定律延伸至词语以外领域的例子,图 3.6 描绘了美国城市人口与排名之间的关系。事实证明,Zipf 定律适用于许多事物的计数。自然界充满了经历指数增长和"网络效应"的系统,如人口动态、经济产出和资源分配^([15])。有趣的是,像 Zipf 定律这样简单的东西能够在广泛的自然和人造现象中成立。诺贝尔奖得主保罗·克鲁格曼在谈论经济模型和 Zipf 定律时,简洁地表达了这一点:
关于经济理论的常见抱怨是,我们的模型过于简化 — 它们提供了对复杂混乱现实过度整洁的观点。 [使用 Zipf 定律] 反之亦然:你有复杂混乱的模型,然而现实却惊人地整洁和简单。
这是克鲁格曼城市人口图的更新版本:^([16])
图 3.4 城市人口分布
与城市和社交网络一样,单词也是如此。让我们首先从 NLTK 下载布朗语料库。
布朗语料库是 1961 年在布朗大学创建的第一个百万字英文电子语料库。该语料库包含来自 500 个来源的文本,这些来源已按体裁分类,如新闻、社论等^([17])。
—— NLTK 文档
>>> nltk.download('brown') # #1 >>> from nltk.corpus import brown >>> brown.words()[:10] # #2 ['The', 'Fulton', 'County', 'Grand', 'Jury', 'said', 'Friday', 'an', 'investigation', 'of'] >>> brown.tagged_words()[:5] # #3 [('The', 'AT'), ('Fulton', 'NP-TL'), ('County', 'NN-TL'), ('Grand', 'JJ-TL'), ('Jury', 'NN-TL')] >>> len(brown.words()) 1161192
因此,拥有超过 100 万个标记,您有一些值得关注的东西。
>>> from collections import Counter >>> puncs = set((',', '.', '--', '-', '!', '?', ... ':', ';', '``', "''", '(', ')', '[', ']')) >>> word_list = (x.lower() for x in brown.words() if x not in puncs) >>> token_counts = Counter(word_list) >>> token_counts.most_common(10) [('the', 69971), ('of', 36412), ('and', 28853), ('to', 26158), ('a', 23195), ('in', 21337), ('that', 10594), ('is', 10109), ('was', 9815), ('he', 9548)]
快速浏览显示,Brown 语料库中的词频遵循了 Zipf 预测的对数关系。 “The”(在词频中排名第 1)出现的次数大约是 “of”(在词频中排名第 2)的两倍,大约是 “and”(在词频中排名第 3)的三倍。如果你不相信我们,可以使用示例代码(ch03.html)中的代码来亲自验证这一点。
简而言之,如果你按照语料库中单词的出现次数对它们进行排名,并按降序列出它们,你会发现,对于足够大的样本,排名列表中的第一个单词在语料库中出现的可能性是第二个单词的两倍。它在列表中出现的可能性是第四个单词的四倍。因此,给定一个大语料库,你可以使用这个分解来统计地说出一个给定单词在该语料库的任何给定文档中出现的可能性有多大。
3.4 逆文档频率(IDF)
现在回到你的文档向量。单词计数和n-gram 计数很有用,但纯单词计数,即使将其归一化为文档的长度,也不能告诉你有关该单词在该文档中相对于语料库中其他文档的重要性的多少。如果你能搞清楚这些信息,你就可以开始描述语料库中的文档了。假设你有一个关于人工智能(AI)的每本书的语料库。“Intelligence” 几乎肯定会在你计算的每一本书(文档)中出现多次,但这并没有提供任何新信息,它并不能帮助区分这些文档。而像 “neural network” 或 “conversational engine” 这样的东西可能在整个语料库中并不那么普遍,但对于频繁出现的文档,你会更多地了解它们的性质。为此,你需要另一种工具。
逆文档频率,或 IDF,是你通过 Zipf 进行主题分析的窗口。让我们拿你之前的词频计数器来扩展一下。你可以计数令牌并将它们分成两种方式:按文档和整个语料库。你将只按文档计数。
让我们返回维基百科中的算法偏见示例,并抓取另一个部分(涉及算法种族和民族歧视),假设它是你偏见语料库中的第二个文档。
算法被批评为是一种掩盖决策中种族偏见的方法。由于过去某些种族和民族群体的对待方式,数据往往会包含隐藏的偏见。例如,黑人可能会比犯同样罪行的白人接受更长的刑期。这可能意味着系统放大了数据中原有的偏见。
…
2019 年 11 月,加州大学伯克利分校的研究人员进行的一项研究揭示,抵押贷款算法在对待拉丁裔和非洲裔美国人方面存在歧视,这种歧视是基于“信用价值”的,这是美国公平借贷法的根源,该法允许贷方使用身份识别措施来确定一个人是否值得获得贷款。这些特定的算法存在于金融科技公司中,并被证明对少数族裔进行了歧视。
— 维基百科
算法偏见:种族和族裔歧视 (en.wikipedia.org/wiki/Algorithmic_bias#Racial_and_ethnic_discrimination
首先,让我们得到语料库中每个文档的总词数:
>>> DATA_DIR = ('https://gitlab.com/tangibleai/nlpia/' ... '-/raw/master/src/nlpia/data') >>> url = DATA_DIR + '/bias_discrimination.txt' >>> bias_discrimination = requests.get(url).content.decode() >>> intro_tokens = [token.text for token in nlp(bias_intro.lower())] >>> disc_tokens = [token.text for token in nlp(bias_discrimination.lower())] >>> intro_total = len(intro_tokens) >>> intro_total 479 >>> disc_total = len (disc_tokens) >>> disc_total 451
现在,拿到几个关于偏见的 tokenized 文档,让我们看看每个文档中术语“偏见”的频率。您将把找到的 TF 存储在两个字典中,每个文档一个。
>>> intro_tf = {} >>> disc_tf = {} >>> intro_counts = Counter(intro_tokens) >>> intro_tf['bias'] = intro_counts['bias'] / intro_total >>> disc_counts = Counter(disc_tokens) >>> disc_tf['bias'] = disc_counts['bias'] / disc_total >>> 'Term Frequency of "bias" in intro is:{:.4f}'.format(intro_tf['bias']) Term Frequency of "bias" in intro is:0.0167 >>> 'Term Frequency of "bias" in discrimination chapter is: {:.4f}'\ ... .format(disc_tf['bias']) 'Term Frequency of "bias" in discrimination chapter is: 0.0022'
好了,你得到了一个比另一个大八倍的数字。那么“介绍”部分关于偏见多八倍?实际上不是。所以你需要深入挖掘一下。首先,看看这些数字与其他一些词的得分比较,比如词"和"。
>>> intro_tf['and'] = intro_counts['and'] / intro_total >>> disc_tf['and'] = disc_counts['and'] / disc_total >>> print('Term Frequency of "and" in intro is: {:.4f}'\ ... .format(intro_tf['and'])) Term Frequency of "and" in intro is: 0.0292 >>> print('Term Frequency of "and" in discrimination chapter is: {:.4f}'\ ... .format(disc_tf['and'])) Term Frequency of "and" in discrimination chapter is: 0.0303
太棒了!你知道这两个文档关于“和”和“偏见”一样多——实际上,歧视章节更多地涉及“和”。哦,等等。
一个衡量术语的逆文档频率的好方法是:这个标记在这个文档中是多么令人惊讶?在统计学、物理学和信息论中,衡量标记的惊讶程度用来衡量其熵或信息内容。这正是你需要衡量特定词的重要性的方式。如果一个术语在一个文档中出现了很多次,但在整个语料库中很少出现,那么它是将该文档的含义与其他文档区分开的词。这
一个术语的 IDF 只是文档总数与术语出现的文档数之比。在当前示例中,对于“和”和“偏见”,答案是相同的:
2 total documents / 2 documents contain "and" = 2/2 = 1 2 total documents / 2 documents contain "bias" = 2/2 = 1
不是很有趣。所以我们来看另一个单词“黑色”。
2 total documents / 1 document contains "black" = 2/1 = 2
好的,这是另一回事了。让我们使用这个“稀有性”度量来加权词频。
>>> num_docs_containing_and = 0 >>> for doc in [intro_tokens, disc_tokens]: ... if 'and' in doc: ... num_docs_containing_and += 1 # #1
然后让我们获取两个文档中“黑色”的词频:
>>> intro_tf['black'] = intro_counts['black'] / intro_total >>> disc_tf['black'] = disc_counts['black'] / disc_total
最后,三者的 IDF。你将像之前的 TF 一样将 IDF 存储在每个文档的字典中:
>>> num_docs = 2 >>> intro_idf = {} >>> disc_idf = {} >>> intro_idf['and'] = num_docs / num_docs_containing_and >>> disc_idf['and'] = num_docs / num_docs_containing_and >>> intro_idf['bias'] = num_docs / num_docs_containing_bias >>> disc_idf['bias'] = num_docs / num_docs_containing_bias >>> intro_idf['black'] = num_docs / num_docs_containing_black >>> disc_idf['black'] = num_docs / num_docs_containing_black
然后在引言文档中找到:
>>> intro_tfidf = {} >>> intro_tfidf['and'] = intro_tf['and'] * intro_idf['and'] >>> intro_tfidf['bias'] = intro_tf['bias'] * intro_idf['bias'] >>> intro_tfidf['black'] = intro_tf['black'] * intro_idf['black']
然后看历史文件:
>>> disc_tfidf = {} >>> disc_tfidf['and'] = disc_tf['and'] * disc_idf['and'] >>> disc_tfidf['bias'] = disc_tf['bias'] * disc_idf['bias'] >>> disc_tfidf['black'] = disc_tf['black'] * disc_idf['black']
3.4.1 兹普夫的回归
差一点了。假设你有一个包含 100 万个文件的语料库(也许你是 baby-Google),有人搜索词“猫”,而在你的 100 万个文件中只有 1 个包含词“猫”的文件。这个的原始 IDF 是:
1,000,000 / 1 = 1,000,000
让我们假设您有 10 个文档中都含有单词"狗"。您的"狗"的逆文档频率(idf)为:
1,000,000 / 10 = 100,000
这是一个很大的区别。您的朋友齐普夫可能会说这太大了,因为它可能经常发生。齐普夫定律表明,当您比较两个单词的频率时,例如"猫"和"狗",即使它们出现的次数相似,频率更高的单词的频率也将比频率较低的单词高得多。因此,齐普夫定律建议您使用log()
函数的逆函数exp()
来缩放所有单词频率(和文档频率)。这确保了具有相似计数的单词,例如"猫"和"狗",在频率上不会有很大差异。这种单词频率的分布将确保您的 TF-IDF 分数更加均匀分布。因此,您应该重新定义 IDF 为该单词在您的文档中出现的原始概率的对数。您还需要对术语频率取对数。
对数函数的底数并不重要,因为您只是想使频率分布均匀,而不是在特定数值范围内缩放它。如果使用底数为 10 的对数函数,您将获得:
搜索:猫
方程 3.3
[ idf=log(1,000,000/1)=6idf=log(1,000,000/1)=6 ]
搜索:狗
方程 3.4
[ idf=log(1,000,000/10)=5idf=log(1,000,000/10)=5 ]
所以现在您更适当地加权了每个 TF 的结果,以符合它们在语言中的出现次数。
然后,对于语料库D中给定文档d中的给定术语t,您得到:
方程 3.5
[ tf(t,d)=count(t)count(d)tf(�,�)=count(�)count(�) ]
方程 3.6
[ idf(t,D)=log(文档数量包含术语 t 的文档数量)idf(�,�)=log(文档数量包含术语 t 的文档数量) ]
方程 3.7
[ tfidf(t,d,D)=tf(t,d)∗idf(t,D)tfidf(�,�,�)=tf(�,�)∗idf(�,�) ]
单词在文档中出现的次数越多,TF(因此 TF-IDF)就会增加。同时,随着包含该单词的文档数的增加,该单词的 IDF(因此 TF-IDF)就会降低。所以现在,你有了一个数字。这是你的计算机可以处理的东西。但它到底是什么呢?它将特定单词或令牌与特定语料库中的特定文档相关联,然后将数值分配给该单词在给定文档中的重要性,考虑到其在整个语料库中的使用情况。
在某些课程中,所有的计算都将在对数空间中进行,以便乘法变为加法,除法变为减法:
>>> log_tf = log(term_occurences_in_doc) -\ ... log(num_terms_in_doc) # #1 >>> log_log_idf = log(log(total_num_docs) -\ ... log(num_docs_containing_term)) # #2 >>> log_tf_idf = log_tf + log_log_idf # #3
这个单一的数字,即 TF-IDF 分数,是所有搜索引擎的谦逊基础。现在你已经能够将单词和文档转换为数字和向量,是时候用一些 Python 来让所有这些数字发挥作用了。你可能永远不会需要从头实现 TF-IDF 公式,因为这些算法已经在许多软件库中为你实现了。你不需要成为线性代数的专家来理解自然语言处理,但如果你对生成像 TF-IDF 分数这样的数字的数学有一个心理模型,那肯定能提高你的信心。如果你理解了数学,你可以自信地为你的应用调整它,甚至可以帮助一个开源项目改进它的自然语言处理算法。
自然语言处理实战第二版(MEAP)(二)(3)https://developer.aliyun.com/article/1517851