深度学习笔记【实践篇】一、Pytorch基础
深度学习绕不开的一个框架:PyTorch。今天整理了一下我学习实践过程中,常用到的一些基础概念和操作。
1. 常用的包&模块
| 功能模块 | 说明 | 示例 |
|---|---|---|
torch.nn | 包含各种网络层(全连接、卷积、池化) | nn.Linear(10, 2) |
torch.optim | 各种优化算法 | optim.SGD(model.parameters(), lr=0.01) |
torch.utils.data | 数据加载工具 | DataLoader(dataset, batch_size=32) |
torchvision | 处理图像相关的模型、数据和变换 | transforms.ToTensor() |
2. 创建张量
张量是 PyTorch 核心,也是基本的数据单元。和Numpy的array很类似,但张量是针对GPU特别优化的。
从列表创建:
torch.tensor([[1, 2], [3, 4]])创建全 0/1 张量:
torch.zeros((3, 3))
torch.ones((2, 3))随机初始化:
torch.rand(3, 3) # 0-1 均匀分布
torch.randn(3, 3) # 标准正态分布序列张量:
torch.arange(0, 10, 2) # 生成 [0, 2, 4, 6, 8]3. 张量的形状与转换
在处理神经网络时,改变数据的"形状"是最常见的操作。
查看形状:
x.shape # 或 x.size()改变维度 (Reshape):
x.contiguous() # 确保张量在内存中是连续的。开辟一块新空间。按照当前的逻辑顺序(比如转置后的顺序),把数据一个一个拷贝过去。
x.view(2, 5) # 要求张量在内存中是连续的(逻辑顺序和内存顺序一致),通常效率更高。只改变形状,不改变底层内存存储。
x.reshape(2, 5) # 推荐,更省心。相当于一个会自动调用.contiguous()的view。所以可能改变内存。增加/减少维度:
x.unsqueeze(0) # 在指定位置增加一个大小为 1 的维度。适配批处理要求
x.squeeze() # 移除所有大小为 1 的维度。去掉没用的“单维度”,简化计算。转置&重排:
x.transpose(dim0, dim1) # 交换两个维度。适配不同库的数据格式。
x.T # 针对 2D 张量,等同于 x.transpose(0, 1)
x.permute(*dims) # 重新排列维度。4. 数学运算
逐元素运算:
+, -, *, /
# 或使用函数形式
torch.add()
torch.mul()矩阵乘法 (Dot Product):
torch.matmul(A, B)
# 或使用简写
A @ B聚合操作:
torch.sum(x) # 求和
torch.mean(x) # 平均值(要求张量为浮点型)
torch.max(x) # 最大值
torch.min(x) # 最小值
torch.argmax(x) # 返回最大值的索引5. 自动求导
Autograd是深度学习的核心,PyTorch 会自动构建计算图并计算梯度。
开启梯度追踪:
x = torch.randn(3, 3, requires_grad=True)反向传播:
loss.backward() # 计算梯度并存储在 x.grad 中停止追踪梯度:
在测试或推理时,使用 with torch.no_grad(): 可以节省内存和计算资源。
with torch.no_grad():
# 推理代码
output = model(input)6. CPU 与 GPU (CUDA) 切换
将计算任务搬到显卡上,用于并行计算,可以加速训练。
device = "cuda" if torch.cuda.is_available() else "cpu"
x = x.to(device) # 将张量移动到 GPU
model = model.to(device) # 将模型参数移动到 GPU7. 张量的隐藏属性和操作
张量管理
x.data # 获取张量的数据。共享内存,不受 Autograd 系统的监控。⚠️危险操作,不要用!用x.detach()代替。
x.grad # 获取张量的梯度。梯度在反向传播后存储在这里,默认会累加。
x.is_contiguous() # 检查张量是否连续
x.clone() # 新内存,拷贝完全一样的张量,但是依然留在计算图中。对克隆后的张量求导,梯度会传回到原张量。
x.detach() # 共享内存,但是不追踪梯度。返回一个 requires_grad=False 的张量。
x.item() # 如果张量是标量(元素个数是1),返回张量的值。否则会报错。常见用法
x.detach().cpu().numpy() # 想把 Tensor 转成 NumPy:NumPy 不支持梯度,所以必须先 detach
x.detach().clone() # 得到一个不追踪梯度且新内存的张量(跟原张量完全无关了)8. PyTorch 底层略知一二(不明觉厉)
8.1 核心:动态图
PyTorch 的核心竞争力在于它的 动态图(Dynamic Graph / Eager Execution)。
TensorFlow 静态图(早期)。
动态图的优势:
- Pythonic:可以像调试普通 Python 程序一样,在计算图的任何位置打断点(pdb)
- 动态控制流:可以直接在模型中使用
if,while,for循环。每一轮迭代,PyTorch 都会根据逻辑重新"录制"一张新的计算图 - 灵活性:特别适合处理自然语言(长度不一)等动态场景
8.2 核心数据结构:Variable
torch.Tensor,对应的在 PyTorch 的底层(C++ 层面),每一个开启了梯度的张量实际上是一个 Variable 对象。它由三部分组成:
- Metadata:存储张量的形状(Shape)、步长(Stride)、设备(CPU/GPU)
- Data Tensor:实际存放数值的连续内存区域
- Autograd Meta:计算图的灵魂,包含:
grad:存储当前张量的梯度grad_fn:指向生成该张量的 Function 对象(正向传播时创建)next_functions:指向父节点的指针,构成了图的边
8.3 grad_fn:计算图的"反向索引"
grad_fn 是 PyTorch 实现自动求导(Autograd)的"探路者"。如果说 data 存储的是张量的当前值,那么 grad_fn 存储的就是这个张量的来源证明。
两种情况:
- 手动创建的张量(叶子节点):如果直接写
x = torch.tensor(1.0),它的grad_fn是None,因为它不是靠运算产生的,它是源头 - 计算出的张量:如果写
y = x * 2,那么y的grad_fn就是<MulBackward0>(乘法反向算子)
工作原理示例:
x = torch.tensor(2.0, requires_grad=True)
y = x ** 2 # y 的 grad_fn 是 <PowBackward0>
z = y + 10 # z 的 grad_fn 是 <AddBackward0>当执行 z.backward() 时:
- PyTorch 查看
z.grad_fn,发现它是AddBackward(加法) - Add 算子告诉系统:去看看我的输入(即
y) - 系统找到
y的grad_fn,发现是PowBackward(幂运算) - 以此类推,直到找到
x(叶子节点),然后把计算出的梯度填进x.grad
常见 grad_fn 类型对照表:
| 运算操作 | 对应的 grad_fn | 含义 |
|---|---|---|
a + b | <AddBackward...> | 加法操作产生的节点 |
a * b | <MulBackward...> | 乘法操作产生的节点 |
a.mean() | <MeanBackward...> | 均值操作产生的节点 |
a.view() | <ViewBackward...> | 形状变换产生的节点 |
a.clone() | <CloneBackward...> | 克隆操作产生的节点 |
注意:如果尝试直接修改一个计算出来的张量(比如 y[0] = 5),PyTorch 有时会报错。原因是如果改了 y 的值,但是 y.grad_fn 还是记录它由 x**2 生成,那么反向传播时的导数就不再准确了。这就是为什么我们要用 .detach() 或 .data 来"绕过"这个追踪系统的原因。
8.4 正向传播:即时"录制"
在静态图(如早期的 TensorFlow)中,需要先画好图,然后再喂数据。而在 PyTorch 中,计算即构图。
当执行 z = x * y 时,PyTorch 的底层 Dispatcher(调度器) 会做两件事:
- 计算数值:调用底层的 ATen 库(C++ 核心库)计算出结果
- 录制节点:创建一个代表"乘法"的反向传播算子(比如
MulBackward),并建立连接
底层逻辑:每进行一次运算,PyTorch 就会在内存中动态分配一个新的 Function 对象,并把输入张量的 grad_fn 链接到这个对象上。这意味着每一轮循环(Iter)产生的图都可以是完全不同的。
8.5 反向传播:拓扑排序
当调用 loss.backward() 时,底层发生了极其严密的操作:
- 触发引擎:启动 C++ 的 Autograd Engine
- 拓扑排序:引擎会从
loss节点开始,对整张图进行拓扑排序。这保证了在计算某个节点的梯度之前,所有依赖它的后续节点的梯度都已经算好了 - 执行算子:按照排序结果,逆序调用每个节点的
backward()函数- 如果节点是
AddBackward,它就把梯度原样传给输入 - 如果节点是
MulBackward,它就应用乘法求导法则
- 如果节点是
- 梯度累加:计算出的导数会被写回叶子节点(Leaf Tensor)的
.grad字段
8.6 显存管理:计算图的销毁
为了极致的性能,PyTorch 默认是 "一次性"图机制:
- 一旦
loss.backward()执行完毕,所有的中间变量(非叶子节点的grad_fn和临时张量)都会被立刻释放 - 这就是为什么不能连续两次调用
.backward(),除非显式指定retain_graph=True
8.7 底层组件:ATen 与 LibTorch
PyTorch 的底层其实是纯 C++ 编写的:
- ATen:这是一个基础的张量库,它抹平了 CPU 和 GPU 之间的差异。它包含了数百个经过优化的算子(C++ 或 CUDA 编写)
- LibTorch:这是 PyTorch 的 C++ 接口,允许在不使用 Python 的情况下运行模型
- Pybind11:负责将这些复杂的 C++ 对象无缝"翻译"成在 Python 里看到的
torch.Tensor
8.8 总结
PyTorch 的底层是一个由 C++ 驱动的算子调度系统。它利用 grad_fn 将离散的张量串联成一个有向无环图,并在反向传播时通过遍历这个图来自动完成极其复杂的链式求导。