一小时速通Pytorch之自动梯度(Autograd)和计算图(Computational Graph)(二)
AutoGrad自动梯度
- 通过官网学习总结而得,加上一些个人的理解。
AutoGrad是原文名称,中文常用自动梯度一词进行描述,在本文中两者等同。
概述
- 在大部分神经网络中我们都是采用随机梯度下降(SGD)的方法来对神经网络参数进行优化,因为梯度在神经网络中具有重要作用
Pytorch的自动梯度旨在帮助初学者快速快速训练和优化神经网络- 这里通过一个简单的神经网络训练过程讲述如何使用自动梯度
用法
❗此教材仅适用运行在CPU上的张量❗
❗即使把张量搬运到GPU上也不适用❗
导入数据
首先从torchvision加载预训练模型resent18模型,然后创建了一个随机张量来表示一张具有3个通道、高度和宽度为64的图像,并将其相应的标签初始化为一些随机值。
其中,预训练模型中的标签形状是(1,1000)。真实情景下一张输入的照片应该只有唯一的标签值,这里只是为了演示使用,并不是正确的写法。
示例代码:
import torch
from torchvision.models import resnet18, ResNet18_Weights# 导入模型和对应的权重参数
model = resnet18(weights=ResNet18_Weights.DEFAULT)
data = torch.rand(1, 3, 64, 64)
labels = torch.rand(1, 1000)
前向传播
把我们生成的图片data输入到神经网络,经过神经网络后得到每个分类(标签)的预测值,这一步称为前向传播
示例代码:
import torch
from torchvision.models import resnet18, ResNet18_Weights# 导入模型和对应的权重参数
model = resnet18(weights=ResNet18_Weights.DEFAULT)
data = torch.rand(1, 3, 64, 64)
labels = torch.rand(1, 1000)# 接下来通过前向传播来进行神经网络的预测
prediction = model(data)
print("预测的最大值为:{},其对应的类别为:{}".format(prediction.max(), prediction.argmax()))
示例输出:(结果具有随机性)
预测的最大值为:2.798292636871338,其对应的类别为:463
计算损失以及反向传播
- 损失函数:神经网络的输出值与其真实标签值的“差值”,这里的“差值”不是数学上的做差运算,而是一种距离,代表真实值与预测值的差距。
通常情况下根据实际神经网络的任务采用不同的损失函数,附表查询使用
| 神经网络任务 | 常用损失函数 |
|---|---|
| 多分类任务 | 交叉熵损失函数(CrossEntropyLoss) |
| 二分类任务 | 二分类交叉熵(BCELoss / BCEWithLogitsLoss) |
| 回归任务 | 均方误差(MSELoss)/ L1 Loss(MAE 损失)/SmoothL1Loss(Huber Loss) |
| 目标检测 | TripletLoss / ContrastiveLoss |
| 图像分割 | DiceLoss / CE |
此处为了方便演示,损失值的计算仅仅是用真实值与标签值做差。
例如:
loss = (prediction - labels).sum()
print("损失值:{}".format(loss))
示例输出:
损失值:-510.5419921875
得到损失值之后,我们需要告诉神经网络去修正不同神经元的权重,把损失值的梯度从输出一步步传递到最开始的一层神经元,这一步称为反向传播
例如:
loss.backward()
当我们对损失值这个张量调用backward这个函数,这个函数会自动计算每个神经元的梯度并且存储在每个参数的grad这个属性里面
优化权重
上一步我们只是计算出来每个神经元的梯度,即得到了优化的方向,接下来我们再使用torch的SGD(随机梯度下降)优化器来对神经网络的参数进行优化,这里采用0.01的学习率**lr(可以理解为沿着梯度下降的反向走的步长),0.9的动量momentum**(可以理解为记住最近一直往哪走,然后继续往这个方向加速走,不会被局部的小波动带偏)
例如:
optim = torch.optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)
最后,我们调用.step()来启动梯度下降,优化器会根据之前反向传播得到每个神经元各自的梯度数据来调整参数,也就是我们常说的训练过程
例如:
optim.step()
至此,一个简单的神经网络训练过程已经讲述完毕
神经网络训练过程完整代码
import torch
from torchvision.models import resnet18, ResNet18_Weights# 导入模型和对应的权重参数
model = resnet18(weights=ResNet18_Weights.DEFAULT)
data = torch.rand(1, 3, 64, 64)
labels = torch.rand(1, 1000)# 接下来通过前向传播来进行神经网络的预测
prediction = model(data)
print("预测的最大值为:{},其对应的类别为:{}".format(prediction.max(), prediction.argmax()))loss = (prediction - labels).sum()
print("损失值:{}".format(loss))loss.backward()optim = torch.optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)
optim.step()
示例输出:
预测的最大值为:2.729147434234619,其对应的类别为:463
损失值:-495.92340087890625
需要注意的是,这个输出没有实际意义,因为标签值和输入数据都是随机的,只是为了演示使用。
至此,你已经有了训练神经网络所需的一切。以下部分详细介绍了AutoGrad的工作原理,你可以跳过它们。
AutoGrad原理(可跳过)
我们构建两个张量a和b并且在构造时选择requires_grad=True,这样我们可以追踪自动梯度的每一步的操作。
例如:
import torcha = torch.tensor([2., 3.], requires_grad=True)
b = torch.tensor([6., 4.], requires_grad=True)
print(a, b, sep='\n')
示例输出:
tensor([2., 3.], requires_grad=True)
tensor([6., 4.], requires_grad=True)
我们再根据下述公式创建Q这个张量,公式Q=3a³ - b²
例如:
Q = 3 * a ** 3 - b ** 2
print("Q:{}".format(Q))
示例输出:
Q:tensor([-12., 65.], grad_fn=<SubBackward0>)
在这里我们假设a和b是神经网络的参数,Q是损失值(误差),在神经网络的训练中我们需要得到误差Q分别关于a和b的梯度,也就是对Q分别求Q对a的偏导数和对Q求b的偏导数
∂Q∂a=9a2∂Q∂b=−2b\begin{array}{l} \frac{\partial Q}{\partial a}=9 a^{2}\\ \frac{\partial Q}{\partial b}=-2 b \end{array} ∂a∂Q=9a2∂b∂Q=−2b
当我们对损失Q调用.backward()时autograd会自动计算相应的梯度值并且存储在对应张量的.grad属性里面。
- 这里需要区分
Q是标量(常量)还是向量(多维的数据)。如果Q是标量的话,pytorch定义Q对Q求导是常数1,因此在调用.backward()的时候不需要传入梯度的数据就可以自动进行反向传播。
例如:
import torchx = torch.ones(2, 2, requires_grad=True)
y = x + 2
z = y * y * 3
out = z.mean() # out 是标量
out.backward() # 不需要传参
- 如果我们的损失
Q是一个多维的向量例如Q(a,b) = f(a,b)= 3a³ - b²即Q是一个关于向量a和向量b的函数,注意其中的a和b是一个向量。此时我们对Q分别求关于a和b的偏导,得到的是一个雅可比矩阵(自行百度)
例如:
假设
a=[a1,a2],b=[b1,b2]Q=3a3−b2=[3a13−b12,3a23−b22]\\a = [a_1, a_2], b = [b_1, b_2]\\ Q = 3a^3 - b^2 = [3a_1^3 - b_1^2, 3a_2^3 - b_2^2] a=[a1,a2],b=[b1,b2]Q=3a3−b2=[3a13−b12,3a23−b22]
∂Q∂a=[9a12009a22]∂Q∂b=[−2b100−2b2]\begin{array}{c} \frac{\partial Q}{\partial a}=\left[\begin{array}{cc} 9 a_{1}^{2} & 0 \\ 0 & 9 a_{2}^{2} \end{array}\right] \\ \frac{\partial Q}{\partial b}=\left[\begin{array}{cc} -2 b_{1} & 0 \\ 0 & -2 b_{2} \end{array}\right] \end{array} ∂a∂Q=[9a12009a22]∂b∂Q=[−2b100−2b2]
得到的偏导数是一个具有多分量的矩阵,我们需要把它通过一种线性组合转成单分量的形式,例如
Q1=3a13−b12,Q2=3a23−b22S=w1Q1+w2Q2其中w1=1,w2=1∣令w=[w1,w2],Q=[Q1,Q2]则S=WQ\\Q_1 = 3a_1^3 - b_1^2,Q_2 = 3a_2^3 - b_2^2\\ S = w_1Q_1 + w_2Q_2\\ 其中w_1 = 1,w_2=1|\\ 令w = [w_1,w_2],Q = [Q_1,Q_2]\\ 则S = WQ Q1=3a13−b12,Q2=3a23−b22S=w1Q1+w2Q2其中w1=1,w2=1∣令w=[w1,w2],Q=[Q1,Q2]则S=WQ
在这里我们对两个分量a1和a2取了相同的权重,权重值分别是w1=1,w2=1然后转成了单分量(标量)的形式,此时我们可以继续进行的反向传播的操作,把上述操作转成代码如下:
external_grad = torch.tensor([1., 1.]) # 自定义S这个标量并且指定每一维分量的权重值
Q.backward(gradient=external_grad) # 这里的Q是向量必须显示指定 gradient
如果我们每一维分量的权重值都相同,还可以采用下述更优雅的写法
示例:
# 法一
Q.sum().backward()
# 法二
Q.mean().backward()# 这两者写法与下述写法完全等价
external_grad = torch.tensor([1., 1.])
Q.backward(gradient=external_grad)
此时我们检查一下我们得到的梯度信息是否是正确的
示例:
print(9 * a ** 2 == a.grad)
print(-2 * b == b.grad)
示例输出:
tensor([True, True])
tensor([True, True])
完整代码
import torcha = torch.tensor([2., 3.], requires_grad=True)
b = torch.tensor([6., 4.], requires_grad=True)
print(a, b, sep='\n')
Q = 3 * a ** 3 - b ** 2
print("Q:{}".format(Q))# 法一
# Q.sum().backward()
# 法二
# Q.mean().backward()
# 这两者写法与下述写法完全等价
external_grad = torch.tensor([1., 1.])
Q.backward(gradient=external_grad)print(9 * a ** 2 == a.grad)
print(-2 * b == b.grad)
计算图
概述
从概念上讲,autograd机制在由Function对象组成的有向无环图(DAG)中记录数据(张量)和所有执行的操作(以及由此产生的新张量)。在这个有向无环图(DAG)中,叶子节点是输入的张量,根节点是输出张量,通过追踪从根节点到叶子结点的这张图的信息,可以使用链式法则来自动计算梯度。
在前向传播中,autograd在同时做两件事:
- 运行指定的操作得到一个结果张量
- 在DAG中保持操作过程的梯度函数。
当我们在DAG的根结点上调用.backward()的时候,反向传播开始,autograd机制会执行以下操作:
- 计算DAG中的每一个梯度值(除了叶子结点,每一个结点都有一个
.grad_fn用来表示自己是在哪一步操作产生的,具体表现为调用哪一个函数产生的),然后把计算出来的梯度进行回传给调用函数的地方 - 把计算出来的梯度通过链式法则传递叶子结点的
grad属性里面
注意事项
在pytorch中,DAG是动态生成的。特别注意的是,这张图是从头开始创建的,当每次调用.backward结束的时候,autograd机制开始画新的图。这正是允许您在模型中使用控制流语句的原因;如果有需要,可以在每次迭代中更改输入张量的形状,数据规模甚至是操作逻辑。
补充(冻结参数)
torch.autograd跟踪所有标志设置为True的张量的操作。对于不需要梯度的张量,将此属性设置为将其排除在梯度计算之外。
例如:
import torcha = torch.tensor([2., 3.], requires_grad=False)
需要注意的是,如果操作中有一个张量是需要保留梯度的,那么与这个变量相关的操作都会保留梯度。
示例:
import torchx = torch.rand(5, 5)
y = torch.rand(5, 5)
z = torch.rand((5, 5), requires_grad=True)a = x + y
print("a 是否需要保留梯度:{}".format(a.requires_grad))
b = x + z
print("b 是否需要保留梯度:{}".format(b.requires_grad))
示例输出:
a 是否需要保留梯度:False
b 是否需要保留梯度:True
在神经网络中,不计算计算梯度的参数通常被称为冻结参数。如果您在训练的时候就知道不需要这些参数的梯度,那么“冻结”模型的一部分是有用的(这可以帮助autograd减少一点计算量)
一般在模型的微调中,我们会冻结模型的大部分参数,如果我们想要对新标签进行预测的话仅需要修改分类器层。接下来让我们通过一个小例子来进行演示。这里我们加载一个预训练的resnet18模型,并冻结所有参数。
示例:
from torchvision.models import resnet18, ResNet18_Weightsmodel = resnet18(weights=ResNet18_Weights.DEFAULT)# 冻结神经网络中的全部参数
for param in model.parameters():param.requires_grad = False
r如果我们想在一个有10个标签的新数据集上微调模型。以resnet这个模型举例,分类器是这个模型的最后一个线性层。我们可以简单地用一个新的默认情况下未冻结参数线性层替换它,把该层充当我们的新的分类器model.fc。这里分类器的输入的512,输出是10
示例:
model.fc = nn.Linear(512, 10)
现在,模型中的所有参数(最后新的分类器层的参数除外)都被冻结。计算梯度的唯一参数是fcmodel.fc的权重和偏差值
示例:
# 采用SGD的方式来训练模型
optimizer = optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)
这里虽然我们把模型的全部参数都传入分类器里面,但是真正计算梯度的参数只有后面新的分类器层的权重也偏差值。
同样的,我们也可以使用torch.no_grad来实现冻结部分参数的功能
完整代码
from torch import optim
from torchvision.models import resnet18, ResNet18_Weightsmodel = resnet18(weights=ResNet18_Weights.DEFAULT)# 冻结神经网络中的全部参数
for param in model.parameters():param.requires_grad = False# 采用SGD的方式来训练模型
optimizer = optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)
这里只是演示模型微调的办法,实际需要新的数据集和10个类别的标签才能进行模型的微调
