关于loss和优化器的细节!👕

loss和优化器的细节

1.梯度累计过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import torch

# 输入和期望输出
x = torch.ones(5) # input tensor
w0 = [[1, 2, 3] for _ in range(5)]
y = torch.zeros(3) # expected output

# 初始化参数
w = torch.tensor(w0, dtype=torch.float32, requires_grad=True)
b0 = [1, 2, 3]
b = torch.tensor(b0, dtype=torch.float32, requires_grad=True)

# 设置优化器和学习率
optimizer = torch.optim.SGD([w, b], lr=0.0009)

# 训练循环
num_epochs = 10000
for epoch in range(num_epochs):
optimizer.zero_grad() # 梯度清零
z = torch.matmul(x, w) + b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y)
loss.backward() # 反向传播计算梯度
torch.nn.utils.clip_grad_norm_([w, b], max_norm=1.0)
optimizer.step() # 更新参数

# print('internal w:', w)
# print('internal b:', b)
# print('internal z:', z)
# print('internal loss:', loss)

if epoch % 500 == 0: # 每100个epoch打印一次损失
print(f'Epoch {epoch}, Loss: {loss.item()}')
print(z)

# 打印最终结果
print('Final w:', w)
print('Final b:', b)



  • w,y在上述代码中为自定义张量,在模型中为某一层中随机初始化的张量
  • 该代码验证了反向传播算法对参数的影响
  • 可以优化的参数为带有requires_grad=True的变量,且变量类型为浮点型
  • 优化器的初始化要设置需要优化的参数,在这里是[w,b],在实际模型训练过程中改为model.parameters()
  • 学习率对实验结果影响较大,影响学习的速度,学习率越大,epoch可以越小
  • 在每一次循环之前要将梯度清零,否则每一次loss.backward都会让梯度累计,查看梯度使用w.grad
  • 使用 torch.nn.utils.clip_grad_norm_ 对梯度进行裁剪,防止梯度爆炸。这里设置最大范数 max_norm1.0,否则,当epoch过多时,z会越来越小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import torch

x = torch.ones(5) # input tensor
w0 = [[1, 2, 3] for _ in range(5)]
y = torch.zeros(3) # expected output
w = torch.tensor(w0, dtype=torch.float32,requires_grad=True)
b0 = [1, 2, 3]
b = torch.tensor(b0, dtype=torch.float32, requires_grad=True)
z = torch.matmul(x, w) + b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y)

loss.backward()
print(w.grad)
print(b.grad)

x1 = torch.ones(5) # input tensor
z1 = torch.matmul(x1, w) + b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z1, y)

loss.backward()
print(w.grad)
print(b.grad)


# 结果
tensor([[0.3325, 0.3333, 0.3333],
[0.3325, 0.3333, 0.3333],
[0.3325, 0.3333, 0.3333],
[0.3325, 0.3333, 0.3333],
[0.3325, 0.3333, 0.3333]])
tensor([0.3325, 0.3333, 0.3333])
tensor([[0.6650, 0.6667, 0.6667],
[0.6650, 0.6667, 0.6667],
[0.6650, 0.6667, 0.6667],
[0.6650, 0.6667, 0.6667],
[0.6650, 0.6667, 0.6667]])
tensor([0.6650, 0.6667, 0.6667])

如上所示,每调用一次loss.backward(),梯度就会累计(直接相加)。

用随机数种子来保留每次随机初始化的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import torch

# 设置随机数种子
torch.manual_seed(42)

# 输入和期望输出
x = torch.ones(5) # input tensor
y = torch.zeros(3) # expected output

# 初始化参数 w 和 b 为随机值
w = torch.randn(5, 3, dtype=torch.float32, requires_grad=True)
b = torch.randn(3, dtype=torch.float32, requires_grad=True)

# 设置优化器和学习率
optimizer = torch.optim.SGD([w, b], lr=0.01) # 降低学习率

# 训练循环
num_epochs = 1000
for epoch in range(num_epochs):
optimizer.zero_grad() # 梯度清零
z = torch.matmul(x, w) + b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y)
loss.backward() # 反向传播计算梯度

# 梯度裁剪,防止梯度爆炸
torch.nn.utils.clip_grad_norm_([w, b], max_norm=1.0)

optimizer.step() # 更新参数

if epoch % 1000 == 0: # 每1000个epoch打印一次损失
print(f'Epoch {epoch}, Loss: {loss.item()}')
print(f'z: {z.detach().numpy()}')

# 打印最终结果
print('Final w:', w)
print('Final b:', b)

2.梯度下降原理

1
2
3
4
5
6
7
8
9
10
11
def train(category_tensor, line_tensor):
hidden = rnn.initHidden()

rnn.zero_grad()

for i in range(line_tensor.size()[0]):
output, hidden = rnn(line_tensor[i], hidden)
# print(rnn.parameters())

loss = criterion(output, category_tensor)
loss.backward()

在上述代码中,output经过多次的rnn,则其计算图为output = {[(i1+h1)\*w+b+i2]\*w+b+i3}\*w+b

w,b在计算图中出现多次,则在loss.backward中,根据下列原则

损失函数中包含了output的整个表达式,所以在计算梯度时会应用相应的求导方式。

结论:

调用一次RNN和多次调用RNN结果不同。

3.loss累加反向传播

在用rnn处理序列任务时,需要累加不同output的loss进行一次反向传播

单个时间步反向传播和最后一起反向传播的区别主要在于参数更新的时机和计算效率。

  1. 单个时间步反向传播
    • 在每个时间步都进行反向传播,即每个时间步都计算损失并更新参数。
    • 优点是可以更快地更新模型参数,因为每个时间步都有梯度信息来更新参数。
    • 缺点是计算开销较大,因为需要在每个时间步都进行一次前向传播和一次反向传播。
  2. 最后一起反向传播
    • 在整个序列处理完毕后,再进行一次反向传播。
    • 优点是计算效率高,因为只需要进行一次前向传播和一次反向传播,节省了计算资源。
    • 缺点是可能会出现梯度消失或爆炸的问题,因为在长序列中梯度信息可能会衰减或者放大。

通常情况下,如果序列长度不是特别长,可以考虑使用单个时间步反向传播;而如果序列长度较长,为了减少计算量,可以考虑最后一起反向传播。此外,还有一些方法,如使用门控循环单元(GRU)或长短期记忆网络(LSTM)等,可以在长序列中更好地处理梯度消失或爆炸的问题。

4.with torch.no_grad()的好处

在推理阶段,如果不使用 torch.no_grad(),虽然模型的参数不会自动改变,但会产生一些不必要的计算开销和潜在的风险。以下是详细说明:

无需梯度计算的开销

在前向传播过程中,PyTorch 会跟踪所有计算以便于后续的反向传播。这会消耗额外的内存来存储计算图。如果你在推理阶段不使用 torch.no_grad(),PyTorch 仍然会跟踪这些操作并构建计算图,导致内存使用的增加。

意外的反向传播风险

尽管你在推理阶段不打算进行反向传播,但如果你不使用 torch.no_grad(),模型的参数和输入仍然会被记录到计算图中。如果在推理过程中你意外调用了 loss.backward(),这会导致梯度计算和潜在的参数更新(如果你之后调用 optimizer.step())。

5.不同优化器以及adam优化器

不同优化器有不同的优化策略,主要是在更新模型参数时的方式上有所不同。以下是几种常见的优化器及其优化策略:

  1. 梯度下降法(Gradient Descent):是最基本的优化算法之一。它根据损失函数对模型参数的梯度方向,以一定的步长更新参数,使损失函数逐渐减小。缺点是可能收敛速度慢,容易陷入局部最优解。
  2. 随机梯度下降法(Stochastic Gradient Descent,SGD):与梯度下降类似,但每次更新参数时只使用一个样本的梯度。因为每次只考虑一个样本,收敛速度较快,但可能导致参数更新的方差较大,甚至无法收敛到最优解。
  3. 小批量随机梯度下降法(Mini-batch Stochastic Gradient Descent):是SGD的一种改进版本,每次更新参数时使用一个小批量(mini-batch)的样本来计算梯度。它综合了梯度下降的稳定性和SGD的收敛速度。
  4. 动量(Momentum):在更新参数时引入了动量项,模拟物体在运动中的惯性,使得更新方向不仅取决于当前的梯度,还取决于历史梯度的加权平均。这样可以加速收敛并减少震荡。
  5. AdaGrad:自适应学习率算法,对每个参数应用不同的学习率,学习率随着参数的更新而逐渐减小。对于频繁出现的参数,学习率将减小得更快,对于不频繁出现的参数,学习率将减小得更慢。
  6. RMSProp(Root Mean Square Propagation):与AdaGrad类似,但对学习率的累积进行了指数加权移动平均,以减少学习率的快速降低。
  7. Adam(Adaptive Moment Estimation):结合了动量和自适应学习率的优点,同时使用了动量的指数衰减平均和梯度的指数衰减平方平均来更新参数。通常被认为是最有效的优化算法之一。

每种优化器都有其适用的场景,通常需要根据具体问题和数据集的特点选择合适的优化器。

Adam(Adaptive Moment Estimation)优化器是一种结合了动量(momentum)和自适应学习率(adaptive learning rate)的优化算法。它在更新参数时同时考虑了梯度的一阶矩估计(梯度的平均值)和二阶矩估计(梯度的平方的平均值),并通过偏置校正来纠正估计的偏差。

具体来说,Adam算法的更新规则如下:

  1. 初始化参数 $θ$ 和一阶矩估计 $m$ 和二阶矩估计 $v$,通常初始化为0。

  2. 在每次迭代中,计算梯度 $g_t$。

  3. 更新一阶矩估计 $m_t$ 和二阶矩估计 $v_t$:

    mt=β1mt−1+(1−β1)gtm_t = β_1m_{t-1} + (1-β_1)g_tmt=β1mt−1+(1−β1)gt

    vt=β2vt−1+(1−β2)gt2v_t = β_2v_{t-1} + (1-β_2)g_t^2vt=β2vt−1+(1−β2)gt2

    其中,$β_1$ 和 $β_2$ 是用于控制一阶矩估计和二阶矩估计的指数衰减率,通常设定为接近1的值,例如0.9 和 0.999。

  4. 根据一阶矩估计和二阶矩估计来更新参数:

    θt+1=θt−αvt+ϵ⋅mtθ_{t+1} = θ_t - \frac{α}{\sqrt{v_t}+\epsilon} \cdot m_tθt+1=θt−vt+ϵα⋅mt

    其中,$α$ 是学习率,$\epsilon$ 是为了数值稳定性而添加的小常数,通常设为 $10^{-8}$。

Adam优化器的优点在于能够在不同参数的梯度变化范围很大的情况下自适应地调整学习率,同时结合了动量的优点,能够在训练过程中保持稳定性并且有效地收敛到局部最优解。因此,Adam优化器在深度学习中被广泛使用,并且通常是默认的优化器之一。


关于loss和优化器的细节!👕
https://yangchuanzhi20.github.io/2024/05/31/人工智能/Pytorch/使用细节/关于loss和优化器的细节/
作者
白色很哇塞
发布于
2024年5月31日
许可协议