PyTorch 核心使用
PyTorch一个基于Python语言的深度学习框架,它将数据封装成张量(Tensor)来进行处理。
PyTorch提供了灵活且高效的工具,用于构建、训练和部署机器学习和深度学习模型。
PyTorch广泛应用于学术研究和工业界,特别是在计算机视觉、自然语言处理、强化学习等领域。
1. 环境准备与安装
-
推荐方式 :使用
conda或pip安装。bashconda activate 环境名称 pip install torch torchvision -i https://pypi.tuna.tsinghua.edu.cn/simple -
验证安装:
pythonimport torch print(torch.__version__) print(f"CUDA available: {torch.cuda.is_available()}") print(f"GPU Count: {torch.cuda.device_count()}")
2. 核心数据结构:张量 (Tensor)
Tensor 是 PyTorch 的基础,类似于 NumPy 的 ndarray,但可以在 GPU 上运行并支持自动求导。
2.1 创建张量
python
import torch
import numpy as np
# 1. 从数据创建
data = [[1, 2], [3, 4]]
x_data = torch.tensor(data) # 最常用,直接复制数据
# 2. 从形状创建 (未初始化/随机/零/一/线性)
x_shape = torch.empty(2, 3) # 未初始化,速度快
x_rand = torch.rand(2, 3) # 0-1均匀分布
x_norm = torch.randn(2, 3) # 标准正态分布
x_int = torch.randint(low, high, size=(2, 3)) # 随机整数类型
x_zeros = torch.zeros(2, 3) # 全零
# torch.zeros_like(x_zeros) # 根据张量形状创建全0张量
x_ones = torch.ones(2, 3) # 全一
# torch.ones_like(x_ones) # 根据张量形状创建全1张量
x_full = torch.full((2, 3), 10) # 全为指定值 torch.full_like(x_full, 20)
x_arange = torch.arange(start, end, step) # 线性 在指定区间按照步长生成元素 不包括尾值
x_linspace = torch.linspace(start, end, number) # 线性 在指定区间按照元素个数生成 包括尾值
# 3. 从 NumPy 转换 (共享内存)
np_arr = np.array([[1, 2], [3, 4]])
torch_from_np = torch.from_numpy(np_arr)
# 注意:修改其中一个,另一个也会变
# 4. 类型转换
x_float = x_data.float()
x_float32 = x_data.type(torch.float32)
x_double = x_data.double()
x_cuda = x_data.to("cuda") if torch.cuda.is_available() else x_data
torch.initial_seed() torch.manual_seed(num)查看 以及 设置 随机数种子
torch.tensor 根据指定数据创建张量 (标量、list、numpy array)
torch.Tensor 根据形状创建张量, 其也可用来创建指定数据的张量
torch.IntTensor、torch.FloatTensor、torch.DoubleTensor 创建指定类型的张量
| 特性 | torch.tensor() |
torch.Tensor() |
torch.FloatTensor() / IntTensor() 等 |
|---|---|---|---|
| 类型 | 工厂函数 | 类构造函数 (torch.FloatTensor 别名) |
特定类型的类构造函数 |
| 数据类型推断 | 支持 (根据输入数据自动判断) | 不支持 (强制默认为 float32) |
固定 (由函数名决定,如 Float, Int) |
| 传入形状的行为 | ❌ 不支持直接传形状创建未初始化张量 | ⚠️ 创建未初始化的张量 (含垃圾值) | ⚠️ 创建未初始化的张量 (含垃圾值) |
| 安全性 | ⭐⭐⭐⭐⭐ (最安全,推荐) | ⭐⭐ (易混淆类型,未初始化风险) | ⭐⭐⭐ (类型明确,但未初始化风险) |
| 推荐用途 | 绝大多数场景,从现有数据创建张量 | 旧代码兼容,或明确需要默认 float 且了解风险时 | 需要特定类型且习惯旧式写法时 (建议改用 dtype 参数) |
| 最佳实践替代 | - | torch.empty() (显式未初始化) torch.zeros() / torch.ones() |
torch.tensor(..., dtype=...) torch.zeros(..., dtype=...) |
🔍PyTorch 数据流转
1. 神经网络训练时的数据流向 (CPU -> GPU)
在深度学习训练中,数据通常经历以下流程:从硬盘读取到内存,转换为 Tensor,最后送入 GPU 进行计算。
OpenCV/PIL/Matplotlib 读取
加载
torch.from_numpy方法
.to 或 .cuda 方法
输入模型
图像/文本加载
硬盘
Numpy Array
CPU Tensor
GPU Tensor
神经网络开始训练
代码实现:将 Tensor 移动到指定设备
首先定义设备对象:
python
# 自动检测是否有 CUDA,有则用 'cuda',否则用 'cpu'
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 创建一个基础张量
tensor = torch.tensor([1, 2, 3])
方法 1:使用 .to() 方法(推荐)
最通用的方法,可以传入 device 对象或字符串。
python
tensor = tensor.to(device)
print(tensor.device) # 输出: cuda:0 (如果有GPU)
方法 2:直接指定设备字符串
python
tensor = tensor.to('cuda:0')
方法 3:直接使用 .cuda()
快捷方式,但灵活性稍差(主要用于快速测试)。
python
# 送到第 0 块 GPU
tensor_gpu0 = tensor.cuda(0)
# 送到第 1 块 GPU
tensor_gpu1 = tensor.cuda(1)
2. 获取测试结果的数据流向 (GPU -> CPU -> 硬盘)
当训练完成或需要保存结果时,需要将数据从 GPU 逆向流回硬盘。由于 GPU 无法直接操作文件系统,必须先回到 CPU 内存。
tensor.detach 方法
tensor.cpu 方法
tensor.numpy 方法
save data 或 image
GPU: 神经网络结果
GPU: 剥离计算图
内存: CPU Tensor
内存: Numpy Array
硬盘
关键步骤解析
- 剥离计算图 (
tensor.detach()):- 如果不需反向传播(如推理阶段),应调用此方法。
- 作用:切断梯度追踪,节省显存并加速计算。
- 转为 CPU 张量 (
tensor.cpu()):- 将数据从显存拷贝到系统内存。
- 转为 Numpy 数组 (
tensor.numpy()):- 大多数图像处理库(如 OpenCV, PIL)和保存函数只接受 Numpy 格式。
链式调用示例
在实际操作中,常将上述步骤连写,并可能包含维度调整或类型转换:
python
# 假设 result 是 GPU 上的 Tensor
result = tensor.detach().cpu().permute(1, 2, 0).double().numpy()
注:中间可能涉及数据类型(如 double)及数据维度(如 permute)的调整,视具体需求而定。
3. Tensor 与 NumPy 数组的转换机制
PyTorch Tensor 和 NumPy ndarray 之间的转换非常频繁,理解它们的内存共享机制至关重要。
情况 A:共享内存 (Zero-Copy)
适用函数 :torch.from_numpy() 和 tensor.numpy()
- 特点 :转换后的新对象与原对象共享同一块物理内存。
- 后果:修改其中一个对象的值,另一个也会随之改变。
- 优点:速度快,无额外内存开销。
- 缺点:容易引发意外的副作用(Side Effects)。
代码演示 (from_numpy)
python
import numpy as np
import torch
data_numpy = np.array([2, 3, 4])
# 转换:共享内存
data_tensor = torch.from_numpy(data_numpy)
# 修改 Tensor
data_tensor[0] = 100
print(data_tensor) # 输出: tensor([100, 3, 4], dtype=torch.int32)
print(data_numpy) # 输出: [100 3 4] <-- Numpy 也被改变了!
如何避免共享?
如果不想共享内存,可以在转换后调用
.clone()(针对 Tensor) 或.copy()(针对 Numpy)。
tensor.clone()array.copy()
情况 B:不共享内存 (Copy)
适用函数 :torch.tensor()
- 特点 :
torch.tensor()总是执行数据拷贝,创建一个新的独立内存块。 - 后果 :修改其中一个对象,另一个不会受影响。
- 优点:安全,数据隔离。
- 缺点:有额外的内存分配和数据拷贝开销。
代码演示 (torch.tensor)
python
import numpy as np
import torch
data_numpy = np.array([2, 3, 4])
# 转换:不共享内存 (深拷贝)
data_tensor = torch.tensor(data_numpy)
# 修改 Tensor
data_tensor[0] = 100
print(data_tensor) # 输出: tensor([100, 3, 4], dtype=torch.int32)
print(data_numpy) # 输出: [2 3 4] <-- Numpy 保持不变
| 转换方向 | 函数/方法 | 内存关系 | 修改影响 | 推荐场景 |
|---|---|---|---|---|
| Numpy -> Tensor | torch.from_numpy(ndarray) |
共享 | 互相影响 | 数据预处理流水线,追求极致性能 |
| Numpy -> Tensor | torch.tensor(ndarray) |
独立 | 互不影响 | 需要数据隔离,防止意外修改 |
| Tensor -> Numpy | tensor.numpy() |
共享 | 互相影响 | 推理后处理,绘图,保存文件 |
2.2 基本运算
python
a = torch.rand(2, 3)
b = torch.rand(2, 3)
# 加法 (多种方式等价)
c1 = a + b
c2 = torch.add(a, b)
c3 = a.add(b)
# 减法 (多种方式等价)
c1 = a - b
c2 = torch.sub(a, b)
c3 = a.sub(b)
# 乘法 (多种方式等价)
c1 = a * b
c2 = torch.mul(a, b)
c3 = a.mul(b)
# 除法 (多种方式等价)
c1 = a / b
c2 = torch.div(a, b)
c3 = a.div(b)
# 取负号 (多种方式等价)
c1 = torch.neg(a)
c2 = a.neg()
# .add_() .sub_() .mul_() .div_() .neg_() (其中带下划线的版本会修改原数据)
# 矩阵乘法
mat_a = torch.rand(3, 4)
mat_b = torch.rand(4, 5)
res = torch.matmul(mat_a, mat_b) # 或 mat_a @ mat_b
# 维度操作
reshaped = a.view(3, 2) # 改变形状 (需元素总数一致)
squeezed = a.squeeze() # 去除维度为1的轴
unsqueezed = a.unsqueeze(0) # 增加维度
transposed = a.t() # 转置 (2D) 或 permute (高维)
🔍PyTorch 数学运算:取余 & 取整
一、取余运算(Remainder)
python
torch.remainder(a, b) # 函数形式
a % b # 运算符形式(推荐)
c = a.remainder(b) # 方法形式
a.remainder_(b) # 就地操作(修改 a 本身)
✅
a % b是最简洁常用的方式,等价于torch.remainder(a, b)⚠️
_后缀表示"就地操作",会直接修改原张量,节省内存但需谨慎使用。
二、取整运算(Rounding)
| 功能 | PyTorch | 其他写法 | Python (标量) | NumPy |
|---|---|---|---|---|
| 向上取整 | torch.ceil(x) |
y = x.ceil() / x.ceil_() |
math.ceil(x) |
np.ceil(x) |
| 向下取整 | torch.floor(x) |
y = x.floor() / x.floor_() |
math.floor(x) |
np.floor(x) |
| 四舍五入 | torch.round(x) |
y = x.round() / x.round_() |
round(x) |
np.round(x) |
💡 所有函数都支持 Tensor 输入,也提供
.xxx_()就地版本。
三、torch.round() 的特殊规则:"银行家舍入法"
python
10.4 → 10
10.6 → 11
10.5 → 10 ← 关键!
11.5 → 12 ← 关键!
规则说明:"四舍六入五成双"
- 当小数部分 正好是 0.5时,舍入到最近的偶数。
10.5 → 10(因为 10 是偶数)11.5 → 12(因为 12 是偶数)
设计目的:
减少大量数据取整时的统计偏差。传统"四舍五入"在遇到 0.5 时总是向上,会导致整体结果偏高;而"银行家舍入"通过交替舍入到偶数,使误差更均衡。
此规则符合 IEEE 754 标准,也是 Python
round()和 NumPynp.round()的默认行为。
| 操作类型 | 函数/符号 | 是否就地 | 适用对象 | 备注 |
|---|---|---|---|---|
| 取余 | a % b |
否 | Tensor / 标量 | 最常用 |
a.remainder(b) |
否 | Tensor | 同上 | |
a.remainder_(b) |
✅ 是 | Tensor | 修改原值 | |
| 向上取整 | torch.ceil(x) |
否 | Tensor | |
x.ceil_() |
✅ 是 | Tensor | ||
| 向下取整 | torch.floor(x) |
否 | Tensor | |
x.floor_() |
✅ 是 | Tensor | ||
| 四舍五入 | torch.round(x) |
否 | Tensor | 遵循"银行家舍入" |
x.round_() |
✅ 是 | Tensor |
2.3 运算函数
| 功能 | 函数形式 | 运算符重载 | 原地操作 (In-place) |
|---|---|---|---|
| 均值 | torch.mean(dim=) |
- | - |
| 求和 | torch.sum(dim=) |
- | - |
| 极大、极小 | torch.max(dim=) torch.min(dim=) |
- | - |
| 整除 | torch.floor_divide(a, b) |
a // b |
a.floor_divide_(b) |
| 取模 | torch.remainder(a, b) |
a % b |
a.remainder_(b) |
| 幂运算 | torch.pow(a, exponent) |
a exponent |
a.pow_(exponent) |
| 平方根 | torch.sqrt(a) |
- | a.sqrt_() |
| 绝对值 | torch.abs(a) |
- | a.abs_() |
| 负数 | torch.neg(a) |
-a |
a.neg_() |
| 倒数 | torch.reciprocal(a) |
- | a.reciprocal_() |
🔍PyTorch 张量:逻辑顺序 vs 存储顺序
一、基本概念
逻辑顺序(Logical Order)
数学或编程接口中呈现的维度顺序 ------ 即你"看到"的形状。
- 最内层维度 = 最右侧维度(如
W宽度) - 例如:2D 张量
(H, W)→ 先遍历行(H),再遍历列(W)
存储顺序(Storage Order)
数据在内存中实际排列的顺序 ------ 即计算机"底层存放"的方式。
- PyTorch 和 NumPy 默认使用 行优先(Row-major) ,也叫 C-style
- 规则:存满一行,换下一行
二、示例:2D 张量 (H=3, W=4)
逻辑视图(用户视角):
tex
W →
H ↓ [0 1 2 3]
[4 5 6 7]
[8 9 10 11]
内存存储(底层真实顺序):
tex
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
↑ 按行依次铺开
✅ 此时:逻辑顺序 == 存储顺序 → 张量是 连续的(contiguous)
三、转置后:逻辑顺序改变,但存储顺序不变!
原始逻辑顺序 (H×W):
tex
[0 1 2 3]
[4 5 6 7]
[8 9 10 11]
转置后逻辑顺序 (W×H):
tex
[0 4 8]
[1 5 9]
[2 6 10]
[3 7 11]
但内存存储顺序仍为:
tex
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] ← 未变!
⚠️ 此时:逻辑顺序 ≠ 存储顺序 → 张量变为 不连续(non-contiguous)
四、非连续张量的问题
当张量逻辑顺序与存储顺序不一致时,会导致:
❗ 1. 内存访问不连续 → 性能下降
CPU/GPU 缓存无法高效预取数据,导致计算变慢。
❗ 2. 某些 API 不支持
部分操作(如 .view()、某些卷积层等)要求输入必须是 contiguous 张量,否则会报错:
python
RuntimeError: view size is not compatible with input tensor's size and stride...
五、解决方案:.contiguous()
如果需要对非连续张量进行受限操作,可调用:
python
tensor = tensor.transpose(0, 1).contiguous()
→ 这会重新分配内存并按当前逻辑顺序复制数据,使其再次连续。
💡 注意:
.contiguous()会消耗额外内存和时间,仅在必要时使用。
六、扩展:三维图像张量 (C, H, W)
对于一张三通道彩图,其 shape 通常为:
tex
dim=0 dim=1 dim=2
C H W
↓ ↓ ↓
[3 × H × W]
- 逻辑上:先通道 → 再行 → 最后列
- 存储上:仍是按最内层(W)最快变化,即:
tex
[C0,H0,W0], [C0,H0,W1], ..., [C0,H0,Wn],
[C0,H1,W0], [C0,H1,W1], ...,
...
[C1,H0,W0], ...
✅ 此时逻辑顺序与存储顺序一致 → 默认 contiguous
| 特性 | 逻辑顺序 | 存储顺序 |
|---|---|---|
| 定义 | 用户看到的维度排列 | 内存中实际字节排列 |
| 默认规则 | 从左到右(外→内) | 行优先(C-style) |
| 是否可变 | 可通过 transpose/permute 改变 | 固定,除非调用 .contiguous() |
| 连续性判断 | 逻辑 == 存储 → contiguous | 否则 non-contiguous |
| 影响 | 无直接性能影响 | 影响访问效率 & API 兼容性 |
✅ 日常操作中无需关心存储顺序,PyTorch 自动处理大多数情况。
⚠️ 遇到以下场景需注意:
- 使用
.view()报错 → 尝试加.contiguous()- 自定义算子或底层优化 → 检查
tensor.is_contiguous()- 高性能推理/训练 → 避免频繁转置导致非连续
2.3 索引操作
python
import torch
data = torch.tensor([
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0]
])
# 1. 基础切片:取前两行,后两列
slice_res = data[:2, 1:]
# [[2., 3.], [5., 6.]]
# 2. 布尔索引:将所有大于 5 的值设为 100
data[data > 5] = 100
# data 变为: [[1, 2, 3], [4, 5, 100], [100, 100, 100]]
# 3. 花式索引:取第 0 行和第 2 行的第 1 列元素
rows = torch.tensor([0, 2])
cols = torch.tensor([1, 1])
fancy_res = data[rows, cols]
# [2., 100.]
# 4. Gather: 按索引表取值
idx = torch.tensor([[0, 2], [1, 0]]) # 2x2
gather_res = torch.gather(data, dim=1, index=idx)
# 第0行取索引0,2 -> 1, 3
# 第1行取索引1,0 -> 5, 4
# 结果: [[1., 3.], [5., 4.]]
高级技巧与注意事项
1.基础索引/切片 vs 花式索引
这是最容易出错的地方:
- 基础索引/切片 (
x[0],x[:2]) → 视图 (View) 。修改结果会改变原张量。 - 花式索引 (
x[[0, 2]],x[mask]) → 拷贝 (Copy) 。修改结果不会改变原张量。
python
x = torch.arange(10)
# 切片 (视图)
s = x[:5]
s[0] = 99
print(x[0]) # 输出 99 (原数据变了)
# 花式索引 (拷贝)
f = x[torch.tensor([0, 1, 2])]
f[0] = -1
print(x[0]) # 输出 99 (原数据没变,因为 f 是拷贝)
2.... (Ellipsis) 索引
用于省略中间的维度,特别适合高维张量(如图像 (B, C, H, W) 或 Transformer (B, Seq, Dim))。
python
x = torch.randn(2, 3, 4, 5)
# 取第一个批次的所有数据,等价于 x[0, :, :, :]
sub = x[0, ...]
# 取所有批次的最后一个通道,等价于 x[:, :, :, -1]
last_channel = x[..., -1]
3.unsqueeze 配合索引
有时索引会导致维度丢失(例如取出一行后从 2D 变 1D),可以使用 unsqueeze 恢复维度以便广播。
python
row = x[0] # shape: (3,)
row_expanded = x[0].unsqueeze(0) # shape: (1, 3),方便与其他矩阵运算
2.4 形状操作
1. 改变形状 (Reshaping)
改变张量的维度结构,但保持元素总数不变。
| 函数/方法 | 说明 | 是否返回视图 | 关键点 |
|---|---|---|---|
tensor.view(*shape) |
改变形状 | 是 (View) | 要求内存连续。如果张量经过 transpose 或 permute 导致内存不连续,会报错。 |
tensor.reshape(*shape) |
改变形状 | 可能是拷贝 | 更灵活。如果内存不连续,它会自动创建一个拷贝。推荐在不确定内存布局时使用。 |
tensor.flatten() |
展平为一维 | 通常是 View | 等价于 view(-1)。 |
torch.flatten(input, start_dim, end_dim) |
部分展平 | 通常是 View | 将指定范围内的维度合并。例如 (B, C, H, W) -> (B, C*H*W)。 |
python
import torch
x = torch.randn(2, 3, 4) # 24个元素
# 1. view (高效,但受限于内存连续性)
y = x.view(6, 4)
# 2. reshape (安全,自动处理非连续内存)
z = x.transpose(0, 1) # 此时内存可能不连续
w = z.reshape(3, 2, 4) # 成功,即使 z 是非连续的
# 3. 使用 -1 自动推断维度
flat = x.view(-1) # shape: (24,)
batch_flat = x.view(2, -1) # shape: (2, 12)
最佳实践 :在现代 PyTorch 代码中,优先使用
reshape(),因为它更安全,性能差异在大多数情况下可忽略。只有在极度追求性能且确保内存连续时,才使用view()。
2. 增加/减少维度 (Squeezing & Un squeezing)
用于调整维度以匹配广播规则或满足层输入要求(如 CNN 需要 4D 输入)。
| 函数/方法 | 说明 | 示例 |
|---|---|---|
torch.unsqueeze(input, dim) 或 input.unsqueeze(dim) |
在指定位置插入大小为 1 的维度。 | x shape (3,) -> unsqueeze(0) -> (1, 3) (增加批次维) |
torch.squeeze(input, dim=None) 或 input.squeeze(dim) |
移除大小为 1 的维度。若指定 dim,仅移除该维度(如果它是 1)。 |
x shape (1, 3, 1, 5) -> squeeze() -> (3, 5) squeeze(0) -> (3, 1, 5) |
tensor[None, :] tensor[:, None] |
Pythonic 写法,等价于 unsqueeze。 | x[None, ...] 等价于 unsqueeze(0) |
python
x = torch.tensor([1, 2, 3]) # shape: (3,)
# 增加维度
x_row = x.unsqueeze(0) # shape: (1, 3)
x_col = x.unsqueeze(1) # shape: (3, 1)
x_none = x[None, :] # shape: (1, 3)
# 减少维度
y = torch.randn(1, 3, 1, 5)
y_squeezed = y.squeeze() # shape: (3, 5) (移除了所有为1的维)
y_part = y.squeeze(0) # shape: (3, 1, 5) (只移除第0维)
3. 维度置换与移动 (Permuting & Moving)
改变维度的顺序,常用于图像处理(HWC <-> CHW)或 Transformer 序列处理。
| 函数/方法 | 说明 | 内存影响 |
|---|---|---|
tensor.permute(*dims) |
任意重排维度顺序。 | 返回非连续张量 (View of non-contiguous)。后续若需 view,通常需先 .contiguous()。 |
tensor.transpose(dim0, dim1) |
交换两个特定维度。 | 同上,返回非连续张量。 |
tensor.mT (PyTorch 1.9+) |
专门用于交换最后两个维度 (矩阵转置)。 | 适用于高维批量矩阵。 |
tensor.moveaxis(source, dest) |
将某个轴从 source 移动到 dest。 | 同上。 |
python
# 图像数据:(Batch, Height, Width, Channel) -> (Batch, Channel, Height, Width)
x = torch.randn(32, 224, 224, 3)
# 方法 1: permute (最常用)
y = x.permute(0, 3, 1, 2) # shape: (32, 3, 224, 224)
# 方法 2: transpose (仅交换两维)
# 如果原来是 (B, C, H, W),想变成 (B, W, H, C) 比较麻烦,permute 更好用
z = x.transpose(1, 3)
# 【重要】如果 permute 后需要 view/flatten,必须调用 contiguous()
# 否则报错: "view size is not compatible with input tensor's size and stride"
# is_contiguous() 可用于判断
# y.is_contiguous() False
w = y.contiguous().view(32, -1)
Contiguous 问题 :只要使用了
transpose,permute,narrow,expand等操作,张量在内存中就可能变得不连续 。如果接下来要用view或某些 CUDA 内核,务必先调用.contiguous()。View vs Copy:
view,squeeze,unsqueeze,transpose,permute→ 通常返回视图(修改会影响原张量,除了 permute 后通常不可写)。reshape(有时),repeat,clone, 花式索引 → 返回拷贝。
2.5 拼接操作
1. 拼接与堆叠 (Concatenation & Stacking)
将多个张量组合成一个更大的张量。
| 函数 | 说明 | 维度变化 | 示例 |
|---|---|---|---|
torch.cat(tensors, dim) |
拼接。沿现有维度连接。 | 总元素数增加,维度数不变。 | (2, 3) + (2, 3) along dim 0 -> (4, 3) |
torch.stack(tensors, dim) |
堆叠。沿新维度连接。 | 维度数 +1。 | (2, 3) + (2, 3) along new dim 0 -> (2, 2, 3) |
torch.chunk(tensor, chunks, dim) |
将张量沿维度切分成若干块。 | 返回列表。 | (4, 3) chunk 2 -> 两个 (2, 3) |
torch.split(tensor, split_size, dim) |
按指定大小切分。 | 返回列表。 | (10, 3) split 3 -> (3,3), (3,3), (3,3), (1,3) |
python
a = torch.ones(2, 3)
b = torch.zeros(2, 3)
# Cat: 行数变多 (2+2=4)
c_cat = torch.cat([a, b], dim=0) # shape: (4, 3)
# Stack: 增加一个维度 (变成 3D)
c_stack = torch.stack([a, b], dim=0) # shape: (2, 2, 3)
# 解释:第0维表示"第几个张量",里面是原来的 (2, 3)
区别核心:
cat像是把两本书的页码连起来,书变厚了。stack像是把两本书放在书架的一层,书架多了一层。
2. 扩展与重复 (Expansion & Repetition)
在不复制数据(逻辑上)或物理复制数据的情况下扩大张量。
| 函数 | 说明 | 内存占用 | 适用场景 |
|---|---|---|---|
tensor.expand(*sizes) |
逻辑扩展。利用广播机制,不复制数据。 | 极低 (仅修改 stride) | 广播运算,如将偏置向量扩展到 Batch 大小。不能修改返回的张量。 |
tensor.repeat(*sizes) |
物理复制。真正复制数据内存。 | 高 (原大小 × 倍数) | 需要独立副本并进行修改时。 |
python
x = torch.tensor([[1], [2], [3]]) # shape: (3, 1)
# Expand: 逻辑上变成 (3, 5),实际内存还是 3 个元素
y = x.expand(3, 5)
# y[0, 0] 和 y[0, 1] 指向同一内存地址!
# 尝试 y[0, 0] = 99 会导致整行都变 (如果是非叶子节点可能报错)
# Repeat: 物理复制,变成真正的 (3, 5) 内存块
z = x.repeat(1, 5)
# 修改 z 不会影响其他列,也不影响 x
🔍PyTorch 张量: 翻转与旋转
torch.flip() ------ 张量翻转
功能:沿指定维度翻转张量的元素顺序。
语法:
python
torch.flip(input, dims)
input:输入张量dims:要翻转的维度(元组或列表,如(0,)、(0, 1))
示例:
python
import torch
x = torch.tensor([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
print("原张量:\n", x)
# 沿第0维(行)翻转
x_flip_0 = torch.flip(x, dims=(0,))
print("\n沿第0维翻转:\n", x_flip_0)
# 沿第1维(列)翻转
x_flip_1 = torch.flip(x, dims=(1,))
print("\n沿第1维翻转:\n", x_flip_1)
# 沿两个维度翻转(相当于图像旋转180度)
x_flip_01 = torch.flip(x, dims=(0, 1))
print("\n沿0和1维翻转:\n", x_flip_01)
输出结果:
tex
原张量:
tensor([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
沿第0维翻转:
tensor([[6, 7, 8],
[3, 4, 5],
[0, 1, 2]])
沿第1维翻转:
tensor([[2, 1, 0],
[5, 4, 3],
[8, 7, 6]])
沿0和1维翻转:
tensor([[8, 7, 6],
[5, 4, 3],
[2, 1, 0]])
torch.rot90() ------ 张量旋转
功能:将张量(通常是 2D 图像)在指定平面上旋转 90 度的整数倍。
语法:
python
torch.rot90(input, k, dims)
input:输入张量k:旋转次数(每次 90 度),k=1表示逆时针 90°,k=2表示 180°,k=3表示 270°(或 -90°)dims:指定旋转平面的两个维度,如(0, 1)表示在 H×W 平面旋转
示例:
python
import torch
x = torch.tensor([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
print("原张量:\n", x)
# 逆时针旋转 90 度 (k=1)
x_rot_90 = torch.rot90(x, k=1, dims=(0, 1))
print("\n逆时针旋转90度:\n", x_rot_90)
# 逆时针旋转 180 度 (k=2)
x_rot_180 = torch.rot90(x, k=2, dims=(0, 1))
print("\n逆时针旋转180度:\n", x_rot_180)
# 逆时针旋转 270 度 (k=3) = 顺时针 90 度
x_rot_270 = torch.rot90(x, k=3, dims=(0, 1))
print("\n逆时针旋转270度:\n", x_rot_270)
输出结果:
tex
原张量:
tensor([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
逆时针旋转90度:
tensor([[2, 5, 8],
[1, 4, 7],
[0, 3, 6]])
逆时针旋转180度:
tensor([[8, 7, 6],
[5, 4, 3],
[2, 1, 0]])
逆时针旋转270度:
tensor([[6, 3, 0],
[7, 4, 1],
[8, 5, 2]])
为什么这里是逆时针?
核心原因:坐标系的差异
我们眼中的"图像坐标系"(屏幕坐标系)
- 原点
(0,0)在左上角。- Y轴向下增长。
- 在这种视角下,顺时针旋转符合直觉。
数学/计算机图形学的"标准坐标系"
- 原点
(0,0)在中心 或左下角。- Y轴向上增长。
- 在这个体系中,逆时针旋转是正方向(这是由数学中的极坐标和旋转矩阵定义的)。
torch.rot90()遵循的是数学上的标准定义 ,即:逆时针旋转为正角度。
⚠️ 关键点:这些操作是否改变存储顺序?
torch.flip()和torch.rot90()通常不会立即改变底层内存存储顺序。- 它们只是改变了张量的"视图" ------ 即逻辑顺序变了,但数据在内存中还是按原来的顺序存放。
- 因此,操作后张量可能变成 非连续(non-contiguous)。
✅ 建议 :在进行卷积、reshape 等操作前,若张量经过 flip/rot90,建议调用
.contiguous()避免报错或性能问题。
PyTorch vs NumPy 基础操作
PyTorch 的 API 设计大量借鉴了 NumPy,因此很多函数名和用法几乎一模一样。
| PyTorch 操作 | NumPy 等价操作 | 是否完全等价 | 说明 |
|---|---|---|---|
tensor.reshape(shape) |
array.reshape(shape) 或 np.reshape(array, shape) |
✅ 基本等价 | 用于改变张量的形状。 |
tensor.squeeze(dim) |
np.squeeze(array, axis=dim) |
✅ 等价 | 用于移除维度大小为 1 的维度。 |
tensor.unsqueeze(dim) |
np.expand_dims(array, axis=dim) |
✅ 等价 | 用于在指定位置插入一个维度大小为 1 的维度。 |
tensor.transpose(dim0, dim1) |
array.T (仅2D) 或 np.transpose(array, axes=(dim1, dim0)) |
⚠️ 接口略有不同 | PyTorch 的 transpose 通常只交换两个轴;NumPy 和 TensorFlow 的 transpose 功能更强,可任意重排所有轴。 |
tensor.permute(*dims) |
np.transpose(array, axes=dims) |
✅ 功能等价 | 用于任意重排张量的维度顺序。 |
torch.cat(tensors, dim) |
np.concatenate(arrays, axis=dim) |
✅ 等价 | 沿着指定轴拼接张量/数组。注意:TF 参数名是 values,NP 是 arrays。 |
torch.stack(tensors, dim) |
np.stack(arrays, axis=dim) |
✅ 完全等价 | 沿着新轴拼接一系列数组。 |
PyTorch ➔ NumPy
注意:NumPy 无法处理 GPU 数据,也无法处理带有梯度历史的张量。转换前必须执行两步操作:
- .cpu(): 将数据从 GPU 移回 CPU。
- .detach(): 断开计算图(去除梯度信息)。
python
# 假设 tensor 是 GPU 上的且需要求梯度的张量
# tensor = torch.tensor([1.0, 2.0], requires_grad=True, device='cuda')
# 错误写法: tensor.numpy() -> 会报错
# 正确写法 (标准套路):
np_arr = tensor.detach().cpu().numpy()
核心差异详解
A. GPU 加速
这是两者最大的性能差异来源。对于大规模矩阵运算,PyTorch 利用 GPU 并行计算,速度可比 NumPy 快数十倍。
python
# PyTorch 特有的设备管理
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
x = torch.randn(1000, 1000).to(device) # 数据上 GPU
y = torch.randn(1000, 1000).to(device)
z = x @ y # 在 GPU 上极速运算
B. 自动求导 (Autograd)
PyTorch 可以记录你在张量上的所有操作,构建计算图,并自动计算梯度。这是训练神经网络的基础。
python
x = torch.tensor([2.0], requires_grad=True) # 开启梯度追踪
y = x ** 2 + 1
y.backward() # 自动求导
print(x.grad) # 输出: tensor([4.]),因为 dy/dx = 2x = 4
NumPy 完全没有这个功能,它只能做纯粹的数值计算。
C. 原地操作 (In-place Operations)
两者都支持原地操作以节省内存,但在 PyTorch 中需格外小心,因为原地修改可能会破坏计算图(导致无法求导)。
- NumPy :
a += 1 - PyTorch :
a.add_(1)(带下划线_的函数通常是原地操作)
总结建议
- 数据处理阶段 :放心使用 NumPy。它生态成熟,配合 Pandas/Matplotlib 处理 CSV、图像预处理非常方便。
- 模型训练阶段 :必须转换为 PyTorch。利用其 GPU 加速和自动求导能力。
- 避坑指南:
- 不要试图把 GPU 张量直接转 NumPy。
- 注意
torch.from_numpy()的内存共享特性,修改原数组可能会意外改变张量数据。 - 矩阵乘法记得用
@或torch.matmul,别用*。
3. 自动求导 (Autograd)
PyTorch 的核心魔法,用于计算梯度。
python
x = torch.tensor([2.0], requires_grad=True)
y = x ** 2 + 3 * x + 1
# 反向传播
y.backward()
# 获取梯度
print(x.grad) # dy/dx = 2x + 3, 当x=2时,结果为 7.0
# 停止梯度追踪 (用于推理或冻结参数)
with torch.no_grad():
z = x ** 2
# 或者
x.detach()
4. 数据加载 (Dataset & DataLoader)
处理大规模数据的标准流程。
python
from torch.utils.data import Dataset, DataLoader
# 1. 自定义 Dataset
class MyDataset(Dataset):
def __init__(self, data, labels):
self.data = data
self.labels = labels
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
return self.data[idx], self.labels[idx]
# 模拟数据
data = torch.rand(100, 10)
labels = torch.randint(0, 2, (100,))
dataset = MyDataset(data, labels)
# 2. 创建 DataLoader
dataloader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=4)
# 3. 迭代使用
for batch_x, batch_y in dataloader:
# 训练逻辑
pass
5. 构建神经网络 (nn.Module)
所有模型都必须继承 nn.Module。
python
import torch.nn as nn
import torch.nn.functional as F
class SimpleNN(nn.Module):
def __init__(self):
super(SimpleNN, self).__init__()
# 定义层
self.fc1 = nn.Linear(10, 64)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.5)
self.fc2 = nn.Linear(64, 2)
def forward(self, x):
# 定义前向传播
x = self.fc1(x)
x = self.relu(x)
x = self.dropout(x)
x = self.fc2(x)
# 注意:通常不在这里写 softmax,损失函数会处理
return x
model = SimpleNN()
if torch.cuda.is_available():
model.to("cuda")
6. 标准训练循环 (Training Loop)
这是深度学习最核心的代码模板。
python
# 初始化
criterion = nn.CrossEntropyLoss() # 损失函数
optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # 优化器
epochs = 10
model.train() # 设置为训练模式 (启用 Dropout, BatchNorm 等)
for epoch in range(epochs):
running_loss = 0.0
for inputs, targets in dataloader:
# 1. 数据移到设备
inputs, targets = inputs.to("cuda"), targets.to("cuda")
# 2. 梯度清零 (非常重要!)
optimizer.zero_grad()
# 3. 前向传播
outputs = model(inputs)
loss = criterion(outputs, targets)
# 4. 反向传播
loss.backward()
# 5. 更新参数
optimizer.step()
running_loss += loss.item()
print(f"Epoch {epoch+1}, Loss: {running_loss/len(dataloader)}")
# 验证/测试模式
model.eval()
with torch.no_grad(): # 不计算梯度,节省内存
# ... 评估逻辑
pass
7. 模型保存与加载
最佳实践是只保存模型参数 (state_dict),而不是整个模型对象。
python
# 保存
torch.save(model.state_dict(), "model_weights.pth")
# 加载
model = SimpleNN() # 必须先实例化模型结构
model.load_state_dict(torch.load("model_weights.pth", map_location="cpu"))
model.eval() # 加载后务必设为评估模式
8. 常见技巧与注意事项
-
设备管理 :
始终使用
device = torch.device("cuda" if torch.cuda.is_available() else "cpu"),并将所有张量和模型.to(device)。不要硬编码"cuda"。 -
梯度裁剪 :
在处理 RNN 或大模型微调时,防止梯度爆炸:
pythontorch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) -
学习率调度器 :
动态调整学习率能显著提升收敛效果:
pythonscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=10) # 在每个 epoch 结束后调用: scheduler.step(val_loss) -
混合精度训练 (AMP) :
使用
torch.cuda.amp可以大幅减少显存占用并加速训练(尤其在现代显卡上):pythonfrom torch.cuda.amp import autocast, GradScaler scaler = GradScaler() with autocast(): outputs = model(inputs) loss = criterion(outputs, targets) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() -
调试工具:
torch.isnan(tensor).any(): 检查是否有 NaN。print(model): 查看模型结构。- 使用
torchsummary或torchinfo库查看参数量和输出形状。