我最早接触"线性表示"是在一本讲线性代数的教材里,当时只觉得这就是一个"把一个向量拆成几个向量的倍数相加"的简单操作,没太当回事。直到后来写机器学习的代码,真刀真枪去处理数据降维、线性回归、矩阵分解这些实际问题时,才意识到线性表示几乎是无处不在的地基。它不只是课本上的一个名词,而是所有线性模型的底层语言。这篇内容,我按照系列教程"第二章"的定位来写,假定你已经知道向量是什么、矩阵怎么乘,但还没建立起"表示"这个视角。读完你会发现,判断一个向量能否被另一组向量表示,本质就是在解一个方程组,而这个判断在后端的特征工程、数值计算、模型可解释性里都扮演着关键角色。
1. 从一个朴素问题说起:为什么需要"线性表示"这个概念
1.1 三个场景告诉你"表示"到底在干嘛
先放下抽象定义,我们看三个具体场景。
第一个场景是物理里的力的合成与分解。你在斜面上放一个物块,重力可以分解成沿斜面向下的分量和垂直于斜面的分量。反过来想,斜面方向的力、垂直方向的力,通过不同的倍数相加,就能"拼"出一个重力。这个过程在数学上就是:目标向量(重力)等于两个方向向量(沿斜面、垂直斜面)各自乘以某个系数后相加。
第二个场景是推荐系统里的用户画像。假设一个用户被表示成一个向量,特征维度是"运动偏好""影音偏好""阅读偏好"。如果三个基向量分别是"最爱运动的人""最爱影音的人""最爱读书的人"的典型画像,那么任意一个用户画像,都可以写成这三个向量的线性组合。系数就表示这个用户跟"典型运动爱好者"有多接近、跟"典型影音爱好者"有多接近。这就是一种用线性表示做语义解释的思路。
第三个场景是计算机图形学里的坐标变换。你在建模软件里看到的物体顶点坐标,都是在世界坐标系下表示的。但你渲染的时候需要把它们转换到相机坐标系、再转换到屏幕坐标系。每一次转换,本质都是把同一个点在新坐标系的一组基向量下重新做线性表示,坐标值就是那组新的系数。
这三个场景背后有一个共同的思维模式:给你一组"基底向量",你试图把任意一个目标向量表达成它们的加权和。 这个"加权和"在数学上就是线性组合,而"能被表达出来"这件事,就叫线性表示。学这一章的关键不是背定义,而是建立一种条件反射:看到一个向量,先问自己——它能不能由我手头这几个向量拼出来?如果能,系数是什么?这个问题的答案,直接决定了后续很多算法能不能跑通。
1.2 线性表示的形式化定义与系数含义
把上面的场景抽象成数学语言。
设有一个向量组:
[
\alpha_1, \alpha_2, \ldots, \alpha_k
]
以及一个目标向量:
[
\beta
]
如果存在一组实数(或复数,取决于你在实空间还是复空间):
[
c_1, c_2, \ldots, c_k
]
使得:
[
\beta = c_1 \alpha_1 + c_2 \alpha_2 + \cdots + c_k \alpha_k
]
就称 (\beta) 可以由向量组 (\alpha_1, \alpha_2, \ldots, \alpha_k) 线性表示,而 (c_1, c_2, \ldots, c_k) 称为表示系数。
这个定义看起来简单,但有几个细节值得抠。
第一,系数可以是零。零系数意味着某个基底向量在表示过程中没有参与,这并不违反定义。所以"零向量可以由任何向量组线性表示"这一条也自然成立,因为把全部系数取零就行。
第二,表示不一定唯一。如果你手头的向量组里面有冗余(存在线性相关的向量),同一个目标向量可能有多种不同的系数组合都能拼出它。这就是后面要讲的"线性相关"概念和表示唯一性之间的直接联系。
第三,系数的大小没有限制,可以是任意实数。这一点容易被忽略,因为很多人潜意识里会觉得"表示"像"拆分"一样应该有比例限制。实际上线性表示里不涉及"不超出总量"这类约束,系数想多大就多大,只看方程是否成立。
在代码里,这个定义对应的就是一次矩阵乘法。把所有 (\alpha_i) 按列排成一个矩阵 (A),把系数 (c_i) 排成列向量 (x),那么是否有解的问题就变成了:
[
A x = \beta
]
接下来整章的算法、代码、坑,全部围绕这个矩阵方程展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 判断"能不能表示":核心算法与NumPy实现
2.1 数学本质:解一个线性方程组
判断 (\beta) 能否由向量组 (\alpha_1, \alpha_2, \ldots, \alpha_k) 线性表示,等价于判断线性方程组 (Ax = \beta) 是否有解。
这里的 (A) 是一个 (m \times n) 矩阵,(m) 是每个向量的维度,(n) 是向量组里向量的个数。(x) 是 (n) 维未知列向量,也就是我们要找的表示系数。
判断是否有解,线性代数课本给了一个非常清晰的标准:系数矩阵的秩等于增广矩阵的秩。
[
\mathrm{rank}(A) = \mathrm{rank}([A \mid \beta])
]
如果相等,方程组有解,(\beta) 可以被表示;如果不相等,方程组无解,(\beta) 无论如何也拼不出来。
为什么要用秩来判断?因为矩阵的秩反映的是系数矩阵列向量张成的子空间的维度。增广矩阵加进来的列就是 (\beta)。如果 (\beta) 原本就落在 (A) 的列空间里,那它不会给增广矩阵带来新的维度,两个秩相等;如果 (\beta) 跳出了列空间,增广矩阵的列空间变大,秩也跟着变大,两个秩就不相等了。
实践里我更喜欢直接求解方程组,而不是只看秩。因为仅判断"能不能",不回答"系数是什么",而实际工程中你几乎总是需要系数。不过秩判定作为理论依据和快速检验手段,在面试题和手推场景里非常有用。
2.2 代码实现:最小二乘求解与秩判定
在NumPy里判断线性表示,我一般写两个函数,一个用最小二乘,一个用秩判定,相互对照,防止单一方法的数值误差误导我。
先看最小二乘版本:
python复制import numpy as np
def check_linear_representation(A, b, tol=1e-8):
"""
判断向量 b 能否由矩阵 A 的列向量线性表示。
参数:
A : m x n 的数组,列向量是候选的基底向量
b : m 维数组,目标向量
tol : 重构误差的容忍阈值
返回:
(can_represent, coefficients, residual_norm)
"""
# 使用最小二乘法求解 A @ x ≈ b
x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None)
# 用求解得到的系数重构目标向量
reconstructed = A @ x
residual_norm = np.linalg.norm(reconstructed - b)
can_represent = residual_norm < tol
return can_represent, x, residual_norm
这个函数的逻辑很直接:不管方程组有没有精确解,最小二乘法总会给出一个让 (|Ax - b|) 最小的 (x)。如果原始问题有精确解,那么重构误差会非常接近零;如果没有精确解,最小二乘给出的系数只是"最接近"的表示,重构误差会明显大于零。
再看基于秩判定的版本:
python复制def check_linear_representation_by_rank(A, b, tol=1e-8):
"""
通过 rank(A) == rank([A | b]) 判断 b 能否由 A 的列向量线性表示。
"""
# 构造增广矩阵
augmented = np.hstack([A, b.reshape(-1, 1)])
rank_A = np.linalg.matrix_rank(A, tol=tol)
rank_augmented = np.linalg.matrix_rank(augmented, tol=tol)
can_represent = (rank_A == rank_augmented)
return can_represent, rank_A, rank_augmented
两个函数配合使用的场景我后面会详细说。这里先给你看一个具体例子,理解它们的行为差异。
python复制# 例1:目标向量在向量组张成的平面内
A = np.array([
[1, 0],
[0, 1]
])
b = np.array([3, 4])
can, x, res = check_linear_representation(A, b)
print(can, x, res)
# 输出:True [3. 4.] 0.0
# 例2:目标向量不在向量组张成的直线上
A_line = np.array([
[1],
[2]
])
b_out = np.array([1, 0])
can2, x2, res2 = check_linear_representation(A_line, b_out)
print(can2, x2, res2)
# 输出:False [0.2] 0.8944271909999155
第二个例子里,如果要求严格精确表示,这个向量确实做不到,因为一条倾斜直线上的点永远到不了横轴上的 (1, 0)。最小二乘给出的系数 0.2,其实是把目标向量投影到这条直线上得到的投影点的参数,重构之后就得到了 ((0.2, 0.4)),和 ((1, 0)) 的距离约为 0.8944。
这就是为什么我说实际工程里不能只看"能不能":很多场景下目标向量虽然不能精确表示,但最小二乘解依然有它的价值。它告诉你"在允许误差的前提下,最优的表示是什么"。
2.3 无解、唯一解、无穷解三种情况
解线性方程组 (Ax = \beta),和线性表示直接相关的有三种结果。
无解。 对应 (\mathrm{rank}(A) < \mathrm{rank}([A \mid \beta]))。目标向量跑出了向量组张成的空间。这在几何上最直观:一条直线上的向量组,没办法表示平面外的向量;一个平面上的向量组,没办法表示空间外的向量。
唯一解。 对应 (\mathrm{rank}(A) = \mathrm{rank}([A \mid \beta]) = n),其中 (n) 是列向量个数。这时向量组线性无关,目标向量在张成空间内部,而且表示方式是唯一确定的。系数可以直接通过求解得到。在NumPy里就是:
python复制x = np.linalg.solve(A, beta)
前提是 (A) 是方阵且满秩。如果不是方阵但列满秩,用最小二乘也能得到唯一解。
无穷多解。 对应 (\mathrm{rank}(A) = \mathrm{rank}([A \mid \beta]) < n)。向量组内部有线性相关关系,存在冗余。目标向量可以被表示,但表示方式不唯一。实际含义是:你可以用不同的系数组合拼出同一个目标向量,这些组合之间的差异恰好落在零空间里。
理解这三种情况,对理解后面的"基"和"张成空间"很有帮助。因为在无穷多解的情况下,你虽然能表示目标向量,但你没法说清楚"这个表示系数到底意味着什么"——它可能只是因为你选了一组冗余的基底,系数分布完全取决于求解器的内部选择。
2.4 判定中容易出现的数值陷阱
这里必须说一个我踩过很多次的坑:浮点误差会让精确判断失控。
在线性代数理论里,"有解"是精确概念。但在计算机里,矩阵元素是浮点数,运算是有限精度的,一个理论上有解的方程组,你用NumPy算出来的重构误差可能是 (10^{-12}) 而不是 (0);一个理论上无解的方程组,误差可能因为数值扰动变得很小。
所以阈值 (tol) 的选择很关键。设大了会把"本来不可表示"误判成"可以表示",设小了会把"本可以表示"误判成"不能表示"。
我常用的做法是:
python复制def check_linear_representation_robust(A, b):
"""
先看秩,再用最小二乘验证,最后结合矩阵规模给出可靠结论。
"""
m, n = A.shape
# 用秩判定作为主判断
augmented = np.hstack([A, b.reshape(-1, 1)])
rank_A = np.linalg.matrix_rank(A)
rank_aug = np.linalg.matrix_rank(augmented)
if rank_A != rank_aug:
return False
# 用最小二乘重构误差作为辅助验证
x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None)
reconstructed = A @ x
residual_norm = np.linalg.norm(reconstructed - b)
# 误差阈值要参考矩阵大小和元素量级
scale = max(np.linalg.norm(A) * np.linalg.norm(x), 1e-12)
tol = 1e-10 * scale
return residual_norm < tol, x, residual_norm
这里我引入了一个基于矩阵规模和系数规模的相对误差阈值,而不是用绝对阈值。原因很简单:处理地理坐标这类数值很大的数据时,误差本身就会很大;处理像素值这类小数值数据时,误差又会很小。绝对阈值在这种跨尺度场景下很不靠谱。
另外要注意,np.linalg.matrix_rank 的默认截断参数是 tol=None,它基于矩阵奇异值自动计算。当矩阵维度很大或者接近奇异时,这个默认值的判断结果可能和你的预期有偏差。我一般显式传入一个阈值,或者用奇异值分解自己看一下奇异值的分布。
3. 张成空间、线性无关与基:线性表示的上下文
3.1 张成空间:一族向量能表示的范围
线性表示关心的不只是"一个特定向量能不能被表示",更关心"一组向量总共能表示哪些向量"。后者就是张成空间的概念。
给定向量组 (\alpha_1, \alpha_2, \ldots, \alpha_k),它们的所有线性组合构成的集合:
[
\mathrm{span}{\alpha_1, \alpha_2, \ldots, \alpha_k} =
\left{ \sum_{i=1}^{k} c_i \alpha_i ;\middle|; c_i \in \mathbb{R} \right}
]
称为这个向量组的张成空间。
你之前学过的"平面""直线""三维空间",在线性代数语境下都可以用张成空间来描述:
- 两个不共线的三维向量张成一个平面(二维子空间)
- 两个共线的三维向量张成一条直线(一维子空间)
- 三个不共面的三维向量张成整个三维空间
用代码思考会更直观:
python复制def is_in_span(A, b, tol=1e-8):
"""判断 b 是否在 A 的列向量张成的空间中"""
return check_linear_representation(A, b, tol=tol)[0]
张成空间这个概念的价值在于,它帮你把"某个具体向量能否被表示"提升为"整个子空间长什么样"。当你在做数据降维时,你本质上是在问:原始高维数据点,能不能在某个低维子空间里被(近似)线性表示出来?这个低维子空间就是某些基向量张成的。
3.2 线性无关:去掉冗余才能谈唯一表示
一个向量组里如果有某个向量能由其他向量线性表示,就说这个向量组线性相关。反过来,如果任何一个向量都不能由其余向量线性表示,就说这个向量组线性无关。
换个角度理解:线性相关意味着张成空间里有冗余。比如平面上三条不共边的射线,第三条一定可以由前两条的组合表示出来。向量组里有个"多余"的向量,它不贡献新的方向。
判断线性无关的代码很简单:
python复制def is_linearly_independent(vectors):
"""
判断一组向量是否线性无关。
vectors: list of arrays 或 m x n 矩阵
"""
A = np.column_stack(vectors) if not isinstance(vectors, np.ndarray) else vectors
n = A.shape[1]
rank = np.linalg.matrix_rank(A)
return rank == n, rank
这里的关键关系是:向量组线性无关,当且仅当它们的张成空间维数等于向量个数。秩就是这个维数。
我在实际写算法时,经常会先做一次线性无关检查。因为后面的最小二乘求解、解唯一性判断、系数稳定性,全部都建立在"基底无冗余"这个前提上。如果向量组线性相关,求出的表示系数虽然数学上仍然成立,但数值上可能极不稳定,甚至完全不可信。
3.3 基与坐标:线性表示的"标准答案"
如果一个向量组线性无关,并且它的张成空间恰好是整个目标空间,那这个向量组就是该空间的一组基。
基的意义在于:空间里的任何一个向量,都可以被这组基唯一地线性表示。那个唯一系数组合,就是该向量在这组基下的坐标。
这里有个思维转变:平时说的"向量的坐标",默认是在标准基下。比如二维向量 ((3, 4)),意思是 (3 \times (1,0) + 4 \times (0,1))。但在另一组基下,比如:
[
e_1 = (1, 1), \quad e_2 = (1, -1)
]
向量 ((3, 4)) 在这组基下的坐标是多少?通过解方程组可以算出来,结果是 ((3.5, -0.5))。也就是说,((3,4) = 3.5(1,1) + (-0.5)(1,-1))。
你看,同一个几何向量,在不同基下的"表示"完全不同。这就是线性表示这项工具的核心能力:在不改变向量本身的前提下,改变它的"表达方式"。
在代码里做基变换,其实就是一个矩阵求逆再乘一次的操作。已知向量 (v) 在标准基下的坐标,想要它在新基 (B)(列向量组成的矩阵)下的坐标,就去解 (B x = v):
python复制def change_basis(v, B):
"""
计算向量 v 在新基 B 下的坐标。
B 的列向量是新基,要求 B 是方阵且可逆。
"""
return np.linalg.solve(B, v)
比如上面的例子:
python复制B = np.array([[1, 1],
[1, -1]])
v = np.array([3, 4])
coords = change_basis(v, B)
print(coords)
# 输出:[3.5 -0.5]
这个操作在信号处理里特别常见。傅里叶变换、小波变换,本质都是把一个信号在不同基函数下重新线性表示。你听到的"时域""频域",其实就是信号在两组不同基下的不同坐标表示。
4. 从理论到应用:线性表示在机器学习项目中的三个典型落点
4.1 线性回归:预测值就是特征的线性表示
线性回归可能是线性表示最朴素的应用。给定特征矩阵 (X),每一行是一个样本,每一列是一个特征。线性回归假设目标值 (y) 可以写成特征的线性组合:
[
y = X w + \varepsilon
]
这里的 (w) 就是表示系数。训练一个线性回归模型,本质上就是找到一组系数,让 (X w) 尽可能精确地等于 (y)。
如果特征之间完全线性相关,(X) 的列向量线性相关,那么 (w) 不唯一。这就是你在机器学习课上学到的"多重共线性"问题。明白了线性表示这一章的秩与唯一性关系,你能立刻从线性代数层面理解为什么岭回归会存在,为什么它给 (w) 加了 (\ell_2) 惩罚项之后,反而能让解稳定下来。
代码上的体现是这样的:
python复制X = np.array([
[1, 2],
[2, 4],
[3, 6],
]) # 第二列是第一列的2倍,列向量线性相关
y = np.array([1, 2, 3])
# 直接用最小二乘,解不唯一
w, residuals, rank, s = np.linalg.lstsq(X, y, rcond=None)
print("rank:", rank)
# 输出:rank: 1
遇到这种情况,最小二乘虽然还会返回一组系数,但你已经不能把这个系数的具体数值当作可靠的"特征重要性"来解读了。这就是为什么做特征工程时,第一步几乎总是检查特征之间的相关性,剔除或者合并高度相关的列。
4.2 PCA降维:从高维表示到主成分表示
主成分分析(PCA)是理解"换一组基重新线性表示"的最佳工程案例。
原始数据通常用标准基下的坐标表示,维数很高。PCA做的事情是找到一组新的正交基,使得数据在这组基下的坐标方差尽可能大。然后丢掉那些方差贡献小的维度,只保留前 (k) 个主成分方向。
用线性表示的语言说就是:把高维空间中的每个样本点,投影到一个由前 (k) 个主成分张成的低维子空间里,用这 (k) 个基向量的线性组合来近似表示原始样本。
代码实现中,最常见的一个步骤是:
python复制from sklearn.decomposition import PCA
pca = PCA(n_components=2)
X_reduced = pca.fit_transform(X)
# X_reduced 的第 j 列,就是样本在主成分方向 j 上的表示系数
fit_transform 输出的本质,就是每个样本在PCA主成分这个新基下的坐标。你说它是降维也行,你说它是换一组基做线性表示也对。当你把这两种说法打通时,PCA就不再是一个黑盒了。
4.3 特征分解:换一组基重新表示矩阵
特征分解 (A = V \Lambda V^{-1}) 也可以从线性表示的角度理解。
矩阵 (A) 的作用是对向量做线性变换。特征向量是那些经过变换后方向不变的向量。如果一组特征向量 (v_1, v_2, \ldots, v_n) 恰好构成一组基,那么任意向量 (x) 都可以先表示成这组基下的坐标,然后让变换在每个特征向量方向上独立地缩放(缩放比例就是特征值),最后再换回标准基坐标。
整个过程就是用特征向量作为基,把矩阵变换"表示"成简单的逐轴缩放操作。
在代码里:
python复制A = np.array([[2, 1],
[1, 2]])
eigenvalues, eigenvectors = np.linalg.eig(A)
# eigenvectors 的列是特征向量
# A @ eigenvectors[:, 0] = eigenvalues[0] * eigenvectors[:, 0]
这个视角看矩阵的幂计算、微分方程求解都会变得非常清爽。很多看起来复杂的矩阵操作,一旦换了基,就变成对角矩阵上的逐元素乘法。
5. 实战结论:这几个坑值得单独记录
5.1 浮点误差会让"精确判断"直接失灵
我前面提过阈值选择的问题,这里再深入说一个具体场景。
假设你有一个三维向量组 (A),理论上它能张成整个三维空间。但是因为你构造数据时非常不巧,三个向量几乎共面,奇异值分别是 ([10, 1, 10^{-15}])。从数学上说,第三个奇异值不为零,向量组仍然张成三维空间,任何三维向量都能被它线性表示。但从数值计算的角度看,第三个奇异值小到和浮点误差一个量级,实际上这个向量组在数值上退化成了二维。
此时如果你用 np.linalg.matrix_rank(A) 判断,默认容差下它可能会给出秩为2,从而误判很多向量"不能被表示"。反过来,如果你自己写了极小容差,它又可能把秩判为3,然后求解出数值上极不稳定的巨大系数。
我的建议是:不要追求绝对精确的数学判定,要结合你所在问题的物理意义和数据精度来定容差。 如果数据本身是从真实传感器采样的,精度只能到 (10^{-3}),那奇异值低于 (10^{-6}) 的方向就应该被当作噪声维度处理。
5.2 矩阵接近奇异时,表示系数会炸裂
这是我在做线性回归实验时踩过的坑。
当你用最小二乘求解 (Ax = b),而 (A) 接近奇异时,得到的系数 (x) 的各个分量会非常大,而且正负交替,看起来完全没有直觉含义。这是因为你试图用一个几乎冗余的基底去表示目标向量,基底之间的微小差异被系数之间的巨大数值差异抵消了。
解决办法有几个:
- 用
np.linalg.lstsq而不是np.linalg.inv(A) @ b,最小二乘在数值上更稳。 - 正则化。给系数加 (\ell_2) 惩罚,对应岭回归的思路。
- 先做秩检查,发现接近奇异就换基底,去掉冗余维度。
这三个方法我在不同项目里都用过。如果只是快速验证一个理论想法,选最小二乘加秩检查就够了;如果是上线模型,通常得靠正则化保证稳定性。
5.3 一个小提醒:不要只依赖单一判断函数
我自己的习惯是上面两个函数一起用。check_linear_representation 给系数和残差,check_linear_representation_by_rank 给严格的数学判断。两者结合起来,一个是"理论层面它行不行",一个是"数值层面我能不能解出来",这在工程上经常是两个不同的问题。
比如前面提到的近奇异的例子。理论上它是能表示的,(A) 满秩,增广矩阵秩也满。但你去求解,系数可能大到离谱,重构误差看似很小,实际模型却完全不可用。只看秩判断你会被误导,只看最小二乘你也会被误导。两个结果放一起比对,你才能看到全貌。
这一章的代码我放在自己的项目里已经用了很久,是处理线性表示问题时最顺手的一套工具。你可以在自己的项目里直接抄走这两个函数,也可以按需要改成适应你数据规模的版本。核心逻辑就一个:线性表示问题,归根到底就是解 (\mathbf{Ax = b}),理解了这句话,这一章就算真正学到了。
