NumPy 矩阵乘法:np.dot、matmul 和 @ 完全指南
Updated on
矩阵乘法是机器学习、计算机图形学、信号处理和科学计算的基础。但 NumPy 提供了三种矩阵相乘的方式 -- np.dot()、np.matmul() 和 @ 运算符 -- 它们之间的区别令人困惑。使用错误的方式可能会默默地产生不正确的结果,特别是在处理不同维度的数组或将逐元素乘法(*)与矩阵乘法混淆时。
本指南阐明了每种方法,展示了何时使用哪种,并提供了每种常见线性代数模式的实用示例。
逐元素乘法 vs 矩阵乘法
首先,关键区别。* 运算符执行逐元素乘法,而非矩阵乘法:
import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
# 逐元素乘法(Hadamard 积)
print(A * B)
# [[ 5 12]
# [21 32]]
# 矩阵乘法
print(A @ B)
# [[19 22]
# [43 50]]矩阵乘法遵循规则:C[i,j] = sum(A[i,k] * B[k,j])(对所有 k)。
@ 运算符(推荐)
@ 运算符(在 Python 3.5 中引入)是进行矩阵乘法最简洁、最可读的方式:
import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
C = A @ B
print(C)
# [[19 22]
# [43 50]]Shape 规则
矩阵乘法 A @ B 要求 A 的列数等于 B 的行数:
import numpy as np
# (2, 3) @ (3, 4) = (2, 4)
A = np.ones((2, 3))
B = np.ones((3, 4))
C = A @ B
print(C.shape) # (2, 4)
# (5, 3) @ (3, 2) = (5, 2)
A = np.random.randn(5, 3)
B = np.random.randn(3, 2)
C = A @ B
print(C.shape) # (5, 2)
# 不匹配会引发 ValueError
# np.ones((2, 3)) @ np.ones((4, 2)) # ValueErrornp.matmul()
np.matmul() 与 @ 完全相同。在所有实际用途中它们是等价的:
import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
# 这些产生相同的结果
result1 = A @ B
result2 = np.matmul(A, B)
print(np.array_equal(result1, result2)) # True使用 @ 编写更简洁的代码。当需要将其作为函数参数传递时(例如传给 reduce),使用 np.matmul()。
np.dot()
np.dot() 更早出现,对于高维数组与 @/matmul 的行为不同:
import numpy as np
# 对于一维数组:点积(标量结果)
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print(np.dot(a, b)) # 32 (1*4 + 2*5 + 3*6)
# 对于二维数组:矩阵乘法(与 @ 相同)
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
print(np.dot(A, B))
# [[19 22]
# [43 50]]关键区别:高维
对于超过 2 维的数组,np.dot() 和 @ 行为不同:
import numpy as np
A = np.random.randn(2, 3, 4)
B = np.random.randn(2, 4, 5)
# @ 作为批量矩阵乘法处理:(2, 3, 4) @ (2, 4, 5) = (2, 3, 5)
result_matmul = A @ B
print(result_matmul.shape) # (2, 3, 5)
# np.dot 使用不同的广播:对 A 的最后一个轴和 B 的倒数第二个轴求和
result_dot = np.dot(A, B)
print(result_dot.shape) # (2, 3, 2, 5) -- 形状不同!建议: 矩阵乘法使用 @ 或 np.matmul()。np.dot() 仅用于一维向量的显式点积。
方法比较
| 特性 | @ 运算符 | np.matmul() | np.dot() | * 运算符 |
|---|---|---|---|---|
| 操作 | 矩阵乘法 | 矩阵乘法 | 点积 | 逐元素 |
| 1D x 1D | 点积(标量) | 点积(标量) | 点积(标量) | 逐元素 |
| 2D x 2D | 矩阵乘法 | 矩阵乘法 | 矩阵乘法 | 逐元素 |
| N-D 批量 | 批量 matmul | 批量 matmul | 不同语义 | 逐元素 |
| 可读性 | 最佳 | 良好 | 一般 | N/A |
| 允许标量 | 否 | 否 | 是 | 是 |
| 推荐 | 是 | 是(函数式) | 仅用于一维点积 | 用于 Hadamard |
点积(向量)
两个向量的点积是它们逐元素乘积的和:
import numpy as np
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
# 对一维向量全部等价
print(np.dot(a, b)) # 32
print(a @ b) # 32
print(np.sum(a * b)) # 32应用
import numpy as np
# 余弦相似度
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
vec1 = np.array([1, 2, 3])
vec2 = np.array([4, 5, 6])
print(f"Cosine similarity: {cosine_similarity(vec1, vec2):.4f}")
# 0.9746
# 将 a 投影到 b 上
def project(a, b):
return (np.dot(a, b) / np.dot(b, b)) * b
print(project(np.array([3, 4]), np.array([1, 0])))
# [3. 0.]矩阵-向量乘法
import numpy as np
# 线性变换
A = np.array([[2, 0], [0, 3]]) # 缩放矩阵
v = np.array([1, 1])
result = A @ v
print(result) # [2 3]
# 在神经网络层中应用权重
weights = np.random.randn(10, 5) # 10 个输出,5 个输入
inputs = np.random.randn(5)
output = weights @ inputs
print(output.shape) # (10,)批量矩阵乘法
对于深度学习和批量处理,@ 自动处理批次:
import numpy as np
# 32 个矩阵的批次,每个 4x3,乘以 32 个矩阵的批次,每个 3x5
batch_A = np.random.randn(32, 4, 3)
batch_B = np.random.randn(32, 3, 5)
result = batch_A @ batch_B
print(result.shape) # (32, 4, 5)实用示例
线性方程组
import numpy as np
# 求解 Ax = b
# 2x + 3y = 8
# 4x + y = 10
A = np.array([[2, 3], [4, 1]])
b = np.array([8, 10])
x = np.linalg.solve(A, b)
print(f"x = {x[0]:.2f}, y = {x[1]:.2f}")
# x = 2.20, y = 1.20
# 验证:A @ x 应该等于 b
print(A @ x) # [8. 10.]简单神经网络前向传播
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
# 网络:3 个输入 -> 4 个隐藏 -> 2 个输出
np.random.seed(42)
W1 = np.random.randn(4, 3) * 0.5 # 第一层权重
b1 = np.zeros(4)
W2 = np.random.randn(2, 4) * 0.5 # 第二层权重
b2 = np.zeros(2)
# 前向传播
X = np.array([1.0, 0.5, -1.0]) # 输入
h = sigmoid(W1 @ X + b1) # 隐藏层
y = sigmoid(W2 @ h + b2) # 输出层
print(f"Input: {X}")
print(f"Hidden: {h}")
print(f"Output: {y}")PCA 投影
import numpy as np
# 生成样本数据
np.random.seed(42)
data = np.random.randn(100, 5) # 100 个样本,5 个特征
# 中心化数据
data_centered = data - data.mean(axis=0)
# 使用矩阵乘法计算协方差矩阵
cov = (data_centered.T @ data_centered) / (len(data) - 1)
# 特征值分解
eigenvalues, eigenvectors = np.linalg.eigh(cov)
# 投影到前 2 个主成分
top_2 = eigenvectors[:, -2:] # 最后 2 个(最大特征值)
projected = data_centered @ top_2
print(f"Original shape: {data.shape}")
print(f"Projected shape: {projected.shape}") # (100, 2)可视化矩阵运算
要探索矩阵变换对数据的影响,PyGWalker (opens in a new tab) 让你在 Jupyter 中交互式地可视化投影数据:
import pandas as pd
import pygwalker as pyg
df = pd.DataFrame(projected, columns=['PC1', 'PC2'])
walker = pyg.walk(df)常见问题
np.dot 和 np.matmul 有什么区别?
对于一维和二维数组,它们产生相同的结果。关键区别在于高维数组:np.matmul()(和 @)将它们视为矩阵的批次,而 np.dot() 使用不同的广播规则。新代码建议使用 @ 或 np.matmul()。
NumPy 中的 @ 运算符做什么?
@ 运算符执行矩阵乘法,等同于 np.matmul()。它在 Python 3.5 中引入。对于二维数组,A @ B 计算标准矩阵积。对于高维数组,它执行批量矩阵乘法。
为什么 A * B 和 A @ B 的结果不同?
A * B 执行逐元素乘法(Hadamard 积),将对应元素相乘。A @ B 执行矩阵乘法,遵循规则 C[i,j] = sum(A[i,k] * B[k,j])。这是根本不同的操作。
如何在 NumPy 中计算两个向量的点积?
对于一维数组(向量),使用 np.dot(a, b)、a @ b 或 np.sum(a * b)。三者都返回相同的标量结果:逐元素乘积之和。
矩阵乘法需要什么形状?
对于 A @ B,A 的最后一个维度必须等于 B 的倒数第二个维度。对于二维:(m, n) @ (n, p) = (m, p)。A 的列数必须等于 B 的行数。
总结
在 NumPy 中进行矩阵乘法,默认使用 @ 运算符 -- 它最具可读性,并能正确处理批量操作。np.dot() 仅用于一维向量的显式点积。永远不要将 *(逐元素)与 @(矩阵乘法)混淆。记住 shape 规则:(m, n) @ (n, p) = (m, p),你就能避免最常见的矩阵乘法错误。