1.自适应学习率
1.1AdaGrad
AdaGrad是典型的自适应学习率方法,其能够根据梯度大小自动调整学习率。AdaGrad 可以做到梯度比较大的时候,学习率就减小,梯度比较小的时候,学习率就放大。
梯度下降更新某个参数 的过程为
(1)
在第 t 个迭代的值减掉在第 t 个迭代参数 i 算出来的梯度
(2)
代表在第 t 个迭代,即
时, 参数
损失 L 的微分,学习率是固定的。
现在要有一个随着参数定制化的学习率,即把原来学习率 η 变成
(3)
的上标为 i,这代表参数 σ 与 i 相关,不同的参数的 σ 不同。
的下标为 t,这代表参数 σ 与迭代相关,不同的迭代也会有不同的 σ。学习率从 η 改成
的时候,学习率就变得参数相关。
参数相关的一个常见的类型是算梯度的均方根。参数的更新过程为
(4)
其中 是初始化参数。而
的计算过程为
(5)
其中是梯度。将
的值代入更新的公式可知
的值是 +1 或 −1。第一次在更新参数,从
更新到
的时候,要么是加上 η,要么是减掉 η,跟梯度的大小无关,这个是第一步的情况。
第二次更新参数过程为
(6)
其中是过去所有计算出来的梯度的平方的平均再开根号,即均方根。同样的操作反复继续下去,第 t + 1 次更新参数的时候,
当作是新的学习率来更新参数。
1.2RMSProp
MSprop 第一步跟 Adagrad 的方法是相同的,即
(7)
第二步更新过程为
(8)
(9)
其中 0 < α < 1,其是一个可以调整的超参数。计算 的方法跟 AdaGrad 算均方根不一样,在 RMSprop 里面,可以自己调整现在的这个梯度的重要性。如果 α 设很小趋近于 0,代表相较于之前算出来的梯度而言,比较重要;如果 α 设很大趋近于 1,代表比较不重要,之前算出来的梯度比较重要。
同样的过程就反复继续下去。
RMSProp 通过 α 可以决定,相较于之前存在
里面的
的重要性有多大。如果使用 RMSprop,就可以动态调整
这一项。下图中黑线是误差表面,球就从 A 走到 B,AB 段的路很平坦,g 很小,更新参数的时候,我们会走比较大的步伐。走动BC 段后梯度变大了,AdaGrad 反应比较慢,而 RMSprop 会把 α 设小一点,让新的、刚看到的梯度的影响比较大,很快地让
的值变大,很快地让步伐变小,RMSprop 可以很快地“踩刹车”。如果走到 CD 段,CD 段是平坦的地方,可以调整 α,让其比较看重最近算出来的梯度,梯度一变小,
的值就变小了,走的步伐就变大了。
2.学习率调度
如下图所示简单的误差表面,我们都训练不起来,加上自适应学习率以后,使用AdaGrad 方法优化的结果如图 所示。一开始优化的时候很顺利,在左转的时候,有 Ada-Grad 以后,可以再继续走下去,走到非常接近终点的位置。走到 BC 段时,因为横轴方向的梯度很小,所以学习率会自动变大,步伐就可以变大,从而不断前进。接下来的问题走到图中红圈的地方,快走到终点的时候突然“爆炸”了。是把过去所有的梯度拿来作平均。在 AB段梯度很大,但在 BC 段,纵轴的方向梯度很小,因此纵轴方向累积了很小的
,累积到一定程度以后,步伐就变很大,但有办法修正回来。因为步伐很大,其会走到梯度比较大的地方。走到梯度比较大的地方后,
会慢慢变大,更新的步伐大小会慢慢变小,从而回到原来的路线。
通过学习率调度可以解决这个问题。之前的学习率调整方法中 η 是一个固定的值,而在学习率调度中 η 跟时间有关,如式(10)所示。学习率调度中最常见的策略是学习率衰减,也称为学习率退火。
(10)
随着参数的不断更新,让 η 越来越小,如果加上学习率下降,可以很平顺地走到终点,如下图所示,在红圈的地方,虽然步伐很大,但 η 变得非常小,步伐乘上 η 就变小了,就可以慢慢地走到终点。
除了学习率下降以外,还有另外一个经典的学习率调度的方式———预热。预热的方法是让学习率 先变大后变小,至于变到多大、变大的速度、变小的速度是超参数。残差网络[8] 里面是有预热的,在残差网络里面,学习率先设置成 0.01,再设置成 0.1,并且其论文还特别说明,一开始用 0.1 反而训练不好。除了残差网络,BERT 和 Transformer 的训练也都使用了预热。
3.优化总结
所以我们从最原始的梯度下降,进化到这一个版本
(11)
其中是动量。这个是目前优化的完整的版本,这种优化器除了 Adam 以外,还有各种变形。但其实各种变形是使用不同的方式来计算
或
,或者是使用不同的学习率调度的方式。
4.分类与回归
分类与回归是深度学习最常见的两种问题。
4.1分类与回归的关系
回归是输入一个向量 x,输出 yˆ,我们希望 yˆ 跟某一个标签 y 越接近越好,y 是要学习的目标。而分类可当作回归来看,输入 x 后,输出仍然是一个标量 yˆ,要让它跟正确答案的那个类越接近越好。yˆ 是一个数字,我们可以把类也变成数字。实际上,在做分类的问题的时候,比较常见的做法是用独热向量表示类。
如果有三个类,标签 y 就是一个三维的向量,比如类 1 是,类 2 是
,类3是
.如果每个类都用一个独热向量来表示,就没有类 1 跟类 2 比较接近,类 1 跟类 3 比较远的问题。如果用独热向量计算距离的话,类两两之间的距离都是一样的。如果目标 y 是一个向量,比如 y 是有三个元素的向量,网络也要输出三个数字才行。如图所示,输出三个数值就是把本来输出一个数值的方法,重复三次。把
乘上三个不同的权重,加上偏置,得到 yˆ1;再把
乘上另外三个权重,再加上另外一个偏置得到 yˆ2;把
再乘上另外一组权重,再加上另外一个偏置得到 yˆ3。输入一个特征向量,产生
跟目标越接近越好。
4.2带有softmax的分类
按照上述的设定,分类实际过程是:输入 x,乘上 W,加上 b,通过激活函数 σ,乘上W′,再加上 b′ 得到向量 yˆ。但实际做分类的时候,往往会把 yˆ 通过 softmax 函数得到 y′,才去计算 y′ 跟 yˆ 之间的距离。
softmax 的计算如式(12)所示,先把所有的 y 取一个指数,,再对其做归一化得到 y′。
(12)
下图是softmax 的块,输入,产生
。比如
,取完指数的时候,exp(3) = 20、exp(1) = 2.7 和 exp(−3) = 0.05,做完归一化后,就变成 0.88、0.12 跟 0。−3取完指数,再做归一化以后,会变成趋近于 0 的值。所以 softmax 除了归一化,让 y′1、y′2 和y′3,变成 0 到 1 之间,和为 1 以外,它还会让大的值跟小的值的差距更大。
上图考虑了三个类的状况,两个类也可以直接套 softmax 函数。但一般有两个类的时候,我们不套 softmax,而是直接取 sigmoid。当只有两个类的时候,sigmoid 和 softmax 是等价的。
4.3分类损失
当我们把 x 输入到一个网络里面产生 yˆ 后,通过 softmax 得到 y′,再去计算 y′ 跟 y 之间的距离 e,如下图所示
计算 y′ 跟 y 之间的距离不只一种做法,可以是如式 (13) 所示的均方误差,即把 y 里面每一个元素拿出来,计算它们的平方和当作误差。
(13)
但是如式(14)所示的交叉熵更常用,当 yˆ 跟 y′ 相同时,可以最小化交叉熵的值,此时均方误差也是最小的。最小化交叉熵其实就是最大化似然
(14)
接下来从优化的角度来说明相较于均方误差,交叉熵是被更常用在分类上。如图 3.35 所示,有一个三类的分类,网络先输出,在通过 softmax 以后,产生
。假设正确答案是
,要计算
跟
之间的距离 e,e 可以是均方误差或交叉熵。假设 y1 的变化是从-10 到 10,y2 的变化也是从-10 到 10,y3 就固定设成-1000。因为 y3 的值很小,通过 softmax 以后,y′3 非常趋近于 0,它跟正确答案非常接近,且它对结果影响很少。总之,我们假设 y3 设一个定值,只看 y1 跟 y2 有变化的时候,对损失 e 的影响。
下图是分别在 e 为均方误差和交叉熵时,y1、y2 的变化对损失的影响,对误差表面的影响,红色代表损失大,蓝色代表损失小。如果 y1 很大,y2 很小,代表 y′1 会很接近 1,y′2会很接近 0。所以不管 e 取均方误差或交叉熵,如果 y1 大、y2 小,损失都是小的;如果 y1小,y2 大,y′1 是 0,y′2 是 1,这个时候损失会比较大。
上图中左上角损失大,右下角损失小,所以期待最后在训练的时候,参数可以“走”到右下角的地方。假设参数优化开始的时候,对应的损失都是左上角。如果选择交叉熵,如图(a) 所示,左上角圆圈所在的点有斜率的,所以可以通过梯度,一路往右下的地方“走”;如果选均方误差,如图(b) 所示,左上角圆圈就卡住了,均方误差在这种损失很大的地方,它是非常平坦的,其梯度是非常小趋近于 0 的。如果初始时在圆圈的位置,离目标非常远,其梯度又很小,无法用梯度下降顺利地“走”到右下角。
因此做分类时,选均方误差的时候,如果没有好的优化器,有非常大的可能性会训练不起来。如果用 Adam,虽然图(b) 中圆圈的梯度很小,但 Adam 会自动调大学习率,还有机会走到右下角,不过训练的过程比较困难。总之,改变损失函数可以改变优化的难度。
实践:HW3(CNN)卷积神经网络-图像分类
1.获得的数据集和代码文件
先进入魔搭社区,创建一个实例,启动,然后点击进入JupyterLab。接着,点击“Terminal”打开命令行窗口,输入以下代码并按下回车键。稍等片刻,数据集和代码文件(notebook)将会自动下载,大约需要一分钟。
git clone https://www.modelscope.cn/datasets/Datawhale/LeeDL-HW3-CNN.git
初始化追踪器:stale 和 best_acc 用于追踪训练过程中的损失和准确率。stale 表示连续没有改进的轮数,当 stale 大于设定的阈值 patience 时,提前停止训练。
训练阶段:在训练阶段,首先确保模型处于训练模式,然后遍历训练数据加载器 train_loader 中的每个批次。对于每个批次,将图像数据 imgs 和对应的标签 labels 传递给模型,计算输出 logits。然后计算交叉熵损失 loss,并清除上一步中参数中存储的梯度。计算参数的梯度,并进行梯度裁剪以稳定训练。最后更新模型参数。
验证阶段:在验证阶段,首先确保模型处于评估模式,然后遍历验证数据加载器 valid_loader 中的每个批次。对于每个批次,将图像数据 imgs 和对应的标签 labels 传递给模型,计算输出 logits。计算损失 loss 和准确率 acc。
打印训练和验证信息:在训练和验证阶段,打印当前轮次的损失和准确率。在验证阶段,如果当前轮次的准确率高于最佳准确率 best_acc,则更新 best_acc 和保存模型。
保存模型:在训练过程中,如果找到更好的模型,则保存模型参数。
![](https://i-blog.csdnimg.cn/direct/9274f9f183c948589ddf0954ac94dcd5.png)
![](https://i-blog.csdnimg.cn/direct/b3cc49f31c7b43329e6cb44054678de1.png)
![](https://i-blog.csdnimg.cn/direct/234d62964da04d71af96ffc178fc55a5.png)
2.一键运行代码
初始化追踪器,这些不是参数,不应该被更改
stale = 0
best_acc = 0
for epoch in range(n_epochs):
# ---------- 训练阶段 ----------
# 确保模型处于训练模式
model.train()
# 这些用于记录训练过程中的信息
train_loss = []
train_accs = []
for batch in tqdm(train_loader):
# 每个批次包含图像数据及其对应的标签
imgs, labels = batch
# imgs = imgs.half()
# print(imgs.shape,labels.shape)
# 前向传播数据。(确保数据和模型位于同一设备上)
logits = model(imgs.to(device))
# 计算交叉熵损失。
# 在计算交叉熵之前不需要应用softmax,因为它会自动完成。
loss = criterion(logits, labels.to(device))
# 清除上一步中参数中存储的梯度
optimizer.zero_grad()
# 计算参数的梯度
loss.backward()
# 为了稳定训练,限制梯度范数
grad_norm = nn.utils.clip_grad_norm_(model.parameters(), max_norm=10)
# 使用计算出的梯度更新参数
optimizer.step()
# 计算当前批次的准确率
acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()
# 记录损失和准确率
train_loss.append(loss.item())
train_accs.append(acc)
train_loss = sum(train_loss) / len(train_loss)
train_acc = sum(train_accs) / len(train_accs)
# 打印信息
print(f"[ 训练 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {train_loss:.5f}, acc = {train_acc:.5f}")
# ---------- 验证阶段 ----------
# 确保模型处于评估模式,以便某些模块如dropout能够正常工作
model.eval()
# 这些用于记录验证过程中的信息
valid_loss = []
valid_accs = []
# 按批次迭代验证集
for batch in tqdm(valid_loader):
# 每个批次包含图像数据及其对应的标签
imgs, labels = batch
# imgs = imgs.half()
# 我们在验证阶段不需要梯度。
# 使用 torch.no_grad() 加速前向传播过程。
with torch.no_grad():
logits = model(imgs.to(device))
# 我们仍然可以计算损失(但不计算梯度)。
loss = criterion(logits, labels.to(device))
# 计算当前批次的准确率
acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()
# 记录损失和准确率
valid_loss.append(loss.item())
valid_accs.append(acc)
# break
# 整个验证集的平均损失和准确率是所记录值的平均
valid_loss = sum(valid_loss) / len(valid_loss)
valid_acc = sum(valid_accs) / len(valid_accs)
# 打印信息
print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")
# 更新日志
if valid_acc > best_acc:
with open(f"./{_exp_name}_log.txt", "a"):
print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f} -> 最佳")
else:
with open(f"./{_exp_name}_log.txt", "a"):
print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")
# 保存模型
if valid_acc > best_acc:
print(f"在第 {epoch} 轮找到最佳模型,正在保存模型")
torch.save(model.state_dict(), f"{_exp_name}_best.ckpt") # 只保存最佳模型以防止输出内存超出错误
best_acc = valid_acc
stale = 0
else:
stale += 1
if stale > patience:
print(f"连续 {patience} 轮没有改进,提前停止")
break
在运行过程中会遇到
以下是我运行后的结果
3.关闭实例
运行完成后记得关闭实例。
4.“泛化”模型的推导
声明
本学习笔记为此笔记发布者使用,以便完成Datawhale AI 夏令营第五期Task02打卡任务,同时记录自己的学习过程。该笔记在原文的基础上进行删改,对个别部分用黑体或者其他字体着重强调以便及时记忆理解。本笔记并不做任何以盈利为目的的用途,仅供个人学习使用,若有侵权,请第一时间告知本人,侵删。