上一节我们建立了一个简单的模型进行分析散点图,利用均方差来实现损失函数的计算,但是并没有计算出具体的参数值,这次我们来计算损失函数的损失值以及不断减小损失值,计算出最优的参数,代码原理非常简单大家可以自行理解查阅资料
import numpy as np
import torch
import time
torch.set_printoptions(edgeitems=2)
t_c = torch.tensor([0.5, 14.0, 15.0, 28.0, 11.0, 8.0,
3.0, -4.0, 6.0, 13.0, 21.0])
t_u = torch.tensor([35.7, 55.9, 58.2, 81.9, 56.3, 48.9,
33.9, 21.8, 48.4, 60.4, 68.4])
t_un = 1 * t_u
def model(t_u, w, b):
return w * t_u + b
def loss_fn(t_p, t_c):
squared_diffs = (t_p - t_c)**2
return squared_diffs.mean()
params = torch.tensor([1.0, 0.0], requires_grad=True)
loss = loss_fn(model(t_u, *params), t_c)
loss.backward()
if params.grad is not None:
params.grad.zero_()
def training_loop(n_epochs, learning_rate, params, t_u, t_c):
for epoch in range(1, n_epochs + 1):
if params.grad is not None: # <1>
params.grad.zero_()
t_p = model(t_u, *params)
loss = loss_fn(t_p, t_c)
loss.backward()
with torch.no_grad(): # <2>
params -= learning_rate * params.grad
if epoch:
# time.sleep(0.2)
print('Epoch %d, Loss %f' % (epoch, float(loss)))
print('params:',params)
return params
training_loop(
n_epochs = 10000,
learning_rate = 1e-2,
params = torch.tensor([1.0, 0.0], requires_grad=True), # <1>
t_u = t_un, # <2>
t_c = t_c)
我来简单说一下代码的思想,代码中规定了一个model()函数用于计算出输入参数得到的预测值,也就是t_p,把预测的值存在t_p中,然后利用t_p与实际值也就是t_c计算出一个方差,loss_fn()函数的作用就是计算预测值与实际值之间的方差,由于我们使用梯度下降算法,所以每次计算完都需要把梯度重新归零防止梯度累加,training_loop()这个函数作用就是循环训练,并更新所有训练的参数,接下来,我们运行代码
实际上,我们得到的损失值竟然在快速变大,越来越离谱,发生了什么呢?也就是出现我们经常说的梯度爆炸的现象
梯度爆炸
梯度爆炸是指在训练深度神经网络时,梯度(即损失函数对参数的导数)变得异常大,导致参数更新幅度过大,破坏了模型的稳定性,并可能使损失函数值急剧增加。这通常发生在以下几种情况:
-
不合适的初始学习率:如果学习率设置得过高,可能会导致在每次参数更新时,参数的改变量过大,从而使得损失函数值快速增加。
-
激活函数选择不当:某些激活函数(如Sigmoid或Tanh)在极端值(如非常大或非常小的输入)下可能会导致梯度消失或梯度爆炸。虽然这种情况更常见于梯度消失,但在某些情况下,特别是与其他因素结合时,也可能导致梯度爆炸。
-
深度过深的网络:非常深的网络可能导致梯度在反向传播过程中累积放大,特别是当每一层的梯度都比较大时。
-
损失函数或优化器设置不当:某些损失函数或优化器可能在特定条件下表现不佳,导致梯度不稳定。
针对你的问题,你可以尝试以下几种方法来解决梯度爆炸问题:
-
降低学习率:使用更小的学习率可以减缓参数更新的速度,从而减少梯度爆炸的风险。
-
使用梯度裁剪(Gradient Clipping):在梯度更新之前,对梯度值进行裁剪,确保梯度的最大绝对值不超过某个阈值。
-
更换激活函数:尝试使用ReLU及其变体(如Leaky ReLU、PReLU等),这些激活函数在大多数情况下能更好地缓解梯度消失或爆炸的问题。
-
简化网络结构:如果可能的话,尝试简化网络结构,减少网络深度或宽度,以减少梯度累积放大的可能性。
-
更换优化器:某些优化器(如Adam、RMSprop等)自带了一定的梯度缩放机制,可能更适合你的任务。
-
正则化:通过添加L1或L2正则化项来限制参数的更新幅度,这有助于保持参数的稳定性。
-
数据预处理:确保输入数据已经过适当的预处理(如归一化、标准化等),以避免由于数据规模不当导致的梯度问题。
实际上也就是发生了过度修正的现象,就是每次修正的步长太大了,导致参数接收到的更新太大了,参数开始来回波动,每一次的修正过度,就会导致下一次的修正也发生过度的现象,优化不稳定,反而是其变得发散而不是收敛,从损失值快速变大就可以发现,问题显而易见,params -= learning_rate * params.grad这行代码所导致的,那我们如何优化来限制它的大小呢,调整学习率和梯度
调整学习率
我们可以调整一下学习率,较小的学习率会使得损失值慢慢减小,我们修改一下学习率继续实验
training_loop(
n_epochs = 10000,
learning_rate = 1e-4,
params = torch.tensor([1.0, 0.0], requires_grad=True), # <1>
t_u = t_un, # <2>
t_c = t_c)
不难发现,和上一次的结果截然相反,这个损失值慢慢减小,直到训练轮数的不断提升,参数和损失值都趋于稳定,由于学习率降低了,从原理出发,我们需要提高训练的轮数来提高结果的稳定,我这里训练轮数改为了10w轮,看看结果吧
可以看到损失值变化非常小,如果加大轮数很容易得到一个最近似的参数值
我们除了调整学习率之外,还可以进行梯度的调整,我们采用的方法叫做归一化输入
归一化输入
从第一次运行完代码的图片中可以发现,权重w的梯度与偏置b的梯度相差大概50倍,这很好地说明权重与偏置的存在于不同的比例空间中,在这中情况下如果学习率的值偏大,很容易导致只能有效更新其中一个参数,对于另外一个参数而言,学习率就会变得不稳定,无法对其进行有效的更新,每个参数都有自己的学习率,他们彼此是独立的,除非参数之间相差不大,但是为了得到准确的结果,还是建议使用适合参数自身的学习率,可以有效的保证模型训练的精度,这里的话小编就不介绍复杂的归一化算法,我们就使用最简单的归一化输入算法。
那我们改变输入,这样梯度就不会有太大的不同,也就是对输入的值进行缩放,仅仅用作计算,最后输出把倍率放回去就行,我们采用对x进行缩小成原来的0.1倍
t_un = 0.1 * t_u
我自己设置的是训练1k轮,看看效果吧
上面10w轮损失值才到3.7,而我归一化处理以后1k就可以到达3.8,可见效率大大提升了。正常的网络当中的参数都是百万级别的,我们仅仅用了最简单的两个办法就可以大大加快了训练的效率,在真实的训练当中我们需要花费大量的时间在进行网络的训练,有两许多优化器以及许多优化算法,可以大大减少了训练的时间,这在深度学习中非常重要。
最后我们来看看计算出来的参数的可视化效果
可视化
params =training_loop(
n_epochs = 10000,
learning_rate = 1e-2,
params = torch.tensor([1.0, 0.0], requires_grad=True),
t_u = t_un,
t_c = t_c)
from matplotlib import pyplot as plt
t_p = model(t_un, *params)
fig = plt.figure(dpi=100)
plt.xlabel("Temperature (°Fahrenheit)")
plt.ylabel("Temperature (°Celsius)")
plt.plot(t_u.numpy(), t_p.detach().numpy()) # <2>
plt.plot(t_u.numpy(), t_c.numpy(), 'o')
plt.show()