为什么在 NLP 分类任务中选择 CNN 呢? 它的主要好处是高效率。在许多方面,由于池化层和卷积核大小所造成的限制 (虽然可以将卷积核设置得更大),会导致丢弃大量的信息,但这并不意味着它们不是有用的模型。大家已经看到,利用 CNN 能够有效地对相对较大的数据集进行检测和预测情感,即使依赖 Word2vec 词嵌入,CNN 也可以在不映射整个语言的条件下,通过较少的词嵌入表示来运行。(本文节选自《自然语言处理实战》一书,我们将在文末抽取五位幸运用户,每人送出一本该书)
语言的真正力量不在于文字本身,而在于文字的间隔、顺序以及词的各种组合。有时候,语言的意义隐藏在文字的背后,蕴含在形成词的特定组合的意图和情感中。无论是人类还是机器,理解隐藏在文字背后的意图,对于同理心强、情商高的倾听者或自然语言的阅读者而言,都是一项重要的技能。就像在思想和观念中,正是词之间的联系创造了语言的深度、信息度和复杂度。除了理解单个词的含义,词之间还有各种各样巧妙的组合方式,有没有一些比 n-gram 匹配更灵活的方法,可以用来衡量这些组合词的意义呢? 我们如何从一个词序列中得到语义和情感——隐性语义信息,从而利用它来做一些事情呢? 更进一步地说,我们如何才能将这种隐藏的语义传达给冰冷的计算机来生成文本呢?
“机器生成的文本”这个短语甚至让人联想到由空洞的金属声音发出的一个个词块。机器也许能让人明白它表达的意思,但仅此而已。其中缺少的是什么呢? 是交流过程中人们变化的语调、流利度以及即使是在非常短暂的交谈中,人们也期待表露出来的个性特点,这些微妙之处存在于字里行间以及词的构建模式中。人们在交流时,会在他们的文字和演讲中蕴含各种语言模式。伟大的作家和演讲家会积极运用这些模式来制造非常好的效果。人们天生具有识别这些模式的能力,这种识别甚至是在无意识的状态下进行的,而这也正是机器生成的文本听起来很糟糕的原因,因为它们不具备这些模式。不过大家可以从人类生成的文本中挖掘这些模式,并将其赋给机器。
在过去的几年里,围绕神经网络的研究迅速开展起来,同时出现了大量可用的开源工具,神经网络在大型数据集中发现模式的能力得到大幅提升,并使 NLP 领域发生了巨大的转变。感知 机迅速转变为前馈网络 (一个多层感知机),并由此衍生出各种变体: 卷积神经网络和循环神经网络,并发展出各种应用于大型数据集上模式挖掘的更为有效和准确的工具。
正如大家看到的 Word2Vec 那样,神经网络给 NLP 领域带来了一个全新的方法。虽然设计神经网络最初的目的是作为一个学习量化输入的机器,这个领域已经从只能处理分类、回归问题 (主题分析、情绪分析) 发展到能够基于以前未见过的输入来生成新文本: 将短语翻译为另一种语言,对未见过的问题生成回复 (聊天机器人),甚至能够生成基于特定作者风格的新文本。
完全理解神经网络的数学原理对于使用本章介绍的工具并不重要,不过这确实有助于加强我们对神经网络内部如何运作的认识。另外,还可以简化神经网络结构 (层数或者神经元数量) 来改进效果,这也有助于大家了解神经网络如何赋予聊天机器人深度。神经网络让聊天机器人成为一个很好的、从表面上看不怎么健谈的倾听者。
7.1 语义理解
词的性质和奥妙与词之间的关系密切相关。这种关系至少有两种表达方式。
(1) 词序——下面两个句子含义完全不一样:
The dog chased the cat.
The cat chased the dog.
(2) 词的邻近度 (proximity)——下面句子的“shone”指的是句子另一端的“hull”:
The ship's hull, despite years at sea, millions of tons of cargo, and two mid-sea
collisions, shone like new.
这些关系的模式 (以及词本身存在的模式) 可以从两个方面来表示: 空间和时间。两者的区 别主要是: 对于前者,要像在书页上的句子那样来处理——在文字的位置上寻找关系 ; 对于后者, 要像说话那样来处理——词和字母变成了时间序列数据。这两者是密切相关的,但是它们标志着 神经网络处理方式的一个关键区别。空间数据通常通过固定宽度的窗口来查看,而时间序列则可 以对于未知的时间无限延展。
基本的前馈网络 (多层感知机) 能够从数据中提取模式,这些模式来自与权重相关的输入片段, 但它无法捕获到词条在空间或时间上的关系。不过前馈神经网络只是神经网络结构的开端部分,目前,自然语言处理领域中两个最重要的模型是卷积神经网络和循环神经网络,以及它们的各种变体。
在图 7-1 中,对神经网络输入层传入 3 个词条。每个输入层神经元都与隐藏层神经元全连接,并各自具有不同的权重。
提示:怎样将词条传入网络呢? 本章使用的两种主要方法是前面章节中使用的独热编码和词向 量。大家可以对输入进行独热编码——在向量中我们考虑的所有可能的词位置上都标记为 0,对正在编码的词的位置标记为 1。或者,也可以使用第 6 章中训练好的词向量。总之,需要将词表示为数字以便进行数学运算。
图 7-1 全连接神经网络
如果将这些词条的顺序从“See Jim run”改为“run See Jim”并将其传入网络中,不出所料, 会得到一个不同的结果。因此请记住,每个输入位置与每个隐藏层神经元都有一个特定的对应权 重 (x1 与 w1 相连,x2 与 w2 相连,以此类推)。
因为词条同时出现在一个样本中的不同位置,所以前馈网络可以学习词条之间的一些特定关系,但是大家可以很容易看出,对于 5 个、10 个或 50 个词条的长句子 (每个位置上都包含所有 可能的词对、三元组等),这会成为一个棘手的问题。幸运的是,我们还有其他可选方案。
7.2 工具包
Python 是神经网络工具包最丰富的语言之一。虽然很多主要的参与者 (如谷歌和 Facebook) 已经转移到较低级别的语言以便于密集计算的实现,不过依然留下了用 Python 在早期模型上投 入大量资源进行开发的痕记。两个主要的神经网络架构分别是 Theano 和 TensorFlow。这两者的 底层计算深度依赖 C 语言,不过它们都提供了强大的 Python API。Facebook 基于 Lua 语言开发 了 Torch,它在 Python 里面也有一个对应的 API 是 PyTorch。这些框架都是高度抽象的工具集, 适用于从头构建模型。Python 社区开发了一些第三方库来简化这些底层架构的使用。Lasagne(Theano) 和 Skflow(TensorFlow) 很受欢迎,我们选择使用 Keras,它在 API 的友好性和功能性 方面比较均衡。Keras 可以使用 TensorFlow 或 Theano 作为后端,这两者各有利弊,我们将使用 TensorFlow 后端来做演示,另外我们还需要 h5py 包来保存已训练模型的内部状态。
Keras 默认以 TensorFlow 作为后端,运行时第一行输出就会提醒大家目前使用的是哪个后端。大家可以通过在环境变量或脚本中修改配置文件来更换后端。Keras 的说明文档非常清晰完整,我们强 烈建议大家在上面多花点儿时间。不过我们在这里也提供一个快速概述:Sequential() 是一个神经网络的抽象类,用于访问 Keras 的基本 API,compile 方法主要用于构建底层权重及它们之间的 相互关系,而 fit 方法计算训练过程中产生的误差并实施最重要的应用反向传播过程。epochs、 batch_size 和 optimizer 是需要调优的超参数,从某种意义上来说,调参也是一门艺术。
遗憾的是,对于神经网络的设计和调优,没有一个放之四海而皆准的方法。对于特定的应用应选择哪种适合的框架,需要大家根据自己的经验和直觉来判断。不过如果能找到和当前的应用 相似的实现案例,那么完全可以使用这个框架并对实现进行调整来满足大家的需求。神经网络框 架或者所有这些花哨的东西并没有什么可怕的。现在我们把话题转回到基于图像处理的自然语言 处理。为什么还有图像? 稍微耐心学习一下就会明白了。
7.3 卷积神经网络
卷积神经网络 (convolutional neural net,CNN) 得名于在数据样本上用滑动窗口 (或卷积) 的概念。
卷积在数学中应用很广泛,通常与时间序列数据相关。在本章中,可以不用关注其中的高阶概念,只需要知道它是用一个可视化盒子在一个区域内滑动 (如图 7-2 所示)。大家将从图像上的滑动窗口概念入手,然后扩展到文本上的滑动窗口。总体来说,就是在较大的数据块上设置一个滑动窗口,每次滑动时只能看到窗口范围内的数据。
图 7-2 卷积窗口函数
7.3.1 构建块
卷积神经网络最早出现在图像处理和图像识别领域,它能够捕捉每个样本中数据点之间的空间关系,也就能识别出图像中的是猫还是狗在驾驶推土机。
卷积网络 (convolutional net),也称为 convnet(这个多出来的 n 很难发音), 不像传统的前馈网络那样对每个元素 (图像中的每个像素) 分配权重,而是定义了一组在图像上移动的过滤器 (filter,也称为卷积核、滤波器或者特征检测器)。这就是卷积!
在图像识别中,每个数据点的元素可以是黑白图像中的每个像素点,取值是 1(on) 或 0(off)。
图 7-3 电线杆图像
也可以是灰度图像中每个像素的强度 (如图 7-3 和图 7-4 所示),或者彩色图像中每个像素的每个颜色通道的强度。
图 7-4 电线杆图像的像素值
卷积核会在输入样本中 (在这个例子中,就是图像的像素值) 进行卷积或滑动。我们先暂 停一下,讲讲滑动是什么意思。在窗口“移动”的时候我们不会做任何事情,大家可以把它看作是一系列的快照,数据通过这个窗口的时候,会做一些处理,窗口向下滑动一点,就再做一 次处理。
提示:正是这个滑动 / 快照使卷积神经网络具有高度的并行性。对给定数据样本的每个快照都可以独 立于其他数据样本进行计算,后面的快照也不需要等待上一个快照。
我们谈论的这些卷积核有多大呢? 卷积核窗口大小的参数由模型构建器选择,并且高度依赖数据内容。不过其中还是有一些共性的。在图像数据中,大家通常会看到窗口大小为 3 × 3(3, 3) 像素。在本章后面回到 NLP 上时我们会更详细地讲解窗口大小的选择。
7.3.2 步长
注意,在滑动阶段,移动的距离是一个参数,一般不会超过卷积核宽度,每个快照通常都与相邻快照有重叠的部分。
每个卷积“走”的距离称为步长,通常设置为 1。只移动一个像素 (或其他小于卷积核宽度 的距离) 将使进入卷积核的不同输入在一个位置和下一个位置之间出现重叠。如果由于步长太大 而使卷积核之间没有重叠,就会失去像素 (在 NLP 中是词条) 与相邻像素之间的“模糊”效果。
这种重叠有一些有趣的特性,特别是在查看卷积核如何随时间变化的时候,这些特性非常明显。
7.3.3 卷积核的组成
到目前为止,我们已经描述了数据上的滑动窗口,以及通过这个窗口来观察数据,但还没有介绍如何处理观察到的数据。
卷积核由两部分组成:
- 一组权重 (就像第 5 章中给神经元分配的权重);
- 一个激活函数。
如前所述,卷积核通常是 3 × 3(也有其他大小和形状)。
提示:卷积核神经元与普通的隐藏层神经元十分相似,但是在扫描输入样本的整个过程中,每个卷 积核的权重是固定的,在整个图像中所有卷积核的权重都一样。卷积神经网络中的每个卷积核都是 独一无二的,但是在图像快照中每个卷积核的元素都是固定的。
当卷积核在图像上滑动时,每次前进一个步长,得到当前覆盖像素的快照,然后将这些像素 的值与卷积核中对应位置的权重相乘。
假设大家用的是 3 × 3 卷积核,从左上角开始,第一个像素 (0, 0) 乘以卷积核第一个位置 (0, 0) 上的权重,第二个像素 (0, 1) 乘以位置 (0, 1) 上的权重,以此类推。
然后对像素和权重 (对应位置) 的乘积求和,并传递到激活函数中 (如图 7-5 所示),通常选择 ReLU 函数 (线性修正单元)——我们待会再讨论这个问题。
在图 7-5 和图 7-6 中,xi 是位置 i 上的像素值,z0 是 ReLU 激活函数的输出 z_0 = max(sum(x * w), 0) 或 z0 = max(xi × wj)), 0)。该激活函数的输出将被记录在输出图像中的一个位置上。卷积核 滑动一个步长,处理下一个快照,并将输出值放在上一个输出值的旁边 (如图 7-6 所示)。
在一个层中有多个这样的卷积核,当它们在整个图像上进行卷积时,会各自创建一个新的“图像”——一个被“过滤”后的图像。假设有 n 个卷积核,在经过这个处理之后,将得到 n 个经过 过滤的新图像。
我们一会儿再来看看对这 n 个新图像的处理。
图 7-5 卷积神经网络步骤
图 7-6 卷积
7.3.4 填充
然而,在图像的边缘会发生一些有趣的事情。如果大家从输入图像的左上角开始一个 3 × 3 的卷积核,每次移动一个像素,当卷积核的最右侧边缘到达输入图像的最右侧边缘时停止,那么 输出的“图像”将比原图像窄两个像素。
Keras 提供了处理这个问题的工具。第一个策略是忽略输出维度变小的问题。在 Keras 中,可以设置参数 padding = 'valid'。使用这种方法时,需要注意下一层输入的维度。这种策 略的缺点是重叠位置上的内部数据点被多次传递到每个卷积核上,原始输入的边缘数据将被欠采 样。在比较大的图像上,这可能不是问题,但是如果把这个概念应用到 Twitter 数据上,例如, 在一个 10 个单词的数据集上进行欠采样,则可能会极大地改变输出结果。
另一个策略称为填充 (padding),即向输入数据的外部边缘添加足够多的数据,使边缘上的第一个数据点可以被视为内部数据点进行处理。这种策略的缺点是向输入数据中添加了可能不相 关的内容,导致偏离了输出结果。大家不需要专门去寻找生成虚假数据的模式,可以用几种不同 的方法进行填充以尽量减少不良影响。具体做法参见代码清单 7-1。
代码清单 7-1 Keras 中一个卷积层的神经网络
稍后将详细介绍实现细节。需要对这些数据位多加注意,大家使用的工具中已经对这些问题进行了很好的处理。
还有一些策略,例如在预处理过程中通过模拟已存在的边缘数据点来预测要填充位置上的值。不过这种策略危险性较大,NLP 应用中一般不会使用。
卷积流水线
现在有 n 个卷积核和 n 个新图像,接下来怎么处理呢? 和大多数神经网络应用一样,我们从一个已标注的数据集开始,任务目标也类似: 预测一个给定新图像的标签。最简单的方法是将每 个过滤后的图像串起来并输入到前馈层。
提示:大家可以将这些经过过滤的图像传递到第二个卷积层,它也有一组卷积核。在实践中,这是最常见的架构,稍后我们会再详细介绍。多层卷积网络对抽象层的学习路径一般是: 首先是边缘, 然后是形状 / 颜色,最后是含义。
不管大家在网络中添加了多少层 (卷积层或其他层),一旦得到一个最终输出,就可以计算出误差并通过网络进行反向传播该误差。
因为激活函数是可微的,所以可以像之前一样反向传播并更新各个卷积核的权重。然后网络会学习到需要什么样的卷积核才能为给定的输入获得正确的输出。
大家可以将此过程视为神经网络在学习检测和提取信息,以便让后面的层能更容易地进行处理。
7.3.5 学习
就像所有神经网络一样,卷积核本身会以初始化为接近零的随机值的权重开始。那么输出的“图像”怎样才不会是噪声呢? 在最初的几轮迭代训练中,它确实只是噪声。
但是大家构建的分类器会根据各个输入数据,从期望标签中获得一定的误差值,并通过激活函数将输入数据反向传播给卷积核。对于误差值的反向传播,我们还需要计算误差对输入权重的导数。
当卷积层刚出现时,它用的是上层梯度对输入权重的导数。这个计算和正常的反向传播类似,对于给定训练样本,卷积核权重会在多个位置上输出对应的结果。
梯度对卷积核权重的导数的具体计算超出了本书的范围,不过可以简单介绍一下,对于一个给定卷积核的权重,梯度是前向传播过程中卷积的每个位置上梯度的和。这是一个相当复杂的公式,两个求和及多个叠加式如下:
公式 7-1 卷积核权值的梯度之和
这一概念与常规的前馈网络基本相同,即计算出每个特定权重对系统总体误差的贡献,然后再来决定如何更好地调整权重,使其能够在后面的训练样本上尽量减小误差。这些细节对于理解卷积神经网络在自然语言处理中的应用并不是特别重要,但还是希望大家对如何调整神经网络结构有直观的认识,在本书后面的内容中会构建这些示例。
小结
- 卷积是在一个大的数据集上滑动窗口 (使关注点保持在整体的一个子集上)。
- 神经网络可以像处理图像一样处理文本并“理解”它们。
- 用 dropout 来阻碍学习过程实际上是有帮助的。
- 情感不仅存在于词中,还存在于使用的语言模式中。
- 神经网络有很多可调参数。
附:机器学习常见工具与技术
许多自然语言处理都涉及机器学习,所以理解机器学习的一些基本工具和技术是有益处的。有些工具已经讨论过,有些还没有,但这里我们会讨论所有这些工具。
D.1 数据选择和避免偏见
数据选择和特征工程会带来偏见的风险 (用人类的话来说)。一旦我们把自己的偏见融入算法中,通过选择一组特定的特征,模型就会适应这些偏见并产生带有偏差的结果。如果我们足够幸运能在投入生产之前发现这种偏见,那么也需要投入大量的工作来消除这种偏见。例如,必须重新构建和重新训练整个流水线,以便能够充分利用分词器的新词汇表。我们必须重新开始。
一个例子是著名的 Word2vec 模型的数据和特征选择。Word2vec 是针对大量的新闻报道进行训练的,从这个语料库中选择了大约 100 万个 n-gram 作为这个模型的词汇表 (特征)。它产生了一个使数据科学家和语言学家兴奋的模型,后者能够对词向量 (如“king − man + woman = queen”) 进行数学运算。但随着研究的深入,在模型中也出现了更多有问题的关系。
例如,对于“医生 − 父亲 + 母亲 = 护士”这个表达式,“护士”的答案并不是人们希望的无偏见和合乎逻辑的结果。性别偏见在不经意间被训练到模型中。类似的种族、宗教甚至地理区域偏见在原始的 Word2vec 模型中普遍存在。谷歌公司的研究人员无意制造这些偏见,偏见存在于数据中,即他们训练 Word2vec 使用的谷歌新闻语料库中词使用统计的数据。
许多新闻报道只是带有文化偏见,因为它们是由记者撰写的,目的是让读者开心。这些记者描写的是一个存在制度偏见和现实生活中人们对待事件的偏见的世界。谷歌新闻中的词使用统计数据仅仅反映的是,在母亲当中当护士的数目要比当医生的多得多,同时在父亲当中当医 生的数目比当护士的多得多。Word2vec 模型只是为我们提供了一个窗口,让我们了解我们创建的世界。
幸运的是,像 Word2vec 这样的模型不需要标记训练数据。因此,我们可以自由选择任何喜欢的文本来训练模型。我们可以选择一个更平衡的、更能代表大家希望模型做出的信念和推理的数据集。当其他人躲在算法背后说他们只是按照模型做事时,我们可以与他们分享自己的数据集,这些数据集更公平地代表了一个社会,在这个社会里,我们渴望为每个人提供平等的机会。
当训练和测试模型时,大家可以依靠自己天生的公正感来帮助决定一个模型何时可以做出影响用户生活的预测。如果得到的模型以我们希望的方式对待所有用户,那么我们可以在晚上睡个好觉。它还可以帮助密切关注那些与大家不同的用户的需求,特别是那些通常处于社会不利地位的用户。如果需要更正式的理由来证明自己的行为,大家还可以学习更多关于统计学、哲学、伦理学、心理学、行为经济学和人类学的知识,来增强大家在本书中学到的计算机科学技能。
作为一名自然语言处理实践者和机器学习工程师,大家有机会训练出比人类做得更好的机器。老板和同事不会告诉大家应该在训练集中添加或删除哪些文本,大家自己有能力影响塑造整体社区和社会的机器的行为。
我们已经为大家提供了一些关于如何组装一个带有更少偏见和更公平的数据集的想法。现在,我们将展示如何使得到的模型与无偏见数据相拟合,以便它们在现实世界中精确和有用。
D.2 模型拟合程度
对于所有机器学习模型,一个主要的挑战是克服模型过度优异的表现。什么是“过度优异” 呢? 在处理所有模型中的样本数据时,给定的算法都可以很好地在给定数据集中找到模式。但是考虑到我们已经知道训练集中所有给定样本的标签 (如果不知道其标签表明它不在训练集中), 因此算法在训练样本的上述预测结果不会特别有用。我们真正的目的是利用这些训练样本来构建一个有泛化能力的模型,能够为一个新样本打上正确标签。尽管该样本与训练集的样本类似,但是它是训练集以外的样本。在训练集之外新样本上的预测性能就是我们想优化的目标。
我们称能够完美描述 (并预测) 训练样本的模型“过拟合”(overfit)(如图 D-1 所示)。这样的模型将很难或没有能力描述新数据。它不是一个通用的模型,当给出一个不在训练集中的样本时,很难相信它会做得很好。
图 D-1 训练样本上的过拟合现象
相反,如果我们的模型在训练样本上做出了许多错误的预测,并且在新样本上也做得很差,则称它“欠拟合”(underfit)(如图 D-2 所示)。在现实世界中,这两种模型都对预测作用不大。因此,下面看看哪些技术能够检测出上述两种拟合问题,更重要的是,我们还会给出一些避免上述问题的方法。
图 D-2 训练样本上的欠拟合现象
D.3 数据集划分
在机器学习实践中,如果数据是黄金,那么标注数据就是 raritanium(某游戏里的一种珍贵 资源)。我们的第一直觉可能是获取带标注数据并把它们全部传递给模型。更多的训练数据会产生更有弹性的模型,对吧? 但这使我们没有办法测试这个模型,只能心中希望它在现实世界中能产生好的结果。这显然是不切实际的。解决方案是将带标注的数据拆分为两个数据集,有时是 3 个数据集: 一个训练集、一个验证集,在某些情况下还有一个测试集。
训练集是显而易见的。在一轮训练中,验证集是我们保留的对模型隐藏的一小部分带标注数据。在验证集上获得良好性能是验证经过训练的模型在训练集之外的新数据上表现良好的第一步。大家经常会看到将一个给定的标注数据集按照训练与验证比 80%/20% 或 70%/30% 进行划分。测试集类似于验证集,也是带标注训练数据的子集,用于测试模型并度量性能。但是这个测试集与验证集有什么不同呢? 在组成上,它们其实没有任何不同,区别在于使用它们的方法。
在训练集上对模型进行训练时,会有若干次迭代,迭代过程中会有不同的超参数。我们选择的最终模型将是在验证集上执行得最好的模型。但是这里有一个问题,我们如何知道自己没有 优化一个仅仅是高度拟合验证集的模型? 我们没有办法验证该模型在其他数据上的性能是否 良好。这就是我们的老板或论文的读者最感兴趣的地方——该模型在他们的数据上的效果到底 如何?
因此,如果有足够的数据,需要将标注数据集的第三部分作为测试集。这将使我们的读者 (或老板) 更有信心,确信模型在训练和调优过程中在从未看到的数据上也可以获得很好的效果。一旦根据验证集性能选择了经过训练的模型,并且不再训练或调整模型,那么就可以对测试集中的每个样本进行预测 (推理)。假如模型在第三部分数据上表现良好,那么它就有不错的泛化性。为了得到这种具有高可信度的模型验证,大家经常会看到数据集按照 60%/20%/20% 的训练 / 验证 / 测试比进行划分的情形。
提示:在对数据集进行训练集、验证集和测试集的划分之前,对数据集进行重新排序是非常重要的。我们希望每个数据子集都是能代表“真实世界”的样本,并且它们需要与期望看到的每个标签的比大致相同。如果训练集有 25% 的正向样本和 75% 的负向样本,那么同样也希望测试集和验证集也有 25% 的正向样本和 75% 的负向样本。如果原始数据集的前面都是负向样本,并且在将数据集划分为 50%/50% 比的训练集 / 测试集前没有打乱数据,那么在训练集中将得到 100% 的负向样本,而在测试集中将得到 50% 的负向样本。这种情况下,模型永远不能从数据集中的正向样本中学习。
D.4 交叉拟合训练
另一个划分训练集 / 测试集的方法是交叉验证或者 k 折交叉验证 (如图 D-3 所示)。交叉验证背后的概念和我们刚讨论过的数据划分非常相似,但是它允许使用所有的带标记数据集进行训练。这个过程将训练集划分为 k 等分,或者说 k 折。然后通过将 k − 1 份数据作为训练集训练模 型并在第 k 份数据上进行验证。之后将第一次尝试中用作训练的 k − 1 份数据中的一份数据作为验证集,剩下的 k − 1 份数据成为新训练集,进行重新训练。
图 D-3 k 折交叉验证
该技术对于分析模型的结构和寻找对各个验证数据性能表现良好的超参数具有重要价值。一旦选择了超参数,还需要选择表现最好的经过训练的模型,因此很容易受到上一节所表述的偏见的影响,因此,在此过程中仍然建议保留一份测试集。
这种方法还提供了关于模型可靠性的一些新信息。我们可以计算一个 P 值,表示模型发现的输入特征和输出预测之间的关系的可能性在统计上是显著的,而不是随机选择的结果。如果训练集确实是真实世界的代表性样本,那么这将是一个非常重要的新信息。
这种对模型有额外信心的代价是,需要 k 倍的训练时间来进行 k 折的交叉验证。所以,如果 想要得到关于问题的 90% 的答案,通常可以简单地做 1 折交叉验证。这个验证方法与我们之前做的训练集 / 验证集划分方法完全相同。我们不会对模型这个对真实世界的动态描述的可靠性有 100% 的信心,但是如果它在测试集中表现良好,也可以非常自信地认为它是预测目标变量的有用模型。所以通过这种实用方法得到的机器学习模型对大多数商业应用来说都是有意义的。
D.5 抑制模型
在 model.fit() 中,梯度下降过分热衷于追求降低模型中可能出现的误差。这可能导致过拟合,即学到的模型在训练集上效果很好,但是在新的未见样本集 (测试集) 上却效果很差。因此,我们可能希望“保留”对模型的控制。以下是 3 种方法:
- 正则化 ;
- 随机 dropout;
- 批归一化。
D.5.1 正则化
在所有机器学习模型中,最终都会出现过拟合。幸运的是,有几种工具可以解决这个问题。第一个是正则化,它是对每个训练步骤的学习参数的惩罚。它通常但不总是参数本身的一个因子。其中,L1 范数和 L2 范数是最常见的做法。
L1 正则化:
L1 是所有参数 (权重) 的绝对值与某个 λ(超参数) 乘积的和,通常是 0 到 1 之间的一个小浮点数。这个和应用于权重的更新——其思想是,较大的权重会产生较大的惩罚,因此鼓励模型 使用更多的、均匀的权重......
L2 正则化:
类似地,L2 是一种权重惩罚,但定义略有不同。这种情况下,它是权重的平方与某个 λ 乘 积的和,这个 λ 值是一个要在训练前选择的单独超参数。
D.5.2 dropout
在神经网络中,dropout 是另一个解决过拟合的办法——乍一看似乎很神奇。dropout 的概念是,在神经网络的任何一层,我们都会在训练的时候,按一定比例关闭通过这一层的信号。注意,这只发生在训练期间,而不是推理期间。在所有训练过程中,网络层中一部分神经元子集都会被 “忽略”,这些输出值被显式地设置为零。因为它们对预测结果没有输入,所以在反向传播步骤中不会进行权重更新。在下一个训练步骤中,将选择层中不同权重的子集,并将其他权重归零。
一个在任何时间都有 20% 处于关闭状态的大脑的网络该如何学习呢? 其思想是,没有一个特 定的权重路径可以完全定义数据的特定属性。该模型必须泛化其内部结构,以便该模型通过神经元的多条路径都能够处理数据。
被关闭的信号的百分比被定义为超参数,因为它是一个介于 0 和 1 之间的浮点数。在实践中,从 0.1 到 0.5 的 dropout 通常是最优的,当然,这是依赖模型的。在推理过程中,dropout 会被忽 略,从而充分利用训练后的权值对新数据进行处理。
Keras 提供了一种非常简单的实现方法,可以在本书的示例和代码清单 D-1 中看到。
D.5.3 批归一化
神经网络中一个称为批归一化的新概念可以帮助对模型进行标准化和泛化。批归一化的思想 是,与输入数据非常相似,每个网络层的输出应该归一化为 0 到 1 之间的值。关于如何、为什么、 什么时候这样做是有益的,以及在什么条件下应该使用它,仍然存在一些争议。我们希望大家自 己去对这个研究方向进行探索。
但是 Keras 的 BatchNormalization 层提供了一个简单的实现方法,如代码清单 D-2 所示。
D.6 非均衡训练集
机器学习模型的好坏取决于提供给它们的数据。只有当样本中涵盖了希望在预测阶段的所有 情况时,拥有大量的数据才有帮助,并且数据集涵盖每种情况仅仅一次是不够的。想象一下我们 正试图预测一副图像到底是一只狗还是一只猫。这时我们手里有一个训练集,里面包含 20 000 张猫的照片,但是狗的照片只有 200 张。如果要在这个数据集中训练一个模型,那么这个模型很 可能只是简单地学会将任何给定的图像都预测为一只猫,而不管输入是什么。从模型的角度来说, 这个结果还可以接受,对不对? 我的意思是,对 99% 的训练样本的预测结果都是正确的。当然, 这个观点实际完全站不住脚,这个模型毫无价值。但是,完全超出了特定模型的范围之外,造成 这种失败的最可能原因是非均衡训练集。
模型可能会非常关注训练集,其原因很简单,来自标记数据中过采样类的信号会压倒来自欠 采样类的信号。权重将更经常地由主类信号的误差进行更新,而来自小类的信号将被忽视。获得 每个类的绝对均匀表示并不重要,因为模型自己能够克服一些噪声。这里的目标只是让类的比例 达到均衡水平。
与任何机器学习任务一样,第一步是长时间、仔细地查看数据,了解一些细节,并对数据实 际表示的内容进行一些粗略的统计。不仅要知道有多少数据,还要知道有多少种类的数据。
那么,如果事情从一开始就没有特别之处,大家会怎么做呢? 如果目标是使类的表示均匀 (确 实如此),则有 3 个主要方法可供选择: 过采样、欠采样和数据增强。
D.6.1 过采样
过采样是一种重复采样来自一个或多个欠表示类的样本的技术。我们以先前的狗 / 猫分类示 例为例 (只有 200 只狗,有 20 000 只猫)。我们可以简单地重复 100 次已有的 200 张狗的图像, 最终得到 40 000 个样本,其中一半是狗,一半是猫。
这是一个极端的例子,因此会导致自身固有的问题。这个网络很可能会很好地识别出这 200 只特定的狗,而不能很好地推广到其他不在训练集中的狗。但是,在不那么极端不平衡的情况下, 过采样技术肯定有助于平衡训练集。
D.6.2 欠采样
欠采样是同一枚硬币的反面。在这里,就是从过度表示的类中删除部分样本。在上面的猫 / 狗示例中,我们将随机删除 19 800 张猫的图片,这样就会剩下 400 个样本,其中一半是狗,一 半是猫。当然,这样做本身也有一个突出的问题,就是我们抛弃了绝大多数的数据,而只在一个 不那么宽泛的数据基础上进行研究。上述例子中这样的极端做法并不理想,但是如果欠表示类本 身包含大量的样本,那么上述极端做法可能是一个很好的解决方案。当然,拥有这么多数据绝对 是太奢侈了。
D.6.3 数据增强
数据增强有点儿棘手,但在适当的情况下它可以给我们带来帮助。增强的意思是生成新的数 据,或者从现有数据的扰动中生成,或者重新生成。AffNIST 就是这样一个例子。著名的 MNIST 数据集由一组手写的 0~9 数字组成 (如图 D-4 所示)。AffNIST 在保留原始标签的同时,以各种 方式对每个数字进行倾斜、旋转和缩放。
图 D-4 最左侧列中的条目是原始 MNIST 中的样本,其他列都是经仿射 转换后包含在 affNIST 中的数据 (图片经“affNIST”授权)
这种特别的做法的目的并不是平衡训练集,而是使像卷积神经网络一样的网络对以其他方式 编写的新数据更具弹性,但这里数据增强的概念仍然适用。
不过,大家必须小心,添加不能真正代表待建模型数据的数据有可能弊大于利。假设数据集 是之前的 200 只狗和 20 000 只猫组成的图片集。我们进一步假设这些图像都是在理想条件下拍 摄的高分辨率彩色图像。现在,给 19 000 名幼儿园教师一盒蜡笔并不一定能得到想要的增强数 据。因此,考虑一下增强的数据会对模型产生什么样的影响。答案并不是在任何时候都清晰无比, 所以如果一定要沿着这条路径走下去的话,在验证模型时请记住模型的影响这一点,并努力围绕 其边缘进行测试,以确保没有无意中引入意外的行为。
最后,再说一件可能价值最小的事情,但这的确是事实: 如果数据集“不完整”,那么首先 应该考虑回到原来的数据源中寻找额外的数据。这种做法并不总是可行,但至少应该把它当作一 种选择。
D.7 性能指标
任何机器学习流水线中最重要的部分都是性能指标。如果不知道学到的机器学习模型运行得 有多好,就无法让它变得更好。当启动机器学习流水线时,要做的第一件事是在任何 sklearn 机 器学习模型上设置一个性能度量方法,例如“.score()”。然后我们构建一个完全随机的分类 / 回归 流水线,并在最后计算性能分数。这使我们能够对流水线进行增量式改进,从而逐步提高分数, 以便更接近最终的目标。这也是让老板和同事确信大家走在正确的轨道上的好方法。
D.7.1 分类的衡量指标
对分类器而言,我们希望它做对两件事: 一是用类标签标记真正属于该类的对象,二是不用 这个标签去标记不属于此类的对象。这两件事对应得到的正确计数值分别称为真阳 (true positive) 和真阴 (true negative)。如果有一个 numpy 数组包含模型分类或预测的所有结果,那么就可以计 算出正确的预测结果,如代码清单 D-3 所示。
通常而言,对模型预测错误的计数也很重要,如代码清单 D-4 所示。
有时,这 4 个数合并成一个 4 × 4 矩阵,称为误差矩阵或混淆矩阵。代码清单 D-5 给出了混 淆矩阵中预测值和真实值的样子。
在混淆矩阵中,我们希望对角线 (左上角和右下角) 上的数字较大,希望对角线外的数字 (左 上角和左下角) 较小。然而,正向类和负向类的顺序是任意的,所以有时可能会看到这个表的数 字被调换了位置。请始终标记好混淆矩阵的列和下标。有时可能会听到统计学家把这个矩阵称为 分类器列联表,但如果坚持使用“混淆矩阵”这个名字的话,就可以避免混淆。
对于机器学习分类问题,有两种有用的方法可以将这 4 种计数值中的一些指标组合成一个性 能指标: 正确率 (precision) 和召回率 (recall)。信息检索 (搜索引擎) 和语义搜索就是此分类 问题的例子,因为那里的目标是将文档分为 (和输入查询) 匹配或不匹配两类。第 2 章中,我们 学习过词干还原和词形归并如何能够提高召回率,但同时降低了正确率。
正确率度量的是模型在检测所感兴趣类的所有对象 (称为正向类) 的能力,因此它也被称为 正向预测值 (positive predictive value)。由于真阳是预测正确的正向类样本数目,而假阳是错误地 标记为正向类的负向类样本数目,因此可以按照代码清单 D-6 所示来计算正确率。
上述例子中的混淆矩阵给出了约 57% 的正确率,因为在所有预测为正向类的样本中有约 57% 是正确的。
召回率和正确率类似,它也被称为灵敏度、真阳率或查全率。因为数据集中的样本总数是真 阳 (true positive) 和假阴 (false negative) 的和,所以可以计算召回率,即检测到的预测正确的 正向类样本占所有样本的百分比,代码如代码清单 D-7 所示。
这就是说上面例子中得到的模型检测到了数据集中 80% 的正向类样本。
D.7.2 回归的衡量指标
用于机器学习回归问题的两个最常见的性能评价指标是均方根误差 (RMSE) 和皮尔逊相关系数 (R2)。事实证明,分类问题背后实际上是回归问题。因此,如果类标签已经转换为数字 (就像我们在上一节中所做的那样),就可以在其上使用回归度量方法。下面的代码示例将复用上一节的那些预测值和真实值。RMSE 对于大多数问题是最有用的,因为它给出的是预测值与真实值可能的相差程度。RMSE 给出的是误差的标准偏差,如代码清单 D-8 所示。
皮尔逊相关系数是回归函数的另一个常见性能指标。sklearn 模块默认将其作为.score() 函数附加到大多数模型上。如果大家不清楚这些指标如何计算的话,那么应该手动计算一下找找感觉。相关系数的计算参见代码清单 D-9。
由此可见我们的样本预测值与真实值的相关度只有 28%。
D.8 专业技巧
一旦掌握了基本知识,那么下面这些简单的技巧将有助于更快地建立良好的模型:
- 使用数据集中的一个小的随机样本子集来发现流水线的可能缺陷 ;
- 当准备将模型部署到生产环境中时,请使用所有的数据来训练模型 ;
- 首先应该尝试自己最了解的方法,这个技巧也适用于特征提取和模型本身 ;
- 在低维特征和目标上使用散点图和散点矩阵,以确保没有遗漏一些明显的模式 ;
- 绘制高维数据作为原始图像,以发现特征的转移 1;
- 当希望最大化向量对之间的差异时,可以尝试对高维数据使用 PCA(对 NLP 数据使用 LSA);
- 当希望在低维空间中进行回归或者寻找匹配的向量对时,可以使用非线性降维,如 t-SNE;
- 构建一个 sklearn.Pipeline 对象,以提高模型和特性提取器的可维护性和可复用性 ;
- 使超参数的调优实现自动化,这样模型就可以了解数据,大家就可以花时间学习机器学习。
超参数调优: 超参数是所有那些确定流水线性能的值,包括模型类型及其配置方式等。超参数还可 以是神经网络中包含的神经元数和层数,或者是 sklearn.linear_model.Ridge 岭回归模型中 的 alpha 值。超参数还包括控制所有预处理步骤的值,例如分词类型、所有忽略的词列表、TF-IDF 词汇表的最小和最大文档频率、是否使用词形归并、TF-IDF 归一化方法等。
超参数调优可能是一个十分缓慢的过程,因为每个实验都需要训练和验证一个新模型。因此,在搜索范围广泛的超参数时,我们需要将数据集减小到具有代表性的最小样本集。当搜索接近满足需求的最终模型时,可以增加数据集的大小,以使用尽可能多的所需数据。
优化流水线的超参数是提高模型性能的方法。实现超参数调优自动化可以节省更多的时间来阅读本书这样的书籍,或者可视化和分析最后的结果。当然大家仍然可以通过直觉设置要尝试的超参数范围来指导调优。
提示:超参数调优最有效的算法是 (从最好到最差):
(1) 贝叶斯搜索 ;
(2) 遗传算法 ;
(3) 随机搜索 ;
(4) 多分辨率网格搜索 ;
(5) 网格搜索。
但是无论如何,在大家进入梦乡时工作的所有计算机搜索算法,都比手动猜测一个个新参数好。
本文转自 公众号:AI前线 ,节选自《自然语言处理实战》,点击阅读原文