PyTorch02-PyTorch基础知识
二、PyTorch基础知识
本章的的内容包含如下:张量概念及特点、张量的运算、自动微分、并行计算简介以及并行计算加速设备。
2.1 张量
2.1.1 张量简介
张量就是“多维数组”,其中的元素的数据必须一致。它是 PyTorch 传输、存储和计算数据的统一标准载体。
无论在深度学习中处理的是什么数据,最终都会被转化成张量:
- 0维张量:标量(Scalar),就是一个独立的数字(如
loss = 0.05)。 - 1维张量:向量(Vector),如一维数组
[1.0, 2.0, 3.0]。 - 2维张量:矩阵(Matrix),如 Excel 表格或灰度图(高 × 宽)。
- 3维张量:时间序列数据、股价、单张彩色图像
(宽, 高, 通道)或一段文本序列(Batch × 序列长度)。 - N维张量:更高维度的数据集合(比如
4维张量常用来表示一批彩色图片
(Batch_size, Channel, Height, Width))。
张量的核心是一个数据容器,一般情况下由数字组成,但也能由字符串组成。
张量 torch.Tensor
是PyTorch中存储和变换数据的主要工具。它跟Numpy多维数组非常类似,可以联想起来对比学习。但Tensor
天生地提供了GPU计算和自动求梯度等更多功能,使用其更加适用于深度学习。
2.1.2 创建张量
创建张量的方法有多种,可以根据需求使用不同方法。常用的如下。
(1)从无到有创建张量
从无到有创建指定形状的或随机张量或特定元素的张量
| 函数 | 功能 | 注意点 | 代码示例 |
|---|---|---|---|
| Tensor(sizes) | 基础构造函数,根据指定形状(shape)在内存中申请空间,创建未初始化的张量。现代
PyTorch 更推荐写 torch.empty(sizes) |
里面的元素是内存残留的“垃圾值”;实际上是
torch.FloatTensor,数据类型只能是
torch.float32 |
x = torch.Tensor(2, 3) # 创建 2 行 3
列未初始化的张量 |
| empty(sizes) | 官方推荐的未初始化张量创建函数(替代Tensor(sizes))。根据指定的形状(sizes)直接在内存中申请空间,不填充任何默认值。 |
比 torch.Tensor(sizes) 更灵活、更规范,可以通过 dtype
指定数据类型(如 torch.int32)、通过 device 指定硬件设备(如
'cuda')。 常用于作为预分配内存的容器,后续会被其他计算结果(如 torch.matmul(..., out=x) 或切片赋值)直接覆盖。 |
import torchx = torch.empty(2, 3)#
创建一个 2x3 的未初始化 float32
张量y = torch.empty((2, 3), dtype=torch.int64, device='cuda')#
也可以直接指定类型和设备(如直接在 GPU 上预分配一块 int64 内存) |
| tensor(data) | 工厂函数,类似于 np.array,从已有的数据(如 Python
列表、元组、NumPy 数组)深拷贝创建张量。 |
会自动推断数据类型(整数推断为 int64,浮点数推断为
float32);传入的是具体数据内容而非形状。 |
x = torch.tensor([[1, 2], [3, 4]]) # 根据列表生成 2x2
张量 |
| ones(sizes) | 创建指定形状且所有元素均为 1 的张量。 | 常用于全 1 矩阵初始化或掩码(Mask)生成;可通过 dtype
参数指定类型。 |
x = torch.ones(2, 3) # 生成 2x3 全 1 张量 |
| zeros(sizes) | 创建指定形状且所有元素均为 0 的张量。 | 最常用的预分配内存或清零方法,常用于梯度累加器、Padding 填充或偏置(Bias)初始化。 | x = torch.zeros(2, 3) # 生成 2x3 全 0 张量 |
| eye(sizes) | 创建主对角线上元素为 1,其余元素为 0 的二维单位矩阵。 | 只能创建二维张量;若传 1 个参数 n 则生成 \(n \times n\) 方阵;若传 2 个参数
n, m 则生成 \(n \times m\)
对角阵。 |
x = torch.eye(3) # 生成 3x3 的单位矩阵 |
| arange(s,e,step) | 在区间 \([s, e)\) 内,按等差步长
step 生成一维张量(类似于 Python 的
range)。 |
左闭右开,即包含起点 \(s\),不包含终点 \(e\)。 | x = torch.arange(0, 10, 2) # 输出: tensor([0, 2, 4, 6,
8]) |
| linspace(s,e,steps) | 在区间 \([s, e]\) 内,均匀切割成
steps 个点,生成一维张量。 |
双闭区间(包含终点 \(e\));第三个参数是点的个数,而不是步长(容易与
arange 混淆)。 |
x = torch.linspace(0, 10, 5) # 输出: tensor([ 0.0, 2.5,
5.0, 7.5, 10.0]) |
| rand/randn(sizes) | rand是生成[0,1)上均匀分布的随机张量;randn是生成服从N(0,1)的正态分布的随机张量 | randn
的结果包含负数,常用于模型权重的初始化(高斯初始化)。 |
u = torch.rand(2, 3) # 2x3 均匀分布随机数。
n = torch.randn(2, 3) # 2x3 标准正态分布随机数 |
| normal(mean,std) | 生成符合自定义正态分布 \(\mathcal{N}(\text{mean}, \text{std}^2)\) 的随机张量。 | mean 和 std
既可以是具体的标量,也可以是张量;若传入张量,可实现为不同位置指定各自不同的均值和标准差。 |
x = torch.normal(mean=0.0, std=1.0, size=(2, 3)) #
指定均值为 0,标准差为 1 |
| randperm(m) | 生成一个包含 \(0\) 到 \(m - 1\) 的随机排列整数序列(Random Permutation)。 | 返回一维张量;最常用于给数据集做随机打乱(Shuffle)或选取随机采样索引。 | x = torch.randperm(5) # 输出类似于: tensor([3, 1, 0, 4,
2]) |
(2)仿照已有张量的形状
不只能仿照形状,还可以同时继承数据类型(dtype)和设备(device),甚至可以根据已有的数据内容来创建。
- 继承已有张量的属性(形状、类型、设备)
常见,通常以 _like 结尾。它们会自动继承原张量
x 的 shape、dtype 和
device(无需手动重新指定 CPU/GPU
或数据类型):
| 函数 | 功能与特点 | 代码示例 |
|---|---|---|
torch.empty_like(x) |
创建一个与 x
同形状、同类型、同设备的未初始化张量(最推荐用于预分配) |
y = torch.empty_like(x) |
torch.zeros_like(x) |
创建与 x 同属性的全 0 张量 |
y = torch.zeros_like(x) |
torch.ones_like(x) |
创建与 x 同属性的全 1 张量 |
y = torch.ones_like(x) |
torch.rand_like(x) |
创建与 x 同属性的 [0, 1)
均匀分布随机张量 |
y = torch.rand_like(x) |
torch.randn_like(x) |
创建与 x
同属性的标准正态分布随机张量 |
y = torch.randn_like(x) |
torch.full_like(x, fill_value) |
创建与 x 同属性且指定填充值的张量 |
y = torch.full_like(x, 3.14) |
即使使用了 _like
函数,仍可以通过显式传入参数来“覆盖”某些属性。例如:
1 | |
- 浅拷贝已有的张量(共享数据内存)
不是想仿照形状,而是想直接引用或变换已有张量的“内容”,同时避免额外的内存开销:
x.clone():深拷贝。在内存中开辟一块全新空间,完整复制x的数据(修改新张量不会影响x)。x.detach():断开计算图。创建一个与x共享内存空间的新张量,但将其从 PyTorch 的自动求导(Autograd)计算图中剥离(常用于推理阶段或固定某些层参数)。x.view(...)/x.reshape(...):改变形状(Reshape)。基于x的数据生成一个新的形状,通常与x共享底层存储内存,不涉及数据复制,速度极快。
| 操作方式 | 精简解释 | 是否共享内存? | 是否断开计算图? | 关键注意点 | 示例代码 |
|---|---|---|---|---|---|
y = x (引用赋值) |
别名绑定 仅创建了一个指向原张量的 Python 引用,未创建新对象。 | 是 (完全同一块内存) | 否 (保留原计算图) | • 修改 y 会直接改变 x。 •
并非真正意义上的拷贝,仅为同一对象的别名。 |
y = x y[0] = 999 (x[0] 也会变成
999) |
y = x.clone() (深拷贝) |
深拷贝 开辟全新物理内存空间,完整复制数据内容。 | 否 (独立全新内存) | 否 (保留原计算图) | • 修改 y 完全不影响 x。 •
clone() 动作本身会被记录在计算图中,梯度会通过
y 回传给 x。 |
y = x.clone() |
y = x.detach()
(断开计算图) |
截断梯度 创建一个与 x
共享数据内存的新张量,但剥离 Autograd 记录。 |
是 (共享底层数据) | 是 (截断梯度) | • 修改 y 的数值仍会影响
x! • 常用于固定参数、推理阶段或将张量转为
NumPy。 |
y = x.detach() |
y = x.view(...)
(维度变换) |
视图变换 在不改变底层内存排列的前提下,重构张量形状。 | 是 (共享底层数据) | 否 (保留原计算图) | • 要求内存必须连续(is_contiguous())。 • 修改
y 的数值会同步改变 x。 |
y = x.view(2, 3) |
y = x.reshape(...)
(安全维度变换) |
灵活变换 优先返回共享内存的视图;若内存不连续则自动触发内存拷贝。 | 视情况而定 (通常共享,必要时拷贝) | 否 (保留原计算图) | • 比 view() 更安全,无需手动调
.contiguous()。 •
绝大多数情况下共享内存,但不能保证 100% 共享。 |
y = x.reshape(2, 3) |
- 仅读取已有张量的形状属性 (
x.shape或x.size())
只需要 x
的形状,但想改变设备或类型,你可以直接把
x.shape(或
x.size())作为普通尺寸参数传给任何创建函数:
1 | |
2.1.3 张量的操作
- 加法操作
1 | |
- 索引操作
1 | |
注意与下面内容进行区分:
1 | |
| 表达式 | 内存行为 | 是否影响共享内存的其他变量? |
|---|---|---|
x = x + 1 |
开辟新内存,重新绑定变量名 x |
❌ 不会(断开原内存关联) |
x += 1 /
x.add_(1) |
就地修改(In-place),改动当前内存 | ✅ 会(共享内存变量同步改变) |
x[:] = x + 1 |
就地覆盖(In-place),改动当前内存 | ✅ 会(共享内存变量同步改变) |
- 取值操作
x.item()
的设计用途是把只包含“唯一一个元素”的张量(1-element tensor)转换为
Python 的原生数值(如 float 或 int)
1 | |
1 | |
1 | |
PyTorch中对Tensor 操作很多,包括转置、索引、切片、数学运算、线性代数、随机数等等,具体使用方法可参考官方文档
2.1.4 广播机制
概念与触发规则
- 概念:广播机制是 PyTorch(以及 NumPy)中一种极其强大的自动处理机制:当对两个形状(Shape)不同的张量进行逐元素算术运算(如加、减、乘、除)时,PyTorch 会在不复制物理内存的前提下,自动把两个张量扩展为相同的形状,以便按位置进行计算。
1 | |
触发规则:
两个张量能否广播,需要从右向左(从末尾维度开始)依次对齐比较各个维度:
- 维度大小相等;
- 其中一个张量的该维度大小为 1;
- 其中一个张量缺失该维度(例如二维张量与一维张量运算)。
只有当所有对应维度都满足上述条件之一时,广播才能成功。在计算时,维度大小为 \(1\) 的张量会被自动沿该维度“复制/拉伸”,达到与另一个张量相同的形状。
示例分析1
按照上述规则顺序对示例进行分析。如下。
第一步:列出两个张量的形状(Shape)
张量
x的形状:(1, 2)张量
y的形状:(3, 1)
第二步:从右向左(从末尾维度开始)逐维对齐比较
我们将两个形状从右端对齐放在一起比较:
| 维度位置 | x 的维度 | y 的维度 | 对齐比较结果 | 是否满足广播条件? | 最终广播目标维度 |
|---|---|---|---|---|---|
| 倒数第 1 维 (最右) | 2 |
1 |
x 的大小为 2,y 的大小为 1 |
满足 (符合规则 2:y 该维度大小为
1) |
2 (y 沿此维度拉伸) |
| 倒数第 2 维 | 1 |
3 |
x 的大小为 1,y 的大小为 3 |
满足 (符合规则 2:x 该维度大小为
1) |
3 (x 沿此维度拉伸) |
由于所有对应维度均符合广播规则,因此本次广播成功!
- 第三步:按规则推导最终的运算过程与形状
经过上述逐维分析后:
确定最终形状:结合每个维度的对齐结果,合并后的目标形状确定为
(3, 2)。张量
x的广播拉伸:- 原形状:
(1, 2)(数值为[[1, 2]]) - 在倒数第 2 维上从
1延伸到3(沿着行方向复制 3 次):
\[ \begin{bmatrix} 1 & 2 \end{bmatrix} \xrightarrow{\text{拉伸为 (3, 2)}} \begin{bmatrix} 1 & 2 \\ 1 & 2 \\ 1 & 2 \end{bmatrix} \]
- 原形状:
张量
y的广播拉伸:- 原形状:
(3, 1)(数值为[[1], [2], [3]]) - 在倒数第 1 维上从
1延伸到2(沿着列方向复制 2 次):
\[ \begin{bmatrix} 1 \\ 2 \\ 3 \end{bmatrix} \xrightarrow{\text{拉伸为 (3, 2)}} \begin{bmatrix} 1 & 1 \\ 2 & 2 \\ 3 & 3 \end{bmatrix} \]
- 原形状:
逐元素相加:
两者拉伸至相同的形状 (3, 2)
后,进行对应位置点对点相加,即得输出: \[
\begin{bmatrix} 1+1 & 2+1 \\ 1+2 & 2+2 \\ 1+3 & 2+3
\end{bmatrix} = \begin{bmatrix} 2 & 3 \\ 3 & 4 \\ 4 & 5
\end{bmatrix}
\]
示例分析2
再来一个举例。针对上述广播规则中的第3条规则。
当其中一个张量缺失某个维度时,广播机制的规则是:
在逻辑上,从左侧(高维方向)自动为缺失维度的张量补齐大小为 \(1\) 的维度,然后再按照规则进行拉伸。
一、 规则对齐分析
假设我们有两个张量:
- 张量
A的形状:(3, 4)(二维张量,例如 3 行 4 列的矩阵) - 张量
B的形状:(4,)(一维张量,包含 4 个元素)
当它们进行 A + B 运算时,PyTorch
会按如下步骤进行对齐:
- 自动在左侧补齐维度
一维张量 B 形状为
(4,),在最左边补齐一个维度后,逻辑形状变成了
(1, 4)。
- 从右向左逐维比较
| 维度位置 | A 的维度 | B 的逻辑维度 | 比较情况 | 广播行为 | 最终维度 |
|---|---|---|---|---|---|
| 倒数第 1 维 (最右) | 4 |
4 |
大小相等 | 无需广播(直接对应位置计算) | 4 |
| 倒数第 2 维 | 3 |
1 (补齐的) |
缺失维度 (补为 1) | 进行广播(B 沿此维度复制 3 次) |
3 |
最终相加结果的形状为
(3, 4)。
二、 代码与图解示例
我们来看一个实际的代码场景:给矩阵的每一行都加同一个向量。
Python
1 | |
计算过程分解:
A的数值(3 行 4 列):\[\begin{bmatrix} 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 \end{bmatrix}\]
B的自动扩展过程:原形状:
[1, 2, 3, 4]形状为(4,)补齐左侧维度为
(1, 4):\[\begin{bmatrix} 1 & 2 & 3 & 4 \end{bmatrix}\]
沿第 0 维(行方向)复制 3 份,拉伸为
(3, 4):\[\begin{bmatrix} 1 & 2 & 3 & 4 \\ 1 & 2 & 3 & 4 \\ 1 & 2 & 3 & 4 \end{bmatrix}\]
最终输出结果
C:\[\begin{bmatrix} 1+1 & 1+2 & 1+3 & 1+4 \\ 1+1 & 1+2 & 1+3 & 1+4 \\ 1+1 & 1+2 & 1+3 & 1+4 \end{bmatrix} = \begin{bmatrix} 2 & 3 & 4 & 5 \\ 2 & 3 & 4 & 5 \\ 2 & 3 & 4 & 5 \end{bmatrix}\]
神经网络中的经典应用
这个规则在深度学习中最常见的使用场景就是加上偏置项(Bias Addition):
假设网络隐藏层输出特征矩阵 X 形状为
(batch_size, feature_dim)(比如 (64, 128),即
64 个样本,每个样本 128 个特征),而偏置项 b 只是一个形状为
(128,) 的一维向量。
直接执行 X + b 时,PyTorch
就会利用“缺失维度自动补齐”的规则,把 b
自动扩展到每一个样本上,无缝完成偏置加法!
2.2 自动微分
PyTorch 中,所有神经网络的核心是
autograd包。autograd包为张量上的所有操作提供了自动求导机制。它是一个在运行时定义
( define-by-run
)的框架,这意味着反向传播是根据代码如何运行来决定的,并且每次迭代可以是不同的。用一句总结就是:PyTorch
中的 autograd 包
是实现神经网络反向传播与自动求导的核心引擎。
2.2.1 核心要点概括
- 动态计算图(Define-by-Run)
- 运行时构建:计算图是在代码执行前向传播(Forward Pass)时实时动态生成的,无需预先定义静态图。
- 灵活性高:反向传播完全取决于当前代码的实际执行路径,支持在每次迭代中使用不同的网络结构(如条件判断、循环语句等)。
- 三大核心机制与属性
requires_grad=True:将 Tensor 的此属性设为
True时,PyTorch 开始追踪在该 Tensor 上的所有数学运算,以便后续计算梯度。grad_fn:记录创建该 Tensor 的计算函数(如
AddBackward0),用于在反向传播时根据链式法则回溯节点。.backward()与.grad:调用标量变量的
.backward()方法会自动触发反向传播,求出的导数结果将累加并存储在对应叶子节点 Tensor 的.grad属性中。
- 常见控制开关
torch.no_grad():上下文管理器,用于在推理/测试阶段关闭梯度追踪与计算,能显著减少显存占用并提升计算速度。
2.2.2 autograd简介
torch.Tensor是这个包的核心类。如果设置它的属性.requires_grad
为
True,那么它将会追踪对于该张量的所有操作。当完成计算后可以通过调用.backward(),来自动计算所有的梯度。这个张量的所有梯度将会自动累加到.grad属性。
注意:在 y.backward() 时,如果 y 是标量,则不需要为 backward() 传入任何参数;否则,需要传入一个与 y 同形的Tensor。
要阻止一个张量被跟踪历史,可以调用.detach()方法将其与计算历史分离,并阻止它未来的计算记录被跟踪。为了防止跟踪历史记录(和使用内存),可以将代码块包装在
with torch.no_grad():中。在评估模型时特别有用,因为模型可能具有
requires_grad = True
的可训练的参数,但是我们不需要在此过程中对他们进行梯度计算。
还有一个类对于autograd的实现非常重要:Function。Tensor和Function
互相连接生成了一个无环图 (acyclic
graph),它编码了完整的计算历史。每个张量都有一个.grad_fn属性,该属性引用了创建该
Tensor 的 Function
(除非这个张量是用户手动创建的,即这个张量的grad_fn是
None )。
2.2.2.1 requires_grad标识
创建一个张量可以手动指定其requires_grad属性的值,如果未指定、默认情况下此属性的值是False。且张量的此参数值在张量创建出来以后还可以修改。
1 | |
2.2.2.2 示例1-手动创建张量grad_fn属性为None
当张量由用户手动创建时,其grad_fn属性返回结果是None。
1 | |
原理解析
y.grad_fn:y是通过x ** 2 + 2算出来的,它的最后一步运算是加法+,因此它的.grad_fn属性指向了一个<AddBackward0>对象。这个对象记录了“如何对加法求导”的算子逻辑。z.grad_fn:z是通过对y调用.sum()得到的,因此它的.grad_fn属性指向了一个<SumBackward0>对象。
当后续调用 z.backward() 进行反向传播时,PyTorch
的自动求导引擎(Autograd)就是沿着这一条由 .grad_fn
构成的链条逆向追溯,层层推导计算出 x
的梯度的。
2.2.2.3 示例2-展示反向传播过程
1 | |
为了彻底看清这套机制,现直接打印出上述代码反向传播自动求导完整的链条结构,并手动演算一次梯度,以直观看到
.grad_fn 是如何把计算图“串”起来的。
详细过程拆解(把逻辑图可视化)
在内存中,代码前向传播时,PyTorch 默默建立了一张动态计算图:
1 | |
当调用 z.backward() 时,后台发生了什么?
Autograd 引擎就像一个顺藤摸瓜的机器人:
- 起点:找到
z的.grad_fn(即<SumBackward0>),计算出 \(\frac{\partial z}{\partial y} = [1, 1]\)。 - 第一次追溯:顺着
<SumBackward0>.next_functions找到上一级节点<PowBackward0>(即y的.grad_fn)。 - 链式法则计算:
<PowBackward0>接收到上一级传来的梯度,结合链式法则计算 \(\frac{\partial y}{\partial x} = 2x\),算出 \(\frac{\partial z}{\partial x} = 1 \times 2x = 2x\)。 - 第二次追溯:继续顺着
<PowBackward0>.next_functions向上找,发现到了叶子节点x的累加节点(<AccumulateGrad>)。 - 终点与赋值:把最终算出的梯度
2x(即 \(2 \times [2.0, 3.0] = [4.0, 6.0]\))存入x.grad属性中,追溯结束。
核心总结
.grad_fn就像是面包屑导航:每个由运算产生的张量都用.grad_fn记录着“我是被哪个数学函数算出来的”。next_functions就像是链条的锁扣:把前后的算子串在一起,形成一条可以倒着走回源头x的通路。backward()就是倒着走这个链条:按照微积分的链式法则(Chain Rule),从终点z一路乘回起点x,最终把算好的梯度写入x.grad。
2.2.2.4 示例3-反向传播的数学推导和代码逻辑
1 | |
结合刚才的示例,把数学推导和代码逻辑一步步拆解出来。
2.2.2.4.1 初始条件与前向传播
假设有:
- 输入向量:\(\vec{x} = \begin{bmatrix} x_1 \\ x_2 \end{bmatrix} = \begin{bmatrix} 2.0 \\ 3.0 \end{bmatrix}\)
- 前向运算:\(\vec{y} = \vec{x}^2 = \begin{bmatrix} x_1^2 \\ x_2^2 \end{bmatrix} = \begin{bmatrix} 2.0^2 \\ 3.0^2 \end{bmatrix} = \begin{bmatrix} 4.0 \\ 9.0 \end{bmatrix}\)
- 传入的梯度权重向量:\(\vec{v} = \begin{bmatrix} v_1 \\ v_2 \end{bmatrix} = \begin{bmatrix} 1.0 \\ 1.0 \end{bmatrix}\)
代码对应的调用是:y.backward(torch.tensor([1.0, 1.0]))
2.2.2.4.2 数学推导过程
步骤 1:构造等价标量 \(S\)
根据公式 \(S = \vec{y} \cdot \vec{v} = y_1 v_1 + y_2 v_2\),将 \(\vec{y}\) 代入:
\[S = (x_1^2 \cdot v_1) + (x_2^2 \cdot v_2)\]
把已知数值 \(v_1 = 1.0, v_2 = 1.0\) 带入:
\[S = x_1^2 \cdot 1.0 + x_2^2 \cdot 1.0 = x_1^2 + x_2^2\]
步骤 2:对各分量分别求偏导数
为了得到梯度 \(\nabla_{\vec{x}} S = \begin{bmatrix} \frac{\partial S}{\partial x_1} \\ \frac{\partial S}{\partial x_2} \end{bmatrix}\),需要利用微积分求导法则:
对 \(x_1\) 求偏导(把 \(x_2\) 视为常数):
\[\frac{\partial S}{\partial x_1} = \frac{\partial}{\partial x_1}(x_1^2 \cdot v_1) + 0 = 2 \cdot x_1 \cdot v_1\]
对 \(x_2\) 求偏导(把 \(x_1\) 视为常数):
\[\frac{\partial S}{\partial x_2} = 0 + \frac{\partial}{\partial x_2}(x_2^2 \cdot v_2) = 2 \cdot x_2 \cdot v_2\]
即梯度的符号表达式为:
\[\text{grad} = \begin{bmatrix} 2 \cdot x_1 \cdot v_1 \\ 2 \cdot x_2 \cdot v_2 \end{bmatrix}\]
步骤 3:带入具体的数值
将前向传播时的 \(x_1 = 2.0, x_2 = 3.0\) 以及 \(v_1 = 1.0, v_2 = 1.0\) 带入上面的偏导公式:
第 1 个分量的梯度:
\[\frac{\partial S}{\partial x_1} = 2 \times 2.0 \times 1.0 = 4.0\]
第 2 个分量的梯度:
\[\frac{\partial S}{\partial x_2} = 2 \times 3.0 \times 1.0 = 6.0\]
2.2.2.4.3 最终结果
经过上述计算,Autograd 将算出的梯度保存在 x.grad
中:
\[\text{x.grad} = \begin{bmatrix} 4.0 \\ 6.0 \end{bmatrix}\]
也就是 PyTorch
最终打印出来的结果:tensor([4., 6.])。
2.2.2.4.4
延伸思考:如果改变传入的 v 会发生什么?
如果传入一个不同的权重,比如
v = torch.tensor([1.0, 0.5]):
- 等价标量:\(S = 1.0 \cdot x_1^2 + 0.5 \cdot x_2^2\)(其实就是公式:\(S = v1 \cdot x_1^2 + v2 \cdot x_2^2\))
- 偏导公式:
- \(\frac{\partial S}{\partial x_1} = 2 \cdot x_1 \cdot v1 = 2 \times 2.0 \times 1.0 = 4.0\)
- \(\frac{\partial S}{\partial x_2} = 2 \cdot x_2 \cdot v2 = 2 \times 3.0 \times 0.5 = 3.0\)
- 最终
x.grad:tensor([4., 3.])
这就是为什么说传入的向量 \(\vec{v}\) 实际上就是给非标量输出的各个分量赋予了不同的求导权重。
2.2.3 梯度
2.2.3.1 向量函数
标量(Scalar) = 只有一个孤零零的数字(如:温度、体重、Loss值)。
向量(Vector) = 有顺序排列的一排数字(如:空间坐标、颜色RGB值、特征向量)。
向量函数 = 只要输出结果是一排数字(向量)的函数,就叫向量函数!
2.2.3.2 梯度计算分析
1 | |
因为out
是一个标量,因此out.backward()和out.backward(torch.tensor(1.))
等价。假如要求导的out
是一个严格的张量(非0维张量),那么调用out.backward()函数就需要传入一个与out
形状一样的张量参数(比如全1),否则报错。
数学上,对于向量函数 \(\vec{y} = f(\vec{x})\) ,其中\(\vec{y} 与 \vec{x}\) 都是一个向量(简单理解成一排数字),那么 \(\vec{y}\) 关于 \(\vec{x}\) 的梯度就是一个雅可比矩阵:
\[ J = \begin{pmatrix} \frac{\partial y_1}{\partial x_1} & \dots & \frac{\partial y_1}{\partial x_n} \\ \vdots & \ddots & \vdots \\ \frac{\partial y_m}{\partial x_1} & \dots & \frac{\partial y_m}{\partial x_n} \end{pmatrix} \]
其中因变量y的个数与自变量的个数可以不一样,一般情况下也不一样。比如天气查询系统,用户输入经度、纬度、时间,得到的某地某时的天气可以表述为一个由多个数组成的一维张量(也叫向量),其中每个数可能温度、湿度、降水概率、空气PM2.5指数、紫外线强度等等。
可以使用 torch.autograd
这个包来计算一些雅可比矩阵的乘积。例如,如果 \(v\) 是一个标量函数 \(l = g(\vec{y})\) 的梯度:\(v = \begin{pmatrix} \frac{\partial l}{\partial
y_1} & \dots & \frac{\partial l}{\partial y_m}
\end{pmatrix}\) ,其中向量函数 \(\vec{y} = f(\vec{x})\)
求梯度的方法已在上面描述。根据链式法则,我们可以得到标量函数 \(l\) 对 \(x\) 的最终梯度(即导数)$v J $ 如下: \[
v J = \begin{pmatrix} \frac{\partial l}{\partial y_1} & \dots &
\frac{\partial l}{\partial y_m} \end{pmatrix}
\begin{pmatrix}
\frac{\partial y_1}{\partial x_1} & \dots & \frac{\partial
y_1}{\partial x_n} \\
\vdots & \ddots & \vdots \\
\frac{\partial y_m}{\partial x_1} & \dots & \frac{\partial
y_m}{\partial x_n}
\end{pmatrix}
= \begin{pmatrix} \frac{\partial l}{\partial x_1} & \dots &
\frac{\partial l}{\partial x_n} \end{pmatrix}
\]
根据这套链式法则计算方法,不论经过了多少轮运算,我们总能计算出多轮计算后得到的最终变量out对最初的自变量x的梯度。
梯度在反向传播过程中默认是累积的,就是说如果做多次反向传播,后一次计算梯度值时会直接将前一次梯度值累加给当次成为新的梯度值,为了保证梯度是非累加后的,可以在反向传播之前把梯度清零(即先执行x.grad.data.zero_())。
1 | |
1 | |
1 | |
临时关闭自动求导引擎。当把代码包装在
with torch.no_grad(): 块中时,PyTorch
会暂时关闭自动求导引擎。即使输入张量(如
x)设置了
requires_grad=True,在该代码块内部进行的所有数学运算,都不会被记录到计算图中。在该代码块内产生的新张量,其
requires_grad 属性强制为 False,且
.grad_fn 为 None。
1 | |
为什么需要 torch.no_grad()?
在实际开发中,with torch.no_grad(): 最主要用于 模型评估 / 推理测试阶段(Model Evaluation / Inference) 和 网络参数更新:
- 大幅节省显存与内存:训练时需要保存中间变量以便反向传播;推理时不需要求导,用 no_grad() 就不需要额外留存计算图和中间激活值,能节省大量显存。
- 加速计算:不需要在后台维护计算图节点,提高前向传播的运行速度。
既想要修改 tensor 的数值,又不希望被 autograd 记录(即不会影响反向传播),那么就需要修改tensor.data 属性值。
1 | |
2.3 并行计算简介
分布式数据并行:https://docs.pytorch.org/tutorials/intermediate/ddp_tutorial.html、https://docs.pytorch.ac.cn/tutorials/intermediate/ddp_tutorial.html
使用PyTorch进行深度学习时,由于数据集比较大(一个GPU显存加载不了,可以使用多个GPU来加载)或为了提升计算速度(将模型分成几个部分放到多个不同GPU上,或将数据集分成几个部分放到不同GPU上进行计算),此时就需要使用并行计算来处理。
2.3.1 为什么要做并行计算
单个GPU无法完成计算、为了提升计算速度。
2.3.2 为什么需要CUDA
CUDA是NVIDIA提供的一种GPU并行计算框架。对 GPU
本身编程,是用 CUDA C/C++(C/C++ 加 CUDA 扩展)配合
CUDA Runtime API 实现的。CUDA 并不是一门从零开始的语言,而是 NVIDIA 在
C/C++ 基础上扩展关键字/内建变量/启动语法后形成的“C/C++ 扩展集”,再加上
Runtime/Driver API 与 nvcc 编译工具链共同组成的编程体系。
在我们使用PyTorch编写深度学习代码时,使用的CUDA是另一个意思,在PyTorch使用
CUDA表示要开始要求我们的模型或者数据开始使用GPU了。我们并不使用CUDA语言、而是通过Python语言编码来操纵与使用GPU。
在编写程序中,当我们使用了 .cuda()
时,其功能是让我们的模型或者数据从CPU迁移到GPU上(默认是0号GPU)当中,通过GPU开始计算。
关于PyTorch中使用显卡的注意事项:
- PyTorch 里用
.cuda()而不是.gpu(),是因为 CUDA 是 NVIDIA 私有的 GPU 编程栈,并非所有 GPU 都支持;命名沿用 Torch 历史,且刻意保留后端特异性(AMD 走 ROCm/HIP)。AMD 的 GPU 计算接口现在主推 ROCm(HIP 语言,语法接近 CUDA),PyTorch 官方已上游支持 ROCm 后端,AMD Instinct 等卡可直接跑 PyTorch。 - 如非必要,尽量减少数据在CPU与GPU之间的复制操作。
- 虽然GPU天生用来执行高并发性操作的,速度很快。但对于简单计算、数据量很小的计算,可以在CPU上执行,此时如果硬放到GPU上计算反而因数据在CPU与GPU间的复制、上下文切换而得不偿失。
- 使用GPU时养成指定GPU编号的习惯,避免默认总是使用0号GPU造成其显存溢出。
2.3.3 常见的并行计算方法
2.3.3.1 拆分模型做并行计算
思路是,将一个模型的各个部分拆分,然后将不同的部分放入到不同GPU来做不同任务的计算。
又可以细分为两种:(1)网络分割(Network partitioning)。将模型的不同层或不同组件拆分开,放到不同GPU上做不同任务。(2)逐层划分(Layer-wise partitioning)。将同一层的模型做一个拆分,让不同的GPU去训练同一层模型的部分任务。
对于GPU之间的通信是一个考验。GPU的通信在这种密集任务中很难办到,所以这个方式慢慢淡出了视野。
2.3.3.2 拆分数据做并行计算
数据并行(Data parallelism)。不拆分模型,而是将输入的数据拆分多个部分分别放在不同的GPU上,同一个模型在不同的GPU上训练自己分到的那部分数据,然后将每个GPU上任务完成后得到的结果进行汇总、反向更新给每个GPU上的模型。
这是当前主流的并行计算方式。
2.3.4 使用CUDA加速训练
2.3.4.1 单卡训练
1 | |
2.3.4.2 多卡训练
单机多卡DP
1 | |
多机多卡DDP
1 | |