Pytorch 1 :Basic Components
导言
Pytorch基础知识,包括张量的使用,和自动微分。
Pytorch简介¶
Pytorch前生Torch
Torch是一个有大量机器学习算法支持的科学计算框架,是一个与Numpy类似的张量(Tensor) 操作库,其特点是特别灵活,但因其采用了小众的编程语言是Lua,所以流行度不高,这也就有了PyTorch的出现。所以其实Torch是 PyTorch的前身,它们的底层语言相同,只是使用了不同的上层包装语言。
PyTorch是一个基于Torch的Python开源机器学习库,用于自然语言处理等应用程序。它主要由Facebookd的人工智能小组开发,不仅能够 实现强大的GPU加速,同时还支持动态神经网络,这一点是现在很多主流框架如TensorFlow都不支持的。 PyTorch提供了两个高级功能:
- 具有强大的GPU加速的张量计算(如Numpy)
- 包含自动求导系统的深度神经网络
主要定位两类人群:
- NumPy 的替代品,可以利用 GPU 的性能进行计算。
- 深度学习研究平台拥有足够的灵活性和速度
优势
- 支持GPU
- 灵活,支持动态神经网络
- TensorFlow和Caffe都是命令式的编程语言,而且是静态的,首先必须构建一个神经网络,然后一次又一次使用相同的结构,如果想要改变网络的结构,就必须从头开始。
- 但是对于PyTorch,通过反向求导技术,可以让你零延迟地任意改变神经网络的行为,而且其实现速度快。正是这一灵活性是PyTorch对比TensorFlow的最大优势。
- 底层代码易于理解
- 命令式体验
- 自定义扩展
劣势
对比TensorFlow,有些处于劣势,目前PyTorch
- 还不支持快速傅里叶、沿维翻转张量和检查无穷与非数值张量;
- 针对移动端、嵌入式部署以及高性能服务器端的部署其性能表现有待提升;
- 其次因为这个框 架较新,使得他的社区没有那么强大,在文档方面其C库大多数没有文档。
安装和使用¶
官网 选择对应cuda版本下载即可
好的!张量(Tensors)是PyTorch的核心数据结构,类似于NumPy中的多维数组(ndarray),但张量支持GPU加速计算,因此在深度学习中非常高效。下面我将详细展开讲解张量的创建、操作以及一些常用功能。1. 什么是张量?¶
张量是一个多维数组,可以表示标量、向量、矩阵以及更高维的数据结构:
- 0维张量:标量(Scalar),例如 5
- 1维张量:向量(Vector),例如 [1, 2, 3]
- 2维张量:矩阵(Matrix),例如 [[1, 2], [3, 4]]
- 3维及以上张量:高维数组,例如图像数据通常是3维张量(通道×高度×宽度)。
PyTorch中的张量支持高效的数学运算,并且可以自动求导,是构建神经网络的基础。
1. 张量的「信息区」和「存储区」¶
(1) 存储区(Storage)¶
- 物理内存:存储区是张量实际数据所在的连续内存块。
- 共享性:多个张量可以共享同一存储区(通过视图操作,如
view()
、slice()
等)。 - 示例:
(2) 信息区(Meta Data)¶
- 逻辑描述:包含张量的形状(
shape
)、步长(stride
)、数据类型(dtype
)、设备(device
)等元信息。 - 不涉及内存拷贝:修改信息区(如
view()
)不会改变存储区。
2. view()
操作¶
- 纯视图操作:仅修改信息区的
shape
和stride
,不改变存储区。 - 前提条件:张量必须是内存连续的(否则会报错)。
- 共享存储区:与原张量共享同一存储区。
x = torch.arange(1, 9) # shape: (8), stride: (1)
y = x.view(2, 4) # shape: (2,4), stride: (4,1)
# 修改y会影响x(共享存储区)
y[0, 0] = 100
print(x) # tensor([100, 2, 3, 4, 5, 6, 7, 8])
不连续时的报错
x = torch.randn(2, 3).transpose(0, 1) # 不连续张量
try:
y = x.view(6) # 报错!
except RuntimeError as e:
print(e) # "view size is not compatible with input tensor's..."
3. reshape()
操作¶
- 智能选择:若张量连续,则行为与
view()
相同(共享存储区);若张量不连续,则自动调用contiguous()
创建新存储区。 - 无报错保证:无论原张量是否连续,均可执行。
# 连续张量:行为同view()
x = torch.arange(1, 9) # 连续
y = x.reshape(2, 4) # 共享存储区
y[0, 0] = 100 # 影响x
# 不连续张量:自动拷贝
x = torch.randn(2, 3).transpose(0, 1) # 不连续
y = x.reshape(6) # 自动调用contiguous(),创建新存储区
y[0] = 100
print(x[0, 0]) # 原张量未被修改
unsqueeze(1)¶
unsqueeze(1)
用于在指定维度(这里是第1维)上增加一个长度为1的维度,从而扩展张量的形状(shape)。这个操作不会改变张量的数据,只是调整它的维度结构,类似于 NumPy 中的np.expand_dims()
。
unsqueeze
一维张量 → 二维张量(列向量)**
import torch
x = torch.tensor([1, 2, 3]) # shape: (3,)
y = x.unsqueeze(1) # 在第1维(列方向)插入维度
print(y.shape) # shape: (3, 1)
print(y)
输出:
- 解释:原始形状
(3,)
→ 插入第1维后变为(3, 1)
(3行1列的矩阵)。
与 view()
/reshape()
的区别¶
方法 | 作用 | 是否改变数据 | 是否要求连续性 |
---|---|---|---|
unsqueeze |
仅插入维度 | ❌ 否 | ✅ 不要求 |
view /reshape |
重新调整形状 | ❌ 否 | view 要求连续存储 |
关键区别:unsqueeze
仅扩展维度,而 view
/reshape
可以任意调整形状(但需元素总数一致)。
其他操作¶
Storage()
返回存储区内容stride()
是在指定维度dim中从一个元素跳到下一个元素所必需的步长。当没有参数传入时,返回所有步长的元组。否则,将返回一个整数值作为特定维度dim中的步长。[b][c][t]
对应物理内存的位置通过以下公式计算:内存位置 = b * stride[0] + c * stride[1] + t * stride[2]
-
permute
重新排列维度顺序,但不改变内存中的数据,只是修改步长(stride)和形状(shape)。- 视图操作:permute 是零拷贝操作,性能极高。
- 可能破坏连续性:新步长可能导致内存不连续。连续性条件:步长必须满足
stride[i] = stride[i+1] * shape[i+1]
-
torch.sort(x, descending=True)
# 降序 torch.arange()
是 PyTorch 中用于生成一个一维张量(向量)的函数,该张量包含从起始值到结束值(不包括结束值)的均匀间隔序列。torch.where(condition, x, y) → Tensor
torch.where 是 PyTorch 中的一个条件选择函数,它根据给定的条件(布尔张量)从两个输入张量中选择元素。其功能类似于 NumPy 的 np.where 或 Python 的三元表达式 x if condition else y,但支持张量的并行化计算和 GPU 加速。torch.zeros_like(...)
:生成与 weighted_down_out 形状相同的全零张量。torch.full_like(input, fill_value, dtype=None, device=None) → Tensor
返回:与 input 形状相同的张量,所有元素均为 fill_value。.numel()
: 计算切片后形状元组中所有维度的乘积(即元素总数)。对于 (B, T),结果为 B * T。.expand(-1, top_k)
[num_tokens, 1] 沿第-1(最后)维度扩展(复制),变成 [num_tokens, top_k]
2. 创建张量¶
PyTorch提供了多种创建张量的方式,以下是一些常见的创建方法:
(1) 从Python列表或NumPy数组创建¶
import torch
# 从列表创建张量
tensor_from_list = torch.tensor([1, 2, 3])
print(tensor_from_list) # 输出: tensor([1, 2, 3])
# 从NumPy数组创建张量
import numpy as np
numpy_array = np.array([4, 5, 6])
tensor_from_numpy = torch.from_numpy(numpy_array)
print(tensor_from_numpy) # 输出: tensor([4, 5, 6], dtype=torch.int32)
(2) 创建特定形状的张量¶
# 创建全零张量
zeros_tensor = torch.zeros(2, 3) # 2行3列的全零矩阵
print(zeros_tensor)
# 创建全一张量
ones_tensor = torch.ones(2, 3) # 2行3列的全一矩阵
print(ones_tensor)
# 创建随机张量
rand_tensor = torch.rand(2, 3) # 2行3列的随机值矩阵(值在0到1之间)
print(rand_tensor)
rand vs randn
- torch.rand:均匀分布 (Uniform Distribution)
- torch.randn:标准正态分布 (Standard Normal Distribution)
(3) 创建特定范围的张量¶
# 创建等差序列张量
arange_tensor = torch.arange(0, 10, 2) # 从0开始,步长为2,小于10
print(arange_tensor) # 输出: tensor([0, 2, 4, 6, 8])
# 创建线性间隔张量
linspace_tensor = torch.linspace(0, 1, 5) # 从0到1,均匀分成5个数
print(linspace_tensor) # 输出: tensor([0.0000, 0.2500, 0.5000, 0.7500, 1.0000])
arange
torch.arange
是 PyTorch 中用于生成等间隔数值序列的函数。它可以接受一个、两个或三个参数,具体用法如下:
- 一个参数:
torch.arange(end)
- 生成从 0 到
end-1
的整数序列。 -
示例:
torch.arange(5)
生成张量[0, 1, 2, 3, 4]
。 -
两个参数:
torch.arange(start, end)
- 生成从
start
到end-1
的整数序列。 -
示例:
torch.arange(2, 5)
生成张量[2, 3, 4]
。 -
三个参数:
torch.arange(start, end, step)
- 生成从
start
到end-1
的序列,步长为step
。 - 示例:
torch.arange(0, 1, 0.2)
生成张量[0.0, 0.2, 0.4, 0.6, 0.8]
。
(4) 创建与现有张量形状相同的张量¶
existing_tensor = torch.tensor([[1, 2], [3, 4]])
new_tensor = torch.zeros_like(existing_tensor) # 创建与existing_tensor形状相同的全零张量
print(new_tensor)
3. 张量的属性¶
每个张量都有一些重要的属性:
- 形状(Shape):张量的维度大小。
- 数据类型(dtype):张量中元素的数据类型(如
float32
、int64
等)。 - 设备(device):张量存储在CPU还是GPU上。
tensor = torch.rand(2, 3)
print("Shape:", tensor.shape) # 输出: torch.Size([2, 3])
print("Data type:", tensor.dtype) # 输出: torch.float32
print("Device:", tensor.device) # 输出: cpu (默认)
4. 张量的操作¶
PyTorch提供了丰富的张量操作,以下是一些常见的操作:
(1) 索引与切片¶
tensor = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(tensor[0, 1]) # 输出: tensor(2) (第0行第1列)
print(tensor[:, 1]) # 输出: tensor([2, 5]) (第1列的所有行)
X[:-1] #切片操作,选取除最后一个维度外的所有维度。例如,若原始形状是 (B, T, D),则切片后得到 (B, T)
在 PyTorch 和 Python 中,切片操作使用 start:end:step
的语法,而 三个冒号 :::
并不是标准语法。用户可能混淆了多维度切片的分隔符(逗号 ,
)与步长符号(双冒号 ::
)。以下是对切片中 ::
和多维度切片的详细解释:
一、::
双冒号的作用:步长控制¶
::
是 步长(stride) 的标记,语法为 [start:end:step]
,用于指定切片时元素的间隔。常见用法:
1. 正向步长:tensor[::2]
表示从头到尾每隔 2 个元素取一个。
-
反向步长:
tensor[::-1]
表示逆序切片。 -
多维张量中的步长:对某个维度单独设置步长。
二、多维度切片的分隔符(逗号 ,
)¶
在多维张量中,不同维度的切片通过 逗号 ,
分隔,每个维度可以独立设置 start:end:step
规则。例如:
# 4D张量(批次×通道×高度×宽度)
tensor = torch.rand(4, 3, 28, 28)
# 取前2个批次、所有通道、每隔2行和2列
sliced = tensor[:2, :, ::2, ::2] # 形状变为 (2, 3, 14, 14)
[:2, :, ::2, ::2]
表示:
• 第1维度:前2个元素;
• 第2维度:全部元素;
• 第3、4维度:步长为2。
三、常见应用场景¶
-
降采样(Downsampling):
通过步长快速缩小特征图尺寸,常用于减少计算量。 -
数据增强:
随机裁剪时结合步长生成不同视角的切片。 -
处理序列数据:
在时间序列中按步长提取子序列。
四、注意事项¶
- 内存共享:切片操作返回的是原张量的视图(view),修改切片会影响原数据。
- 维度对齐:逗号分隔的维度数需与张量维度一致,否则会报错。
- 负索引与步长:反向切片时需注意索引范围。
总结¶
• ::
用于步长控制,如 tensor[::2]
;
• 逗号 ,
分隔不同维度的切片规则,如 tensor[:, 1:8:2, :]
;
• :::
三冒号不存在,可能是对多维度切片的误解。
合理使用步长和多维度切片,可以高效处理图像、序列和高维数据。
(2) 改变形状¶
维度-1,自动推断维度大小
在使用 view
、reshape
方法时,如果其中一个维度的大小为 -1,那么 PyTorch 会自动推断这个维度的大小。
注意:
- 元素总数必须匹配:使用 view 变形时,新形状的元素总数必须与原张量的元素总数相同。否则会抛出错误。
- 连续性要求:view 操作要求张量是连续存储的(即内存布局是连续的)。如果张量不是连续的,可以先调用 .contiguous() 方法将其变为连续的再进行变形。
(3) 数学运算¶
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])
# 加法
print(a + b) # 输出: tensor([5, 7, 9])
# 矩阵乘法
matrix_a = torch.tensor([[1, 2], [3, 4]])
matrix_b = torch.tensor([[5, 6], [7, 8]])
print(torch.matmul(matrix_a, matrix_b)) # 输出: tensor([[19, 22], [43, 50]])
# 是 PyTorch 中的一个张量操作,用于按照指定的索引将源张量(source)的值累加到目标张量(input)的指定位置上。它是一个原地操作(in-place,函数名末尾的下划线 _ 表示这一点),会直接修改目标张量。
torch.index_add_
# cumsum 是 累加求和(Cumulative Sum) 的操作,它会沿着张量的某个维度逐步计算元素的累加和。
torch.cumsum(input, dim, *, dtype=None, out=None) → Tensor
torch.einsum
¶
torch.einsum
用于执行基于爱因斯坦求和约定(Einstein summation convention)的张量操作。爱因斯坦求和约定是一种表示张量运算的简洁方式,可以用于矩阵乘法、外积、转置等多种操作。
爱因斯坦求和约定通过使用索引标签来指定张量的维度,并通过重复索引标签来表示求和操作。具体来说:
- 单个索引标签:表示一个维度。
- 重复的索引标签:表示在这些维度上进行求和。
- 不同的索引标签:表示新的维度。
torch.einsum(equation, *operands)
其中:
- equation
是一个字符串,描述了输入张量如何组合以生成输出张量。
- *operands
是输入张量。
矩阵转置
矩阵乘法
假设你有两个矩阵 A
和 B
,你想计算它们的矩阵乘法 C = A @ B
。
import torch
A = torch.tensor([[1, 2], [3, 4]])
B = torch.tensor([[5, 6], [7, 8]])
# 使用 einsum 进行矩阵乘法
C = torch.einsum('ik,kj->ij', A, B)
print(C)
输出:
解释:
- ik
表示矩阵 A
的维度。
- kj
表示矩阵 B
的维度。
- ij
表示结果矩阵 C
的维度。
- 重复的索引 k
表示在该维度上进行求和。
外积(Outer Product)
外积(Outer Product)是一种张量运算,用于生成一个新的张量,其维度是输入张量维度的组合。具体来说,外积操作将两个向量扩展为一个矩阵,其中每个元素是两个向量对应元素的乘积。
给定两个向量 \( \mathbf{a} \) 和 \( \mathbf{b} \),它们的外积 \( \mathbf{C} \) 定义为:
其中 \( \mathbf{a} \) 是一个 \( m \)-维向量,\( \mathbf{b} \) 是一个 \( n \)-维向量,结果矩阵 \( \mathbf{C} \) 是一个 \( m \times n \) 的矩阵。
假设我们有两个向量:
[ \mathbf{a} = \begin{bmatrix} 1 \ 2 \ 3 \end{bmatrix} ] [ \mathbf{b} = \begin{bmatrix} 4 \ 5 \end{bmatrix} ]
它们的外积 \( \mathbf{C} \) 计算如下:
torch.einsum
计算外积
t
是一个一维张量(向量),假设其形状为(m,)
。inv_freq
是另一个一维张量(向量),假设其形状为(n,)
。"i,j->ij"
是 einsum 的方程字符串:i
表示t
的维度。j
表示inv_freq
的维度。ij
表示结果张量freqs
的维度。- 结果
freqs
是一个形状为(m, n)
的二维张量,其中每个元素freqs[i, j] = t[i] * inv_freq[j]
。
假设 t
和 inv_freq
的值如下:
import torch
t = torch.tensor([1, 2, 3], dtype=torch.float32)
inv_freq = torch.tensor([0.1, 0.2], dtype=torch.float32)
freqs = torch.einsum("i,j->ij", t, inv_freq)
print(freqs)
输出:
解释:
- t
是一个形状为 (3,)
的向量。
- inv_freq
是一个形状为 (2,)
的向量。
- freqs
是一个形状为 (3, 2)
的矩阵,其中每个元素是 t
和 inv_freq
对应元素的乘积。
scatter_add_¶
scatter_add_(dim, index, src)
沿 dim=0 方向,将 src(ones)的值累加到 token_counts 的 index(filtered_experts)指定位置。 示例: filtered_experts = [1, 3, 4, 4],ones = [1, 1, 1, 1]。 执行后:
token_counts[1] += 1 → [0, 1, 0, 0, 0]
token_counts[3] += 1 → [0, 1, 0, 1, 0]
token_counts[4] += 1 → [0, 1, 0, 1, 1]
token_counts[4] += 1 → [0, 1, 0, 1, 2]
最终 token_counts = [0, 1, 0, 1, 2]。
torch.bincount 也能实现相同功能,但:
1. 性能问题:scatter_add_ 是原位操作(in-place),通常比 bincount 更快。 2. 显存优化:避免创建临时张量,减少显存占用。
(4) 广播机制¶
PyTorch支持广播机制,允许对不同形状的张量进行运算:
(5) 张量的拼接与分割¶
# 拼接
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])
print(torch.cat((a, b), dim=0)) # 输出: tensor([1, 2, 3, 4, 5, 6])
# 分割
tensor = torch.arange(6)
chunks = torch.chunk(tensor, 2) # 将张量分成2块,将张量沿指定维度(dim)均分成指定数量的块。
print(chunks) # 输出: (tensor([0, 1, 2]), tensor([3, 4, 5]))
cat 用法
torch.cat
是 PyTorch 中用于在给定维度上拼接张量的函数。以下是 torch.cat
的使用说明和 dim
参数的解释:
-
参数说明:
-
tensors
: 需要拼接的张量序列,可以是元组或列表。 dim
: 指定在哪个维度上进行拼接,默认值为 0。dim=-1
表示最后一个维度。dim=-2
表示倒数第二个维度,依此类推。
示例 一维张量拼接
import torch
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])
result = torch.cat((a, b), dim=0)
print(result) # 输出: tensor([1, 2, 3, 4, 5, 6])
二维张量拼接
a = torch.tensor([[1, 2], [3, 4]])
b = torch.tensor([[5, 6], [7, 8]])
# 在第 0 维拼接
result_0 = torch.cat((a, b), dim=0)
print(result_0)
# 输出:
# tensor([[1, 2],
# [3, 4],
# [5, 6],
# [7, 8]])
# 在第 1 维拼接
result_1 = torch.cat((a, b), dim=1)
print(result_1)
# 输出:
# tensor([[1, 2, 5, 6],
# [3, 4, 7, 8]])
通过这些示例可以看出,dim
参数决定了拼接的方向和方式。
5. 张量与GPU¶
PyTorch支持将张量移动到GPU上进行加速计算:
# 检查是否有可用的GPU
if torch.cuda.is_available():
device = torch.device("cuda") # 使用GPU
else:
device = torch.device("cpu") # 使用CPU
# 将张量移动到GPU
tensor = torch.tensor([1, 2, 3])
tensor = tensor.to(device)
print(tensor.device) # 输出: cuda:0 (如果GPU可用)
自动求导机制(Autograd)¶
PyTorch 的自动求导机制(Autograd)是它的一个核心功能,能够自动计算梯度,从而简化了反向传播和优化过程。它通过 torch.autograd
模块来实现,主要用于自动计算张量的梯度,并执行链式法则(即,反向传播)。
In-place 操作的正确性检查
在 PyTorch 中,In-place 操作指的是直接修改张量内容的操作,而不是创建一个新张量。例如,tensor.add_()
、tensor.mul_()
等函数就是典型的 in-place 操作。In-place 操作有一个潜在的问题:它们会修改原始数据,这可能会影响到计算图,从而导致梯度计算出现问题。
在训练过程中,PyTorch 会构建一个计算图,其中包含了所有的操作和依赖关系。在进行反向传播(backward)时,PyTorch 会依照计算图计算梯度。如果你对某个张量进行了 in-place 操作,可能会破坏计算图的结构,导致反向传播失败。
因此,PyTorch 会检查你是否在计算图中正在被使用的 Variable 上执行了 in-place 操作。如果发生这种情况,PyTorch 会在反向传播时抛出错误。这确保了,如果没有错误,反向传播中的梯度计算一定是正确的。
举个例子,如果你有一个张量 x
,并对它执行了 x.add_(5)
(in-place 操作),然后在反向传播时 PyTorch 会发现这个张量已经被修改,导致错误。所以 PyTorch 会提醒你避免这种情况发生。
基本概念¶
-
张量(Tensor):
-
PyTorch 中的数据类型叫做
Tensor
,它可以在计算图中存储数据,并支持梯度计算。 - 当一个张量需要计算梯度时,
requires_grad
参数必须设置为True
。
-
计算图(Computation Graph):
-
在 PyTorch 中,所有的操作(加法、乘法、矩阵乘法等)都会构成一个计算图,这个图记录了张量之间的运算关系。
- 只有
requires_grad=True
的张量及其操作会被记录在计算图中; - 非
requires_grad=True
的张量(如默认的requires_grad=False
)不会被追踪,它们的计算不影响计算图和梯度的计算。
- 只有
- 每个操作都会被记录为一个节点,边表示张量之间的关系。
- 当我们执行反向传播时,PyTorch 会根据这个计算图逐步计算梯度。
-
梯度(Gradients):
-
梯度是用来指导模型学习(模型参数通过梯度+优化器更新)的,通常在训练过程中通过反向传播来计算。
- PyTorch 的
autograd
会自动追踪每个张量的操作,并计算梯度。
主要功能和操作¶
1. requires_grad
参数¶
- 当你创建一个张量时,如果你想计算它的梯度,必须设置
requires_grad=True
, 否则默认值是 False。 - 如果
requires_grad
为True
,PyTorch 会追踪该张量的所有操作。
在这个例子中,x
被标记为需要计算梯度,因此 y
和 z
也会被自动记录到计算图中。
什么张量需要计算梯度
- 固定张量是不需要计算梯度的,比如不变的输入数据、计算中常量的、模型中的常量参数。
- 需要迭代更新的张量是需要计算梯度的,一般是模型的可训练参数(如 nn.Linear 层的 weight 和 bias)。可以通过优化器和自动计算的梯度来更新这些张量。
# model.parameters():每个 nn.Module 子类(例如 Linear)都有 parameters() 方法,
# 返回该层的所有可训练参数(例如权重和偏置)。这些参数的梯度会在反向传播中自动计算。
optimizer = optim.SGD(model.parameters(), lr=0.01) # 随机梯度下降优化器
# 输入数据和目标
x = torch.randn(1, 2) # 假设输入是2维数据
y_true = torch.randn(1, 1) # 目标输出
# 前向传播
y_pred = model(x)
# 计算损失
loss = criterion(y_pred, y_true)
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
2. backward()
方法¶
当计算图建立完毕并且需要进行反向传播时,调用 backward()
方法,它会计算梯度并存储在对应张量的 .grad
属性中。
这个 backward()
方法会计算 z
相对于 x
的梯度,并将结果存储在 x.grad
中。你可以通过 x.grad
访问梯度。
3. 禁用梯度计算(torch.no_grad()
)¶
在某些情况下,我们可能不希望追踪梯度,比如在模型推理(inference)时。可以通过 torch.no_grad()
上下文管理器来禁用梯度计算。
这样,y
的计算不会记录到计算图中,节省了内存和计算。
对于不需要计算梯度的张量
当你不需要某个张量参与梯度计算时,可以使用 .detach()
方法将该张量从计算图中分离出来,这样它就不会影响到后续的梯度计算。
detach()
方法返回一个新的张量,这个张量和原来的张量共享数据,但不会跟踪历史操作,也不会被用于计算梯度。
举个例子:
x = torch.tensor([1.0, 2.0], requires_grad=True)
y = x * 2 # y 是计算图的一部分,会记录操作
z = y.detach() # z 不会记录操作,不会参与梯度计算
当你不再需要计算梯度时,可以使用 detach()
将不需要的部分从计算图中“脱离”,例如在某些特定的推理任务中,你可能只需要计算某些张量的值,而不希望它们参与梯度计算。
这样做的好处是节省内存和计算资源,尤其在训练和推理分离的情况下非常常用。
4. retain_graph
参数¶
默认情况下,反向传播计算完成后,计算图会被销毁。但如果你需要多次反向传播(例如,在多次计算中反向传播),可以使用 retain_graph=True
来保留计算图。
5. grad_fn
属性¶
每个张量都包含一个 grad_fn
属性,记录了产生该张量的操作。如果你对一个张量进行了操作,PyTorch 会自动为该操作创建一个节点,并将它赋值给 grad_fn
。
6. 多张量反向传播¶
在某些情况下,可能有多个输出需要计算梯度,可以通过传递一个梯度输入来指定反向传播的起点。
示例:简单的自动求导
下面是一个简单的例子,展示了如何使用 autograd
:
import torch
# 创建一个张量,开启求导
x = torch.ones(2, 2, requires_grad=True)
# 执行一些操作
y = x * 2
z = y.mean()
# 反向传播计算梯度
z.backward()
# 查看梯度
print(x.grad)
这段代码中,x
是一个 2x2 的张量,经过乘法操作后,y
是一个包含 x * 2
的新张量,最后计算了 y
的平均值 z
。调用 z.backward()
会计算 z
相对于 x
的梯度,结果存储在 x.grad
中。
复杂组件的自动求导¶
复杂组件的可训练参数都是requires_grad=True
示例:高层组件的自动求导
- 在构建神经网络时会使用
nn.Module
类及其子类(如nn.Linear
),但是自动微分的机制在这些高层组件背后依然起作用。 torch.nn.Module
是所有神经网络层(例如Linear、Conv2d
等)和模型的基类。每个nn.Module
组件都有内部的张量(如权重和偏置),并且会执行一些计算。即使是这些高层组件,自动微分机制依然会工作,跟我们直接操作 Tensor 时的机制是相同的。
import torch
import torch.nn as nn
import torch.optim as optim
# 定义一个简单的神经网络模型
class SimpleModel(nn.Module):
def __init__(self):
super(SimpleModel, self).__init__()
self.linear = nn.Linear(2, 1) # 输入2个特征,输出1个预测值
def forward(self, x):
return self.linear(x)
# 创建模型、损失函数和优化器
model = SimpleModel()
criterion = nn.MSELoss() # 均方误差损失
# model.parameters():每个 nn.Module 子类(例如 Linear)都有 parameters() 方法,
# 返回该层的所有可训练参数(例如权重和偏置)。这些参数的梯度会在反向传播中自动计算。
optimizer = optim.SGD(model.parameters(), lr=0.01) # 随机梯度下降优化器
# 输入数据和目标
x = torch.randn(1, 2) # 假设输入是2维数据
y_true = torch.randn(1, 1) # 目标输出
# 前向传播
y_pred = model(x)
# 计算损失
loss = criterion(y_pred, y_true)
# 反向传播
loss.backward()
# 查看参数的梯度
print(model.linear.weight.grad) # 权重的梯度
print(model.linear.bias.grad) # 偏置的梯度
# 更新参数
optimizer.step()
可训练参数¶
识别¶
一般来说,
- 神经网络层(如
nn.Linear
、nn.Conv2d
、nn.BatchNorm2d
等)通常会有可训练参数, - 而像激活函数(
sigmoid
、ReLU
等)和归一化层(nn.MaxPool2d
、nn.AvgPool2d
等)中并不是所有的操作都有可训练参数。
可以通过以下几种方法来判断一个层或模块是否有可训练参数:
1. parameters()
检查
- 每个 PyTorch
nn.Module
(例如nn.Linear
、nn.Conv2d
等)都有一个parameters()
方法,它返回该模块的所有可训练参数。 - 如果该模块有可训练的参数(即
requires_grad=True
),这些参数就会出现在parameters()
中。
示例代码"
import torch
import torch.nn as nn
# 创建不同的层
linear_layer = nn.Linear(2, 1)
conv_layer = nn.Conv2d(1, 1, 3)
sigmoid_layer = nn.Sigmoid()
# 打印线性层的可训练参数
print("Linear layer parameters:")
for param in linear_layer.parameters():
print(param.shape)
# 打印卷积层的可训练参数
print("\nConv2d layer parameters:")
for param in conv_layer.parameters():
print(param.shape)
# 打印 Sigmoid 层的可训练参数
print("\nSigmoid layer parameters:")
for param in sigmoid_layer.parameters():
print(param.shape)
输出:
Linear layer parameters:
torch.Size([1, 2]) # weight
torch.Size([1]) # bias
Conv2d layer parameters:
torch.Size([1, 1, 3, 3]) # weight
torch.Size([1]) # bias
Sigmoid layer parameters:
- 对于
nn.Linear
和nn.Conv2d
层,parameters()
方法返回了 权重(weight) 和 偏置(bias),这些都是可训练参数。 - 对于
nn.Sigmoid
,它没有任何可训练参数,parameters()
返回的是空的。
通过 named_parameters()
方法检查
named_parameters()
方法类似于 parameters()
,但它会返回每个参数的名称,帮助你更清晰地看到每个参数的作用。
# 打印线性层的可训练参数名称和形状
print("Linear layer named parameters:")
for name, param in linear_layer.named_parameters():
print(f"{name}: {param.shape}")
输出:
这样,你可以更清楚地看到每个参数的名称(例如 weight
和 bias
)。
通过 requires_grad
属性检查
如果你只是想检查某个参数是否是可训练的,可以直接查看它的 requires_grad
属性。
自动注册参数¶
PyTorch 的 nn.Module
会自动管理你定义的层的可训练参数(如 weight
和 bias
),这是通过 Python 中的 torch.nn.Parameter
类型实现的。
torch.nn.Parameter
¶
torch.nn.Parameter
:当你定义模型的参数时(比如权重weight
或偏置bias
),如果你将它们定义为nn.Parameter
对象,它们会自动注册为该模块的可训练参数。PyTorch 会把这些Parameter
自动放入模块的parameters()
返回的列表中。nn.Module
会追踪这些Parameter
:一旦你将这些Parameter
注册到nn.Module
中,nn.Module
会自动管理它们的requires_grad
属性,并将它们添加到模块的参数集合中。
nn.Linear
层的实现
class Linear(nn.Module):
def __init__(self, in_features, out_features, bias=True):
super(Linear, self).__init__()
# weight 会自动成为 nn.Parameter 类型
self.weight = Parameter(torch.Tensor(out_features, in_features))
if bias:
# bias 会自动成为 nn.Parameter 类型
self.bias = Parameter(torch.Tensor(out_features))
else:
self.bias = None
self.reset_parameters()
在这个代码中:
self.weight
和self.bias
都是nn.Parameter
对象,因此它们会被自动注册为该Linear
层的可训练参数。nn.Module
会通过调用self.parameters()
或self.named_parameters()
来收集这些Parameter
对象。
注册流程¶
- 当你实例化一个层(例如
nn.Linear
)时,PyTorch 会自动为该层的权重和偏置(如果存在)创建nn.Parameter
对象,并将它们添加到模块的参数列表中。 - 这样,所有继承自
nn.Module
的层都会自动将它们的可训练参数注册到父类的parameters()
方法中。
只有self.xxx
才会被注册
这段代码实际上不会按你预期的方式注册层的参数。让我们来分析一下为什么:
class MyModel(nn.Module):
def __init__(self):
super(MyModel, self).__init__()
def forward(self, x):
tmp1 = nn.Linear(2, 2)
tmp2 = nn.Conv2d(1, 1, 3)
return tmp2(tmp1(x))
在 forward
方法中,你在每次执行前向传播时,创建了 tmp1
和 tmp2
两个层(nn.Linear
和 nn.Conv2d
)。但是问题是:
tmp1
和tmp2
是局部变量:它们仅在forward
方法内部存在,无法像类属性那样在类实例中持久保存。每次调用forward
时,都会重新创建这些层的实例。- 即使梯度被跟踪了,优化器也更新了参数,但是下次forward执行时也用不了,会重新创建新的参数。
管理¶
PyTorch 会通过内部机制来管理层的参数。具体来说:
- 当你调用
parameters()
或named_parameters()
时,nn.Module
会返回它管理的所有nn.Parameter
对象。 - 这些
Parameter
对象的requires_grad
属性会自动设置为True
,除非你显式地设置为False
。
class MyModel(nn.Module):
def __init__(self):
super(MyModel, self).__init__()
self.layer1 = nn.Linear(2, 2)
self.layer2 = nn.Conv2d(1, 1, 3)
def forward(self, x):
return self.layer2(self.layer1(x))
model = MyModel()
# 查看 model 中所有的可训练参数
for param in model.parameters():
print(param.shape)
parameters()
背后的实现机制
PyTorch 的实现并不会把所有层的参数直接放入一个 parameters()
列表,而是通过一种递归方式来查找所有子模块的参数。每当你在模型中创建一个子模块(如 nn.Linear
、nn.Conv2d
等),PyTorch 会自动注册该模块的 Parameter
对象,并将这些对象添加到父模块的参数集合中。
具体来说,nn.Module
类有一个 _modules
属性,这个属性用来存储该模块的所有子模块(例如 self.layer1
和 self.layer2
)。在调用 parameters()
时,nn.Module
会递归地查找所有子模块,并返回所有 Parameter
对象。