第二部分 优化篇
文章目录
第5章 基于梯度下降法的最优化方法
梯度下降法的基本思想:如果要找到某函数的最小值,最好的方法是沿着负梯度方向探寻。假设有一个参数向量 x x x 及其梯度 d x dx dx,更新形式是:
x = x + ( − l r × d x ) x \ =x + (-lr \times d_x) x =x+(−lr×dx)
其中 l r lr lr 称为学习率,是超参数,是正的小常量。 在整个数据集上计算梯度进行参数更新时,只要学习率足够小,每次更新参数时总能使损失函数值减小 。参数初始化相关的内容可参见 5.9 节,梯度计算方法见第6章。
5.1 随机梯度下降法SGD
参数更新中的梯度是通过对训练集中所有样本的平均损失求梯度得到的。 当训练集规模较小时,计算梯度不成问题。 然而对于大规模数据集(比如 IrnageNet ),当训练样本达到百万量级时,需要计算整个训练集的梯度,才能更新一次参数 这样更新效率太低,浪费计算资源。 一个常用的代替方法是从训练集中随机抽取小批量样本,计算它们平均损失的梯度,来实现一次参数更新。 小批量样本可包含 64个或 128 个样本,而整个训练集有 120 万个样本。
用小批量样本的平均损失代替全体样本的平均损失进行参数更新,可以加快参数更新频率,加速收敛。 小批量样本的平均损失是全体样本平均损失的无偏估计。 这是因为训练集中的同类样本是相关的,同类样本中不同个体的损失是相似的,所以随机抽取的一个样本损失可以作为该类所有样本损失的估计。
如果小批量样本中只有一个样本,那么称为随机梯度下降法(Stochastic Gradient Descent, SGD )。SGD 使用不广,因为矩阵的向量化操作使一次计算 128 个样本的梯度比 128 次计算一个样本的梯度要高效很多。 SGD 指每次使用一个样本来更新参数,但我们经常使用SGD 来指代小批量梯度下降法。 小批量样本的数量是一个超参数,它受存储器的存储容量限制,一般设置为 32、 64、 128 等 2 的指数,是因为许多矩阵向量化操作实现时,如果数据量是 2 的指数,运算效率会更高。SGD 及其改进算法是深度学习中最常用的优化算法,本章主要介绍这些算法。
SGD 算法中, 每次都要随机抽取batch 个样本, 实现时可以采用先整体打乱训练集,然后每次按顺序取batch 个样本的方式。这种实现方法效率高,代码如下:
import numpy as np
def calu_gradient(batch_data, labels, x):
return np.random.randn(np.size(x))
num_train_samples = 1000
im_height = 32
im_width = 32
im_dims = 3
num_class = 10
batch = 20
learning_rate = 10**(-4)
dim_x = 1000
x = np.random.randn(dim_x)
train_data = np.random.randn(num_train_samples, im_height, im_width, im_dims)
train_labels = np.random.randint(num_class, size=num_train_samples)
epoch_num = 20
for epoch in range(epoch_num):
shuffle_no = list(range(num_train_samples))
np.random.shuffle(shuffle_no) # 1
train_labels = train_labels[shuffle_no] # 2
train_data = train_data[shuffle_no]
for i in range(0, num_train_samples, batch): # epoch
batch_data = train_data[i:i+batch,:] # 3
labels = train_labels[i:i+batch]
dx = calu_gradient(batch_data, labels, x) # 4
x += -learning_rate*dx # 5
首先定义梯度计算函数,这里采用随机数模拟梯度,具体计算见第6 章。定义一些相关变量和学习率,并随机生成训练集。语句①获得随机打乱的整数序列,语句②整体打乱训练集,语句③顺序取出batch 个样本,语句④计算梯度,语句⑤更新参数。所有训练样本都训练一次,称为一个周期( epoch )。注意,每个训练周期开始时,必须重新打乱训练集。
基于SGD 的改进算法,只有语句⑤的参数更新不同,前面程序都是一样的,故后面只给出更新参数的程序段。
5.2 基本动量法
把梯度下降法想象成小球从山坡滚向山谷的过程:
- 损失值是小球当前的高度
- 最小化损失值就是希望小球该动到高度最小的山谷
设小球的空间位置是 x x x,每次移动的路径矢量是 d x dx dx,位置 x x x 处的山的高度就是损失值。那么**基本梯度下降法的小球是这样“滚动”**的:(在每个点沿着当前梯度最陡的方向滚动),详细描述:
- 在出发点 A 处,计算点 A 在各个方向的坡度,沿着坡度最陡的方向移动一段距离到达 B 点, 然后停下来。在 B 点再计算坡度最陡的方向,沿着这个方向走一段路,再停下。注意每到一个新位置, 小球都必须停下来。
- 准确地说, 小球并没有滚动下山,而是盲人下山的方式,走一步停一步,用拐杖探明坡度最陡的方向,沿此方向移动一步后停下来,周而复始,直到到达山谷或平地,此时各个方向上的坡度都为0 ,盲
人无法移动了。
一个真正的小球要比盲人高效得多,从起始点 A 静止滚动到点 B 的时候,小球获得一定速度,继续滚动, 小球会越滚越快,快速滚向谷底。动量法就是通过模拟小球的滚动过程来加速神经网络的收敛。这时就需要一个速度变量, 根据速度变量来更新参数。速度变量积累了历史梯度信息,使之具有惯性,当梯度方向一致时,加速收敛;当梯度方向不一致时,减小路径曲折程度。代码如下:
mu = 0.9
v = mu*v # 1
v += - lr*dx # 2
x += v
- 假设每个时刻的梯度 d x dx dx 相等,由语句①和语句②可得: v = − l r / ( 1 m u ) × d x v=-lr/(1 mu)× dx v=−lr/(1mu)×dx ,此时相当于学习率为 l r / ( 1 − m u ) lr/(1 - mu) lr/(1−mu), $ mu= 0.9$ 表示 10 10 10 倍于 SGD 算法的收敛速度。把 1 / ( 1 − m u ) 1 /(1- mu) 1/(1−mu) 看作放大率更容易理解, m u mu mu 越大, 放大率越大,收敛可能越快。
- 假设每个时刻的梯度 d x dx dx 总是 0 0 0 (相当于小球滚动到平地),由语句①可得: v = m u n × v 0 v= mu^n × v_0 v=mun×v0 ,其中 n n n 是参数更新次数。可见, v v v 是指数衰减,小球只要滚动到平地时的初速度 v 0 v_0 v0 足够大,就有机会冲出当前平地,到达一个更低的山谷;由于历史原因,速度 ν ν ν 被称为动量,所以该方法称为动量法。
5.3 Nesterov动量法
基本动量法每步的前进方向都是由下降方向的历史累积 v v v 和当前点的梯度方向 d x dx dx 合成的。换个角度看,可以认为下降方向本来应该为 v v v (语句②),但需要考虑当前位置处的实际情况来调整 v v v ,调整量就是当前梯度 d x dx dx (语句③)。
上述方法是由当前信息调整 v v v ,我们能不能通过 ν ν ν 的历史信息进行加速呢?
由于每次更新时, v v v 也会发生改变, d v = v − v p r e dv = v-v_{pre} dv=v−vpre ,即用前后两次之差 d v dv dv 来调整 x x x ,达到加速收敛,其中 v p r e v_{pre} vpre 是上次的速度(参见语句①)。这种方法被命名为 Nesterov Accelerated Gradient ,简称NAG 。其代码如下:
#nesterov_momentumGD
mu = 0.9
pre_v = v
v = mu*v # 1
v += - lr*dx # 2
x += v + mu*(v - pre_v) # 3
5.4 AdaGrad
5.5 RMSProp
5.6 Adam
5.7 AmsGrad
5.8 学习率退火
训练深度网络的时候,学习率如果一直固定不变的话,损失函数就难以达到深谷。通常随着训练次数的增加,学习率会逐渐变小,这就是学习率退火。
以小球滚动为例,学习率很高时,小球的动能很大,小球会无规律地四处滚动,难以滚入损失函数更深、更窄的山谷里面。学习率很小时,贝。小球动能太小,滚动过慢。所以刚开始的时候,学习率应该取比较大的值然后逐渐减小。速率减小得不能过慢,也不能过快。通常,学习率退火有如下3 种方式:
-
随训练周期衰减:每进行几个周期( epoch ,所有样本都训练了一次称为一个周期),降低一次学习率。典型的做法是每 5 个周期学习率减少一半, 或者每 20 个周期减少到0.1,这些数值都是经验值。在实践中更为常见的做法是:每当验证集错误率停止下降时,就把学习率降低到原来的 1 / 10 1/10 1/10 。
-
指数衰减: 数学公式是 α = α 0 e − k t α = α_0e^{-kt} α=α0e−kt ,其中 α 0 α_0 α0 和 k k k 是超参数, t t t 是迭代次数。
-
反比衰减: 数学公式是 α = α 0 / ( 1 + k t ) α = α_0/(1+kt) α=α0/(1+kt) , 其中 α 0 α_0 α0和 k k k 是超参数, t t t 是法代次数。
实践中,最常用的退火方法是随训练周期衰减,因为其超参数(衰减比例和训练周期)具有更好的实际意义。最后, 如果有足够的计算资源,可以让衰减更慢些,这样训练时间会更长些。
5.9 参数初始化
开始训练网络之前,需要初始化网络的权重。由于此时对任务没有先验知识,所以不可能知道某个属性对分类是正相关还是负相关,所以权重初始化为 0 比较合理。但是,全部权重都初始化为 0 是错误的,那样会使网络中同一层的每个神经元都计算出同样的输出 0 ,然后它们就会在反向传播中计算出同样的梯度,从而进行同样的参数更新,这样神经元之间就失去了不对称性的源头。
-
小随机数初始化: 权重初始值接近。但不能等于 0 0 0 ,是比较合理的。采用小的正态分布的随机数 w = 0.01 × n p . r a n d o m . r a n d n ( D , H ) w = 0.01 × np.random.randn(D,H) w=0.01×np.random.randn(D,H) 的来打破对称性。但并不是小数值一定会得到好的结果,这样会减慢收敛速度。因为在梯度反向传播的时候, 会计算出非常小的梯度。
使用 1 / s q r t ( n ) 1 / sqrt(n) 1/sqrt(n) 来校准方差,使输出神经元的方差为 1 1 1 ,这样参数初始化为 w = n p . r a n d o m . r a n d n ( n ) / s q r t ( n ) w = np.random.randn(n) / sqrt(n) w=np.random.randn(n)/sqrt(n),其中 n n n 是神经元连接的输入神经元数量。实践证明,这可以提高收敛速度。 -
偏置初始化:通常将偏置初始化为0 或者小常数( 0.01 ),这是因为随机权重矩阵已经打破了网络对称性。
当前推荐的初始化为: 当使用 ReLU 时,用 w = n p . r a n d o m . r a d n ( n ) × s q r t ( 2.0 / n ) w = np.random.radn(n) × sqrt(2.0 /n) w=np.random.radn(n)×sqrt(2.0/n) 来进行权重初始化。代码如下:
import numpy as np
in_depth = 128
out_depth = 32
std = np.sqrt(2/in_depth)
weights = std * np.random.randn(in_depth, out_depth)
bias = np.zeros((1, out_depth))
5.10 超参数调优
机器学习的各种模型,包括线性模型、常规神经网络和卷积网络等, 以及各种优化算法(如梯度下降法),都会引人很多超参数。贯穿本书的最重要的超参数有两个一一初始学习率和正则化系数( L2 惩罚),还有很多值比较稳定的超参数, 比如动量法中的动量参数mu (一般取0.9 ) 等。如何确定最优的超参数? 超参数不能通过学习算法进行优化, 是人为指定的,所以为了获得最优的超参数, 需要不断尝试不同取值,即在一定范围内搜索最优值。下面是一些注意事项。
首先,要设置合理的超参数范围,其学习率和正则化系数要在对数尺度上进行搜索。例如, 一个典型的学习率搜索范围是: l r = 10 ∗ ∗ u n i f o r m ( − 6 , 1 ) lr = 10 ** uniform(-6, 1 ) lr=10∗∗uniform(−6,1) 。这是因为通过学习率乘以梯度、正则化系数乘以权重,来对模型产生影响。但是有一些参数( dropout )还是在原始尺度上进行搜索( d r o p o u t = u n i f o r m ( 0 , 1 ) dropout = uniform( 0 , 1) dropout=uniform(0,1) )。
其次,随机搜索优于网格搜索,它可以更精确地发现那些比较重要的好的超参数数值,而且程序也更容易实现。超参数随机搜索的代码如下:
import numpy as np
lr=[0, -6] # 1
reg=[-3, -6] # 2
num_try=10 # 3
minlr = min(lr)
maxlr = max(lr)
randn = np.random.rand(num_try*2) # 4
lr_array = 10**(minlr + (maxlr - minlr)*randn[0: num_try]) # 5
minreg = min(reg)
maxreg = max(reg)
reg_array = 10**(minreg + (maxreg - minreg)*randn[num_try: 2*num_try])
lr_regs = zip(lr_array, reg_array) # 6
for lr_reg in lr_regs: # 7
# train()
pass
- 语句①定义学习率对数搜索范围
- 语句②定义正则化系数对数搜索范围
- 语句③定义随机搜索次数
- 语句④生成[0,1 ]均匀随机数
- 语句⑤生成对数尺度的学习率数组
- 语句⑥对每一个超参数组合进行训练
为了快速找到最优超参数,要先粗后精地分阶段搜索。实践中,
先进行粗略范围(比如
10
∗
∗
u
n
i
f
o
r
m
(
−
6
,
1
)
10 ** uniform(-6, 1 )
10∗∗uniform(−6,1) )搜索,然后根据好结果出现的地方缩小搜索范围。进行粗搜索的时候,模型训练一个周期就可以了。因为如果超参数设定不合理,模型会无法学习,或者损失值会突然增大很多。在精细搜索阶段,要缩小搜索范围,模型需要运行多个周期,比如 5 个。最后,在最终的范围内进行仔细搜索,运行更多个周期,比如 20 个。
特别要注意边界上的最优值,一旦最优值位于边界上,则需重新设定包含该最优值的搜索范围,再次进行精细搜索。
由于超参数调优需要大量的计算资源,可能需要几天甚至几周、几个月,所以实践中只使用一个验证集,不进行交叉验证。同时需要监控每个训练周期后验证集的准确率和存储模型的记录点(记录点中应包含各种模型性能统计数据,比如损失值随训练周期的变化和验证集的准确率等),所以文件名中最好包含验证集的性能参数,方便后期查找和排序。