4、张量操作:索引、切片、拼接、拆分、数学运算、广播机制、与NumPy互转
张量(Tensor)是PyTorch中最核心的数据结构,几乎所有深度学习模型的参数、输入输出、中间特征都以张量形式存在。如果把模型比作一台精密机器,张量就是机器中流动的原料和零件,而张量操作则是加工这些原料的工艺手段。本章将系统讲解PyTorch中张量的索引与切片、拼接与拆分、数学运算、广播机制以及与NumPy的互转方法。掌握这些操作,是后续构建神经网络、编写训练循环、处理数据管道的必要基础。
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=0 | 3个 (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,则扩展该维度以匹配另一个。
- 若其中一个张量缺少该维度,则视为1并扩展。
- 若两个维度都不为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→Tensor | torch.from_numpy(arr) | 共享 | 加载数据、预处理 |
| Tensor→NumPy | t.numpy() | 共享 | 可视化、保存结果 |
| 独立拷贝 | t.clone().numpy() | 独立 | 避免意外修改 |
| GPU→NumPy | t.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 综合案例:张量操作在批处理中的应用
假设我们要构建一个简单的文本分类数据管道,输入为一批英文句子,需要将句子转换为固定长度的词嵌入矩阵,并构造批次。具体步骤如下:
- 使用分词器将句子转为词索引列表,长度不足的用0填充(padding)。
- 将填充后的列表转为NumPy数组,再转为Tensor。
- 通过
nn.Embedding层将索引映射为词向量,得到形状(batch, seq_len, embed_dim)的张量。 - 使用
torch.stack将多个句子堆叠为批次。 - 在训练中,使用切片提取每个样本的特征,通过
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 自动生成,但理解底层操作有助于调试和自定义数据管道。
shape,再检查广播规则,通常能快速定位问题。