PyTorch 核心使用

PyTorch 核心使用

PyTorch一个基于Python语言的深度学习框架,它将数据封装成张量(Tensor)来进行处理。

PyTorch提供了灵活且高效的工具,用于构建、训练和部署机器学习和深度学习模型。

PyTorch广泛应用于学术研究和工业界,特别是在计算机视觉、自然语言处理、强化学习等领域。

1. 环境准备与安装

  • 推荐方式 :使用 condapip 安装。

    bash 复制代码
    conda activate 环境名称
    pip install torch torchvision   -i   https://pypi.tuna.tsinghua.edu.cn/simple
  • 验证安装

    python 复制代码
    import 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
硬盘

关键步骤解析

  1. 剥离计算图 (tensor.detach()):
    • 如果不需反向传播(如推理阶段),应调用此方法。
    • 作用:切断梯度追踪,节省显存并加速计算。
  2. 转为 CPU 张量 (tensor.cpu()):
    • 将数据从显存拷贝到系统内存。
  3. 转为 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() 和 NumPy np.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) 要求内存连续。如果张量经过 transposepermute 导致内存不连续,会报错。
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 数据,也无法处理带有梯度历史的张量。转换前必须执行两步操作:

  1. .cpu(): 将数据从 GPU 移回 CPU。
  2. .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) (带下划线 _ 的函数通常是原地操作)
总结建议
  1. 数据处理阶段 :放心使用 NumPy。它生态成熟,配合 Pandas/Matplotlib 处理 CSV、图像预处理非常方便。
  2. 模型训练阶段 :必须转换为 PyTorch。利用其 GPU 加速和自动求导能力。
  3. 避坑指南:
    • 不要试图把 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. 常见技巧与注意事项

  1. 设备管理

    始终使用 device = torch.device("cuda" if torch.cuda.is_available() else "cpu"),并将所有张量和模型 .to(device)。不要硬编码 "cuda"

  2. 梯度裁剪

    在处理 RNN 或大模型微调时,防止梯度爆炸:

    python 复制代码
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
  3. 学习率调度器

    动态调整学习率能显著提升收敛效果:

    python 复制代码
    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=10)
    # 在每个 epoch 结束后调用: scheduler.step(val_loss)
  4. 混合精度训练 (AMP)

    使用 torch.cuda.amp 可以大幅减少显存占用并加速训练(尤其在现代显卡上):

    python 复制代码
    from 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()
  5. 调试工具:

    • torch.isnan(tensor).any(): 检查是否有 NaN。
    • print(model): 查看模型结构。
    • 使用 torchsummarytorchinfo 库查看参数量和输出形状。
相关推荐
gjhave2 小时前
强化学习论文(A3C)
人工智能·机器学习
2301_773553622 小时前
Tailwind CSS如何实现固定定位布局_使用fixed与z-index控制CSS层级
jvm·数据库·python
2301_814809862 小时前
Bootstrap 5中浮动标签(Floating Labels)怎么用?
jvm·数据库·python
roman_日积跬步-终至千里2 小时前
【深度学习】国科大:CIFAR-100 图像分类项目
人工智能·深度学习·分类
jarvisuni2 小时前
成了!Opus4.7直接克隆Claude桌面版!
人工智能·ai编程
解救女汉子2 小时前
如何处理SQL存储过程大数据导入_利用数据泵或外部表
jvm·数据库·python
qq_372906932 小时前
HTML函数在系统字体渲染模糊是硬件问题吗_显示输出链路排查【方法】
jvm·数据库·python
Polar__Star2 小时前
如何在 PHP 包含文件中动态排除特定页面的导航项
jvm·数据库·python
码农的神经元2 小时前
2026 MathorCup C 题实战复盘:从高血脂风险预警到 6 个月干预优化的建模思路与 Python 落地
c语言·开发语言·python