☰
NumPy 线性代数的具体实现
2026/10/8 9:43:21 网站建设 项目流程

前言


先明确一个前提: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,)

rankint矩阵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少一层歧义。


常见坑点



  1. ❌ 用np.linalg.inv(A) @ b解方程组,认为它和solve等价。


✅ 直接np.linalg.solve(A, b);显式求逆会放大误差、代价更高,且接近奇异时得到的错误结果不会报错。



  1. ❌ 把A * B当成矩阵乘法。


✅*是逐元素相乘;矩阵乘法用@或np.matmul。



  1. ❌ 认为numpy.dot和numpy.matmul完全等价,在高维数组上随便换着用。


✅ 二维时结果相同,高维时dot按轴求和、matmul按矩阵栈广播;做矩阵乘法优先用@。



  1. ❌ 调用np.linalg.lstsq只接一个返回值。


✅ 它返回(x, residuals, rank, s)四元组;residuals在b为一维时形状是(1,),秩不足时是空数组。



  1. ❌ 以为lstsq的rcond默认值永远是-1。


✅ 从 NumPy 2.0 起默认是None(用机器精度乘以max(M, N)),显式传-1才是机器精度;跨版本时显式写出更稳。



  1. ❌ 用svd返回的Vh时再对它取一次转置。


✅ 第三个返回值已经是共轭转置后的Vh,重构应写U @ np.diag(S) @ Vh。



  1. ❌ 对实对称矩阵用np.linalg.eig,并假设特征值按升序排列。


✅ 对称/厄米矩阵用eigh;且无论用哪个,都不要假设特征值的顺序。



  1. ❌ 看到solve报了LinAlgError就去求伪逆硬算。


✅ 报错说明a奇异或非方阵,先检查矩阵秩与条件数;确实需要最小二乘解时用lstsq。


总结




需求该用不该用



矩阵乘法@/np.matmul*(那是逐元素乘)

解a @ x = b(方阵满秩)np.linalg.solveinv再相乘

非方阵 / 奇异矩阵求近似解np.linalg.lstsqsolve(会直接报错)

真的需要逆矩阵本身np.linalg.inv/pinv仅为解方程而求逆

对称矩阵特征值np.linalg.eighnp.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;本文代码未在本机运行,仅作人工推演。





需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询