ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深度学习必啃的线性代数:从矩阵运算到SVD与PCA实战

深度学习必啃的线性代数:从矩阵运算到SVD与PCA实战

1. 为什么深度学习非要啃线性代数

1.1 大学线代与炼丹实践之间的那道断层

先说个现象:很多人大学毕业时把线性代数考得不错,行列式、克莱姆法则、伴随矩阵背得滚瓜烂熟,但一接触 PyTorch,看到torch.mm、view、permute就完全懵了。这不是你不行,而是大学线代和深度学习实战之间存在一道断层:课本反复训练你“怎么算”,却很少告诉你“这个运算在数据里到底意味着什么”。

我这几年的体会是,深度学习本质上就是在做大规模矩阵运算,只不过传统线代考试里3×3的矩阵,在实际项目中变成了几万维的稀疏大矩阵。梯度下降是向量和矩阵在流动,反向传播是雅可比矩阵的链式相乘,注意力机制是矩阵加权组合,模型压缩是低秩近似。可以说,线性代数不是深度学习的前置选修课,而是这门学科本身的骨架。花书把第二章安排成开篇数学基础,目的就是让读者在同一个符号系统里理解后续每一章,这一章跳过去,后面肯定要回来补课。

1.2 第二章在花书里的地位:全书符号系统的起点

花书第二章篇幅不算长,但信息密度极高,我建议你把它当成一份“数学词典+工具箱”来读。这一章覆盖的内容,我大致梳理一下:标量、向量、矩阵、张量的定义与命名习惯;转置和矩阵乘法的规则;单位矩阵与逆矩阵;线性相关、生成子空间与秩;范数;特征值与特征向量、特征分解;奇异值分解;Moore-Penrose伪逆;最后是以特征分解为核心的主成分分析。

这些概念看起来各自独立,实际上有一条暗线:从单个数字出发,逐步构建出“描述数据、变换数据、压缩数据、度量数据”的完整工具链。读的时候不要只记公式,要带着问题读:向量是数据样本,矩阵是数据的形状偏移和批量集合,特征分解是在找数据的主要变化方向,SVD是在找任意矩阵的主要成分。有了这根线,后面学卷积、学循环网络、学Transformer、学生成模型,都会少走很多弯路。下面我就按这条主线,把第二章拆开来讲,并且补上我实际写代码时踩过的坑。

2. 从零理解符号:标量、向量、矩阵、张量

2.1 四种“容器”分别装什么数据

线性代数开篇讲的是符号约定。很多人觉得这部分是废话,但它恰恰是后面所有代码里的第一坑。按花书的约定,单个数是标量(scalar),用小写斜体字母表示;一列数是向量(vector),用小写粗体字母表示;一个二维数组是矩阵(matrix),用大写粗体表示;维度超过二维的数组叫张量(tensor)。在深度学习里,这套对应关系非常具体:学习率是标量,一个样本的特征是向量,一批样本拼在一起就是矩阵,彩色图片多了一个通道维度,所以常被表示成高×宽×通道的三维张量。

用代码来对应会更清晰。PyTorch里一个形状为(batch, channels, height, width)的四维张量,本质就是把4张3维图片数据打包在一起。你最早接触的线性层,其实就是输入向量(或输入矩阵) × 权重矩阵 + 偏置向量,这里的权重矩阵就是一个“可学习的变换参数”。理解这一点后,再看各种深度学习框架里的nn.Linear就不会发怵了。

提示:花书默认向量是列向量,即(n,1)形状。但现代深度学习框架在表示单样本时通常使用(1,n)的行向量,并且用batch维度放在最前面。这个约定差异会导致转置方向反直觉,我见过太多人在手推公式和写代码之间对不上号。建议手推时用花书符号,写代码时先确认框架的形状文档。

2.2 约定与批量思维:把一列向量升级成一张表

单独研究一个样本没意思,深度学习要求并行处理一大批样本。假设有 m 个样本,每个样本有 n 个特征,把它们写成 X,形状是 (m, n)。线性层的计算是X @ W + b,其中 W 的形状是 (n, k),b 的形状是 (k,),结果是形状 (m, k) 的输出矩阵。这行代码的意思是:一次性把 m 个样本全部通过同一个线性变换,得到 m 个新的 k 维特征向量。

这种“批量思维”很容易被初学者忽略,却是矩阵乘法的最高频用法。你不需要写循环,一个矩阵乘法就能替你完成大量样本的并行计算。GPU 对这种操作极度友好,因为矩阵乘法的数据访问模式非常规则,可以拆成无数小的矩阵乘子任务丢给不同计算单元。这也是为什么所有深度学习框架都以矩阵乘为核心算子,而不是用 for 循环逐样本处理。理解这一层,你就能明白(m, n)@(n, k)这种形状检查为什么如此重要:如果 n 对不上,框架直接报错,而报错信息往往是你调试模型的第一步线索。

3. 矩阵乘法:从“行乘列”到“列的线性组合”

3.1 两种视角,缺一不可

矩阵乘法的教科书定义是:结果矩阵的第 i 行第 j 列,等于 A 的第 i 行与 B 的第 j 列做点积。这个定义对写循环、算具体值很有用,但对理解机器学习的变换过程帮助不大。我强烈推荐你在头脑里建立第二种视角:把矩阵 A 按列拆开,把向量 x 按分量拆开,那么A @ x等于“A 的每一列乘以对应分量后的求和”。换句话说,A 作用在 x 上,等价于用 x 作为权重,去组合 A 的各个列方向。

这个线性组合视角才是理解神经网络的关键。全连接层的每个输出神经元,本质上是对输入特征做加权求和;卷积层的每个输出特征图,是对输入通道做加权组合;注意力机制的Attention = softmax(QK^T) @ V,更是把 V 的各行按注意力权重做加权平均。如果你只会“行乘列”的视角,看到注意力公式会一头雾水,一旦换成“按权重挑选并组合信息”的视角,整个公式就变得非常自然:模型决定每条信息的权重,然后把信息按权重混合成输出。

3.2 线性组合视角如何帮助理解注意力机制

举一个我常跟读者讲的例子。设 V 是若干条信息组成的矩阵,每行是一条信息。注意力权重矩阵 S 的每一行是一个概率分布,S @ V的每一行就是把 V 的所有行按概率加权求和。这个过程就像在会议上发言:你不可能同时说所有观点,而是根据重要程度给每个观点分配发言时间,最后输出的是一个综合了所有观点的结论。当某个权重接近0时,对应信息几乎不起作用,这在数学上就等价于“忽略这一列”。

理解了这一点,你再看多头注意力、加权特征融合、图神经网络里的消息传递,就会发现它们都是线性组合在不同数据形态上的复用。花书第二章虽然没有直接讲注意力,但矩阵乘法的这个视角早就把思维工具给你了。

3.3 算力成本与形状检查:先说形状,再谈性能

矩阵乘法的代价不可忽视。两个形状分别为 (m, n) 和 (n, p) 的矩阵相乘,乘法加法操作总数约为2 × m × n × p,这个数字在模型尺寸变大后爆炸式增长。这也是为什么现代模型强调参数量、FLOPs(每秒浮点运算次数)和带宽优化。实际调试里,我更常遇到的是形状错误,而不是算力不足。经验是:写完一行涉及多个矩阵的代码,先用假张量跑一遍正向和反向,再上真实数据。形状报错时,把每个参与运算的张量的形状列出来,从后往前逐个对齐维度,几千维的大模型也逃不过这个笨办法。

4. 单位矩阵、逆矩阵与秩:可逆与不可逆到底意味着什么

4.1 为什么神经网络训练不直接求逆

单位矩阵是矩阵世界里的数字1,任何矩阵乘单位矩阵都保持不变。逆矩阵则回答了线性方程组“能不能反解”的问题:如果 A 可逆,那么Ax = b的唯一解是x = A^{-1}b。这个公式看起来很优雅,但现实中我们几乎不会在神经网络的训练里直接用逆矩阵。原因很简单:求逆是 O(n^3) 的计算量,而且矩阵接近奇异时数值误差会被急剧放大,误差反而比迭代优化更严重。

真正会用到逆矩阵的地方是传统统计分析和几何计算。比如最小二乘回归的闭式解θ = (X^T X)^{-1} X^T y,在手推教材中很漂亮,但在实际数据上,只要特征之间存在一点多重共线性,X^T X就可能接近奇异,直接求逆就会产生离谱的系数。这正好引出深度学习选择梯度下降而不是一步到位的根本原因之一:高维非线性问题没有闭式解,即使有,数值稳定性也难以保证。

4.2 线性相关、生成子空间与秩:你能记住多少有效信息

线性相关描述的是:一组向量里,某一个能否由其余向量的线性组合表示。如果能,说明它没有携带额外信息。从数据角度看,如果特征矩阵有两列完全成比例,这两列就是冗余的。生成子空间是指一组向量的所有线性组合能张成的空间,秩则是生成子空间的维度,也是矩阵有效信息量的度量。

花书里特别强调,一个不可逆方阵的秩小于其行数,这通常意味着数据里有冗余维度或者事实上处于更低维的空间。我用这个视角处理过实际项目中的特征工程:当 200 个特征的多重共线性严重时,矩阵的秩可能只有 150 左右,这时强行用全部特征训练,不仅速度慢,权重估计也不稳。先用奇异值看看有效秩,再做降维或正则化,结果会稳定很多。判断矩阵是否满秩,千万别靠行列式是否为0,正确做法是检查奇异值的大小或者直接调用np.linalg.matrix_rank,它底层就是用SVD来判断的。

4.3 伪逆:不可逆时的最小二乘救兵

很多线性方程组是无解或有无穷多解的,比如测量数据噪声大、样本数少于特征数,或者特征之间有相关性。花书引入的Moore-Penrose伪逆就是在这个场景下发挥作用的。在给定矩阵 A 的SVDA = U Σ V^T后,伪逆为A^+ = V Σ^+ U^T,其中Σ^+是把奇异值取倒数(奇异值为0的保持0)得到的对角矩阵。伪逆对应的解有一个很好的性质:在所有可行解里,它让||Ax - y||最小,同时让||x||也最小。

我实际使用伪逆最多的地方是小规模线性系统的快速求解。当正规方程因为X^T X奇异而崩溃时,用np.linalg.pinv(X) @ y可以直接算出最小范数最小二乘解。这里有个实操细节:伪逆的默认容差是max(M, N) * eps * max(singular_values),如果数据量纲差异大,你需要手动调整容差参数,否则噪声维度的小奇异值会被错误保留。这个坑在特征值差异超过几个数量级的数据集上特别明显。

5. 范数:把“大小”量化成可优化的目标

5.1 Lp范数到底是干什么的

机器学习需要度量“向量有多大”“两个向量有多接近”,范数就是这套尺子。花书里定义Lp范数为向量元素绝对值 p 次方之和再开 p 次方,其中L2范数是我们最熟悉的欧氏距离,L1范数则是曼哈顿距离——只能沿着坐标轴走的距离。还有最大范数L∞,返回值里绝对值最大的那个分量。

范数的用途很直接:损失函数要度量模型输出和真实标签的误差,所以回归任务常用均方误差(平方的L2);稀疏性正则化要用L1;对抗样本攻击里常用L∞描述最大扰动幅度。对于矩阵,常用的是Frobenius范数,它把矩阵视为一长串数值向量,算平方和的平方根。PyTorch里的权重衰减、NumPy里的np.linalg.norm,都是这套概念的工程实现。

5.2 L1与L2正则化:一个推稀疏,一个压均匀

在花书第二章的语境里,范数最核心的应用是正则化。L2正则化是在损失函数里加一项权重平方和,效果是让所有权重同时被压缩到较小值,但不会主动把某个权重变成精确的0。L1正则化加的则是权重的绝对值之和,它倾向于把不重要的权重直接压成0,从而带来稀疏性。为什么会有这个区别?从几何上看,L2的等高面是光滑的圆球形状,切点大概率不在坐标轴上;L1的等高面是菱形的,顶点恰好落在坐标轴上,优化过程很容易把某些维度推到0。

这个区别体现在模型选择上:想要特征选择、想要可解释的稀疏模型,用L1;想要抑制过拟合、让模型更平滑,优先L2;L1和L2结合(例如Elastic Net)则可以兼顾稀疏和稳定。实际调参时,我通常先试L2,如果模型过拟合严重且特征冗余明显,再加L1并调小惩罚系数。正则化系数太大会把模型压死,太小等于没加,这个度需要通过验证集来看。

5.3 用NumPy感受范数:三行代码看行为

看公式不如跑一遍代码,用NumPy 可以很直观地感受不同范数对同一个向量的差异:

import numpy as np x = np.array([3.0, -4.0, 0.0, 1.0]) print(np.linalg.norm(x, ord=2)) # L2: 5.099 print(np.linalg.norm(x, ord=1)) # L1: 8.0 print(np.linalg.norm(x, ord=np.inf)) # L∞: 4.0 A = np.random.randn(8, 3) print(np.linalg.norm(A, ord='fro')) # 矩阵Frobenius范数

从这个例子能直观看出:L2把大的分量和平滑分量都算在一起,L1更强调绝对值的总和,L∞则只关心最大的单一影响。哪种范数适合你的任务,往往取决于你的优化目标到底是“整体差异小”还是“最大偏差可接受”。做图像去噪时L2会让噪声均匀平滑,而L1更擅长保留边缘细节,原因就在于两者对异常值的惩罚力度完全不同。

6. 特征值与特征向量:矩阵的“主轴”与PCA

6.1 特征分解背后的几何直觉

特征值与特征向量,是理解矩阵最深刻的一把钥匙。一个矩阵作用于向量,通常会把向量拉伸、旋转、偏斜,但如果某个向量经过这个矩阵作用后,只是被拉伸或者压缩,方向不变,这个方向就叫特征向量,拉伸倍数就是特征值。特征分解A = Q Λ Q^{-1}的意思就是:矩阵 A 的效果,可以拆成“换到特征向量基”、“逐坐标伸缩”、“换回原基”三步。

花书里特别提到,实数对称矩阵的特征分解可以写成A = Q Λ Q^T,这里的 Q 是正交矩阵,相当于一个旋转/反射操作。这个性质在机器学习中极其常用,因为协方差矩阵一定是对称半正定的,它的特征值都是非负实数,特征向量相互正交。可以说,特征分解把矩阵拆成了“方向集合 + 方向伸缩倍率”,这是后面PCA的基础。

6.2 特征值大小如何影响深度网络的训练稳定性

特征值大小不只是几何概念,它直接影响深度网络的梯度行为。反向传播本质上是一串雅可比矩阵连乘,如果某个雅可比矩阵在所有方向上的特征值都小于1,多次连乘后梯度会指数级衰减,浅层网络几乎收不到有效梯度;如果特征值大于1,梯度又可能爆炸。这个现象在循环网络和深层前馈网络里尤其明显。

虽然现代架构用残差连接、归一化手段缓解了这个问题,但理解特征值能帮你解释很多模型行为。比如批量归一化之所以稳定训练,一部分原因是它把每层的激活分布拉回到合理的尺度,从而间接调整了特征值范围。我调过的一个深层网络,前期训练发散,检查后发现某一层的权重矩阵谱范数(最大奇异值)过大,做了谱归一化或权重裁剪后明显改善。这类问题,如果没有特征值/奇异值的概念,会完全不知道从哪里入手。

6.3 PCA实操:主成分提取与降维投影

PCA(主成分分析)是特征分解最经典的应用。假设有 n 个样本、d 个特征,PCA要做的是找到数据变化最大的几个方向,把数据投影到这些方向上,实现降维、去相关和可视化。操作流程分四步:先对数据做中心化(每个特征减去均值);再计算协方差矩阵;对这个对称矩阵做特征分解;最后按特征值从大到小取出前 k 个特征向量,组成投影矩阵。

用NumPy实现非常直接:

import numpy as np X = np.random.randn(200, 5) # 200个样本,5维特征 X_center = X - X.mean(axis=0) # 中心化 cov = X_center.T @ X_center / (len(X) - 1) # 协方差矩阵 eigvals, eigvecs = np.linalg.eigh(cov) # 对称矩阵用eigh更稳定 idx = np.argsort(eigvals)[::-1] # 从大到小排序 eigvecs = eigvecs[:, idx] k = 2 projected = X_center @ eigvecs[:, :k] # 降到2维

这段代码里有个关键细节:eigh默认把特征值从小到大排列,所以必须先argsort反转顺序,再取前 k 个特征向量。很多人拿到eigh的结果直接取[:, :k],结果取到的是方差最小的方向,降维效果自然一塌糊涂。投影之后,数据在新的坐标轴下几乎不相关,方差最大的方向落在第一主轴,这比直接丢掉特征列要科学得多,因为PCA保留了数据内部的主要结构。

7. 奇异值分解:被低估的矩阵终极工具

7.1 SVD为什么重要:任何矩阵都能分解

特征分解只适用于方阵,而且不是所有方阵都可以对角化。SVD奇异值分解则是更通用的分解:任何矩阵 A 都可以写成A = U Σ V^T,其中 U 的列是A A^T的特征向量,V 的列是A^T A的特征向量,Σ 对角线上的奇异值是这些特征值的平方根。更妙的是,奇异值一定非负,并且通常从大到小排列,后面的奇异值衰减得飞快,这说明很多矩阵的信息其实集中在前面几个分量上。

我甚至觉得SVD是花书第二章里“性价比”最高的一块内容。它一个工具解决了三类问题:判断矩阵的秩(非零奇异值个数)、条件数(最大奇异值除以最小奇异值)、以及最佳低秩近似(截断SVD)。深度学习里的模型压缩、人脸识别中的特征脸、推荐系统的隐语义模型、自然语言处理里的潜在语义分析,底层都是这个思想。

7.2 SVD三件套在推荐、压缩、隐语义中的用法

在很多实际系统中,SVD就是把一个巨大的稀疏矩阵分解成两个低维稠密矩阵的乘积。比如推荐系统里,用户-物品评分矩阵 R 可以近似分解成U_k Σ_k V_k^T,其中U_k代表用户隐因子,V_k代表物品隐因子,缺失的评分用这两个矩阵的乘积来预测。这比单纯用平均值填补要精准得多,因为它捕捉了用户和物品之间的潜在模式。

图像压缩是更直观的理解方式。一张灰度图天然就是一个二维矩阵,对它做SVD,只保留最大的 k 个奇异值,重建出来的图像依然能保留主体结构。k 越小时,存储量从m*n降到k*(m+n),压缩比例相当可观,虽然边缘细节会有损失,但整体轮廓的保真度往往超乎预期。对深度学习来说,这种低秩近似的思想还被用于全连接层,把一个(a, b)的大权重矩阵拆成(a, r)和(r, b)两个小矩阵,减少参数量,是一种经典模型压缩手法。

7.3 一个可跑的SVD图像压缩Demo

我建议你亲自跑一遍这个流程,比读十遍公式都管用。用任意灰度图即可,如果没有图片,直接用随机矩阵也足够理解过程。

import numpy as np from numpy.linalg import svd # 模拟一张 64x64 的灰度图 image = np.random.randn(64, 64) def svd_compress(img, k): u, s, vt = svd(img, full_matrices=False) u_k, s_k, vt_k = u[:, :k], s[:k], vt[:k, :] reconstructed = (u_k * s_k) @ vt_k return reconstructed, (s,) for k in [4, 8, 16, 32]: recon, _ = svd_compress(image, k) error = np.linalg.norm(image - recon) / np.linalg.norm(image) print(f"k={k}, 相对误差={error:.3f}")

代码里有两个初学者必踩的坑。第一,svd返回的第三个值是V^T而不是V,所以重建时要vt_k @而不是vt_k.T @,很多人在这里转置来转置去,越改越错。第二,必须用full_matrices=False,否则当原矩阵不是方阵时,U 和 Vt 会补出大量全零或无关的分量,白白浪费内存。生成真实图像时,k 值的选取依据重建误差曲线而定,通常在误差降到拐点附近时性价比最高。

8. 常见问题与避坑指南

8.1 求逆或特征分解报错,先查这三件事

用NumPy处理矩阵时,最常遇到的报错是LinAlgError: Singular matrix,这个报错不一定表示矩阵严格奇异,更多时候是数值上接近奇异。我排查这类问题固定三件事:第一,打印矩阵的形状和元素量级,看看是不是拼接数据时混入了全零列或重复列;第二,用np.linalg.matrix_rank看有效秩是否等于行数;第三,用np.linalg.cond看条件数,条件数超过1e12基本就可以认定数值不可靠。如果是求协方差矩阵的逆,或者正规方程,加上一个小对角项λI可以显著稳定结果,这个技巧叫作正则化或岭估计,原理就是把所有特征值垫高一点点,避免接近零的特征值把倒数放大到爆炸。

8.2 特征值排序、符号和SVD返回值的细节

特征值分解的细节坑太多了。numpy.linalg.eig并不保证返回的特征向量按特征值大小排序,返回的特征向量每个可以乘一个任意非零标量,所以符号是可变的。eigh对对称矩阵返回升序特征值,记得反转。SVD的奇异值默认降序,这一点反而是最省心的。还有一个容易被忽视的问题:eig对非对称矩阵可能返回复数特征值和复数特征向量,如果你用实数矩阵做特征分解却得到复数结果,别慌,这是数学上正常的事,但说明你要么用了eig而不是eigh,要么矩阵本身就不是对称的。

8.3 精度与数值稳定性:float32不是拿来硬算求逆的

我在自己项目中遇到过最隐蔽的坑,是浮点精度问题。深度学习框架默认用 float32,这在训练前向传播时没问题,但如果你想在框架里直接算矩阵求逆、特征分解或较大规模的SVD,float32的精度很容易把微弱的小奇异值全部磨平。做法建议:验证数值性质(求秩、算条件数、算伪逆)时,先把矩阵转成 float64,得到结论后再回到 float32 做训练。混合精度训练流行的今天,很多人误以为float16/32足以应对所有数值计算,实际上线性代数中的病态矩阵对精度的敏感程度远高于前向推理。常规深度学习训练里没人会去主动求逆,但一旦你需要,先让精度翻倍。

8.4 想真正吃透第二章,我的复习顺序建议

最后分享我自己啃这一章的经验,不是让大家一句句背公式,而是把它当作工具箱逐个验证。第一步,找一套可视化做得好的线性代数课程,把矩阵的几何变换、特征向量的意义、SVD的分解过程看一遍,建立直觉图像;第二步,对照花书第二章的正文,把每个公式的符号和维度捋一遍,重点搞懂为什么对称矩阵的特征分解那么干净、为什么伪逆用SVD定义;第三步,把正文里的公式用NumPy复现,包括求范数、解线性方程组、计算PCA、截断SVD重建,每个小实验控制在十几行代码内;第四步,把“线性组合视角”“特征分解找主轴”“SVD做低秩近似”这三个思维模型记在脑子里,遇到具体的模型结构就往这三个模型上套。

读完这一章后,我个人的体会是:最值得投入时间的地方不是记住公式,而是建立“看到矩阵就想到空间变换”的直觉。比如看到全连接层,我想到的是对输入特征做线性组合;看到协方差矩阵,我想到的是数据在哪些方向波动最大;看到大权重矩阵,我想到能不能用SVD做低秩近似。这些直觉一旦建立,后面所有章节的公式都会变得有血有肉。希望你读完这篇笔记之后,也能带着“这是一套空间工具”的心态去翻花书,而不是把它们当成一堆待背的符号。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进