文章目录
- 神经网络入门二
-
- [3 神经网络优化方法](#3 神经网络优化方法)
-
- [3.1 梯度下降算法回顾](#3.1 梯度下降算法回顾)
- [3.2 反向传播(BP算法)](#3.2 反向传播(BP算法))
-
- [3.2.1 反向传播概念](#3.2.1 反向传播概念)
- [3.2.2 反向传播详解](#3.2.2 反向传播详解)
- [3.3 梯度下降优化方法](#3.3 梯度下降优化方法)
-
- [3.3.1 指数加权平均](#3.3.1 指数加权平均)
- [3.3.2 动量算法Momentum](#3.3.2 动量算法Momentum)
- [3.3.3 AdaGrad](#3.3.3 AdaGrad)
- [3.3.4 RMSProp](#3.3.4 RMSProp)
- [3.3.5 Adam](#3.3.5 Adam)
- [3.3.6 小结](#3.3.6 小结)
- [4 学习率衰减优化方法](#4 学习率衰减优化方法)
-
- [4.1 为什么要进行学习率优化](#4.1 为什么要进行学习率优化)
- [4.2 等间隔学习率衰减](#4.2 等间隔学习率衰减)
- [4.3 指定间隔学习率衰减](#4.3 指定间隔学习率衰减)
- [4.4 按指数学习率衰减](#4.4 按指数学习率衰减)
- [4.5 小结](#4.5 小结)
- [5 正则化方法](#5 正则化方法)
-
- [5.1 什么是正则化](#5.1 什么是正则化)
- [5.2 Dropout正则化](#5.2 Dropout正则化)
- [5.3 批量归一正则化(Batch Normalization)](#5.3 批量归一正则化(Batch Normalization))
神经网络入门二
3 神经网络优化方法
多层神经网络的学习能力比单层网络强得多。想要训练多层网络,需要更强大的学习算法。误差反向传播算法(Back Propagation)是其中最杰出的代表,它是目前最成功的神经网络学习算法。现实任务使用神经网络时,大多是在使用 BP 算法进行训练,值得指出的是 BP 算法不仅可用于多层前馈神经网络,还可以用于其他类型的神经网络。通常说 BP 网络时,一般是指用 BP 算法训练的多层前馈神经网络。
这就需要了解两个概念:
- 正向传播:指的是数据通过网络从输入层到输出层的传递过程。这个过程的目的是计算网络的输出值(预测值),从而与目标值(真实值)比较以计算误差。
- 反向传播:指的是计算损失函数相对于网络中各参数(权重和偏置)的梯度,指导优化器更新参数,从而使神经网络的预测更接近目标值。
3.1 梯度下降算法回顾
梯度下降法 简单来说就是一种寻找使损失函数最小化的方法。
从数学角度来看,梯度的方向是函数增长速度最快的方向,那么梯度的反方向就是函数减少最快的方向,所以有:
其中,η是学习率,如果学习率太小 ,那么每次训练之后得到的效果都太小,增大训练的时间成本 。如果,学习率太大 ,那就有可能直接跳过最优解,进入无限的训练中。解决的方法就是,学习率也需要随着训练的进行而变化。
在上图中我们展示了一维和多维的损失函数,损失函数呈碗状。在训练过程中损失函数对权重的偏导数就是损失函数在该位置点的梯度。我们可以看到,沿着负梯度方向移动,就可以到达损失函数底部,从而使损失函数最小化。这种利用损失函数的梯度迭代地寻找最小值的过程就是梯度下降的过程。
在进行模型训练时,有三个基础的概念:
- Epoch: 使用全部数据对模型进行以此完整训练,训练次数
- Batch: 使用训练集中的小部分样本对模型权重进行以此反向传播的参数更新,每次训练每批次样本数量
- Iteration: 使用一个 Batch 数据对模型进行一次参数更新的过程,每次训练批次数
假设数据集有 50000 个训练样本,现在选择 Batch Size = 256 对模型进行训练。
每个 Epoch 要训练的图片数量:50000
训练集具有的 Batch 个数:50000/256+1=196
每个 Epoch 具有的 Iteration 个数:196
10个 Epoch 具有的 Iteration 个数:1960
在深度学习中,梯度下降的几种方式的根本区别就在于Batch Size不同,如下表所示:
注:上表中 Mini-Batch 的 Batch 个数为 N / B + 1 是针对未整除的情况。整除则是 N / B。
3.2 反向传播(BP算法)
利用反向传播算法 对神经网络进行训练。该方法与梯度下降算法 相结合,对网络中所有权重计算损失函数的梯度 ,并利用梯度值来更新权值以最小化损失函数。
3.2.1 反向传播概念
前向传播:指的是数据输入到神经网络中,逐层向前传输,一直运算到输出层为止。
反向传播(Back Propagation):利用损失函数ERROR值,从后往前,结合梯度下降算法,依次求各个参数的偏导,并进行参数更新。
在网络的训练过程中经过前向传播后得到的最终结果跟训练样本的真实值总是存在一定误差,这个误差便是损失函数 ERROR。想要减小这个误差,就用损失函数 ERROR,从后往前,依次求各个参数的偏导,这就是反向传播(Back Propagation)。
3.2.2 反向传播详解
反向传播算法利用链式法则对神经网络中的各个节点的权重进行更新。
【举个栗子🌰:】
如下图是一个简单的神经网络用来举例:激活函数为sigmoid
前向传播运算:
接下来是反向传播(求网络误差对各个权重参数的梯度):
我们先来求最简单的,求误差E对w5的导数。首先明确这是一个链式法则 的求导过程,要求误差E对w5的导数,需要先求误差E对 o u t o 1 out_{o1} outo1的导数,再求 o u t o 1 out_{o1} outo1对 n e t o 1 net_{o1} neto1的导数,最后再求 n e t o 1 net_{o1} neto1对 w 5 w_5 w5的导数,经过这个链式法则 ,我们就可以求出误差E对 w 5 w_5 w5的导数(偏导),如下图所示:
导数(梯度)已经计算出来了,下面就是反向传播与参数更新过程:
如果要想求误差E对w1的导数,误差E对w1的求导路径不止一条,这会稍微复杂一点,但换汤不换药,计算过程如下所示:
至此,反向传播算法的过程就讲完了啦!
3.3 梯度下降优化方法
梯度下降优化算法中,可能会碰到以下情况:
- 碰到平缓区域,梯度值较小,参数优化变慢
- 碰到 "鞍点" ,梯度为0,参数无法优化
- 碰到局部最小值,参数不是最优
对于这些问题, 出现了一些对梯度下降算法的优化方法,例如:Momentum 、AdaGrad 、RMSprop 、Adam 等
3.3.1 指数加权平均
我们最常见的算数平均指的是将所有数加起来除以数的个数,每个数的权重是相同的。指数加权平均指的是给每个数赋予不同的权重求得平均数。移动平均数,指的是计算最近邻的 N 个数来获得平均数。
指数移动加权平均则是参考各数值,并且各数值的权重都不同,距离越远的数字对平均数计算的贡献就越小(权重较小),距离越近则对平均数的计算贡献就越大(权重越大)。
比如:明天气温怎么样,和昨天气温有很大关系,而和一个月前的气温关系就小一些。
计算公式可以用下面的式子来表示:
- S t S_t St 表示指数加权平均值;
- Y t Y_t Yt 表示t时刻的值;
- β 调节权重系数,该值越大平均数越平缓。
第100天的指数加权平均值为:
下面通过代码来看结果,随机产生 30 天的气温数据:
python
import torch
import matplotlib.pyplot as plt
ELEMENT_NUMBER = 30
# 1. 实际平均温度
def test01():
# 固定随机数种子
torch.manual_seed(0)
# 产生30天的随机温度
temperature = torch.randn(size=[ELEMENT_NUMBER,]) * 10
print(temperature)
# 绘制平均温度
days = torch.arange(1, ELEMENT_NUMBER + 1, 1)
plt.plot(days, temperature, color='r')
plt.scatter(days, temperature)
plt.show()
# 2. 指数加权平均温度
def test02(beta=0.9):
# 固定随机数种子
torch.manual_seed(0)
# 产生30天的随机温度
temperature = torch.randn(size=[ELEMENT_NUMBER,]) * 10
print(temperature)
exp_weight_avg = []
# idx从1开始
for idx, temp in enumerate(temperature, 1):
# 第一个元素的 EWA 值等于自身
if idx == 1:
exp_weight_avg.append(temp)
continue
# 第二个元素的 EWA 值等于上一个 EWA 乘以 β + 当前气温乘以 (1-β)
# idx-2:2-2=0,exp_weight_avg列表中第一个值的下标值
new_temp = exp_weight_avg[idx - 2] * beta + (1 - beta) * temp
exp_weight_avg.append(new_temp)
days = torch.arange(1, ELEMENT_NUMBER + 1, 1)
plt.plot(days, exp_weight_avg, color='r')
plt.scatter(days, temperature)
plt.show()
if __name__ == '__main__':
test01()
test02(0.5)
test02(0.9)
从程序运行结果可以看到:
- 指数加权平均绘制出的气温变化曲线更加平缓
- β 的值越大,则绘制出的折线越加平缓,波动越小(1-β越小,t时刻的 S t S_t St越不依赖 Y t Y_t Yt的值)
- β 值一般默认都是 0.9
3.3.2 动量算法Momentum
当梯度下降碰到 "峡谷" 、"平缓"、"鞍点" 区域时, 参数更新速度变慢。 Momentum 通过指数加权平均法,累计历史梯度值,进行参数更新,越近的梯度值对当前参数更新的重要性越大。
梯度计算公式:
s t = β s t − 1 + ( 1 − β ) g t s_t=βs_{t−1}+(1−β)g_t st=βst−1+(1−β)gt
参数更新公式:
w t = w t − 1 − η s t w_t=w_{t−1}−ηs_t wt=wt−1−ηst
s t s_t st是当前时刻指数加权平均梯度值
s t − 1 s_{t-1} st−1是历史指数加权平均梯度值
g t g_t gt是当前时刻的梯度值
β 是调节权重系数,通常取 0.9 或 0.99
η是学习率
w t w_t wt是当前时刻模型权重参数
python
咱们举个例子,假设:权重 β 为 0.9,例如:
第一次梯度值:s1 = g1 = w1
第二次梯度值:s2 = 0.9*s1 + g2*0.1
第三次梯度值:s3 = 0.9*s2 + g3*0.1
第四次梯度值:s4 = 0.9*s3 + g4*0.1
1. w 表示初始梯度
2. g 表示当前轮数计算出的梯度值
3. s 表示历史梯度移动加权平均值
梯度下降公式中梯度的计算,就不再是当前时刻t的梯度值,而是历史梯度值的指数移动加权平均值。
公式修改为:
Wt = Wt-1 - η*St
Wt:当前时刻模型权重参数
St:当前时刻指数加权平均梯度值
η:学习率
Monmentum 优化方法是如何一定程度上克服 "平缓"、"鞍点"、"峡谷" 的问题呢?
- 当处于鞍点位置时,由于当前的梯度为 0,参数无法更新。但是 Momentum 动量梯度下降算法已经在先前积累了一些梯度值,很有可能使得跨过鞍点。
- 由于 mini-batch 普通的梯度下降算法,每次选取少数的样本梯度确定前进方向,可能会出现震荡,使得训练时间变长。Momentum 使用移动加权平均,平滑了梯度的变化,使得前进方向更加平缓,有利于加快训练过程。一定程度上有利于降低 "峡谷" 问题的影响。
在pytorch中动量梯度优化法编程实践如下:
python
def test01():
# 1 初始化权重参数
w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
loss = ((w ** 2) / 2.0).sum()
# 2 实例化优化方法:SGD 指定参数beta=0.9
optimizer = torch.optim.SGD([w], lr=0.01, momentum=0.9)
# 3 第1次更新 计算梯度,并对参数进行更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
# 4 第2次更新 计算梯度,并对参数进行更新
# 使用更新后的参数机选输出结果
loss = ((w ** 2) / 2.0).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
结果显示:
python
第1次: 梯度w.grad: 1.000000, 更新后的权重:0.990000
第2次: 梯度w.grad: 0.990000, 更新后的权重:0.971100
3.3.3 AdaGrad
AdaGrad 通过对不同的参数分量使用不同的学习率,AdaGrad 的学习率总体会逐渐减小,这是因为 AdaGrad 认为:在起初时,我们距离最优目标仍较远,可以使用较大的学习率,加快训练速度,随着迭代次数的增加,学习率逐渐下降。
其计算步骤如下:
-
初始化学习率 η、初始化参数w、小常数 σ = 1e-10
-
初始化梯度累计变量 s = 0
-
从训练集中采样 m 个样本的小批量,计算梯度 g t g_t gt
-
累积平方梯度 : s t s_t st = s t − 1 s_{t-1} st−1 + g t g_t gt ⊙ g t g_t gt,⊙ 表示各个分量相乘
-
学习率 η 的计算公式如下:
η = η s t + σ η\over\sqrt{s_t}+σ st +ση
-
权重参数更新公式如下:
w t w_t wt = w t − 1 w_{t-1} wt−1 - η s t + σ η\over\sqrt{s_t}+σ st +ση * g t g_t gt
-
重复 3-7 步骤
AdaGrad 缺点是可能会使得学习率过早、过量的降低,导致模型训练后期学习率太小,较难找到最优解。
在PyTorch中AdaGrad优化法编程实践如下:
python
def test02():
# 1 初始化权重参数
w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
loss = ((w ** 2) / 2.0).sum()
# 2 实例化优化方法:adagrad优化方法
optimizer = torch.optim.Adagrad([w], lr=0.01)
# 3 第1次更新 计算梯度,并对参数进行更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
# 4 第2次更新 计算梯度,并对参数进行更新
# 使用更新后的参数机选输出结果
loss = ((w ** 2) / 2.0).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
结果显示:
python
第1次: 梯度w.grad: 1.000000, 更新后的权重:0.990000
第2次: 梯度w.grad: 0.990000, 更新后的权重:0.982965
3.3.4 RMSProp
RMSProp 优化算法是对 AdaGrad 的优化 。最主要的不同是,其使用指数加权平均梯度替换历史梯度的平方和。
其计算过程如下:
-
初始化学习率 η、初始化权重参数w、小常数 σ = 1e-10
-
初始化梯度累计变量 s = 0
-
从训练集中采样 m 个样本的小批量,计算梯度 g t g_t gt
-
使用指数加权平均累计历史梯度,⊙ 表示各个分量相乘,公式如下:
s t s_t st = β s t − 1 s_{t-1} st−1 + (1-β) g t g_t gt⊙ g t g_t gt
-
学习率 η 的计算公式如下:
η = η s t + σ η\over\sqrt{s_t}+σ st +ση
-
权重参数更新公式如下:
w t w_t wt = w t − 1 w_{t-1} wt−1 - η s t + σ η\over\sqrt{s_t}+σ st +ση * g t g_t gt
-
重复 3-7 步骤
RMSProp 与 AdaGrad 最大的区别是对梯度的累积方式不同,对于每个梯度分量仍然使用不同的学习率。
RMSProp 通过引入衰减系数β,控制历史梯度对历史梯度信息获取的多少. 被证明在神经网络非凸条件下的优化更好,学习率衰减更加合理一些。
需要注意的是:AdaGrad 和 RMSProp 都是对于不同的参数分量使用不同的学习率,如果某个参数分量的梯度值较大,则对应的学习率就会较小,如果某个参数分量的梯度较小,则对应的学习率就会较大一些。
在PyTorch中RMSprop梯度优化法,编程实践如下:
python
def test03():
# 1 初始化权重参数
w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
loss = ((w ** 2) / 2.0).sum()
# 2 实例化优化方法:RMSprop算法,其中alpha对应beta
optimizer = torch.optim.RMSprop([w], lr=0.01, alpha=0.9)
# 3 第1次更新 计算梯度,并对参数进行更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
# 4 第2次更新 计算梯度,并对参数进行更新
# 使用更新后的参数机选输出结果
loss = ((w ** 2) / 2.0).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
结果显示:
python
第1次: 梯度w.grad: 1.000000, 更新后的权重:0.968377
第2次: 梯度w.grad: 0.968377, 更新后的权重:0.945788
3.3.5 Adam
-
Momentum 使用指数加权平均计算当前的梯度值
-
AdaGrad、RMSProp 使用自适应的学习率
-
Adam优化算法(Adaptive Moment Estimation,自适应矩估计)将 Momentum 和 RMSProp 算法结合在一起
- 修正梯度: 使⽤梯度的指数加权平均
- 修正学习率: 使⽤梯度平⽅的指数加权平均
-
原理 :Adam 是结合了 Momentum 和 RMSProp 优化算法的优点的自适应学习率算法。它计算了梯度的一阶矩(平均值)和二阶矩(梯度的方差)的自适应估计,从而动态调整学习率。
-
梯度计算公式:
m t = β 1 m t − 1 + ( 1 − β 1 ) g t m_t=β_1m_{t−1}+(1−β_1)g_t mt=β1mt−1+(1−β1)gt
s t = β 2 s t − 1 + ( 1 − β 2 ) g t 2 s_t=β_2s_{t−1}+(1−β_2)gt^2 st=β2st−1+(1−β2)gt2
m t ^ \hat{m_t} mt^ = m t 1 − β 1 t m_t\over1−β_1^t 1−β1tmt, s t ^ \hat{s_t} st^= s t 1 − β 2 t s_t\over1−β_2^t 1−β2tst
-
权重参数更新公式:
w t w_t wt = w t − 1 w_{t−1} wt−1 − η s t ^ + ϵ η\over\sqrt{\hat{s_t}}+ϵ st^ +ϵη m t ^ \hat{m_t} mt^
其中, m t m_t mt 是梯度的一阶矩估计, s t s_t st 是梯度的二阶矩估计,$ \hat{m_t}$和 s t ^ \hat{s_t} st^ 是偏差校正后的估计。
在PyTroch中,Adam梯度优化法编程实践如下:
python
def test04():
# 1 初始化权重参数
w = torch.tensor([1.0], requires_grad=True)
loss = ((w ** 2) / 2.0).sum()
# 2 实例化优化方法:Adam算法,其中betas是指数加权的系数
optimizer = torch.optim.Adam([w], lr=0.01, betas=[0.9, 0.99])
# 3 第1次更新 计算梯度,并对参数进行更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
# 4 第2次更新 计算梯度,并对参数进行更新
# 使用更新后的参数机选输出结果
loss = ((w ** 2) / 2.0).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
结果显示:
python
第1次: 梯度w.grad: 1.000000, 更新后的权重:0.990000
第2次: 梯度w.grad: 0.990000, 更新后的权重:0.980003
3.3.6 小结
优化算法 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
SGD | 简单、容易实现。 | 收敛速度较慢,容易震荡,特别是在复杂问题中。 | 用于简单任务,或者当数据特征分布相对稳定时。 |
Momentum | 可以加速收敛,减少震荡,特别是在高曲率区域。 | 需要手动调整动量超参数,可能会在小步长训练中过度更新。 | 用于非平稳优化问题,尤其是深度学习中的应用。 |
AdaGrad | 自适应调整学习率,适用于稀疏数据。 | 学习率会在训练过程中逐渐衰减,可能导致早期停滞。 | 适合稀疏数据,如 NLP 或推荐系统中的特征。 |
RMSProp | 解决了 AdaGrad 学习率过早衰减的问题,适应性强。 | 需要选择合适的超参数,更新可能会过于激进。 | 适用于动态问题、非平稳目标函数,如深度学习训练。 |
Adam | 结合了 Momentum 和 RMSProp 的优点,适应性强且稳定。 | 需要调节更多的超参数,训练过程中可能会产生较大波动。 | 广泛适用于各种深度学习任务,特别是非平稳和复杂问题。 |
-
简单任务和较小的模型:SGD 或 Momentum
-
复杂任务或有大量数据:Adam 是最常用的选择,因其在大部分任务上都表现优秀
-
需要处理稀疏数据或文本数据:Adagrad 或 RMSProp
4 学习率衰减优化方法
4.1 为什么要进行学习率优化
在训练神经网络时,一般情况下学习率都会随着训练而变化。这主要是由于,在神经网络训练的后期,如果学习率过高,会造成loss的振荡 ,但是如果学习率减小的过慢,又会造成收敛变慢的情况。
运行下面代码,观察学习率设置不同对网络训练的影响:
python
# x看成是权重,y看成是loss,下面通过代码来理解学习率的作用
def func(x_t):
return torch.pow(2*x_t, 2) # y = 4 x ^2
# 采用较小的学习率,梯度下降的速度慢
# 采用较大的学习率,梯度下降太快越过了最小值点,导致不收敛,甚至震荡
def test():
x = torch.tensor([2.], requires_grad=True)
# 记录loss迭代次数,画曲线
iter_rec, loss_rec, x_rec = list(), list(), list()
# 实验学习率: 0.01 0.02 0.03 0.1 0.2 0.3 0.4
# lr = 0.1 # 正常的梯度下降
# lr = 0.125 # 当学习率设置0.125 一下子求出一个最优解
# x=0 y=0 在x=0处梯度等于0 x的值x=x-lr*x.grad就不用更新了
# 后续再多少次迭代 都固定在最优点
lr = 0.2 # x从2.0一下子跨过0点,到了左侧负数区域
# lr = 0.3 # 梯度越来越大 梯度爆炸
max_iteration = 4
for i in range(max_iteration):
y = func(x) # 得出loss值
y.backward() # 计算x的梯度
print("Iter:{}, X:{:8}, X.grad:{:8}, loss:{:10}".format(
i, x.detach().numpy()[0], x.grad.detach().numpy()[0], y.item()))
x_rec.append(x.item()) # 梯度下降点 列表
# 更新参数
x.data.sub_(lr * x.grad) # x = x - x.grad
x.grad.zero_()
iter_rec.append(i) # 迭代次数 列表
loss_rec.append(y) # 损失值 列表
# 迭代次数-损失值 关系图
plt.subplot(121).plot(iter_rec, loss_rec, '-ro')
plt.grid()
plt.xlabel("Iteration X")
plt.ylabel("Loss value Y")
# 函数曲线-下降轨迹 显示图
x_t = torch.linspace(-3, 3, 100)
y = func(x_t)
plt.subplot(122).plot(x_t.numpy(), y.numpy(), label="y = 4*x^2")
y_rec = [func(torch.tensor(i)).item() for i in x_rec]
print('x_rec--->', x_rec)
print('y_rec--->', y_rec)
# 指定线的颜色和样式(-ro:红色圆圈,b-:蓝色实线等)
plt.subplot(122).plot(x_rec, y_rec, '-ro')
plt.grid()
plt.legend()
plt.show()
运行效果图如下:
可以看出:采用较小的学习率,梯度下降的速度慢;采用较大的学习率,梯度下降太快越过了最小值点,导致震荡,甚至不收敛(梯度爆炸)。
4.2 等间隔学习率衰减
等间隔学习率衰减方式如下所示:
在PyTorch中实现时使用:
python
# step_size:调整间隔数=50
# gamma:调整系数=0.5
# 调整方式:lr = lr * gamma
optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.1)
具体使用方式如下:
python
import torch
from torch import optim
import matplotlib.pyplot as plt
def test_StepLR():
# 0.参数初始化
LR = 0.1 # 设置学习率初始化值为0.1
iteration = 10
max_epoch = 200
# 1 初始化参数
y_true = torch.tensor([0])
x = torch.tensor([1.0])
w = torch.tensor([1.0], requires_grad=True)
# 2.优化器
optimizer = optim.SGD([w], lr=LR, momentum=0.9)
# 3.设置学习率下降策略
scheduler_lr = optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.5)
# 4.获取学习率的值和当前的epoch
lr_list, epoch_list = [], []
for epoch in range(max_epoch):
lr_list.append(scheduler_lr.get_last_lr()) # 获取当前lr
epoch_list.append(epoch) # 获取当前的epoch
for i in range(iteration): # 遍历每一个batch数据
loss = (w*x-y_true)**2 # 目标函数
optimizer.zero_grad()
# 反向传播
loss.backward()
optimizer.step()
# 更新下一个epoch的学习率
scheduler_lr.step()
# 5.绘制学习率变化的曲线
plt.plot(epoch_list, lr_list, label="Step LR Scheduler")
plt.xlabel("Epoch")
plt.ylabel("Learning rate")
plt.legend()
plt.show()
4.3 指定间隔学习率衰减
指定间隔学习率衰减的效果如下:
在PyTorch中实现时使用:
python
# milestones:设定调整轮次:[50, 125, 160]
# gamma:调整系数
# 调整方式:lr = lr * gamma
optim.lr_scheduler.MultiStepLR(optimizer, milestones, gamma=0.1, last_epoch=-1)
具体使用方式如下所示:
python
import torch
from torch import optim
import matplotlib.pyplot as plt
def test_MultiStepLR():
torch.manual_seed(1)
LR = 0.1
iteration = 10
max_epoch = 200
y_true = torch.tensor([0])
x = torch.tensor([1.0])
w = torch.tensor([1.0], requires_grad=True)
optimizer = optim.SGD([w], lr=LR, momentum=0.9)
# 设定调整时刻数
milestones = [50, 125, 160]
# 设置学习率下降策略
scheduler_lr = optim.lr_scheduler.MultiStepLR(optimizer, milestones=milestones, gamma=0.5)
lr_list, epoch_list = list(), list()
for epoch in range(max_epoch):
lr_list.append(scheduler_lr.get_last_lr())
epoch_list.append(epoch)
for i in range(iteration):
loss = (w*x-y_true)**2
optimizer.zero_grad()
# 反向传播
loss.backward()
# 参数更新
optimizer.step()
# 更新下一个epoch的学习率
scheduler_lr.step()
plt.plot(epoch_list, lr_list, label="Multi Step LR Scheduler\nmilestones:{}".format(milestones))
plt.xlabel("Epoch")
plt.ylabel("Learning rate")
plt.legend()
plt.show()
4.4 按指数学习率衰减
按指数衰减调整学习率的效果如下:
在PyTorch中实现时使用:
python
# gamma:指数的底
# 调整方式
# lr= lr∗gamma^epoch
optim.lr_scheduler.ExponentialLR(optimizer, gamma)
具体使用方式如下所示:
python
import torch
from torch import optim
import matplotlib.pyplot as plt
def test_ExponentialLR():
# 0.参数初始化
LR = 0.1 # 设置学习率初始化值为0.1
iteration = 10
max_epoch = 200
# 1 初始化参数
y_true = torch.tensor([0])
x = torch.tensor([1.0])
w = torch.tensor([1.0], requires_grad=True)
# 2.优化器
optimizer = optim.SGD([w], lr=LR, momentum=0.9)
# 3.设置学习率下降策略
gamma = 0.95
scheduler_lr = optim.lr_scheduler.ExponentialLR(optimizer, gamma=gamma)
# 4.获取学习率的值和当前的epoch
lr_list, epoch_list = list(), list()
for epoch in range(max_epoch):
lr_list.append(scheduler_lr.get_last_lr())
epoch_list.append(epoch)
for i in range(iteration): # 遍历每一个batch数据
loss = (w*x-y_true)**2
optimizer.zero_grad()
# 反向传播
loss.backward()
optimizer.step()
# 更新下一个epoch的学习率
scheduler_lr.step()
# 5.绘制学习率变化的曲线
plt.plot(epoch_list, lr_list, label="Multi Step LR Scheduler")
plt.xlabel("Epoch")
plt.ylabel("Learning rate")
plt.legend()
plt.show()
4.5 小结
方法 | 等间隔学习率衰减 (Step Decay) | 指定间隔学习率衰减 (Exponential Decay) | 指数学习率衰减 (Exponential Moving Average Decay) |
---|---|---|---|
衰减方式 | 固定步长衰减 | 指定步长衰减 | 平滑指数衰减,历史平均考虑 |
实现难度 | 简单易实现 | 相对简单,容易调整 | 需要额外历史计算,较复杂 |
适用场景 | 大型数据集、较为简单的任务 | 对训练平稳性要求较高的任务 | 高精度训练,避免过快收敛 |
优点 | 直观,易于调试,适用于大批量数据 | 易于调试,稳定训练过程 | 平滑且考虑历史更新,收敛稳定性较强 |
缺点 | 学习率变化较大,可能跳过最优点 | 在某些情况下可能衰减过快,导致优化提前停滞 | 超参数调节较为复杂,可能需要更多的计算资源 |
5 正则化方法
5.1 什么是正则化
- 在设计机器学习算法时希望在新样本上的泛化能力强。许多机器学习算法都采用相关的策略来减小测试误差,这些策略被统称为正则化
- 神经网络强大的表示能力经常遇到过拟合,所以需要使用不同形式的正则化策略
- 目前在深度学习中使用较多的策略有范数惩罚,DropOut,特殊的网络层等,接下来我们对其进行详细的介绍
5.2 Dropout正则化
在训练深层神经网络时,由于模型参数较多,在数据量不足的情况下,很容易过拟合。Dropout(中文翻译成随机失活)是一个简单有效的正则化方法。
- 在训练过程中,Dropout的实现是让神经元以超参数p(丢弃概率)的概率停止工作或者激活被置为0,未被置为0的进行缩放,缩放比例为1/(1-p)。训练过程可以认为是对完整的神经网络的一些子集进行训练,每次基于输入数据只更新子网络的参数
- 在实际应用中,Dropout参数p的概率通常取值在0.2到0.5之间
- 对于较小的模型或较复杂的任务,丢弃率可以选择0.3或更小
- 对于非常深的网络,较大的丢弃率(如0.5或0.6)可能会有效防止过拟合
- 实际应用中,通常会在全连接层(激活函数后)之后添加Dropout层
- 在测试过程中,随机失活不起作用
- 在测试阶段,使用所有的神经元进行预测,以获得更稳定的结果
- 直接使用训练好的模型进行测试,由于所有的神经元都参与计算,输出的期望值会比训练阶段高。测试阶段的期望输出是 E[x_test] = x
- 测试/推理模式:model.eval()
- 缩放的必要性
- 在训练阶段,将参与计算的神经元的输出除以(1-p)
- 经过Dropout后的期望输出变为 E[x_dropout] = [(1-p) * x] / (1-p) = x,与测试阶段的期望输出一致
- 训练模型:model.train()
我们通过一段代码观察下dropout的效果:
python
import torch
import torch.nn as nn
def test():
# 初始化随机失活层
dropout = nn.Dropout(p=0.4)
# 初始化输入数据:表示某一层的weight信息
inputs = torch.randint(0, 10, size=[1, 4]).float()
layer = nn.Linear(4,5)
y = layer(inputs)
y = torch.relu(y)
print("未失活FC层的输出结果:\n", y)
y = dropout(y)
print("失活后FC层的输出结果:\n", y)
输出结果:
python
未失活FC层的输出结果:
tensor([[0.0000, 1.8033, 1.4608, 4.5189, 6.9116]], grad_fn=<ReluBackward0>)
失活后FC层的输出结果:
tensor([[0.0000, 3.0055, 2.4346, 7.5315, 11.5193]], grad_fn=<MulBackward0>)
上述代码将Dropout层的丢弃概率p设置为0.4,此时经过Dropout层计算的张量中就出现了很多0, 未变为0的按照(1/(1-0.4))进行处理。
5.3 批量归一正则化(Batch Normalization)
在神经网络的训练过程中,流经网络的数据都是一个batch,每个batch之间的数据分布变化非常剧烈,这就使得网络参数频繁的进行大的调整以适应流经网络的不同分布的数据,给模型训练带来非常大的不稳定性,使得模型难以收敛。如果我们对每一个batch的数据进行标准化之后,数据分布就变得稳定,参数的梯度变化也变得稳定,有助于加快模型的收敛。
通过标准化每一层的输入,使其均值接近0,方差接近1,从而加速训练并提高泛化能力。
先对数据标准化,再对数据重构(缩放+平移),写成公式如下所示:
λ和β是可学习的参数,它相当于对标准化后的值做了一个线性变换 ,λ为系数,β为偏置;
eps 通常指为 1e-5,避免分母为 0;
E(x) 表示变量的均值;
Var(x) 表示变量的方差;
批量归一化的步骤如下:
-
计算均值和方差 :对于每个神经元(即每一层的输入特征),计算该特征在一个小批量(batch)上的均值 μ B μ_B μB 和方差 σ B 2 \sigma_B^2 σB2,它们的计算公式如下:
μ B = 1 m ∑ i = 1 m x i μ_B=\frac{1}{m} \sum_{i=1}^{m} x_i μB=m1∑i=1mxi
σ B 2 = 1 m ∑ i = 1 m ( x i − μ B ) 2 σ_B^2=\frac{1}{m} \sum_{i=1}^{m} (x_i - \mu_B)^2 σB2=m1∑i=1m(xi−μB)2
其中 x i x_i xi 表示小批量中的第 i i i 个样本, m m m 是小批量的样本数量。
-
标准化:然后,对每个样本的输入进行标准化,得到归一化的输出:
x ^ i = x i − μ B σ B 2 + ϵ \hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}} x^i=σB2+ϵ xi−μB
其中, ϵ ϵ ϵ 是一个小常数,用来避免除以零的情况。
-
缩放和平移 :为了让网络能够恢复其学习能力,BN 层引入了两个可训练的参数 γ γ γ 和 β β β,分别用于缩放 和平移:
y i = γ x ^ i + β y_i = \gamma \hat{x}_i + β yi=γx^i+β
其中, γ γ γ 和 β β β 是可学习的参数,通过 γ 和 β,BN 层不再是简单的将每一层输入强行变为标准正态分布,而是允许网络学习更适合于该层的输入分布;规范化操作会丢失原始输入的一些信息,而 γ γ γ 和 β β β 可以弥补这种信息损失。
批量归一化的作用:
-
**减少内部协方差偏移:**通过对每层的输入进行标准化,减少了输入数据分布的变化,从而加速了训练过程,并使得网络在训练过程中更加稳定。
-
加速训练:
- 在没有批量归一化的情况下,神经网络的训练通常会很慢,尤其是深度网络。因为在每层的训练过程中,输入数据的分布(特别是前几层)会不断变化,这会导致网络学习速度缓慢。
- 批量归一化通过确保每层的输入数据在训练时分布稳定,有效减少了这种变化,从而加速了训练过程。
-
**起到正则化作用:**批量归一化可以视作一种正则化方法,因为它引入了对训练样本的噪声(不同批次的统计信息不同,批次较小的均值和方差估计会更加不准确),使得模型不容易依赖特定的输入特征,从而起到一定的正则化效果,减少了对其他正则化技术(如Dropout)的需求。
-
**提升泛化能力:**由于其正则化效果,批量归一化能帮助网络在测试集上取得更好的性能。
批量归一化层在计算机视觉领域使用较多
Batch Normalization 的使用步骤:
- 在网络层后添加 BN 层:
- 通常,BN 层会添加在卷积层 (Conv2d) 或全连接层 (Linear) 之后,激活函数之前。
- 例如:Conv2d -> BN -> ReLU 或者 Linear -> BN -> ReLU。
- 训练时: model.train()
- BN 层会计算当前批次的均值 μ μ μ 和方差 σ 2 σ² σ2。
- 然后,利用这两个统计量对当前批次的数据进行规范化。
- 规范化后的数据会被缩放 γ γ γ 和平移 β β β。
- 同时,BN 层还会维护一个全局均值 和全局方差的移动平均值,用于推理阶段。
- 推理时: model.eval()
- 推理时,不会再使用当前批次的均值和方差,而是使用训练阶段计算的全局均值 和全局方差。
- 同样,规范化后的数据会被缩放 γ γ γ 和平移 β β β。
python
import torch
import torch.nn as nn
"""
BatchNorm1d:主要应用于全连接层或处理一维数据的网络,例如文本处理。它接收形状为 (N, num_features) 的张量作为输入。
BatchNorm2d:主要应用于卷积神经网络,处理二维图像数据或特征图。它接收形状为 (N, C, H, W) 的张量作为输入。
BatchNorm3d:主要用于三维卷积神经网络 (3D CNN),处理三维数据,例如视频或医学图像。它接收形状为 (N, C, D, H, W) 的张量作为输入。
"""
def tes01():
# 创建测试样本, 假设是经过卷积层(Conv2d)处理后的特征图
# (N, C, H, W): 一张图, 两个通道, 每个通道3行4列
# 可以创建1个样本, 图像的BN是对每个通道的特征图(行列数据)进行标准化
input_2d = torch.randn(size=(1, 2, 3, 4))
print("input-->", input_2d)
# num_features:输入特征数
# eps:非常小的浮点数,防止除以0的错误
# momentum:动量系数
# affine:默认为True,γ和β被使用,让BN层更加灵活
bn2d = nn.BatchNorm2d(num_features=2, eps=1e-05, momentum=0.1, affine=True)
output = bn2d(input_2d)
print("output-->", output)
print(output.size())
print(bn2d.weight)
print(bn2d.bias)
def tes02():
# 创建测试样本
# 2个样本, 1个特征
# 不能创建1个样本, 无法统计均值和方差
input_1d = torch.randn(size=(2, 2))
# 创建线性层对象
linear1 = nn.Linear(in_features=2, out_features=3)
# 创建BN层对象
# num_features:输入特征数
bn1d = nn.BatchNorm1d(num_features=3) # 20 output features
output_1d = linear1(input_1d)
# 进行批量归一化
output = bn1d(output_1d)
print("output-->", output)
print(output.size()) # (32, 20)
输出结果:
python
test01:
input_2d--> tensor([[[[-0.2751, -1.2183, -0.5106, -0.1540],
[-0.4585, -0.5989, -0.6063, 0.5986],
[-0.4745, 0.1496, -1.1266, -1.2377]],
[[ 0.2580, 1.2065, 1.4598, 0.8387],
[-0.4586, 0.8938, -0.3328, 0.1192],
[-0.3265, -0.6263, 0.0419, -1.2231]]]])
output--> tensor([[[[ 0.4164, -1.3889, -0.0343, 0.6484],
[ 0.0655, -0.2032, -0.2175, 2.0889],
[ 0.0349, 1.2294, -1.2134, -1.4262]],
[[ 0.1340, 1.3582, 1.6853, 0.8835],
[-0.7910, 0.9546, -0.6287, -0.0452],
[-0.6205, -1.0075, -0.1449, -1.7779]]]],
grad_fn=<NativeBatchNormBackward0>)
torch.Size([1, 2, 3, 4])
Parameter containing:
tensor([1., 1.], requires_grad=True)
Parameter containing:
tensor([0., 0.], requires_grad=True)
test02:
output--> tensor([[-0.9998, 1.0000, 1.0000],
[ 0.9998, -1.0000, -1.0000]], grad_fn=<NativeBatchNormBackward0>)
torch.Size([2, 3])