文章目录
优化与深度学习
在一个深度学习问题中,我们通常会预先定义一个损失函数。有了损失函数以后,我们就可以使用优化算法试图将其最小化。在优化中,这样的损失函数通常被称作优化问题的目标函数(objective function)。依据惯例,优化算法通常只考虑最小化目标函数。其实,任何最大化问题都可以很容易地转化为最小化问题,只需令目标函数的相反数为新的目标函数即可。
算法原理
1、梯度下降
目标函数的输入为向量,输出为标量。假设目标函数 的输入是一个 维向量 。目标函数 有关 的梯度是一个由 个偏导数组成的向量:
为表示简洁,我们用 代替 。梯度中每个偏导数元素 代表着 在 有关输入 的变化率。为了测量 沿着单位向量 (即 )方向上的变化率,在多元微积分中,我们定义 在 上沿着 方向的方向导数为
依据方向导数性质,以上方向导数可以改写为
方向导数 给出了 在 上沿着所有可能方向的变化率。为了最小化 ,我们希望找到 能被降低最快的方向。因此,我们可以通过单位向量 来最小化方向导数 。
由于 , 其中 为梯度 和单位向量 之间的夹角,当 时, 取得最小值 。因此,当 在梯度方向 的相反方向时,方向导数 被最小化。因此,我们可能通过梯度下降算法来不断降低目标函数 的值:
同样,其中 (取正数)称作学习率。
随机梯度下降
在深度学习里,目标函数通常是训练数据集中有关各个样本的损失函数的平均。设 是有关索引为 的训练数据样本的损失函数, 是训练数据样本数, 是模型的参数向量,那么目标函数定义为
目标函数在 处的梯度计算为
如果使用梯度下降,每次自变量迭代的计算开销为 ,它随着 线性增长。因此,当训练数据样本数很大时,梯度下降每次迭代的计算开销很高。
随机梯度下降(stochastic gradient descent,SGD)减少了每次迭代的计算开销。在随机梯度下降的每次迭代中,我们随机均匀采样的一个样本索引 ,并计算梯度 来迭代 :
这里 同样是学习率。可以看到每次迭代的计算开销从梯度下降的 降到了常数 。值得强调的是,随机梯度 是对梯度 的无偏估计:
这意味着,平均来说,随机梯度是对梯度的一个良好的估计。
小批量随机梯度下降
在每一次迭代中,梯度下降使用整个训练数据集来计算梯度;而随机梯度下降在每次迭代中只随机采样一个样本来计算梯度。在深度学习的训练过程中,我们往往在每轮迭代中随机均匀采样多个样本来组成一个小批量,然后使用这个小批量来计算梯度,这时则需要应用小批量随机梯度下降来优化损失函数。
设目标函数 。在迭代开始前的时间步设为0。该时间步的自变量记为 ,通常由随机初始化得到。在接下来的每一个时间步 中,小批量随机梯度下降随机均匀采样一个由训练数据样本索引组成的小批量 。我们可以通过重复采样(sampling with replacement)或者不重复采样(sampling without replacement)得到一个小批量中的各个样本。前者允许同一个小批量中出现重复的样本,后者则不允许如此,且更常见。对于这两者间的任一种方式,都可以使用
来计算时间步 的小批量 上目标函数位于 处的梯度 。这里 代表批量大小,即小批量中样本的个数,是一个超参数。同随机梯度一样,重复采样所得的小批量随机梯度 也是对梯度 的无偏估计。给定学习率 (取正数),小批量随机梯度下降对自变量的迭代如下:
基于随机采样得到的梯度的方差在迭代过程中无法减小,因此在实际中,(小批量)随机梯度下降的学习率可以在迭代过程中自我衰减,例如 (通常 或者 )、 (如 )或者每迭代若干次后将学习率衰减一次。如此一来,学习率和(小批量)随机梯度乘积的方差会减小。而梯度下降在迭代过程中一直使用目标函数的真实梯度,无须自我衰减学习率。
小批量随机梯度下降中每次迭代的计算开销为 。当批量大小为1时,该算法即为随机梯度下降;当批量大小等于训练数据样本数时,该算法即为梯度下降。当批量较小时,每次迭代中使用的样本少,这会导致并行处理和内存使用效率变低。这使得在计算同样数目样本的情况下比使用更大批量时所花时间更多。当批量较大时,每个小批量梯度里可能含有更多的冗余信息。为了得到较好的解,批量较大时比批量较小时需要计算的样本数目可能更多,例如增大迭代周期数。
2、动量法
梯度下降的问题
目标函数有关自变量的梯度代表了目标函数在自变量当前位置下降最快的方向,因此,梯度下降也叫作最陡下降(steepest descent)。在每次迭代中,梯度下降根据自变量当前位置,沿着当前位置的梯度更新自变量。然而,如果自变量的迭代方向仅仅取决于自变量当前位置,这可能会带来一些问题,如下图的迭代轨迹所示。
可以看到,同一位置上,目标函数在竖直方向(
轴方向)比在水平方向(
轴方向)的斜率的绝对值更大。因此,给定学习率,梯度下降迭代自变量时会使自变量在竖直方向比在水平方向移动幅度更大。那么,我们需要一个较小的学习率从而避免自变量在竖直方向上越过目标函数最优解。然而,这会造成自变量在水平方向上朝最优解移动变慢。
如果将学习率调得稍大一点,那么此时自变量在竖直方向不断越过最优解并逐渐发散,如下图所示。
指数加权移动平均
为了从数学上理解动量法,让我们先解释一下指数加权移动平均(exponentially weighted moving average)。给定超参数 ,当前时间步 的变量 是上一时间步 的变量 和当前时间步另一变量 的线性组合:
我们可以对 展开:
令
,那么
。
因为
所以当 时, ,如 。如果把 当作一个比较小的数,我们可以在近似中忽略所有含 和比 更高阶的系数的项。例如,当 时,
因此,在实际中,我们常常将 看作是对最近 个时间步的 值的加权平均。例如,当 时, 可以被看作对最近20个时间步的 值的加权平均;当 时, 可以看作是对最近10个时间步的 值的加权平均。而且,离当前时间步 越近的 值获得的权重越大(越接近1)。
由指数加权移动平均理解动量法
现在,我们对动量法的速度变量做变形:
由指数加权移动平均的形式可得,速度变量 实际上对序列 做了指数加权移动平均。换句话说,相比于小批量随机梯度下降,动量法在每个时间步的自变量更新量近似于将最近 个时间步的普通更新量(即学习率乘以梯度)做了指数加权移动平均后再除以 。所以,在动量法中,自变量在各个方向上的移动幅度不仅取决当前梯度,还取决于过去的各个梯度在各个方向上是否一致。
更新参数:
使用动量法优化参数之后,迭代轨迹变为:
可以看到使用较小的学习率和动量超参数时,动量法在竖直方向上的移动更加平滑,且在水平方向上更快逼近最优解。如果使用较大的学习率,此时自变量也不再发散:
3、AdaGrad 算法
在之前介绍过的优化算法中,目标函数自变量的每一个元素在相同时间步都使用同一个学习率来自我迭代。举个例子,假设目标函数为 ,自变量为一个二维向量 ,该向量中每一个元素在迭代时都使用相同的学习率。例如,在学习率为 的梯度下降中,元素 和 都使用相同的学习率 来自我迭代:
在动量法里,我们看到当 和 的梯度值有较大差别时,需要选择足够小的学习率使得自变量在梯度值较大的维度上不发散。但这样会导致自变量在梯度值较小的维度上迭代过慢。动量法依赖指数加权移动平均使得自变量的更新方向更加一致,从而降低发散的可能。本节我们介绍 AdaGrad 算法,它根据自变量在每个维度的梯度值的大小来调整各个维度上的学习率,从而避免统一的学习率难以适应所有维度的问题。
AdaGrad 算法会使用一个小批量随机梯度 的(按元素)平方来累加变量 。在时间步0,AdaGrad 将 中每个元素初始化为0。在时间步 ,首先将小批量随机梯度 按元素平方后累加到变量 :
其中 是按元素相乘。接着,我们将目标函数自变量中每个元素的学习率通过按元素运算重新调整一下:
其中
是学习率,
是为了防止分母为0而添加的常数,如
。这里开方、除法和乘法的运算都是按元素运算的。这些按元素运算使得目标函数自变量中每个元素都分别拥有自己的学习率。
可以看到,自变量的迭代轨迹较平滑。但由于
的累加效果使学习率不断衰减,自变量在迭代后期的移动幅度较小。
通过增大学习率,可以看到自变量更为迅速地逼近了最优解:
需要强调的是,小批量随机梯度按元素平方的累加变量
出现在学习率的分母项中。因此,如果目标函数有关自变量中某个元素的偏导数一直都较大,那么该元素的学习率将下降较快;反之,如果目标函数有关自变量中某个元素的偏导数一直都较小,那么该元素的学习率将下降较慢。然而,由于
一直在累加按元素平方的梯度,自变量中每个元素的学习率在迭代过程中一直在降低(或不变)。所以,当学习率在迭代早期降得较快且当前解依然不佳时,AdaGrad 算法在迭代后期由于学习率过小,可能较难找到一个有用的解。
4、RMSProp 算法
我们在 AdaGrad 算法中提到,因为调整学习率时分母上的变量 一直在累加按元素平方的小批量随机梯度,所以目标函数自变量每个元素的学习率在迭代过程中一直在降低(或不变)。因此,当学习率在迭代早期降得较快且当前解依然不佳时,AdaGrad 算法在迭代后期由于学习率过小,可能较难找到一个有用的解。为了解决这一问题,RMSProp 算法对 AdaGrad 算法做了一点小小的修改。
我们在动量法里介绍过指数加权移动平均。不同于 AdaGrad 算法里的状态变量 (截至时间步 内的所有小批量随机梯度 按元素平方和),RMSProp 算法将这些梯度按元素平方做指数加权移动平均。具体来说,给定超参数 ,RMSProp 算法在时间步 计算
和 AdaGrad 算法一样,RMSProp 算法将目标函数自变量中每个元素的学习率通过按元素运算重新调整,然后更新自变量
其中
是学习率,
是为了维持数值稳定性而添加的常数,如
。因为 RMSProp 算法的状态变量
是对平方项
的指数加权移动平均,所以可以看作是最近
个时间步的小批量随机梯度平方项的加权平均。如此一来,自变量每个元素的学习率在迭代过程中就不再一直降低(或不变)。
5、AdaDelta 算法
除了 RMSProp 算法以外,另一个常用优化算法 AdaDelta 算法也针对 AdaGrad 算法在迭代后期可能较难找到有用解的问题做了改进。有意思的是,AdaDelta 算法没有学习率这一超参数。
AdaDelta 算法也像 RMSProp 算法一样,使用了小批量随机梯度 按元素平方的指数加权移动平均变量 。在时间步0,它的所有元素被初始化为0。给定超参数 (对应 RMSProp 算法中的 ),在时间步 ,同 RMSProp 算法一样计算:
与 RMSProp 算法不同的是,AdaDelta 算法还维护一个额外的状态变量 ,其元素同样在时间步0时被初始化为0。我们使用 来计算自变量的变化量:
其中 是为了维持数值稳定性而添加的常数,如 。接着更新自变量:
最后,我们使用 来记录自变量变化量 按元素平方的指数加权移动平均:
可以看到,如不考虑 的影响,AdaDelta 算法跟 RMSProp 算法的不同之处在于使用 来替代学习率 。
6、Adam 算法
Adam 算法在 RMSProp 算法基础上对小批量随机梯度也做了指数加权移动平均,所以 Adam 算法可以看做是 RMSProp 算法与动量法的结合。
Adam 算法使用了动量变量 和 RMSProp 算法中小批量随机梯度按元素平方的指数加权移动平均变量 ,并在时间步0将它们中每个元素初始化为0。给定超参数 (建议设为0.9),时间步 的动量变量 即小批量随机梯度 的指数加权移动平均:
和 RMSProp 算法中一样,给定超参数 (建议设为0.999), 将小批量随机梯度按元素平方后的项 做指数加权移动平均得到 :
由于我们将 和 中的元素都初始化为0, 在时间步 我们得到 。将过去各时间步小批量随机梯度的权值相加,得到 。需要注意的是,当 较小时,过去各时间步小批量随机梯度权值之和会较小。例如,当 时, 。为了消除这样的影响,对于任意时间步 ,我们可以将 再除以 ,从而使过去各时间步小批量随机梯度权值之和为1。这也叫作偏差修正。在 Adam 算法中,我们对变量 和 均作偏差修正:
接下来,Adam 算法使用以上偏差修正后的变量 和 ,将模型参数中每个元素的学习率通过按元素运算重新调整:
其中 是学习率, 是为了维持数值稳定性而添加的常数,如 。和 AdaGrad 算法、RMSProp 算法以及 AdaDelta 算法一样,目标函数自变量中每个元素都分别拥有自己的学习率。最后,使用 迭代自变量:
低级API实现优化函数定义
这里我们将使用一个来自NASA的测试不同飞机机翼噪音的数据集来比较各个优化算法。我们使用该数据集的前1,500个样本和5个特征,并使用标准化对数据进行预处理。
导入库和数据集
import numpy as np
import time
import tensorflow as tf
from tensorflow import keras
import matplotlib.pyplot as plt
%matplotlib inline
data = np.genfromtxt('./airfoil_self_noise.dat', delimiter='\t')
data = (data - data.mean(axis=0)) / data.std(axis=0)
features = tf.convert_to_tensor(data[:1500, :-1],dtype=tf.float32)
labels = tf.convert_to_tensor(data[:1500, -1],dtype=tf.float32)
定义各种优化方法
# 梯度下降
def sgd(params, states, hyperparams, grads):
for i,p in enumerate(params):
p.assign_sub(hyperparams['lr'] * grads[i])
# 动量法
def init_momentum_states():
v_w = tf.Variable(tf.zeros((features.shape[1],1)))
v_b = tf.Variable(tf.zeros(1))
return (v_w, v_b)
def sgd_momentum(params, states, hyperparams,grads):
i=0
for p,v in zip(params, states):
v.assign(hyperparams['momentum'] * v + hyperparams['lr'] * grads[i])
p.assign_sub(v)
i+=1
# AdaGrad
def init_adagrad_states():
s_w = tf.Variable(tf.zeros((features.shape[1],1),dtype=tf.float32))
s_b = tf.Variable(tf.zeros(1,dtype=tf.float32))
return (s_w, s_b)
def adagrad(params, states, hyperparams, grads):
eps = 1e-6
i=0
for p, s in zip(params, states):
s.assign_add(grads[i]**2)
p.assign_sub(hyperparams['lr']*grads[i]/tf.sqrt(s+eps))
i+=1
# RMSProp
def init_rmsprop_states():
s_w = tf.Variable(tf.zeros((features.shape[1],1),dtype=tf.float32))
s_b = tf.Variable(tf.zeros(1,dtype=tf.float32))
return (s_w, s_b)
def rmsprop(params, states, hyperparams, grads):
gamma = hyperparams['gamma']
eps = 1e-6
i = 0
for p, s in zip(params, states):
s.assign(gamma*s + (1-gamma)*(grads[i]**2))
p.assign_sub(hyperparams['lr']*grads[i]/tf.sqrt(s+eps))
i+=1
# AdaDelta
def init_adadelta_states():
s_w = tf.Variable(tf.zeros((features.shape[1],1),dtype=tf.float32))
s_b = tf.Variable(tf.zeros(1,dtype=tf.float32))
delta_w = tf.Variable(tf.zeros((features.shape[1],1),dtype=tf.float32))
delta_b = tf.Variable(tf.zeros(1,dtype=tf.float32))
return ((s_w, delta_w), (s_b, delta_b))
def adadelta(params, states, hyperparams, grads):
rho = hyperparams['rho']
eps = 1e-5
i = 0
for p, (s, delta) in zip(params, states):
s.assign(rho*s + (1-rho)*(grads[i]**2))
g = grads[i] * tf.sqrt((delta+eps)/(s+eps))
p.assign_sub(g)
delta.assign(rho * delta + (1-rho) * (g**2))
i+=1
# Adam
def init_adam_states():
v_w, v_b = tf.Variable(tf.zeros((features.shape[1],1),dtype=tf.float32)), tf.Variable(tf.zeros(1,dtype=tf.float32))
s_w, s_b = tf.Variable(tf.zeros((features.shape[1],1),dtype=tf.float32)), tf.Variable(tf.zeros(1,dtype=tf.float32))
return ((v_w, s_w), (v_b, s_b))
def adam(params, states, hyperparams, grads):
beta1, beta2, eps, i = 0.9, 0.999, 1e-6, 0
for p, (v, s) in zip(params, states):
v.assign(beta1 * v + (1 - beta1) * grads[i])
s.assign(beta2 * s + (1 - beta2) * grads[i]**2)
v_bias_corr = v / (1 - beta1 ** (i+1))
s_bias_corr = s / (1 - beta2 ** (i+1))
p.assign_sub(hyperparams['lr']*v_bias_corr/(np.sqrt(s_bias_corr) + eps))
i+=1
定义损失函数
def loss(y_hat, y):
return tf.reduce_mean((y_hat - tf.reshape(y, y_hat.shape)) ** 2 / 2)
定义训练函数
def train(optimizer_fn, states, hyperparams, features, labels,
batch_size=10, num_epochs=2):
# 初始化模型
w = tf.Variable(np.random.normal(0, 0.01, size=(features.shape[1], 1)), dtype=tf.float32)
b = tf.Variable(tf.zeros(1,dtype=tf.float32))
ls = []
data_iter = tf.data.Dataset.from_tensor_slices((features,labels)).batch(batch_size)
data_iter = data_iter.shuffle(100)
for _ in range(num_epochs):
start = time.time()
for batch_i, (X, y) in enumerate(data_iter):
with tf.GradientTape() as tape:
pred = tf.matmul(X, w) + b
l = loss(pred, y) # 使用平均损失
grads = tape.gradient(l, [w,b])
optimizer_fn([w, b], states, hyperparams, grads) # 迭代模型参数
if (batch_i + 1) * batch_size % 100 == 0:
ls.append(loss(tf.matmul(features, w) + b, labels)) # 每100个样本记录下当前训练误差
# 打印结果和作图
print('loss: %f, %f sec per epoch' % (ls[-1], time.time() - start))
plt.figure()
plt.rcParams['figure.figsize'] = (3.5, 2.5)
plt.plot(np.linspace(0, num_epochs, len(ls)), ls)
plt.xlabel('epoch')
plt.ylabel('loss')
训练
1、梯度下降
def train_sgd(lr, batch_size, num_epochs=2):
train(sgd, None, {'lr': lr}, features, labels, batch_size, num_epochs)
train_sgd(1, 1500, 6) # 梯度下降
train_sgd(0.1, 200) # 小批量随机梯度下降
train_sgd(0.005, 1) # 随机梯度下降
loss: 0.246635, 0.007980 sec per epoch
loss: 0.271517, 0.023935 sec per epoch
loss: 0.252278, 3.953179 sec per epoch
2、动量法
def train_momentum(lr, momentum, batch_size, num_epochs=2):
train(sgd_momentum, init_momentum_states(),
{'lr': lr, 'momentum': momentum}, features, labels, batch_size, num_epochs)
train_momentum(lr=0.02, momentum=0.5, batch_size=10, num_epochs=2)
loss: 0.256722, 1.106707 sec per epoch
3、AdaGrad 算法
def train_adagrad(lr, batch_size, num_epochs=2):
train(adagrad, init_adagrad_states(), {'lr': lr}, features, labels, batch_size, num_epochs)
train_adagrad(lr=0.1, batch_size=10, num_epochs=2)
loss: 0.245070, 4.161112 sec per epoch
4、RMSProp 算法
def train_rmsprop(lr, gamma, batch_size, num_epochs=2):
train(rmsprop, init_rmsprop_states(), {'lr': lr, 'gamma': gamma},
features, labels, batch_size, num_epochs)
train_rmsprop(lr=0.01, gamma=0.9, batch_size=10, num_epochs=2)
loss: 0.246331, 1.370769 sec per epoch
5、AdaDelta 算法
def train_adadelta(rho, batch_size, num_epochs=2):
train(adadelta, init_adadelta_states(), {'rho': rho},
features, labels, batch_size, num_epochs)
train_adadelta(rho=0.9, batch_size=10, num_epochs=2)
loss: 0.250182, 1.592784 sec per epoch
6、Adam 算法
def train_adam(lr, batch_size, num_epochs=2):
train(adam, init_adam_states(), {'lr': lr}, features,labels, batch_size, num_epochs)
train_adam(lr=0.01, batch_size=10, num_epochs=2)
loss: 0.242108, 3.648249 sec per epoch
Tensorflow2.0实现优化
1、梯度下降
trainer = tf.keras.optimizers.SGD(learning_rate=0.05)
train(trainer, {"lr": 0.05}, features, labels, 10)
2、动量法
trainer = tf.keras.optimizers.SGD(learning_rate=0.004,momentum=0.9)
train(trainer, {'lr': 0.004, 'momentum': 0.9}, features, labels)
3、AdaGrad 算法
trainer = tf.keras.optimizers.Adagrad(learning_rate=0.01)
train(trainer, {'lr': 0.01}, features, labels)
4、RMSProp 算法
trainer = tf.keras.optimizers.RMSprop(learning_rate=0.01,rho=0.9)
train(trainer, {'lr': 0.01}, features, labels)
5、AdaDelta 算法
trainer = tf.keras.optimizers.Adadelta(learning_rate=0.01, rho=0.9)
train(trainer, {'rho': 0.9}, features, labels)
6、Adam 算法
trainer = tf.keras.optimizers.Adam(learning_rate=0.01)
train(trainer, {'learning_rate': 0.01}, features, labels)