第三章:用单词做数学运算(TF-IDF 向量)
本章包括
- 计算单词、n-grams 和词频以分析含义
- 使用Zipf 定律预测单词出现概率
- 将自然语言文本表示为向量
- 使用文档频率在文本集合中找到相关文档
- 使用余弦相似度估计文档对的相似性
收集并计数了单词(标记),并将它们分桶为词干或词元后,就可以对它们做一些有趣的事情了。检测单词对于简单任务非常有用,比如获取关于单词使用情况的统计信息或进行关键词搜索。但是你想知道哪些单词对特定文档和整个语料库更重要。因此,你可以使用该“重要性”值来根据每个文档内关键字的重要性来查找语料库中的相关文档。这样一来,垃圾邮件检测器就不太可能因为单个脏话或少量带有些许垃圾邮件特征的词而被触发。你想要测量一条 Mastodon 消息有多积极和亲社会,尤其是当你有各种单词,这些单词具有不同程度的“积极性”分数或标签时。如果你对这些单词相对于其他文档出现的频率有一个想法,那么你可以用它来进一步确定文档的“积极性”。在本章中,你将了解有关单词及其在文档中使用的更为细致、不那么二元的度量方法。这种方法是商业搜索引擎和垃圾邮件过滤器从自然语言生成特征的主要方法已经有几十年了。
你冒险的下一步是将第二章的文字转化为连续数字,而不仅仅是表示单词计数或二进制“位向量”的整数。通过在连续空间中表示单词,你可以用更加有趣的数学来操作它们的表达。你的目标是找到单词的数值表示,以某种方式捕捉它们所代表的重要性或信息内容。你得等到第四章才能看到如何将这些信息内容转化为代表单词含义的数字。
在本章中,我们将研究三种越来越强大的方式来表示单词及其在文档中的重要性:
- 词袋 — 单词计数或频率的向量
- n-gram 包 — 单词对(bigrams)、三元组(trigrams)等的计数
- TF-IDF 向量 — 更好地表示单词的重要性的单词分数
重要
TF-IDF 代表**词频乘以逆文档频率。词频是文档中每个单词的计数,这是你在前几章学到的。逆文档频率意味着你将每个单词的计数除以该单词出现的文档数。
这些技术可以分别应用,也可以作为 NLP 流水线的一部分应用。这些都是统计模型,因为它们基于频率。在本书的后面,您将看到各种方法,更深入地了解单词之间的关系、模式和非线性。
但是这些“浅层”自然语言处理(NLP)机器是强大且实用的,用于许多实际应用,如搜索、垃圾邮件过滤、情感分析,甚至聊天机器人。
3.1 词袋模型
在上一章中,您创建了文本的第一个向量空间模型。您对每个单词进行了独热编码,然后将所有这些向量与二进制 OR(或裁剪的 sum
)结合起来,以创建文本的向量表示。这个二进制词袋向量在加载到诸如 Pandas DataFrame 等数据结构时,可以用作文档检索的优秀索引。
接着你看了一个更有用的向量表示,它计算了给定文本中每个单词出现的次数或频率。作为第一个近似,你假设一个单词出现的次数越多,它在文档中的贡献就越大。一个频繁提到“机翼”和“方向舵”的文档,可能与涉及喷气飞机或航空旅行的问题更相关,而不是频繁提到“猫”和“重力”的文档。或者,如果你已经将一些词分类为表达积极情感的词——像“好”、“最好”、“喜悦”和“棒极了”——那么包含这些词的文档越多,它就越有可能具有积极的“情感”。不过,你可以想象到一个依赖这些简单规则的算法可能会犯错或走上错误的道路。
让我们看一个统计单词出现次数有用的例子:
>>> import spacy >>> # spacy.cli.download("en_core_web_sm") # #1 >>> nlp = spacy.load("en_core_web_sm") >>> sentence = ('It has also arisen in criminal justice, healthcare, and ' ... 'hiring, compounding existing racial, economic, and gender biases.') >>> doc = nlp(sentence) >>> tokens = [token.text for token in doc] >>> tokens ['It', 'has', 'also', 'arisen', 'in', 'criminal', 'justice', ',', 'healthcare', ',', 'and', 'hiring', ',', 'compounding', 'existing', 'racial', ',', 'economic', ',', 'and', 'gender', 'biases', '.']
您只需要下载 SpaCy 语言模型一次,它可能会消耗大量的互联网带宽。所以只有在您的 Python 虚拟环境中首次运行此代码时才运行 cli.download()
。SpaCy 语言模型将自然语言文本进行标记化,并返回一个包含输入文本中所有标记序列的文档对象(Doc
类)。它还会将文档分段,以便您在 .sents
属性中获得一个句子序列。借助 Python 的 set()
类型,您可以将这个标记序列转换为文本中所有唯一单词的集合。
文档或语料库中所有唯一单词的列表称为其词汇或词典。创建您的词汇是您的 NLP 流水线中最重要的步骤。如果您不识别特定的标记并为其分配一个位置来存储它,您的流水线将完全忽略它。在大多数 NLP 流水线中,您将定义一个名为(超出词汇)的单个标记,您将在其中存储有关您的流水线正在忽略的所有标记的信息,例如它们的出现次数。因此,您没有包含在词汇中的所有不寻常或虚构的“超级长的”单词将被合并到一个单一的通用标记中,而您的 NLP 流水线将无法计算其含义。
Python 的Counter
类是一种高效计算任何东西(包括标记)在序列或数组中出现次数的方法。在第二章中,您了解到Counter
是一种特殊类型的字典,其中键是数组中的所有唯一对象,而字典的值是每个对象的计数。
>>> from collections import Counter >>> bag_of_words = Counter(tokens) >>> bag_of_words Counter({',': 5, 'and': 2, 'It': 1, 'has': 1, 'also': 1, 'arisen': 1, ...})
collections.Counter
对象实际上是一个 dict
。这意味着键技术上存储在无序集合或set
中,有时也称为“bag”。它可能看起来像这个字典已经保持了您的句子中单词的顺序,但这只是一种错觉。您有幸因为您的句子中没有包含许多重复的标记。而 Python 的最新版本(3.6 及以上)基于您在字典中插入新键的时间来维护键的顺序。但是您即将从这些标记和它们的计数的字典中创建向量。您需要向量来对一系列文档(在这种情况下是句子)进行线性代数和机器学习。您的词袋向量将使用一致的索引号来跟踪每个唯一标记在向量中的位置。这样,诸如“and”或逗号之类的标记的计数将在您的文档的所有向量中累加——维基百科文章标题为“算法偏见”的句子中。
重要提示
对于 NLP,字典中键的顺序并不重要,因为您将在向量中保持一致的排序,例如 Pandas Series。正如在第二章中一样,Counter 字典根据您处理语料库的每个文档的时间顺序对您的词汇(dict
键)进行排序。有时您可能希望对您的词汇进行按字母顺序排列以便于分析。一旦您为计数向量的每个标记分配了一个维度,务必记录下该顺序以备将来使用,这样您就可以在不重新处理所有文档的情况下重新使用您的流水线而无需重新训练它。如果您试图复制他人的 NLP 流水线,您将想要重用其确切的词汇表(标记列表),并按照完全相同的顺序重复使用。否则,您将需要按照与他们相同的顺序,使用完全相同的软件来处理他们的训练数据集。
对于像这样的短文档,打乱的词袋仍然包含了关于句子原始意图的大量信息。词袋中的信息足以执行一些强大的任务,比如检测垃圾邮件、计算情感(积极性或其他情绪),甚至检测微妙的意图,比如讽刺。它可能是一个袋子,但它充满了意义和信息。为了更容易地思考这些词,并确保你的管道是一致的,你想要以某种一致的顺序对它们进行排序。要按计数对标记进行排名,Counter 对象有一个方便的方法,即most_common
。
>>> bag_of_words.most_common(3) # #1 [(',', 5), ('and', 2), ('It', 1)]
这很方便!Counter.most_common
方法将给出一个排名列表,其中包含你想要的任意数量的标记,并与其计数配对为 2 元组。但这还不是你想要的。你需要一个向量表示来轻松地对你的标记计数进行数学运算。
Pandas Series
是一种高效的数据结构,用于存储标记计数,包括来自most_common
方法的 2 元组。Pandas Series
的好处是,每当你使用像加号(+
)或(*
)甚至.dot()
这样的数学运算符时,它都表现得像一个向量(numpy 数组)。而且你仍然可以使用正常的方括号(['token']
)语法访问与每个标记关联的每个命名(标记)维度。
提示
Python 表达式x[y]
与x.*getitem*(y)
是相同的。方括号([
和]
)是字典、列表、Pandas Series 和许多其他容器对象上隐藏的.*getitem*()
方法的语法糖(简写)。如果你想在自己的容器类上使用这个运算符,你只需要定义一个.*getitem*(index_value)
方法,从你的容器中检索适当的元素即可。
你可以使用内置的dict
类型构造函数将任意的 2 元组列表转换为字典。而且你可以使用Series
构造函数将任意字典转换为 Pandas Series
。
>>> import pandas as pd >>> most_common = dict(bag_of_words.most_common()) # #1 >>> counts = pd.Series(most_common) # #2 >>> counts , 5 and 2 It 1 has 1 also 1 ...
当你将 Pandas Series 打印到屏幕上时,它会显示得很好,这在你试图理解一个标记计数向量包含的内容时可能会很方便。现在你已经创建了一个计数向量,你可以像对待任何其他 Pandas Series 一样对待它。
>>> len(counts) # #1 18 >>> counts.sum() 23 >>> len(tokens) # #2 23 >>> counts / counts.sum() # #3 , 0.217391 and 0.086957 It 0.043478 has 0.043478 also 0.043478 ...
你可以看到这个句子中有 23 个标记,但是你的词汇表中只有 18 个唯一的标记。因此,即使其他文档没有使用这些相同的 18 个词,你的每个文档向量也需要至少有 18 个值。这样可以让每个标记在你的计数向量中拥有自己的维度(槽)。每个标记在你的向量中被分配一个与其在词汇表中位置相对应的“槽”。向量中的某些标记计数将为零,这正是你想要的。
逗号(“,”)标记和单词"and"位于你的most_common
词项列表的顶部是有道理的。逗号使用了五次,单词"and"使用了两次,而在这个句子中所有其他单词只使用了一次。在这个特定句子中,你的前两个词项或标记是",“和"and”。这是自然语言文本的一个相当普遍的问题——最常见的词往往是最无意义的。这些停用词并不能告诉你关于这个文档意义的很多,所以你可能会完全忽略它们。一个更好的方法是使用你的文档中词的统计数据来衡量你的词项计数,而不是别人从他们的文档中列出的停用词的任意列表。
一个词在给定文档中出现的次数被称为词频,通常缩写为"TF"。你可能想要做的第一件事情之一就是通过文档中的词项数进行归一化(除以)。这将为你提供文档中包含词项的相对频率(百分比或分数),而不考虑文档的长度。查看一下单词"justice"的相对频率,看看这种方法是否能恰当地体现这个词在这段文本中的重要性。
>>> counts['justice'] 1 >>> counts['justice'] / counts.sum() 0.043...
在这篇维基百科文章开头句子中,单词"justice"的标准化词频约为 4%。而且你可能不会期望随着你处理文章中更多的句子,这个百分比会上升。如果句子和文章都在谈论"justice"大致相同的数量,那么这个标准化词频分数在整个文档中将保持大致不变。
根据这个词频,单词"justice"在这个句子中代表了约 4%的意义。考虑到这个单词对句子的意义有多重要,这并不多。所以你需要再做一步归一化,以使这个词相对于句子中的其他词得到提升。
要给单词"justice"一个重要性或重要性评分,你需要一些关于它的统计数据,不仅仅是这一个句子。你需要找出"justice"在其他地方的使用情况。幸运的是对于初学的 NLP 工程师来说,维基百科充满了许多语言的高质量准确的自然语言文本。你可以使用这些文本来"教"你的机器关于单词"justice"在许多文档中的重要性。为了展示这种方法的威力,你只需要从维基百科上的算法偏见文章中选取几段。这里有一些来自维基百科文章"Algorithmic Bias"的句子。
算法偏见描述了计算机系统中的系统性和可重复的错误,这些错误会导致不公平的结果,例如偏袒某个任意的用户群体而不是其他人。偏见可能由许多因素引起,包括但不限于算法的设计或数据编码、收集、选择或用于训练算法的方式的意外或未预料到的使用或决策。
…
算法偏见已在各种情况下被引用,从选举结果到网络仇恨言论的传播。它还出现在刑事司法、医疗保健和招聘中,加剧了现有的种族、经济和性别偏见。
…
由于算法的专有性质,即通常被视为商业机密,导致了对理解、研究和发现算法偏见的问题仍然存在。
— 维基百科
算法偏见 (en.wikipedia.org/wiki/Algorithmic_bias
)
看看这些句子,看看是否能找到对您理解文本至关重要的关键字。您的算法需要确保包含这些词,并计算有关它们的统计数据。如果您尝试使用 Python 自动(程序化地)检测这些重要单词,您将如何计算重要性得分?看看您是否能想出如何使用 Counter
字典来帮助您的算法理解算法偏见的某些方面。
>>> sentence = "Algorithmic bias has been cited in cases ranging from " \ ... "election outcomes to the spread of online hate speech." >>> tokens = [tok.text for tok in nlp(sentence)] >>> counts = Counter(tokens) >>> dict(counts) {'Algorithmic': 1, 'bias': 1, 'has': 1, 'been': 1, 'cited': 1, 'in': 1, 'cases': 1, 'ranging': 1, 'from': 1, 'election': 1, 'outcomes': 1, 'to': 1, 'the': 1, 'spread': 1, 'of': 1, 'online': 1, 'hate': 1, 'speech': 1, '.': 1})
看起来这句话根本没有重复使用任何词。频率分析和词频向量的关键在于单词使用的统计数据相对于其他单词。因此,我们需要输入其他句子,并创建基于单词如何在其他地方使用的归一化的有用单词计数。要理解“算法偏见”,您可以花时间阅读并将维基百科文章的所有内容输入到 Python 字符串中。或者,您可以使用 nlpia2_wikipedia
Python 包从维基百科下载文本,然后使用自然语言处理找到您需要复习的关键概念。要直接从维基百科检索最新的“算法偏见”文本,请使用 nlpia2.wikipedia
而不是官方(但已废弃)的维基百科包。
>>> from nlpia2 import wikipedia as wiki >>> page = wiki.page('Algorithmic Bias') # #1 >>> page.content[:70] 'Algorithmic bias describes systematic and repeatable errors in a compu'
您还可以从 GitLab 上的 nlpia2
包中下载包含维基百科文章的前 3 段的文本文件。如果您已经克隆了 nlpia2
包,您将在本地硬盘上看到 src/nlpia2/ch03/bias_intro.txt
。如果您尚未从源代码安装 nlpia2
,您可以使用此处的代码片段使用 requests
包检索文件。requests
包是一个方便的工具,用于从网络上抓取和下载自然语言文本。ChatGPT、Bard 和 YouChat 显得如此聪明的所有文本都是使用类似 requests
的工具从网页上抓取的。
>>> import requests >>> url = ('https://gitlab.com/tangibleai/nlpia2/' ... '-/raw/main/src/nlpia2/ch03/bias_intro.txt') >>> response = requests.get(url) >>> response <Response [200]>
requests
包返回一个包含 HTTP 响应的对象,其中包含报头(在.headers
中)和正文(.text
)的内容。nlpia2
包数据中的 bias_intro.txt
文件是维基百科文章的前三段的 2023 快照。
>>> bias_intro_bytes = response.content # #1 >>> bias_intro = response.text # #2 >>> assert bias_intro_bytes.decode() == bias_intro # #3 >>> bias_intro[:70] 'Algorithmic bias describes systematic and repeatable errors in a compu'
对于纯文本文档,您可以使用response.content
属性,该属性包含原始 HTML 页面的bytes
。如果要获取一个字符串,可以使用response.text
属性将文本字节自动解码为 unicode str
。
来自collections
模块的 Python 标准库中的Counter
类非常适合高效计数任何对象的序列。这对于 NLP 来说非常完美,当您希望计算一组令牌中唯一单词和标点的出现次数时:
>>> tokens = [tok.text for tok in nlp(bias_intro)] >>> counts = Counter(tokens) >>> counts Counter({'Algorithmic': 3, 'bias': 6, 'describes': 1, 'systematic': 2, ... >>> counts.most_common(5) [(',', 35), ('of', 16), ('.', 16), ('to', 15), ('and', 14)]
好吧,这些计数的统计意义更大一些。但是仍然有许多无意义的词汇和标点符号,它们的计数似乎很高。这篇维基百科文章可能并不真的是关于“of”、“to”、“commas”或“periods”等标记。也许关注最不常见的标记会比关注最常见的标记更有用。
>>> counts.most_common()[-4:] ('inputs', 1), ('between', 1), ('same', 1), ('service', 1)]
这个方法不那么成功。您可能希望找到类似“偏见”、“算法”和“数据”之类的术语。因此,您需要使用一个平衡计数的公式,以得到“刚刚好”的术语的“Goldilocks”得分。你可以这样做,通过得到另一个有用的计数——一个单词出现在多少篇文章中的计数,称为“文档频率”。这就是事情变得真正有趣的时候。
如果您有一个大型语料库,您可以基于一个令牌在所有文档中使用的频率来归一化(除以)文档中的计数。由于您刚开始使用令牌计数向量,最好将维基百科文章摘要拆分成更小的文档(句子或段落),以创建一些小文档。这样,您至少可以在一页上看到所有文档,并通过脑海中运行代码来确定所有计数的来源。在接下来的部分中,您将拆分“Algorithm Bias”文章文本为句子,并尝试不同的归一化和结构化计数字典的方法,以使它们在自然语言处理中更有用。
3.1.1 文本向量化
Counter
字典非常适合计数文本中的标记。但是向量才是真正需要的东西。原来字典可以通过在字典列表上调用 DataFrame
构造函数来强制转换为 DataFrame
或 Series
。Pandas 将负责所有簿记工作,以便每个唯一的令牌或字典键都有自己的列。当文档的 Counter
字典缺少特定的键时,Pandas 会创建 NaN 值,因为该文档不包含该单词或符号。
一旦您将“算法偏差”文章拆分为行,您将看到向量表示的威力。您很快就会明白为什么 Pandas Series 与标准 Python dict.
相比,对于处理标记计数来说是一个更有用的数据结构。
>>> docs = [nlp(s) for s in bias_intro.split('\n') ... if s.strip()] # #1 >>> counts = [] >>> for doc in docs: ... counts.append(Counter([ ... t.text.lower() for t in doc])) # #2 >>> df = pd.DataFrame(counts) >>> df = df.fillna(0).astype(int) # #3 >>> len(df) 16 >>> df.head() algorithmic bias describes systematic ... between same service 0 1 1 1 1 ... 0 0 0 1 0 1 0 0 ... 0 0 0 2 1 1 0 0 ... 0 0 0 3 1 1 0 1 ... 0 0 0 4 0 1 0 0 ... 0 0 0
当您的向量维度用于保存标记或字符串的分数时,这就是您想要使用 Pandas DataFrame
或Series
来存储您的向量的时候。这样,您就可以看到每个维度的用途。检查一下我们在本章开头提到的那个句子。它碰巧是维基百科文章中的第 11 个句子。
>>> df.iloc[10] # #1 algorithmic 0 bias 0 describes 0 systematic 0 and 2 ... Name: 10, Length: 246, dtype: int64
现在这个 Pandas Series
是一个向量。这是您可以进行数学计算的东西。当您进行数学计算时,Pandas 将跟踪哪个单词在哪里,以便“偏见”和“正义”不会被意外加在一起。这个 DataFrame 中的行向量每个词汇中的一个“维度”。事实上,df.columns
属性包含您的词汇表。
等等,标准英语词典中有超过 30,000 个单词。如果您开始处理大量维基百科文章而不仅仅是几句话,那么将有很多维度要处理。您可能习惯于 2D 和 3D 向量,因为它们易于可视化。但是 30,000 个维度的概念,例如距离和长度,甚至是否有效?事实证明它们确实有效,您将在本书后面学习如何改进这些高维向量。现在只需知道向量的每个元素用于表示您希望向量表示的文档中单词的计数,权重或重要性。
您将在每个文档中找到每个独特的单词,然后找到所有文档中的所有独特单词。在数学中,这就是每个文档中所有单词集合的并集。这些文档的主要单词集合称为您的管道的词汇。如果您决定跟踪有关每个单词的其他语言信息,例如拼写变体或词性,您可以称之为词典。您可能会发现使用术语语料库来描述一组文档的学者也可能会使用单词“词典”,只是因为它是比“词汇”更精确的技术术语。
因此,看一看这三段话的词汇或词典。首先,您将进行大小写转换(小写化),以忽略大写字母(例如专有名词)之间的差异,并将它们组合到一个词汇标记中。这将减少您管道后续阶段中词汇表中独特单词的数量,这可以使您更容易看到发生的事情。
>>> docs_tokens = [] >>> for doc in docs: ... docs_tokens.append([ ... tok.text.lower() for tok in nlp(doc.text)]) # #1 >>> len(docs_tokens[0]) 27
现在您已经将这 28 篇文档(句子)全部标记化了,您可以将所有这些标记列表连接在一起,以创建一个包含所有标记的大列表,包括重复。此标记列表与原始文档唯一的区别在于,它已经被分割成句子并标记化为单词。
>>> all_doc_tokens = [] >>> for tokens in docs_tokens: ... all_doc_tokens.extend(tokens) >>> len(all_doc_tokens) 482
从整个段落的标记序列中创建词汇表(词典)。你的词汇表是你语料库中所有唯一标记的列表。就像图书馆中的词典一样,词汇表不包含任何重复项。除了 dict
类型,你还知道哪些 Python 数据类型可以去除重复项?
>>> vocab = set(all_doc_tokens) # #1 >>> vocab = sorted(vocab) # #2 >>> len(vocab) 246 >>> len(all_doc_tokens) / len(vocab) # #3 1.959...
使用 set
数据类型确保没有标记被计数两次。在转换所有标记为小写之后,你的短语料库中只有 248 个拼写独特的标记。这意味着,平均而言,每个标记几乎被使用了两次(498 / 248
)。
>>> vocab # #1 ['"', "'s", ',', '-', '.', '2018', ';', 'a', 'ability', 'accurately', 'across', 'addressed', 'advanced', 'algorithm', 'algorithmic', 'algorithms', 'also', 'an', 'analysis', ... 'within', 'world', 'wrongful']
通常最好在回到文档中计算标记并将它们放入词汇表的正确位置之前,遍历整个语料库以建立起你的词汇表。如果你这样做,可以按字母顺序排列你的词汇表,这样更容易跟踪每个标记计数应该在向量中的大致位置。你还可以过滤掉非常频繁或非常稀有的标记,这样你就可以忽略它们并保持维度较低。当你想要计算比 1-gram 更长的 n-grams 时,这一点尤为重要。
假设你想要统计这个全部小写的 1-gram 词汇中的所有 248 个标记的计数,你可以重新组装你的计数向量矩阵。
>>> count_vectors = [] >>> for tokens in docs_tokens: ... count_vectors.append(Counter(tokens)) >>> tf = pd.DataFrame(count_vectors) # #1 >>> tf = tf.T.sort_index().T >>> tf = tf.fillna(0).astype(int) >>> tf " 's , ... within world wrongful 0 0 0 1 ... 0 0 0 1 0 0 3 ... 0 0 0 2 0 0 5 ... 0 0 0 3 2 0 0 ... 0 0 0 4 0 1 1 ... 0 0 0 5 0 0 0 ... 0 0 0 6 0 0 4 ... 0 1 0 ... 11 0 0 1 ... 0 0 1 12 0 0 3 ... 0 0 0 13 0 0 1 ... 0 0 0 14 0 0 2 ... 0 0 0 15 2 0 4 ... 1 0 0 16 rows × 246 columns
浏览几个这些计数向量,看看你能否在“算法偏见”维基百科文章中找到它们对应的句子。你能否通过仅查看向量来感受到每个句子在说什么?一个计数向量将文档的要点放入一个数值向量中。对于一个对单词含义一无所知的机器来说,将这些计数归一化为标记的总体频率是有帮助的。为此,你将使用 Scikit-Learn 包。
3.1.2 更快、更好、更容易的标记计数
现在你已经手动创建了你的计数向量,你可能想知道是否有人为所有这些标记计数和记账构建了一个库。你通常可以依靠 Scikit-Learn (sklearn
) 包来满足你所有的自然语言处理需求。如果你已经安装了 nlpia2
包,你已经安装了 Scikit-Learn (sklearn
)。如果你更愿意手动安装它,这是一种方式。
pip install scipy, scikit-learn
在 ipython
控制台或 jupyter notebook
中,你可以使用感叹号在行首运行 bash 命令。
!pip install scipy, scikit-learn
一旦你设置好了你的环境并安装了 Scikit-Learn,你就可以创建术语频率向量了。CountVectorizer
类似于你之前使用过的 Counter
类的列表。它是一个标准的转换器类,具有符合 sklearn API 的.fit()
和 .transform()
方法,适用于所有机器学习模型。
列出 3.1 使用 sklearn
计算单词计数向量
>>> from sklearn.feature_extraction.text import CountVectorizer >>> corpus = [doc.text for doc in docs] >>> vectorizer = CountVectorizer() >>> count_vectors = vectorizer.fit_transform(corpus) # #1 >>> print(count_vectors.toarray()) # #2 [[1 0 3 1 1 0 2 1 0 0 0 1 0 3 1 1] [1 0 1 0 0 1 1 0 1 1 0 0 1 0 0 0] [0 2 0 0 0 1 1 0 1 1 1 0 0 0 0 0]]
现在你有一个矩阵(在 Python 中实际上是一个列表的列表),代表了三个文档(矩阵的三行)和词汇表中每个词的计数组成了矩阵的列。这很快!只需一行代码vectorize.fit_transform(corpus)
,我们就达到了与你需要手动进行分词、创建词汇表和计数术语的几十行代码相同的结果。请注意,这些向量的长度为 16,而不是像你手动创建的向量一样的 18。这是因为 Scikit-Learn 对句子进行了稍微不同的分词(它只考虑两个或更多字母的单词作为标记)并且去掉了标点符号。
所以,你有三个向量,每个文档一个。现在呢?你能做什么?你的文档词数向量可以做任何向量可以做的很酷的事情,所以让我们先学习更多关于向量和向量空间的知识。
3.1.3 将你的代码向量化
如果你在网上读到关于“向量化代码”的内容,意味着与“向量化文本”完全不同。向量化文本是将文本转换为该文本的有意义的向量表示。向量化代码是通过利用强大的编译库(如numpy
)加速代码,并尽可能少地使用 Python 进行数学运算。之所以称其为“向量化”,是因为你可以使用向量代数表示法来消除代码中的for
循环,这是许多 NLP 管道中最慢的部分。而不是使用for
循环遍历向量或矩阵中的所有元素进行数学运算,你只需使用 numpy 来在编译的 C 代码中为你执行for
循环。Pandas 在其向量代数中使用了numpy
,所以你可以混合和匹配 DataFrame 和 numpy 数组或 Python 浮点数,所有这些都将运行得非常快。
>>> v1 = np.array(list(range(5))) >>> v2 = pd.Series(reversed(range(5))) >>> slow_answer = sum([4.2 * (x1 * x2) for x1, x2 in zip(v1, v2)]) >>> slow_answer 42.0 >>> faster_answer = sum(4.2 * v1 * v2) # #1 >>> faster_answer 42.0 >>> fastest_answer = 4.2 * v1.dot(v2) # #2 >>> fastest_answer 42.0
Python 的动态类型设计使得所有这些魔法成为可能。当你将一个float
乘以一个array
或DataFrame
时,不会因为你在两种不同类型上进行数学运算而引发错误,解释器会弄清楚你想要做什么,就像苏鲁一样。“让它成为”并且它将以最快的方式计算你所寻找的东西,使用编译的 C 代码而不是 Python 的for
循环。
提示
如果你在代码中使用向量化来消除一些for
循环,你可以将你的 NLP 管道加速 100 倍甚至更多。这意味着你可以尝试 100 倍以上的模型。柏林社会科学中心(WZB)有一个关于向量化的很棒的教程。而且如果你在网站的其他地方搜索,你会发现这可能是唯一一个对 NLP 和 AI 对社会影响的统计数据和数据有信任的来源。
3.1.4 向量空间
向量是线性代数或向量代数的主要构建块。它们是向量空间中的一组有序数字或坐标。它们描述了该空间中的位置或位置。或者它们可以用来标识该空间中的特定方向和大小或距离。向量空间是该空间中可能出现的所有可能向量的集合。因此,具有两个值的向量将位于二维向量空间中,具有三个值的向量将位于三维向量空间中,依此类推。
一张纸上的一小块图,或者图像中的像素网格,都是不错的二维向量空间。你可以看到这些坐标的顺序很重要。如果你颠倒了图纸上位置的 x 和 y 坐标,而没有颠倒所有的向量计算,那么你所有的线性代数问题的答案都会被颠倒。图纸和图像是矩形的,或者欧几里得的空间的例子,因为 x 和 y 坐标是彼此垂直的。本章中讨论的向量都是矩形的,欧几里得的空间。
地图或地球上的经度和纬度呢?那地理坐标空间肯定是二维的,因为它是一组有序的两个数字:纬度和经度。但每个纬度-经度对描述的是一个近似球面的点——地球表面。纬度-经度向量空间不是直线的,并且欧几里得几何在其中不完全适用。这意味着在计算由一对二维地理坐标或任何非欧几里得空间中的点表示的距离或接近度时,你必须小心。想想如何计算波特兰和纽约的纬度和经度坐标之间的距离。^([6])
图 3.1 展示了一种可视化三个二维向量(5, 5)
、(3, 2)
和(-1, 1)
的方法。向量的头部(由箭头尖端表示)用于标识向量空间中的位置。因此,该图中的向量头将位于这三个坐标对处。位置向量的尾部(由箭头的“后部”表示)始终位于原点,或(0, 0)
。
图 3. 1. 二维向量
三维向量空间呢?你生活的三维物理世界中的位置和速度可以用三维向量中的 x、y 和 z 坐标来表示。但你并不限于正常的三维空间。你可以有 5 个维度、10 个维度、5000 个维度,等等。线性代数都能得到相同的结果。随着维度的增加,你可能需要更多的计算能力。你会遇到一些“维度灾难”问题,但你可以等到第十章再处理它们。^([7])
对于自然语言文档向量空间,您的向量空间的维度是整个语料库中出现的不同单词数量的计数。对于 TF(和即将出现的 TF-IDF),我们将此维度称为大写字母“K”。这个不同单词的数量也是您语料库的词汇量大小,所以在学术论文中它通常被称为“|V|”。然后,您可以用一个 K 维向量描述这个 K 维向量空间中的每个文档。在关于哈利和吉尔的三个文档语料库中,K = 18(或者如果您的分词器去除了标点符号,则为 16)。因为人类不能轻易地可视化超过三维的空间,所以让我们暂时搁置大部分维度,看一看其中的两个,这样你就可以在这张平面上的页面上对这些向量进行可视化表示了。因此,在图 3.2 中,K 被缩减为两个,以便二维查看 18 维哈利和吉尔向量空间。
图 3. 2. 2D 项频率向量
K 维向量的工作方式相同,只是您不能轻易地可视化它们。现在您已经有了每个文档的表示,并且知道它们共享一个共同的空间,您可以比较它们了。您可以通过减去它们并计算它们之间的距离的长度来测量向量之间的欧几里德距离,这称为 2-范数距离。它是一只“乌鸦”飞行(直线)从一个向量的尖端(头部)到另一个向量的尖端的距离。查看线性代数附录 C,了解为什么这对于单词计数(项频率)向量是个糟糕的主意。
如果两个向量具有相似的方向,则它们是“相似的”。它们可能具有相似的大小(长度),这意味着单词计数(项频率)向量的长度大致相同。但是您是否在词汇量空间中对文档长度感兴趣?可能不。您希望您对文档相似性的估计发现相同单词的使用大致相同的次数和相似的比例。这样准确的估计会让您相信它们所代表的文档可能在讨论相似的内容。
图 3. 3. 2D 向量及其之间的角度
余弦相似度,是两个向量之间的夹角(θ)的余弦值。图 3.3 显示了如何使用方程 3.1 计算余弦相似度点积。余弦相似度在 NLP 工程师中很受欢迎,因为:
- 即使对于高维向量也能快速计算
- 对单个维度的变化敏感
- 对高维向量表现良好
- 其值介于 -1 和 1 之间
你可以使用余弦相似度而不拖慢你的 NLP 管道,因为你只需要计算点积。你可能会惊讶地发现,你不需要计算余弦函数就能得到余弦相似度。你可以使用线性代数点积,它不需要进行任何三角函数计算。这使得计算非常高效(快速)。余弦相似度独立地考虑每个维度及其对向量方向的影响,即使对于高维向量也是如此。TF-IDF 可能有数千甚至数百万个维度,因此你需要使用一个在维度数量增加时不会降低有用性的度量(称为维度灾难)。
余弦相似度的另一个重要优势是它输出一个介于 -1 和 +1 之间的值:
- -1 表示向量指向完全相反的方向 - 这只会发生在具有负值的向量上(TF-IDF 向量除外)
- 0 表示向量是垂直或正交的 - 这会在你的两个 TF-IDF 向量不共享任何相同单词(维度)时发生
- +1 表示两个向量完全对齐 - 这会在你的两个文档使用相同单词且相对频率相同的情况下发生
这样更容易猜测在管道内的条件表达式中使用的好阈值。以下是在你的线性代数教科书中归一化点积的样子:
方程式 3.1
[ A⋅B=|A||B|∗cos(θ)�⋅�=|�||�|∗���(�) ]
在 Python 中,你可能会使用类似以下的代码来计算余弦相似度:
>>> A.dot(B) == (np.linalg.norm(A) * np.linalg.norm(B)) * \ ... np.cos(angle_between_A_and_B)
如果你解出这个方程得到 np.cos(angle_between_A_and_B)
(称为“向量 A 和 B 之间的余弦相似度”),你可以导出计算余弦相似度的代码:
列表 3.2 Python 中的余弦相似度公式
>>> cos_similarity_between_A_and_B = np.cos(angle_between_A_and_B) \ ... = A.dot(B) / (np.linalg.norm(A) * np.linalg.norm(B))
用线性代数表示,这变成了方程式 3.2:
方程式 3.2 两个向量之间的余弦相似度
[ cos(θ)=A⋅B|A||B|���(�)=�⋅�|�||�| ]
或者在纯 Python 中,不使用 numpy
:
列表 3.3 在 Python 中计算余弦相似度
>>> import math >>> def cosine_sim(vec1, vec2): ... vec1 = [val for val in vec1.values()] # #1 ... 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)
因此,你需要计算你感兴趣的两个向量的点积 - 将每个向量的元素成对相乘 - 然后将这些乘积相加。然后你除以每个向量的范数(大小或长度)。向量范数与其从头到尾的欧几里德距离相同 - 其元素平方和的平方根。这个归一化点积,就像余弦函数的输出一样,将是介于 -1 和 1 之间的值。它是这两个向量之间夹角的余弦。它给出了这两个向量指向相同方向的程度的值。[8]
1 的余弦相似度代表指向所有维度上完全相同方向的标准化向量。这些向量可能具有不同的长度或大小,但它们指向相同的方向。请记住,你将点积除以每个向量的范数。因此,余弦相似度值越接近 1,两个向量在角度上越接近。对于 NLP 文档向量,如果余弦相似度接近 1,你就知道这些文档使用相似的词汇以相似的比例。因此,文档向量彼此接近的文档很可能在谈论相同的事情。
0 的余弦相似度代表两个向量没有共享成分。它们在所有维度上都是正交的,即在所有维度上都是垂直的。对于 NLP 的 TF 向量来说,只有当两个文档没有共同的词时才会出现这种情况。这并不一定意味着它们具有不同的含义或主题,只是它们使用完全不同的词语。
-1 的余弦相似度代表两个完全相反的向量,完全相反。它们指向相反的方向。对于简单的词频(词项频率)向量甚至是标准化的 TF 向量(稍后我们会讨论),这种情况永远不会发生。单词的计数永远不会是负数。因此,词频(词项频率)向量始终位于向量空间的同一“象限”中。你的任何词频向量都不可能在向量空间的一个象限中悄悄溜走。你的任何词频向量都不可能有与另一个词频向量相反的分量(词频),因为词频就是不能是负数。
在本章节中,你不会看到任何自然语言文档向量对的负余弦相似度值。但在下一章中,我们将发展出一种概念,即相互“相反”的单词和主题。这将显示为余弦相似度小于零,甚至是 -1 的文档、单词和主题。
如果你想要计算常规 numpy
向量的余弦相似度,比如由 CountVectorizer
返回的向量,你可以使用 Scikit-Learn 内置的工具。这是如何计算我们在 3.4 中计算的词向量 1 和 2 之间的余弦相似度的方法:
第 3.4 节 余弦相似度
>>> from sklearn.metrics.pairwise import cosine_similarity >>> vec1 = tf.values[:1,:] # #1 >>> vec2 = tf.values[1:2,:] >>> cosine_similarity(vec1, vec2) array([[0.117...]])
对词频(tf
)DataFrame 进行切片的操作可能看起来是检索向量的奇怪方式。这是因为 SciKit-Learn 用于计算余弦相似度的函数已经被优化为在大型向量数组(2-D 矩阵)上高效工作。这段代码将 DataFrame 的第一行和第二行切片为包含文本第一句中单词计数的 1xN 数组。这个第一句话来自于“算法偏见”文章的计数向量与该文章第二句话只有 11.7% 的相似度(余弦相似度为 0.117)。看起来第二句话与第一句话共享的单词非常少。
为了更深入地了解余弦距离,你可以检查代码 3.3,它会给你与sklearn
余弦相似度函数在等效的 numpy 数组中给出的 Counter 字典相同的答案。当你尝试预测一个 NLP 算法的输出,然后根据实际情况进行修正时,它会提高你对 NLP 工作原理的直觉。
自然语言处理实战第二版(MEAP)(二)(2)https://developer.aliyun.com/article/1517848