给出结论:误差反向传播算法,传播的是损失函数对各个参数的偏导数(就是导数),也可以简单理解成梯度(梯度的定义可不是这样的)
大致流程是搭一个简单网络,找到里面的各个参数,训练一次网络并进行误差反向传播,再次查看各个参数,验证结果,以下是详细过程。
这里我们新建一个简化的网络,输入层、隐藏层和输出层都只设置一个节点,并且不加激励函数,因为像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('----------------')