第七章:通过卷积神经网络(CNNs)在文本中找到知识的核心
本章内容包括
- 理解自然语言处理的神经网络
- 在序列中找到模式
- 使用 PyTorch 构建 CNN
- 训练一个 CNN
- 训练嵌入
- 对文本进行分类
在本章中,你将解锁卷积在自然语言处理中被误解的超能力。这将帮助你的机器通过检测单词序列中的模式以及它们与邻居的关系来理解单词。
卷积神经网络(CNNs)在计算机视觉(图像处理)领域大行其道。但很少有企业认识到 CNNs 在自然语言处理中的威力。这为你在自然语言处理学习中以及理解 CNNs 能做什么的企业家创造了机会。例如,2022 年,科尔·霍华德和汉内斯·哈普克(本书第一版的共同作者)利用他们的自然语言处理 CNN 专业知识帮助他们的初创公司自动化业务和会计决策。^([1])并且学术界的深度学习专家,如克里斯托弗·曼宁和杰弗里·辛顿,使用 CNNs 在自然语言处理领域击败竞争对手。你也可以。
那么为什么 CNNs 在行业和大型科技公司中没有引起注意呢?因为它们太好了——太有效率了。CNNs 不需要大量的数据和计算资源,这是大科技公司在人工智能领域的垄断力量的核心。他们对能够"扩展"到海量数据集的模型感兴趣,比如阅读整个互联网。拥有大数据访问权限的研究人员专注于利用他们在数据方面的竞争优势的问题和模型,即"新石油"。^([2])让人们为一个任何人都可以在自己的笔记本电脑上训练和运行的模型付钱是很困难的。
另一个更加平凡的原因是 CNNs 被忽视的原因是,为自然语言处理正确配置和调整的 CNNs 很难找到。我找不到一个在 PyTorch、Keras 或 TensorFlow 中为自然语言处理实现的 CNNs 的单一参考实现。而非官方的实现似乎将用于图像处理的 CNN 通道转置为在嵌入维度上创建卷积,而不是在时间上进行卷积。很快你就会明白为什么这是一个糟糕的想法。但别担心,你很快就会看到别人犯的错误,你将像专业人士一样构建 CNNs。你的 CNNs 将比博客圈中出现的任何东西更有效率,性能更高。
或许你正在问自己,为什么在自然语言处理领域新潮流是transformers时,你还应该学习卷积神经网络(CNNs)。你可能听说过GPT-J、GPT-Neo、PaLM等等。阅读完本章后,你将能够基于 CNNs 构建更好、更快、更便宜的自然语言处理模型,而其他人还在浪费时间和金钱在千亿参数的 transformers 上。你不需要大型 transformers 所需的昂贵计算资源和训练数据。^([3]) ^([4]) ^([5])
- PaLM:540B 参数
- GPT-3:175B 参数
- T5-11B:11B 参数(FOSS,胜过 GPT-3)
- GPT-J:6B 参数(FOSS,胜过 GPT-3)
- CNNs(本章中):少于 200k 参数
是的,在本章中,你将学会如何构建比你在新闻中读到的大型 Transformer 小一百万倍且更快的 CNN 模型。而 CNN 通常是完成任务的最佳工具。
7.1 单词序列中的模式
在以前的章节中,单个单词对你来说效果很好。你可以用单个单词说很多话。你只需要选择正确的单词或在一段文字中找到关键词,那通常就能捕捉到意思。而且在你以前解决的那些问题中,顺序并不是很重要。如果你将“初级工程师”或“数据科学家”等工作标题的所有单词都放入词袋(BOW)向量中,那么搅在一起的 BOW 包含了原始标题的大部分信息内容。这就是为什么本书中以前的所有例子在短语或单个单词上效果最好的原因。这就是为什么关键词通常足以了解一个工作标题的最重要信息或理解一部电影的主要内容。
这也是为什么要选择几个词来总结一本书或一份带有标题的工作是如此困难。对于短语,单词的出现是唯一重要的。当你想要表达一个完整的思想,不仅仅是一个标题时,你必须使用更长的单词序列。而且顺序很重要。
在 NLP 之前,甚至在计算机出现之前,人类使用一种叫做卷积的数学运算来检测序列中的模式。对于 NLP,卷积被用来检测跨越多个单词甚至多个句子的模式。最初的卷积是用鹅毛笔甚至粘土板上的楔形文字手工制作的!一旦计算机被发明出来,研究人员和数学家就会手工制作数学公式来匹配他们想要解决的每个问题。用于图像处理的常见手工制作内核包括拉普拉斯、索贝尔和高斯滤波器。在数字信号处理中,类似于 NLP 中使用的低通和高通卷积滤波器可以根据第一原理进行设计。如果你是一个视觉学习者或对计算机视觉感兴趣,通过查看维基百科上用于这些卷积滤波器的热图绘制,你可能会更容易理解卷积。这些滤波器甚至可能给你关于初始化 CNN 滤波器权重以加快学习并创建更可解释的深度学习语言模型的想法。
但是随着时间的推移,这变得乏味了,我们甚至不再认为手工制作的滤镜在计算机视觉或自然语言处理中很重要。相反,我们使用统计学和神经网络来自动学习在图像和文本中寻找的模式。研究人员开始使用线性全连接网络(多层感知器)。但是这些网络存在一个严重的问题,即泛化能力过强,无法识别当单词模式从句子开头移动到句子末尾时。全连接神经网络不具有尺度不变性和平移不变性。但是后来 David Rumelhart 发明了,Geoffrey Hinton 推广了反向传播方法,帮助 CNN 和深度学习使世界摆脱了长时间的人工智能冬季。[9] [10] 这种方法孕育了第一个实用的计算机视觉、时间序列预测和自然语言处理的 CNN。
想出如何将卷积与神经网络结合起来创建 CNN 只是神经网络所需要的提升。CNN 现在主导着计算机视觉。而对于自然语言处理,CNN 仍然是许多先进的自然语言处理问题中最有效的模型。例如,spaCy 切换到 CNNs 版本 2.0。CNNs 对于命名实体识别(NER)和其他单词标记问题非常有效。[11] 而且你的大脑中的 CNNs 似乎负责识别其他动物无法理解的语言模式。
7.1.1 尺度和平移不变性
CNNs 相对于以前的 NLP 算法的主要优势是,它们可以识别文本中的模式,无论这些模式在文本中出现在哪里(平移不变性)以及它们有多分散(尺度不变性)。TF-IDF 向量没有任何方法可以识别和从文本中的模式进行泛化。而全连接神经网络会从文本中特定位置的特定模式进行过度泛化。
早在 1990 年代,像 Yann LeCun、Yoshua Bengio 和 Geoffrey Hinton 这样的著名研究人员就已经开始将卷积用于计算机视觉和 OCR(光学字符识别)。[12] 他们从我们的大脑中得到了这个想法。神经网络通常被称为“类脑”计算,因为它们模仿或模拟了我们大脑中发生的事情。神经网络在软件中模拟了大脑(生物神经元网络)在生物硬件中的工作。而且因为 CNNs 基于大脑,所以它们可以用于各种“非处方”NLP 应用:语音、音频、文本、天气和时间序列。NLP CNNs 对于任何一系列符号或数值向量(嵌入)都很有用。这种直觉使你能够将你的 NLP CNNs 应用于各种各样的问题,你在工作中会遇到,比如金融时间序列预测和天气预测。
卷积的缩放不变性意味着即使别人将他们的单词模式拉长时间通过说话慢或添加大量废话,你仍可以理解他们。翻译的不变性意味着你可以理解人们的意图,无论他们先说好消息还是坏消息。你可能已经很善于处理来自父母、教师和老板的反馈,无论是真正的建设性批评还是即使“肉”隐藏在“表扬三明治”之内。也许是因为我们使用语言的微妙方式以及语言在文化和记忆中的重要性,卷积被建立在我们的大脑中。我们是唯一有卷积网络内置在大脑中的物种。有些人在处理声音的大脑区域——赫氏回旋部(HG)甚至有着高达三层的卷积层发生。1
你很快就会看到如何将平移和缩放不变的卷积滤波器的威力嵌入你自己的神经网络中。你将使用卷积神经网络对问题和“toots(Mastodon2”帖子进行分类,甚至还可以识别莫尔斯电码中的嘟嗒声和哔哔声。你的机器很快就能判断一个问题是有关人、物、历史日期还是一个一般概念。你甚至可以尝试看看问题分类器是否可以判断别人是否在约你出去。你可能会惊讶地发现,CNN 可以检测出你在网上阅读到的灾难性帖子之间的微妙差异:灾难性的 “birdsite” 帖子与现实中的灾难之间的差异。
7.2 卷积
卷积这个概念并不像听起来那么复杂。它的数学公式几乎和计算相关系数一样简单。相关系数帮助你测量模式和信号之间的协方差或相似性。事实上,它的目的和相关系数相同——模式识别。相关系数可以帮助你检测一系列数字和另一系列数字之间的相似性,这些数字代表你要匹配的模式。
7.2.1 处理自然语言文本的模板
你见过字母模板吗?字母模板是一个有印刷字母轮廓的纸板或塑料片。当你想给某物(例如店铺标志或橱窗展示)上字时,你可以使用模板,使你的标志看起来像印刷文字一样。你可以像使用可移动遮蔽胶带一样使用模板,以防止你涂绘到错误的位置。但在这个例子中,你要反向使用模板。而不是用模板画字,你要使用模板检测字母和单词的模式。你的 NLP 模板是一个带权重(浮点数)的数组,称为滤波器或内核。
因此,想象一下,你为文本中的九个字母(以及一个空格字符)创建了一份字母模板"are sacred"。想象一下,它恰好是你正在阅读的书中文本的大小和形状。
图 7.1 一个真实的模板
现在,在你的脑海中,将模板放在书的顶部,以覆盖页面,你只能看到符合模板切口的单词。你需要将该模板滑过页面,直到该模板与书中的这对单词对齐。在那时,你将能够通过模板或掩膜清晰地看到单词的拼写。文本的黑色字母会填补模板的空洞。而你看到的黑色数量是匹配程度的度量。如果你使用了白色模板,单词"are sacred"将闪耀出来,这将是你唯一能看到的单词。
如果你这样使用模板,将其滑动到文本中,以找到模式和文本之间的最大匹配,你就在使用模板进行卷积!当谈论深度学习和 CNN 时,模板被称为卷积核或过滤器。在 CNN 中,卷积核是浮点数数组而不是纸板剪影。卷积核被设计成匹配文本中的一般模式。你的文本也被转换成数字值的数组。卷积是将卷积核滑动过你的文本数字表示,以查看其中的内容。
十年前,在有了 CNN 之前,你不得不手工制作适合你想象的任何模式的卷积核。但是使用 CNN 时,除了决定卷积核的宽度 - 你认为需要多少个字母或单词来捕捉你需要的模式,你不需要编程卷积核。你的 CNN 优化器将填充卷积核中的权重。当你训练模型时,优化器会找到最能预测 NLP 问题目标变量的模式所匹配的最佳权重数组。反向传播算法会逐步调整权重,直到它们与你的数据的正确模式匹配。
为了对 CNN 的工作原理有一个完整的理解,你需要在脑海中增加一些与模板和卷积核相关的步骤,将其融合到一个自然语言处理流程中。CNN 需要执行三项任务来使用卷积核(模板)。
- 测量卷积核和文本之间的匹配或相似度
- 在文本中滑动卷积核寻找最大匹配值
- 使用激活函数将最大值转换为二进制值或概率。
你可以将印刷版的黑暗程度视为印版和文本之间匹配程度的一种度量。因此,卷积神经网络(CNN)的第一步是将核函数中的权重乘以文本中的数值,然后将所有乘积相加,得到总的匹配分数。这仅仅是核函数与该文本窗口之间的点积或相关性。
第二步是在文本上滑动窗口,并再次进行步骤 1 的点积。这个卷积窗口滑动、乘法和求和被称为卷积。卷积将一个数字序列转换为与原始文本序列大小相同的另一个数字序列。根据滑动和乘法(卷积)的细节,您可能得到一个稍微较短或较长的数字序列。但无论如何,卷积操作输出一个数字序列,其中每个可能的核函数位置都有一个数值。
第三步是判断文本中是否存在一个良好的匹配。为此,你的 CNN 将卷积输出的一系列值转换为一个单一的值。结果是一个表示核函数模式可能在文本中某处的概率的单一值。大多数 CNN 设计成将这一系列数值的最大值作为匹配的度量。这种方法被称为“最大池化”,因为它将卷积中的所有值集中到一个最大值中。
注意
如果你要寻找的模式在文本的不同位置上分布开来,那么你可能想尝试一些“均值池化”来处理一些核函数。
你可以看到,卷积使得你的 CNN 能够提取依赖于单词顺序的模式。这使得 CNN 的核函数能够识别自然语言文本意义上的微妙差别,而这些差别如果你只使用词袋(BOW)表示法的话就会丢失。
单词是神圣的。如果你以正确顺序使用正确的单词,你就能微调世界一点点。
—— 汤姆·斯托帕德
真实的事物
在前几章中,你通过学习如何最好地将文本分词为单词,并计算每个单词的向量表示来将单词视为神圣的。现在,你可以将这个技巧与卷积相结合,以便通过你的下一个 Mastodon 聊天机器人“微调世界”。^([16])
7.2.2 再多一点铅字
还记得字母模板的类比吗?反向字母模板对 NLP 来说并不是那么有用,因为硬纸板切割只能匹配单词的“形状”。您想要匹配单词在句子中的使用方式的含义和语法。那么你如何升级你的反向模板概念,使其更像你需要的 NLP?假设你想要你的模板检测(形容词,名词)
2-gram,例如 “right word” 和 “right order” 在汤姆·斯托帕德的引语中。以下是您如何用词性标记部分引用中的单词的方法。
>>> import pandas as pd >>> import spacy >>> nlp = spacy.load('en_core_web_md') # #1 >>> text = 'right ones in the right order you can nudge the world' >>> doc = nlp(text) >>> df = pd.DataFrame([ ... {k: getattr(t, k) for k in 'text pos_'.split()} ... for t in doc])
text pos_ 0 right ADJ 1 ones NOUN 2 in ADP 3 the DET 4 right ADJ 5 order NOUN 6 you PRON 7 can AUX 8 nudge VERB 9 the DET 10 world NOUN
就像你在第六章中学到的一样,你希望为每个单词创建一个向量表示,以便文本可以转换为数字,用于 CNN 中。
>>> pd.get_dummies(df, columns=['pos_'], prefix='', prefix_sep='')
text ADJ ADP AUX DET NOUN PRON VERB 0 right 1 0 0 0 0 0 0 1 ones 0 0 0 0 1 0 0 2 in 0 1 0 0 0 0 0 3 the 0 0 0 1 0 0 0 4 right 1 0 0 0 0 0 0 5 order 0 0 0 0 1 0 0 6 you 0 0 0 0 0 1 0 7 can 0 0 1 0 0 0 0 8 nudge 0 0 0 0 0 0 1 9 the 0 0 0 1 0 0 0 10 world 0 0 0 0 1 0 0
现在你的模板或内核将必须扩展一点以跨越两个 7-D 单热矢量。你将为单热编码向量中的 1 创建想象中的切割,以使孔的模式与您想要匹配的词性序列相匹配。你的形容词-名词模板在第一行和第一列中有形容词在 2-gram 开头的孔。你需要在第二行和第五列中为名词作为 2-gram 中的第二个单词的孔。当你将你的想象模板滑动到每一对词时,它将根据模板是否匹配文本输出布尔值True
或False
。
第一对单词将创建一个匹配:
0, 1 (right, ones) (ADJ, NOUN) _True_
将模板移动以覆盖第二个 2 克拉姆,它将输出 False,因为两个克拉姆以名词开头,以失败的方式结束
1, 2 (ones, in) (NOUN, ADP) False
继续使用剩余的单词,我们最终得到了这个 10 个词短语的 9 元素图。
跨度 | 对 | 匹配? |
0, 1 | (正确的,那些) | True (1) |
1, 2 | (那些,在) | False (0) |
2, 3 | (在,那个) | False (0) |
3, 4 | (正确的,右) | False (0) |
4, 5 | (正确的,秩序) | True (1) |
5, 6 | (秩序,你) | False (0) |
6, 7 | (你,可以) | False (0) |
7, 8 | (可以,推动) | False (0) |
8, 9 | (推动,那个) | False (0) |
9, 10 | (这个,世界) | False (0) |
恭喜。你刚刚做的是卷积。你将输入文本的较小块,本例中为 2 克拉姆,转换为显示你正在寻找的模式的匹配位置。将填充添加到您的令牌序列通常是有帮助的。并将您的文本剪切到最大长度。这样可以确保您的输出序列始终具有相同的长度,无论您的文本有多长您的内核。
卷积,然后是
- 一种转换…
- 可能已被填充的输入…
- 生成地图…
- 其中某些条件存在的输入的位置(例如,两个连续的副词)
章节后面,你将使用术语核和步幅来讨论你的模板以及如何将其滑动到文本上。在这种情况下,你的步幅为一,核大小为二。而对于词性向量,你的核被设计为处理 7 维嵌入向量。如果你使用相同大小的核,但将其以步幅为二滑动到文本上,那么你会得到以下输出:
跨度 | 配对 | 匹配? |
0, 1 | (right, ones) | True (1) |
2, 3 | (in, the) | False (0) |
4, 5 | (right, order) | True (1) |
6, 7 | (you, can) | False (0) |
8, 9 | (nudge, the) | False (0) |
在这种情况下,你的步幅运气很好,因为两个形容词-名词对之间的词数是偶数。所以你的核成功地检测到了模式的两个匹配项。但是在这种配置下,你只有 50%的几率会如此幸运。因此,使用步幅为一和核大小为二或更大的情况更为常见。
7.2.3 相关性与卷积
如果你忘记了,清单 7.1 应该会提醒你 Python 中相关性是什么样子的。(你也可以使用scipy.stats.pearsonr
)。
清单 7.1 相关性的 Python 实现
>>> def corr(a, b): ... """ Compute the Pearson correlation coefficient R """ ... a = a - np.mean(a) ... b = b - np.mean(b) ... return sum(a * b) / np.sqrt(sum(a*a) * sum(b*b)) >>> a = np.array([0, 1, 2, 0, 1, 2, 0, 1, 2]) >>> b = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8]) >>> corr(a, b) 0.316... >>> corr(a, a)
然而,相关性只在系列长度相同时才有效。而且你肯定希望创建一些能够处理比表示文本的数字序列更短的模式的数学内容。这就是数学家提出卷积概念的方式。他们将较长的序列分成与较短序列相同长度的较小序列,然后对这些序列对的每一个应用相关函数。这样,卷积可以处理任何两个序列的数字,无论它们的长度有多长或多短。所以在自然语言处理中,我们可以将我们的模式(称为核)设计得尽可能短。而标记(文本)的序列可以任意长。你可以在文本的滑动窗口上计算相关性,从而创建代表文本含义的相关系数序列。
7.2.4 卷积作为映射函数
CNNs(无论是在我们的大脑中还是在机器中)是 map-reduce 算法中的“映射”部分。它输出一个比原始序列短的新序列,但还不够短。这将在流水线的减少部分后面进行。注意每个卷积层的输出大小。
卷积的数学运算可以让你在文本中无论何处(或何时)都能检测到模式。如果一个自然语言处理算法生成的特征向量无论一个特定词语模式出现在何处(何时)都相同,我们称之为“时不变”。卷积是一个时不变的操作,因此非常适用于文本分类、情感分析和自然语言理解。与你目前使用的其他方法相比,时不变性是卷积的一个巨大优势。你的 CNN 输出向量为你提供了一致的表达方式,表达了文本中的思想,无论该思想在文本中的哪个位置表达出来。与单词嵌入表示不同,卷积将注意力集中在向量的顺序意义上,并不会将它们全部混合成毫无意义的平均值。
卷积的另一个优势是,它输出的文本向量表示大小始终相同,无论你的文本有多长。无论你的文本是一个词名还是一份长达一万字的文档,对该序列的卷积都会输出相同大小的向量来表示该文本的含义。卷积创建的嵌入向量可用于做各种预测,就像你在第六章中使用单词嵌入所做的一样。但现在,这些嵌入将作用于单词序列,而不仅仅是单个单词。你的嵌入,你的含义向量表示,无论你处理的文本是三个词“我爱你”还是更长的文本:“我对你感到深深的爱和欣慰。”爱的感觉或情感会在两个向量中相同的位置结束,尽管单词“爱”出现在文本的不同位置。文本的含义分布在整个向量上,形成所谓的“密集”向量表示。当你使用卷积时,文本向量表示中没有间隙。与之前章节中稀疏的 TF-IDF 向量不同,你的卷积输出向量的维度都是填充的,对你处理的每一小段文本都有意义。
7.2.5 Python 卷积示例
您将从一个纯 Python 实现的卷积开始。这将为您提供卷积的数学模型,更重要的是,为卷积的矩阵和向量形状提供心理模型。这将帮助您理解卷积神经网络中每一层的目的。对于这第一个卷积,您将在卷积核中硬编码权重以计算 2 点移动平均值。如果您想要从 Robinhood 的日常加密货币价格中提取一些机器学习特征,这可能很有用。或者也许更好的想象一下,您正在尝试解决一个可解决的问题,比如对像波特兰(俄勒冈州)这样多雨城市的降雨报告进行一些 2 点平均值的特征工程。或者更好的是,想象您正在尝试构建一个检测自然语言文本中副词部分的词性标签下降的检测器。因为这是一个硬编码的核,所以您现在不必担心训练或拟合您的卷积数据。
您将硬编码此卷积以检测数字序列中的模式,就像您在第二章中硬编码正则表达式来识别字符序列中的标记一样。当您硬编码卷积滤波器时,您必须知道您要寻找的模式,以便将该模式放入您的卷积系数中。这对于易于识别的模式非常有效,比如数值下降或数值短暂上升。这些是本章后面将要寻找的摩尔斯电码“文本”的模式。在本章的第三节中,您将学习如何利用这一技能在 PyTorch 中构建一个卷积神经网络,该网络可以自行学习在您的文本中寻找哪些模式。
在计算机视觉和图像处理中,您需要使用 2-D 卷积滤波器,这样您就可以检测垂直和水平模式,以及中间的所有内容。对于自然语言处理,您只需要 1 维卷积滤波器。您只需在一个维度上进行卷积,即时间维度,在您的标记序列中的位置。您可以将嵌入向量的组件,或者也许是其他词性,存储在卷积的通道
中。稍后会详细讨论这一点,等您完成纯 Python 卷积。以下是也许是最简单但有用的 1-D 卷积的 Python 代码。
列表 7.4 显示了如何在纯 Python 中创建一个 1-D 卷积,用于一个硬编码的核([.5, .5]
),其中只有两个权重为 .5
的权重。
这个核正在计算数字序列中两个数字的移动平均值。对于自然语言处理,输入序列中的数字表示词汇表中标记的出现(存在或不存在)。而且您的标记可以是任何东西,例如我们在示例中用于标记副词出现(存在性)的词性标签。或者输入可以是每个标记中词嵌入维度的波动数值。
这个移动平均滤波器可以检测到连续出现两个事物的情况,因为 (.5 * 1 + .5 * 1)
是 1
。代码会以数字 1
来告诉您它找到了某些东西。卷积对于像这样的其他自然语言处理算法可能会错过的模式非常擅长。与寻找两个词的两个实例不同,您将寻找连续出现的两个意思。而且您刚刚在上一章中了解了不同的意思方面,即单词向量的维度。现在,您只寻找单词的一个方面,即它们的词性。你要找的是两个连续的副词。
合适的词可能具有影响力,但没有一个词能像合适的暂停一样有效。
— 马克·吐温
你能找出两个连续出现的副词吗?我不得不借助 SpaCy 来找到这个例子。类似这样的微妙意义模式对于人类来说很难有意识地注意到。但是对于卷积滤波器来说,测量文本的副词特性只是一门数学问题。卷积将并行处理您可能正在寻找的所有其他意义方面。实际上,一旦您完成了第一个例子,您将对单词的所有方面运行卷积。当您使用前一章节中跟踪单词所有维度的词嵌入时,卷积效果最佳。
卷积将查看单词意思的所有维度以及所有维度的单词意义的模式。卷积神经网络(CNN)会查看您的目标输出(目标变量),以查找影响目标变量的单词嵌入的所有维度中的所有模式。对于这个例子,您将定义一个“副词句”为在句子中连续包含两个副词的句子。这只是为了帮助您看到一个非常简单的问题的数学计算。副词特性只是您需要在机器学习流程中从文本中提取的众多特征之一。CNN 将通过学习适当的副词特性、名词特性、停词特性和其他很多“特性”的组合来自动完成这种工程。现在,您只需手动完成这一个副词特性。目标是了解 CNN 可以学习识别数据中的哪些模式。
图 7.2 展示了如何使用 SpaCy 对引用进行词性标注,然后创建一个二进制系列来表示你正在搜索的单词的一个方面,即副词性。
列表 7.2 用词性标记引用
>>> nlp = spacy.load('en_core_web_md') >>> quote = "The right word may be effective, but no word was ever" \ ... " as effective as a rightly timed pause." >>> tagged_words = { ... t.text: [t.pos_, int(t.pos_ == 'ADV')] # #1 ... for t in nlp(quote)} >>> df_quote = pd.DataFrame(tagged_words, index=['POS', 'ADV']) >>> print(df_quote)
The right word may be ... a rightly timed pause . POS DET ADJ NOUN AUX AUX ... DET ADV VERB NOUN PUNCT ADV 0 0 0 0 0 ... 0 1 0 0 0
现在你有了你的一串ADV
的零和一,所以你可以用卷积来处理它,以匹配你正在寻找的模式。
列表 7.3 为卷积定义输入序列
>>> inpt = list(df_quote.loc['ADV']) >>> print(inpt)
[0, 0, 0, ... 0, 1, 1, 0, 0...]
哇,这种作弊效果太好了!我们清楚地看到在句子中有两个副词是连续的。让我们使用我们的卷积滤波器来找出确切的位置。
列表 7.4 纯 Python 中的卷积
>>> kernel = [.5, .5] # #1 >>> >>> output = [] >>> for i in range(len(inpt) - 1): # #2 ... z = 0 ... for k, weight in enumerate(kernel): # #3 ... z = z + weight * inpt[i + k] ... output.append(z) >>> >>> print(f'inpt:\n{inpt}') >>> print(f'len(inpt): {len(inpt)}') >>> print(f'output:\n{[int(o) if int(o)==o else o for o in output]}') >>> print(f'len(output): {len(output)}')
inpt: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0., 1, 1., 0, 0, 0., 1., 0, 0, 0] len(inpt): 20 output: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, .5, 1, .5, 0, 0, .5, .5, 0, 0] len(output): 19
现在你可以明白为什么你必须在输入序列的末尾停止for
循环了。否则,我们的内核中的 2 个权重将会溢出到输入序列的末尾。你可能在其他地方见过这种软件模式称为“map-reduce”。你可以看到如何使用 Python 内置函数map()
和filter()
来实现列表 7.4 中的代码。
如果你把和函数作为你的池化函数,你可以创建一个移动平均卷积,根据我们的 2 个连续副词的定义来计算文本的副词性。如果你想要计算一个无权重的移动平均,你只需要确保你的内核值都是1 / len(kernel)
,这样它们就会加起来为 1,并且都是相等的。
图 7.5 将创建一条线图,帮助你可视化卷积输出和原始的is_adv
输入重叠在一起。
图 7.5 输入(is_adv)和输出(副词性)的折线图
>>> import pandas as pd >>> from matplotlib import pyplot as plt >>> plt.rcParams['figure.dpi'] = 120 # #1 >>> import seaborn as sns >>> sns.set_theme('paper') # #2 >>> df = pd.DataFrame([inpt, output], index=['inpt', 'output']).T >>> ax = df.plot(style=['+-', 'o:'], linewidth=3)
你有没有注意到这个大小为 2 的内核的卷积的输出序列产生的输出比输入序列短一个?图 7.2 显示了这个移动平均卷积的输入和输出的线图。当你把两个数字分别乘以.5
然后相加时,你得到这两个数字的平均值。所以这个特定的内核([.5, .5]
)是一个非常小的(两个样本)移动平均滤波器。
图 7.2 is_adv
和副词性
卷积的线图
看着图 7.2,你可能会注意到它看起来有点像金融时间序列数据或每日降雨量值的移动平均或平滑滤波器。对于你的 GreenPill 令牌价格的 7 天移动平均,你将使用一个大小为 7 的卷积内核,每周的每一天都为0.142
。一个大小为 7 的移动平均卷积将会更加平滑你副词性中的尖峰,从而在你的线图中创建一个更加曲线的信号。但是除非你精心制作了一个包含七个连续副词的声明,否则你永远不会在任何有机引用中获得 1.0 的副词性分数。
你可以将列表 7.6 中的 Python 脚本泛化,以创建一个卷积函数,即使内核大小发生变化也能正常工作。这样你就可以在以后的例子中重复使用它。
清单 7.6 通用卷积函数
>>> def convolve(inpt, kernel): ... output = [] ... for i in range(len(inpt) - len(kernel) + 1): # #1 ... output.append( ... sum( ... [ ... inpt[i + k] * kernel[k] ... for k in range(len(kernel)) # #2 ... ] ... ) ... ) ... return output
你在这里创建的 convolve()
函数将输入乘以核权重相加。你也可以使用 Python 的 map()
函数来创建卷积。你使用了 Python 的 sum()
函数来 减少 输出中的数据量。这种组合使卷积算法成为一个你在计算机科学或数据科学课程中可能听说过的 map reduce 操作。
重要
像卷积这样的 map-reduce 操作高度可并行化。数据窗口的每个核乘法可以同时并行进行。这种可并行性是使卷积成为处理自然语言数据的一种强大、高效和成功的方式的原因。
7.2.6 PyTorch 1-D CNN 在 4-D 嵌入向量上
你可以看到 1-D 卷积是如何用于在令牌序列中查找简单模式的。在之前的章节中,你使用正则表达式来查找字符序列中的模式。但是对于涉及单词意义的多个不同方面的语法的更复杂模式呢?为此,你需要使用单词嵌入(来自第六章)结合 卷积神经网络。你想要使用 PyTorch 来处理所有这些线性代数操作的簿记。通过使用 4-D 独热编码向量来表示单词的词性,你将在下一个示例中简化它。稍后,你将学习如何使用 300-D GloVE 向量,这些向量除了保留单词的语法角色外,还跟踪单词的含义。
因为词嵌入或向量捕捉了单词中所有不同的意义组成部分,它们包括了词性。就像之前的广告引用示例一样,你将根据单词的词性匹配一个语法模式。但这次,你的单词将具有表示名词、动词和副词的 3-D 词性向量。你的新 CNN 可以检测到一个非常特定的模式,即一个副词后跟一个动词,然后是一个名词。你的 CNN 正在寻找马克·吐温引用中的“正确的时机”。如果需要帮助创建一个包含“正确时机”的 POS 标签的 DataFrame,请参考清单 7.2。
>>> tags = 'ADV ADJ VERB NOUN'.split() >>> tagged_words = [ ... [tok.text] + [int(tok.pos_ == tag) for tag in tags] # #1 ... for tok in nlp(quote)] # #2 >>> >>> df = pd.DataFrame(tagged_words, columns=['token'] + tags).T >>> print(df)
The right word may be ... a rightly timed pause . ADV 0 0 0 0 0 ... 0 1 0 0 0 ADJ 0 1 0 0 0 ... 0 0 0 0 0 VERB 0 0 0 0 0 ... 0 0 1 0 0 NOUN 0 0 1 0 0 ... 0 0 0 1 0
图 7.3 带有词性标记的句子
为了保持高效,PyTorch 不接受任意的 Pandas 或 numpy 对象。相反,你必须将所有输入数据转换为具有 torch.float
或 torch.int
数据类型(dtype
)对象的 torch.Tensor
容器。
清单 7.7 将 DataFrame 转换为正确大小的张量
>>> import torch >>> x = torch.tensor( ... df.iloc[1:].astype(float).values, ... dtype=torch.float32) # #1 >>> x = x.unsqueeze(0) # #2
现在你构建了我们想在文本中搜索的模式:副词、动词,然后名词。你需要为你关心的每个词性创建一个单独的过滤器或核。每个核将与其他核对齐,以同时在单词意义的所有方面找到你正在寻找的模式。
在此之前,您只需要担心一个维度,即副词标签。现在,您需要处理这些单词向量的所有 4 个维度,以确保模式正确。您需要协调四个不同的“特征”或数据通道。因此,对于一个 3 个词、4 个通道的核心,我们需要一个 4x3 矩阵。每一行代表一个通道(词性标签),每一列代表序列中的一个单词。单词向量是 4 维列向量。
>>> kernel = pd.DataFrame( ... [[1, 0, 0.], ... [0, 0, 0.], ... [0, 1, 0.], ... [0, 0, 1.]], index=tags) >>> print(kernel)
您可以看到,这个 DataFrame 只是您想要在文本样本中匹配的向量序列的精确副本。当然,您之所以能够做到这一点,是因为您在这一个玩具示例中知道您在寻找什么。在真实的神经网络中,深度学习优化器将使用反向传播来学习最有助于预测您的目标变量(标签)的向量序列。
机器如何匹配模式是如何可能的?是什么数学导致核心始终匹配其包含的模式?在图 7.4 中,您可以自己进行一些数据的滤波器跨越几个步骤的数学计算。这将帮助您了解所有这些是如何工作的,以及为什么它既简单又强大。
图 7.4 自己检查卷积模式匹配
在让 PyTorch 进行数学计算之前,您是否检查了图 7.4 中的数学计算?确保在让 PyTorch 进行数学计算之前进行此操作,以嵌入此数学模式到您的神经网络中,以便将来如果您需要调试 CNN 中的问题,您就可以进行数学计算。
在 PyTorch 或任何其他设计用于同时处理多个样本的深度学习框架中,您必须将核张量进行扩展,以添加一个维度来容纳额外的样本。您扩展的核(权重矩阵)需要与输入数据的批次具有相同的形状。第一个维度用于输入到卷积层的来自训练或测试数据集的样本。通常,这将是嵌入层的输出,并且已经具有适当的大小。但是,由于您正在硬编码所有权重和输入数据以了解 Conv1d 层的工作原理,因此您需要扩展 2-D 张量矩阵以创建 3-D 张量立方体。由于您只有一个引用要通过卷积推进数据集,因此您只需要在第一个维度上具有大小为 1 的尺寸。
列表 7.8 将硬编码的权重加载到 Conv1d 层中
>>> kernel = torch.tensor(kernel.values, dtype=torch.float32) >>> kernel = kernel.unsqueeze(0) # #1 >>> conv = torch.nn.Conv1d(in_channels=4, ... out_channels=1, ... kernel_size=3, ... bias=False) >>> conv.load_state_dict({'weight': kernel}) >>> print(conv.weight) tensor([[[1., 0., 0.], [0., 0., 0.], [0., 1., 0.], [0., 0., 1.]]])
最后,您准备好看看您手工制作的核心是否可以检测到文本中的副词、动词、名词序列。
列表 7.9 通过卷积层运行单个示例
>>> y = np.array(conv.forward(x).detach()).squeeze() >>> df.loc['y'] = pd.Series(y) >>> df 0 1 2 3 4 ... 15 16 17 18 19 token The right word may be ... a rightly timed pause . ADV 0 0 0 0 0 ... 0 1 0 0 0 ADJ 0 1 0 0 0 ... 0 0 0 0 0 VERB 0 0 0 1 0 ... 0 0 1 0 0 NOUN 0 0 1 0 0 ... 0 0 0 1 0 y 1.0 0.0 1.0 0.0 0.0 ... 0.0 3.0 0.0 NaN NaN
图 7.5 Conv1d 输出正确预测定时暂停
y 值达到最大值 3,其中内核中的所有 3 个值为 1 的部分与句子中的三个 1 完美匹配,形成了相同的词性标签模式。您的内核正确地检测到了句子末尾的副词、动词、名词序列。您卷积输出的值为 3 是正确的,因为在序列中第 16 个单词“rightly” 的位置,存在 3 个与您的模式匹配的单词。这是匹配您的模式的 3 个单词序列的位置,分别位于位置 16、17 和 18。而且,输出值为三是有意义的,因为每个匹配的词性都在您的内核中具有权重为一,总共有三个匹配。
别担心,您永远不必再为卷积神经网络手工制作内核…除非您想提醒自己数学是如何工作的,以便向他人解释。
7.2.7 自然示例
在眼睛和相机的光学世界中,卷积无处不在。当您通过偏振太阳镜向下看海洋或湖泊表面时,镜片会对光进行卷积以滤除噪声。偏振眼镜的镜片有助于渔民滤除散射光,并看穿水面下找到鱼。
至于更疯狂的例子,想象一下一只斑马站在围栏后面。斑马的条纹可以被视为一种视觉自然语言。斑马的条纹向捕食者和潜在伴侣发送关于斑马健康状况的信号。当斑马在草地、竹林或树干之间奔跑时发生的卷积会产生一种闪烁效果,使斑马难以捕捉。
在图 7.6 中,您可以将卡通围栏视为交替数值的内核。而背景中的斑马则像您的数据,其条纹中的光暗区域具有交替的数值。而且卷积是对称的,因为乘法和加法是可交换的操作。因此,如果您愿意,您可以将斑马的条纹视为滤波器,而一长段围栏视为数据。
图 7.6 斑马在围栏后面 ^([18])
想象一下图 7.6 中的斑马走在围栏后面,或者围栏在斑马前面滑动。当斑马行走时,围栏中的缝隙将定期与斑马的条纹对齐。这将在我们移动围栏(内核)或斑马时创建光与暗的图案。当斑马的黑色条纹与棕色围栏的缝隙对齐时,这些地方将变暗。当斑马的白色部分与围栏的缝隙对齐时,它们就能透过,因此斑马会显得更亮。因此,如果您想要识别黑色和白色的交替值或交替的数值,您可以在您的内核中使用交替的高(1)和低值(0)。
如果你不经常看到斑马在栅栏后面走动,也许下一个类比会更好理解。如果你在海滩上待一段时间,你可以把浪潮想象成海底的一种自然机械卷积。当波浪经过海底并接近海滩时,它们会上升或下降,这取决于水面下隐藏的东西,比如沙洲、大石头或礁石。沙洲和石头就像你试图用卷积神经网络检测的单词意义的组成部分一样。波浪在沙洲上涨的过程就像卷积乘法操作一样,在你的数据上波浪潮过去。
现在想象一下,你在靠近水边挖了一个洞。当浪潮爬上岸时,取决于波浪的高度,一些浪潮会溢入你的小水池中。你沙堡前的水池或护城河就像卷积中的减少或求和操作一样。事实上,你会看到我们后来使用的一种操作叫做“最大池化”,它在卷积神经网络中的行为非常像这样。最大池化帮助你的卷积测量出特定单词模式的“影响”,就像你的沙堆在海岸上累积了浪潮的影响一样。即使没有别的,这张关于浪潮和沙堡的图像也会帮助你在本章后面看到时记住技术术语最大池化。
自然语言处理实战第二版(MEAP)(四)(2)https://developer.aliyun.com/article/1517993