
本文介绍两种完全向量化的方法——np.einsum 和广播式 @(matmul)——用于对两个三维数组沿共享内维(如最后一维)执行批量点积运算,避免 Python 循环,显著提升性能。
本文介绍两种完全向量化的方法——`np.einsum` 和广播式 `@`(`matmul`)——用于对两个三维数组沿共享内维(如最后一维)执行批量点积运算,避免 python 循环,显著提升性能。
在科学计算和深度学习中,常需对一批矩阵(如 N × A × B)与另一批向量/矩阵(如 M × C × B)沿共享维度 B 做批量内积,结果期望为 N × M × A(当 C = 1 时可进一步压缩为 N × M × A)。原始循环实现虽直观但效率低下:
import numpy as np
X = np.arange(0, 10 * 2 * 3).reshape(10, 2, 3) # shape: (10, 2, 3)
Y = np.arange(0, 8 * 1 * 3).reshape(8, 1, 3) # shape: (8, 1, 3)
Z_loop = np.empty((10, 8, 2))
for i, x_i in enumerate(X):
for j, y_j in enumerate(Y):
Z_loop[i, j, :] = (x_i @ y_j.T)[:, 0] # x_i: (2,3), y_j.T: (3,1) → (2,1) → squeeze
以下提供两种高性能替代方案:
✅ 方案一:使用 np.einsum(推荐初学者,语义清晰)
einsum 通过爱因斯坦求和约定直接描述张量操作,无需手动调整维度:
Z = np.einsum('ijk,lmk->ilj', X, Y) # shape: (10, 8, 2)
-
'ijk'对应X.shape == (10,2,3)→i=10,j=2,k=3 -
'lmk'对应Y.shape == (8,1,3)→l=8,m=1,k=3(k重复表示求和轴) -
'->ilj'指定输出维度顺序:(i,l,j) = (10,8,2) - 等价于:对每个
(i,l,j),计算Σₖ X[i,j,k] * Y[l,0,k](m固定为 0,自动广播)
✅ 优势:代码简洁、可读性强、维度逻辑显式;对非规则形状鲁棒性高。
✅ 方案二:广播 + @ 运算符(性能极致,适合高频调用)
利用 np.matmul 对 ≥3D 数组的“广播式矩阵乘法”特性(最后两维按 2D 规则相乘,其余维度广播):
Z = (X[:, None, ...] @ Y[None, ..., ::-1].transpose(0, 1, 3, 2))[..., 0] # 更清晰的写法: X_ext = X[:, None, :, :] # (10,1,2,3) Y_ext = Y[None, :, :, :] # (1,8,1,3) → 需转置使内维对齐 Y_reshaped = Y_ext.transpose(0, 1, 3, 2) # → (1,8,3,1) Z = (X_ext @ Y_reshaped)[..., 0] # (10,8,2,1) → squeeze last dim
关键步骤解析:
-
X[:, None, ...]→ 插入新轴,升维为(10, 1, 2, 3) -
Y[None, ...]→ 升维为(1, 8, 1, 3),再.transpose(0,1,3,2)得(1, 8, 3, 1) -
@自动广播前两维:(10,1,2,3) @ (1,8,3,1) → (10,8,2,1) -
[..., 0]去除冗余的单位维,得(10,8,2)
✅ 优势:底层高度优化,小规模数据更快(实测快约 3×);所有 reshape/transposed 均为视图(view),零内存拷贝(O(1) 开销)。
⚠️ 注意事项与选型建议
-
形状兼容性:
@方案要求参与乘法的最后两维满足(a,b) @ (b,c),务必通过transpose或swapaxes对齐;einsum则更灵活。 -
性能权衡:小数组(如示例)
@更快;大数组(如(30,50,60)×(20,1,60))二者接近,einsum可能反超,建议实测。 -
可维护性:团队协作或复杂逻辑中优先选
einsum;核心循环(如训练循环内)可选@并添加注释说明维度变换逻辑。 -
验证正确性:始终用
np.allclose(Z_einsum, Z_matmul)校验结果一致性。
两种方法均彻底摆脱 Python 循环,将全部计算移交至 NumPy 底层 C/Fortran 实现,是生产环境的标准实践。










