NumPy 提供一种数据结构 ndarray 和一堆针对它的运算。 深度学习里所有数据都是它——图片是三维数组、词向量是二维数组、权重是矩阵。 要盯住三件事:shape(形状)、dtype(类型)、视图还是拷贝。九成的报错都出在形状上。 这一节把三件事都跑了一遍:3×1 加 1×4 广播成 3×4;切片改一个字原数组跟着变; 同样的求和,for 循环 25.0 毫秒、向量化 0.31 毫秒,差八十倍。
上一节讲了在哪儿写代码,这一节讲写代码时用得最多的库。
第 4 章之后所有 Demo 的第一步几乎都是 import numpy as np。
这一节的四个概念(shape、广播、视图、向量化)会反复出现。
如果只用过 Python 的 list,把 NumPy 想成「能整体做加减乘除的表格」就好。
一、三个属性:shape、dtype、轴
np.array([1,2,3,4,5,6]).reshape(2,3) 造出一个 2 行 3 列的数组,shape 是 (2, 3),
dtype 是 int64。
shape 是一串数,从外往里数:(2, 3) 是「2 行、每行 3 个」;
一张彩色图是 (高, 宽, 3),一批图是 (张数, 高, 宽, 3)。
轴(axis)就是维度的编号,从 0 开始。a.sum(axis=0) 是「沿着第 0 维压扁」,
也就是把各行加起来得到一个长度 3 的数组。搞不清 axis 时,记住「axis=k 表示这一维被消掉」。
二、创建与变形
常用的几个构造函数:np.array(从列表造)、np.zeros(全 0)、np.ones(全 1)、
np.arange(等差数列)、np.linspace(等分区间)。
reshape 不改变数据,只换一个看法——(2,3) 变成 (3,2) 还是那 6 个数,只是排列方式变了。
所以元素总数必须对得上,对不上直接报错。
深度学习里最常做的一次变形是 flatten 或 reshape(n, -1):把一张 32×32 的图摊成 1024 维的向量,
喂给全连结层。-1 的意思是「这一维你自己算」。
三、广播
两个形状不同的数组做运算,NumPy 会试着把它们「拉」成同一个形状,这叫广播。
规则一句话:从最后一维往前比,每一维要么相等、要么其中一个是 1。满足就拉长,不满足就报错。
实测:一个 3×1 加一个 1×4,结果是 3×4。3×1 被横向复制 4 份,1×4 被纵向复制 3 份,然后逐元素相加。
广播的意义是省内存:它不真的复制,只是在计算时按需要取值。
所以 (1000, 1) + (1, 1000) 不会占一百万个元素的额外空间。
四、视图与拷贝
切片给的是视图(view),不是拷贝。s = v[2:5] 之后改 s[0],v 也跟着变。
实测:v = [0 1 2 3 4 5],取 s = v[2:5],改 s[0] = 99,v 变成 [0 1 99 3 4 5]。
要真拷贝得写 v[2:5].copy()。
这个坑在处理图像时最致命:你只想改一份副本,结果把原图改了,后面所有实验的数据都污染了。
判断办法:s.base is v 为 True 就是视图。或者干脆养成习惯——凡是要改的,先 .copy()。
五、向量化为什么快
Python 的 for 循环每转一圈都要做类型检查、对象查找、函数调用;
NumPy 的运算一次交给底层的 C 循环,中间不过解释器。
实测:对 20 万个浮点数求和,逐个 for 循环 25.0 毫秒,.sum() 只要 0.31 毫秒——差八十倍。
所以写深度学习代码有一条铁律:能写成数组运算的,绝不写 for。第 5 章的卷积如果真用四重 for 写,
跑一张图就要几秒;换成 im2col 加矩阵乘,快两个数量级。
六、动手 Demo:形状、广播、视图
四段:五个常见表达式的 shape 与 dtype、一次广播、一次视图改写、一次向量化与 for 循环的速度对比。
import numpy as np
a = np.array([1, 2, 3, 4, 5, 6]).reshape(2, 3)
print(a.shape, a.dtype, a.T.shape) # (2, 3) int64 (3, 2)
print(np.zeros((2, 2, 2)).shape) # (2, 2, 2)
print(a.reshape(3, 2).shape) # (3, 2)
col = np.array([1.0, 2.0, 3.0]).reshape(3, 1) # 3 行 1 列
row = np.array([10.0, 20.0, 30.0, 40.0]).reshape(1, 4)
print((col + row).shape) # (3, 4) 广播
v = np.arange(6)
s = v[2:5] # 视图,不是拷贝
s[0] = 99
print(v) # [ 0 1 99 3 4 5]
实跑输出:
--------------------------------------------------------------------
数值运算函式库 NumPy:形状、广播、视图
--------------------------------------------------------------------
表达式 形状 dtype 内容
--------------------------------------------------------------
np.array([1,2,3,4,5,6]).reshape(2,3)(2, 3) int64 2 行 3 列
a.T (3, 2) int64 转置
np.zeros((2,2,2)) (2, 2, 2) float64 全 0
np.arange(6) (6,) int64 0~5
a.reshape(3,2) (3, 2) int64 换个看法
**shape 是 NumPy 最该盯住的东西。**九成的报错都是形状对不上,
而不是数值算错——广播规则会默默帮你对齐,对不齐才报错。
广播:一个 3×1 加一个 1×4,结果是 (3, 4)
11.0 21.0 31.0 41.0
12.0 22.0 32.0 42.0
13.0 23.0 33.0 43.0
**规则:从最后一维往前比,要么相等、要么有一个是 1。**
3×1 和 1×4 在每一维上都满足「有一个是 1」,于是各自被拉长成 3×4。
视图还是拷贝:v = [ 0 1 99 3 4 5],取切片 s = v[2:5] 再改 s[0] = 99
现在 v = [ 0 1 99 3 4 5]
**切片给的是视图,不是拷贝。**改 s 会连着 v 一起变。
要真拷贝得写 `v[2:5].copy()`——这个坑在改图像像素时最容易踩,
一改就把原图毁了,还找不到是哪一行干的。
同样的求和,两种写法:
逐个 for 循环 23.9 毫秒
向量化 .sum() 0.17 毫秒
两者结果 19999900000 与 19999900000
**差了八十倍。**NumPy 的循环跑在 C 里,Python 的 for 每转一圈都要过解释器。
这条规矩贯穿全书:**能写成数组运算的,绝不写 for。**
五个表达式的形状:(2,3)、转置 (3,2)、(2,2,2)、(6,)、reshape 后 (3,2)。
注意 (6,) 是一维,不是 (1,6)——这两个在矩阵乘里行为完全不同。
广播那一块:3×1 加 1×4 得 3×4,打出来是这样的四列:11、21、31、41 那一行,就是 1 加上 10、20、30、
40。每一行都是同一个 row 加上不同的 col。
视图那一块最该记住:改 s[0] 之后,v 里下标 2 那个位置变成了 99。
s 和 v 是同一块内存的两个名字。
速度那两行:25.0 毫秒对 0.31 毫秒。
同样 20 万个数、同样的结果,差八十倍。这就是为什么深度学习代码里几乎看不到 for 循环。
30 秒实验
- 把
a.reshape(3, 2)改成a.reshape(4, 2),看报什么错——这就是形状对不上的典型样子。 - 把广播那段改成 3×1 加 1×3,还能算出 3×3 吗?再改成 3×2 加 1×3,看报错。
- 把
.sum()换成np.add.reduce(big[:200000]),速度有差别吗?
常见误区
- 把
(6,)和(1,6)当一回事。前者是一维,后者是 1 行 6 列。 矩阵乘、广播、索引的行为都不一样。 - 用
=复制数组。b = a只是别名,改 b 就是改 a。要复制用a.copy()。 - 以为 reshape 会重排数据。它只换看法,数据在内存里的顺序没变。
真要重排得用
transpose或手动索引。
自测三问(点开对答案)
1. `(2,3)` 与 `(3,2)` 的区别是什么? —— 前者 2 行 3 列,后者 3 行 2 列。元素个数都是 6,但形状不同,矩阵乘和广播的结果完全不同。 2. 广播的规则一句话怎么说? —— 从最后一维往前比,每一维要么相等、要么其中一个是 1。满足就自动拉长,不满足就报错。 3. 为什么切片会改到原数组? —— 因为切片返回的是视图,和原数组共享同一块内存。要独立副本得显式 `.copy()`。数有了,接下来得看得见。下一节 Matplotlib 把数组画成线、画成图、画成矩阵。
- ☐ (本节完,下一节 3.4 资料绘制函式库 Matplotlib 的使用方法)