自然语言处理实战:机器学习常见工具与技术

lirika 2020-09-27

许多自然语言处理都涉及机器学习,所以理解机器学习的一些基本工具和技术是有益处的。有些工具已经在前几章中讨论过,有些还没有,但这里我们会讨论所有这些工具。

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折。然后通过将− 1份数据作为训练集训练模型并在第k份数据上进行验证。之后将第一次尝试中用作训练的− 1份数据中的一份数据作为验证集,剩下的− 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-1 Keras中的dropout层会减少过拟合

>>> from keras.models import Sequential 
>>> from keras.layers import Dropout, LSTM, Flatten, Dense  
>>> num_neurons = 20  
>>> maxlen = 100 
>>> embedding_dims = 300 
>>> model = Sequential() >>> model.add(LSTM(num_neurons, return_sequences=True, 
...                input_shape=(maxlen, embedding_dims)))>>> model.add(Dropout(.2))    
>>> model.add(Flatten()) 
>>> model.add(Dense(1, activation='sigmoid')) 

D.5.3 批归一化

神经网络中一个称为批归一化的新概念可以帮助对模型进行标准化和泛化。批归一化的思想是,与输入数据非常相似,每个网络层的输出应该归一化为0到1之间的值。关于如何、为什么、什么时候这样做是有益的,以及在什么条件下应该使用它,仍然存在一些争议。我们希望大家自己去对这个研究方向进行探索。

但是Keras的BatchNormalization层提供了一个简单的实现方法,如代码清单D-2所示。

代码清单D-2 归一化BatchNormalization

>>> from keras.models import Sequential 
>>> from keras.layers import Activation, Dropout, LSTM, Flatten, Dense 
>>> from keras.layers.normalization import BatchNormalization 
>>> model = Sequential()>>> model.add(Dense(64, input_dim=14)) 
>>> model.add(BatchNormalization()) 
>>> model.add(Activation('sigmoid')) 
>>> model.add(Dense(64, input_dim=14)) 
>>> model.add(BatchNormalization()) 
>>> model.add(Activation('sigmoid')) 
>>> model.add(Dense(1, activation='sigmoid')) 

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给出了混淆矩阵中预测值和真实值的样子。

代码清单D-5 混淆矩阵

>>> confusion = [[true_positives, false_positives], 
...              [false_negatives, true_negatives]]>>> confusion[[4, 3], [1, 2]] 
>>> import pandas as pd 
>>> confusion = pd.DataFrame(confusion, columns=[1, 0], index=[1, 0]) 
>>> confusion.index.name = r'pred \ truth' 
>>> confusion              1   0 
pred \ truth 1             4   1  

0             3   2 

在混淆矩阵中,我们希望对角线(左上角和右下角)上的数字较大,希望对角线外的数字(左上角和左下角)较小。然而,正向类和负向类的顺序是任意的,所以有时可能会看到这个表的数字被调换了位置。请始终标记好混淆矩阵的列和下标。有时可能会听到统计学家把这个矩阵称为分类器列联表,但如果坚持使用“混淆矩阵”这个名字的话,就可以避免混淆。

对于机器学习分类问题,有两种有用的方法可以将这4种计数值中的一些指标组合成一个性能指标:正确率(precision)和召回率(recall)。信息检索(搜索引擎)和语义搜索就是此分类问题的例子,因为那里的目标是将文档分为(和输入查询)匹配或不匹配两类。第2章中,我们学习过词干还原和词形归并如何能够提高召回率,但同时降低了正确率。

正确率度量的是模型在检测所感兴趣类的所有对象(称为正向类)的能力,因此它也被称为正向预测值(positive predictive value)。由于真阳是预测正确的正向类样本数目,而假阳是错误地标记为正向类的负向类样本数目,因此可以按照代码清单D-6所示来计算正确率。

代码清单D-6 正确率

>>> precision = true_positives / (true_positives + false_positives) 
>>> precision0.571... 

上述例子中的混淆矩阵给出了约57%的正确率,因为在所有预测为正向类的样本中有约57%是正确的。

召回率和正确率类似,它也被称为灵敏度、真阳率或查全率。因为数据集中的样本总数是真阳(true positive)和假阴(false negative)的和,所以可以计算召回率,即检测到的预测正确的正向类样本占所有样本的百分比,代码如代码清单D-7所示。

代码清单D-7 召回率

>>> recall = true_positives / (true_positives + false_negatives) 
>>> recall0.8 

这就是说上面例子中得到的模型检测到了数据集中80%的正向类样本。

D.7.2 回归的衡量指标

用于机器学习回归问题的两个最常见的性能评价指标是均方根误差(RMSE)和皮尔逊相关系数(R2)。事实证明,分类问题背后实际上是回归问题。因此,如果类标签已经转换为数字(就像我们在上一节中所做的那样),就可以在其上使用回归度量方法。下面的代码示例将复用上一节的那些预测值和真实值。RMSE对于大多数问题是最有用的,因为它给出的是预测值与真实值可能的相差程度。RMSE给出的是误差的标准偏差,如代码清单D-8所示。

代码清单D-8 均方根误差(RMSE)

>>> y_true = np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1]) 
>>> y_pred = np.array([0, 0, 1, 1, 1, 1, 1, 0, 0, 0]) 
>>> rmse = np.sqrt((y_true - y_pred) ** 2) / len(y_true)) 
>>> rmse 

0.632... 

皮尔逊相关系数是回归函数的另一个常见性能指标。sklearn模块默认将其作为.score()函数附加到大多数模型上。如果大家不清楚这些指标如何计算的话,那么应该手动计算一下找找感觉。相关系数的计算参见代码清单D-9。

代码清单D-9 相关系数

>>> corr = pd.DataFrame([y_true, y_pred]).T.corr() 
>>> corr[0][1] 

0.218...>>> np.mean((y_pred - np.mean(y_pred)) * (y_true - np.mean(y_true))) / 
...     np.std(y_pred) / np.std(y_true) 

0.218... 

由此可见我们的样本预测值与真实值的相关度只有28%。

D.8 专业技巧

一旦掌握了基本知识,那么下面这些简单的技巧将有助于更快地建立良好的模型:

  • 使用数据集中的一个小的随机样本子集来发现流水线的可能缺陷;.
  • 当准备将模型部署到生产环境中时,请使用所有的数据来训练模型;
  • 首先应该尝试自己最了解的方法,这个技巧也适用于特征提取和模型本身;
  • 在低维特征和目标上使用散点图和散点矩阵,以确保没有遗漏一些明显的模式;
  • 绘制高维数据作为原始图像,以发现特征的转移**
  • 当希望最大化向量对之间的差异时,可以尝试对高维数据使用PCA(对NLP数据使用LSA);
  • 当希望在低维空间中进行回归或者寻找匹配的向量对时,可以使用非线性降维,如t-SNE;
  • 构建一个sklearn.Pipeline对象,以提高模型和特性提取器的可维护性和可复用性;
  • 使超参数的调优实现自动化,这样模型就可以了解数据,大家就可以花时间学习机器学习。

超参数调优 超参数是所有那些确定流水线性能的值,包括模型类型及其配置方式等。超参数还可以是神经网络中包含的神经元数和层数,或者是sklearn.linear_model.Ridge岭回归模型中的alpha值。超参数还包括控制所有预处理步骤的值,例如分词类型、所有忽略的词列表、TF-IDF词汇表的最小和最大文档频率、是否使用词形归并、TF-IDF归一化方法等。

超参数调优可能是一个十分缓慢的过程,因为每个实验都需要训练和验证一个新模型。因此,在搜索范围广泛的超参数时,我们需要将数据集减小到具有代表性的最小样本集。当搜索接近满足需求的最终模型时,可以增加数据集的大小,以使用尽可能多的所需数据。

优化流水线的超参数是提高模型性能的方法。实现超参数调优自动化可以节省更多的时间来阅读本书这样的书籍,或者可视化和分析最后的结果。当然大家仍然可以通过直觉设置要尝试的超参数范围来指导调优。

提示 超参数调优最有效的算法是(从最好到最差):

(1)贝叶斯搜索;

(2)遗传算法;

(3)随机搜索;

(4)多分辨率网格搜索;

(5)网格搜索。

相关推荐