002.Autograd
2024.3.21 (有点小难,菜就多写)
1.backward()原理
神经网络基本过程分为正向(forward)和反向(backward)的过程,通常我们在pytorch中只用关心正向过程的搭建即可。通常包括一下几个过程。
-
数据准备:input输入和label预测标签值
-
网络的构建:先构建卷积层,再构建激活层,池化层,最后是全连接层
-
优化器:选择需要优化的参数和优化的方式(SGD,BGD等)
-
权重初始化:即在每一次循环中都要保证梯度值被初始化,防止梯度累加
-
训练策略:将以上步骤整合并封装
在完成以上构造后我们即完成了前向的过程
而.backward()作为封装好的类,在求每一层的梯度的时候则不用我们再一一手动把每一层函数连续求偏导计算梯度
2.backward()初始化
一般情况下,变量.backward()
,求前项的这个过程中,括号内是不用传参数的,因为通常此时tensor经过了神经网络后已经转化成了标量(一维).但我们有时候要对tensor类型进行反向求梯度的过程,此时我们就要向括号内传入一个全1矩阵来作为起点.
output_grad = torch.ones_like(o)
o.backward(output_grad)
3.is_leaf()叶子节点
- 叶子节点的作用: 在backward过程中要判断每一层的Tensor的梯度值是否要保存(即是否参与反向这个过程中的计算),如果一个节点不是叶子节点,那么它的节点值则不会保存
- 判断一个节点是否是叶子节点: 通常由用户创建的且没有参与卷积运算的tensor变量都是叶子节点,但当用户自定义但是已经参与运算的tensor也会变成非叶子节点
- 强制保存非叶子节点的grad: 为了观察非叶子节点在传播过程中的梯度值的变化,我们需要对这些节点进行
.retain_grad
操作来保留变量,从而将其强制转化成叶子节点
import numpy as np
import torch
data0 = torch.randn(2, 2, 4)
w_0 = torch.randn(2, 4, 3, requires_grad=True)
w_1 = torch.randn(2, 3, 5, requires_grad = True)
data2 = torch.bmm(data0, w_0)
data3 = torch.sigmoid(data2)
output = torch.matmul(data3, w_1)
output.backward(torch.ones_like(output))
print(w_0.is_leaf)
print(w_0.grad)
w_0 = w_0 - 0.001 * w_0.grad
print(w_1.is_leaf)
w_1.retain_grad()
print(w_1.is_leaf)
print(w_1.grad)
4.hook方法
-
作用: 在正向传播的过程中我们想对某一层的tensor对应的梯度(grad)进行操作,但如果用传统的
.retain_grad
方法那么就只能将梯度原封不动的取出来,而不能对梯度进行放大或者缩小等放缩操作,这时候我们就需要用到hook方法 -
写法: 在类中,我们通常先定义hook函数,再在迭代中对指定tensor执行
.register_hook(hook)
如果是非叶子节点,那么还要保留grad
.retain_grad()
import torch
import numpy as np
torch.manual_seed(0)
x = torch.ones(5, requires_grad=True)
w = torch.randn(5, 5, requires_grad=True)
b = torch.randn_like(x)
label = torch.Tensor([0, 0, 1, 0, 0])
grad_list = []
def hook(grad):
#这里的形参可以随便写,但是默认是对grad进行操作
#grad=grad
#[-0.0405, -0.0722, -0.1572, 0.3101, -0.0403]
grad = grad * 2
#[-0.0809, -0.1444, -0.3143, 0.6202, -0.0805]
grad_list.append(grad)
return grad
for i in range(10):
if w.grad:
w.grad.zero_()
z = torch.matmul(w, x) + b
output = torch.sigmoid(z)
#注意每次hook操作都是在backward之后才进行的,之前都是None
output.register_hook(hook)
output.retain_grad()
loss = (output - label).var()
loss.backward()
#print(output.grad)
w = w - 0.2 * grad_list[-1]
print(w)
#print("loss: ", loss)
5.梯度清零
在每次进行下一次迭代更新参数的过程中,我们通常面临梯度叠加的误区,即相邻两次迭代产生的梯度会叠加在一起,从而造成一些计算的错误,所以每一次迭代进行前我们都要确保梯度清空.
- 方法一:采用运算式的形式,即每次.backward()后对变量进行对于自身的运算,即自我迭代,这是变量本身已经更新,所以梯度清零.
- 方法二:直接在每次迭代前判断tensor的grad是否是0,若是,则使用
.grad.zero_()
,将grad清零
import numpy as np
import torch
torch.manual_seed(0)
x = torch.ones(5, requires_grad=True)
w = torch.randn(5, 5, requires_grad=True) # 叶子节点
b = torch.randn_like(x)
label = torch.Tensor([0, 0, 1, 0, 0])
for i in range(4):
z = torch.matmul(w, x) + b # linear layer
output = torch.sigmoid(z)
loss = (output-label).var() # l2 loss
loss.backward()
print(w.grad)
#w = w - 0.2 * w.grad
import numpy as np
import torch
torch.manual_seed(0)
x = torch.ones(5, requires_grad=True)
w = torch.randn(5, 5, requires_grad=True) # 叶子节点
b = torch.randn_like(x)
label = torch.Tensor([0, 0, 1, 0, 0])
for i in range(4):
w.retain_grad()
z = torch.matmul(w, x) + b # linear layer
output = torch.sigmoid(z)
loss = (output-label).var() # l2 loss
loss.backward()
print(w.grad)
w = w - 0.2 * w.grad
6.with torch.no_grad()
在前向过程中只要有一个的叶子节点的require_grad是true,那么后续过程中的所有的叶子节点也是true,所以不能进行一些原地 .function_(methods)
这一类的操作,原因还是在反向过程中,这些tensor内的grad会参与运算;
import torch
import numpy as np
torch.manual_seed(0)
x = torch.ones(5, requires_grad=True)
y = torch.randn(5, 5, requires_grad=True) # 叶子节点
b = torch.randn_like(x)
label = torch.Tensor([0, 0, 1, 0, 0])
for i in range(100):
if i > 0:
y.grad.zero_()
z = torch.matmul(y, x) + b # linear layer
output = torch.sigmoid(z)
loss = (output - label).var() # l2 loss
z.sub_(0.2 * z.grad)
loss.backward()
print("loss: ", loss)
这时候我们就可以在每次循环中加入
with torch.no_grad():
后加具体叶子节点的运算操作,那么由此产生的tensor的 requires_grad
则会变成false,那么对应的 grad_fn
也会消失
import torch
import numpy as np
torch.manual_seed(0)
x = torch.ones(5, requires_grad=True)
y = torch.randn(5, 5, requires_grad=True) # 叶子节点
b = torch.randn_like(x)
label = torch.Tensor([0, 0, 1, 0, 0])
for i in range(100):
if i > 0:
y.grad.zero_()
z = torch.matmul(y, x) + b # linear layer
output = torch.sigmoid(z)
loss = (output - label).var() # l2 loss
with torch.no_grad():
loss=(output-label).var()
loss.backward()
print("loss: ", loss)
7.自定义梯度计算
- 当pytorch内置的方法不能满足我们的一些操作时,(通常是需要将多个算子组合包装为新的类,或者自定义函数求梯度),的时候,我们就需要自定义求梯度的方法。
- 在定义新的类的时候,包括
foward
函数 +backward
函数 . 最后再运用.apply
函数实例化和.backward
求梯度def forward
- 关于形参 :此函数必须接受一个context ctx作为第一个参数,之后可以传入任何参数 ,即在运算中
- 在定义了运算法则之后,通过
ctx.save_for_backward(x1,x2,x3,....)
来存在前项运算中所产生的对应值 - 在
return
的过程中,返回最后计算的值,而不是中间计算的值
def backward
- 关于形参 : context ctx作为第一个参数,然后是第二个参数grad_output,里面存储forward后tensor的梯度
- 在函数内部,自己自定义反向传播公式(求偏导公式)
- return的结果是对应每一个input(对应于forward里的各个input)的梯度
Exp.apply
- 关于实参 :一一传入需要计算的一一对应的变量即可
- 之后紧接着这个变量
.backward()
即可
import numpy as np
import torch
class Exp(torch.autograd.Function): # 继承这个 function
@staticmethod
def forward(ctx, x: torch.Tensor, y: torch.Tensor, z: int):
w = x * z
out = x * y + y * z + w * y
ctx.save_for_backward(x, y, w, out)
ctx.z = z # z is not a tensor
print("===========exp forward")
return out
@staticmethod
def backward(ctx, grad_out):
x, y, w, out = ctx.saved_tensors
z = ctx.z
gx = grad_out * (y + y * z)
gy = grad_out * (x + z + w)
gz = None
print("============== exp backward")
return gx, gy, gz
a = torch.tensor(1., requires_grad=True, dtype=torch.double)
b = torch.tensor(2., requires_grad=True, dtype=torch.double)
c = 4
d = Exp.apply(a, b, c)
d.backward()
print(a.grad)
8.手写第一个简单网络
ps:linear()的用法
-
形参:1.in_features:即我们的输入,在我们输入矩阵的时候通常会输入一个n*m的矩阵(n行m列)这里的in_features通常指m,(可以理解为每一组里面有多少元素)
2.out_features:这里即我们期望的通过线性变化(矩阵相乘)得到的维度
可以理解为输入的(n乘m 的矩阵乘了一个m乘out_features的矩阵,得到一个
n乘out_features的矩阵)
3.bias:后面的常数项,即对矩阵内部每一个元素都加上的元素
import numpy as np
import torch
input = torch.tensor([5, 10]).reshape(1, 2).to(torch.float32)
linear_1 = torch.nn.Linear(2, 3)
x=linear_1(input)
print(input)
print(x)
1.设计阶段
通常写在迭代的最前面,这里设计了一个深度是两层的神经网络
连接层:运用linear进行全连接
激活层:两层全选sigmod进行激活
模型评估:选择MSE(均方误差)来评测
优化策略:这里选择SGD(随机梯度下降)来优化
2.代码
import numpy as np
import torch
#设计阶段呢
input = torch.tensor([5, 10]).reshape(1, 2).to(torch.float32)
linear_1 = torch.nn.Linear(2, 3)
act_1 = torch.nn.Sigmoid()
linear_2 = torch.nn.Linear(3, 2)
act_2 = torch.nn.Sigmoid()
criteration = torch.nn.MSELoss()
optimizer = torch.optim.SGD([{"params": linear_1.parameters()},
{"params": linear_2.parameters()}], lr=0.5)
label = torch.tensor([0.01, 0.99]).reshape(1, 2)
for i in range(100):
optimizer.zero_grad()
x = linear_1(input)
x = act_1(x)
x = linear_2(x)
output = act_2(x)
loss = criteration(output, label)
loss.backward()
optimizer.step() # 更新权重
print(loss)