跳到主要内容

从 NumPy 到 Tensor — 深度学习的数据容器

这一章讲三件事: 为什么 Python 自带的列表装不下深度学习的数据; NumPy 用「广播」和整批运算两招把数组运算做出几百倍的速度差; 以及 PyTorch 的 Tensor 怎么在 NumPy 的用法之上,加上 GPU 和梯度。 它是全书的地基:后面 12 章里出现的每一个张量(多维数组),都遵守本章定下的规矩。

1. 顶层全景:数据进模型前,先变成一摞数

一张图片 224×224×3、一句话 30 个词、一批 64 张图
│ 全部压平成「数的多维表格」

ndarray / Tensor(形状如 [64, 3, 224, 224])
│ ① 形状对不上?→ 广播规则自动对齐
│ ② 循环太慢? → 向量化,一条指令算整批

一批批喂给模型(Mini-Batch),GPU 整批并行吃下

图说:本章的所有机制都在解决同一个问题——让「几十万个数的同一套运算」
一次做完,而不是写循环一个一个来。

2. 为什么列表和数组都不够用

你用 Python 存一组数,第一反应是列表。但列表里存的不是数,是数的「地址」。

[1, 2, 3] 这三个数,列表实际存的是 3 个指针(内存地址)加 3 个整数对象——取一个数要先查地址再找值1。算 100 万个数的加法,光查地址就查 100 万次。

Python 自带的 array 类型确实直接存数值,但它不支持多维——而图片天生就是「高×宽×通道」的三维结构,矩阵(按行和列排成方队的一堆数)乘法需要二维2

深度学习要的是第三种东西:ndarray(N-dimensional Array,多维数组)——一整块连续内存,存同一类型的数据;配套一族能对整块数组操作的函数,叫 ufunc(universal function,通用函数)3

「连续 + 同类型」是后面一切速度的来源:知道了每个数多宽、排在哪里,取第 100 万个数不用查表,算一次偏移就知道在哪。CPU 的向量指令和 GPU 的几千个核心,都只吃这种摆法。

3. 生成与取用:四类来源,一套切片

建数组有四类入口,对应四种真实需求:

入口典型函数什么时候用
从已有数据转换np.array(列表)手头已有 Python 数据
随机生成np.random.rand / randn初始化网络参数
特定形状np.zeros / ones / eye / diag全零偏置(加在加法上的常数)、单位矩阵
等差序列np.arange / linspace造坐标点、画曲线

其中随机生成有一条纪律:先设随机种子(让随机序列从一个固定起点开始重放),同一份代码每次跑出同一份「随机」数——不然两次实验的差异,你分不清是方法不同还是运气不同4

取数只有一套写法,和 Python 列表的切片完全同构——行话叫索引:

nd[3] 第 4 个元素(从 0 数)
nd[3:6] 第 4 到第 6 个
nd[1:6:2] 隔一个取一个
nd[::-2] 倒着隔一个取
nd12[1:3,1:3] 二维:取第 2~3 行、第 2~3 列的交叉块
nd12[(nd12>3)&(nd12<10)] 条件:只取值在 (3,10) 开区间内的元素

图说:最后一条叫「布尔掩码」——用一个和数组同形状的 True/False
表去筛数。这在后面筛出「预测错的样本」时会天天用。

顺带一个容易踩的坑:linspace(0, 1, 10) 生成的不是 0.1、0.2、…、1.0,因为 linspace 保证包含起点和终点,10 个点把 [0,1] 等分成 9 段,步长是 1/9≈0.111。想得到 0.1 步长,把起点改成 0.1 即可5

4. 两种乘法:一字之差,两种运算

深度学习里 90% 的矩阵运算只涉及两种乘法,分清它们比记住一百个函数有用。

名字运算符规则输出形状
逐元乘(Element-Wise)*np.multiply对应位置的数各自相乘与输入相同
点积(Dot Product)np.dot行乘列再求和(m×k)·(k×n)→(m×n)

拿书里的例子:A=[[1,2],[-1,4]],B=[[2,0],[3,4]]。逐元乘把四个位置上的数各自相乘,得 [[2,0],[-3,16]];点积则是 A 的第 1 行与 B 的第 1 列做「乘了再加」:1×2+2×3=8,以此类推6。点积要求 A 的列数等于 B 的行数——这个「内部维度(行、列的长度数)一致」的约束,是后面每一章算网络层输出形状的依据。

逐元乘有一个立刻能看到的用途:把一个矩阵丢进激活函数(sigmoid、ReLU 这类逐个处理的函数),输出形状和输入一模一样

所以「线性(直来直去)变换 → 激活函数 → 线性变换」可以无限叠下去,形状永远对得上7

5. 主走查:广播——(4,1) 的矩阵怎么加上 (3,) 的向量

这是本章的主走查。 场景:给 4 个样本各减去一个均值向量。A 形状 (4,1),B 形状 (3,)。直接加,形状对不上——广播机制(broadcasting,NumPy 对形状不同的数组做运算时自动补齐形状的规则)在背后做了四步8:

A 是 (4,1): B 是 (3,):
[[ 0] [0 1 2]
[10]
[20]
[30]]

规则①:维度少的一方,在前面补 1 维 → B 从 (3,) 变成 (1,3)
规则②:输出形状 = 每个维度上取最大 → (4,3)
规则③:某维长度相等或为 1 才能运算 → 4 vs 1 ✓,3 vs 3 ✓
规则④:长度为 1 的维,沿该维「复制」第一组值去凑

A 复制成: B 复制成: 相加:
[[ 0 0 0] [[0 1 2] [[ 0 1 2]
[10 10 10] [0 1 2] [10 11 12]
[20 20 20] [0 1 2] [20 21 22]
[30 30 30]] [0 1 2]] [30 31 32]]

动手核一遍 A+B 的第 2 行:10+0=10、10+1=11、10+1=12,和输出第二行 [10,11,12] 一致9

最反直觉的一点:内存里并没有发生那次复制。 NumPy 用网格对象记下「这一维是重复的」,只在形状实在凑不上时才报错10。所以广播既省了写循环,也省了内存——这是它和「手动 repeat 再相加」的本质区别。

边界要记两条:广播只对「长度为 1 或相等」的维度放行,(4,2) 加 (3,) 会直接报错;以及它让错误静默——本想逐样本减均值,形状笔误成 (1,4) 也能广播成功,结果全错但不报错。审形状比审数值更重要。

6. 整批运算:同一个点积,差 400 倍

有了广播,「不写循环」就成了算得出来的收益。书里做了两组对照实验,数据都在同一个数量级:

对比慢的写法快的写法差距
100 万个数求 sinmath.sin 逐个循环:0.517snp.sin 整批:0.054s约 10 倍11
100 万维点积for 循环累加:798msnp.dot:1.9ms约 400 倍12

400 倍从哪来?循环版每圈都要走一遍「解释器取数→乘→加→存回」;np.dot 一次调用进 C 实现,连续内存+单指令多数据(SIMD,一条指令同时算多个数对)把开销摊平。结论:深度学习代码里出现显式 for 循环遍历数据,基本都是设计错误——要么用整批运算的算子,要么把数据分成小份分次喂:每小份叫一个批(一次喂进模型的一份数据),即下一节的 batch 切分。

batch 的标准做法是把「10 万条数据一次吃完会爆内存」和「一次 1 条又吃不满 GPU 的并行(几千个核心同时开工)能力」两头都避开。

具体四步:打乱、定批大小(一个批里装几条,如 100)、按步长切片、循环喂进模型13

这种按批喂的做法就叫批量(成批处理)。这个「打乱→切批」的四步,第 3 章 DataLoader 会原样再做一遍——区别只是 NumPy 手搓,还是框架代劳。

7. Tensor:带 GPU 和梯度的 NumPy

PyTorch 的核心数据结构 Tensor(张量)对 NumPy 用户几乎零成本:接口(调用写法)有两套风格(torch.add(x,y)x.add(y) 等价),切片语法(取子数组的写法)照搬,广播规则照搬14。真正多了三样东西:

其一,GPU。 ndarray 的加速在 CPU,Tensor 一个 .cuda() 就把数据搬进显存(GPU 的内存),由几千个核心整批并行算——第 5 章专讲加速本身15

其二,两套改数据的规矩。 x.add(y) 返回新张量、x 不变;x.add_(y) 带下划线后缀,就地把结果写回 x16。框架大量用下划线版做原地更新,读代码时看到 _ 结尾,先想「数据被改了」。

其三,构造函数的坑。 torch.tensor(1) 返回数值 1、类型从数据推断;torch.Tensor(1) 返回一个长度为 1 的随机初始化张量(默认浮点)——书里实测前者得 1,后者得 3.5731e-20 这样的随机值17。想要「形状为 1 的空张量」和「值为 1 的标量(单个数)」时,用错了就是找半天的 bug。

形状操作里还有一个内存陷阱值得单独说:viewreshape 都能把 (2,3) 变成 (3,2),但 view 要求原数据在内存里连续,不满足就报错;reshape 不满足时会自动复制一份再变。确定要共享内存(不复制)用 view;只想改形状用 reshape18。转置就是典型的「逻辑上变了、内存没动」操作,转置后想 view 得先调 contiguous() 把数据抄整齐。

8. 作者的判断与证据

给了证据的: 速度对比是作者跑出来的真实数字(10 倍、400 倍),不是估算;广播的四条规则有图形和代码双重演示,且明确指出实现上「不是真正复制,否则太耗内存」10

作者的建议,给理由的: 把 NumPy 放在全书第一章,理由是「Tensor 与 NumPy 非常相似,转换方便,掌握 NumPy 是学好 PyTorch 的重要基础」(「第1章 Numpy基础」第 1 段,text/02-ch01-1-numpy.txt:6,搜「重要基础」)。这个判断从今天看成立——后来 PyTorch 官方也把「NumPy 式体验」做成了卖点。

作者的偏好,没给硬证据的: 批处理「一般采用 Mini-Batch」只给了定性论证(整集会撑爆内存、单条吃不满并行)13,没给最优批大小怎么定的方法。这个缺口要到第 5 章讲学习率与优化器时才间接补上。

9. 边界与局限

  • 本章是 2020 年的 NumPy/PyTorch 1.0 用法。 下划线就地操作、torch.Tensor 大写构造函数今天仍在,但新代码更推荐 torch.tensor 小写版;GPU 初始化等细节也有更新。用法层面读本书时要带着「1.0 时代」这根弦。
  • 书里没讲 dtype 管理。 float32/float64 混算的精度坑、torch.dtype/torch.device 这套现代写法,要等附录 A 提到的 0.4 版本变更才露头。
  • 400 倍是 CPU 上的数字。 GPU 上「循环 vs 整批运算」的差距更大,但书没有给 GPU 版对照——GPU 的收益要等第 5 章才展开。

10. 可带走的

  1. 数值计算选 ndarray/Tensor,不选 Python 列表——省的是「查地址」的时间,得的是「整批运算」的资格;
  2. 逐元乘 vs 点积:形状相同时想逐元素,想「行乘列求和」才用点积;点积的维度约束决定了每一层网络的输出形状;
  3. 广播规则一句话:短的补 1 维,长度 1 的复制,输出取每维最大;但对不上形状会静默算错而不是报错,审形状先于审数值;
  4. 数值代码里出现遍历数据的 for 循环,先怀疑设计;同一点积循环版慢 400 倍是书里的实测;
  5. 随机实验先设种子,否则两次差异分不清是方法还是运气;
  6. x.add_(y) 的下划线 = 就地改数据;torch.Tensor(1) 是随机张量、torch.tensor(1) 是标量,别混;
  7. view 不复制内存但要求连续,reshape 保证成功可能复制;转置后要 contiguous() 才能 view;
  8. 批处理四步(取数→打乱→定批→切片)是全书所有训练循环的雏形。

11. 原文地图

主题原书章原文位置
list 存指针、array 不多维第1章 Numpy基础text/02-ch01-1-numpy.txt:10(搜「3个指针」) · text/02-ch01-1-numpy.txt:12(搜「不支持多维」)
ndarray 与 ufunc第1章 Numpy基础text/02-ch01-1-numpy.txt:676(搜「两种基本的对象」)
随机种子第1章 Numpy基础text/02-ch01-1-numpy.txt:116(搜「随机种子」)
linspace 包含端点第1章 Numpy基础text/02-ch01-1-numpy.txt:226(搜「步长则为」)
切片与掩码取数第1章 Numpy基础text/02-ch01-1-numpy.txt:239(搜「获取指定位置」) · text/02-ch01-1-numpy.txt:250(搜「值域之内」)
逐元乘与点积第1章 Numpy基础text/02-ch01-1-numpy.txt:297(搜「逐元乘法」) · text/02-ch01-1-numpy.txt:362(搜「内积」)
激活函数不改形状第1章 Numpy基础text/02-ch01-1-numpy.txt:339(搜「输出与输入形状一致」)
广播四规则第1章 Numpy基础text/02-ch01-1-numpy.txt:746(搜「广播机制」) · text/02-ch01-1-numpy.txt:749(搜「最长的数组看齐」)
广播实例与数值第1章 Numpy基础text/02-ch01-1-numpy.txt:770(搜「4×1矩阵」) · text/02-ch01-1-numpy.txt:801(搜「30 31 32」)
不真正复制内存第1章 Numpy基础text/02-ch01-1-numpy.txt:780(搜「不是真正复制」)
批处理理由与步骤第1章 Numpy基础text/02-ch01-1-numpy.txt:636(搜「平行处理优势」) · text/02-ch01-1-numpy.txt:647(搜「批处理数据集」)
sin 快 10 倍、点积快 400 倍第1章 Numpy基础text/02-ch01-1-numpy.txt:707(搜「快近10倍」) · text/02-ch01-1-numpy.txt:742(搜「400倍」)
Tensor 靠 GPU 加速第2章 PyTorch基础text/03-ch02-2-pytorch.txt:234(搜「放在GPU中进行加速运算」)
两套接口、下划线就地第2章 PyTorch基础text/03-ch02-2-pytorch.txt:239(搜「torch.function」) · text/03-ch02-2-pytorch.txt:250(搜「下划线后缀」)
Tensor 与 tensor 的区别第2章 PyTorch基础text/03-ch02-2-pytorch.txt:297(搜「推断数据类型」) · text/03-ch02-2-pytorch.txt:310(搜「3.5731e-20」)
view 与 reshape第2章 PyTorch基础text/03-ch02-2-pytorch.txt:367(搜「contiguous」) · text/03-ch02-2-pytorch.txt:373(搜「共享相同的数据」)

Footnotes

  1. 出处:「第1章 Numpy基础」第 10 段(text/02-ch01-1-numpy.txt:10,搜「3个指针」)。

  2. 出处:「第1章 Numpy基础」第 12 段(text/02-ch01-1-numpy.txt:12,搜「不支持多维」)。

  3. 出处:「第1章 Numpy基础」第 676 段(text/02-ch01-1-numpy.txt:676,搜「两种基本的对象」)。

  4. 出处:「第1章 Numpy基础」第 116 段(text/02-ch01-1-numpy.txt:116,搜「随机种子」)。

  5. 出处:「第1章 Numpy基础」第 226 段(text/02-ch01-1-numpy.txt:226,搜「步长则为」)。

  6. 出处:「第1章 Numpy基础」第 311 段(text/02-ch01-1-numpy.txt:311,搜「A*B」)与第 148 段(text/02-ch01-1-numpy.txt:148,搜「X3」)。两例的矩阵与输出值均出自原文。

  7. 出处:「第1章 Numpy基础」第 339 段(text/02-ch01-1-numpy.txt:339,搜「输出与输入形状一致」)。

  8. 出处:「第1章 Numpy基础」第 746 段(text/02-ch01-1-numpy.txt:746,搜「广播机制」)、第 749 段(text/02-ch01-1-numpy.txt:749,搜「最长的数组看齐」)。四条规则的原文表述在 749–764 段。

  9. 出处:「第1章 Numpy基础」第 770 段(text/02-ch01-1-numpy.txt:770,搜「4×1矩阵」)与第 801 段(text/02-ch01-1-numpy.txt:801,搜「30 31 32」)。输出矩阵四个数值行均为原文运行结果。

  10. 出处:「第1章 Numpy基础」第 780 段(text/02-ch01-1-numpy.txt:780,搜「不是真正复制」)。 2

  11. 出处:「第1章 Numpy基础」第 704 段(text/02-ch01-1-numpy.txt:704,搜「math.sin」)与第 707 段(text/02-ch01-1-numpy.txt:707,搜「快近10倍」)。

  12. 出处:「第1章 Numpy基础」第 738 段(text/02-ch01-1-numpy.txt:738,搜「798」)与第 742 段(text/02-ch01-1-numpy.txt:742,搜「400倍」)。

  13. 出处:「第1章 Numpy基础」第 636 段(text/02-ch01-1-numpy.txt:636,搜「平行处理优势」)与第 647 段(text/02-ch01-1-numpy.txt:647,搜「批处理数据集」)。 2

  14. 出处:「第2章 PyTorch基础」第 239 段(text/03-ch02-2-pytorch.txt:239,搜「torch.function」)。

  15. 出处:「第2章 PyTorch基础」第 234 段(text/03-ch02-2-pytorch.txt:234,搜「放在GPU中进行加速运算」)。

  16. 出处:「第2章 PyTorch基础」第 250 段(text/03-ch02-2-pytorch.txt:250,搜「下划线后缀」)。

  17. 出处:「第2章 PyTorch基础」第 297 段(text/03-ch02-2-pytorch.txt:297,搜「推断数据类型」)与第 310 段(text/03-ch02-2-pytorch.txt:310,搜「3.5731e-20」)。

  18. 出处:「第2章 PyTorch基础」第 367 段(text/03-ch02-2-pytorch.txt:367,搜「contiguous」)与第 373 段(text/03-ch02-2-pytorch.txt:373,搜「共享相同的数据」)。