惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
小众软件
小众软件
博客园 - 叶小钗
宝玉的分享
宝玉的分享
博客园_首页
Hugging Face - Blog
Hugging Face - Blog
人人都是产品经理
人人都是产品经理
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
SegmentFault 最新的问题
B
Blog RSS Feed
Engineering at Meta
Engineering at Meta
N
Netflix TechBlog - Medium
Google DeepMind News
Google DeepMind News
U
Unit 42
F
Fortinet All Blogs
IT之家
IT之家
Y
Y Combinator Blog
Martin Fowler
Martin Fowler
T
The Blog of Author Tim Ferriss
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The GitHub Blog
The GitHub Blog
Stack Overflow Blog
Stack Overflow Blog
Blog — PlanetScale
Blog — PlanetScale
酷 壳 – CoolShell
酷 壳 – CoolShell

又见苍岚

COLMAP PatchMatch Stereo 算法详解 事件驱动的状态机框架:从理论到工程实践 Git 在国内网络环境下无法 Push 的排查与修复 —— 配置 Clash 代理 分段五次多项式插值原理详解 路径插值方法深度对比研究 Claude Code 使用指南 OpenClaw 记忆管理与技能创建指南 CBS(Conflict-Based Search)算法详解 A* 算法及其变种详解 OpenClaw 配置多 Agents Windows Powershell 无法加载文件,因为在此系统上禁止运行脚本问题的解决方案 MaxClaw 安装流程 大模型 AI 名词介绍 AList 网盘聚合工具简介 Protobuf 简介与测试 Claude Code 简介以及 GLM 4.7 模型接入 Github 歌词下载工具 163MusicLyrics Python __getattr__ 懒加载 Python TypedDict 机器人仿真平台 Gazebo 安装记录 机器人仿真平台 Gazebo 简介 多机器人路径规划问题(Multi-Agent Path Finding, MAPF)简介 Python exifread 读取修改过的 jpeg 信息错误问题修复 3D 坐标系变换的理解 3D 旋转矩阵基本概念 MongoDB Compass 介绍 Python 环境管理工具 uv Flutter 开发指南 Snipaste 安装下载与黑屏问题解决方案 全局路径规划算法记录
PyTorch 学习 -2- 自动求导
Yiwei Zhang · 2023-07-07 · via 又见苍岚

本文介绍张量自动求导的基本知识 。

参考 深入浅出PyTorch ,系统补齐基础知识。

本节目录

  • autograd的求导机制
  • 梯度的反向传播

前言

PyTorch 中,所有神经网络的核心是 autograd 包。autograd 包为张量上的所有操作提供了自动求导机制。它是一个在运行时定义 ( define-by-run )的框架,这意味着反向传播是根据代码如何运行来决定的,并且每次迭代可以是不同的。

Autograd

torch.Tensor 是这个包的核心类。如果设置它的属性 .requires_gradTrue,那么它将会追踪对于该张量的所有操作。当完成计算后可以通过调用 .backward(),来自动计算所有的梯度。这个张量的所有梯度将会自动累加到.grad属性。

注意:在 y.backward() 时,如果 y 是标量,则不需要为 backward() 传入任何参数;否则,需要传入一个与 y 同形的 Tensor。

阻止梯度

要阻止一个张量被跟踪历史,可以调用.detach()方法将其与计算历史分离,并阻止它未来的计算记录被跟踪。为了防止跟踪历史记录(和使用内存),可以将代码块包装在 with torch.no_grad(): 中。在评估模型时特别有用,因为模型可能具有 requires_grad = True 的可训练的参数,但是我们不需要在此过程中对他们进行梯度计算。

Function

还有一个类对于autograd的实现非常重要:FunctionTensor Function 互相连接生成了一个无环图 (acyclic graph),它编码了完整的计算历史。每个张量都有一个.grad_fn属性,该属性引用了创建 Tensor 自身的Function(除非这个张量是用户手动创建的,即这个张量的grad_fnNone )。下面给出的例子中,张量由用户手动创建,因此grad_fn返回结果是None。

1
2
3
4
5
from __future__ import print_function
import torch
x = torch.randn(3,3,requires_grad=True)
print(x.grad_fn)
None

计算导数

如果需要计算导数,可以在 Tensor 上调用 .backward()。如果 Tensor 是一个标量(即它包含一个元素的数据),则不需要为 backward() 指定任何参数,但是如果它有更多的元素,则需要指定一个gradient参数,该参数是形状匹配的张量。

创建一个张量并设置requires_grad=True用来追踪其计算历史

1
2
3
4
x = torch.ones(2, 2, requires_grad=True)
print(x)
tensor([[1., 1.],
[1., 1.]], requires_grad=True)

对这个张量做一次运算:

1
2
3
4
y = x**2
print(y)
tensor([[1., 1.],
[1., 1.]], grad_fn=<PowBackward0>)

y是计算的结果,所以它有grad_fn属性。

1
2
print(y.grad_fn)
<PowBackward0 object at 0x000001CB45988C70>

对 y 进行更多操作

1
2
3
4
5
6
z = y * y * 3
out = z.mean()

print(z, out)
tensor([[3., 3.],
[3., 3.]], grad_fn=<MulBackward0>) tensor(3., grad_fn=<MeanBackward0>)

requires_grad_

.requires_grad_(...) 原地改变了现有张量的requires_grad标志。如果没有指定的话,默认输入的这个标志是 False

1
2
3
4
5
6
7
8
9
10
a = torch.randn(2, 2) # 缺失情况下默认 requires_grad = False
a = ((a * 3) / (a - 1))
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
b = (a * a).sum()
print(b.grad_fn)
False
True
<SumBackward0 object at 0x000001CB4A19FB50>

梯度

现在开始进行反向传播,因为 out 是一个标量,因此out.backward() out.backward(torch.tensor(1.)) 等价。

1
out.backward()

输出导数 d(out)/dx

1
2
3
print(x.grad)
tensor([[3., 3.],
[3., 3.]])

数学上,若有向量函数 $ \vec{y}=f(\vec{x}) $ ,那么 $ \vec{y} $ 关于 $ \vec{x} $ 的梯度就是一个雅可比矩阵 :
$ J=\left(\begin{array}{ccc}\frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}} \ \vdots & \ddots & \vdots \ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}}\end{array}\right) $ 而 torch.autograd 这个包就是用来计算一些雅可比矩阵的乘积的。例如,如果

$ v $ 是一个标量函数 $ l=g(\vec{y}) $ 的梯度:$ v=\left(\begin{array}{lll}\frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}}\end{array}\right) $ 由链式法则,我们可以得到:
$$
v J=\left(\begin{array}{lll}
\frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}}
\end{array}\right)\left(\begin{array}{ccc}
\frac{\partial y 1}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}} \
\vdots & \ddots & \vdots \
\frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}}
\end{array}\right)=\left(\begin{array}{lll}
\frac{\partial l}{\partial x_{1}} & \cdots & \frac{\partial l}{\partial x_{n}}
\end{array}\right)
$$

注意:grad 在反向传播过程中是累加的(accumulated),这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 再来反向传播⼀一次,注意grad是累加的
out2 = x.sum()
out2.backward()
print(x.grad)

out3 = x.sum()
x.grad.data.zero_()
out3.backward()
print(x.grad)
tensor([[4., 4.],
[4., 4.]])
tensor([[1., 1.],
[1., 1.]])

现在我们来看一个雅可比向量积的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
x = torch.randn(3, requires_grad=True)
print(x)

y = x * 2
i = 0
while y.data.norm() < 1000:
y = y * 2
i = i + 1
print(y)
print(i)
tensor([-0.9332, 1.9616, 0.1739], requires_grad=True)
tensor([-477.7843, 1004.3264, 89.0424], grad_fn=<MulBackward0>)
8

在这种情况下,y 不再是标量。torch.autograd 不能直接计算完整的雅可比矩阵,但是如果我们只想要雅可比向量积,只需将这个向量作为参数传给 backward:

1
2
3
4
5
v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(v)

print(x.grad)
tensor([5.1200e+01, 5.1200e+02, 5.1200e-02])

也可以通过将代码块包装在 with torch.no_grad(): 中,来阻止 autograd 跟踪设置了.requires_grad=True的张量的历史记录。

1
2
3
4
5
6
7
8
print(x.requires_grad)
print((x ** 2).requires_grad)

with torch.no_grad():
print((x ** 2).requires_grad)
True
True
False

如果我们想要修改 tensor 的数值,但是又不希望被 autograd 记录(即不会影响反向传播), 那么我们可以对 tensor.data 进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
x = torch.ones(1,requires_grad=True)

print(x.data) # 还是一个tensor
print(x.data.requires_grad) # 但是已经是独立于计算图之外

y = 2 * x
x.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播

y.backward()
print(x) # 更改data的值也会影响tensor的值
print(x.grad)
tensor([1.])
False
tensor([100.], requires_grad=True)
tensor([2.])

雅克比矩阵

向量对向量的求导结果是雅克比矩阵,比如如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import torch

if __name__ == '__main__':
x = torch.tensor([1,2,3], dtype=torch.float64, requires_grad=True)
x1 = x ** 2
y = 2 * x1

print(x.grad)
y.backward(torch.ones(3), retain_graph=True)
print(x.grad)
y.backward(torch.ones(3), retain_graph=False)
print(x.grad)
pass

-->
None
tensor([ 4., 8., 12.], dtype=torch.float64)
tensor([ 8., 16., 24.], dtype=torch.float64)

可以看到,有 $ \vec{X} =[x_1,x_2,x_3] = [1,2,3]$ ,$ \vec{Y} = 2 \vec{X}^2 $
$$
\vec{Y} '= 4 \vec{X}
$$
因此雅克比矩阵为:
$$
\frac{\partial \vec{Y}}{\partial \vec{X}}=\left(\begin{array}{ccc}\frac{\partial y_{1}}{\partial x_{1}} & \frac{\partial y_{1}}{\partial x_{2}} & \frac{\partial y_{1}}{\partial x_{3}} \ \frac{\partial y_{2}}{\partial x_{1}} & \frac{\partial y_{2}}{\partial x_{2}} & \frac{\partial y_{2}}{\partial x_{3}} \ \frac{\partial y_{3}}{\partial x_{1}} & \frac{\partial y_{3}}{\partial x_{2}} & \frac{\partial y_{3}}{\partial x_{3}}\end{array}\right)= \left(\begin{array}{ccc} 4&0&0\0 &8&0\0&0&12 \end{array}\right)
$$
然而这个雅可比矩阵无法直接落到 $\vec{X}$ 的梯度分量上,维度都不一样,因此需要降维,降维也得有个依据啊,因此需要用户输入与 $\vec{Y}$ 维度相同的权重向量,左乘到雅可比矩阵上。
$$
\left{\begin{array}{l}\frac{\partial o u t}{\partial a_{1}}=k_{1} * \frac{\partial \text { out }{1}}{\partial a{1}}+k_{2} * \frac{\partial o u t_{2}}{\partial a_{1}}+k_{3} * \frac{\partial o u t_{3}}{\partial a_{1}}+\ldots+k_{n} * \frac{\partial o u t_{n}}{\partial a_{1}} \ \frac{\partial o u t}{\partial a_{2}}=k_{1} * \frac{\partial o u_{1}}{\partial a_{2}}+k_{2} * \frac{\partial o u t_{2}}{\partial a_{2}}+k_{3} * \frac{\partial o u t_{3}}{\partial a_{2}}+\ldots+k_{n} * \frac{\partial o u t_{n}}{\partial a_{2}} \ \cdots \ \frac{\partial o u t}{\partial a_{n}}=k_{1} * \frac{\partial \text { out }{1}}{\partial a{n}}+k_{2} * \frac{\partial o u t_{2}}{\partial a_{n}}+k_{3} * \frac{\partial o u t_{3}}{\partial a_{n}}+\ldots+k_{n} * \frac{\partial o u t_{n}}{\partial a_{n}}\end{array}\right.
$$
因此对 $y$ 求导 backward 时,需要输入权重张量。

默认情况下求导后导数信息会被清空,其中 retain_graph 参数则可以保持梯度信息不丢失。

参考资料

文章链接:
https://www.zywvvd.com/notes/study/deep-learning/pytorch/torch-learning/torch-learning-2/