作者 | BoCong-Deng
编辑 | 屠敏
封图 | 自东方 IC
出品 | CSDN 博客
写在前面
在我们进行模型训练时, 如果你只是想要让模型具有不错的性能, 那么盲目地尝试网络架构足以达到目的. 而在本文中, 我们将为你提供一套用于构建最先进深度学习模型的必备技术的快速指南, 从而让模型由 "具有不错的性能" 上升到 "性能卓越且满足我们的一些需要". 本文的叙述以及代码的编写时基于 TensorFlow 中 keras 来进行表述的.
高级架构模式
残差连接
残差连接 (residual connection) 是一种常见的类图网络组件, 在 2015 年之后的许多网络架构 (包括 Xception)中都可以见到. 2015 年末, 来自微软的何恺明等人在 ILSVRC ImageNet 挑战赛 中获胜, 其中引入了这一方法. 残差连接解决了困扰所有大规模深度学习模型的两个共性问题: 梯度消失和表示瓶颈.
通常来说, 向任何多于 10 层的模型中添加残差连接, 都可能会有所帮助. 残差连接是让前面某层的输出作为后面某层的输入, 从而在序列网络中有效地创造了一条 捷径. 前面层的输出没有与后面层的激活连接在一起, 而是与后面层的激活相加(这里假设两 个激活的形状相同).
如果它们的形状不同, 我们可以用一个线性变换将前面层的激活改变成目标形状(例如, 这个线性变换可以是不带激活的 Dense 层; 对于卷积特征图, 可以是不带激活 1*1 卷积). 如果特征图的尺寸相同, 在 Keras 中实现残差连接的方法如下, 用的是恒等残差连接(identity residual connection). 这个例子假设我们有一个四维输入张量 x.
from keras import layersx = ... y = layers.Conv2D(128, 3, activation='relu', padding='same')(x) y = layers.Conv2D(128, 3, activation='relu', padding='same')(y) y = layers.Conv2D(128, 3, activation='relu', padding='same')(y) y = layers.add([y, x])
如果特征图的尺寸不同, 实现残差连接的方法如下, 用的是线性残差连接(linear residual connection). 同样, 假设我们有一个四维输入张量 x.
from keras import layers x = ... y = layers.Conv2D(128, 3, activation='relu', padding='same')(x) y = layers.Conv2D(128, 3, activation='relu', padding='same')(y) y = layers.MaxPooling2D(2, strides=2)(y) residual = layers.Conv2D(128, 1, strides=2, padding='same')(x) y = layers.add([y, residual])
批标准化
标准化 (normalization) 是一大类方法, 用于让机器学习模型看到的不同样本彼此之间更加 相似, 这有助于模型的学习与对新数据的泛化. 最常见的数据标准化形式就是你已经在本书中 多次见到的那种形式: 将数据减去其平均值使其中心为 0, 然后将数据除以其标准差使其标准 差为 1. 实际上, 这种做法假设数据服从正态分布(也叫高斯分布), 并确保让该分布的中心为 0, 同时缩放到方差为 1.
normalized_data = (data - np.mean(data, axis=...)) / np.std(data, axis=...)
前面的示例都是在将数据输入模型之前对数据做标准化. 但在网络的每一次变换之后都应该考虑数据标准化. 即使输入 Dense 或 Conv2D 网络的数据均值为 0, 方差为 1, 也没有理由 假定网络输出的数据也是这样.
批标准化 (batch normalization) 是 Ioffe 和 Szegedy 在 2015 年提出的一种层的类型 a(在 Keras 中是 BatchNormalization), 即使在训练过程中均值和方差随时间发生变化, 它也可以 适应性地将数据标准化.
批标准化的工作原理是, 训练过程中在内部保存已读取每批数据均值 和方差的指数移动平均值.
批标准化的主要效果是, 它有助于梯度传播(这一点和残差连接很 像), 因此允许更深的网络. 对于有些特别深的网络, 只有包含多个 BatchNormalization 层 时才能进行训练. 例如, BatchNormalization 广泛用于 Keras 内置的许多高级卷积神经网络 架构, 比如 ResNet50,Inception V3 和 Xception.
BatchNormalization 层通常在卷积层或密集连接层之后使用.
conv_model.add(layers.Conv2D(32, 3, activation='relu')) conv_model.add(layers.BatchNormalization()) dense_model.add(layers.Dense(32, activation='relu')) dense_model.add(layers.BatchNormalization())
BatchNormalization 层接收一个 axis 参数, 它指定应该对哪个特征轴做标准化. 这 个参数的默认值是 - 1, 即输入张量的最后一个轴. 对于 Dense 层, Conv1D 层, RNN 层和将 data_format 设为 "channels_last"(通道在后)的 Conv2D 层, 这个默认值都是正确的. 但有少数人使用将 data_format 设为 "channels_first"(通道在前)的 Conv2D 层, 这时 特征轴是编号为 1 的轴, 因此 BatchNormalization 的 axis 参数应该相应地设为 1.
深度可分离卷积
如果我告诉你, 有一个层可以替代 Conv2D, 并可以让模型更加轻量 (即更少的可训练权 重参数), 速度更快(即更少的浮点数运算), 还可以让任务性能提高几个百分点, 你觉得怎么样? 我说的正是深度可分离卷积(depthwise separable convolution) 层(SeparableConv2D)的作用. 这个层对输入的每个通道分别执行空间卷积, 然后通过逐点卷积 (1*1 卷积) 将输出通道混合, 如下图. 这相当于将空间特征学习和通道特征学习分开, 如果你假设输入的 空间位置高度相关, 但不同的通道之间相对独立, 那么这么做是很有意义的. 它需要的参数要少很多, 计算量也更小, 因此可以得到更小, 更快的模型. 因为它是一种执行卷积更高效的方法, 所以往往能够使用更少的数据学到更好的表示, 从而得到性能更好的模型.
超参数优化
构建深度学习模型时, 你必须做出许多看似随意的决定: 应该堆叠多少层? 每层应该 包含多少个单元或过滤器? 激活应该使用 relu 还是其他函数? 在某一层之后是否应该使用 BatchNormalization ? 应该使用多大的 dropout 比率? 还有很多. 这些在架构层面的参数叫作超参数(hyperparameter), 以便将其与模型参数区分开来, 后者通过反向传播进行训练.
在实践中, 经验丰富的机器学习工程师和研究人员会培养出直觉, 能够判断上述选择哪些 可行, 哪些不可行. 也就是说, 他们学会了调节超参数的技巧. 但是调节超参数并没有正式成 文的规则. 如果你想要在某项任务上达到最佳性能, 那么就不能满足于一个容易犯错的人随意 做出的选择. 即使你拥有很好的直觉, 最初的选择也几乎不可能是最优的. 你可以手动调节你 的选择, 重新训练模型, 如此不停重复来改进你的选择, 这也是机器学习工程师和研究人员大 部分时间都在做的事情. 但是, 整天调节超参数不应该是人类的工作, 最好留给机器去做.
因此, 你需要制定一个原则, 系统性地自动探索可能的决策空间. 你需要搜索架构空间, 并根据经验找到性能最佳的架构. 这正是超参数自动优化领域的内容. 这个领域是一个完整的 研究领域, 而且很重要.
超参数优化的过程通常如下所示.
选择一组超参数(自动选择).
构建相应的模型.
将模型在训练数据上拟合, 并衡量其在验证数据上的最终性能.
选择要尝试的下一组超参数(自动选择).
重复上述过程.
最后, 衡量模型在测试数据上的性能.
这个过程的关键在于, 给定许多组超参数, 使用验证性能的历史来选择下一组需要评估的 超参数的算法. 有多种不同的技术可供选择: 贝叶斯优化, 遗传算法, 简单随机搜索等. 训练模型权重相对简单: 在小批量数据上计算损失函数, 然后用反向传播算法让权重向正确的方向移动. 与此相反, 更新超参数则非常具有挑战性. 我们来考虑以下两点.
计算反馈信号 (这组超参数在这个任务上是否得到了一个高性能的模型) 的计算代价可能非常高, 它需要在数据集上创建一个新模型并从头开始训练.
超参数空间通常由许多离散的决定组成, 因而既不是连续的, 也不是可微的. 因此, 你通常不能在超参数空间中做梯度下降. 相反, 你必须依赖不使用梯度的优化方法, 而这些方法的效率比梯度下降要低很多.
这些挑战非常困难, 而这个领域还很年轻, 因此我们目前只能使用非常有限的工具来优 化模型. 通常情况下, 随机搜索 (随机选择需要评估的超参数, 并重复这一过程) 就是最好的 解决方案, 虽然这也是最简单的解决方案. 但有一种工具确实比随机搜索更好, 它就是 Hyperopt. 它是一个用于超参数优化的 Python 库, 其内部使用 Parzen 估计器的树来预测哪组超 参数可能会得到好的结果. 另一个叫作 Hyperas 的库将 Hyperopt 与 Keras 模型集成在一起. 一 定要试试.
模型集成
想要在一项任务上获得最佳结果, 另一种强大的技术是模型集成(model ensembling). 集 成是指将一系列不同模型的预测结果汇集到一起, 从而得到更好的预测结果. 观察机器学习竞赛, 特别是 Kaggle 上的竞赛, 你会发现优胜者都是将很多模型集成到一起, 它必然可以打败任何单 个模型, 无论这个模型的表现多么好.
集成依赖于这样的假设, 即对于独立训练的不同良好模型, 它们表现良好可能是因为不同 的原因: 每个模型都从略有不同的角度观察数据来做出预测, 得到了 "真相" 的一部分, 但不 是全部真相. 你可能听说过盲人摸象的古代寓言: 一群盲人第一次遇到大象, 想要通过触摸来 了解大象. 每个人都摸到了大象身体的不同部位, 但只摸到了一部分, 比如鼻子或一条腿. 这 些人描述的大象是这样的,"它像一条蛇"" 像一根柱子或一棵树 ", 等等. 这些盲人就好比机器 学习模型, 每个人都试图根据自己的假设 (这些假设就是模型的独特架构和独特的随机权重初 始化) 并从自己的角度来理解训练数据的多面性. 每个人都得到了数据真相的一部分, 但不是 全部真相. 将他们的观点汇集在一起, 你可以得到对数据更加准确的描述. 大象是多个部分的 组合, 每个盲人说的都不完全准确, 但综合起来就成了一个相当准确的故事.
我们以分类问题为例. 想要将一组分类器的预测结果汇集在一起[即分类器集成(ensemble the classifiers)], 最简单的方法就是将它们的预测结果取平均值作为预测结果.
- preds_a = model_a.predict(x_val) preds_b = model_b.predict(x_val) preds_c = model_c.predict(x_val) preds_d = model_d.predict(x_val)
- final_preds = 0.25 * (preds_a + preds_b + preds_c + preds_d)
只有这组分类器中每一个的性能差不多一样好时, 这种方法才奏效. 如果其中一个分类器 性能比其他的差很多, 那么最终预测结果可能不如这一组中的最佳分类器那么好.
将分类器集成有一个更聪明的做法, 即加权平均, 其权重在验证数据上学习得到. 通常来 说, 更好的分类器被赋予更大的权重, 而较差的分类器则被赋予较小的权重. 为了找到一组好 的集成权重, 你可以使用随机搜索或简单的优化算法(比如 Nelder-Mead 方法).
preds_a = model_a.predict(x_val) preds_b = model_b.predict(x_val) preds_c = model_c.predict(x_val) preds_d = model_d.predict(x_val) final_preds = 0.5 * preds_a + 0.25 * preds_b + 0.1 * preds_c + 0.15 * preds_d
还有许多其他变体, 比如你可以对预测结果先取指数再做平均. 一般来说, 简单的加权平均, 其权重在验证数据上进行最优化, 这是一个很强大的基准方法. 想要保证集成方法有效, 关键在于这组分类器的多样性(diversity). 多样性就是力量. 如 果所有盲人都只摸到大象的鼻子, 那么他们会一致认为大象像蛇, 并且永远不会知道大象的真 实模样. 是多样性让集成方法能够取得良好效果. 用机器学习的术语来说, 如果所有模型的偏 差都在同一个方向上, 那么集成也会保留同样的偏差. 如果各个模型的偏差在不同方向上, 那么这些偏差会彼此抵消, 集成结果会更加稳定, 更加准确.
来源: https://blog.csdn.net/dQCFKyQDXYm3F8rB0/article/details/107624746