本系列为 斯坦福CS231n 《深度学习与计算机视觉(Deep Learning for Computer Vision)》的全套学习笔记,对应的课程视频可以在 这里 查看。更多资料获取方式见文末。
通过ShowMeAI前序文章 深度学习与CV教程(3) | 损失函数与最优化 ,深度学习与CV教程(4) | 神经网络与反向传播,深度学习与CV教程(5) | 卷积神经网络 我们已经学习掌握了以下内容:
【本篇】和【下篇】 ShowMeAI讲解训练神经网络的核心方法与关键点,主要包括:
关于激活函数的详细知识也可以参考阅读ShowMeAI的 深度学习教程 | 吴恩达专项课程 · 全套笔记解读 中的文章 浅层神经网络 里【激活函数】板块内容。
在全连接层或者卷积层,输入数据与权重相乘后累加的结果送给一个非线性函数,即激活函数(activation function)。每个激活函数的输入都是一个数字,然后对其进行某种固定的数学操作。
下面是在实践中可能遇到的几种激活函数:
数学公式: σ ( x ) = 1 / ( 1 + e − x ) \sigma(x) = 1 / (1 + e^{-x}) σ(x)=1/(1+e−x)
求导公式: d σ ( x ) d x = ( 1 − σ ( x ) ) σ ( x ) \frac{d\sigma(x)}{dx} = \left( 1 - \sigma(x) \right) \sigma(x) dxdσ(x)=(1−σ(x))σ(x) (不小于 0 0 0 )
特点:把输入值「挤压」到 0 0 0 到 1 1 1 范围内。Sigmoid 函数把输入的实数值「挤压」到 0 0 0 到 1 1 1 范围内,很大的负数变成 0 0 0,很大的正数变成 1 1 1,在历史神经网络中,Sigmoid 函数很常用,因为它对神经元的激活频率有良好的解释:从完全不激活( 0 0 0)到假定最大频率处的完全饱和(saturated)的激活( 1 1 1) 。
然而现在 Sigmoid 函数已经很少使用了,因为它有三个主要缺点:
缺点①:Sigmoid 函数饱和时使梯度消失
缺点②:Sigmoid 函数的输出不是零中心的
缺点③: 指数型计算量比较大。
数学公式: tanh ( x ) = 2 σ ( 2 x ) − 1 \tanh(x) = 2 \sigma(2x) -1 tanh(x)=2σ(2x)−1
特点:将实数值压缩到 [ − 1 , 1 ] [-1,1] [−1,1] 之间
和 S i g m o i d Sigmoid Sigmoid 神经元一样,它也存在饱和问题,但是和 S i g m o i d Sigmoid Sigmoid 神经元不同的是,它的输出是零中心的。因此,在实际操作中, t a n h tanh tanh 非线性函数比 S i g m o i d Sigmoid Sigmoid 非线性函数更受欢迎。注意 t a n h tanh tanh 神经元是一个简单放大的 S i g m o i d Sigmoid Sigmoid 神经元。
数学公式: f ( x ) = max ( 0 , x ) f(x) = \max(0, x) f(x)=max(0,x)
特点:一个关于 0 0 0 的阈值
优点:
缺点:
公式: f ( x ) = 1 ( x < 0 ) ( α x ) + 1 ( x > = 0 ) ( x ) f(x) = \mathbb{1}(x < 0) (\alpha x) + \mathbb{1}(x>=0) (x) f(x)=1(x<0)(αx)+1(x>=0)(x), α \alpha α 是小常量
特点:解决「 ReLU 死亡」问题, x < 0 x<0 x<0 时给出一个很小的梯度值,比如 0.01 0.01 0.01。
Leaky ReLU 修正了 x < 0 x<0 x<0 时 ReLU 的问题,有研究指出这个激活函数表现很不错,但是其效果并不是很稳定。Kaiming He等人在2015年发布的论文 Delving Deep into Rectifiers 中介绍了一种新方法 PReLU,把负区间上的斜率当做每个神经元中的一个参数,然而无法确定该激活函数在不同任务中均有益处。
公式: f ( x ) = { x i f x > 0 α ( e x p ( x ) − 1 ) o t h e r w i s e f(x)=\begin{cases} x & if \space\space x>0 \\ \alpha(exp(x)-1) & otherwise \end{cases} f(x)={xα(exp(x)−1)if x>0otherwise
特点:介于 ReLU 和Leaky ReLU 之间
具有 ReLU 的所有优点,但是不包括计算量;介于 ReLU 和 Leaky ReLU 之间,有负饱和的问题,但是对噪声有较强的鲁棒性。
max ( w 1 T x + b 1 , w 2 T x + b 2 ) \max \left(w_{1}^{T} x+b_{1}, w_{2}^{T} x+b_{2}\right) max(w1Tx+b1,w2Tx+b2)
公式: m a x ( w 1 T x + b 1 , w 2 T x + b 2 ) max(w_1^Tx+b_1, w_2^Tx + b_2) max(w1Tx+b1,w2Tx+b2)
特点:是对 ReLU 和 leaky ReLU 的一般化归纳
对于权重和数据的内积结果不再使用非线性函数,直接比较两个线性函数。ReLU 和 Leaky ReLU 都是这个公式的特殊情况,比如 ReLU 就是当 w 1 = 1 w_1=1 w1=1, b 1 = 0 b_1=0 b1=0 的时候。
Maxout 拥有 ReLU 单元的所有优点(线性操作和不饱和),而没有它的缺点(死亡的 ReLU 单元)。然而和 ReLU 对比,它每个神经元的参数数量增加了一倍,这就导致整体参数量激增。
实际应用Tips :
关于深度学习数据预处理的知识也可以对比阅读ShowMeAI的深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章深度学习的实用层面里【标准化输入】板块内容。
关于数据预处理有 3 个常用的符号,数据矩阵 X X X,假设其尺寸是 [ N × D ] [N \times D] [N×D]( N N N 是数据样本的数量, D D D 是数据的维度)。
减均值法是数据预处理最常用的形式。它对数据中每个独立特征减去平均值,在每个维度上都将数据的中心都迁移到原点。
在 numpy 中,该操作可以通过代码 X -= np.mean(X, axis=0)
实现。而对于图像,更常用的是对所有像素都减去一个值,可以用 X -= np.mean(X)
实现,也可以在 3 个颜色通道上分别操作。
具体来讲,假如训练数据是 50000 50000 50000 张 32 × 32 × 3 32 \times 32 \times 3 32×32×3 的图片:
之所以执行减均值操作,是因为解决输入数据大多数都是正或者负的问题。虽然经过这种操作,数据变成零中心的,但是仍然只能第一层解决 Sigmoid 非零均值的问题,后面会有更严重的问题。
归一化是指将数据的所有维度都归一化,使其数值范围都近似相等。
有两种常用方法可以实现归一化。
X /= np.std(X, axis=0)
。在图像处理中,由于像素的数值范围几乎是一致的(都在0-255之间),所以进行这个额外的预处理步骤并不是很必要。
在中间的零中心化数据的数值范围不同,但在右边归一化数据中数值范围相同。
这是另一种机器学习中比较常用的预处理形式,但在图像处理中基本不用。在这种处理中,先对数据进行零中心化处理,然后计算协方差矩阵,它展示了数据中的相关性结构。
# 假设输入数据矩阵X的尺寸为[N x D]
X -= np.mean(X, axis = 0) # 对数据进行零中心化(重要)
cov = np.dot(X.T, X) / X.shape[0] # 得到数据的协方差矩阵,DxD
数据协方差矩阵的第 ( i , j ) (i, j) (i,j) 个元素是数据第 i i i 个和第 j j j 个维度的协方差。具体来说,该矩阵的对角线上的元素是方差。还有,协方差矩阵是对称和半正定的。我们可以对数据协方差矩阵进行 SVD(奇异值分解)运算。
U,S,V = np.linalg.svd(cov)
U U U 的列是特征向量, S S S 是装有奇异值的1维数组(因为 cov 是对称且半正定的,所以S中元素是特征值的平方)。为了去除数据相关性,将已经零中心化处理过的原始数据投影到特征基准上:
Xrot = np.dot(X,U) # 对数据去相关性
np.linalg.svd
的一个良好性质是在它的返回值U中,特征向量是按照特征值的大小排列的。我们可以利用这个性质来对数据降维,只要使用前面的小部分特征向量,丢弃掉那些包含的数据没有方差的维度,这个操作也被称为 主成分分析(Principal Component Analysis 简称PCA) 降维:
Xrot_reduced = np.dot(X, U[:,:100]) # Xrot_reduced 变成 [N x 100]
经过上面的操作,将原始的数据集的大小由 [ N × D ] [N \times D] [N×D] 降到了 [ N × 100 ] [N \times 100] [N×100],留下了数据中包含最大方差的的 100 个维度。通常使用 PCA 降维过的数据训练线性分类器和神经网络会达到非常好的性能效果,同时还能节省时间和存储器空间。
有一问题是为什么使用协方差矩阵进行 SVD 分解而不是使用原 X X X 矩阵进行?
其实都是可以的,只对数据 X X X(可以不是方阵)进行 SVD 分解,做 PCA 降维(避免了求协方差矩阵)的话一般用到的是右奇异向量 V V V,即 V V V 的前几列是需要的特征向量(注意
np.linalg.svd
返回的是V.T
)。 X X X 是 N × D N \times D N×D,则 U U U 是 N × N N \times N N×N, V V V 是 D × D D \times D D×D;而对协方差矩阵( D × D D \times D D×D)做 SVD 分解用于 PCA 降维的话,可以随意取左右奇异向量 U U U、 V V V(都是 D × D D \times D D×D)之一,因为两个向量是一样的。
最后一个在实践中会看见的变换是白化(whitening)。白化操作的输入是特征基准上的数据,然后对每个维度除以其特征值来对数值范围进行归一化。
白化变换的几何解释是:如果数据服从多变量的高斯分布,那么经过白化后,数据的分布将会是一个均值为零,且协方差相等的矩阵。
该操作的代码如下:
# 对数据进行白化操作:
# 除以特征值
Xwhite = Xrot / np.sqrt(S + 1e-5)
注意分母中添加了 1e-5
(或一个更小的常量)来防止分母为 0 0 0,该变换的一个缺陷是在变换的过程中可能会夸大数据中的噪声,这是因为它将所有维度都拉伸到相同的数值范围,这些维度中也包含了那些只有极少差异性(方差小)而大多是噪声的维度。
在实际操作中,这个问题可以用更强的平滑来解决(例如:采用比 1e-5
更大的值)。
下图为 CIFAR-10 数据集上的 PCA、白化等操作结果可视化。
从左往右4张子图:
U.transpose()[:144,:]
来实现,然后将得到的 3072 个数值可视化。可以看见图像变得有点模糊了,然而,大多数信息还是保留了下来。U.transpose()[:144,:]
转换到图像像素基准上。实际上在卷积神经网络中并不会采用PCA和白化,对数据进行零中心化操作还是非常重要的,对每个像素进行归一化也很常见。
补充说明:
进行预处理很重要的一点是:任何预处理策略(比如数据均值)都只能在训练集数据上进行计算,然后再应用到验证集或者测试集上。
关于神经网络权重初始化的知识也可以对比阅读ShowMeAI的深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章深度学习的实用层面里【权重初始化缓解梯度消失和爆炸】板块内容。
初始化网络参数是训练神经网络里非常重要的一步,有不同的初始化方式,我们来看看他们各自的特点。
对一个两层的全连接网络,如果输入给网络的所有参数初始化为 0 0 0 会怎样?
这种做法是错误的。 因为如果网络中的每个神经元都计算出同样的输出,然后它们就会在反向传播中计算出同样的梯度,从而进行同样的参数更新。换句话说,如果权重被初始化为同样的值,神经元之间就失去了不对称性的源头。
现在权重初始值要非常接近 0 0 0 又不能等于 0 0 0,解决方法就是将权重初始化为很小的数值,以此来打破对称性。
其思路是:如果神经元刚开始的时候是随机且不相等的,那么它们将计算出不同的更新,并将自身变成整个网络的不同部分。
实现方法是:W = 0.01 * np.random.randn(D,H)
。其中 randn
函数是基于零均值和标准差的一个高斯分布来生成随机数的。
小随机数初始化在简单的网络中效果比较好,但是网络结构比较深的情况不一定会得到好的结果。比如一个 10 层的全连接网络,每层 500 个神经元,使用 t a n h tanh tanh 激活函数,用小随机数初始化。
代码与输出图像如下:
import numpy as np
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
# 假设一些高斯分布单元
D = np.random.randn(1000, 500)
hidden_layer_sizes = [500]*10 # 隐藏层尺寸都是500,10层
nonlinearities = ['tanh']*len(hidden_layer_sizes) # 非线性函数都是用tanh函数
act = {'relu': lambda x: np.maximum(0, x), 'tanh': lambda x: np.tanh(x)}
Hs = {}
for i in range(len(hidden_layer_sizes)):
X = D if i == 0 else Hs[i-1] # 当前隐藏层的输入
fan_in = X.shape[1]
fan_out = hidden_layer_sizes[i]
W = np.random.randn(fan_in, fan_out) * 0.01 # 权重初始化
H = np.dot(X, W) # 得到当前层输出
H = act[nonlinearities[i]](H) # 激活函数
Hs[i] = H # 保存当前层的结果并作为下层的输入
# 观察每一层的分布
print('输入层的均值:%f 方差:%f'% (np.mean(D), np.std(D)))
layer_means = [np.mean(H) for i,H in Hs.items()]
layer_stds = [np.std(H) for i,H in Hs.items()]
for i,H in Hs.items():
print('隐藏层%d的均值:%f 方差:%f' % (i+1, layer_means[i], layer_stds[i]))
# 画图
plt.figure()
plt.subplot(121)
plt.plot(list(Hs.keys()), layer_means, 'ob-')
plt.title('layer mean')
plt.subplot(122)
plt.plot(Hs.keys(), layer_stds, 'or-')
plt.title('layer std')
# 绘制分布图
plt.figure()
for i,H in Hs.items():
plt.subplot(1, len(Hs), i+1)
plt.hist(H.ravel(), 30, range=(-1,1))
plt.show()
可以看到只有第一层的输出均值方差比较好,输出接近高斯分布,后面几层均值方差基本为 0 0 0,这样导致的后果是正向传播的激活值基本为 0 0 0,反向传播时就会计算出非常小的梯度(因权重的梯度就是层的输入,输入接近 0 0 0,梯度接近 0 0 0 ),参数基本不会更新。
如果上面的例子不用小随机数,即 W = np.random.randn(fan_in, fan_out) * 1
,此时会怎样呢?
此时,由于权重较大并且使用的 tanh 函数,所有神经元都会饱和,输出为 + 1 +1 +1 或 − 1 -1 −1,梯度为 0 0 0,如下图所示,均值在 0 0 0 附近波动,方差较大在 0.98 0.98 0.98 附近波动,神经元输出大多为 + 1 +1 +1 或 − 1 -1 −1。
上述分析可以看出,权重过小可能会导致网络崩溃,权重过大可能会导致网络饱和,所以都在研究出一种合理的初始化方式。一种很好的经验是使用Xavier初始化:
W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in)
这是Glorot等在2010年发表的 论文。这样就保证了网络中所有神经元起始时有近似同样的输出分布。实践经验证明,这样做可以提高收敛的速度。
原理:假设神经元的权重 w w w 与输入 x x x 的内积为 s = ∑ i n w i x i s = \sum_i^n w_i x_i s=∑inwixi,这是还没有进行非线性激活函数运算之前的原始数值。此时 s s s 的方差:
Var ( s ) = Var ( ∑ i n w i x i ) = ∑ i n Var ( w i x i ) = ∑ i n [ E ( w i ) ] 2 Var ( x i ) + E [ ( x i ) ] 2 Var ( w i ) + Var ( x i ) Var ( w i ) = ∑ i n Var ( x i ) Var ( w i ) = n Var ( w ) Var ( x ) \begin{aligned} \text{Var}(s) &= \text{Var}(\sum_i^n w_ix_i) \\ &= \sum_i^n \text{Var}(w_ix_i) \\ &= \sum_i^n [E(w_i)]^2\text{Var}(x_i) + E[(x_i)]^2\text{Var}(w_i) + \text{Var}(x_i)\text{Var}(w_i) \\ &= \sum_i^n \text{Var}(x_i)\text{Var}(w_i) \\ &= n \text{Var}(w) \text{Var}(x) \end{aligned} Var(s)=Var(i∑nwixi)=i∑nVar(wixi)=i∑n[E(wi)]2Var(xi)+E[(xi)]2Var(wi)+Var(xi)Var(wi)=i∑nVar(xi)Var(wi)=nVar(w)Var(x)
前三步使用的是方差的性质(累加性、独立变量相乘);
第三步中,假设输入和权重的均值都是 0 0 0,即 E [ x i ] = E [ w i ] = 0 E[x_i] = E[w_i] = 0 E[xi]=E[wi]=0,但是 ReLU 函数中均值应该是正数。在最后一步,我们假设所有的 w i , x i w_i,x_i wi,xi 都服从同样的分布。从这个推导过程我们可以看见,如果想要 s s s 有和输入 x x x 一样的方差,那么在初始化的时候必须保证每个权重 w w w 的方差是 1 / n 1/n 1/n 。
又因为对于一个随机变量 X X X 和标量 a a a,有 Var ( a X ) = a 2 Var ( X ) \text{Var}(aX) = a^2\text{Var}(X) Var(aX)=a2Var(X),这就说明可以让 w w w 基于标准高斯分布(方差为1)取样,然后乘以 a = 1 / n a = \sqrt{1/n} a=1/n,即 Var ( 1 / n ⋅ w ) = 1 / n Var ( w ) = 1 / n \text{Var}( \sqrt{1/n}\cdot w) = 1/n\text{Var}(w)=1/n Var(1/n⋅w)=1/nVar(w)=1/n,此时就能保证 Var ( s ) = Var ( x ) \text{Var}(s) =\text{Var}(x) Var(s)=Var(x)。
代码为:W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in)
,其中fan_in
就是上文的 n n n。
不过作者在论文中推荐的是:
W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in + fan_out)
,使 Var ( w ) = 2 / ( n i n + n o u t ) \text{Var}(w) = 2/(n_{in} + n_{out}) Var(w)=2/(nin+nout),其中 n i n , n o u t n_{in}, n_{out} nin,nout 是前一层和后一层中单元的个数,这是基于妥协和对反向传播中梯度的分析得出的结论)
输出结果为:
图上可以看出,后面几层的输入输出分布很接近高斯分布。
但是使用 ReLU 函数这种关系会被打破,同样 w w w 使用单位高斯并且校准方差,然而使用 ReLU 函数后每层会消除一半的神经元(置 0 0 0 ),结果会使方差每次减半,会有越来越多的神经元失活,输出为 0 0 0 的神经元越来越多。如下图所示:
解决方法是 W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in/2)
。因为每次有一半的神经元失活,校准时除2即可,这样得到的结果会比较好。
这是2015年何凯明的论文 Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification 提到的方法,这个形式是神经网络算法使用 ReLU 神经元时的当前最佳推荐。结果如下:
另一个处理非标定方差的方法是将所有权重矩阵设为 0 0 0,但是为了打破对称性,每个神经元都同下一层固定数目的神经元随机连接(其权重数值由一个小的高斯分布生成)。一个比较典型的连接数目是10个。
偏置项(biases)的初始化:通常将偏置初始化为 0 0 0。
合适的初始化设置仍然是现在比较活跃的研究领域,经典的论文有:
当前的推荐是使用 ReLU 激活函数,并且使用 w = np.random.randn(n) * sqrt(2.0/n)
来进行权重初始化,n
是上一层神经元的个数,这是何凯明的论文得出的结论,也称作 He初始化。
关于Batch Normalization的详细图示讲解也可以对比阅读ShowMeAI的深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章网络优化:超参数调优、正则化、批归一化和程序框架里【Batch Normalization】板块内容。
批量归一化 是 loffe 和 Szegedy 最近才提出的方法,该方法一定程度解决了如何合理初始化神经网络这个棘手问题,其做法是让激活数据在训练开始前通过一个网络,网络处理数据使其服从标准高斯分布。
归一化是一个简单可求导的操作,所以上述思路是可行的。在实现层面,应用这个技巧通常意味着全连接层(或者是卷积层,后续会讲)与激活函数之间添加一个BatchNorm层。在神经网络中使用批量归一化已经变得非常常见,在实践中使用了批量归一化的网络对于不好的初始值有更强的鲁棒性。
具体来说,我们希望每一层网络的输入都近似符合标准高斯分布,考虑有 N N N 个激活数据的小批量输入,每个输入 x x x 有 D D D 维,即 x = ( x ( 1 ) ⋯ x ( d ) ) x = (x^{(1)} \cdots x^{(d)}) x=(x(1)⋯x(d)),那么对这个小批量数据的每个维度进行归一化,使符合单位高斯分布,应用下面的公式:
x ^ ( k ) = x ( k ) − E [ x ( k ) ] Var [ x ( k ) ] \hat{x}^{(k)} =\frac{x^{(k)}-\text{E}[x^{(k)}]}{\sqrt{\text{Var}[x^{(k)}]}} x^(k)=Var[x(k)]x(k)−E[x(k)]
批量归一化会把输入限制在非线性函数的线性区域,有时候我们并不想没有一点饱和,所以希望能控制饱和程度,即在归一化完成后,我们在下一步添加两个参数去缩放和平移归一化后的激活数据:
y ( k ) = γ ( k ) x ^ ( k ) + β ( k ) y^{(k)} = \gamma ^{(k)}\hat{x} ^{(k)}+\beta ^{(k)} y(k)=γ(k)x^(k)+β(k)
这两个参数可以在网络中学习,并且能实现我们想要的效果。的确,通过设置: γ ( k ) = Var [ x ( k ) ] \gamma ^{(k)}=\sqrt{\text{Var}[x^{(k)}]} γ(k)=Var[x(k)], β ( k ) = E [ x ( k ) ] \beta ^{(k)}=\text{E}[x^{(k)}] β(k)=E[x(k)] 可以恢复原始激活数据,如果这样做的确最优的话。现在网络有了为了让网络达到较好的训练效果而去学习控制让 tanh 具有更高或更低饱和程度的能力。
当使用随机优化时,我们不能基于整个训练集去计算。我们会做一个简化:由于我们在 SGD 中使用小批量,每个小批量都可以得到激活数据的均值和方差的估计。这样,用于归一化的数据完全可以参与梯度反向传播。
批量归一化的思想:考虑一个尺寸为 m m m 的小批量B。由于归一化被独立地应用于激活数据 x x x 的每个维度,因此让我们关注特定激活数据维度 x ( k ) x(k) x(k) 并且为了清楚起见省略 k k k。在小批量中共有 m m m 个这种激活数据维度 x ( k ) x(k) x(k): B = x 1 ⋯ m \text{B} ={x_{1 \cdots m}} B=x1⋯m
归一化后的值为: x ^ 1 ⋯ m \hat{x}_{1 \cdots m} x^1⋯m
线性转化后的值为: y 1 ⋯ m y_{1 \cdots m} y1⋯m
这种线性转化是批量归一化转化: BN γ , β : x 1 ⋯ m → y 1 ⋯ m \text{BN}_{\gamma, \beta} : x_{1 \cdots m} → y_{1 \cdots m} BNγ,β:x1⋯m→y1⋯m
于是,我们的小批量激活数据 B = x 1 ⋯ m \text{B} ={x_{1 \cdots m}} B=x1⋯m 通过BN层,有两个参数需要学习: γ \gamma γ, β \beta β ( ε \varepsilon ε 是为了维持数值稳定在小批量方差上添加的小常数)。
该BN层的输出为: y i = BN γ , β ( x i ) , i = 1 ⋯ m {y_i=\text{BN}_{\gamma, \beta}(x_i)},i=1 \cdots m yi=BNγ,β(xi),i=1⋯m,该层的计算有:
小批量均值: μ B ← 1 m ∑ i = 1 m x i \mu _B\leftarrow \frac{1}{m} \sum_{i=1}^m x_i μB←m1∑i=1mxi
小批量方差: σ B 2 ← 1 m ∑ i = 1 m ( x i − μ B ) 2 \sigma^2 _B\leftarrow \frac{1}{m} \sum_{i=1}^m (x_i-\mu _B)^2 σB2←m1∑i=1m(xi−μB)2
归一化: x ^ i ← x i − μ B σ B 2 + ε \hat{x} _i\leftarrow \frac{x_i-\mu _B}{\sqrt{\sigma^2 _B+\varepsilon } } x^i←σB2+εxi−μB
缩放和平移: y i ← γ x ^ i + β ≡ BN γ , β ( x i ) y_i\leftarrow \gamma \hat{x} _i+\beta \equiv \text{BN}_{\gamma,\beta }(x_i) yi←γx^i+β≡BNγ,β(xi)
补充说明:测试时不使用小批量中计算的均值和方差,相反,使用训练期间激活数据的一个固定的经验均值,例如可以使用在训练期间的平均值作为估计。
总结:批量归一化可以理解为在网络的每一层之前都做预处理,将输入数据转化为单位高斯数据或者进行平移伸缩,只是这种操作以另一种方式与网络集成在了一起。
事实证明,批量归一化能使网络更容易训练,但是对批量的大小有依赖性,批量太小效果不好,批量太大又受到硬件的限制。所以在对输入批量大小具有上限的复杂网络中不太有用。
目前已经提出了几种批量归一化的替代方案来缓解这个问题,其中一个就是层归一化。我们不再对这个小批量进行归一化,而是对特征向量进行归一化。换句话说,当使用层归一化时,基于该特征向量内的所有项的总和来归一化对应于单个数据点。
层归一化测试与训练的行为相同,都是计算每个样本的归一。可用于循环神经网络。
空间批量归一化(Spatial Batch Normalization)是对深度进行归一化。
卷积神经网络中的层归一化是对每张图片进行归一化。
实例归一化既对图片又对数据进行归一化;
组归一化(Group Normalization)2018年何凯明的论文 Group Normalization 提出了一种中间技术。
两层神经网络,一个隐藏层有 50 个神经元,输入图像是 3072 维的向量,输出层有 10 个神经元,代表10种分类。
使用小参数进行初始化,使正则损失为 0 0 0,确保得到的损失值与期望一致。
例如,输入数据集为CIFAR-10的图像分类
提高正则化强度,损失值会变大。
def init_two_layer_model(input_size, hidden_size, output_size):
model = {}
model["W1"] = 0.0001 * np.random.randn(input_size, hidden_size)
model['b1'] = np.zeros(hidden_size)
model['W2'] = 0.0001 * np.random.randn(hidden_size, output_size)
model['b2'] = np.zeros(output_size)
return model
model = init_two_layer_model(32*32*3, 50, 10)
loss, grad = two_layer_net(X_train, model, y_train, 0) # 0没有正则损失
print(loss)
对小数据子集过拟合。
model = init_two_layer_model(32*32*3, 50, 10)
trainer = ClassifierTrainer()
X_tiny = X_train[:20] # 选前20个作为样本
y_tiny = y_train[:20]
best_model, stats = trainer.train(X_tiny, y_tiny, X_tiny, y_tiny,
model, two_layer_net, verbose=True,
num_epochs=200, reg=0.0, update='sgd',
learning_rate=1e-3, learning_rate_decay=1,
sample_batchs=False)
理论上将进行梯度检查很简单,就是简单地把解析梯度和数值计算梯度进行比较。然而从实际操作层面上来说,这个过程更加复杂且容易出错。下面是一些常用的技巧:
① 使用中心化公式。
在使用有限差值近似来计算数值梯度的时候,常见的公式是: d f ( x ) d x = f ( x + h ) − f ( x ) h \frac{df(x)}{dx} = \frac{f(x + h) - f(x)}{h} dxdf(x)=hf(x+h)−f(x) 其中 h h h 是一个很小的数字,在实践中近似为 1e-5
。但是在实践中证明,使用中心化公式效果更好: d f ( x ) d x = f ( x + h ) − f ( x − h ) 2 h \frac{df(x)}{dx} = \frac{f(x + h) - f(x - h)}{2h} dxdf(x)=2hf(x+h)−f(x−h) 该公式在检查梯度的每个维度的时候,会要求计算两次损失函数(所以计算资源的耗费也是两倍),但是梯度的近似值会准确很多。
② 使用相对误差来比较。
数值梯度 f n ′ f'_n fn′ 和解析梯度 f a ′ f'_a fa′ 的绝对误差并不能准确的表明二者的差距,应当使用相对误差。 ∣ f a ′ − f n ′ ∣ max ( ∣ f a ′ ∣ , ∣ f n ′ ∣ ) \frac{\mid f'_a - f'_n \mid}{\max(\mid f'_a \mid, \mid f'_n \mid)} max(∣fa′∣,∣fn′∣)∣fa′−fn′∣ 在实践中:相对误差大于 1e-2
通常就意味着梯度可能出错;小于 1e-7
才是比较好的结果。但是网络的深度越深,相对误差就越高。所以对于一个10层网络,1e-2
的相对误差值可能就行,因为误差一直在累积。相反,如果一个可微函数的相对误差值是 1e-2
,那么通常说明梯度实现不正确。
③ 使用双精度。
一个常见的错误是使用单精度浮点数来进行梯度检查,这样会导致即使梯度实现正确,相对误差值也会很高(比如1e-2
)。保持在浮点数的有效范围。把原始的解析梯度和数值梯度数据打印出来,确保用来比较的数字的值不是过小。
④ 注意目标函数的不可导点(kinks) 。
在进行梯度检查时,一个导致不准确的原因是不可导点问题。不可导点是指目标函数不可导的部分,由 ReLU 函数、SVM损失、Maxout神经元等引入。考虑当 x=-1e-6
时,对 ReLU 函数进行梯度检查。因为 x < 0 x<0 x<0,所以解析梯度在该点的梯度为0。然而,在这里数值梯度会突然计算出一个非零的梯度值,因为 f ( x + h ) f(x+h) f(x+h) 可能越过了不可导点(例如:如果 h>1e-6
),导致了一个非零的结果。解决这个问题的有效方法是使用少量数据点。这样不可导点会减少,并且如果梯度检查对2-3个数据点都有效,那么基本上对整个批量数据也是没问题的。
⑤ 谨慎设置h。
并不是越小越好,如果无法进行梯度检查,可以试试试试将 h h h 调到 1e-4
或者 1e-6
。
在操作的特性模式中梯度检查。为了安全起见,最好让网络学习(「预热」)一小段时间,等到损失函数开始下降的之后再进行梯度检查。在第一次迭代就进行梯度检查的危险就在于,此时可能正处在不正常的边界情况,从而掩盖了梯度没有正确实现的事实。
⑥ 关闭正则损失。
推荐先关掉正则化对数据损失做单独检查,然后对正则化做单独检查,防止正则化损失吞没掉数据损失。
设置一个较小的正则强度,找到使损失下降的学习率。
best_model, stats = trainer.train(X_tiny, y_tiny, X_tiny, y_tiny,
model, two_layer_net, verbose=True,
num_epochs=10, reg=0.000001, update='sgd',
learning_rate=1e-6, learning_rate_decay=1,
sample_batchs=False)
学习率为 1 0 − 6 10^{-6} 10−6 时,损失下降缓慢,说明学习速率过小。
如果把学习率设为另一个极端: 1 0 6 10^{6} 106,如下图所示,会发生损失爆炸:
NaN通常意味着学习率过高,导致损失过大。设为 1 0 − 3 10^{-3} 10−3 时仍然爆炸,一个比较合理的范围是 [ 1 0 − 5 , 1 0 − 3 ] [10^{-5}, 10^{-3}] [10−5,10−3]。
训练期间第一个要跟踪的数值就是损失值,它在前向传播时对每个独立的批数据进行计算。
在下面的图表中, x x x 轴通常都是表示周期(epochs)单位,该单位衡量了在训练中每个样本数据都被观察过的次数的期望(一个 epoch 意味着每个样本数据都被观察过了一次)。相较于迭代次数(iterations) ,一般更倾向跟踪 epoch,这是因为迭代次数与数据的批尺寸(batchsize)有关,而批尺寸的设置又可以是任意的。
比如一共有 1000个 训练样本,每次 SGD 使用的小批量是 10 个样本,一次迭代指的是用这 10 个样本训练一次,而1000个样本都被使用过一次才是一次 epoch,即这 1000 个样本全部被训练过一次需要 100 次 iterations,一次 epoch。
下图展示的是损失值随时间的变化,曲线形状会给出学习率设置的情况:
左图展示了不同的学习率的效果。过低的学习率导致算法的改善是线性的。高一些的学习率会看起来呈几何指数下降,更高的学习率会让损失值很快下降,但是接着就停在一个不好的损失值上(绿线)。这是因为最优化的「能量」太大,参数随机震荡,不能最优化到一个很好的点上。过高的学习率又会导致损失爆炸。
右图显示了一个典型的随时间变化的损失函数值,在CIFAR-10数据集上面训练了一个小的网络,这个损失函数值曲线看起来比较合理(虽然可能学习率有点小,但是很难说),而且指出了批数据的数量可能有点太小(因为损失值的噪音很大)。损失值的震荡程度和批尺寸(batch size)有关,当批尺寸为1,震荡会相对较大。当批尺寸就是整个数据集时震荡就会最小,因为每个梯度更新都是单调地优化损失函数(除非学习率设置得过高)。
下图这种开始损失不变,然后开始学习的情况,说明初始值设置的不合理。
在训练分类器的时候,需要跟踪的第二重要的数值是验证集和训练集的准确率。这个图表能够展现知道模型过拟合的程度:
训练集准确率和验证集准确率间的间距指明了模型过拟合的程度。在图中,蓝色的验证集曲线比训练集准确率低了很多,这就说明模型有很强的过拟合。遇到这种情况,就应该增大正则化强度(更强的L2权重惩罚,更多的随机失活等)或收集更多的数据。另一种可能就是验证集曲线和训练集曲线很接近,这种情况说明模型容量还不够大:应该通过增加参数数量让模型容量更大些。
最后一个应该跟踪的量是权重中更新值的数量和全部值的数量之间的比例**。注意**:是更新的,而不是原始梯度(比如,在普通sgd中就是梯度乘以学习率)。需要对每个参数集的更新比例进行单独的计算和跟踪。一个经验性的结论是这个比例应该在 1e-3
左右。如果更低,说明学习率可能太小,如果更高,说明学习率可能太高。下面是具体例子:
# 假设参数向量为W,其梯度向量为dW
param_scale = np.linalg.norm(W.ravel()) # ravel将多维数组转化成一维;
# np.linalg.norm默认求L2范式
update = -learning_rate*dW # 简单SGD更新
update_scale = np.linalg.norm(update.ravel())
W += update # 实际更新
print update_scale / param_scale # 要得到1e-3左右
如果数据是图像像素数据,那么把第一层特征可视化会有帮助:
左图: 特征充满了噪音,这暗示了网络可能出现了问题:网络没有收敛,学习率设置不恰当,正则化惩罚的权重过低。
右图: 特征不错,平滑,干净而且种类繁多,说明训练过程进行良好。
关于超参数调优的讲解也可以对比阅读ShowMeAI的深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章网络优化:超参数调优、正则化、批归一化和程序框架里【超参数调优】板块内容。
如何进行超参数调优呢?常需要设置的超参数有三个:
下面介绍几个常用的策略:
在大多数情况下,一个尺寸合理的验证集可以让代码更简单,不需要用几个数据集来交叉验证。
选择几个非常分散的数值,然后使用几次 epoch(完整数据集训练一轮是1个epoch)去学习。经过几次 epoch,基本就能发现哪些数值较好哪些不好。比如很快就 nan(往往超过初始损失 3 倍就可以认为是 nan,就可以结束训练。),或者没有反应,然后进行调整。
发现比较好的区间后,就可以精细搜索,epoch 次数更多,运行时间更长。比如之前的网络,每次进行 5 次 epoch,对较好的区间进行搜索,找到准确率比较高的值,然后进一步精确查找。注意,需要在对数尺度上进行超参数搜索。
也就是说,我们从标准分布中随机生成了一个实数,然后让它成为 10 的次数。对于正则化强度,可以采用同样的策略。直观地说,这是因为学习率和正则化强度都对于训练的动态进程有乘的效果。
例如:当学习率是 0.001 的时候,如果对其固定地增加 0.01,那么对于学习进程会有很大影响。然而当学习率是 10 的时候,影响就微乎其微了。这就是因为学习率乘以了计算出的梯度。
比起加上或者减少某些值,思考学习率的范围是乘以或者除以某些值更加自然。但是有一些参数(比如随机失活)还是在原始尺度上进行搜索。
max_count = 100
for count in range(max_count):
reg = 10**uniform(-5, 5) # random模块的函数uniform,会在-5~5范围内随机选择一个实数
# reg在10^-5~10^5之间取值,指数函数
lr = 10**uniform(-3, -6)
model = init_two_layer_model(32 * 32 * 3, 50, 10)
trainer = ClassifierTrainer()
best_model, stats = trainer.train(X_tiny, y_tiny, X_tiny, y_tiny,
model, two_layer_net, verbose=False,
num_epochs=5, reg=reg, update='momentum',
learning_rate=lr, learning_rate_decay=0.9,
sample_batchs=True, batch_size=100)
比较好的结果在红框中,学习率在 10e-4
左右,正则强度在 10e-4~10e-1
左右,需要进一步精细搜索。修改代码:
max_count = 100
for count in range(max_count):
reg = 10**uniform(-4, 0)
lr = 10**uniform(-3, -4)
有一个相对较好的准确率: 53 % 53\% 53%。但是这里却有一个问题,这些比较高的准确率都是学习率在 10e-4
附近,也就是说都在我们设置的区间边缘,或许 10e-5
或 10e-6
有更好的结果。所以在设置区间的时候,要把较好的值放在区间中间,而不是区间边缘。
随机搜索优于网格搜索。Bergstra 和 Bengio 在文章 Random Search for Hyper-Parameter Optimization 中说「随机选择比网格化的选择更加有效」,而且在实践中也更容易实现。通常,有些超参数比其余的更重要,通过随机搜索,而不是网格化的搜索,可以让你更精确地发现那些比较重要的超参数的好数值。
上图中绿色函数部分是比较重要的参数影响,黄色是不重要的参数影响,同样取9个点,如果采用均匀采样就会错过很多重要的点,随机搜索就不会。
下一篇 深度学习与CV教程(7) | 神经网络训练技巧 (下) 会讲到的学习率衰减方案、更新类型、正则化、以及网络结构(深度、尺寸)等都需要超参数调优。
可以点击 B站 查看视频的【双语字幕】版本
【字幕+资料下载】斯坦福CS231n | 面向视觉识别的卷积神经网络 (2017·全16讲)