前言
先明确一个前提:NumPy 不是 Python 标准库,它是第三方库,使用前需要pip install numpy。本机没有 Python 解释器,也没有安装 NumPy,所以本文的示例无法在本机运行验证,只能逐行人工推演;所有函数名、参数表和返回结构都以 NumPy 官方文档为准,请你以自己的环境为准。另外,NumPy 里的「矩阵」指的是二维的numpy.array对象,官方已经不再推荐使用它的numpy.matrix子类,即使在纯线性代数场景下也一样。
第二个要纠正的误解是「线性代数函数都在numpy.linalg里」。不是:矩阵乘法dot、matmul、inner、outer、kron都是顶层的numpy函数(numpy.dot、numpy.matmul),不是numpy.linalg的成员;而求解、分解、特征值、行列式、秩、条件数这些,才是numpy.linalg的强项。把这两类混为一谈,是查文档时找不到函数的主要原因。
第三个误解是「求逆就能解方程」。数学上确实可以写成x = A⁻¹b,但数值计算上不该这么做:先求逆再相乘既慢又不稳,正确做法是用numpy.linalg.solve。这一点会在第三节展开,它是本文最实用的一条。
本文按「乘法 → 解方程 → 分解 → 矩阵特征量」的顺序,讲清numpy.linalg里各个函数在做什么、什么时候用哪个。
一、dot/matmul/@三者的关系
先讲怎么乘。官方文档说得很明确:@运算符从 NumPy 1.10.0 引入,在计算二维数组之间的矩阵乘法时,它优于其他写法;numpy.matmul就是@的实现。也就是说A @ B与np.matmul(A, B)是一回事。
numpy.dot也能做矩阵乘法,但它和matmul并不是同义词,差别在高维和标量上:
| 写法 | 二维数组 | 高维数组 | 标量参与 |
|---|
A @ B/np.matmul(A, B) | 矩阵乘法 | 按「矩阵栈」处理并广播 | 不允许标量 |
np.dot(A, B) | 矩阵乘法 | 对A的最后一轴与B的倒数第二轴求和 | 允许,退化为数乘 |
所以一条实用规则是:做矩阵乘法就用@,语义最清楚,也不会在高维时踩到dot那种「按轴求和」的隐含行为。dot留给你明确需要它那套广义语义的地方。
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
A = np.array([[1., 2.], [3., 4.]])
B = np.array([[5., 6.], [7., 8.]])
print(A @ B) # 矩阵乘法
print(np.matmul(A, B)) # 与上一行完全等价
print(A * B) # 逐元素相乘,不是矩阵乘法最后一行要单独强调:*是逐元素相乘(element-wise),不是矩阵乘法。把*当成矩阵乘法,是初学阶段最典型的一类 bug。
顺带说一句 Python 2 的事:Python 2.7 已于 2020 年 1 月 1 日停止维护,新版 NumPy 也早已不支持 Python 2。网上老代码里的print语句、xrange、numpy.matrix的种种写法,在 Python 3 的新版本里要么语法错误,要么已不推荐,不要照抄。
二、解线性方程组:solve与lstsq
签名是numpy.linalg.solve(a, b),解的是a @ x = b。官方文档给出的若干约束值得逐条记下:
a必须是方阵且满秩(各行/各列线性无关),否则抛LinAlgError;- 它内部走 LAPACK 的
_gesv例程; - 广播规则生效,
a可以是「堆叠」的多个方阵——这也是 NumPy 相对 SciPy 的一个优势,numpy.linalg.solve能一次处理一批矩阵。
还有一条版本差异要注意:从 NumPy 2.0 起,b只有恰好是一维(ndim == 1)时才被当作形状(M,)的列向量,其他情况一律按(M, K)的矩阵栈处理;在此之前的版本,若b.ndim等于a.ndim - 1就会按向量栈处理。跨版本写代码时这一点要留意。
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
A = np.array([[3., 1.], [1., 2.]])
b = np.array([9., 8.])
x = np.linalg.solve(A, b)
print(x) # 方程组的解
print(np.allclose(A @ x, b)) # True,验证一下如果a不是方阵,或者虽然是方阵但奇异(不满秩),solve会直接报错——这时候不该去凑一个逆,而应该换lstsq。
numpy.linalg.lstsq(a, b, rcond=None)求的是最小二乘解,方程可以是不定的、恰定的或超定的。如果a是方阵且满秩,返回的解在舍入误差范围内就是精确解;否则它最小化||b - a @ x||的欧氏二范数;如果存在多个最小化解,返回其中二范数最小的那个。
它的返回值是一个四元组,这一点经常被记错:
| 返回项 | 形状 | 含义 |
|---|
x | (N,)或(N, K) | 最小二乘解;b是二维时解按列排 |
residuals | (1,)、(K,)或(0,) | 每列残差的平方和;b是一维时形状为(1,) |
rank | int | 矩阵a的秩 |
s | (min(M, N),) | a的奇异值 |
rcond是奇异值的截断比例,小于「rcond乘以最大奇异值」的奇异值在判定秩时被当作零。它的默认值在 NumPy 2.0 变了:以前默认是-1(并用一次警告提示即将改变),现在是None,表示使用「机器精度乘以max(M, N)」;显式传-1则是使用机器精度。这个改动会实实在在影响秩判定和返回的解,所以显式写上rcond=None反而更稳。
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
A = np.array([[0., 1.], [1., 1.], [2., 1.]])
b = np.array([1., 2., 3.])
x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None)
print(x.shape) # (2,)
print(residuals.shape) # (1,)
print(rank) # 2三、为什么「能用solve就不要用inv再相乘」
这是本文最值得记住的一条。
先说清楚两种写法在数学上等价:
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
A = np.array([[3., 1.], [1., 2.]])
b = np.array([9., 8.])
x1 = np.linalg.solve(A, b) # 推荐
x2 = np.linalg.inv(A) @ b # 不推荐,数值行为更差它们的结果在理想数学世界里一样(在A可逆时),但计算机用的是有限精度浮点,两条路径的误差传播完全不同。
原因有两层。第一层是步骤:solve内部走的是分解后求解的路径(LAPACK 的_gesv会做带主元的分解再回代),一步到位;而「先inv再相乘」相当于先完整构造出A⁻¹的每一个元素,再做一次矩阵乘——显式求逆这个动作本身就会放大误差。第二层是代价:显式求逆要做一次完整的矩阵分解加一次求逆,之后还要再做一次乘法;直接解只做一次分解加一次回代。矩阵越大,差距越明显。
还有一个容易被忽略的点:如果A接近奇异,显式求逆的结果可能完全没有意义——你会得到一组看起来很正常的数字,但它们和真解差得很远,而且没有任何报错。相比之下solve对真正奇异的输入会抛LinAlgError,至少让你知道出事了。
那numpy.linalg.inv(a)什么时候用?——当你真的需要那个逆矩阵本身的时候(比如后续要反复用它做别的计算、或者要把它交给另一个库)。仅仅为了解一次方程组而求逆,是典型的用错工具。
numpy.linalg.pinv(a, rcond=..., hermitian=...)是伪逆(Moore–Penrose 广义逆),用于非方阵或奇异的矩阵。它的签名里除了rcond还有hermitian,以及较新版本加入的rtol。具体参数的默认值和版本差异请以官方文档为准,因为rcond/rtol的默认值在近几个版本调整过。
四、分解与特征值:det/eig/svd/qr
这一组是「把矩阵拆开」的工具:
| 函数 | 作用 | 返回值要点 |
|---|
numpy.linalg.det(a) | 行列式 | 返回标量 |
numpy.linalg.slogdet(a) | 符号与对数行列式 | 返回(sign, logabsdet) |
numpy.linalg.eig(a) | 特征值与右特征向量(一般方阵) | 返回(w, v) |
numpy.linalg.eigh(a[, UPLO]) | 对称/厄米矩阵的特征值 | 返回(w, v),只用下三角或上三角 |
numpy.linalg.svd(a[, full_matrices, compute_uv, hermitian]) | 奇异值分解 | 默认返回(U, S, Vh) |
numpy.linalg.qr(a[, mode]) | QR 分解 | 默认mode='reduced',返回(Q, R) |
numpy.linalg.cholesky(a, /, *[, upper]) | Cholesky 分解 | 要求对称正定 |
几个容易记错的地方:
svd返回的第三个是Vh,不是V。它已经转置(准确说是共轭转置)过了,所以重构矩阵时应写U @ np.diag(S) @ Vh,而不是再对Vh取转置。这一条错了通常不会报错,只会得到一个形状对不上的结果或错误的重构。
full_matrices决定U和Vh的形状。默认True时返回完整的方阵;设成False时返回「经济型」分解,U为(M, K)、Vh为(K, N),其中K = min(M, N)。大矩阵上通常用False省内存。
eig与eigh不能混用。一般矩阵用eig;实对称或复厄米矩阵用eigh,后者会利用对称性、更快也更稳,并且返回的特征值默认是升序的。eigh还有UPLO参数决定读下三角('L',默认)还是上三角('U')——只读一半意味着另一半被当成镜像,输入不对称时结果会出乎意料。
特征值的顺序不保证。eig返回的特征值没有规定顺序,不要假设「第一个是最大的」。
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
A = np.array([[0., 1.], [1., 1.], [2., 1.]])
U, S, Vh = np.linalg.svd(A, full_matrices=False)
print(U.shape, S.shape, Vh.shape) # (3, 2) (2,) (2,)
Q, R = np.linalg.qr(A)
print(Q.shape, R.shape) # (3, 2) (2,)
D = np.array([[2., 0.], [0., 3.]])
w, v = np.linalg.eig(D)
print(sorted(w)) # 特征值为 2 和 3,顺序不保证,所以先排序五、矩阵秩与条件数
这两个量回答了「这个方程组好不好解」的问题,做数值计算时经常要先用它们探路。
numpy.linalg.matrix_rank(A[, tol, hermitian, rtol])用 SVD 求秩。为什么不用「数非零奇异值」这种朴素办法?因为浮点误差会让本该为零的奇异值变成1e-16这种小量,必须有一个容差。tol就是这个阈值,不传时由最大奇异值和矩阵维度推算得出。
numpy.linalg.cond(x[, p])求条件数。它衡量的是「输入的微小扰动会被放大多少倍」:条件数大,说明矩阵接近奇异,解对输入极其敏感——b上一点点测量误差,可能让x面目全非。看到大的条件数,就该对结果保持怀疑,而不是照单全收。
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
A = np.array([[1., 2.], [2., 4.]]) # 两行线性相关,矩阵奇异
print(np.linalg.matrix_rank(A)) # 1
print(np.linalg.cond(A)) # 很大的数(1 除以 0 的极限情形是 inf)顺便区分一下范数:numpy.linalg.norm(x[, ord, axis, keepdims])同时管向量范数和矩阵范数,靠ord和axis决定语义。如果你只想明确地要向量范数或矩阵范数,官方提供了numpy.linalg.vector_norm和numpy.linalg.matrix_norm这两个更专一的函数,语义比norm少一层歧义。
常见坑点
- ❌ 用
np.linalg.inv(A) @ b解方程组,认为它和solve等价。
✅ 直接np.linalg.solve(A, b);显式求逆会放大误差、代价更高,且接近奇异时得到的错误结果不会报错。
- ❌ 把
A * B当成矩阵乘法。
✅*是逐元素相乘;矩阵乘法用@或np.matmul。
- ❌ 认为
numpy.dot和numpy.matmul完全等价,在高维数组上随便换着用。
✅ 二维时结果相同,高维时dot按轴求和、matmul按矩阵栈广播;做矩阵乘法优先用@。
- ❌ 调用
np.linalg.lstsq只接一个返回值。
✅ 它返回(x, residuals, rank, s)四元组;residuals在b为一维时形状是(1,),秩不足时是空数组。
- ❌ 以为
lstsq的rcond默认值永远是-1。
✅ 从 NumPy 2.0 起默认是None(用机器精度乘以max(M, N)),显式传-1才是机器精度;跨版本时显式写出更稳。
- ❌ 用
svd返回的Vh时再对它取一次转置。
✅ 第三个返回值已经是共轭转置后的Vh,重构应写U @ np.diag(S) @ Vh。
- ❌ 对实对称矩阵用
np.linalg.eig,并假设特征值按升序排列。
✅ 对称/厄米矩阵用eigh;且无论用哪个,都不要假设特征值的顺序。
- ❌ 看到
solve报了LinAlgError就去求伪逆硬算。
✅ 报错说明a奇异或非方阵,先检查矩阵秩与条件数;确实需要最小二乘解时用lstsq。
总结
| 需求 | 该用 | 不该用 |
|---|
| 矩阵乘法 | @/np.matmul | *(那是逐元素乘) |
解a @ x = b(方阵满秩) | np.linalg.solve | inv再相乘 |
| 非方阵 / 奇异矩阵求近似解 | np.linalg.lstsq | solve(会直接报错) |
| 真的需要逆矩阵本身 | np.linalg.inv/pinv | 仅为解方程而求逆 |
| 对称矩阵特征值 | np.linalg.eigh | np.linalg.eig |
| 奇异值分解 | np.linalg.svd(注意Vh) | 忘记第三个返回值已转置 |
| 判断可解性 | matrix_rank+cond | 直接算完就用 |
把numpy.linalg用对,核心不在于记住多少个函数名,而在于两件事:分清哪些是顶层的numpy函数、哪些属于numpy.linalg;以及在数值路径上选对工具——解方程用solve、对称矩阵用eigh、可解性先看秩与条件数。做到这两点,绝大多数「结果莫名其妙」的线性代数问题都会消失。
参考:numpy.linalg、numpy.matmul、numpy.linalg.solve、numpy.linalg.lstsq等的签名与版本说明以 NumPy 官方文档为准;NumPy 为第三方库,需pip install numpy;本文代码未在本机运行,仅作人工推演。