改善神经网络的性能
1 数据集
1.1 数据集的划分
如果数据集不是太大,比如包含的样本只有一万或以下,那么传统的做法一般按照以下比例对数据集进行划分训练集(train_set)、验证集(dev_set)以及测试集(test_set)
训练集:60% | 验证集:20% | 验证集:20% |
---|
在机器学习的领域里,如果只有训练集和验证集而没有单独的测试集的话,大多数人会将训练集就称为训练集,而把验证集称为测试集。但是它们实际上做的事情是把测试集当成了 hold-out 交叉验证集,这只是名称的不同,但是都是在做与验证集相同的事情。这时,数据集的划分比例就变成了:
训练集:70% | 测试(验证)集:30% |
---|
如果一共只有 100 个样本,或者 1000 个样本,甚至到 10000 个样本,以上的比例都是合理的。但在大数据时代,数据量庞大了很多,这个比例可能会发生变化。
验证集的意义是用来测试不同的算法并确定哪种最好,所以验证集只需大到能够在评估多种不同的算法时快速选出较好的一种。在数据量特别庞大的情况下达成这个目标可能不需要多达20%的数据。所以如果总共有 100 万个样本,那么验证集可能只要 1 万个样本就足够用来评估两种算法中哪一种更好。
与验证集相似,测试集的主要功能是对训练好的分类器的性能给出可信度较高的评估。同样如果总共有 100 万个样本,但只要 1 万个就足够能对模型的性能给出比较准确的估计了。
所以总结起来,当设定机器学习问题时 我通常将数据分为训练集、验证集和测试集。如果数据集比较小,可以采用传统的分割比率,但如果数据集大了很多,那也可以使验证集和测试集远小于总数据 20% 甚至远少于 10%
1.2 数据集的分布
我们需要用验证集对许多不同的模型进行评估,费尽全力改善模型在验证集上的性能。但是如果训练集与验证集里面的数据分布不同,则模型很难在验证集上有好的表现。比如对于行人检测,如果训练集里都是低分辨率的图片,而验证集里全是高分辨率的图片,那么训练出来的模型则很难适应验证集。如果开发集和测试集的数据分布相同就很方便了。
2 偏差与方差
2.1 高偏差与高偏差
高偏差: 欠拟合,可以通过训练集误差判断
高方差: 过拟合,可以通过验证集误差判断
最优误差(贝叶斯误差):高偏差与高方差的判定应该以最优误差为基准
2.2 解决方案
高偏差解决方案:
1、采用更大规模的网络(一般都会其作用,至少可以拟合,有时会造成过拟合)
2、延长训练时间(可能有用)
3、寻找更适合的神经网络架构(可能有用)
高方差解决方案:
1、采用更多数据(最优办法)
2、正则化
3、寻找更适合的神经网络架构(可能有用)
深度学习早期很难找到一个工具,使之减少偏差或方差却不影响到另外一方,但在当前的深度学习和大数据时代,只要正则适度,通常构建一个更大的网络便可以在不影响方差的同时减少偏差。而采用更多的数据通常可以在不过多影响偏差的同时减少方差。
3 防止过拟合
3.1 L2正则化(L2 regularization)
L2正则化是在最小化训练误差的基础上,尽可能采用简单的模型,以此提高泛化预测精度。除了 L2 正则化,还有 L1 正则化,这里仅仅讨论前者。
L2正则化通过改变代价函数来实现,具体来说是在原代价函数的基础上加上了一个权重
W
W
W 的平方和( L2 范数的平方),即:
正则化参数
λ
\lambda
λ 通常设置为较小值,当其值为 0 时,正则化项就失去了作用,所以可以通过设置
λ
=
0
\lambda=0
λ=0 来关闭L2 正则化。此外,
λ
\lambda
λ (lambda) 在python中是保留字,所以编程时一般使用 lambd 来表示
λ
\lambda
λ 。
因为代价函数发生了变化,正则项也会对
W
[
i
]
W^{[i]}
W[i] 产生梯度,即
d
d
W
[
i
]
(
1
2
λ
m
W
[
i
]
2
)
=
λ
m
W
[
i
]
\frac{\mathrm{d}}{\mathrm{d}W^{[i]}}(\frac{1}{2}\frac{\lambda}{m}{W^{[i]}}^2)=\frac{\lambda}{m}W^{[i]}
dW[i]d(21mλW[i]2)=mλW[i],在反向传播中,还应在原求导公式的后面我们还应该加上正则项部分的导数。
d
W
[
i
]
=
1
m
d
Z
[
i
]
A
[
i
−
1
]
T
+
λ
m
W
[
i
]
dW^{[i]} = \frac{1}{m}dZ^{[i]}A^{[i-1]^T}+\frac{\lambda}{m}W^{[i]}
dW[i]=m1dZ[i]A[i−1]T+mλW[i]
那么为什么 L2 正则化能够防止过拟合呢?
当正则项系数
λ
\lambda
λ 很大时,对参数的惩罚也将很大,导致部分权重
W
W
W 在更新后变得特别小(趋向于零)。这个现象也叫做权重衰减,直观上看上去这部分
W
W
W 对应的隐层节点就失去了意义, 模型就变得简单了许多,神经网络看起来更像是逻辑回归或者线性模型,而简单的模型并不适合非常复杂的决策以及过度复杂的非线性边界划分。拿
t
a
n
h
tanh
tanh 激活函数举例,当
W
W
W 非常小时,
Z
Z
Z 就更趋于零,而在
Z
=
0
Z=0
Z=0 附近,
t
a
n
h
(
Z
)
tanh(Z)
tanh(Z) 相当于一个线性模型。
所以正则项系数 λ \lambda λ 可以理解为模型复杂度的惩罚力度,当其很大时,模型复杂度将变小,也就是模型将更为简单,不会使得对数据过于拟合。综上所述,加入正则化项,就是在最小化经验误差(训练误差)的情况下,可以让我们选择解更简单(部分 W 趋向于0)的解。因此,加正则化项就是结构风险最小化的一种实现。具体可以参考这篇文章。
L2 正则化也有一个缺点,当正则化参数 lambda 很小时,其起到的消除过拟合的作用是微乎其微的,但当 lambda 过大时,又会造成欠拟合。我们需要通过多次测试,才能找出最好的 lambda 的值。
3.2 Dropout 正则化
L1、L2正则化是通过修改代价函数来实现消减过拟合,而Dropout则是通过修改神经网络本身来达到同样的目的。具体来说,针对每个样本,Dropout 会随机关闭神经网络中部分节点,并且利用剩下的节点完成前向传播和反向传播。也就是说,对于不同的样本,梯度下降算法使用的神经网络是不同的。下面我们用 Inverted Dropout 来实现。
执行 Inverted Dropout 的时候,会借助一个叫做 keep_prob 的标量,其取值介于 0 与 1 之间,它可以看作是一个阈值,比如,若 keep_prob = 0.8,那么将留下 80% 的节点,而关闭其余 20% 的节点。当 keep_prob = 1 时,会保留神经网络中全部的节点,也就是相当于关闭了 Dropout。
现在知道了需要关闭节点的比例,那么到底该关闭哪些节点呢?这里 keep_prob 会与向量 D 合作来共同决定关闭哪些节点。向量 D 与所有样本的激活值向量 A、激活梯度向量dA 的形状相同,其每个元素的值也介于 0 与 1 之间,且呈均匀分布。Inverted Dropout 方法会把所有小于 keep_prob 的元素重新赋值为 1;而把向量 D 中所有大于等于 keep_prob 的元素重新赋值为 0,表示向量 A、dA 中对应位置的激活值或导数是无效的,所以 Inverted Dropout 还会把 A、dA 中这些对应位置的元素重新赋值为 0,这样就不会在传播的时候对后面的计算产生影响,从而达到了关闭部分节点的效果。
假设有 m 个样本,则向量 D 包含 m 列,每一列对应一个样本。因为 D 是随机初始化的,所以每一列中哪些位置的元素大于 keep_prob 也是随机的。也就是说,针对每个样本,关闭的神经网络节点也是不相同的。而在梯度下降算法的每次迭代中,某层的哪些节点与后一层某个节点的合作关系也不是固定的,从而减少了某个节点对前一层某些节点的固定依赖。
以神经网络的第一层为例,在前向传播中,Inverted Dropout 分为以下四个步骤:
1、创建一个与 A1 形状相同,元素值介于 0 与 1 之间且均匀分布的矩阵 D1:
D1 = np.random.rand(A1.shape[0], A1.shape[1])
2、把向量 D1 所有小于 keep_prob 的元素重新赋值为 1,大于等于 keep_prob 的元素重新赋值为 0:D1 = D1 < keep_prob
3、关闭 A1 中的部分神经网络节点:A1 = A1 * D1
4、缩放没有关闭的神经网络节点的激活值,确保激活值与在没有使用Dropout 时具有相同的预期:A1 = A1 / keep_prob
而反向传播会相对简单一些,只需两个步骤:
1、再次关闭在前向传播中被关闭的神经元:
dA1 = dA1 * D1
2、缩放没有关闭的神经网络节点的激活梯度:dA1 = dA1 / keep_prob
在用 Dropout 解决过拟合问题时需要注意3点:
- 只需要在训练时,在前向传播和反向传播中应用 Dropout,当训练完毕进入测试阶段时,我们应该关掉 Dropout。
- 输入层和输出层是不能用 Dropout 进行处理。
- 每一层的 keep_prob 可以不一样。
3.3 数据扩增(Data augmentation)
当出现过拟合现象时,有可能是样本匮乏造成的,但从网上获取或者自己制作新样本往往又需要很大的成本。这时,我们可以在已有的样本上做一些处理来得到新的样本。
例如在图像识别与图像分类任务中,我们可以通过将已有图像左右反转,旋转等方式来得到新的样本。
对于一些光学字符识别任务,可以通过扭曲等方式来获取新的样本。
3.4 early stopping
当我们执行梯度下降算法时,随着训练时间的增加,模型在训练集上的误差一般会单调递减至收敛到 0,而模型在验证集上的误差则会先一直下降,到达某个点后,然后开始慢慢上升,如下图所示。
顾名思义,early stopping 就是在到达那个点时停止训练,这个时候得到的在验证集上的误差是最小的。但是采用 early stopping 也有一个缺点,不能同时降低在训练集上与在验证集上的误差。
4 数据归一化
为了使梯度下降时 cost 更快收敛,可以对输入数据进行归一化处理。
偷个懒: https://www.jianshu.com/p/63f8fb43a400
5 梯度消失与梯度爆炸
5.1 什么是梯度消失(爆炸)
训练很深的神经网络时,随着层数的增加,导数会出现指数级的下降,则导致梯度消失。或者指数级的增加,导致梯度爆炸;本质是梯度传递的链式法则所导致的矩阵高次幂(反向传播会逐层对函数求偏导相乘);
5.2 梯度消失(爆炸)的原因与危害
假设这里有一个
L
L
L 层的神经网络,不同的层激活函数可以不一样,我们用
g
[
i
]
(
Z
[
i
]
)
g^{[i]}(Z^{[i]})
g[i](Z[i])表示第 i 层的激活函数。则在前向传播中,前一层的输出作为后一层的输入,一层一层的计算直至输出
A
[
L
]
A^{[L]}
A[L],如下图所示。
在反向传播算法中,我们通过链式求导法则从后向前逐层求导,前一层的导数依赖于后一层导数的计算结果,如下所示(注意这里并没有考虑矩阵乘法的维度是否匹配,可以将各个矩阵当作一个标量看待):
由推导可以看出,代价函数对神经网络第 i 层的权重
W
[
i
]
W^{[i]}
W[i] 的偏导(梯度)是由网络深度、从这一层到输出层的权重
W
W
W 和 激活函数导数
g
′
g'
g′ 决定的。
如果每一层权重 W W W 经过初始化后绝对值较大,且与对应层的激活函数导数 g ′ g' g′ 的乘积绝对值大于 1,哪怕是大了很小的一点,在反向传播的时候梯度 ∂ J ∂ W \frac{\partial{J}}{\partial{W}} ∂W∂J 会从后向前逐层呈指数增加,这样就造成了梯度爆炸。在极端情况下,梯度爆炸会造成权重 W W W 在更新后的绝对值变得非常大,以至于溢出,导致 NaN 值(即无穷大)。
但在训练神经网络时,梯度消失出现得更多。梯度消失与梯度爆炸正好相反。如果每一层权重 W W W 经过初始化后绝对值较小,且与对应层的激活函数导数 g ′ g' g′ 的乘积绝对值位于 0 与 1 之间,在反向传播的时候梯度 ∂ J ∂ W \frac{\partial{J}}{\partial{W}} ∂W∂J 会从后向前逐层呈指数减小,这样就造成了梯度消失。梯度消失会拖慢梯度下降的速度,增加代价函数收敛到 0 的时间,严重时可能使代价函数根本不会收敛于 0 ,这样花再多的时间训练也无济于事。
5.3 防止梯度消失(爆炸)
我们可以通过从造成梯度消失(爆炸)的三个原因下手,从而防止梯度消失(爆炸)的出现:
1、神经网络参数
W
W
W 的初始化:当出现梯度消失或者梯度爆炸时,我们可以尝试通过 5.4 的方式来初始
W
W
W 以调整其大小。
2、更换激活函数:比如当
Z
Z
Z 的绝对值较大时,
s
i
g
m
o
i
d
(
Z
)
sigmoid(Z)
sigmoid(Z) 与
t
a
n
h
(
Z
)
tanh(Z)
tanh(Z) 函数的导数是接近于 0 的,这很容易造成梯度消失,此时
R
e
L
U
ReLU
ReLU 函数是一个不错的选择。 所以我们在隐藏层中最好不要选择
s
i
g
m
o
i
d
sigmoid
sigmoid 与
t
a
n
h
tanh
tanh 作为激活函数。
3、神经网络的深度:神经网络的层数越多,就越容易造成梯度爆炸与梯度消失。所以,可以在使性能不打折扣的情况下缓慢地减少层数,以达到防止梯度爆炸与梯度消失的目的。
5.4 权重 W 的初始化
权重 W W W 初始化不正确可能导致梯度消失或爆炸,这也会降低优化算法的速度。而正确的的初始化方法可以:
- 加速梯度下降
- 使代价函数收敛到较低的训练(和泛化)误差
在逻辑回归中我们可以把权重
W
W
W 全部初始化为 0,逻辑回归可以很好的工作。但用同样的方法初始化神经网络的权重
W
W
W 是行不通的,因为会引起对称问题,从而导致同一层的神经元学习到同样的参数,进而做一些同样的计算工作。在神经网络中,为了打破对称性,取而代之的是用随机初始化。但将权重初始化为非常大的随机值也不能很好地工作,所以,我们还需要将随机初始化后的参数进一步做缩放处理。下面给出了一些常见的初始化方法,其中 W[i]
表示第 i
层的权重,n[i]
表示第 i
层的节点数。
1、初始化为较小的随机数:如均值为 0,方差为 0.01 的高斯分布:
W[i] = np.random.randn(n[i], n[i-1]) * 0.01
但是此方法只适用于较浅的网络,对于深层次网络依然会出现梯度消失的现象。
2、MSRA初始化:初始化为均值为 0,方差为 2/n 的高斯分布。
W[i] = np.random.randn(n[i], n[i-1]) * np.sqrt(2 / n[i-1])
此方法由何恺明大神在论文《Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification》中提出,针对ReLU激活函数的权重初始化方法。 具体原理在论文中有详细分析。
3、Xavier 初始化:
W[i] = np.random.randn(n[i], n[i-1]) * np.sqrt(1 / n[i-1])
基于正态分布初始化的问题,此方法可以保持输入和输出的方差一致。其优点是:随着网络层数的加深,每层的激活函数值仍能保持着正态分布,对于 tanh 激活函数可以较快的进行梯度更新。也就是说,Xavier 初始化适用于 tanh 激活函数。
另一种适用于 tanh 激活函数的权重初始化方法是方差使用 Var(W[i]) = 2 / (n[i] + n[i-1])
,即:
W[i] = np.random.randn(n[i], n[i-1]) * np.sqrt(2 / (n[i] + n[i-1]))
这两种方法在论文《Understanding the difficulty of training deep feedforward neural networks》中有详细证明。
5.5 梯度检验
在梯度下降算法中,我们需要反向传播来求代价函数对参数的导数,通过得到的结果再来更新参数。反向传播计算导数反向传播是通过链式法则和求导公式的方式来求解代价函数对各个参数的导数,当网络结构很复杂时,链式法则的推导就变得非常繁琐。如果在某一层导数计算出现错误,那么可能会导致后面所有层的导数计算都是错误的。而反向传播又是学习算法中核心过程之一,所以确保反向传播的正确性也是至关重要的。
我们可以用梯度检验来检查反向传播过程是否是正确无误的。步骤如下:
1、如图,将所有参数 W 和 b 按顺序整合在一起,并将每个 W 和 b 伸展成向量。将整理好后的结果储存在向量 values 中。
2、使用定义法(区别于高等数学中的单边导数)。求代价函数对某个 W 或 b 的导数。这里以 W1 为例:
∂ J ( W 1 , . . . , W L , b 1 , . . . , b L ) ∂ θ = lim ϵ → 0 J ( W 1 + ϵ , . . . , W L , b 1 , . . . , b L ) − J ( W 1 − ϵ , . . . , W L , b 1 , . . . , b L ) 2 ϵ \frac{\partial{J(W1,...,WL,b1,...,bL)}}{\partial{\theta}}=\lim_{\epsilon \to 0}\frac{J(W1+\epsilon,...,WL,b1,...,bL)-J(W1-\epsilon,...,WL,b1,...,bL)}{2\epsilon} ∂θ∂J(W1,...,WL,b1,...,bL)=ϵ→0lim2ϵJ(W1+ϵ,...,WL,b1,...,bL)−J(W1−ϵ,...,WL,b1,...,bL)按照这个公式求出代价函数对所有 W 和 b 的导数,并按照储存在向量 gradapprox 中。在编码过程中,我们用 ϵ = 1 0 − 7 \epsilon=10^{-7} ϵ=10−7 来代替 ϵ → 0 \epsilon \to 0 ϵ→0.
.
3、将反向传播的结果转化成与 gradapprox 相对应的向量grad中,通过下面的公式判断两个向量接近程度。 d i f f e r e n c e = ∣ ∣ g r a d − g r a d a p p r o x ∣ ∣ 2 ∣ ∣ g r a d ∣ ∣ 2 + ∣ ∣ g r a d a p p r o x ∣ ∣ 2 difference = \frac{||grad-gradapprox||_2}{||grad||_2+||gradapprox||_2} difference=∣∣grad∣∣2+∣∣gradapprox∣∣2∣∣grad−gradapprox∣∣2 公式中 ∣ ∣ g r a d ∣ ∣ 2 ||grad||_2 ∣∣grad∣∣2 表示 grad 的欧几里得范数,编程时可以用np.linalg.norm(grad)
实现。一般地,当结果 d i f f e r e n c e < 1 0 − 7 difference<10^{-7} difference<10−7 时,表示反向传播算法计算的导数是没有问题的。
使用梯度检验同样需要注意几点:
- 在 Debug 时使用梯度检验,训练时不要使用,应为梯度检验需要很高的时间成本。
- 当使用了 L2 正则化时,代价函数发生变化,此时梯度检验需要加上正则项对参数 W 或 b 的导数。
- 梯度检验不要和 Dropout 一起使用,可以先用梯度检验确认方向传播算法的正确性,然后在用 Dropout 进行训练。