NumPy: 广播机制

广播机制

1. 你将学到

❶ 广播 3 条规则 ❷ 标量→1D→2D 广播 ❸ 兼容性判断 ❹ 常见陷阱 ❺ 广播 vs 循环性能


2. 一个开发者的真实故事

(1) 痛点

Bob 想把 1×3 行向量加到 4×3 矩阵每行上,老老实实写了 4 行 for 循环——代码丑、速度慢、还容易写错索引。

(2) 解法

Alice 一句话点醒:"不用循环——NumPy 自动广播。行向量'广播'成 4×3 再加。"Bob 看 Mermaid 图,瞬间理解维度如何"虚扩展"。

(3) 收益

删掉循环后,代码从 6 行缩到 1 行,速度提升 50 倍以上——广播是 NumPy 向量化运算的基石。


3. 广播规则详解

(1) 广播 3 条规则

NumPy 广播(broadcasting)让不同形状的数组在算术运算中自动"对齐"维度,无需手动复制数据。核心规则如下:

规则 说明 示例
规则 1:维度对齐 从最右侧维度开始,向左对齐,不足的维度在左侧补 1 (3,) + (2,3) → 补成 (1,3) + (2,3)
规则 2:尺寸为 1 可扩展 某维度尺寸为 1 时,沿该维度复制到与另一数组相同 (1,3) + (2,3)(2,3) + (2,3)
规则 3:其余必须相同 非尺寸-1 的维度必须完全一致,否则报错 (2,3) + (4,3) → ❌ 2≠4

广播规则速查表

情况 形状 A 形状 B 结果形状 是否兼容
标量 + 数组 () (3,4) (3,4)
1D + 2D (3,) (4,3) (4,3)
列向量 + 行向量 (4,1) (1,3) (4,3)
两个 2D (3,1) (1,5) (3,5)
维度不匹配 (2,3) (4,3)
无尺寸-1 可扩展 (3,) (4,)

(2) 广播步骤可视化

(4,3) 矩阵 + (3,) 行向量为例,广播的完整过程:

100%
graph TB
    A["Matrix A<br/>shape=(4,3)"] --> D{"Rule 1:<br/>Align dimensions"}
    B["Vector B<br/>shape=(3,)"] --> D
    D --> E["Pad left dim<br/>B: (3,) → (1,3)"]
    E --> F{"Rule 2:<br/>Expand dim=1"}
    F --> G["Expand B along axis 0<br/>(1,3) → (4,3)"]
    G --> H["A + B_expanded<br/>shape=(4,3)"]

    style A fill:#4CAF50,color:#fff
    style B fill:#2196F3,color:#fff
    style D fill:#FF9800,color:#fff
    style F fill:#FF9800,color:#fff
    style G fill:#9C27B0,color:#fff
    style H fill:#E91E63,color:#fff
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

关键理解:广播不复制数据——它只在计算时"假装"复制,实际通过 stride 技巧实现零拷贝。


(3) 标量 + 数组广播

标量被视为 0 维数组,广播时沿所有维度扩展。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:标量加数组(难度⭐)

PYTHON
import numpy as np

a = np.array([[1, 2, 3],
              [4, 5, 6]])

# Scalar broadcasts to (2,3)
result = a + 10
print(result)
# [[11 12 13]
#  [14 15 16]]

# Equivalent manual loop (slow!)
result_loop = np.empty_like(a)
for i in range(a.shape[0]):
    for j in range(a.shape[1]):
        result_loop[i, j] = a[i, j] + 10

print(np.array_equal(result, result_loop))  # True
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。

PYTHON
# Scalar works with any shape
b = np.arange(24).reshape(2, 3, 4)
print((b * 2).shape)  # (2, 3, 4) — scalar broadcasts to all dims
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

(4) 1D + 2D 广播

一维数组与二维数组运算时,1D 数组沿行方向广播。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:行向量加矩阵(难度⭐)

PYTHON
import numpy as np

matrix = np.array([[10, 20, 30],
                   [40, 50, 60],
                   [70, 80, 90],
                   [100, 110, 120]])  # shape (4,3)

row = np.array([1, 2, 3])             # shape (3,)

# row: (3,) → pad → (1,3) → expand → (4,3)
result = matrix + row
print(result)
# [[ 11  22  33]
#  [ 41  52  63]
#  [ 71  82  93]
#  [101 112 123]]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:列向量加矩阵(难度⭐⭐)

列向量广播需要先正确塑造形状——2D 列向量沿列方向扩展。

PYTHON
import numpy as np

matrix = np.array([[10, 20, 30],
                   [40, 50, 60],
                   [70, 80, 90],
                   [100, 110, 120]])  # shape (4,3)

col = np.array([[1],
                [2],
                [3],
                [4]])                  # shape (4,1)

# col: (4,1) → expand axis 1 → (4,3)
result = matrix + col
print(result)
# [[ 11  21  31]
#  [ 42  52  62]
#  [ 73  83  93]
#  [104 114 124]]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。

PYTHON
# Common trick: use newaxis to create column vector from 1D
col2 = np.array([1, 2, 3, 4])[:, np.newaxis]  # (4,) → (4,1)
print(col2.shape)  # (4, 1)
print(np.array_equal(matrix + col, matrix + col2))  # True
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

(5) 2D + 2D 广播

两个 2D 数组可以在不同维度各自广播,产生"外积"效果。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:两个 2D 广播(难度⭐⭐)

PYTHON
import numpy as np

a = np.array([[1],
              [2],
              [3]])    # shape (3,1)

b = np.array([[10, 20, 30, 40]])  # shape (1,4)

# a expands along axis 1: (3,1) → (3,4)
# b expands along axis 0: (1,4) → (3,4)
result = a + b
print(result)
# [[11 21 31 41]
#  [12 22 32 42]
#  [13 23 33 43]]

# This is essentially an outer operation
print(result.shape)  # (3, 4)
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

(6) broadcast_to 与 broadcast_arrays

NumPy 提供了显式广播工具,用于调试或需要中间结果的场景。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:broadcast_to 和 broadcast_arrays(难度⭐⭐)

PYTHON
import numpy as np

# np.broadcast_to: explicitly broadcast to target shape
row = np.array([1, 2, 3])  # shape (3,)
expanded = np.broadcast_to(row, (4, 3))
print(expanded)
# [[1 2 3]
#  [1 2 3]
#  [1 2 3]
#  [1 2 3]]
print(expanded.shape)  # (4, 3)

# Note: broadcast_to returns a read-only view
try:
    expanded[0, 0] = 999  # ValueError — read-only!
except ValueError as e:
    print(f"Error: {e}")
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。

PYTHON
# np.broadcast_arrays: broadcast multiple arrays together
a = np.array([[1], [2], [3]])     # (3,1)
b = np.array([[10, 20, 30, 40]])  # (1,4)

ba, bb = np.broadcast_arrays(a, b)
print(ba.shape)  # (3, 4)
print(bb.shape)  # (3, 4)

print(ba)
# [[1 1 1 1]
#  [2 2 2 2]
#  [3 3 3 3]]
print(bb)
# [[10 20 30 40]
#  [10 20 30 40]
#  [10 20 30 40]]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

(7) 不兼容报错

当两个形状无法通过广播规则对齐时,NumPy 抛出 ValueError

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:不兼容形状报错(难度⭐)

PYTHON
import numpy as np

a = np.ones((3, 4))   # shape (3,4)
b = np.ones((2, 4))   # shape (2,4)

# axis 0: 3 vs 2, neither is 1 → incompatible!
try:
    result = a + b
except ValueError as e:
    print(f"Error: {e}")
    # operands could not be broadcast together with shapes (3,4) (2,4)
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。

PYTHON
# Fix: reshape or slice to make dimensions compatible
a = np.ones((3, 4))
c = np.ones((1, 4))   # (1,4) — axis 0 is 1, can expand
print((a + c).shape)   # (3, 4) — works!

# Another incompatible case
d = np.array([1, 2, 3])   # (3,)
e = np.array([1, 2, 3, 4]) # (4,)
try:
    print(d + e)
except ValueError as e2:
    print(f"Error: {e2}")
    # operands could not be broadcast together with shapes (3,) (4,)
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

兼容判断流程

100%
graph TB
    S["Input shapes A, B"] --> P["Pad shorter shape<br/>with 1s on the left"]
    P --> C{"For each axis:<br/>size_A == size_B<br/>or size_A == 1<br/>or size_B == 1?"}
    C -->|Yes, all axes| OK["Compatible!<br/>Result shape = max per axis"]
    C -->|No, any axis| FAIL["Incompatible!<br/>ValueError"]
    
    style S fill:#4CAF50,color:#fff
    style OK fill:#2196F3,color:#fff
    style FAIL fill:#F44336,color:#fff
    style C fill:#FF9800,color:#fff
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

(8) 常见场景与结果形状

场景 形状 A 形状 B 广播过程 结果形状
标量 + 任意 (3,4) () 标量→(3,4) (3,4)
行向量 + 矩阵 (4,3) (3,) (3,)→(1,3)→(4,3) (4,3)
列向量 + 矩阵 (4,3) (4,1) (4,1)→(4,3) (4,3)
列 + 行向量 (3,1) (1,4) 各自扩展→(3,4) (3,4)
3D + 1D (2,3,4) (4,) (4,)→(1,1,4)→(2,3,4) (2,3,4)
3D + 2D (2,3,4) (3,4) (3,4)→(1,3,4)→(2,3,4) (2,3,4)
3D + 列向量 (2,3,4) (3,1) (3,1)→(1,3,1)→(2,3,4) (2,3,4)

(9) 广播 vs 循环性能

方法 10,000 元素 100,000 元素 1,000,000 元素
Python 循环 ~5 ms ~50 ms ~500 ms
NumPy 广播 ~0.05 ms ~0.2 ms ~2 ms
加速倍数 ~100× ~250× ~250×
⚠️ 注意: 以下代码需在本地 Python 环境中运行。

PYTHON
import numpy as np
import time

size = 100_000
a = np.random.rand(size, 3)
b = np.array([1.0, 2.0, 3.0])

# Method 1: Broadcasting
start = time.perf_counter()
result1 = a + b
t_broadcast = time.perf_counter() - start

# Method 2: Python loop
start = time.perf_counter()
result2 = np.empty_like(a)
for i in range(a.shape[0]):
    result2[i] = a[i] + b
t_loop = time.perf_counter() - start

print(f"Broadcast: {t_broadcast*1000:.2f} ms")
print(f"Loop:      {t_loop*1000:.2f} ms")
print(f"Speedup:   {t_loop/t_broadcast:.0f}x")
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

(10) 综合示例:数据中心化与标准化

场景:Carol 有一份 5 名学生、3 门课的成绩数据,要用广播实现数据中心化(减均值)和标准化(减均值除标准差)。

PYTHON
import numpy as np

# Carol's student scores: 5 students, 3 subjects
scores = np.array([
    [80, 75, 90],
    [65, 88, 72],
    [90, 92, 85],
    [70, 60, 78],
    [85, 80, 88]
])
print(f"Scores shape: {scores.shape}")  # (5, 3)

# Step 1: Center — subtract mean per subject (along axis 0)
mean = scores.mean(axis=0)    # shape (3,)
print(f"Mean per subject: {mean}")  # e.g. [78. 79. 82.6]

# Broadcasting: (5,3) - (3,) → (5,3) - (1,3) → (5,3)
centered = scores - mean
print(f"Centered:\n{centered}")

# Verify: each column mean should be ~0
print(f"Column means after centering: {centered.mean(axis=0)}")
# [~0 ~0 ~0]

# Step 2: Standardize — divide by std per subject
std = scores.std(axis=0)      # shape (3,)
print(f"Std per subject: {std}")

# Broadcasting: (5,3) / (3,) → (5,3)
standardized = (scores - mean) / std
print(f"Standardized:\n{standardized}")

# Verify: each column mean≈0, std≈1
print(f"Means: {standardized.mean(axis=0)}")   # ≈ [0, 0, 0]
print(f"Stds:  {standardized.std(axis=0)}")     # ≈ [1, 1, 1]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。

PYTHON
# Broadcasting makes per-row operations equally easy
# Add a different offset to each row
row_offsets = np.array([5, 10, 15, 20, 25])[:, np.newaxis]  # (5,1)
adjusted = scores + row_offsets  # (5,3) + (5,1) → (5,3)
print(f"Adjusted with row offsets:\n{adjusted}")

# Per-subject scaling (different multiplier per subject)
subject_scale = np.array([1.0, 0.5, 2.0])  # (3,)
scaled = scores * subject_scale  # (5,3) * (3,) → (5,3)
print(f"Scaled by subject:\n{scaled}")
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

❓ 常见问题

Q 广播会复制数据吗?
A 不会。广播通过 stride 技巧让数组"看起来"被扩展,实际内存零增长。但 broadcast_to 返回的是只读视图,不能原地修改。
Q 什么时候会报不兼容错误?
A 当两个数组在某个维度上尺寸不同且都不为 1 时。例如 (3,4) + (2,4) 在轴 0 上 3≠2 且都不为 1,报 ValueError
Q 广播比循环快多少?
A 典型场景下 100~250 倍。原因:① 广播在 C 层执行,无 Python 解释器开销;② 无逐元素函数调用开销;③ CPU 缓存友好。数据量越大加速越明显。
Q 3D 广播怎么理解?
A 和 2D 完全一样的规则。先左侧补 1 对齐维度,再逐轴判断。例如 (2,3,4) + (3,1)(3,1) 补成 (1,3,1) → 扩展成 (2,3,4)。从最右轴往左逐轴检查即可。
Q broadcast_to 和自动广播有什么区别?
A 自动广播发生在算术运算时,隐式执行且结果可写。broadcast_to 是显式函数,可指定目标形状,但返回只读视图。需要可写结果时用 .copy() 复制一份。
Q 列向量和行向量怎么区分?
A 行向量 shape 是 (1,n)(n,)(1D),沿行方向广播。列向量 shape 是 (n,1),沿列方向广播。用 [:, np.newaxis] 可把 1D 转为列向量。

📖 小节


📝 作业

(1) 判断形状兼容性

判断以下形状对是否兼容,若兼容写出结果形状:

  1. (5,3) + (3,) → ?
  2. (4,1) + (1,6) → ?
  3. (2,3,4) + (2,1,4) → ?
  4. (3,) + (4,) → ?
  5. (6,) + (2,3,6) → ?

(2) 每行加不同偏移量

  1. 创建 a = np.arange(12).reshape(4,3)
  2. 创建偏移量 offsets = np.array([100, 200, 300, 400])
  3. 用广播让每行加上对应的偏移量(提示:offsets[:, np.newaxis]
  4. 打印结果,验证第 0 行各元素是否都加了 100

(3) 广播计算距离矩阵

  1. 创建点集 points = np.array([[0,0], [1,0], [0,1], [1,1]])(4 个 2D 点)
  2. 用广播计算 4×4 距离矩阵 D[i,j] = ||points[i] - points[j]||
  3. 提示:points[:, np.newaxis, :] shape (4,1,2)points[np.newaxis, :, :] shape (1,4,2)
  4. 广播后差值 shape (4,4,2),再沿 axis=2 求 norm
  5. 打印距离矩阵,验证 D[0,0] = 0,D[0,3] = √2 ≈ 1.414
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏