Appearance
NumPy:从数组基础到高效数值计算
NumPy 的学习主线不是记住函数清单,而是能够在计算前说明输入和输出的形状、数值类型、轴语义以及内存是否共享。本手册从可运行的小数组出发,逐步建立广播、统计、线性代数和工程实践能力。
阅读方式
第一至第三篇建立数组心智模型,第四篇处理数值可靠性,第五篇进入高阶计算,第六篇关注性能与交付。每个 Python 代码块自带导入和数据,可独立运行;示例中的断言用于核对语义,不代表穷尽所有输入情况。
导航目录
第一篇:入门——认识数组
第二篇:基础——正确选择与组织数据
第三篇:核心——用数组表达计算
第四篇:可靠性——处理数值边界
第五篇:进阶——线性代数与批量计算
第六篇:工程——性能、实战与排错
一、定位、环境与第一个计算
1.1 NumPy 解决什么问题
NumPy 提供多维数组和批量数值运算。规则、同类型的数值数据适合它;有列名、混合类型和表连接需求时通常使用 Pandas,优化、积分等科学算法可使用 SciPy,自动微分或 GPU 训练需选择对应框架。NumPy 可以与这些工具交换数据,但并不等于它们的底层执行引擎。
| Python 列表 | NumPy 数组 |
|---|---|
| 存储对象引用,可容纳异质对象 | 所有元素遵循同一 dtype,可包含结构化字段 |
| 加号通常表示拼接 | 加号表示逐元素数值运算 |
| 常用 Python 循环逐个处理 | 将批量运算交给底层实现 |
| 长度动态变化方便 | 固定大小缓冲区更适合批量计算 |
NumPy 数组也能存对象引用,但这种 object 数组通常失去紧凑数值存储和运算性能优势。小规模任务中,转换与分配成本可能比计算更大,不能断言任何 NumPy 写法都更快。
1.2 环境与版本边界
建议使用 Python 3.11+、NumPy 2.x;以下范围用于限制主版本,不是跨平台已验证的锁文件。正式项目固定实际验证过的 Python、NumPy 和依赖版本。
bash
python -m venv .venv
# Windows cmd 激活:.venv\Scripts\activate.bat
python -m pip install "numpy>=2.0,<3"本文不使用 NumPy 2.0 已移除的 asfarray,改为显式指定浮点类型的 asarray;也不依赖较新次版本才新增的参数。升级到 2.x 时,要同时核对类型提升规则和其他扩展库的二进制兼容性。
1.3 第一个带验收的计算
python
import numpy as np
scores = np.array([[80, 90, 100], [70, 80, 90]], dtype=np.float64)
student_mean = scores.mean(axis=1)
print("NumPy:", np.__version__)
print("每个学生的均分:", student_mean)
np.testing.assert_allclose(student_mean, [90, 80])这里行代表学生、列代表科目;沿科目轴求平均后,每个学生保留一个结果。轴编号不自带业务意义,业务语义由数据组织方式决定。
二、形状、轴与数据类型
2.1 先写出形状,再写表达式
text
二维成绩表 shape = (学生数, 科目数) = (2, 3)
科目 0 科目 1 科目 2
学生 0 80 90 100
学生 1 70 80 90
axis=0 聚合:消去学生轴 -> 每科一个结果,shape=(3,)
axis=1 聚合:消去科目轴 -> 每人一个结果,shape=(2,)| 属性 | 含义 | 不应混淆 |
|---|---|---|
| ndim | 轴的数量 | 不等于元素数 |
| shape | 每个轴的长度 | 一维向量不是行矩阵或列矩阵 |
| size | 元素总数 | 不等于字节数 |
| dtype | 元素表示方式 | 不等于业务单位 |
| itemsize | 一个元素的字节数 | object 数组统计的是引用槽位 |
| nbytes | size × itemsize | 不包含对象开销,也不反映共享缓冲区实际占用 |
python
import numpy as np
a = np.arange(6, dtype=np.int32).reshape(2, 3)
scalar = np.array(7)
vector = np.array([1, 2, 3])
print(a.shape, a.ndim, a.size, a.dtype, a.itemsize, a.nbytes)
assert a.nbytes == 24
assert scalar.shape == ()
assert vector.shape == (3,)
assert vector[None, :].shape == (1, 3)
assert vector[:, None].shape == (3, 1)2.2 dtype 是计算契约
整数位宽决定范围,浮点位宽影响精度和内存,布尔数组用于掩码。混合整数和浮点常被提升为浮点,混合数字与字符串可能变成字符串数组,并不是仍能进行数值加法。
显式使用 int32、int64、float32、float64 便于表达意图。默认整数类型受版本和平台影响,不把示例中默认类型写死;索引尺寸可使用平台对应的 intp。
三、数组创建与随机数
3.1 根据数据来源选择工具
| 工具 | 适用目的 | 注意事项 |
|---|---|---|
| array | 从列表、元组构建数组 | 默认复制输入数组数据 |
| asarray | 接收已有数组或可转换输入 | dtype、布局满足时可复用,列表仍需分配 |
| zeros / ones / full | 初始化确定值 | 显式指定所需 dtype |
| empty | 分配待覆盖的数值缓冲区 | 未初始化值没有可依赖含义,不是随机采样 |
| arange | 按步长创建半开序列 | 浮点步长可能产生边界误差 |
| linspace | 按数量均匀采样 | 默认包含终点,可设置 endpoint=False |
| eye / identity / diag | 单位类矩阵和对角结构 | diag 对一维输入构造矩阵,对二维输入提取对角 |
python
import numpy as np
print(np.array((1, 2, 3), dtype=np.float64))
print(np.zeros((2, 3)), np.ones(3, dtype=np.int32))
print(np.full((2, 2), -1))
buffer = np.empty((2, 3), dtype=np.float64)
buffer[:] = 5.0 # 完整覆盖之后才读取
assert np.all(buffer == 5)
np.testing.assert_array_equal(np.arange(0, 6, 2), [0, 2, 4])
np.testing.assert_allclose(np.linspace(0, 1, 5), [0, 0.25, 0.5, 0.75, 1])
print(np.eye(2, 3), np.identity(2), np.diag([2, 3]))
record = {"height": 170, "weight": 60}
fields = ["height", "weight"]
values = np.array([record[name] for name in fields], dtype=np.float64)
assert values.shape == (2,)字典直接传入 array 不会自动成为规则数值表,应明确字段与顺序。不规则嵌套列表也不是普通二维数值矩阵,应先补齐、分组或使用更合适的数据结构。
3.2 独立随机生成器
python
import numpy as np
rng = np.random.default_rng(42)
print(rng.random((2, 3))) # [0, 1) 均匀分布
print(rng.normal(0, 1, size=5)) # 均值 0、标准差 1
print(rng.integers(1, 10, size=5)) # 包含 1,不包含 10
print(rng.choice(10, size=3, replace=False))
print(rng.permutation(5))
a = np.random.default_rng(42).integers(0, 100, 10)
b = np.random.default_rng(42).integers(0, 100, 10)
np.testing.assert_array_equal(a, b)相同环境、相同生成器状态和调用顺序可复现;多次调用同一生成器会推进状态,不会重复首批结果。Generator 不承诺跨所有版本的随机序列完全一致,长期复现需记录版本、算法和调用方式。并行任务使用 SeedSequence 派生子流,不给所有任务重复相同种子。
四、索引、切片与条件选择
4.1 基础索引保留哪些轴
整数索引通常消去对应轴;切片保留轴,区间左闭右开;None 增加长度为 1 的轴,省略号代表未显式写出的轴。
python
import numpy as np
a = np.arange(12).reshape(3, 4)
assert a[1, 2] == 6
assert a[1].shape == (4,)
assert a[1:2].shape == (1, 4)
assert a[:, 1].shape == (3,)
assert a[:, 1:2].shape == (3, 1)
np.testing.assert_array_equal(a[0, ::-1], [3, 2, 1, 0])
print(a[..., -1])4.2 布尔索引与高级索引
与数组同形的完整布尔掩码提取元素,结果为一维;只匹配首轴的布尔掩码筛选行,保留其余轴。整数数组索引与布尔索引统称高级索引,读取结果是副本。
python
import numpy as np
a = np.arange(12).reshape(3, 4)
selected = a[a > 7]
rows = a[np.array([True, False, True])]
assert selected.shape == (4,)
assert rows.shape == (2, 4)
paired = a[[0, 2], [1, 3]] # 成对坐标,不是笛卡尔积
rectangle = a[np.ix_([0, 2], [1, 3])]
np.testing.assert_array_equal(paired, [1, 11])
np.testing.assert_array_equal(rectangle, [[1, 3], [9, 11]])
assert not np.shares_memory(rows, a)
a[a % 2 == 0] = -1 # 直接索引赋值仍修改原数组
assert a[0, 0] == -1不要把“高级索引读取返回副本”误解成“索引赋值无法修改原数组”。链式写法先取出副本再修改可能失效,应一次明确写出目标索引。
4.3 重复索引累加
python
import numpy as np
indices = np.array([0, 0, 1])
a = np.zeros(3, dtype=np.int64)
b = a.copy()
a[indices] += 1 # 带缓冲的读取、运算、写回,不保证逐次累加
np.add.at(b, indices, 1)
np.testing.assert_array_equal(a, [1, 1, 0])
np.testing.assert_array_equal(b, [2, 1, 0])对重复下标逐项累加使用 add.at;若只是非负整数标签计数,通常 bincount 更直接。
五、视图、副本与内存布局
5.1 元数据与缓冲区分开理解
核心模型
数组由数据缓冲区以及 shape、dtype、strides 等元数据组成。多个数组可共享同一缓冲区;换形状或转置不一定搬运数据,但修改共享数据会相互影响。
text
原数组 -------- 元数据 A ----+
+---- 共享数据缓冲区
切片视图 ------ 元数据 B ----+
显式副本 ------ 元数据 C --------- 独立数据缓冲区python
import numpy as np
a = np.arange(6, dtype=np.int64)
view = a[1:4]
copied = view.copy()
view[0] = 99
copied[0] = -1
assert a[1] == 99
assert np.shares_memory(a, view)
assert not np.shares_memory(a, copied)
assert np.asarray(a) is a
assert not np.shares_memory(np.array(a), a)普通数值数组的基本切片是视图,标量索引返回的是元素值。object 数组的 copy 只复制引用槽位,不深复制内部 Python 对象。小视图也可能使整个大缓冲区继续存活,长期保存小子集时复制反而能释放大块内存。
5.2 连续性影响转换成本
python
import numpy as np
a = np.arange(12, dtype=np.float64).reshape(3, 4)
t = a.T
print("strides:", a.strides, t.strides)
assert a.flags.c_contiguous
assert t.flags.f_contiguous
assert np.shares_memory(a, t)
assert np.shares_memory(a, a.ravel())
assert not np.shares_memory(a, a.flatten())
assert not np.shares_memory(t, t.ravel(order="C"))
contiguous = np.ascontiguousarray(t)
assert contiguous.flags.c_contiguousstrides 是沿每个轴移动一格的字节跨度,可为负或零;C/F 顺序分别优先最后/最前轴。reshape 和 ravel 在可行时共享内存,否则复制;flatten 总返回副本。仅查看 base 或对象身份不足以判断两个数组是否重叠,shares_memory 更直接,极复杂步长情况下精确判断也可能昂贵。
六、形状调整、拼接与拆分
6.1 reshape 不是转置
| 操作 | 改变什么 | 边界 |
|---|---|---|
| reshape | 按指定遍历顺序重新分组 | 元素总数不变,只能推断一个 -1 维度 |
| transpose / T | 重排轴 | 一维数组的 T 不会变成列矩阵;高维 T 反转全部轴 |
| swapaxes / moveaxis | 交换或移动指定轴 | 不修改轴内元素顺序 |
| expand_dims / None | 新增长度为 1 的轴 | 常用于广播对齐 |
| squeeze | 移除长度为 1 的轴 | 指定 axis 可避免意外删除批次轴 |
python
import numpy as np
a = np.arange(12).reshape(3, 4)
assert a.reshape(2, -1).shape == (2, 6)
assert a.T.shape == (4, 3)
assert not np.array_equal(a.T, a.reshape(4, 3))
batch = a[None, :, :]
assert batch.shape == (1, 3, 4)
assert np.squeeze(batch, axis=0).shape == (3, 4)
assert np.swapaxes(batch, 1, 2).shape == (1, 4, 3)
assert np.moveaxis(batch, 0, -1).shape == (3, 4, 1)6.2 两种 resize 完全不同
数组方法 resize 原地更改大小,扩展部分填零,但重分配受数据所有权和引用等约束;函数 np.resize 返回新数组,扩展时重复原数据。二者都不是图像插值缩放,普通形状变换优先 reshape。
python
import numpy as np
a = np.array([1, 2, 3])
a.resize(5)
np.testing.assert_array_equal(a, [1, 2, 3, 0, 0])
b = np.resize(np.array([1, 2, 3]), 5)
np.testing.assert_array_equal(b, [1, 2, 3, 1, 2])6.3 现有轴拼接与新轴堆叠
python
import numpy as np
a = np.arange(6).reshape(2, 3)
b = a + 10
assert np.concatenate([a, b], axis=0).shape == (4, 3)
assert np.concatenate([a, b], axis=1).shape == (2, 6)
assert np.stack([a, b], axis=0).shape == (2, 2, 3)
assert np.vstack([a, b]).shape == (4, 3)
assert np.hstack([a, b]).shape == (2, 6)
assert np.dstack([a, b]).shape == (2, 3, 2)
parts = np.split(a, 2, axis=0)
assert np.shares_memory(parts[0], a)
assert [len(x) for x in np.array_split(np.arange(7), 3)] == [3, 2, 2]concatenate 要求非拼接轴相同,stack 要求输入形状相同;hstack 对一维输入沿唯一轴拼接,对二维输入沿列拼接。split 等分需整除,array_split 允许不等长;vsplit、hsplit 是指定方向的便利接口。避免循环中反复 concatenate 或 append,它们会重复分配复制,应收集分块后一次拼接或预分配。
七、逐元素运算与通用函数
7.1 运算单位是元素
加减乘除、幂、取余与比较通常按元素计算;乘号不是矩阵乘法。sqrt、exp、log、sin 等通用函数(ufunc)把数值操作应用到数组,支持广播以及 out、where 等参数。
python
import numpy as np
a = np.array([1, 4, 9], dtype=np.float64)
np.testing.assert_allclose(np.sqrt(a), [1, 2, 3])
np.testing.assert_allclose(np.log(np.exp([0., 1.])), [0, 1])
np.testing.assert_allclose(np.sin([0, np.pi / 2]), [0, 1], atol=1e-15)
print(a + 2, a * 2, a / 2, a ** 2, a % 2)
mask = (a >= 4) & (a < 9)
assert mask.tolist() == [False, True, False]
assert mask.any() and not mask.all()
np.testing.assert_array_equal(np.logical_not(mask), ~mask)组合逐元素条件要加括号并使用 &、| 或逻辑函数,不能用 Python 的 and/or。不要直接用多元素数组作为 if 条件,应明确是检查任意元素、全部元素还是数组是否非空。
7.2 条件选择不等于短路计算
where 的三个参数在调用前已求值,所以用 where 选择结果不能阻止除零或对负数开方。需要受控计算时,使用 ufunc 的 where,并预先初始化 out。
python
import numpy as np
numerator = np.array([4., 8., 9.])
denominator = np.array([2., 0., 3.])
result = np.full_like(numerator, np.nan)
np.divide(numerator, denominator, out=result, where=denominator != 0)
np.testing.assert_allclose(result, [2, np.nan, 3], equal_nan=True)
labels = np.where(numerator > 5, "high", "low")
assert labels.tolist() == ["low", "high", "high"]不提供已初始化的 out 时,被 where 屏蔽位置可能保留未初始化内容。原地运算还要考虑输出 dtype 和共享内存,整数数组不能无损地原地除以浮点数。
八、广播规则与形状推导
8.1 从尾轴逐一对齐
两个维度长度相等或其中之一为 1 时兼容;缺失的前导维度视为 1。结果取非 1 的长度:长度 0 与 1 兼容时结果仍为 0,不要简单理解为总取最大值。
text
每列偏置:
数据 (2, 3)
偏置 (3,) -> (1, 3)
结果 (2, 3)
每行偏置:
数据 (2, 3)
偏置 (2, 1)
结果 (2, 3)
不兼容: (2, 3) 与 (2,) -> 末轴 3 与 2 冲突python
import numpy as np
x = np.arange(6).reshape(2, 3)
column_bias = np.array([10, 20, 30])
row_bias = np.array([100, 200])
np.testing.assert_array_equal(x + column_bias, [[10, 21, 32], [13, 24, 35]])
np.testing.assert_array_equal(x + row_bias[:, None], [[100, 101, 102], [203, 204, 205]])
assert np.broadcast_shapes((2, 3), (2, 1)) == (2, 3)
expanded = np.broadcast_to(column_bias, (2, 3))
assert not expanded.flags.writeable
assert np.shares_memory(expanded, column_bias)broadcast_to 返回只读视图,避免把重复映射的内存当独立单元写入;需要写入时显式复制。广播可以避免物化重复输入,但计算结果仍可能分配很大内存。
8.2 能运行不代表语义正确
python
import numpy as np
predicted = np.array([[1.], [2.], [3.]])
target = np.array([1., 2., 3.])
wrong = predicted - target
assert wrong.shape == (3, 3) # 本想逐样本相减,却变成两两差
correct = predicted[:, 0] - target
assert correct.shape == (3,)
np.testing.assert_array_equal(correct, [0, 0, 0])机器学习中尤其要核对一维标签与二维预测。不能为了消除报错随意 reshape,应先确认每个轴表示什么;正式函数用显式异常验证契约,开发示例才用断言辅助检查。
九、按轴统计、排序与计数
9.1 聚合是消去指定轴
python
import numpy as np
x = np.array([[1., 2., 3.], [4., 5., 6.]])
column_mean = x.mean(axis=0, keepdims=True)
row_sum = x.sum(axis=1)
assert column_mean.shape == (1, 3)
np.testing.assert_allclose(column_mean, [[2.5, 3.5, 4.5]])
np.testing.assert_allclose(row_sum, [6, 15])
np.testing.assert_allclose((x - column_mean).mean(axis=0), [0, 0, 0])
print("median:", np.median(x), "quartiles:", np.quantile(x, [0.25, 0.75]))
print("population std:", x.std(), "sample std:", x.std(ddof=1))
index = x.argmax()
assert np.unravel_index(index, x.shape) == (1, 2)
assert x.argmax(axis=1).tolist() == [2, 2]keepdims 将被聚合轴保留为长度 1,便于回广播。std/var 默认 ddof=0,分母为 N;ddof=1 使用 N-1,在合适采样假设下用于样本方差的无偏估计,但样本标准差本身并非因此无偏。有效样本数必须大于 ddof。
argmin/argmax 未指定轴时返回扁平索引,指定轴则返回该轴位置;并列时通常取首次位置。quantile 用 0 ~ 1 的分位水平,percentile 用 0 ~ 100,具体插值方法会影响小样本结果。
9.2 排序、去重与标签计数
python
import numpy as np
values = np.array([30, 10, 30, 20])
order = np.argsort(values, kind="stable")
np.testing.assert_array_equal(values[order], [10, 20, 30, 30])
np.testing.assert_array_equal(np.sort(values), values[order])
unique, inverse, counts = np.unique(values, return_inverse=True, return_counts=True)
np.testing.assert_array_equal(unique[inverse], values)
np.testing.assert_array_equal(counts, [1, 1, 2])
labels = np.array([0, 2, 2, 1, 2])
np.testing.assert_array_equal(np.bincount(labels, minlength=4), [1, 1, 3, 0])
print("cumulative:", np.cumsum([1, 2, 3]))
print("insertion positions:", np.searchsorted([10, 20, 30], [15, 30]))sort 函数返回排序副本,数组 sort 方法原地修改;argsort 返回索引,适合让关联数据保持同步。unique 默认排序,不是保留首次出现顺序;bincount 需要非负整数标签,最大标签非常大时可能造成巨大分配。Top-k 可用 argpartition 减少完整排序成本,但选中部分内部仍无序,需要再排序。
十、缺失、无穷与异常值
10.1 先识别,再决定如何处理
NaN 表示非数值结果或某种缺失约定,无穷可能来自溢出或除零。整数数组不能直接表示浮点 NaN;NaN 不等于自身,使用 isnan 检查,isfinite 同时排除 NaN 和正负无穷。
python
import numpy as np
a = np.array([1., np.nan, np.inf, -np.inf, 5.])
assert np.isnan(a).tolist() == [False, True, False, False, False]
assert np.isinf(a).tolist() == [False, False, True, True, False]
np.testing.assert_array_equal(a[np.isfinite(a)], [1, 5])
clean = np.nan_to_num(a, nan=0., posinf=100., neginf=-100.)
np.testing.assert_array_equal(clean, [1, 0, 100, -100, 5])
assert np.isnan(a[1]) # 默认返回副本
finite_or_nan = np.where(np.isfinite(a), a, np.nan)
assert np.nanmean(finite_or_nan) == 3nanmean 忽略 NaN,但不会忽略无穷。nan_to_num 默认用巨大有限值代替无穷,可能把计算错误隐藏成离群点;实际处理策略应基于业务,不是遇到缺失就填零。
10.2 全缺失列要有明确策略
python
import numpy as np
x = np.array([[1., np.nan, 5.], [3., np.nan, np.nan]])
finite = np.isfinite(x)
count = finite.sum(axis=0)
total = np.where(finite, x, 0.).sum(axis=0)
means = np.full(x.shape[1], np.nan)
np.divide(total, count, out=means, where=count > 0)
np.testing.assert_allclose(means, [2, np.nan, 5], equal_nan=True)
assert (count == 0).tolist() == [False, True, False]选择报错、丢列、固定默认值或保留缺失,取决于输入契约;不能在没有观测的列上假装估计出均值。机器学习中填补与截断阈值只能从训练数据学习,验证与推理复用同一规则。
10.3 截断与浮点错误控制
python
import numpy as np
values = np.array([-10., 20., 150.])
np.testing.assert_array_equal(np.clip(values, 0, 100), [0, 20, 100])
try:
with np.errstate(divide="raise", invalid="raise", over="raise"):
np.log(np.array([0., 1.]))
except FloatingPointError:
print("捕获预期的 log(0) 错误")
else:
raise AssertionError("应触发浮点错误")clip 改变极端值而不是删除样本,不会证明样本是错误数据,也不处理 NaN。errstate 临时改变错误处理方式,退出恢复原配置;不要全局屏蔽警告来掩盖算法问题。
十一、精度、溢出与类型转换
11.1 在计算之前选择类型
python
import numpy as np
small = np.array([100, 120], dtype=np.int8)
print("int8 range:", np.iinfo(np.int8))
print("float32 precision:", np.finfo(np.float32).eps)
wide = small.astype(np.int64)
np.testing.assert_array_equal(wide * 2, [200, 240])
floats = np.asarray([1, 2, 3], dtype=np.float64)
assert floats.dtype == np.float64
np.testing.assert_array_equal(np.array([1.9, -1.9]).astype(np.int32), [1, -1])
assert floats.astype(np.float64, copy=False) is floats固定宽度整数可能溢出,数组整数运算未必发出警告;先溢出再转宽类型无法恢复结果。astype 默认复制,copy=False 只是允许复用,改变 dtype 仍可能需要分配。浮点转整数是朝零截断,不是四舍五入,还应先检查有限性和范围。
NumPy 2.x 与 Python 标量混算时不一定按 Python 值大小提升数组精度,超范围标量可能直接报错。不要依赖隐式提升来保护数值范围;也不要使用旧教程中的 copy=False 来表达“必要时允许复制”,NumPy 2.x 对部分构造接口的此选项要求严格不复制,普通转换应使用 asarray。
11.2 近似数值使用明确容差
python
import numpy as np
a = np.array([0.1 + 0.2])
b = np.array([0.3])
assert not np.array_equal(a, b)
np.testing.assert_allclose(a, b, rtol=1e-12, atol=1e-15)
np.testing.assert_array_equal(np.round([0.5, 1.5, 2.5, 3.5]), [0, 2, 2, 4])
x = np.array([1., 2., 3.], dtype=np.float32)
mean = x.mean(dtype=np.float64)
assert mean.dtype == np.float64isclose/allclose 常按绝对误差不超过 atol 加 rtol 乘参考值绝对值判定,并非天然对称;接近零时默认绝对容差可能过宽。shape 不同也可能被广播后判为接近,验收形状应单独检查。精确整数结果用 array_equal。
round 对可精确表示的中点采用趋近偶数规则,二进制浮点还会影响十进制边界,不是通用的“逢五进一”。财务精确十进制应考虑整数最小单位或 Decimal。累加时提升精度可以减少误差,但不能补回输入在低精度存储阶段已丢失的信息。
十二、矩阵乘法与稳定求解
12.1 明确收缩的是哪些轴
| 运算 | 形状规则 | 语义 |
|---|---|---|
| 逐元素乘法 | 按广播规则 | 对应位置相乘 |
| 一维 @ 一维 | (k,) 与 (k,) 得标量 | 内积,不自动做复共轭 |
| 二维 @ 二维 | (m,k) 与 (k,n) 得 (m,n) | 矩阵乘法 |
| 批量 @ | (...,m,k) 与 (...,k,n) | 前导批次轴广播,最后两轴做矩阵乘法 |
| dot | 一二维常与预期内积/矩阵乘法相符 | 高维收缩规则不同于 matmul,不可无条件互换 |
python
import numpy as np
A = np.array([[1., 2.], [3., 4.]])
B = np.array([[5., 6.], [7., 8.]])
np.testing.assert_allclose(A @ B, [[19, 22], [43, 50]])
np.testing.assert_allclose(np.matmul(A, B), np.dot(A, B))
assert not np.array_equal(A * B, A @ B)
batch = np.stack([A, A + 1])
assert (batch @ B).shape == (2, 2, 2)
np.testing.assert_allclose(np.einsum("bij,jk->bik", batch, B), batch @ B)12.2 解方程不必显式求逆
python
import numpy as np
A = np.array([[3., 1.], [1., 2.]])
b = np.array([9., 8.])
x = np.linalg.solve(A, b)
np.testing.assert_allclose(x, [2, 3])
np.testing.assert_allclose(A @ x, b)
print("residual:", np.linalg.norm(A @ x - b))
print("condition number:", np.linalg.cond(A))
print("determinant:", np.linalg.det(A))
np.testing.assert_allclose(A @ np.linalg.inv(A), np.eye(2), atol=1e-12)
design = np.array([[1., 0.], [1., 1.], [1., 2.]])
coef, residuals, rank, singular_values = np.linalg.lstsq(design, [1., 3., 5.], rcond=None)
np.testing.assert_allclose(coef, [1, 2])
assert rank == 2solve 用于方阵线性系统,lstsq 用于超定、欠定或秩相关最小二乘问题。奇异矩阵不能直接求逆;近奇异矩阵也可能返回严重不可靠结果。行列式不是稳健的病态判据,需结合条件数、尺度、秩和残差;残差小也不意味着解对扰动不敏感。
12.3 对称矩阵与分解
python
import numpy as np
A = np.array([[2., 1.], [1., 2.]])
values, vectors = np.linalg.eigh(A)
np.testing.assert_allclose(A @ vectors, vectors * values, atol=1e-12)
U, s, Vh = np.linalg.svd(A, full_matrices=False)
np.testing.assert_allclose((U * s) @ Vh, A, atol=1e-12)
assert np.linalg.norm(np.array([3., 4.])) == 5实对称或复 Hermitian 矩阵优先 eigh,一般方阵用 eig,后者可能返回复数。特征向量存在符号或复相位不唯一,重根对应子空间的基也不唯一;验证重构或方程关系,不硬编码向量符号。
十三、向量化、距离与滑动窗口
13.1 向量化不是把循环藏起来
真正的向量化将批量工作交给数组运算或线性代数实现;np.vectorize 主要提供调用便利,本质通常仍是 Python 层逐元素执行,不是编译加速器。批量计算还要控制临时数组,不能只比较代码行数。
python
import numpy as np
from timeit import repeat
x = np.arange(10_000, dtype=np.float64)
def loop_version():
return np.array([v * v + 1 for v in x])
def array_version():
return x * x + 1
np.testing.assert_allclose(loop_version(), array_version())
for name, function in [("loop", loop_version), ("array", array_version)]:
timings = repeat(function, number=3, repeat=3)
print(name, "best seconds per call:", min(timings) / 3)这里两种方法都返回数组,但它们的分配路径仍不同。计时只说明当前机器与样本规模的表现,不设固定加速倍数;测试前先验证数值一致性,正式基准还需考虑预热、线程、缓存和峰值内存。
13.2 两两距离的形状与内存
python
import numpy as np
X = np.array([[0., 0.], [1., 1.], [2., 0.]])
Y = np.array([[0., 1.], [3., 0.]])
difference = X[:, None, :] - Y[None, :, :]
assert difference.shape == (3, 2, 2)
direct = np.sum(difference ** 2, axis=-1)
expanded = (X ** 2).sum(axis=1, keepdims=True) + (Y ** 2).sum(axis=1)[None, :] - 2 * X @ Y.T
expanded = np.maximum(expanded, 0.)
np.testing.assert_allclose(direct, expanded)
np.testing.assert_allclose(direct, [[1, 9], [1, 5], [5, 1]])直接差分形成 N×M×D 中间数组;平方和展开避免该中间量,但仍生成 N×M 结果。对很大且彼此接近的向量,展开式容易出现消减误差;截零只处理微小负值,不能恢复丢失精度。需要高可靠近距离时可分块直接差分,或使用专业距离实现。
13.3 滑动窗口视图
python
import numpy as np
from numpy.lib.stride_tricks import sliding_window_view
signal = np.array([1., 2., 3., 4., 5.])
windows = sliding_window_view(signal, window_shape=3)
assert windows.shape == (3, 3)
assert np.shares_memory(windows, signal)
assert not windows.flags.writeable
np.testing.assert_allclose(windows.mean(axis=-1), [2, 3, 4])窗口间共享元素,默认只读;不要轻易用低层 as_strided 构造可写重叠视图。创建视图没有复制整个窗口集合,不代表后续聚合零成本,直接窗口平均通常仍需 O(N× 窗口长度) 运算;大窗口考虑累计和或专用滤波算法,并评估累计误差与边界约定。
十四、大数组、文件读写与复现
14.1 先做内存预算
一个形状为 (N,M,D) 的 float64 数值数组,其元素字节数是 N×M×D×8;表达式还可能同时保留输入、临时值和输出。nbytes 不是进程峰值内存,转置视图与原数组也不能简单相加来计算独立占用。
python
import numpy as np
x = np.linspace(0, 1, 1000, dtype=np.float64)
result = np.empty_like(x)
np.multiply(x, 2, out=result)
np.add(result, 1, out=result)
np.testing.assert_allclose(result, 2 * x + 1)
assert result.nbytes == x.size * x.itemsize
assert not np.shares_memory(result, x)out 可以复用缓冲区,但必须考虑别名和依赖顺序;把原输入改掉后再计算其统计量,可能悄悄改变算法。float32 占用通常是 float64 的一半,但应根据误差预算决定,而不是为省内存一律降精度。
14.2 文件格式与安全加载
| 格式/方式 | 优点 | 边界 |
|---|---|---|
| NPY | 保存单个数组的形状和 dtype | 适合数值数组,可用于内存映射 |
| NPZ | 一个容器保存多个命名数组 | 压缩省空间但增加解压成本,不适合按普通 NPY 方式直接映射 |
| CSV / 文本 | 易交换和人工检查 | 不完整保存 dtype/形状语义,读写慢且可能损失精度 |
| memmap / mmap_mode | 按需映射文件页 | 不是自动分布式,也不代表运算不占内存 |
不加载不可信的对象序列化
数值数组加载使用 allow_pickle=False。启用 pickle 可执行不可信对象代码,不应为了消除报错随意打开。即使禁用 pickle,外部大文件仍需大小、来源和资源限制。
下面使用内存文件演示保存与加载,不在工作目录生成制品:
python
from io import BytesIO, StringIO
import numpy as np
x = np.arange(12, dtype=np.float64).reshape(4, 3)
npy_buffer = BytesIO()
np.save(npy_buffer, x, allow_pickle=False)
npy_buffer.seek(0)
restored = np.load(npy_buffer, allow_pickle=False)
np.testing.assert_array_equal(restored, x)
assert restored.dtype == x.dtype
archive = BytesIO()
np.savez_compressed(archive, values=x, column_mean=x.mean(axis=0))
archive.seek(0)
with np.load(archive, allow_pickle=False) as content:
np.testing.assert_array_equal(content["values"], x)
text = StringIO()
np.savetxt(text, x, delimiter=",", fmt="%.17g")
text.seek(0)
np.testing.assert_allclose(np.loadtxt(text, delimiter=","), x)对已有可信 NPY 文件,可以通过 load 的 mmap_mode="r" 获得只读映射,并按行分块处理。普通运算仍可能物化整个结果;必须避免无意复制全量映射。写映射时需关注 flush、文件生命周期和跨进程并发,flush 不等于事务或进程同步。Windows 上不要在映射仍被持有时假设能替换或删除文件。
14.3 可复现不只是固定种子
记录 NumPy、Python、平台、BLAS 实现与线程设置,以及数据和代码版本。浮点归约次序、线程调度和数学库变化可能影响末位;使用业务容差验收,不默认要求跨平台逐位一致。
python
import numpy as np
seed = np.random.SeedSequence(2026)
children = seed.spawn(2)
streams = [np.random.default_rng(child) for child in children]
print("worker 0:", streams[0].integers(0, 100, 4))
print("worker 1:", streams[1].integers(0, 100, 4))子流用于合理隔离并行任务的随机状态,不把短样本偶然不相同当成统计独立性的证明。多进程和内部 BLAS 线程同时开启时还要防止过度并行。
十五、三个完整实践案例
15.1 训练统计量复用于新数据
目标:按列标准化,训练常量列的缩放因子设为 1,不发生除零;禁止在测试数据上重新估计均值和标准差。
python
import numpy as np
def fit_standardizer(data):
x = np.asarray(data, dtype=np.float64)
if x.ndim != 2 or 0 in x.shape or not np.isfinite(x).all():
raise ValueError("expected a nonempty finite 2D array")
mean = x.mean(axis=0, keepdims=True)
scale = x.std(axis=0, keepdims=True)
scale = np.where(scale == 0, 1., scale)
return mean, scale
def transform(data, mean, scale):
x = np.asarray(data, dtype=np.float64)
if x.ndim != 2 or x.shape[1] != mean.shape[1]:
raise ValueError("feature count mismatch")
if not np.isfinite(x).all():
raise ValueError("nonfinite input")
return (x - mean) / scale
train = np.array([[1., 10., 5.], [2., 20., 5.], [3., 30., 5.]])
test = np.array([[4., 40., 5.]])
mean, scale = fit_standardizer(train)
z_train = transform(train, mean, scale)
z_test = transform(test, mean, scale)
np.testing.assert_allclose(z_train.mean(axis=0), [0, 0, 0], atol=1e-12)
np.testing.assert_array_equal(z_train[:, 2], [0, 0, 0])
assert z_test.shape == (1, 3)
np.testing.assert_array_equal(train[:, 0], [1, 2, 3])
print("test standardized:", z_test)标准化不使任意分布变成正态分布;训练常量列在线上可能出现新值,仍需监控。这里处理精确零方差,近零方差和极大有限输入还需根据尺度、溢出和误差预算增加策略。Min-max 归一化也要处理零极差,未来值可能超出训练范围。
15.2 RGB 图像的通道归一化
目标:显式约定 H×W×C 布局,先转浮点再进行通道计算,避免无符号整数减法溢出。
python
import numpy as np
image = np.array([
[[255, 0, 0], [0, 255, 0]],
[[0, 0, 255], [255, 255, 255]],
], dtype=np.uint8)
if image.ndim != 3 or image.shape[-1] != 3:
raise ValueError("expected HWC RGB image")
x = image.astype(np.float32) / 255.0
mean = np.array([0.5, 0.5, 0.5], dtype=np.float32)
scale = np.array([0.5, 0.5, 0.5], dtype=np.float32)
normalized = (x - mean) / scale
chw = np.moveaxis(normalized, -1, 0)
assert normalized.shape == (2, 2, 3)
assert chw.shape == (3, 2, 2)
np.testing.assert_allclose(normalized[0, 0], [1, -1, -1])
restored = np.rint(np.clip(normalized * scale + mean, 0, 1) * 255).astype(np.uint8)
np.testing.assert_array_equal(restored, image)通道均值和缩放值仅用于演示,真实模型需遵循其训练预处理契约;还要核对 RGB/BGR、色彩空间、透明通道和取值范围。轴变换不等于图像缩放,转置视图也可能不连续,传给外部库前检查布局要求。
15.3 分块最近中心分配
目标:为每个样本寻找最近中心,不一次性创建整个 N×K×D 差分数组。这个函数只做分配,不包含中心更新,因此不是完整 K-Means 训练。
python
import numpy as np
def nearest_centers(data, centers, block_size=128):
x = np.asarray(data, dtype=np.float64)
c = np.asarray(centers, dtype=np.float64)
if x.ndim != 2 or c.ndim != 2 or x.shape[1] != c.shape[1]:
raise ValueError("expected two 2D arrays with matching feature counts")
if c.shape[0] == 0 or c.shape[1] == 0:
raise ValueError("centers and feature dimension must be nonempty")
if not isinstance(block_size, (int, np.integer)) or block_size <= 0:
raise ValueError("block_size must be a positive integer")
if not np.isfinite(x).all() or not np.isfinite(c).all():
raise ValueError("inputs must be finite")
result = np.empty(x.shape[0], dtype=np.intp)
for start in range(0, len(x), block_size):
block = x[start:start + block_size]
delta = block[:, None, :] - c[None, :, :]
squared_distance = np.einsum("bkd,bkd->bk", delta, delta)
result[start:start + len(block)] = squared_distance.argmin(axis=1)
return result
rng = np.random.default_rng(42)
x = rng.normal(size=(23, 4))
centers = rng.normal(size=(3, 4))
actual = nearest_centers(x, centers, block_size=5)
reference = ((x[:, None, :] - centers[None, :, :]) ** 2).sum(axis=-1).argmin(axis=1)
np.testing.assert_array_equal(actual, reference)
assert nearest_centers(np.empty((0, 4)), centers).shape == (0,)
print("assignments:", actual)差分中间量约为块大小 × 中心数 × 特征数 ×8 字节,仍需根据预算选块;中心数极大时还要沿中心维分块维护当前最优距离。大幅值可能在平方时溢出,有限输入并不保证有限结果,需约束尺度或增加浮点错误检查。
十六、排错速查与学习资源
16.1 按问题寻找原因
| 症状 | 首先检查 | 修正方向 |
|---|---|---|
| 广播报错 | 从末轴比对 shape 和业务含义 | 明确加轴,不随意 reshape |
| 能运行但输出维度异常 | 一维与列向量是否混用 | 固定输入输出契约 |
| 修改子数组改变原数组 | 基本切片、转置、共享缓冲区 | 隔离修改时 copy |
| 修改子数组没有影响原数组 | 高级索引是否先生成副本 | 直接对原数组索引赋值 |
| 重复索引没有累计多次 | 缓冲读写行为 | add.at 或 bincount |
| 原地除法报类型错误 | 输出 dtype 是否仍为整数 | 先转换浮点,或分配浮点输出 |
| 大整数计算得到负数 | 溢出是否已发生 | 计算前使用足够宽类型 |
| 归一化出现 NaN | 常量列、全缺失列、除零 | 显式策略和受控 divide |
| 随机结果不同 | 生成器状态、调用顺序、版本 | 独立状态与完整实验记录 |
| 性能低或内存高 | Python 回调、复制、临时张量、线程 | 真正向量化、分块、缓冲复用 |
| 求逆成功但结果异常 | 条件数、尺度与秩 | solve/lstsq、残差和扰动分析 |
16.2 关键接口速查
| 目的 | 首选工具 | 一句话提醒 |
|---|---|---|
| 构建数值数组 | array / asarray | 后者允许必要时复制,不保证独立数据 |
| 选择行列组合 | 基本切片 / ix_ | 成对高级索引不是笛卡尔积 |
| 判断内存共享 | shares_memory | 不是比较两个对象是否同一个 |
| 展平 | ravel / flatten | 前者可能复制,后者总复制 |
| 拼接与堆叠 | concatenate / stack | 现有轴与新轴的区别 |
| 按轴统计 | sum / mean / std | axis 指被消去的轴,keepdims 保留长度 1 |
| 有限值筛选 | isfinite | nanmean 不自动忽略无穷 |
| 近似比较 | allclose / testing.assert_allclose | 显式容差并检查 shape |
| 矩阵乘法 | matmul / @ | 高维 dot 不等价 |
| 线性系统 | solve / lstsq | 不默认先求逆 |
| 随机数 | default_rng / SeedSequence | 避免共享全局状态和重复子流 |
| 文件加载 | load | 数值文件禁用 pickle,检查资源规模 |
16.3 学习资源与验收标准
stable 文档会更新,应切换到安装版本核对参数。一个可靠数值函数至少应说明轴含义、输入范围、dtype、是否修改输入、错误策略与输出形状;测试正常值之外,还应覆盖空输入、常量、非有限值、重复索引和边界尺度。
结语
先确认形状和轴,再确认类型和内存,最后优化表达式与计算预算。能解释结果为什么正确、何时会失效,比把操作压缩成一行更重要。