深度学习入门(五):学习相关的技巧
文章目录
- 前言
- 如何找到最优的参数
- 随机梯度下降法(SGD)
- 原理
- 优缺点
- Momentum
- AdaGrad
- Adam
- 总结与对比
- 权重初始值的设置
- 全部设置为0可以吗?
- 权重初始值对激活层分布的影响
- 如何控制激活层的分布
- batch normalization
- batch normalization效果测试
- 如何防止过拟合——正则化登场
- 正则化方法一:权值衰减
- 正则化方法二:dropout
- 原理
- 为什么dropout可以抑制过拟合
- 超参数的验证
前言
本章我们将从:
- 如何找到最优的参数?(SGD、Momentum、AdaGrad、Adam)
- 参数/权重的初始值如何设置?(是否可以全部初始化为0?权重初始值对激活层分布的影响?)
- 怎么控制激活层的分布?(Batch Normalization)
- 怎么防止过拟合?(正则化:权值衰减、dropout)
- 如何高效寻找超参数?(随机采样 VS 网格搜索)
这5个问题入手,介绍一下学习相关的技巧。
如何找到最优的参数
神经网络学习的目的是找到使损失函数的值尽可能小的参数。这是寻找最优参数的问题,解决这个问题的过程称为最优化。
随机梯度下降法(SGD)
原理
在梯度下降法一文中,我们已经学习过梯度下降法了。复习一下它的公式是:
w = w − η ∂ L ∂ w w = w - \eta\frac{\partial L}{\partial w} w=w−η∂w∂L
啰嗦几句,因为梯度 ∂ L ∂ w \frac{\partial L}{\partial w} ∂w∂L代表的是函数增长最快的方向,这里我们是希望损失函数的值最小,所以要沿着梯度的反方向走,因此是用减号。
用代码表示为:
class SGD:class __init__(self, lr=0.01):self.lr = lrdef update(self, params, grads):for key in params.keys():params[key] -= self.lr * grads[key]
实际调用时:
network = TwoLayerNet()
optimizer = SGD()for i in range(1000):x_batch, t_batch = get_mini_batch()grads = network.gradient(x_batch, t_batch)params = network.paramsoptimmizer.update(params, grads)
优缺点
- 优点:简单、容易实现;
- 缺点:在某些情况下搜索不高效,本质原因是损失函数值下降最快的方向 ≠ \neq =直接奔向最小值,它可能会产生震荡(在不同方向来回变换),从而导致收敛慢。
举个例子,假设我们想寻找下面这个函数的最小值:
f ( x , y ) = 1 20 x 2 + y 2 f(x,y) =\frac{1}{20} x^2+y^2 f(x,y)=201x2+y2
该函数的图像和等高线如下图:
该函数的梯度如下图所示(箭头的方向代表梯度的方向,箭头的长度代表梯度的模,即梯度向量的大小)。可以看到,该梯度的特征是:y轴方向大,x轴方向小。
最终基于SGD搜索的效果如下,可以看到搜索过程呈“之”字型,这是一个相当低效的搜索路径。当函数的形状非均向,比如呈延伸状,搜索的效率就会非常低下。
为了解决上述的低效问题,我们接下来学习Momentum, AdaGrad和Adam三种方法,来取代SGD。
Momentum
先看一下数学公式:
v = α v − η ∂ L ∂ w w = w + v v = \alpha v- \eta\frac{\partial L}{\partial w}\\ w = w+v v=αv−η∂w∂Lw=w+v
和SGD的公式对比一下:
w = w − η ∂ L ∂ w w = w - \eta\frac{\partial L}{\partial w} w=w−η∂w∂L
发现多了 v v v这一项。通过下面的代码理解一下 v v v的计算。初始化为0,保存了参数结构一样的数据,剩余的代码就是实现上面的数学公式。
class Momentum:def __init__(self, lr=0.1, momentum=0.9):self.lr = lrself.momentum = momentumself.v = Nonedef update(self, params, grads):if self.v is None:self.v = dict()for key, val in params.items():self.v[key] = np.zeros_like(val)for key, val in params.items():self.v[key] = self.momentum*self.v[key] - self.lr*grads[key]params[key] += self.v[key]
看一下Momentum的效果如下图所示,可以发现“之”字型震荡的幅度变小了。怎么理解这个优化效果呢?(重要)
因为 v v v初始化为0,一开始 v v v保存的就是梯度的负方向 − η ∂ L ∂ w - \eta\frac{\partial L}{\partial w} −η∂w∂L。想想震荡过程的一个特点是,一会儿朝前一会儿往后,也就是说梯度的方向是相反的。那么在 v v v第一次更新的时候, v = α v − η ∂ L ∂ w v = \alpha v- \eta\frac{\partial L}{\partial w} v=αv−η∂w∂L,式子的第一项是初始化的梯度,第二项当前的梯度(势必和之前方向相反),这时两者加和就出现了一个抵消的作用。
前例中,x轴方向受到的力虽然非常小,但是一直稳定的在受力,一直朝着同一个方向,慢慢会形成一个加速度的感觉~y轴方向受到的力虽然非常大,但是在来回震荡,一会儿朝前一会儿朝后,会相互抵消。因此和SGD相比,momentum可以更快的朝x轴的方向靠近,并减弱y轴方向的震荡,即“之”字震动的幅度。
回到Momentum这个词本身,它是动量的意思。它就像一个小球在斜面上滚动,小球是有惯性的,就算在某个方向上的受力有突然急剧的变化,它也不会立刻大幅的偏离轨道,而是“考虑”过去的方向和速度。
AdaGrad
Momentum是针对SGD震荡的缺点,引入了“惯性”减少了衰减。参数的更新中还有一个参数很重要,那就是学习率 η \eta η。学习率太大,可能导致学习太发散,不能正确学习;学习率太小,可能导致学习的太慢,收敛需要很长时间。
一种常用的技巧是「学习率衰减」,即在开始的时候多学,在后面的时候少学,先看公式:
h = h + ∂ L ∂ w ⊙ ∂ L ∂ w w = w − η ∗ 1 h ∂ L ∂ w h=h+\frac{\partial L}{\partial w} \odot \frac{\partial L}{\partial w}\\ w=w-\eta*\frac{1}{\sqrt{h}}\frac{\partial L}{\partial w} h=h+∂w∂L⊙∂w∂Lw=w−η∗h1∂w∂L
上式的 h h h存放了过去所有梯度值的平方和,最后更新参数时,如果过去更新的梯度平方和越大,则本次学习率越小。
class Adam:def __init__(self, lr=0.1, momentum=0.9):self.lr = lrself.h = Nonedef update(self, params, grads):if self.h is None:self.h = dict()for key, val in params.items():self.h[key] = np.zeros_like(val)for key, val in params.items():self.h[key] += grads[key]*grads[key]params[key] -= self.lr * grads[key] * 1/np.sqrt(self.h[key]+1e-7)
代码里最后一行加了一个 1 e − 7 1e-7 1e−7,这是为了避免分母为0的时候出现报错。
下图为用AdaGrad求解的图示:
可以看到"之"字震荡的问题减弱了很多。这是因为前期学习的时候,y轴方向的梯度较大,那么在学习率衰减的作用下,后面会减小这个更新的步伐,因此,y轴方向上更新的程度被减弱。
Adam
如果结合Momentum的思路和AdaGrad的思路在一起会怎么样呢?Adam就是这样一种方法。我读的这本书没有对它进行展开说明,但有一张图可以显示Adam的效果:
总结与对比
注意,目前不存在在所有问题中都表现最好的方法。
权重初始值的设置
全部设置为0可以吗?
先说结论,不可以。
假设我们有一个2层的神经网络,如果初始权重为0,那么正向传播时,第二层的输入全部为0,通过下图回顾一下乘法节点和加法节点的反向传播,如果第二层正向传播的输入全部是相同的值,那么反向传播时,所有的权重都会被更新为相同的值(下图的 x x x、 y y y都相等,那么反向传播时,值也一样),这使得神经网络拥有许多不同权重的意义丧失了。
因此,不仅是全部设置为0不可以,全部设置为一个相同的非零数也不可以,因为同样会有权重相同的问题。
为了瓦解“权重均一化” / “权重的对称结构”,必须随机生成初始权重。
权重初始值对激活层分布的影响
这里作者做了一个实验,通过改变权重初始值的分布,观察激活层的分布,代码如下:
import numpy as np
import matplotlib.pyplot as pltdef sigmoid(x):return 1 / (1 + np.exp(-x))x = np.random.randn(1000, 100) # 1000个数据
node_num = 100 # 各隐藏层的节点(神经元)数
hidden_layer_size = 5 # 隐藏层有5层
activations = {} # 激活值的结果保存在这里for i in range(hidden_layer_size):if i != 0:x = activations[i-1]w = np.random.randn(node_num, node_num) * 1 # 生成一个随机权重矩阵 w,其维度为 node_num × node_num(即行数和列数都等于 node_num),服从标准正态分布,最后的1控制的是标准差。z = np.dot(x, w)a = sigmoid(z) # sigmoid函数activations[i] = a
然后可视化:
# 绘制直方图
for i, a in activations.items():plt.subplot(1, len(activations), i+1)plt.title(str(i+1) + "-layer")plt.hist(a.flatten(), 30, range=(0,1))
plt.show()
最后得到下图:可以看到这是一个偏向0和1的分布。回想一下sigmoid的图像是S型曲线,随着输出不断靠近0或1,导数不断的趋近于0。因此,偏向0和1的分布会导致反向传播过程中,梯度不断减小,最后消失。这个问题称为梯度消失(gradient vanishing)。
如果我们修改一下权重初始值的分布会怎样呢?将上面的标准差从1改为0.01试试看:
# w = np.random.randn(node_num, node_num) * 1 # 生成一个随机权重矩阵 w,其维度为 node_num × node_num(即行数和列数都等于 node_num),服从标准正态分布,最后的1控制的是标准差。
w = np.random.randn(node_num, node_num) * 0.01
得到下图:现在的分布集中在0.5附近;
- 好消息:因为分布不再集中在0和1附近,所以不会再出现梯度消失的问题了;
- 坏消息:激活值的分布有所偏向,意味着神经网络的表现力会受限。因为如果不同的神经元都输出相近的值,那许多神经元也就没有存在的意义了。比如100个神经元输出的结果都基本相同,那么和使用1个神经元来表达,基本是同样的事情。
为了使各层激活值呈现具有相同广度的分布,Xavier Glorot等人推导了一些比较好用的初始权重的分布,俗称“Xavier初始值”:
node_num = 100 # 前一层的节点数
w = np.random.randn(node_num, node_num) / np.sqrt(node_num)
从公式可以看到,前一层的节点越多,要设定的目标节点的初始权重就越小。
最终分布的表现:
激活函数不同,适配的初始权重分布也不同,作者总结了一下,当激活函数使用ReLU时,权重初始值使用He初始值,当激活函数为sigmoid或tanh等S型曲线函数时,初始值使用Xavier初始值。这是目前的最佳实践。
如何控制激活层的分布
上面介绍了通过调整权重的初始值,可以使得各层激活值的分布拥有适当的广度,从而使得网络有充分的表现力。那么如果不通过权重初始值的调整,而是强制的“控制激活值的分布”会如何呢?Batch normalization就是基于这样的思路诞生的!
batch normalization
如下图所示,batch norm通过对神经网络中插入对数据分布进行正规化的层,称为batch norm层,使得激活值分布具有相当的广度。
具体而言,是在学习时以mini-batch为单位,按mini-batch进行正规化(使数据分布的均值为0,方差为1),数学公式如下:
m m m表示一个mini-batch里的输入数据数量,首先求了mini-batch里输入数据的均值,然后求了方差,最后对每个数字进行了正规化计算。最后一个式子里分母有个 ϵ \epsilon ϵ,是一个极小的值,为了防止分母为0的情况。
还没有结束,接着batch norm会对正规化后的数据进行平移和缩放,
这里的 γ \gamma γ和 β \beta β是参数,一开始 γ = 1 \gamma=1 γ=1, β = 0 \beta=0 β=0,然后再通过学习调整到合适的值。
batch normalization效果测试
最后作者做了一个实验,如下图所示,实现代表加入了batch normalization,虚线表示没有batch normalization。几乎所有的情况下都是使用了batch normalization后学习的更快。同时也观测到,在某些不适用batch normalization的情况下,如果不设置一个合适的权重初始值,学习将无法正常进行。并且,加入batch normalization后,对权重初始值变得健壮(不那么依赖权重初始值)。
如何防止过拟合——正则化登场
第一次看到正则化这个词觉得非常抽象,完全联想不到过拟合,去问了一下GPT,才稍微能理解一点为什么取这个名字了。
正则化是从英语单词"regularization"翻译过来的,有“规则化”的意思,核心思想是通过引入某项规则,使得系统或解变得更加“规范”,更稳定,即防止过度复杂化(过拟合)。姑且这么理解吧~
正则化方法一:权值衰减
很多过拟合的原因是权重参数过大,因此权值衰减的思路应运而生。它的核心思想是,对大的权重进行惩罚,抑制过拟合~
复习一下,神经网络的学习是使得损失函数的值最小,这时,如果给损失函数加上权重的平方范数(L2范数,权值衰减也可称为L2正则化) 1 2 λ w 2 \frac{1}{2}\lambda w^2 21λw2(这里的 1 2 \frac{1}{2} 21是用于将求导结果调整为 λ w \lambda w λw的调整用常量),这样权重越大,惩罚就越大。
λ \lambda λ是用于调整惩罚程度的项,越大则惩罚的越重。
直观看一下测试的效果:
下图是过拟合的典型表现:
下图是加了权值衰减后的变化:可以看到,虽然训练数据和测试数据的识别精度仍然有一定差距,但对比上图,gap已经小了很多,且训练数据的精度不再是100%了,这说明过拟合受到了抑制。
正则化方法二:dropout
原理
当神经网络变得很复杂时,权值衰减抑制过拟合的效果就变得十分有限了,这种情况下一般采用dropout方法。它的核心思想是,在训练时,随机筛选并删除一些神经元,被删除的神经元不再传递信号。
如下图所示:
- 训练时,每传递一次数据,都要随机选择删除的神经元;
- 测试时,虽然会传递所有神经元的信号,但是对于每一个神经元的输出,都要乘以它训练时被删除的概率;
看一下代码实现:正向传播时,如果是做训练,则通过self.mask以False的形式保存要删除的神经元,然后选择性的传输神经元数据;如果是做测试,则所有的神经元数据都传播,但是都会乘以一个被删除的比例;反向传播时,被删除的神经元将会停止传播。
class dropout:def __init__(self, dropout_ratio=0.5):self.dropout_ratio = dropout_ratioself.mask = Nonedef forward(self, x, train_flag=True):if train_falg:self.mask = np.random.rand(*x.shape) > self.dropout_ratioreturn x * self.maskelse:return x * (1-self.dropout_ratio)def backward(self, dout):return dout * self.mask
核心看下面这行代码的逻辑:它以False的形式保存了要删除的神经元。
self.mask = np.random.rand(*x.shape) > self.dropout_ratio
最终看一下实验结果:
可以看到训练精度和测试精度的gap同样变小了,而且训练的精度同样没有达到100%。这说明即使是表现力强的网络(该测试用的是7层网络,每层有100个神经元),也可以被抑制过拟合。
为什么dropout可以抑制过拟合
训练时随机删除神经元的操作,使得每次都在用不同的模型进行学习。而推理时,通过对每个神经元的输出乘以被删除的比例,相当于对不同模型的结果取了平均值。
这与机器学习中集成学习的思想(训练多个学习器,推理时再取多个学习器结果的平均值)不谋而合。
也就是说,dropout将集成学习的效果(模拟地)通过一个网络实现了。
超参数的验证
神经网络中,除了权重、偏置这类希望模型自动学习的参数外,剩下的就是超参数,是需要我们人工指定的,比如各层神经元的数量、batch大小、更新时的学习率、权值衰减系数、dropout rate等。
这一小节作者介绍了高效寻找超参数的方法,我先提炼2个核心注意的点:
1. 验证超参数不能直接在训练数据上验证,而是要在单独的验证数据上测试;
2. 超参数寻优时,与网格搜索这类有规律的搜索相比,随机采样的方式往往更好。
具体寻优的步骤:
- 设定超参数的寻优范围;
- 从设定的超参数范围中,随机采样;
- 使用上面的到的超参数进行学习,在验证数据上评估识别精度;
- 通过识别精度的结果,缩小测试范围。
这里第2步为了节省计算资源,提升搜索效率,通常设置较小的epoch(1个epoch = 模型完整的看完一个训练集,通常一个模型要训练几十个甚至上百个epoch,才能收敛至较好的性能)。