4、多维数组切片:逗号分隔的维度索引,高维张量的切片逻辑
好,咱们进入第四讲。前面几章我们聊了 Python 列表切片、NumPy 基础切片,那些都是一维或二维的操作。但到了深度学习模型里,你面对的是张量——说白了就是多维数组。3 维、4 维甚至 5 维都很常见。
我第一次接触 4 维张量时,说实话有点懵。那时候调一个图像批处理的 bug,打印出来的 shape 是 (32, 3, 224, 224),我想取第一张图的绿色通道,愣是写错了索引,结果跑出来一堆乱码。嗯,从那以后我彻底搞懂了逗号分隔的维度索引是怎么回事。
4.1 从一维到多维:索引的扩展
先看个简单的。一维数组的切片,你肯定熟悉:
import numpy as np
arr_1d = np.array([0, 1, 2, 3, 4, 5])
print(arr_1d[2:5]) # 输出 [2 3 4]
二维数组呢?用逗号隔开行和列:
arr_2d = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
print(arr_2d[1, :2]) # 第1行,前两列 → [4 5]
你看,逗号左边是第 0 维(行),右边是第 1 维(列)。这个规则一直往上推——有多少维,就用多少个逗号分隔的切片表达式。
核心原则:tensor[dim0_slice, dim1_slice, dim2_slice, ...]
每个维度独立切片,最终结果是对应位置的“交集”。
4.2 三维张量:图像数据的切片实战
三维张量在模型中太常见了。比如一张彩色图片,shape 是 (H, W, C)——高、宽、通道。或者有些框架用 (C, H, W),但切片逻辑一样。
假设我们有一张 4×4 的 RGB 图像:
# 模拟一张 4x4 的 RGB 图像,shape = (4, 4, 3)
img = np.random.randint(0, 256, size=(4, 4, 3), dtype=np.uint8)
# 取左上角 2x2 区域的所有通道
patch = img[:2, :2, :]
print(patch.shape) # (2, 2, 3)
# 取整张图的红色通道(第0通道)
red_channel = img[:, :, 0]
print(red_channel.shape) # (4, 4)
# 取第2行、第3列的一个像素(所有通道)
pixel = img[2, 3, :]
print(pixel.shape) # (3,)
我个人习惯把三维切片想象成“切豆腐”。第一个维度切高度方向,第二个维度切宽度方向,第三个维度切通道方向。你切哪几刀,就拿到哪一块。
小技巧:如果你只想取某个维度的全部元素,用 : 或者 ...(省略号)都可以。img[..., 0] 等价于 img[:, :, 0],写起来更简洁。
4.3 四维张量:批量数据的切片逻辑
到了四维,才是真正的实战场景。模型训练时,数据通常是 (batch_size, channels, height, width) 或者 (batch_size, height, width, channels)。
举个例子,一个批次 32 张图,3 通道,224×224:
batch = np.random.randn(32, 3, 224, 224)
# 取第0张图
img0 = batch[0, :, :, :] # shape (3, 224, 224)
# 取前8张图的绿色通道(通道索引1)
green_batch = batch[:8, 1, :, :] # shape (8, 224, 224)
# 取第2张图,中心 100x100 区域
center_crop = batch[2, :, 62:162, 62:162] # shape (3, 100, 100)
# 每隔一张取一张,共16张
half_batch = batch[::2, :, :, :] # shape (16, 3, 224, 224)
你看,每个维度都可以独立使用 start:stop:step 的切片语法。我曾经在项目里写过一个 bug,把 batch[:8, 1, :, :] 写成了 batch[:8][1],结果取出来的是第 1 张图,而不是前 8 张图的绿色通道。这两个完全不一样——batch[:8] 返回前 8 张图,再 [1] 取第 1 张;而逗号分隔的写法是同时切片多个维度。
注意:arr[a][b] 和 arr[a, b] 在 NumPy 里结果可能不同。arr[a][b] 是先取第 0 维的 a,再取结果的第 0 维的 b;而 arr[a, b] 是同时取第 0 维的 a 和第 1 维的 b。对于高维张量,务必使用逗号分隔的写法。
4.4 高维张量切片的核心逻辑
到了 5 维甚至更高,逻辑完全一样。我画了一张图帮你理解:
从图上你能看到,维度每增加一层,切片表达式就多一个逗号分隔的索引。每个维度可以独立使用 start:stop:step、标量索引、或者 ... 省略号。
4.5 切片中的“维度坍缩”现象
这一点很重要,我经常看到新手在这里翻车。当你用标量(单个数字)索引某个维度时,那个维度会消失——也就是“坍缩”了。
arr = np.random.randn(4, 3, 5)
print(arr[0].shape) # (3, 5) → 第0维坍缩
print(arr[0, 1].shape) # (5,) → 第0、1维都坍缩
print(arr[0, 1, 2].shape) # () → 标量,0维张量
# 如果你不想坍缩,用切片保留维度
print(arr[0:1].shape) # (1, 3, 5) → 第0维保留,长度为1
为什么会这样?因为标量索引定位到一个具体元素,而切片索引保留了一个区间。在模型推理时,你经常需要保持批次维度存在——比如单张图推理,也要用 img[None, ...] 或者 img[np.newaxis, ...] 增加一个批次维度。
避坑指南:如果你写模型的前向传播,输入 shape 是 (B, C, H, W),即使 B=1 也要保持 4 维。用 input_tensor[0:1] 而不是 input_tensor[0],否则维度不对会报错。
4.6 省略号 ... 的妙用
高维张量切片时,写一堆冒号很烦人。省略号 ... 可以代表“所有剩余的维度”。
arr_5d = np.random.randn(2, 3, 4, 5, 6)
# 下面两种写法等价
a = arr_5d[0, ..., 0] # 取第0批次、最后一个维度的第0个
b = arr_5d[0, :, :, :, 0] # 同上
print(a.shape) # (3, 4, 5)
# 取所有批次、所有通道、第10到20行、所有列、所有深度
c = arr_5d[:, :, 10:20, ...] # 等价于 arr_5d[:, :, 10:20, :, :]
我个人习惯在 4 维及以上张量中尽量用 ...,代码更干净。但要注意:一个切片表达式里只能有一个 ...,否则解释器不知道你省略的是哪些维度。
4.7 实战:模型特征图的切片
最后来个真实场景。假设你从卷积网络中间层拿到一个特征图,shape 是 (1, 256, 14, 14)——1 张图,256 个通道,14×14 空间尺寸。你想可视化前 16 个通道的响应:
feature_map = np.random.randn(1, 256, 14, 14)
# 取前16个通道,保持批次和空间维度
vis_maps = feature_map[0, :16, :, :] # shape (16, 14, 14)
# 或者用省略号
vis_maps = feature_map[0, :16, ...] # 同上
# 如果你想把通道维度放到最后(方便 matplotlib 显示)
vis_maps = feature_map[0, :16, :, :].transpose(1, 2, 0) # shape (14, 14, 16)
你看,切片和维度重排经常配合使用。我在做模型可视化时,几乎每天都在写这类代码。记住一点:切片只是第一步,后续的 reshape、transpose、squeeze 才是真正把数据用起来的操作。
我的习惯:写切片前,先打印 tensor.shape 确认维度顺序。很多 bug 都源于记错了维度含义——比如 PyTorch 的 (B, C, H, W) 和 TensorFlow 的 (B, H, W, C) 切片方式完全不同。
4.8 本章小结
| 概念 | 说明 |
|---|---|
| 逗号分隔维度 | 每个维度一个切片表达式,用逗号隔开 |
| 标量索引坍缩 | 用单个数字索引的维度会消失 |
| 切片保留维度 | 用 start:stop 或 start:stop:step 保留维度 |
省略号 ... |
代表所有未显式指定的维度,一个表达式只能用一次 |
| 维度顺序 | 不同框架的维度排列不同,切片前务必确认 |