4、张量操作:索引、切片、拼接、拆分、数学运算、广播机制、与NumPy互转

张量(Tensor)是PyTorch中最核心的数据结构,几乎所有深度学习模型的参数、输入输出、中间特征都以张量形式存在。如果把模型比作一台精密机器,张量就是机器中流动的原料和零件,而张量操作则是加工这些原料的工艺手段。本章将系统讲解PyTorch中张量的索引与切片、拼接与拆分、数学运算、广播机制以及与NumPy的互转方法。掌握这些操作,是后续构建神经网络、编写训练循环、处理数据管道的必要基础。

PyTorch 张量操作全景图 张量 Tensor shape, dtype, device 索引与切片 t[1, :, 2] 布尔掩码 / 花式索引 视图 vs 拷贝 拼接与拆分 cat / stack / chunk split / unbind 维度对齐原则 数学运算 add / sub / mul / div matmul / exp / log in-place 操作 广播机制 从右向左对齐维度 维度为1或缺失可扩展 NumPy 互转 from_numpy / numpy() 共享内存 vs 拷贝 视图与内存布局 contiguous / reshape stride 步长机制 所有操作均需关注:维度匹配、数据类型一致、设备(CPU/GPU)统一

4.1 索引与切片:精准定位张量元素

核心原理

PyTorch张量的索引与Python原生列表、NumPy数组的索引规则高度一致,但增加了深度学习场景下的特殊需求。索引的本质是通过下标或条件表达式,从张量中选取特定位置的数据。PyTorch支持三种索引方式:基本索引(整数下标)、切片索引(start:stop:step)、高级索引(整数数组、布尔掩码)。理解索引的关键在于区分操作返回的是视图(View)还是拷贝(Copy)——视图与原张量共享内存,修改视图会影响原张量;拷贝则拥有独立内存。

操作流程与判断方法

基本索引使用整数定位单个元素,例如 t[1, 2] 取第1行第2列(0-indexed)。切片索引使用冒号分隔起始、结束和步长,例如 t[0:2, :, 1::2] 表示取前两行、所有列、从第1列开始每隔一列。高级索引中,整数数组索引(花式索引)允许用列表或张量指定多个位置,布尔掩码则通过条件表达式生成True/False矩阵进行筛选。判断视图还是拷贝的简单规则:基本索引和切片返回视图,花式索引和布尔掩码返回拷贝。

索引方式示例返回类型典型应用
整数索引t[2, 3]标量张量(0维)取单个值
切片索引t[:, 1:3]视图取子区域
花式索引t[[0,2], [1,3]]拷贝按列表取行/列
布尔掩码t[t > 0.5]拷贝(1D)条件筛选

案例拆解:图像裁剪与数据增强

在图像处理任务中,常需要从大图中随机裁剪出固定尺寸的小图。假设有一张形状为 (3, 256, 256) 的RGB图像张量 img(C, H, W),需要裁剪出左上角 (50, 80) 开始、大小为 128×128 的区域:

crop = img[:, 50:178, 80:208]  # 高度50~177,宽度80~207
# 注意:切片是左闭右开区间,因此结束索引为起始+尺寸

若需要随机裁剪,可先生成随机起始坐标,再切片。此操作返回视图,内存零拷贝,效率极高。但若后续需要对该区域做归一化等操作且不希望影响原图,则应调用 .clone() 显式拷贝。

常见误区:切片索引的结束位置是开区间,初学者常写成 img[:, 50:178, 80:208] 却误以为取到178和208,实际取到177和207。另外,对视图进行原地修改(如 crop.fill_(0))会直接改变原张量,这在数据预处理中可能引发隐蔽的bug。

4.2 拼接与拆分:灵活组装与分解张量

核心原理

拼接(Concatenation)是将多个张量沿指定维度合并,拆分(Splitting)则是将一个张量沿某维度分割成多个子张量。PyTorch提供 torch.cat、torch.stack 用于拼接,torch.chunk、torch.split、torch.unbind 用于拆分。拼接要求除拼接维度外,其他维度形状完全一致;stack 则创建一个新维度来堆叠张量,不要求形状完全一致(但除新维度外其余维度需相同)。

操作流程与对比

  • cat:沿已有维度拼接,不增加维度。例如两个形状为 (2,3) 的张量沿dim=0拼接得到 (4,3)。
  • stack:沿新维度堆叠。两个 (2,3) 张量stack后得到 (2,2,3),新维度在dim=0。
  • chunk:将张量均分为n块,返回列表。若无法整除,最后一块较小。
  • split:按指定长度或每块大小拆分,更灵活。
  • unbind:沿指定维度移除该维度,返回n个张量(每个少一维)。
函数输入示例输出形状适用场景
cat两个 (2,3)(4,3) 或 (2,6)合并特征图、拼接数据
stack两个 (2,3)(2,2,3)构造批次、堆叠序列
chunk(6,4) 分3块3个 (2,4)分块处理
split(6,4) 按[2,4](2,4) 和 (4,4)非均匀拆分
unbind(3,4) 沿dim=03个 (4,)逐行/逐列处理

实战场景:批处理中的特征融合

在自然语言处理中,一个批次包含多个句子,每个句子经过编码器后得到形状为 (seq_len, hidden_dim) 的特征。若需要将多个句子的特征堆叠成一个批次,使用 torch.stack 沿新维度拼接:

sent1 = torch.randn(5, 256)  # 5个词,256维
sent2 = torch.randn(7, 256)
# 由于seq_len不同,不能直接stack,需先padding或使用cat
padded = torch.cat([sent1, sent2], dim=0)  # 错误示范:维度不匹配
# 正确做法:先pad到相同长度,再stack
sent1_pad = torch.nn.functional.pad(sent1, (0,0,0,2))  # pad到7
batch = torch.stack([sent1_pad, sent2], dim=0)  # (2, 7, 256)
经验提醒:当需要沿新维度组织数据时优先考虑 stack;当需要合并已有维度时使用 cat。拆分时,chunk 适合均匀分块,split 适合按需分配长度,unbind 适合逐元素遍历。

4.3 数学运算与广播机制

核心原理

张量的数学运算包括逐元素运算(element-wise)和矩阵运算。逐元素运算如 add、sub、mul、div、exp、log、sqrt 等,要求两个张量形状相同或满足广播条件。矩阵运算如 matmul(矩阵乘法)、dot(点积)遵循线性代数规则。广播机制(Broadcasting)是PyTorch自动扩展维度以匹配形状的规则,其核心原则是:从最后一个维度开始比较,若两个维度相等、其中一个为1或缺失,则可广播;否则报错。

广播规则详解

假设有两个张量A和B,从右向左逐维比较:

  1. 若维度大小相同,则匹配。
  2. 若其中一个维度大小为1,则扩展该维度以匹配另一个。
  3. 若其中一个张量缺少该维度,则视为1并扩展。
  4. 若两个维度都不为1且不相等,则无法广播。
a = torch.randn(3, 1, 5)  # 形状 (3,1,5)
b = torch.randn(1, 4, 1)  # 形状 (1,4,1)
c = a + b  # 广播后形状 (3,4,5)
# 比较过程:从右向左
# 第2维:5 vs 1 → 扩展b的5
# 第1维:1 vs 4 → 扩展a的4
# 第0维:3 vs 1 → 扩展b的3

案例拆解:归一化与损失计算

在训练神经网络时,经常需要对特征张量进行标准化。假设特征张量 x 形状为 (batch, features),需要减去均值并除以标准差。均值 mean 和标准差 std 形状为 (features,),广播机制自动将统计量扩展到每个样本:

x = torch.randn(32, 128)  # 32个样本,128维特征
mean = x.mean(dim=0)       # 形状 (128,)
std = x.std(dim=0)         # 形状 (128,)
x_norm = (x - mean) / std  # 广播: (32,128) - (128,) → (32,128)

在计算交叉熵损失时,模型输出 logits 形状为 (batch, num_classes),标签 labels 形状为 (batch,),使用 torch.nn.functional.cross_entropy 时内部会通过广播机制处理索引匹配。

常见误区:广播机制虽然方便,但过度依赖可能导致内存暴涨。例如 a 形状 (1000,1,1000) 与 b 形状 (1,1000,1) 相加,实际会生成 (1000,1000,1000) 的中间张量,占用约8GB内存(float32)。此时应使用显式循环或 einsum 避免内存爆炸。

4.4 与NumPy互转及内存共享机制

核心原理

PyTorch张量与NumPy数组可以高效互转,二者在CPU上共享底层内存(当张量在CPU上且为连续内存时)。转换函数为 torch.from_numpy()(NumPy→Tensor)和 tensor.numpy()(Tensor→NumPy)。共享内存意味着修改一方会影响另一方,这在数据预处理中需格外小心。若需要独立副本,应使用 .clone() 或 np.copy()。

操作流程与注意事项

  • NumPy→Tensor:t = torch.from_numpy(arr),要求arr为 np.ndarray 且dtype为PyTorch支持的数值类型(float32、int64等)。
  • Tensor→NumPy:arr = t.numpy(),要求t在CPU上且不要求梯度(requires_grad=False)。
  • GPU张量:需先调用 .cpu() 再转NumPy。
  • 非连续内存:若张量经过转置或切片导致内存不连续,.numpy() 会报错,需先调用 .contiguous()。
操作代码内存关系适用场景
NumPy→Tensortorch.from_numpy(arr)共享加载数据、预处理
Tensor→NumPyt.numpy()共享可视化、保存结果
独立拷贝t.clone().numpy()独立避免意外修改
GPU→NumPyt.cpu().numpy()拷贝从GPU取回数据

实战场景:数据加载与可视化

在图像分类任务中,常用OpenCV或PIL读取图像为NumPy数组,然后转为PyTorch张量输入模型。训练完成后,需要将预测结果转回NumPy进行可视化:

import numpy as np
import torch
# 读取图像 (H, W, C) uint8
img_np = np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8)
# 转为Tensor并调整维度为 (C, H, W) float32
img_t = torch.from_numpy(img_np).permute(2, 0, 1).float() / 255.0
# 模型推理...
output = model(img_t.unsqueeze(0))
# 结果转NumPy
pred_np = output.detach().cpu().numpy()  # 形状 (1, num_classes)
# 注意:detach() 去除梯度,cpu() 移到CPU,numpy() 转数组
经验提醒:在数据加载阶段,尽量使用 torch.from_numpy 避免额外拷贝;在训练循环中,所有数据应保持Tensor格式,仅在可视化或保存时转NumPy。若张量需要梯度,必须先 detach() 再转NumPy,否则会报错。

4.5 综合案例:张量操作在批处理中的应用

假设我们要构建一个简单的文本分类数据管道,输入为一批英文句子,需要将句子转换为固定长度的词嵌入矩阵,并构造批次。具体步骤如下:

  1. 使用分词器将句子转为词索引列表,长度不足的用0填充(padding)。
  2. 将填充后的列表转为NumPy数组,再转为Tensor。
  3. 通过 nn.Embedding 层将索引映射为词向量,得到形状 (batch, seq_len, embed_dim) 的张量。
  4. 使用 torch.stack 将多个句子堆叠为批次。
  5. 在训练中,使用切片提取每个样本的特征,通过 cat 拼接不同层的输出。
sentences = [["I", "love", "PyTorch"], ["Deep", "learning", "is", "fun"]]
# 假设词表映射 vocab = {"I":1, "love":2, "PyTorch":3, "Deep":4, "learning":5, "is":6, "fun":7}
# padding到相同长度
max_len = 4
padded = []
for sent in sentences:
    ids = [vocab[w] for w in sent]
    padded.append(ids + [0]*(max_len - len(ids)))
arr = np.array(padded, dtype=np.int64)  # (2, 4)
tensor = torch.from_numpy(arr)          # 共享内存
embed = torch.nn.Embedding(8, 16)       # 词表大小8,嵌入维度16
embedded = embed(tensor)                # (2, 4, 16)
# 拼接两个句子的嵌入
combined = torch.cat([embedded[0], embedded[1]], dim=0)  # (8, 16)
# 拆分回两个句子
sent1, sent2 = torch.split(combined, [4, 4], dim=0)

该案例综合运用了NumPy互转、索引切片、拼接拆分和数学运算。实际训练中,批次数据通常由 DataLoader 自动生成,但理解底层操作有助于调试和自定义数据管道。

总结复盘:张量操作是PyTorch编程的基本功。索引切片需注意视图与拷贝的区别;拼接拆分需确保维度匹配;数学运算需理解广播规则;NumPy互转需关注内存共享。建议读者在Jupyter Notebook中逐行运行本章代码,观察张量形状和内存地址变化,建立直观感受。遇到维度错误时,先打印各张量的 shape,再检查广播规则,通常能快速定位问题。