跳到正文
本地阅读 · 广播、axis 与向量化

广播、axis 与向量化

计算机基础 · NumPy

前置知识:ndarray、shape 与 dtype · 索引、切片与拷贝

本章目录

本章学习目标

  • 逐维判断广播是否合法并写出结果形状
  • 按业务含义选择聚合轴与 keepdims
  • 用数组运算实现标准化并检查边界

一、广播是形状规则,不是自动猜测业务

数组运算首先要求确定每个轴代表什么。本章约定二维矩阵的行是样本、列是特征,例如两天的三种商品销量 X=[[1,2,3],[4,5,6]],形状为 (2,3)。给每个商品加一个修正值 [10,20,30] 时,向量的长度对应列,结果为 [[11,22,33],[14,25,36]]。标量加法也遵守同一规则,但 NumPy 并没有为每个位置实际复制一份标量。

判断广播时把形状右对齐,从最右侧开始逐轴比较:长度相等,或其中一个是一,才可以继续;缺少的左侧轴按一处理。因此 (2,3) 与 (3,) 合法,结果为 (2,3);与 (2,) 不合法,因为最右侧三和二冲突。如果要给每一行加不同值,应把长度二的向量变成 (2,1)。相同元素数量不代表能广播;即使代码可以运行,也要检查它究竟沿哪个业务维度传播。本课程 NumPy 示例以 2.4.4 为验证版本。

二、用小矩阵理解 axis 和 keepdims

axis 指被聚合消去的轴,不是最终保留下来的轴。对 X=[[1,2,3],[4,5,6]],sum(axis=0) 把两行合成一行,结果 [5,7,9],回答每个商品总销量;sum(axis=1) 把每行三列合成一个数,结果 [6,15],回答每天总销量。不写 axis 通常聚合全部元素,得到二十一,已经无法区分日期或商品。

列均值是 [2.5,3.5,4.5],相减后两行都是各列相对平均的偏移。行均值是 [2,5],直接与 (2,3) 相减会失败。设置 mean(axis=1,keepdims=True) 保留被消去轴的长度一,结果形状 (2,1),即可得到 [[-1,0,1],[-1,0,1]]。保留维度不仅用于避免异常,还能把计算意图写进代码。三维数组同样适用;先在纸上标注样本、时间、特征,再决定需要消去哪个轴。

三、标准化:先写公式再写数组表达式

假设每列是不同单位的特征,标准化公式为每个值减去该列均值,再除以该列标准差。示例 [[1,10,7],[3,20,7]] 的列均值为 [2,15,7],总体标准差为 [1,5,0],前两列变成负一和正一,第三列是常数,没有可供放大的变化。直接除以零会产生警告和非有限值;本章明确把常数列中心化结果保留为零,并用安全分母一完成计算。

使用 mean(axis=0,keepdims=True) 和 std(axis=0,keepdims=True),二者形状都是 (1,3);np.where(std==0,1,std) 只改变分母,不改变原始特征。必须说明 std 默认 ddof=0 是总体口径,若统计估计要求样本标准差则选择 ddof=1,不能只因为两个结果不同就认定实现错误。在预测任务中均值和标准差只由训练集拟合,再原样用于验证与测试集,否则未来分布被带入训练流程。

四、向量化有成本,也需要反例

向量化是把逐元素工作交给数组底层实现,减少 Python 循环的解释开销;它并不会自动优化全部内存使用。a[:,None]-b[None,:] 可以构造两组数的两两差值,若两组各有十万个数,结果包含一百亿个元素,float64 仅结果就约八十 GB。广播输入通常不用重复存储,但运算结果和中间数组仍然需要内存,此时应分批处理或使用更合适的算法。

另一个危险反例是 (n,1) 减 (n,)。右对齐后形成 (n,n),程序可能成功但已从逐样本误差变成两两差值。机器学习中预测值和标签形状必须一致,先断言 shape,再算损失。不要把 np.vectorize 当作高性能保证,它主要提供逐元素函数调用的便利接口。写向量化代码时保留一个小规模循环参考,并用 allclose 验证数值等价,再考虑速度。

五、实验步骤与解释标准

先在交互演示选择按列和按行聚合,手算每个输出位置的来源,再运行配套脚本。脚本分别打印列和、行和、中心化、标准化与外差矩阵,并用断言检查常数列没有出现 NaN。把输入改成空的 (0,3) 时,形状仍然合法,但均值没有统计意义,应在数据入口拒绝空样本而不是静默填零。把第三列改成极小变化时,是否判定为常数要依据业务阈值,不能随意用精确零代替数值稳定性设计。

面试回答应包括四件事:每个轴代表什么;聚合后形状是什么;广播从哪些位置复用值;输入边界会导致什么结果。能画出形状对齐过程比背诵术语更可靠。网页交互是固定数据的 JavaScript 教学模拟,真实计算仍以本章 Python 脚本为准;如果模拟与脚本不一致,应记录输入和两个输出并检查规则,不能因为界面看起来合理就接受。

交互演示:观察机制

完整示例与实际输出

import numpy as np
X = np.array([[1., 2., 3.], [4., 5., 6.]])
print("column sum:", X.sum(axis=0).tolist())
print("row sum:", X.sum(axis=1).tolist())
centered = X - X.mean(axis=1, keepdims=True)
assert np.allclose(centered, [[-1, 0, 1], [-1, 0, 1]])
print("row centered:", centered.tolist())
features = np.array([[1., 10., 7.], [3., 20., 7.]])
mu = features.mean(axis=0, keepdims=True)
sd = features.std(axis=0, keepdims=True)
Z = (features - mu) / np.where(sd == 0, 1., sd)
assert np.allclose(Z, [[-1, -1, 0], [1, 1, 0]])
assert np.isfinite(Z).all()
print("standardized:", Z.tolist())
a = np.array([1., 3.]); b = np.array([10., 20., 30.])
outer = a[:, None] - b[None, :]
assert outer.shape == (2, 3)
print("pairwise difference:", outer.tolist())
try:
    X + np.array([1., 2.])
except ValueError:
    print("incompatible shapes correctly rejected")
else:
    raise AssertionError("broadcast must fail")

实际运行通过CPU Python execution.

column sum: [5.0, 7.0, 9.0]
row sum: [6.0, 15.0]
row centered: [[-1.0, 0.0, 1.0], [-1.0, 0.0, 1.0]]
standardized: [[-1.0, -1.0, 0.0], [1.0, 1.0, 0.0]]
pairwise difference: [[-9.0, -19.0, -29.0], [-7.0, -17.0, -27.0]]
incompatible shapes correctly rejected

五道自测:先回答,再核对

01 · (4,3) 与 (3,) 相加得到什么形状?

查看提示

把后一形状补成 (1,3)。

查看参考答案

(4,3),长度三的向量沿四行复用。

02 · (4,3) 与 (4,) 为什么失败?

查看提示

从右侧比较。

查看参考答案

最右侧长度三与四都不为一且不相等;按行加数需要 (4,1)。

03 · 对两行三列矩阵求 axis=0 的均值保留谁?

查看提示

被消去的是哪一轴?

查看参考答案

消去行轴,保留三个列特征;无 keepdims 时 shape=(3,)。

04 · 标签 (100,) 与预测 (100,1) 相减是否安全?

查看提示

能运行不等于含义正确。

查看参考答案

会广播成 (100,100),得到所有样本两两误差;应先统一为相同形状。

05 · 广播输入很小,结果就一定很小吗?

查看提示

想想两两距离。

查看参考答案

不一定;(n,1) 与 (1,m) 会产生 (n,m) 的输出,需要检查结果内存。

两项动手任务

任务 1 · 将 [[2,4],[6,8],[10,12]] 按列中心化,打印形状并验证每列均值接近零。

提示

axis=0;均值保持二维。

完整参考解答与检查方式

列均值为六和八,中心化后三行为负四、零、正四。

import numpy as np
X = np.array([[2.,4.],[6.,8.],[10.,12.]])
Y = X - X.mean(axis=0, keepdims=True)
assert Y.shape == X.shape and np.allclose(Y.mean(axis=0), 0)
print(Y.tolist(), Y.shape)

任务 2 · 为每天两件商品销量 [[2,3],[4,5]] 分别乘日期权重 [10,100],解释错误写法为什么没有报错。

提示

把日期权重放在 (2,1),不要留成 (2,)。

完整参考解答与检查方式

正确结果是 [[20,30],[400,500]];错误的一维权重恰好长度等于列数,会按商品广播,得到 [[20,300],[40,500]]。

import numpy as np
X=np.array([[2,3],[4,5]]); w=np.array([10,100])
correct=X*w[:,None]; wrong=X*w
assert correct.tolist()==[[20,30],[400,500]]
print(correct.tolist(), wrong.tolist())

官方资料与掌握状态

完成两项任务后,按独立完成程度记录。状态会尽量保存在当前浏览器,可导出备份。

动手任务记录

掌握状态与复习

关掉资料独立完成后记 A;提示后完成记 B;需要重学记 C。

记录尽量保存在当前浏览器,建议定期导出备份。