Python实战:用NumPy玩转矩阵乘法、转置与逆矩阵(附代码示例)

最近在帮几个刚入行数据科学的朋友看代码,发现一个挺有意思的现象:很多人对线性代数的概念背得滚瓜烂熟,什么特征值、奇异值分解张口就来,但一到实际写Python代码处理矩阵运算时,却常常在最基础的矩阵乘法上栽跟头。要么是维度对不上,要么是用了错误的函数导致性能瓶颈,更别提高效地求逆矩阵了。这让我意识到,理论知识和工程实践之间,其实隔着一层很薄的窗户纸,捅破了就豁然开朗。

NumPy作为Python科学计算的基石,其矩阵运算能力强大到令人惊叹,但真正能把它“玩转”的人并不多。今天,我们就抛开枯燥的公式推导,直接从代码和实战场景出发,聊聊如何用NumPy优雅、高效且不出错地完成矩阵乘法、转置和求逆这些核心操作。无论你是正在啃《线性代数及其应用》的学生,还是需要快速处理数据的分析师,这篇文章都能给你带来即学即用的技巧和避坑指南。

1. 环境准备与NumPy矩阵基础

在开始任何矩阵运算之前,确保你的工作环境已经就绪是第一步。我强烈建议使用Anaconda来管理Python环境,它能帮你省去大量配置依赖的麻烦。

# 如果你还没有安装NumPy,可以通过pip安装
pip install numpy

# 或者使用conda(推荐,尤其对于科学计算栈)
conda install numpy

安装完成后,在Python脚本或Jupyter Notebook中导入NumPy,并习惯性地给它起个别名np,这几乎是业内的标准做法。

import numpy as np
print(f"NumPy版本: {np.__version__}")

接下来,我们需要理解NumPy中表示矩阵的两种主要数据结构:多维数组(ndarray)矩阵对象(matrix)。虽然matrix类型在某些情况下写法更接近数学公式,但ndarray才是NumPy的绝对核心,功能更全面,社区支持也更好。因此,本文所有示例都将基于ndarray

创建一个矩阵(或者说二维数组)非常简单:

# 创建一个3x2的矩阵
A = np.array([[1, 2],
              [3, 4],
              [5, 6]])
print("矩阵A:")
print(A)
print(f"A的形状: {A.shape}")  # 输出 (3, 2)
print(f"A的数据类型: {A.dtype}") # 通常是 int64 或 float64

注意:在进行矩阵运算,特别是求逆时,确保矩阵元素的数据类型是浮点数(如float64)通常更安全,可以避免整数除法带来的意外结果。你可以使用A = A.astype(np.float64)进行转换,或者在创建时直接指定dtype=np.float64

一个新手常犯的错误是混淆了“数组”和“矩阵”的概念。在NumPy中,即使我们称二维ndarray为矩阵,它的运算规则(尤其是乘法)也与数学中的矩阵乘法定义严格对应,这需要我们使用特定的函数或运算符。

2. 深入理解与实战矩阵乘法

矩阵乘法是线性代数的灵魂操作,也是深度学习、图形学等领域的计算核心。在NumPy中,实现矩阵乘法有几种方式,它们看似相似,实则各有玄机。

2.1 三种乘法方式辨析

首先,我们创建两个示例矩阵:

A = np.array([[1, 2],
              [3, 4]])  # 2x2
B = np.array([[5, 6],
              [7, 8]])  # 2x2
C = np.array([[1, 2, 3],
              [4, 5, 6]]) # 2x3

1. np.dot(a, b)a.dot(b) 这是最经典的矩阵乘法函数,执行的是标准的数学矩阵乘法。它要求a的列数等于b的行数。

result_dot = np.dot(A, B)
# 等价于 A.dot(B)
print("np.dot(A, B):")
print(result_dot)
# 计算过程:(1*5+2*7, 1*6+2*8; 3*5+4*7, 3*6+4*8) = [[19, 22], [43, 50]]

2. @ 运算符 (Python 3.5+) 这是我最推荐在日常代码中使用的方式,它语法简洁,意图明确,就是执行矩阵乘法。

result_at = A @ B
print("A @ B:")
print(result_at)  # 结果与 np.dot(A, B) 完全相同

3. np.matmul(a, b) 功能与np.dot在二维数组上几乎一致,但在处理高维数组(批次矩阵乘法)时行为更直观、可预测,是深度学习框架(如TensorFlow, PyTorch)中matmul操作的原型。

result_matmul = np.matmul(A, B)
print("np.matmul(A, B):")
print(result_matmul)

那么,*运算符呢?千万注意:在ndarray上使用*执行的是逐元素乘法(Element-wise Multiplication),而不是矩阵乘法!

result_elementwise = A * B
print("A * B (逐元素乘法):")
print(result_elementwise)  # 输出 [[5, 12], [21, 32]]

为了更清晰地对比,我们用一个表格总结:

操作符/函数 含义 输入要求 主要用途
* 逐元素乘法 形状必须完全相同 图像处理、数值缩放
np.dot() / .dot() 矩阵点积/乘法 a的末轴与b的倒数第二轴维度相同 通用矩阵乘法,兼容旧代码
@ 矩阵乘法 np.dot 现代代码首选,清晰直观
np.matmul() 矩阵乘法 np.dot,高维数组行为更优 批次矩阵运算,深度学习

2.2 常见维度错误与调试技巧

矩阵乘法出错,十有八九是维度不匹配。假设我们有一个2x3的矩阵C,想与2x2的矩阵A相乘。

try:
    error_result = A @ C  # A是2x2, C是2x3
except ValueError as e:
    print(f"错误信息: {e}")  # 会报错:shapes (2,2) and (2,3) not aligned

错误在于A的列数(2)不等于C的行数(2)。矩阵乘法要求第一个矩阵的列数等于第二个矩阵的行数。正确的做法是让AC的转置相乘,或者检查你的计算图逻辑。

一个实用的调试技巧是,在乘法前打印矩阵形状:

print(f"A.shape: {A.shape}, C.shape: {C.shape}")
# 如果想让 A 和 C 能相乘,需要满足 A.shape[1] == C.shape[0]
# 这里 2 != 2,所以不能直接 A @ C,但可以 A @ C.T (C的转置,变成3x2)
if A.shape[1] == C.shape[0]:
    result = A @ C
else:
    print("维度不匹配!请检查矩阵顺序或是否需要转置。")
    # 也许你需要的是 C @ A?
    if C.shape[1] == A.shape[0]:
        result = C @ A
        print("使用 C @ A 计算成功。")

2.3 矩阵的幂运算

矩阵的幂,即矩阵连乘自身,在马尔可夫链、图论邻接矩阵等场景中非常有用。NumPy没有直接提供矩阵幂运算符,但我们可以利用np.linalg.matrix_power函数。

# 计算矩阵A的3次幂:A^3 = A @ A @ A
A = np.array([[1, 2], [3, 4]])
A_cubed = np.linalg.matrix_power(A, 3)
print("A^3:")
print(A_cubed)

# 验证一下:手动连乘
manual_cubed = A @ A @ A
print("手动 A @ A @ A:")
print(manual_cubed)
print("结果是否一致?", np.allclose(A_cubed, manual_cubed))

提示:np.allclose()是判断两个浮点数矩阵是否“近似相等”的好方法,能避免浮点数精度误差导致的误判。

对于零次幂,NumPy将其定义为同尺寸的单位矩阵:

A_to_0 = np.linalg.matrix_power(A, 0)
print("A^0 (单位矩阵):")
print(A_to_0)

3. 矩阵转置:不仅仅是行列互换

转置操作将矩阵的行列互换,在公式推导、求解方程组和数据处理中无处不在。NumPy中实现转置简单到令人发指,但背后的细节值得深究。

3.1 基础转置操作

使用.T属性是最直接的方法:

D = np.array([[1, 2, 3],
              [4, 5, 6]]) # 2x3
D_transpose = D.T
print("原矩阵 D (2x3):")
print(D)
print("转置后 D.T (3x2):")
print(D_transpose)

对于一维数组,转置操作是无效的,因为它仍然是一维的。这常常让初学者困惑。

vec = np.array([1, 2, 3])
print(f"一维数组 vec: {vec}, shape: {vec.shape}")
print(f"vec.T 仍然是: {vec.T}, shape: {vec.T.shape}") # 形状不变

如果需要对一维数组进行“转置”以参与矩阵乘法,通常需要将其重塑为二维的行向量或列向量:

# 重塑为行向量 (1, n)
row_vec = vec.reshape(1, -1)  # -1表示自动推断
# 重塑为列向量 (n, 1)
col_vec = vec.reshape(-1, 1)
print(f"行向量: {row_vec}, shape: {row_vec.shape}")
print(f"列向量: {col_vec}, shape: {col_vec.shape}")

3.2 np.transpose() 与高维数组转置

.Tnp.transpose()的简便写法。对于二维矩阵,两者完全等价。但np.transpose()的强大之处在于处理**高维数组(张量)**时,可以指定任意复杂的轴变换顺序。

假设我们有一个3维张量,代表一批图像(批量大小,高度,宽度):

tensor = np.random.randn(10, 64, 64)  # 10张64x64的图片
# 将轴顺序从 (批量, 高, 宽) 转换为 (高, 宽, 批量)
tensor_transposed = np.transpose(tensor, (1, 2, 0))
print(f"转置前形状: {tensor.shape}")
print(f"转置后形状: {tensor_transposed.shape}")

3.3 转置在矩阵方程中的应用

转置的一个关键应用是求解最小二乘问题。例如,对于超定方程组 $Ax = b$(方程数多于未知数),通常无精确解。我们可以通过求解 $A^T A x = A^T b$ 来得到最优的近似解(最小二乘解)。

# 模拟一个超定方程组:3个方程,2个未知数
A_over = np.array([[1, 1],
                   [1, 2],
                   [1, 3]])
b_over = np.array([3, 5, 7])

# 使用正规方程 (Normal Equation) 求解最小二乘解
# x = (A^T A)^{-1} A^T b
ATA = A_over.T @ A_over
ATb = A_over.T @ b_over
# 这里先不求逆,下一节会详细讲
print("A^T A:")
print(ATA)
print("A^T b:")
print(ATb)

4. 逆矩阵:存在性、求解与数值稳定性

逆矩阵是矩阵理论中一个既强大又“脆弱”的概念。说它强大,是因为它能直接用于求解线性方程组;说它脆弱,是因为并非所有矩阵都可逆,且数值计算中求逆容易引入不稳定因素。

4.1 判断矩阵是否可逆

一个矩阵可逆的首要条件是它为方阵(行数等于列数)。其次,其行列式不能为零。

def is_invertible(M):
    """一个简单的可逆性检查函数(仅适用于小矩阵)"""
    if M.shape[0] != M.shape[1]:
        print("矩阵不是方阵,不可逆。")
        return False
    det = np.linalg.det(M)
    print(f"矩阵行列式: {det:.6f}")
    # 由于浮点误差,判断是否接近0
    if np.abs(det) < 1e-10:
        print("行列式接近0,矩阵可能奇异(不可逆或病态)。")
        return False
    return True

M1 = np.array([[4, 7],
               [2, 6]])
M2 = np.array([[1, 2],
               [2, 4]]) # 第二行是第一行的两倍,行列式为0,不可逆

print("检查 M1:")
print(is_invertible(M1))
print("\n检查 M2:")
print(is_invertible(M2))

4.2 使用np.linalg.inv()求逆

对于可逆方阵,求逆直接使用np.linalg.inv()

M = np.array([[4, 7],
              [2, 6]], dtype=np.float64) # 明确使用浮点类型

if is_invertible(M):
    M_inv = np.linalg.inv(M)
    print("矩阵 M:")
    print(M)
    print("逆矩阵 M_inv:")
    print(M_inv)
    
    # 验证:M * M_inv 应近似于单位矩阵
    I = M @ M_inv
    print("M * M_inv (应接近单位矩阵):")
    print(I)
    print("是否接近单位矩阵?", np.allclose(I, np.eye(2)))

4.3 求解线性方程组:更优的选择

在实际应用中,直接求逆矩阵来解方程 Ax = b 通常是下策(计算量大且数值不稳定)。NumPy提供了更专业、更稳定的求解器np.linalg.solve

A = np.array([[3, 1],
              [1, 2]], dtype=np.float64)
b = np.array([9, 8], dtype=np.float64)

# 方法一:低效且不稳定的求逆法 (不推荐)
x_bad = np.linalg.inv(A) @ b

# 方法二:专用求解器 (强烈推荐)
x_good = np.linalg.solve(A, b)

print("方程: 3x + y = 9; x + 2y = 8")
print(f"求逆法解: {x_bad}")
print(f"专用求解器解: {x_good}")
print("两者是否接近?", np.allclose(x_bad, x_good))

np.linalg.solve内部会使用LU分解、Cholesky分解等数值稳定的算法,速度更快,精度更高。对于大型稀疏矩阵,还有scipy.sparse.linalg.spsolve等更高效的专用函数。

4.4 处理不可逆或病态矩阵:伪逆

当矩阵不可逆(奇异)或条件数很大(病态)时,np.linalg.inv会抛出LinAlgError。此时,摩尔-彭罗斯伪逆(Moore-Penrose Pseudoinverse) np.linalg.pinv 是一个有用的工具。它能对任意矩阵(包括非方阵)给出一个“广义逆”,在最小二乘问题中尤其有用。

# 回到之前超定方程组 A_over * x = b_over 的例子
A_over = np.array([[1, 1],
                   [1, 2],
                   [1, 3]])
b_over = np.array([3, 5, 7])

# A_over不是方阵,无法求逆。使用伪逆求最小二乘解。
A_pinv = np.linalg.pinv(A_over)  # 计算伪逆
x_least_squares = A_pinv @ b_over

print("超定方程组 A*x = b")
print("A:")
print(A_over)
print("b:", b_over)
print("\n使用伪逆求得的最小二乘解 x:")
print(x_least_squares)

# 计算残差,看看拟合效果
residual = b_over - A_over @ x_least_squares
print("残差 (b - A*x):", residual)
print("残差平方和:", np.sum(residual**2))

伪逆基于奇异值分解(SVD),能优雅地处理秩亏矩阵,是许多机器学习算法(如线性回归)背后的数学基础。

5. 综合实战:一个简单的线性回归案例

让我们把矩阵乘法、转置和逆矩阵的知识串联起来,实现一个从零开始的多元线性回归。假设我们有一组房屋数据,特征包括面积和房间数,目标是预测房价。

import numpy as np
# 设置随机种子以便复现结果
np.random.seed(42)

# 生成模拟数据
num_samples = 100
# 特征:面积(平米)和房间数
X = np.random.randn(num_samples, 2)
X[:, 0] = X[:, 0] * 50 + 100  # 面积 ~ N(100, 50)
X[:, 1] = np.random.randint(1, 6, num_samples) # 房间数 1-5
# 真实参数:面积权重=2000,房间数权重=50000,截距=100000
true_weights = np.array([2000, 50000])
true_intercept = 100000
# 生成目标值(房价),加入一些噪声
y = X @ true_weights + true_intercept + np.random.randn(num_samples) * 30000

print("数据形状:")
print(f"特征矩阵 X: {X.shape}")
print(f"目标向量 y: {y.shape}")

为了使用矩阵公式求解线性回归参数 $\theta = (X^T X)^{-1} X^T y$,我们需要给特征矩阵添加一列1(对应截距项)。

# 在X前添加一列1,用于拟合截距项
X_b = np.c_[np.ones((num_samples, 1)), X]  # 现在X_b的形状是 (100, 3)
print("添加截距项后的设计矩阵 X_b 形状:", X_b.shape)

现在,应用正规方程求解:

# 计算 theta = (X_b^T * X_b)^(-1) * X_b^T * y
# 分步计算以清晰展示过程
XTX = X_b.T @ X_b  # 矩阵乘法
XTX_inv = np.linalg.inv(XTX) # 求逆
XTy = X_b.T @ y    # 矩阵乘法
theta = XTX_inv @ XTy # 矩阵乘法

print("通过正规方程求解的回归参数 theta [截距, 面积权重, 房间数权重]:")
print(theta)
print("\n真实参数 [截距, 面积权重, 房间数权重]:")
print([true_intercept, true_weights[0], true_weights[1]])

可以看到,我们通过一系列矩阵运算,成功估计出了接近真实值的模型参数。这个例子完美展示了矩阵乘法、转置和逆矩阵如何协同工作,解决一个实际的机器学习问题。

最后,记得对于大规模数据,直接求逆 (X^T X)^{-1} 计算成本很高且可能数值不稳定。在实际的机器学习库(如scikit-learn)中,会使用更高级的数值方法(如Cholesky分解或梯度下降)来求解。但理解其背后的矩阵原理,无疑能让你更自信地使用这些工具。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐