神经网络学习参数和搜索最优超参数的过程
梯度检查
- 简单的把解析梯度和数值计算梯度进行比较,但实际上操作很复杂
在使用有限差值近似来计算数值梯度的时候,常见的公式是:
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)
在实践中证明,使用中心化公式效果更好:
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)- 使用双精度:单精度会浮点导致即使梯度实现正确,相对误差值也
会很高 - 注意Loss函数的不可导点
- 注意步长
- 注意正则化项
- 使用双精度:单精度会浮点导致即使梯度实现正确,相对误差值也
合理性检查
- 寻找特定情况的正确损失值 :在使用小参数进行初始化时,确保得到的损失值与期望一致。例如,对于一个跑CIFAR-10的Softmax分类器,一般期望它的初始损失值是2.302,这是因为初始时预计每个类别的概率是0.1(10个类别)如果没看到这样的损失值,那么初始化中就可能有问题。
- 提高正则化强度时导致损失值变大
检查学习过程
Model | datasets |
---|---|
“sequential” | mnist |
Layer (type) | Output Shape | Param |
---|---|---|
flatten (Flatten) | (None, 784) | 0 |
dense (Dense) | (None, 32) | 25120 |
dense_1 (Dense) | (None, 10) | 330 |
optimizer | loss | epochs | batch_size |
---|---|---|---|
‘adam’ | ‘sparse_categorical_crossentropy’ |
损失函数
- 在训练的时候,应该跟踪多个重要数值。 这些数值输出的图表是观察训练进程的窗口,帮助你直观理解不同的超参数的设置效果
- 损失值的震荡程度和批尺寸(batch size)有关当批尺寸为1,震荡会相对较大。当批尺寸就是整个数据集时震荡就会最小
例:
optimizer | loss | epochs | batch_size |
---|---|---|---|
‘adam’ | ‘sparse_categorical_crossentropy’ | 20 | 512 |
optimizer | loss | epochs | batch_size |
---|---|---|---|
‘adam’ | ‘sparse_categorical_crossentropy’ | 20 | 1 |
训练集和验证集准确率
- 在训练分类器的时候,需要跟踪的第二重要的数值是验证集和训练集的准确率,这个图表能够展现知道模型过拟合
的程度
optimizer | loss | epochs | batch_size |
---|---|---|---|
‘adam’ | ‘sparse_categorical_crossentropy’ | 20 | 1 |
optimizer | loss | epochs | batch_size |
---|---|---|---|
‘adam’ | ‘sparse_categorical_crossentropy’ | 20 | 512 |
参数更新
随机梯度下降及各种更新方法
普通更新。最简单的更新形式是沿着负梯度方向改变参数(因为梯度指向的是上升方向,但是我们通常希望最小化损失函数)。假设有一个参数向量x及其梯度dx,那么最简单的更新的形式是:
# 普通更新
x = x ‐ learning_rate * dx
learning_rate是一个超参数,它是一个固定的常量。当在整个数据集上进行计算时,只要学习率足够低,总是能在损失函数上得到非负的进展。
动量(Momentum)更新
# 动量更新
v = mu * v ‐ learning_rate * dx # 与速度融合
x = x + v # 与位置融合
引入了一个初始化为0的变量v和一个超参数mu。说得不恰当一点,这个变量(mu)在最优化的过程中被看做动量(一般值设为0.9),但其物理意义与摩擦系数更一致。这个变量有效地抑制了速度,降低了系统的动能,不然质点在山底永远不会停下来。通过交叉验证,这个参数通常设为[0.5,0.9,0.95,0.99]中的一个。
Nesterov动量
Nesterov动量的核心思路是,当参数向量位于某个位置x时,观察上面的动量更新公式可以发现,动量部分(忽视带梯度的第二个部分)会通过mu * v稍微改变参数向量。因此,如果要计算梯度,那么可以将未来的近似位置x +mu * v看做是“向前看”,这个点在我们一会儿要停止的位置附近。因此,计算x + mu * v的梯度而不是“旧”位置x的梯度就有意义了。
在动量(Momentum)更新的基础上:
# Nesterov更新
x_ahead = x + mu * v #在动量的作用下带到的新点x_ahead
# 计算dx_ahead(在x_ahead处的梯度,而不是在x处的梯度)
v = mu * v ‐ learning_rate * dx_ahead
x += v
学习率退火
- 随步数衰减:每进行几个周期就根据一些因素降低学习率。典型的值是每过5个周期就将学习率减少一半,或者每20个周期减少到之前的0.1。这些数值的设定是严重依赖具体问题和模型的选择的。在实践中可能看见这么一种经验做法:使用一个固定的学习率来进行训练的同时观察验证集错误率,每当验证集错误率停止下降,就乘以一个常数(比如0.5)来降低学习率。
- 指数衰减。数学公式是:
a = a 0 e − k t a = a_0 e^{-kt} a=a0e−kt
其中 a 0 、 k a_0、k a0、k是超参数 t t t
是迭代次数(也可以使用周期作为单位.
- 1/t衰减的数学公式是:
a = a 0 1 + k t a = \frac{a_0}{1 + kt} a=1+kta0
其中 a 0 、 k a_0、k a0、k是超参数, t t t是迭代次数。
在实践中,我们发现随步数衰减的随机失活(dropout)更受欢迎,因为它使用的超参数(衰减系数和以周期为时间单位的步数)比K更有解释性。
如果训练卡住了,怎么判断是卡在局部最小值点还是鞍点?
核心公式: L ( θ ) ≈ L ( θ ′ ) + ( θ − θ ′ ) T g + 1 2 ( θ − θ ′ ) T H ( θ − θ ′ ) ( L o s s ( θ ) 在 θ ′ 处 泰 勒 开 ) L(\theta) \approx L(\theta^{'}) + (\theta - \theta^{'})^Tg + \frac{1}{2}(\theta - \theta^{'})^TH(\theta - \theta^{'}) (Loss(\theta)在\theta^{'}处泰勒开) L(θ)≈L(θ′)+(θ−θ′)Tg+21(θ−θ′)TH(θ−θ′)(Loss(θ)在θ′处泰勒开)
这里
H
f
(
x
)
Hf(x)
Hf(x)是Hessian矩阵,它是函数的二阶偏导数的平方矩阵 ,g是梯度向量,这和梯度下降中一样。直观理解上,Hessian矩阵描述了损失函数的局部曲率,从而使得可以进行更高效的参数更新。具体来说,就是乘以Hessian转置矩阵可以让最优化过程在曲率小的时候大步前进,在曲率大的时候小步前进。
但计算(以及求逆)Hessian矩阵操作非常耗费时间和空间。所以很难运用到实际的深度学习应用中去.
逐参数适应学习率方法
Adagrad
# 假设有梯度和参数向量x
cache = cache + dx**2
x = x ‐ learning_rate * dx / (np.sqrt(cache) + eps)
变量cache的尺寸和梯度矩阵的尺寸是一样的,还跟踪了每个参数的梯度的平方和。平滑的式子eps(一般设为1e-4到1e-8之间)是防止出现除以0的情况。 Adagrad的一个缺点是,在深度学习中单调的学习率被证明通常过于激进且过早停止学习。
RMSProp
这个方法用一种很简单的方式修改了Adagrad方法,让它不那么激进,单调地降低了学习率。具体说来,就是它使用了一个梯度平方的滑动平均:
cache = decay_rate * cache + (1 ‐ decay_rate) * dx**2
x = x ‐ learning_rate * dx / (np.sqrt(cache) + eps)
decay_rate是一个超参数,常用的值是[0.9,0.99,0.999].
RMSProp和Adagrad不同,其更新不会让学习率单调变小。
Adam
Adam看起来像是RMSProp的动量版。简化的代码是下面这样:
m = beta1*m + (1‐beta1)*dx
v = beta2*v + (1‐beta2)*(dx**2)
x = x ‐ learning_rate * m / (np.sqrt(v) + eps)
这个更新方法看起来真的和RMSProp很像,除了使用的是平滑版的梯度m,而不是用的原始梯度向量dx。论文中推荐的参数eps=1e-8, beta1=0.9, beta2=0.999。在实际操作中,我们推荐Adam作为默认的算法,一般而言跑起来比RMSProp要好一点。但是也可以试试SGD+Nesterov动量。
超参数调优
训练一个神经网络会遇到很多超参数设置。神经网络最常用的设置有:
- 初始学习率。
- 学习率衰减方式(例如一个衰减常量)。
- 正则化强度(L2惩罚,随机失活强度)。
比起交叉验证最好使用一个验证集。在大多数情况下,一个尺寸合理的验证集可以让代码更简单,不需要用几个数据集来交叉验证。你可能会听到人们说他们“交叉验证”一个参数,但是大多数情况下,他们实际是使用的一个验证集。
评价
模型集成
在交叉验证中发现最好的模型。使用交叉验证来得到最好的超参数,然后取其中最好的几个(比如10个)模型来进行集成。这样就提高了集成的多样性,但风险在于可能会包含不够理想的模型。在实际操作中,这样操作起来比较简单,在交叉验证后就不需要额外的训练了。
- 同一个模型,不同的初始化。使用交叉验证来得到最好的超参数,然后用最好的参数来训练不同初始化条件的模型。这种方法的风险在于多样性只来自于不同的初始化条件。
- 在交叉验证中发现最好的模型。使用交叉验证来得到最好的超参数,然后取其中最好的几个(比如10个)模型来进行集成。这样就提高了集成的多样性,但风险在于可能会包含不够理想的模型。在实际操作中,这样操作起来比较简单,在交叉验证后就不需要额外的训练了。
总结
训练一个神经网络需要的步骤
- 利用小批量数据对实现进行梯度检查,还要注意各种错误。
- 进行合理性检查,确认初始损失值是合理的,在小数据集上能得到100%的准确率。
- 在训练时,跟踪损失函数值,训练集和验证集准确率,如果愿意,还可以跟踪更新的参数量相对于总参数量的比例(一般在1e-3左右),然后如果是对于卷积神经网络,可以将第一层的权重可视化。
- 推荐的两个更新方法是SGD+Nesterov动量方法,或者Adam方法。
- 随着训练进行学习率衰减。比如,在固定多少个周期后让学习率减半,或者当验证集准确率下降的时候。
- 使用随机搜索(不要用网格搜索)来搜索最优的超参数。
- 进行模型集成来获得额外的性能提高。
试验代码
import tensorflow as tf
import matplotlib.pyplot as plt
import os
os.environ["CUDA_VISIBLE_DEVICES"] = '0'
(train_image, train_label), (test_image, test_label) = tf.keras.
datasets.mnist.load_data()
print(train_image.shape)
model = tf.keras.Sequential()
model.add(tf.keras.layers.Flatten(input_shape=(28, 28)))
model.add(tf.keras.layers.Dense(32, activation='relu'))
model.add(tf.keras.layers.Dense(10, activation='softmax'))
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['acc'])
history = model.fit(train_image, train_label, epochs=20, batch_size=1,
validation_data=(test_image, test_label))
print(model.summary())
# model.save("./model/minist_model.h5")
plt.figure()
plt.plot(history.epoch, history.history.get('loss'), label='loss')
plt.plot(history.epoch, history.history.get('val_loss'), label='val_loss')
plt.show()
plt.figure()
plt.plot(history.epoch, history.history.get('acc'), label='acc')
plt.plot(history.epoch, history.history.get('val_acc'), label='val_acc')
plt.show()
et(‘val_loss’), label=‘val_loss’)
plt.show()
plt.figure()
plt.plot(history.epoch, history.history.get(‘acc’), label=‘acc’)
plt.plot(history.epoch, history.history.get(‘val_acc’), label=‘val_acc’)
plt.show()
'''
参考:
李宏毅老师的机器学习
2021-6-7
'''