# 8 高级实践
译者：@Laiczhang
本章包含在某些情况下您可以在实践中使用的技术的描述。 它被称为“高级实践”并不是因为所提出的技术更复杂，而是因为它们应用于某些非常具体的背景中。 在许多实际情况中，您很可能不需要使用这些技术，但有时它们非常有用。

## 8.1 处理不平衡的数据集

在许多实际情况中，您的标记数据集将代表某些类的示例。 例如，当您的分类师必须区分真实和欺诈性的电子商务交易时，就是这种情况：真实交易的例子更为频繁。 如果您使用具有软边距的SVM，则可以为错误分类的示例定义成本。 由于噪声始终存在于训练数据中，因此许多真实交易的例子很可能通过贡献成本而最终落在决策边界的错误一侧。
![8-1](https://github.com/apachecn/ml-book-100-zh/blob/master/doc/img/8-1.png)
SVM算法将尝试移动超平面以尽可能避免错误分类的示例。 “欺诈性”的例子属于少数，可能会被错误分类，以便正确地对更多类别的多数例子进行分类。 这种情况如图1a所示。 对于应用于不平衡数据集的大多数学习算法，观察到此问题。

如果你将少数类的例子的错误分类成本设置得更高，那么模型将更加努力避免对这些例子进行错误分类，显然是因为大多数类的一些例子的错误分类成本，如图1b所示。

一些SVM实现（包括scikit-learn中的SVC）允许您为每个类提供权重。 在寻找最佳超平面时，学习算法会将此信息考虑在内。

如果您的学习算法不允许加权类，您可以尝试通过制作此类示例的多个副本来增加某些类示例的重要性（这被称为过采样）。

相反的方法是从训练集中随机删除多数类的一些示例（欠采样）。

您也可以尝试通过随机抽取少数类的几个示例的特征值并将它们组合以获得该类的新示例来创建合成示例。 有两种流行的算法通过创建合成示例对少数类进行过采样：合成少数过采样技术（SMOTE）和自适应合成采样方法（ADASYN）。

SMOTE和ADASYN在很多方面的工作方式相似。 对于少数类的给定示例$x_{i}$，他们选择此示例的$k$个最近邻居（让我们将这组$k$个例子称为$S_{k}$），然后创建一个合成示例$x_{new}$作为$\mathbf{x}_{i}+\lambda\left(\mathbf{x}_{z i}-\mathbf{x}_{i}\right)$，其中$x_{z}$i是一个示例 从$S_{k}$中随机选择的少数民族。 插值超参数λ是$[0,1]$范围内的随机数。

SMOTE和ADASYN都随机选择数据集中所有可能的$x_{i}$。 在ADASYN中，为每个$x_{i}$生成的合成示例的数量与Sk中不属于少数类的示例的数量成比例。 因此，在少数类的例子很少的区域中产生更多的合成例子。

有些算法对不平衡数据集的问题不太敏感。 决策树以及随机森林和梯度增强通常在不平衡数据集上表现良好。

## 8.2 结合模型

像随机森林这样的集合算法通常组合相同性质的模型。 它们通过组合数百个弱模型来提升性能。 在实践中，我们有时可以通过组合使用不同学习算法的强大模型来获得额外的性能提升。 在这种情况下，我们通常只使用两个或三个模型。

结合模型有三种典型方法：
1）平均，
2）多数投票，
3）堆叠。

平均值适用于回归以及返回分类分数的分类模型。 您只需将所有模型（我们称之为基本模型）应用于输入$x$，然后对预测进行平均。 要查看平均模型是否比每个单独的算法更好，您可以使用您选择的度量标准在验证集上进行测试。

多数投票适用于分类模型。 您将所有基础模型应用于输入$x$，然后在所有预测中返回多数类。 在平局的情况下，您要么随机选择其中一个类，要么返回错误消息（如果错误分类的事实会产生相当大的成本）。

堆叠包括构建一个元模型，该模型将基础模型的输出作为输入。 假设您想要组合分类器$f_{1}$和分类器$f_{2}$，它们都预测同一组类。 要为堆叠模型创建训练示例$\left(\hat{\mathbf{x}}_{i}, \hat{y}_{i}\right)$，请设置$\hat{\mathbf{x}}_{i}=\left[f_{1}(\mathbf{x}), f_{2}(\mathbf{x})\right]$和$\hat{y}_{i}=y_{i}$。

如果您的某些基础模型不仅返回一个类，而且还返回每个类的分数，您也可以将这些值用作功能。

为了训练堆叠模型，建议使用训练集中的示例并使用交叉验证调整堆叠模型的超参数。

显然，您必须确保堆叠模型在验证集上的性能优于您堆叠的每个基础模型。

结合多个模型可以带来更好的整体性能的原因是观察到，当几个不相关的强模型同意时，他们更有可能就正确的结果达成一致。 这里的关键词是“不相关的”。理想情况下，必须使用不同的特征或使用不同性质的算法来获得不同的强模型 - 例如，SVM和随机森林。 将不同版本的决策树学习算法或具有不同超参数的多个SVM组合在一起可能不会导致显着的性能提升。

## 8.3 训练神经网络

在神经网络训练中，一个具有挑战性的方面是将您的数据转换为网络可以使用的输入。 如果您的输入是图像，则首先必须调整所有图像的大小，使它们具有相同的尺寸。 之后，像素通常首先被标准化，然后归一化到范围$[0,1]$。

文本必须被标记化（分成片段，例如单词，标点符号和其他符号）。 对于CNN和RNN，使用单热编码将每个标记转换为向量，因此文本变为单热向量的列表。 另一种通常是表示令牌的更好方法是使用单词嵌入。 对于多层感知器，要将文本转换为向量，单词方法可能效果很好，尤其是对于较大的文本（大于SMS消息和推文）。

特定神经网络架构的选择是一个不同的选择。 对于同样的问题，如seq2seq学习，有各种各样的架构，几乎每年都会提出新的架构。 我建议使用谷歌学术搜索或微软学术搜索引擎，针对您的问题进行最先进的解决方案研究，以便使用关键词和时间范围搜索科学出版物。 如果你不介意使用不太现代的架构，我建议在GitHub上寻找实现的架构，并找到一个可以通过微小的修改应用于你的数据的架构。

实际上，当您对数据进行预处理，清理和规范化以及创建更大的训练集时，现代体系结构优于旧体系结构的优势变得不那么重要。 许多现代神经网络架构是来自几个实验室和公司的几位科学家合作的结果; 这些模型可能非常复杂，无法自行实现，通常需要很大的计算能力来训练。 尝试从最近的科学论文中复制结果所花费的时间可能不值得。 这次可以更好地用于围绕不太现代但稳定的模型构建解决方案并获得更多的培训数据。

一旦决定了网络的体系结构，就必须决定层数，类型和大小。 建议从一层或两层开始，训练模型并查看它是否能很好地获得训练数据（具有低偏差）。 如果不是，则逐渐增加每层的大小和层数，直到模型完美地匹配训练数据。 在这种情况下，如果模型在验证数据上表现不佳（具有高差异），则应将正则化添加到模型中。 如果在添加正则化之后，模型不再处理训练数据，则再次稍微增加网络的大小，并继续像这样迭代地工作，直到模型根据训练和验证数据得到足够好的结果。 你的指标。

## 8.4 高级正规化

在神经网络中，除了L1和L2正则化之外，您还可以使用神经网络特定正则化器：丢失，批量归一化和早期停止。 批量归一化在技术上不是正则化技术，但它通常在模型上具有正则化效应。

辍学的概念非常简单。 每次通过网络运行训练示例时，都会暂时从计算中随机排除某些单位。 排除的单位百分比越高，正则化效应越高。 神经网络库允许对于层的oryoucanspecifythedropout参数进行更多的操作。 dropout参数在$[0,1]$范围内，必须通过在验证数据上进行调整来实验。

dropout的概念非常简单。 每次通过网络运行训练示例时，都会暂时从计算中随机排除某些单位。 排除的单位百分比越高，正则化效应越高。 神经网络库允许在两个连续的图层之间添加dropout图层，或者您可以指定图层的dropout参数。 dropout参数在[0,1]范围内，必须通过在验证数据上进行调整来实验。

批量标准化是一种技术，它包括在后续层的单元接收它们作为输入之前标准化每层的输出。 在实践中，批量标准化导致更快和更稳定的训练，以及一些正规化效果。 因此，尝试使用批量标准化始终是个好主意。 在神经网络库中，您通常可以在两个层之间插入批量标准化层.

早停法是通过在每个时期之后保存初步模型并在验证集上评估初步模型的性能来训练神经网络的方式。正如您在第4章关于梯度下降的部分所记得的那样，随着时代数量的增加，成本降低。降低的成本意味着该模型很好地提供了训练数据。但是，在某些时候，在某个时代之后，模型可以重新开始：成本不断下降，但模型对验证数据的性能会下降。如果您在文件中保留每个纪元后的模型版本，则可以在开始观察验证集上的性能下降后停止培训。或者，您可以继续运行固定数量的时期的训练过程，然后，最后，您选择最佳模型。每个纪元后保存的模型称为检查点。一些机器学习从业者经常依赖这种技术;其他人试图正确地规范模型以避免这种不良行为。

另一种可以不仅应用于神经网络而且应用于几乎任何学习算法的正则化技术称为数据增强。 此技术通常用于规范与图像一起使用的模型。 获得原始标记训练集后，可以通过对原始图像应用各种变换从原始示例创建合成示例：轻微缩放，旋转，闪烁，变暗等。 您将原始标签保留在这些合成示例中。 实际上，这通常会提高模型的性能。

## 8.5 处理多个输入

在许多实际问题中，您将使用多模态数据。 例如，您的输入可以是图像和文本，二进制输出可以指示文本是否描述此图像。

浅层学习算法并不特别适合处理多模态数据。 但是，这并不意味着它是不可能的。 例如，您可以在图像上训练一个模型，在文本上训练另一个模型。 然后你可以使用我们上面讨论过的模型组合技术。

如果您不能将问题划分为两个独立的子问题，则可以尝试对每个输入进行矢量化（通过应用相应的特征工程方法），然后简单地将两个特征向量连接在一起以形成一个更宽的特征向量。 例如，如果您的图像具有$\left[i^{(1)}, i^{(2)}, i^{(3)}\right]$的特征，并且您的文本具有$\left[i^{(1)}, i^{(2)}, i^{(3)}，i^{(4)}\right]$的特征 你的级联特征向量将是$\left[i^{(1)}, i^{(2)}, i^{(3)}, t^{(1)}, t^{(2)}, t^{(3)}, t^{(4)}\right]$.

使用神经网络，您可以获得更多灵活性。 您可以构建两个子网，每种类型的输入一个子网。 例如，CNN子网将读取图像，而RNN子网将读取文本。 两个子网都将最后一层作为嵌入：CNN具有图像嵌入，而RNN具有文本嵌入。 然后，您可以连接两个嵌入，然后在连接嵌入之上添加一个分类层，例如softmax或sigmoid。 神经网络库提供简单易用的工具，允许连接来自多个子网的层。

## 8.6 处理多个输出

在某些问题中，您希望预测一个输入的多个输出。 我们在前一章中考虑了多标签分类。 多输出的一些问题可以有效地转换成多标签分类问题。 特别是具有相同性质标签（如标签）或假标签的那些可以创建为原始标签组合的完整枚举。

但是，在某些情况下，输出是多模式的，并且它们的组合不能有效地列举。 请考虑以下示例：您要构建一个模型来检测图像上的对象并返回其坐标。 同一模型还必须返回对象的标签，例如“人”，“猫”或“仓鼠”。您的训练示例将有一个图像作为输入，一个矢量具有对象的坐标，另一个矢量具有单热编码标签。

要处理这种情况，您可以创建一个可用作编码器的子网。 它将使用例如一个或几个卷积层读取输入图像。 编码器的最后一层是图像的嵌入。 然后在嵌入层顶部添加另外两个子网：一个将嵌入向量作为输入并预测对象的坐标。 这个第一个子网可以有一个ReLU作为最后一层，这是预测正实数的好选择，例如坐标; 这个子网可以使用均方误差成本$C_{1}$。 第二个子网将采用相同的嵌入向量作为输入，并预测每个标签的概率。 该第二子网可以具有softmax作为最后一层，其适合于概率输出，并且使用平均负对数似然成本$C_{2}$（也称为交叉熵成本）。

显然，您对准确预测的坐标和标签感兴趣。 然而，在时间上是不可能实现的。 通过尝试优化，你可能会伤害第二个，反过来。 你可以做的是在$\gamma C_{1}+(1-\gamma) C_{2}$中加入另一个超参数$γ（0,1）$和组合成功函数。 然后，您可以像验证任何其他超参数一样调整验证数据上的$γ$值.

## 8.7 迁移学习

迁移学习可能是神经网络比浅层模型具有独特优势的地方。 在迁移学习中，您选择在某些数据集上训练的现有模型，并调整此模型以预测来自另一个数据集的示例，与建立该模型的数据集不同。 第二个数据集与用于验证和测试的保留集不同。 它可能代表了一些其他现象，或者，正如机器学习科学家所说，它可能来自另一个统计分布。

例如，假设您已训练模型以识别（和标记）大型标记数据集上的野生动物。 过了一段时间，你还有另外一个问题要解决：你需要建立一个能识别家畜的模型。 使用浅层学习算法，您没有太多选择：您必须构建另一个大的标记数据集，现在用于家畜。

通过神经网络，情况更加有利。 神经网络中的迁移学习就像这样。

1.在原始大数据集（野生动物）上构建深度模型。 

2.为第二个模型（家畜）编译一个小得多的标记数据集。 

3.从第一个模型中删除最后一个或多个图层。 通常，这些是负责分类或回归的层次; 它们通常遵循嵌入层。 

4.使用适合新问题的新图层替换已删除的图层。 

5.您“冻结”第一个模型中剩余的图层的参数。 

6.您使用较小的标记数据集和梯度下降来训练仅新层的参数。

通常，网上有大量的视觉问题深度模型。 您可以找到一个很有可能用于解决问题的方法，下载该模型，删除多个最后一层（要删除的图层数量是一个超参数），放置自己的预测图层并训练您的模型。

即使您没有现有模型，当您的问题需要标记数据集获取成本非常高时，转移学习仍然可以帮助您调整，但您可以获得更容易获得标签的另一个数据集。 假设您构建了一个文档分类模型。 您从雇主那里获得了标签的分类，它包含一千个类别。 在这种情况下，您需要主动提供给某人
a）阅读，理解和记忆类别之间的差异，
b）阅读多达一百万份文件并对其进行注释。 这听起来不太好。

为了节省标记这么多的例子，你可以考虑使用维基百科页面作为数据集来构建你的第一个模型。 维基百科页面的标签可以通过获取维基百科页面所属的类别自动获得。 一旦您的第一个模型学会了很好地预测维基百科类别，您就可以转移这种学习来预测雇主分类的类别。 通常，如果您从头开始解决原始问题，您需要的雇主问题的注释示例要少得多。

## 8.8 算法效率

并非所有能够解决问题的算法都是实用的。 有些人可能很快; 有些可能太慢了。 一些问题可以通过快速算法解决，对于其他问题，不存在快速算法。

被称为算法分析的计算机科学的子领域涉及确定和比较算法的复杂性。 大O符号是根据其输入大小增长时的运行时间或空间需求如何增长而使用的类别算法。

例如，假设我们遇到了在大小为$N$的示例集$S$中找到两个最远的一维示例的问题。我们可以用来解决这个问题的一个算法看起来像这样（在Python中这里和下面）：

~~~python
def find_max_distance(S):
    result = None
    max_distance = 0
    for x1 in S:
        for x2 in S:
            if abs(x1 - x2) >= max_distance:
                max_distance = abs(x1 - x2)
                result = (x1, x2)
    return result
~~~

在中间算法中，我们循环索引，并且在第一个循环中进行了非常规，我们再次循环在$S$中的所有值。 因此，上述算法对数字进行$N^{2}$比较。 如果我们以比较（一次），绝对（两次）和分配（两次）操作所花费的时间作为单位时间，那么该算法的时间复杂度（或简单地，复杂性）最多为5$N^{2}$。 当在最坏的情况下测量算法的复杂性时，使用大O符号。 对于上述算法，使用大O表示法，我们写出算法的复杂度为$O\left(N^{2}\right)$（常数，如5，被忽略）。

对于同样的问题，我们可以制作另一种算法：
~~~Python
def find_max_distance(S):
    result = None
    min_x = float("inf")
    max_x = float("-inf")
    for x in S:
        if x < min_x:
            min_x = x
        elif x >max_x:
            max_x = x
    result = (max_x, min_x)
    return result
~~~
在上面的算法中，我们只将S中的所有值循环一次，因此算法的复杂度为$O（N）$。 在这种情况下，我们说后一种算法比前者算法更有效。

通常，当算法的大O符号的复杂度是输入大小的多项式时，算法被称为高效算法。 因此，$O（N）$和$O\left(N^{2}\right)$都是有效的。 但是，对于非常大的输入，$O\left(N^{2}\right)$算法仍然可能非常慢。 在大数据时代，科学家们经常寻找$O(\log N)$算法。

从实际角度来看，当您实现算法时，应尽可能避免使用循环。 例如，您应该对矩阵和向量使用操作，而不是循环。 在Python中，要计算$wx$，你应该写
~~~python

import numpy
wx = numpy.dot(w,x)

and not

wx = 0
for i in range(N)
    wx += w[i] * x[i]
~~~

使用适当的数据结构。 如果集合中元素的顺序无关紧要，请使用set而不是list。 在Python中，当$S$被声明为一个集合时，验证特定示例$x$是否属于$S$的操作是有效的，并且当$S$被声明为列表时，它是无效的。

另一个重要的结构，你可以使用它来进行比赛。 它在其他语言中称为字典或散列映射。 它允许您通过非常快速的键查找来定义键值对的集合。

除非你确切知道你做了什么，否则总是喜欢使用流行的库来编写自己的科学代码。 科学Python软件包如numpy，scipy和scikit-learn是由经验丰富的科学家和工程师构建的，并且考虑到了效率。 他们有许多用C编程语言实现的方法以获得最大速度。

如果你需要遍历大量的元素集合，请使用生成器，它一次返回一个元素，而不是一次返回所有元素。

使用Python中的cPro文件包来找到代码中的低效率。

最后，当从算法的角度来看代码中没有什么可以改进时，你可以通过使用以下方法进一步提高代码的速度：
- 多处理包以并行运行计算，以及
- PyPy，Numba或类似工具将Python代码编译成 快速，优化的机器代码。