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中的张量支持高效的数学运算,并且可以自动求导,是构建神经网络的基础。
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)
(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列的所有行)
(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]])
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
对应元素的乘积。
(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块
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
对象。