Pytorch 是如何进行梯度计算的?
Pytorch 作为最受欢迎的深度学习框架之一,其对于梯度计算的支持自然是不言而喻的。最近对 Pytorch 进行了更进一步的学习,发现了它为了服务深度学习,做了很多针对性的设计。搭积木并不是 Pytorch 的本质,梯度才是。而 Pytorch 对于梯度的运算方式,则是这篇文章讨论的重点。我相信如果要将一个工具用好,深入了解原理是必不可少的一步。我做了一些可以拿来分享的笔记,于是有了这篇介绍基础但好像又不那么基础的文章。
Tensor
与梯度计算相关的性质
在进行基本运算的时候,Pytorch 的 Tensor
确实可以当作 Numpy 的 ndarray
处理,但到了神经网络这里,Tensor
的优势才真正显现出来。为了支持神经网络的反向梯度传播,Tensor
里提供的额外性质大抵包括这么几个:
- 是否为参数
requires_grad
- 梯度数值
grad
- 梯度函数
grad_fn
requires_grad
一般来说,当我们创建一个 Tensor 时,我们可以令 requires_grad=False
来定义它为常量,或者 requires_grad=False
定义其为参数。常量不存储任何梯度信息,为参数的梯度计算而服务。requires_grad
默认是 False
。
import torch
# 声明为常量
c1 = torch.randn(5, 3)
c2 = torch.Tensor([1, 2, 3])
# 声明为参数
param = torch.ones(3, requires_grad=True)
print(c1.requires_grad, c2.requires_grad, param.requires_grad) # False, False, True
复制代码
而对是否为参量的修改也可以在定义完成后,通过 requires_grad_()
来修改:
# 常量 -> 参数
c2.requires_grad_(True)
# 参数 -> 常量
param.requires_grad_(False)
print(c2.requires_grad, param.requires_grad) # True, False
复制代码
grad & grad_fn
这两个东西按照字面意思就可以理解,grad
是这个参数的梯度值,是一个 Tensor,而 grad_fn
是这个参数的梯度函数,存储了这个参数经历的上一步运算,这和后面要讲到的反向传播树形图有关。
Pytorch 梯度计算工具包:autograd
梯度反向传播其实就是链式法则在固定点处的计算友好版。不需要算表达式,只要数值就行,具体原理不细说了。在 Pytorch 中,这一过程是由 autograd
包自动进行的。这里拿 Pytorch 官方教程 来说明一下反向传播过程。
import torch
x = torch.ones(5)
y = torch.zeros(3)
W = torch.randn(5, 3, requires_grad=True)
b = torch.randn(3, requires_grad=True)
z = torch.matmul(x, W) + b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y)
复制代码
这里,x 是输入,z 是输出,y 是标签,待优化的参数则是 W 和 b。最终使用 CE (Cross Entropy 交叉熵)来计算 loss。其实就是一个简单的,不包含激活函数的线性层:
在这里例子中,w.requires_grad
和 b.requires_grad
被设为 True
,其余变量的相关属性都是 False
。在最开始,w.grad
和 b.grad
被初始化为 None
。那么现在,我想计算 loss 相对于这两个参数的梯度 ∂loss∂w\frac{\partial loss}{\partial w} 和 ∂loss∂b\frac{\partial loss}{\partial b}. 这个过程在实现起来很简单,只需要
loss.backward()
复制代码
就直接将算好的梯度写入 .grad
里面了。后面就可以用
print(W.grad)
print(b.grad)
复制代码
把计算好的梯度打印出来了。
但问题是, backward()
究竟做了什么呢?为了回答这个问题,我们需要从前向过程开始讲起。
前向过程:构建计算图
实际上,在计算梯度之前的从变量计算 loss 过程中, Pytorch 会事先将变量之间的关系存储为图的形式,即计算图(Computational Graph)。上面例子中的计算图如下
这个图是一个有向无环图,所有的出发点被称为叶节点(入度为0,图中 x, W, b, y),而终点被称为根节点(出度为0,图中 loss)。没错,这是一个树形结构。
反向过程:梯度反向传播
与前向计算类似,反向传播的过程中同样存在一个图结构。这个图大体上和计算图类似,不同的是里面仅包含了根节点到声明为参数的叶节点之间的路径:
这个图不难理解,按照链式法则,我们在计算梯度时会列出下面的式子(注意这里只是简单表示梯度的传播关系,实际的计算式比这个多了转置,调了顺序,但还是线性的):
这个式子里每一个梯度都可以和上图中的反向传播函数对应上。而反向传播函数正如前面所说,是存储在 grad_fn
里面的。我们可以将它们打印出来:
print(f"Gradient function for z = {z.grad_fn}") # BinaryCrossEntropyLossWithLogitsBackward0
print(f"Gradient function for loss = {loss.grad_fn}") # AddBackward0
复制代码
要注意的是,Wx 对应的节点是隐藏的,如果用一个变量去代替它,也可以将其反向传播函数打印出来,即乘法运算对应的 MvBackward0
。
有了这样一张图,梯度就可以从最末端的 loss 逐步传递到每个参数,用于后续的优化工作。
作者:零度蛋花粥
来源:稀土掘金