用784个数字去描述一张手写数字的灰度图片,中间藏着多少向量、矩阵和梯度的较量?上一回我们搭好了第一条Python数据流水线,学会了加载、查看和摆弄数据。这一回要往下一层走:不管是最简单的线性回归,还是动辄上百层的深度神经网络,它们脚下其实就踩着三块基石——线性代数、微积分和凸优化。我们拿MNIST数据集当沙盘,从零开始把每个概念都跑一遍,最后会亲手用奇异值分解压缩图像、手动算出梯度,再求解一个有约束的优化问题。这些操作背后的那条线简单得出奇:每一次对模型做的变换,本质上都是在把向量和矩阵搬来搬去,每一步都靠导数来指路,每一条边界都由约束条件画出来。
线性代数负责把数据组织成高维空间里的点和方向,微积分负责告诉你往哪个方向走代价最小,凸优化则保证你不会在一个假的山谷里停下来发呆。三种数学工具叠在一起,像齿轮一样严丝合缝。先抛开那些花哨的框架不谈,把一张28×28像素的图片压扁成一个784维的向量,光是这一步,就已经站在了矩阵运算的入口。
我们用的是MNIST手写数字数据库:7万张灰度图,每张28行28列,像素值从0到255。第一步动作很直接——把每张图片抻成一个784维的向量。原本一个28×28的网格,现在变成一行长长的数字序列。这样一来,整个数据集就坍缩成了一张7万行、784列的大矩阵,也就是说,我们把7万个样本扔进了一个784维的空间里,每个样本都是这个高维空间中的一个点。后面所有关于相似度、压缩、重构的故事,都发生在这个点云里。
在做任何线性代数操作之前,先看一眼数据长什么样。探索性分析给出的结论很干净:十个数字类别的分布相当均衡,出现频率最高的数字“1”也只占了11.25%,没有明显的长尾偏斜。前5000张图片里找不到缺失值,也没有重复样本。像素强度分布呈现出典型的双峰形态——绝大多数像素都靠近黑色背景,只有很小一部分像素亮得扎眼,那是笔迹所在。随机抽五张图出来看,有的数字写得浓重,有的清淡,有的甚至带点个人风格,歪歪扭扭。再按每个数字类各抽一张排成一排,从0到9,一眼能看出哪些数字容易混淆。这些变化都不算坏事,线性模型应付得了,但代价是需要很多个分量才能把整个多样性吃下来。到底要多少个分量,等我们跑完奇异值分解就一清二楚。
在这里,每张图片就是一个向量。向量的“长度”怎么算?L1范数把所有像素的绝对值加在一起,一副五号的图片L1范数是107.94。L2范数走的是平方和再开方的路子,同一张图算出来是9.56。两种度量方法都在描述“亮度”,但口味不同:L1对每个像素一视同仁,不管你是暗是亮,统统加起来;L2则对大数值更敏感,敢冒头的像素会被平方放大,惩罚更重。换个角度想,L2更在意那几笔浓墨重彩的笔画,而L1更在意全部笔迹的总和。
有了向量,有了长度,下一个自然的问题是:两张图片有多像?答案藏在点积里。拿第一张图片和第二张图片做点积,得到53.76,余弦夹角是0.55。这说明它们共享了不少背景上的暗像素模式——毕竟大多数区域都是空的。真正正交的情况,也就是点积为零,在自然图像里几乎碰不上,因为暗背景像素会自动产生一种消极的“对齐”,让两个向量永远沾点边。所以,在MNIST图像的空间里,不存在两个完全无关的样本,它们至少会因为共同的黑色背景而产生一点微弱的牵绊。
单打独斗求相似度太慢了。矩阵乘法的好处是能一口气算出一大堆点积。我们把数据矩阵X和自己的转置相乘,得到Gram矩阵G = X @ X^T,大小是500×500。G里的每一个入口都是两张图片之间的相似度得分。对角线上的数字就是每张图片L2范数的平方:91.35、103.81、59.27,等等。这一下子,五百张图片之间的两两关系全都摆在了桌面上。Gram矩阵就是这批手写数字的集体自画像,哪些图像互为近邻,哪些互相看着不顺眼,全被压缩在这一个方阵里。
理解了向量和点积之后,再往后走一步就是整个深度学习框架里无时无刻不在运转的机制:张量形状和广播。我们的图片原始形态是一个四维张量:(N, 1, 28, 28),分别对应样本数、通道数、高度、宽度。先把它压平成(N, 784),紧接着,一行代码就给所有像素统统一口加上0.5的偏置:X_broadcast = torch.from_numpy(X).float() + bias。广播机制偷偷地把一个标量0.5拉伸成可以匹配500行、784列的隐形矩阵,根本不用写循环。这一步看起来微不足道,但在真实模型里,偏置项、均值减法、批量归一化全都靠这套拉伸逻辑在后台高速运转。如果没有广播,每一次加减运算都得手动扩展维度,代码量会暴涨,可读性也会崩塌。
现在,我们手上有向量,有相似度度量,有高效的矩阵运算,自然可以开始问一些更硬的问题:怎么把一张784维的图片压缩到几十个维度而不丢失太多信息?怎么度量一个数值计算流程的稳定性?怎么写出一段既省内存又跑得快的数值代码?这些问题会把我们引向奇异值分解、条件数和梯度计算。
压缩的第一站是奇异值分解,也就是把一个大矩阵拆成三个更小的矩阵相乘。原始图像矩阵的行代表样本,列代表像素,而SVD会把列所张成的空间重新组织一遍,找出数据里真正有方差的方向。前面提到,有些数字写得轻,有些写得怪,这些变化只会集中在少数几个主要方向上,而剩下的方向贡献很少。当我们只保留前k个奇异值和对应的奇异向量时,就相当于把784维的数据投影到一个k维的子空间里,图像被大幅度压缩,却依然保留了最主要的笔迹特征。到底k取多少,得看具体任务,但MNIST图像的测试早就告诉我们,前几十个分量就足够把“5”和“6”分得清清楚楚。
但SVD的意义不止于压缩。它还透露出数据对微小扰动的敏感程度。矩阵的条件数——最大奇异值与最小奇异值的比值——如果过大,就说明某些方向上的信息特别脆弱,随便一点噪声都会让结果翻车。在训练深层网络时,梯度矩阵的条件数过大往往意味着梯度消失或爆炸,优化器会在原地打转或者直接被炸飞。所以,看一个矩阵的条件数,就像给整个计算流程做个体检,能提前预警哪些操作可能会不稳定。
接下来,轮到微积分登场。导数的作用很简单:告诉你改变输入的每个分量,输出会跟着变多少。对于一张图像,如果我们定义了一个损失函数——比如重构误差——那么对每个像素求偏导数,就能得到一个梯度向量。这个梯度向量把784个像素各自对误差的“贡献”量化了出来。按梯度的反方向去微调这些像素值,重构误差就会下降。这就是梯度下降的几何本质:在784维的曲面上,沿着最陡峭的下坡方向迈一小步,反复迭代,直到掉进一个足够浅的山谷里。
手算一遍梯度能撕掉很多魔法感。拿一个简单得不能再简单的函数,比如 f(x) = ||Wx - y||²,对x求偏导,展开、合并、套链式法则,最后得到的是一个解析表达式。这个表达式一端连着W,另一端连着残差。推一遍之后,再看深度学习框架里自动求导的输出,就不会觉得那是凭空变出来的魔法,而是和手工推导一一对应的机械运算。偏导数和链式法则在每一层网络里不断重复,反向传播不过是一次系统化的梯度传递罢了。
有导数指路还不够,还需要知道在路的尽头等着的是不是一个真正的最低点。这里就轮到凸优化了。一个优化问题如果是凸的,局部最小值就是全局最小值,不用担心被困在假谷底。但真实世界的问题,尤其是神经网络,往往是非凸的,损失曲面上布满鞍点和局部极小值。在这种情况下,凸优化提供了一套语言:拉格朗日乘子、约束条件、对偶问题,这些都帮助我们理解求解器的行为边界。即使问题非凸,当我们施加正则化项或者限制参数范数时,本质上是在加一个凸的约束壳,把搜索空间框在一个更规矩的区域里。
把这三块拼在一起,就看清了一条流水线:数据进来,先被组织成矩阵和张量;矩阵的各种分解和变换帮我们做降维和特征提取;梯度计算告诉我们朝哪走;约束条件画出可行域的边界,优化算法沿着梯度方向在允许的区域内步步试探。从MNIST图片的784维向量出发,走过L1和L2范数的不同量尺,穿过点积和Gram矩阵的相似度视觉,再借广播机制把标量偏置安静地加到每一个像素上,最后,SVD揭示数据本质维度,条件数警告数值脆弱点,梯度指明下坡路,凸优化确保路走得通。每一个概念都不是孤岛,而是被“变换”这根线串起来的必经站点。
再回头看,为什么每个机器学习模型都离不开线性代数和微积分?因为模型在做的所有事情——学习、预测、压缩、生成——最后都要转化为对高维数组的一系列可微操作。线性代数负责让这些操作在矩阵和张量层面高效运行,微积分负责给出每一步动作的修正方向和幅度。没有线性代数,数据就是一堆零散的像素;没有微积分,模型就停在原地不知道该变哪里。两条腿缺一条,就只剩下一套再漂亮也动不了的数学骨架。
热门跟贴