pytorch计算图

计算图的作用范围

 
pytorch计算图及求导由pytorch框架自动实现
从import torch这一行开始,计算图就开始生效了,
其后所有requires_grad=True的tensor都会自动被添加到计算图中

计算图的结构

 
建模的核心是模型设计,所有的tensor都要归于模型model 

model与target要作为参数放入损失函数loss_fn 

loss = loss_fn(模型输出,target)
最终计算出一个模型输出 与 目标标签的 偏差 

损失函数才是整张计算图的终点
就像一棵倒着的多叉树,损失函数是根 

 

    

求导的大概过程

 
pytorch求导的过程,则是先从损失函数开始,再到模型,
这与数据先流过模型,再流入损失函数的方向刚好相反,
所以,model的计算方法叫正向传播forward
而自损失函数开始求导时,叫反向传播backward 

loss.backward()对整个计算图,
因为只有模型中有可变参数定义,
实际就是对模型中的可导参数求一遍导数

 

    

nn.Parameter作用

 
nn.Parameter 是一个迭代器,向其加入参数,就可以通过model.parameters()访问了

模型设计时,init方法会初始化一系列的类模板,
这些类模板的参数,都会自动加入model.parameters()

后续会将model.parameters(),学习率 作为参数供优化器调用
如果是自己设计的参数,就需要手工将之加入nn.Parameter

nn.Parameter用法示例

 
import torch
from torch import nn 
class DLModel(nn.Module):
    def __init__(self,in_features, out_features):
        super().__init__()
        self.w = nn.Parameter(torch.ones(2,1, dtype=torch.float32),requires_grad=True)
        self.b = nn.Parameter(torch.ones(1, dtype=torch.float32),requires_grad=True)
    
    def forward(self,X):
        x = X@self.w + self.b 
        return x 
    
model = DLModel(in_features=2,out_features=1)
for param in model.parameters():
    print(param)
Parameter containing:
tensor([[1.],
        [1.]], requires_grad=True)
Parameter containing:
tensor([1.], requires_grad=True)

 


 

  

 


backward后才有梯度

 
import torch 
w1 = torch.tensor([1.0,1.0],requires_grad=True)
b = torch.tensor(1.0,requires_grad=True)  
y = w1  + b # tensor([2., 2.], grad_fn=)
loss = y.sum() #转为标量
loss.backward()
w1.grad  #tensor([1., 1.])
    

 
b.grad #tensor(2.)

b.grad这梯度是2不是1,这一点就有点跳跃... 
    
b.ndim #0 

b实际上是一个标题,维度为0,
w1+b首先会自动触发广播机制,b由1.0转化为[1.0,1.0],然后进行[1.0,1.0]+[1.0,1.0]向量的加法 

向量的加法是各种元素分别个加
w1[0]+b + w1[1]+b = w1[0] + w1[1] + 2b

深度学习全是矩阵运算,矩阵计算的最小单位是向量计算,
向量计算最终,还是要转化为 相乘相加 ,这里b被加了2次
向量有多少个维度,b就会被加多少次 

 

    

模型验证

 
import torch
from torch import nn 
class DLModel(nn.Module):
    def __init__(self,in_features, out_features):
        super().__init__()
        self.w = nn.Parameter(torch.ones(2,1, dtype=torch.float32),requires_grad=True)
        self.b = nn.Parameter(torch.ones(1, dtype=torch.float32),requires_grad=True)
    
    def forward(self,X):
        x = X@self.w + self.b 
        return x 
    
model = DLModel(in_features=2,out_features=1)

X = torch.tensor([[0.1,0.1]],dtype=torch.float32)
y = torch.tensor([[1]],dtype=torch.float32)
y_out = model(X)
y_out

查看梯度

 
# 没有backward之前,param.grad没有data属性,所以下面的代码执行就报错
# AttributeError: 'NoneType' object has no attribute 'data'
for param in model.parameters():
    print(param.grad.data)

损失计算

 
# 为了方便理解及计算,这里采用L1Loss,就是绝对值,一个一次函数
loss_fn = nn.L1Loss()
loss = loss_fn(y_out,y)
loss

梯度下降,也就是对整个计算图进行求导运算

 
loss.backward()

再次查看梯度

 
for param in model.parameters():
    print(param.grad.data)
tensor([[0.1000],
    [0.1000]])
tensor([1.])

梯度计算理解

 
L1Loss,计算如下
loss = |y_out - y| = |x@w + b - y | = |x1*w1 + x2*w2 + b - y|
这里x=[0.1,0.1], b=1, w=[1,1], y=1 
0.1*1 + 0.1*1 + 1 - 1 = 0.2 是正数
对w1求偏导,其结果就是x1
对w2求偏导,其结果就是x2

 

    

 


 

  

 


累加的梯度

可导参数梯度计算默认累加

 
for i in range(3):
    X = torch.tensor([[0.1,0.1]],dtype=torch.float32)
    y = torch.tensor([[1]],dtype=torch.float32)
    y_out = model(X)
    loss_fn = nn.L1Loss()
    loss = loss_fn(y_out,y)
    loss.backward()
    for param in model.parameters():
        print(param.grad.data)
        tensor([[0.2000],
        [0.2000]])
tensor([2.])
tensor([[0.3000],
        [0.3000]])
tensor([3.])
tensor([[0.4000],
        [0.4000]])
tensor([4.])

由于损失函数是一次函数,其梯度就是常量x[0.1,0.1]
不管参数是多少,梯度就是x,
但这里梯度每次都增加0.1,就是当前梯度累加到了前一次的梯度上了

解决方法:每次梯度下降前清空现有可导参数的梯度

 
import torch
from torch import nn 
class DLModel(nn.Module):
    def __init__(self,in_features, out_features):
        super().__init__()
        self.w = nn.Parameter(torch.ones(2,1, dtype=torch.float32),requires_grad=True)
        self.b = nn.Parameter(torch.ones(1, dtype=torch.float32),requires_grad=True)
    
    def forward(self,X):
        x = X@self.w + self.b 
        return x 
    
model = DLModel(in_features=2,out_features=1)

X = torch.tensor([[0.1,0.1]],dtype=torch.float32)
y = torch.tensor([[1]],dtype=torch.float32)
y_out = model(X)


loss_fn = nn.L1Loss()
loss = loss_fn(y_out,y)


has_grad = False
for i in range(3):
    X = torch.tensor([[0.1,0.1*(i+1)]],dtype=torch.float32)
    print("X:",X)
    y = torch.tensor([[1]],dtype=torch.float32)
    y_out = model(X)
    loss_fn = nn.L1Loss()
    loss = loss_fn(y_out,y)
    for param in model.parameters():
        if has_grad:
            param.grad.data.zero_()
    loss.backward()
    has_grad = True
    for param in model.parameters():
        print(param.grad.data)
        break  # 只看参数w的梯度
    print("----------------------------")

X: tensor([[0.1000, 0.1000]])
tensor([[0.1000],
        [0.1000]])
----------------------------
X: tensor([[0.1000, 0.2000]])
tensor([[0.1000],
        [0.2000]])
----------------------------
X: tensor([[0.1000, 0.3000]])
tensor([[0.1000],
        [0.3000]])
----------------------------
    
优化器清空梯度

先梯度下降产生一个梯度

 
import torch
from torch import nn 
class DLModel(nn.Module):
    def __init__(self,in_features, out_features):
        super().__init__()
        self.w = nn.Parameter(torch.ones(2,1, dtype=torch.float32),requires_grad=True)
        self.b = nn.Parameter(torch.ones(1, dtype=torch.float32),requires_grad=True)
    
    def forward(self,X):
        x = X@self.w + self.b 
        return x 
    
model = DLModel(in_features=2,out_features=1)

X = torch.tensor([[0.1,0.1]],dtype=torch.float32)
y = torch.tensor([[1]],dtype=torch.float32)
y_out = model(X)

loss_fn = nn.L1Loss()
loss = loss_fn(y_out,y)
loss.backward()

for param in model.parameters():
    print(param.grad.data)
    break  # 只看参数w的梯度
tensor([[0.1000],
    [0.1000]])   

优化器清空梯度

 
optim = torch.optim.Adam(params=model.parameters(),lr=0.001)
optim.zero_grad()

for param in model.parameters():
    print(param.grad.data)
    break  # 只看参数w的梯度
tensor([[0.],
    [0.]])
被清空的梯度与没有梯度不同,
没有梯度是没有tensor.grad.data这个属性
被清空梯度的是tensor.grad.data为0 

优化器可以对没有梯度的tensor执行清空操作

 
import torch
from torch import nn 
class DLModel(nn.Module):
    def __init__(self,in_features, out_features):
        super().__init__()
        self.w = nn.Parameter(torch.ones(2,1, dtype=torch.float32),requires_grad=True)
        self.b = nn.Parameter(torch.ones(1, dtype=torch.float32),requires_grad=True)
    
    def forward(self,X):
        x = X@self.w + self.b 
        return x 
    
model = DLModel(in_features=2,out_features=1)

X = torch.tensor([[0.1,0.1]],dtype=torch.float32)
y = torch.tensor([[1]],dtype=torch.float32)
y_out = model(X)

optim = torch.optim.Adam(params=model.parameters(),lr=0.001)
optim.zero_grad()

这还没有没有进行梯度下降,但优化器可以去清空梯度,
显然,是代码内部做了条件判断,
类似于前面的代码:

for param in model.parameters():
    if has_grad:
        param.grad.data.zero_()

优化器更新模型参数

梯度变化的来源

 
优化器使用梯度下降法更新参数,大致就是

w = w - 学习率*w.梯度 

这个w.梯度是变化的梯度 

它的变化来源主要有二:
1. 函数及w本身
2. 数据,变量w在不同的数据处有不同的梯度

比如,y = 3w*w与y=2w*w 的导函数分别是
y = 6w 与 y=4w   

这里,主要因素是数据,
不同的数据,会让w产生不同的变化,这种变化会归于w梯度中
再通过 “w = w - 学习率*w.梯度 ” 来调整w 


更新参数的提前

 
提前1:有数据
提前2:从损失开始求导,计算梯度,才有梯度,这个过程就是反向传播 

1. 数据代入模型得到模型输出y_out = model(X)
2. 模型输出与标签代入损失函数,形成以损失函数为根的计算图, loss = loss_fn(y_out,标签)
3. loss.backward() 
4. 优化器更新参数,更新后清空参数梯度

优化器更新参数

 
import torch
from torch import nn 
class DLModel(nn.Module):
    def __init__(self,in_features, out_features):
        super().__init__()
        self.w = nn.Parameter(torch.ones(2,1, dtype=torch.float32),requires_grad=True)
        self.b = nn.Parameter(torch.ones(1, dtype=torch.float32),requires_grad=True)
    
    def forward(self,X):
        x = X@self.w + self.b 
        return x 
    
model = DLModel(in_features=2,out_features=1)

X = torch.tensor([[0.1,0.1]],dtype=torch.float32)
y = torch.tensor([[1]],dtype=torch.float32)
y_out = model(X)


for param in model.parameters():
    print(param)

Parameter containing:
tensor([[1.],
        [1.]], requires_grad=True)
Parameter containing:
tensor([1.], requires_grad=True)


loss_fn = nn.L1Loss()
loss = loss_fn(y_out,y)
loss.backward()

optim = torch.optim.Adam(params=model.parameters(),lr=0.001)
optim.step()

# 更新一次参数后,参数就变化一点
for param in model.parameters():
    print(param)
Parameter containing:
tensor([[0.9990],
        [0.9990]], requires_grad=True)
Parameter containing:
tensor([0.9990], requires_grad=True)


优化器SGD

SGD:随机梯度下降

 
import torch
from torch import nn 
class DLModel(nn.Module):
    def __init__(self,in_features, out_features):
        super().__init__()
        self.w = nn.Parameter(torch.ones(2,1, dtype=torch.float32),requires_grad=True)
        self.b = nn.Parameter(torch.ones(1, dtype=torch.float32),requires_grad=True)
    
    def forward(self,X):
        x = X@self.w + self.b 
        return x 
    
model = DLModel(in_features=2,out_features=1)

X = torch.tensor([[0.1,0.1]],dtype=torch.float32)
y = torch.tensor([[1]],dtype=torch.float32)
y_out = model(X)

 
for param in model.parameters():
    print(param)
Parameter containing:
tensor([[1.],
        [1.]], requires_grad=True)
Parameter containing:
tensor([1.], requires_grad=True)

loss_fn = nn.L1Loss()
loss = loss_fn(y_out,y)
loss.backward()
for param in model.parameters():
    print(param.grad.data)
tensor([[0.1000],
    [0.1000]])
tensor([1.])

随机梯度下降

 
optim = torch.optim.SGD(params=model.parameters(),lr=0.001)
optim.step()

for param in model.parameters():
    print(param)
Parameter containing:
tensor([[0.9999],
        [0.9999]], requires_grad=True)
Parameter containing:
tensor([0.9990], requires_grad=True)
    
w = w - 学习率*w.grad.data = 1 - 0.001*0.1 = 0.9999

然而,对于偏置b,
它的结果是0.9990与0.9999不符,
这就体现出了随机性

 
X = torch.tensor([[0.1,0.1]],dtype=torch.float32)
y = torch.tensor([[1]],dtype=torch.float32)
y_out = model(X)
loss = loss_fn(y_out,y)
loss.backward()
optim.step()
optim.zero_grad()

for param in model.parameters():
    print(param)
Parameter containing:
tensor([[0.9997],
        [0.9997]], requires_grad=True)
Parameter containing:
tensor([0.9970], requires_grad=True)

梯度是0.1,下一次参数更新后,本应该是0.9998,而这里是0.9997, 
说明SGD并不是按步就班地执行梯度下降的公式,而在公式中加入了随机因子

优化器Adam

自适应

 
这个自适应体现在,
开始的时候参数是随机初始化的,
认为该参数距离最优化参数,有较大的距离,
所以,开始的步子应该大一些,
优化一段时间后,再逐步减小步子的长度
以免错过最优解 

开始时步子大

 
import torch
from torch import nn 
class DLModel(nn.Module):
    def __init__(self,in_features, out_features):
        super().__init__()
        self.w = nn.Parameter(torch.ones(2,1, dtype=torch.float32),requires_grad=True)
        self.b = nn.Parameter(torch.ones(1, dtype=torch.float32),requires_grad=True)
    
    def forward(self,X):
        x = X@self.w + self.b 
        return x 
    
model = DLModel(in_features=2,out_features=1)

X = torch.tensor([[0.1,0.1]],dtype=torch.float32)
y = torch.tensor([[1]],dtype=torch.float32)
y_out = model(X)

loss_fn = nn.L1Loss()
loss = loss_fn(y_out,y)
loss.backward()

 
optim = torch.optim.Adam(params=model.parameters(),lr=0.001)
optim.step()

 
for param in model.parameters():
    print(param)
Parameter containing:
tensor([[0.9990],
        [0.9990]], requires_grad=True)
Parameter containing:
tensor([0.9990], requires_grad=True)

 
X = torch.tensor([[0.1,0.1]],dtype=torch.float32)
y = torch.tensor([[1]],dtype=torch.float32)
y_out = model(X)
loss = loss_fn(y_out,y)
loss.backward()
optim.step()
optim.zero_grad()

for param in model.parameters():
    print(param)
Parameter containing:
tensor([[0.9980],
        [0.9980]], requires_grad=True)
Parameter containing:
tensor([0.9980], requires_grad=True)
    

 
w = w - 学习率*w.grad.data = 1 - 0.001*0.1 = 0.9999

而Adam更像是使用的0.01的学习率 
w = w - 学习率*w.grad.data = 1 - 0.01*0.1 = 0.9990 

开始以10倍预定速度在更新参数

torch.no_grad

涉及模型或者说涉及任何可导tensor都会自动关联到计算图上

 
y_out = model(X)
loss = loss_fn(y_out,标签)
loss.backward() 
梯度下降是从损失函数开始的,然后反向传播回去,那到什么时候终止?

模型是可以套模型的,任何调用模型的地方,或者模型内部,
都可以看作一层层的函数嵌套,
换句话说,只要能关联的上,都会被求导

torch.no_grad可以指定一个范围,
不考虑梯度计算,即不修改模型参数,比如,模型预测时

模型预测

 
def predict(model, X):
    """模型预测
    """
    with torch.no_grad():
        y_pred = model(X)
    return y_pred

参考