收藏ShowMeAI查看更多精彩内容
本系列为吴恩达老师《深度学习专业课程》学习与总结整理所得,对应的课程视频可以在这里查看。
在ShowMeAI前一篇文章 神经网络优化算法 中我们对以下内容进行了介绍:
本篇我们将重点展开介绍超参数调试、BN(Batch Normalization批归一化)和深度学习编程框架三个部分的内容。
深度神经网络需要调试的超参数(Hyperparameters)较多,我们来看看如何对其调试。
吴恩达老师前面讲到过的超参数中,重要程度依次是(仅供参考):
我们下面来看看神经网络的超参数选择与调试方法。在传统的机器学习中,我们对每个参数等距离选取任意个数的点,然后,分别使用不同点对应的参数组合进行训练,最后根据验证集上的表现好坏,来选定最佳的参数。
例如有两个待调试的参数Hyperparameter 1和Hyperparameter 2,在每个参数上分别均匀间隔选取\(5\)个点,这样构成了\(5 \times 5=25\)种参数组合,如图所示。这种做法在参数比较少的时候效果较好。
但是在深度神经网络模型中,我们一般不采用这种均匀间隔取点的方法,比较好的做法是使用随机选择。也就是说,对于上面这个例子,我们随机选择25个点,作为待调试的超参数,如下图所示:
随机化选择参数的目的是为了尽可能地得到更多种参数组合。还是上面的例子,如果使用均匀采样的话,每个参数只有5种情况;而使用随机采样的话,每个参数有25种可能的情况,因此更有可能得到最佳的参数组合。
这种做法带来的另外一个好处就是对重要性不同的参数之间的选择效果更好。假设hyperparameter1为\(\alpha\),hyperparameter2为\(\varepsilon\),显然二者的重要性是不一样的。
其实,在实际应用中完全不知道哪个参数更加重要的情况下,随机采样的方式能有效解决这一问题,但是均匀采样做不到这点。
在经过随机采样之后,我们可能得到某些区域模型的表现较好。然而,为了得到更精确的最佳参数,我们应该继续对选定的区域进行由粗到细的采样(coarse to fine sampling scheme)。也就是放大表现较好的区域,再对此区域做更密集的随机采样。例如,对图中右下角的方形区域再做25点的随机采样,以获得最佳参数。
综上,超参调试过程的技巧总结如下:
上一段讲到使用随机采样调试超参数,对于某些超参数是可以进行尺度均匀采样的,但是某些超参数需要选择不同的合适尺度进行随机采样。举例来说:
① 对于超参数(#layers)和(#hidden units),都是正整数,是可以进行均匀随机采样的,即超参数每次变化的尺度都是一致的(如每次变化为1,犹如一个刻度尺一样,刻度是均匀的)。
② 对于超参数\(\alpha\),待调范围是\([0.0001, 1]\)。
对于非均匀采样,一种常用的做法是将linear scale转换为log scale,将均匀尺度转化为非均匀尺度,然后再在log scale下进行均匀采样。这样,\([0.0001, 0.001]\)、\([0.001, 0.01]\)、\([0.01, 0.1]\)、\([0.1, 1]\)各个区间内随机采样的超参数个数基本一致,也就扩大了之前\([0.0001, 0.1]\)区间内采样值个数。
我们以重要参数学习率和动量衰减参数为例:
上述操作的原因是当 \(\beta\) 接近 1 时,即使 \(\beta\) 只有微小的改变,所得结果的灵敏度会有较大的变化。例如,\(\beta\) 从 0.9 增加到 0.9005 对结果\((1/(1-\beta\)))几乎没有影响,而 \(\beta\) 从 0.999 到 0.9995 对结果的影响巨大(从 1000 个值中计算平均值变为 2000 个值中计算平均值)。
(1) 深度学习如今已经应用到许多不同的领域。不同的应用出现相互交融的现象,某个应用领域的超参数设定有可能通用于另一领域。不同应用领域的人也应该更多地阅读其他研究领域的 paper,跨领域地寻找灵感。
(2) 考虑到数据的变化或者服务器的变更等因素,建议每隔几个月至少一次,重新测试或评估超参数,来获得实时的最佳模型;
(3) 根据你所拥有的计算资源来决定你训练模型的方式:
Sergey Ioffe和Christian Szegedy两位学者在paper中提出了批标准化(Batch Normalization,经常简称为 BN)方法。Batch Normalization不仅可以让调试超参数更加简单,而且可以让神经网络模型更加「健壮」。也就是说较好模型可接受的超参数范围更大一些,包容性更强,使得更容易去训练一个深度神经网络。
接下来,我们就来介绍什么是Batch Normalization,以及它是如何工作的。
之前,我们对输入特征 \(X\) 使用了标准化处理。我们也可以用同样的思路处理隐藏层的激活值\(a^{[l]}\) ,以加速 \(W^{[l+1]}\)和 \(b^{[l+1]}\)的训练。在实践中,经常选择标准化隐藏层输入 \(Z^{[l]}\),这里我们对第\(l\)层隐层做如下处理:
其中,\(m\)是单个 Mini-Batch 所包含的样本个数,\(\varepsilon\) 是为了防止分母为零,通常取 \(10^{-8}\)。
我们来从「样本」和「通道」维度对上述BN环节做一个可视化展开详解,对于某一层\(N \times D\)的输入,我们上述操作的计算过程如下图所示。(注意其中\(i\)为样本维度,\(j\)为通道维度)
在经过上述处理前后的数据分布如下图所示,我们可以看到这个处理对数据的「Normalization」影响。
这样,我们使得所有的输入 \(z^{(i)}\)均值为 0,方差为 1。但我们不想粗暴地让处理后的隐藏层单元直接变为均值 0 方差 1,这样原本学习到的数据分布就被直接抹掉了。因此,我们引入可学习参数\(\gamma\) 和 \(\beta\) ,对\(z_{norm}^{(i)}\)进行线性变换,如下:
其中,\(\gamma\) 和 \(\beta\) 都是模型的学习参数,所以可以用各种梯度下降算法来更新 \(\gamma\) 和 \(\beta\) 的值,如同更新神经网络的权重一样。
通过对 \(\gamma\) 和 \(\beta\) 的合理设置,可以让 \(\tilde z^{(i)}\)的均值和方差为任意值。这样,我们对隐藏层的 \(z^{(i)}\)进行标准化处理,用得到的 \(\tilde z^{(i)}\)替代 \(z^{(i)}\)。
公式中设置可学习参数 \(\gamma\) 和 \(\beta\),原因是如果各隐藏层的输入均值在靠近 0 的区域,即处于激活函数的线性区域,不利于训练非线性神经网络,从而得到效果较差的模型。因此,需要用 \(\gamma\) 和 \(\beta\) 对标准化后的结果做进一步处理。
上面讲解了对某单一隐层的所有神经元进行BN批归一化的方法,总结示意图如下:
接下来我们研究一下如何把Bath Norm应用到整个神经网络中,对于 L 层神经网络,经过 Batch Normalization 的作用,整体流程如下:
实际上,Batch Normalization 经常使用在 Mini-Batch 上,这也是其名称的由来。
使用 Batch Normalization 时,因为标准化处理中包含减去均值的一步,因此 \(b\) 实际上没有起到作用,其数值效果交由 \(\beta\) 来实现。因此,在 Batch Normalization 中,可以省略 \(b\) 或者暂时设置为 0。
在使用梯度下降算法时,分别对 \(W^{[l]}\),\(\beta^{[l]}\)和 \(\gamma ^{[l]}\)进行迭代更新。
除了传统的梯度下降算法之外,也同样可以使用ShowMeAI上一篇文章 神经网络优化算法 提到的动量梯度下降、RMSProp 或者 Adam 等优化算法。
Batch Normalization 效果很好的原因有以下两点:
关于第二点,如果实际应用样本和训练样本的数据分布不同(如下图中的黑猫图片和橘猫图片),我们称发生了「Covariate Shift」。这种情况下,一般要对模型进行重新训练。Batch Normalization 的作用就是减小 Covariate Shift 所带来的影响,让模型变得更加健壮,鲁棒性(Robustness)更强。
即使输入的值改变了,由于 Batch Normalization 的作用,使得均值和方差保持不变(由 \(\gamma\) 和 \(\beta\) 决定),限制了在前层的参数更新对数值分布的影响程度,因此后层的学习变得更容易一些。Batch Normalization 减少了各层 \(W\) 和 \(b\) 之间的耦合性,让各层更加独立,实现自我训练学习的效果。
另外,Batch Normalization 也起到微弱的正则化(regularization)效果。因为在每个 Mini-Batch 而非整个数据集上计算均值和方差,只由这一小部分数据估计得出的均值和方差会有一些噪声,因此最终计算出的 \(\tilde z^{(i)}\)也有一定噪声。类似于 Dropout,这种噪声会使得神经元不会再特别依赖于任何一个输入特征。
因为 Batch Normalization 只有微弱的正则化效果,因此可以和 Dropout 一起使用,以获得更强大的正则化效果。通过应用更大的 Mini-Batch 大小,可以减少噪声,从而减少这种正则化效果。
吴恩达老师也提醒大家,不要将 Batch Normalization 作为正则化的手段,而是当作加速学习的方式。正则化只是一种非期望的副作用,Batch Normalization 解决的还是反向传播过程中的梯度问题(梯度消失和爆炸)。
Batch Normalization 将数据以 Mini-Batch 的形式逐一处理,但在测试时,可能需要对每一个样本逐一处理,这样无法得到 \(\mu\) 和 \(\sigma^2\)。
理论上,我们可以将所有训练集放入最终的神经网络模型中,然后将每个隐藏层计算得到的 \(\mu^{[l]}\)和 \(\sigma^{2[l]}\)直接作为测试过程的 \(\mu\) 和 \(\sigma\) 来使用。但是,实际应用中一般不使用这种方法,而是使用之前学习过的指数加权平均的方法来预测测试过程单个样本的 \(\mu\) 和 \(\sigma^2\)。
对于第 \(l\) 层隐藏层,考虑所有 Mini-Batch 在该隐藏层下的 \(\mu^{[l]}\)和 \(\sigma^{2[l]}\),然后用指数加权平均的方式来预测得到当前单个样本的 \(\mu^{[l]}\)和 \(\sigma^{2[l]}\)。这样就实现了对测试过程单个样本的均值和方差估计。
目前为止,介绍的分类例子都是二分类问题:神经网络输出层只有一个神经元,表示预测输出 \(\hat{y}\) 是正类的概率 \(P(y = 1 \mid x)\),\(\hat{y}> 0.5\) 则判断为正类,反之判断为负类。
对于多分类问题,用 \(C\) 表示种类个数,则神经网络输出层,也就是第\(L\) 层的单元数量 \(n^{[L]} = C\)。每个神经元的输出依次对应属于该类的概率,即 \(P(y=c \mid x),c=0,1,...,C-1\)。有一种 Logistic 回归的一般形式,叫做 Softmax 回归,可以处理多分类问题。
对于 Softmax 回归模型的输出层,即第 \(L\) 层,有:\(Z^{[L]} = W^{[L]}a^{[L-1]} + b^{[L]}\)
for i in range(L)
,有:\(a^{[L]}_i = \frac{e^{Z^{[L]}_i}}{\sum^C_{i=1}e^{Z^{[L]}_i}}\)
为输出层每个神经元的输出,对应属于该类的概率,满足:\(\sum^C_{i=1}a^{[L]}_i = 1\)
一个直观的计算例子如下:
下图为一些softmax线性分类器得到的分类边界
我们来看看softmax分类器的损失函数,这里定义其损失函数为:\(L(\hat y, y) = -\sum^C_{j=1}y_jlog\hat y_j\)
当 \(i\) 为样本真实类别,则有:\(y_j = 0, j \ne i\)
因此,损失函数可以简化为:\(L(\hat y, y) = -y_ilog\hat y_i = log \hat y_i\)
所有 \(m\) 个样本的成本函数为:\(J = \frac{1}{m}\sum^m_{i=1}L(\hat y, y)\)
多分类的 Softmax 回归模型与二分类的 Logistic 回归模型只有输出层上有一点区别。经过不太一样的推导过程,仍有
\[dZ^{[L]} = A^{[L]} - Y \]反向传播过程的其他步骤也和 Logistic 回归的一致。
参考资料:Softmax回归 - Ufldl
目前最火的深度学习框架之一是来自google的Tensorflow 。下面简单做一个介绍。(更多的TensorFlow实战方法,请参考)
Tensorflow 框架内可以直接调用梯度下降算法,极大地降低了编程人员的工作量。例如以下代码:
import numpy as np import tensorflow as tf cofficients = np.array([[1.],[-10.],[25.]]) w = tf.Variable(0,dtype=tf.float32) x = tf.placeholder(tf.float32,[3,1]) # Tensorflow 重载了加减乘除符号 cost = x[0][0]*w**2 + x[1][0]*w + x[2][0] # 改变下面这行代码,可以换用更好的优化算法 train = tf.train.GradientDescentOptimizer(0.01).minimize(cost) init = tf.global_variables_initializer() session = tf.Session() session.run(init) for i in range(1000): session.run(train, feed_dict=(x:coefficients)) print(session.run(w))
打印为 4.99999,基本可以认为是我们需要的结果。更改 cofficients 的值可以得到不同的结果 w。
上述代码中:
session = tf.Session() session.run(init) print(session.run(w))
也可以写作:
with tf.Session() as session: session.run(init) print(session.run(w))
with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的「清理」操作,释放资源,比如文件使用后自动关闭、线程中锁的自动获取和释放等。
想了解更多 Tensorflow 有关知识,请参考 官方文档。