验证误差反向传播算法

给出结论:误差反向传播算法,传播的是损失函数对各个参数的偏导数(就是导数),也可以简单理解成梯度(梯度的定义可不是这样的)

大致流程是搭一个简单网络,找到里面的各个参数,训练一次网络并进行误差反向传播,再次查看各个参数,验证结果,以下是详细过程。

这里我们新建一个简化的网络,输入层、隐藏层和输出层都只设置一个节点,并且不加激励函数,因为像relu这种函数会过掉小于0的数据,影响计算

import torch

# 搭建神经网络
class Net(torch.nn.Module):
    def __init__(self, n_feature, n_hidden, n_output):
        super().__init__()
        self.hidden = torch.nn.Linear(n_feature, n_hidden)
        self.predict = torch.nn.Linear(n_hidden, n_output)

    def forward(self, x): # 前向计算
#         x = F.relu(self.hidden(x))
        x = self.hidden(x)
        x = self.predict(x)
        return x
        
net = Net(1, 1, 1)
print(net)

# Net(
#   (hidden): Linear(in_features=1, out_features=1, bias=True)
#   (predict): Linear(in_features=1, out_features=1, bias=True)
# )

查看一下网络里的权重和偏置:

for name, param in net.named_parameters():
    print('-->name:', name, '-->grad_requirs:', param.requires_grad, ' -->grad_value:', param.grad)
    for p in param:
        print(p)
    print('----------------')

打印结果:

-->name: hidden.weight -->grad_requirs: True  -->grad_value: None
tensor([-0.6566], grad_fn=<UnbindBackward>)
----------------
-->name: hidden.bias -->grad_requirs: True  -->grad_value: None
tensor(-0.1939, grad_fn=<UnbindBackward>)
----------------
-->name: predict.weight -->grad_requirs: True  -->grad_value: None
tensor([0.9205], grad_fn=<UnbindBackward>)
----------------
-->name: predict.bias -->grad_requirs: True  -->grad_value: None
tensor(0.1605, grad_fn=<UnbindBackward>)
----------------

可以看到网络里一共4个参数,分别是隐藏层hidden和输出层predict的权重和偏置,这里我们记输入层节点为x、隐藏层节点为h、输入层为p,隐藏层hidden的权重和偏置为w1、b1,输出层predict的权重和偏置为w2(此时w2的值为0.9205)、b2,就可以有以下两个公式(各个层的输出结果):

  • 隐藏层hidden输出结果为:h = x * w1 + b1
  • 输出层predict输出结果为(也是神经网络的预测值):p = h * w2 + b2 = (x * w1 + b1) * w2 + b2

准备数据,定义优化器、损失函数,并训练一次网络,最后查看参数(这里优化器采用常用的SGD,损失函数采用均方误差MSELoss,且为了方便,x的数据元素也只有一个)

# x 的数据之生成一个,方便计算和观察
x = torch.unsqueeze(torch.linspace(-1, 1, 1), dim=1)
y = x.pow(2) + 0.2 * torch.rand(x.size())

# 优化器和损失函数,学习率lr设为0.1
omptimzer = torch.optim.SGD(net.parameters(), lr=0.1)
loss_func = torch.nn.MSELoss()

prediction = net(x)    # 带入x,执行一次网络,得到预测值p
loss = loss_func(prediction, y)  # 计算均方误差

omptimzer.zero_grad()  # 先将所有参数的梯度每次置零
loss.backward()        # 计算所有节点的梯度
omptimzer.step()       # 用上面的计算值,和学习率更新网络参数
    
# 打印一些参数
print(f'x:{x.data.numpy()}, y:{y.data.numpy()}')
print(f'pred:{prediction.tolist()}, loss:{loss.tolist()}\n')

for name, param in net.named_parameters():
    print('-->name:', name, '-->grad_requirs:', param.requires_grad, ' -->grad_value:', param.grad)
    for p in param:
        print(p)
    print('----------------')

打印结果:

x:[[-1.]], y:[[1.1921173]]
pred:[[0.5864818096160889]], loss:0.3667943775653839

-->name: hidden.weight -->grad_requirs: True  -->grad_value: tensor([[1.1150]])
tensor([-0.7681], grad_fn=<UnbindBackward>)
----------------
-->name: hidden.bias -->grad_requirs: True  -->grad_value: tensor([-1.1150])
tensor(-0.0824, grad_fn=<UnbindBackward>)
----------------
-->name: predict.weight -->grad_requirs: True  -->grad_value: tensor([[-0.5605]])
tensor([0.9766], grad_fn=<UnbindBackward>)
----------------
-->name: predict.bias -->grad_requirs: True  -->grad_value: tensor([-1.2113])
tensor(0.2817, grad_fn=<UnbindBackward>)
----------------

可以看到 -->grad_value 后跟着一个tensor值,这个值就是当前变量的梯度(w2的梯度值为-0.5605,w2更新之后的值为0.9766),下面开始验证:

没有训练之前各个参数的值为:

  • w1 = -0.6566; b1 = -0.1939; w2 = 0.9205; b1 = 0.1605
  • 计算 h = x * w1 + b1 = (-1) * (-0.6566) + (-0.1939) = 0.4627
  • 同理计算 p = h * w2 + b2 = 0.58641535 (和打印值0.5864818096160889差距不大,是由于w1、b1、w2、b2只有4位小数导致的,问题不大),这里验证了神经网络的前向计算过程

开始验证梯度反向传播过程:

  • loss_func = torch.nn.MSELoss() 损失函数公式为 loss(a, b) = s u m ( i = 1 ) n ( a i − b i ) 2 sum_(i=1)^n (a_i - b_i)^2 sum(​i=1)n(ai​−bi​)2表示的是所有a、b之差的平方和,再除以个数
  • 由于这里的x、y、p这些矩阵里的元素只有,即n=1,所以这里 loss_func(p, y)= ( p − y ) 2 (p - y)^2 (p−y)2 ,所以可以求w2的梯度为: ∂ l o s s ∂ w 2 = 2 ( p − y ) ∂ p ∂ w 2 \frac{\partial loss}{\partial w2} = 2(p - y)\frac{\partial p}{\partial w2} ∂w2∂loss​=2(p−y)∂w2∂p​ = 2 * (0.5864 - 1.1921) * 0.4627 = -0.56051478 和 打印值-0.5605一模一样,验证了梯度的计算过程(同样可以验证其他的参数)
  • 接下来验证梯度更新: w = w − l r ∗ ∂ l o s s ∂ w w = w - lr * \frac{\partial loss}{\partial w} w=w−lr∗∂w∂loss​ 所以w2 = w2 - lr * w2的梯度 = 0.9205 - 0.1 * (-0.5605) = 0.97655 和打印结果0.9766一模一样,这里就验证了网络里的各个参数的更新过程

到这里就已经结束了,其实可以看到往后传播的是损失函数对各个参数的梯度,而不是计算出来的误差值loss(均方误差),当然也只验证了这一例子,如果有错,可在评论区讨论

其实后面可以再添加一段训练网络的代码(将omptimzer.zero_grad()注释掉),就可以验证,每次计算的梯度是会累加到grad属性上来,这也就说明了,为什么在每一轮训练是都要就梯度置0了,这里附上代码,供需要的人验证

#!/usr/bin/env python
import torch

# 搭建神经网络
class Net(torch.nn.Module):
    def __init__(self, n_feature, n_hidden, n_output):
        super().__init__()
        self.hidden = torch.nn.Linear(n_feature, n_hidden)
        self.predict = torch.nn.Linear(n_hidden, n_output)

    def forward(self, x):  # 前向计算
        #         x = F.relu(self.hidden(x))
        x = self.hidden(x)
        x = self.predict(x)
        return x

# 打印网络结构
net = Net(1, 1, 1)
print(net)

# 查看一下网络里的权重和偏置
for name, param in net.named_parameters():
    print('-->name:', name, '-->grad_requirs:', param.requires_grad, ' -->grad_value:', param.grad)
    for p in param:
        print(p)
    print('----------------')

# 生成数据
# x 的数据之生成一个,方便计算和观察
x = torch.unsqueeze(torch.linspace(-1, 1, 1), dim=1)
y = x.pow(2) + 0.2 * torch.rand(x.size())

# 优化器和损失函数,学习率lr设为0.1
omptimzer = torch.optim.SGD(net.parameters(), lr=0.1)
loss_func = torch.nn.MSELoss()

######################################################################
# 验证反向传播
prediction = net(x)  # 带入x,执行一次网络,得到预测值p
loss = loss_func(prediction, y)  # 计算均方误差

omptimzer.zero_grad()  # 先将所有参数的梯度每次置零
loss.backward()  # 计算所有节点的梯度
omptimzer.step()  # 用上面的计算值,和学习率更新网络参数

# 打印一些参数
print(f'\nx:{x.data.numpy()}, y:{y.data.numpy()}')
print(f'pred:{prediction.tolist()}, loss:{loss.tolist()}\n')

for name, param in net.named_parameters():
    print('-->name:', name, '-->grad_requirs:', param.requires_grad, ' -->grad_value:', param.grad)
    for p in param:
        print(p)
    print('----------------')

######################################################################
# 再训练一次网络,验证梯度是否是累加
prediction = net(x)  # 带入x,执行一次网络,得到预测值p
loss = loss_func(prediction, y)  # 计算均方误差

#omptimzer.zero_grad()  # 先将所有参数的梯度每次置零
loss.backward()  # 计算所有节点的梯度
omptimzer.step()  # 用上面的计算值,和学习率更新网络参数

# 打印一些参数
print(f'\nx:{x.data.numpy()}, y:{y.data.numpy()}')
print(f'pred:{prediction.tolist()}, loss:{loss.tolist()}\n')

for name, param in net.named_parameters():
    print('-->name:', name, '-->grad_requirs:', param.requires_grad, ' -->grad_value:', param.grad)
    for p in param:
        print(p)
    print('----------------')
上一篇:【深度学习】60题PyTorch简易入门指南,做技术的弄潮儿


下一篇:pytorch基础