为「一堆数字」准备的四个词
如果「线性代数」这个词让你肩膀一紧,那就放松下来吧。本篇里几乎所有内容,都不过是把数字装进盒子,再约定好搬动它们的规则而已。机器学习背后的数学,大体上就是这件事,只是在极大的盒子上飞快地重复。我们先给这些盒子起名字:标量、向量、矩阵、张量这四个名字,说穿了就是在回答一句话——「我手上的数字有几个方向?」
标量(scalar)就是单独一个数,孤零零的:21 度的气温、4.99 的价格、一个学习率。向量(vector)是按固定顺序排好的一串数,比如 `[21, 4.99, 0]`——一个带有几个坐标的点。矩阵(matrix)是一格一格排成行与列的数字网格,像一张电子表格。而张量(tensor)只是个总称,指「任意维数的数字阵列」——标量是 0 维张量、向量是 1 维张量、矩阵是 2 维张量,往上还能继续堆。
来看一个具体的阶梯。一个灰度像素是标量。一行像素是向量。一整张灰度图是矩阵。彩色图像再加上红、绿、蓝三个通道——它就成了 3 维张量。而你一次喂给模型的一批 64 张彩色图,就是 4 维张量。这里没发生什么神秘的事;你只是不断地「再加一个可以沿着数下去的方向」罢了。
向量既是一支箭,也是一个地址
同时握住关于向量的两幅图,会很有帮助。第一幅图:向量是一支从原点指向空间中某一点的箭头——`[3, 2]` 就是「向右走 3,向上走 2」。第二幅图:向量是一个地址,是一串能精确钉住唯一一个位置的坐标。它们是同一件事的两种看法,哪幅图更顺手取决于当下。箭头让方向和长度变得真切;地址则让它成为计算机能存下的东西。
两个日常操作几乎是白送的。相加两个向量,就是逐格相加:`[3, 2] + [1, 4] = [4, 6]`——把第二支箭的尾巴接到第一支箭的头上,看你最后落在哪里。用一个数去缩放,会把整支箭拉长或缩短:`2 × [3, 2] = [6, 4]`,方向不变,长度翻倍。箭头的长度有它自己的名字,叫向量范数(norm),它回答的是「这东西有多大?」——每当你要衡量一个预测偏离了多远时,都会再遇到这个问题。
下面这一跃,正是向量对机器学习至关重要的原因:没有任何规定说你必须止步于 2 个或 3 个方向。一个向量可以活在 10 维、768 维,乃至 4096 维里。你没法在脑中想象一支 768 维的箭,这没关系——没人能。你保留那些规则(逐格相加、每格缩放、测量长度),而悄悄丢掉那幅心像。正是这一步,让同样朴素的算术能去描述一个点、一句话,或一张脸。
点积:两样东西有多「合拍」?
两个向量之间最重要的一个操作,就是点积(dot product)。它的配方简单到近乎让人小看:把对应的格子相乘,再全部加起来。对于 `[1, 2, 3]` 和 `[4, 0, 5]`,就是 `1×4 + 2×0 + 3×5 = 4 + 0 + 15 = 19`。两个向量进去,一个标量出来。正是这种「坍缩」——把许多数字压成一个——让它无处不在。
可这一个数到底意味着什么?把它想成一个「合拍程度」的分数。当两支箭指向同一方向时,它们的点积又大又正。当它们成直角——彻底毫不相关——时,点积为零。当它们指向相反方向时,点积变负。于是点积悄悄衡量的是:「这两个向量在多大程度上朝同一方向使劲,并按各自的长度加权?」这正是为什么它会化身为「相似度分数」,出现在你的搜索词嵌入向量(embedding)与每篇文档的嵌入向量之间,或两个词义之间。
这也是单个人工神经元的心跳。一个神经元把它的输入当作一个向量、把它的权重(weight)当作另一个向量,取两者的点积,得到的这一个数就是神经元在决定要不要「激活」之前的原始反应。每当你听说某个网络在做「数十亿次乘加运算」,它做的正是这件事——成卡车成卡车的点积。把这一个操作吃透,后面相当大一部分内容就不再吓人了。
def dot(a, b):
total = 0
for i in range(len(a)): # walk both lists together
total += a[i] * b[i] # multiply matching slots, add up
return total
dot([1, 2, 3], [4, 0, 5]) # -> 19 (one number out)矩阵乘法,无非是许多个点积
如果说点积是两个向量之间的一次握手,那么矩阵乘法(matrix multiplication)就是一整屋子的握手一气呵成。要把两个矩阵相乘,你把第一个矩阵的每一行,与第二个矩阵的每一列做点积。每一个这样的点积,就成为结果中的一个元素。这就是全部规则——底下没有藏着别的魔法。
有一条礼仪规则,几乎绊倒每一个初学者:内侧的尺寸必须相等。一个 3×2 的矩阵,只能去乘一个有 2 行的矩阵,因为第一个矩阵的每一行(长度为 2)必须与第二个矩阵的每一列(长度为 2)逐格对齐。结果的尺寸则取外侧的两个数:3×2 乘以 2×4,得到 3×4。如果那两个内侧的数对不上,乘法就根本没有定义——而「形状不匹配」的报错,将是你在这个领域里最常撞见的一个 bug。
理解矩阵乘法更深一层的方式,是把它看作一种变换(transformation):矩阵是一台机器,吃进一个向量,吐出一个(通常不一样的)向量。喂给它一个点,它就把这个点旋转、拉伸、压扁,或投影到一个新位置。神经网络里的一整层做的恰恰是这件事——它用一个权重矩阵去乘你的输入向量,把这些输入重新组合成一组新的特征,其中每一个输出,都是全部输入各自的一份加权混合。矩阵乘法,本质上就是「按选定的比例把东西掺和到一起」这个操作。
为什么一切都变成了向量
模型没法直接拿「猫」这个词或一张日落照片做任何事。它只懂得把数字相乘、相加。所以第一件事永远是:把那个杂乱的真实事物,变成一个数字向量——一串模型能在上面运算的列表。你在前面的阶梯里已经认识了特征(feature)这个概念:向量的每一格,都是这个事物的一项可测量的属性。一座房子可能变成 `[面积, 卧室数, 房龄, 到学校距离]`;就数学而言,这一个向量就是这座房子。
而美妙的回报是几何性的。一旦一切都成了向量,「相似的东西」就变成「彼此挨得很近的点」,「这个和那个不一样」就变成「这些箭指向不同的方向」——而你现在知道,这不过就是个点积。学习得到的嵌入向量对词语和图像做的正是这件事:它把它们安放进一个高维空间,使得「意义」化为了「距离与方向」。搜索、推荐、聚类、比对,最后全都归结为在向量之间做测量。
把工具箱合起来
让我们追踪一次极小的「模型走一遍」,看看这些零件如何咬合到一起。无论你跑的是线性回归,还是一个庞大的网络,它的形状都一样,只是规模不同。
- 把输入编码成一个向量——每一格都是这个样本的一项特征(那座房子、那块像素、那个词)。
- 用一个权重矩阵去乘这个向量——每个输出对应一个点积,把输入重新组合成新的特征。
- 把得到的结果向量读作模型的回答——一个分数、一个预测,或是送给下一层的输入。
- 把许多个这样的步骤叠起来,你就得到了一个深层网络;矩阵里的那些数字,正是训练所要学习的东西。
这真的就是你读懂这条阶梯余下部分所需的、绝大部分的线性代数了。数据变成向量;向量被排进矩阵与张量;点积衡量合拍程度;矩阵乘法负责重组与变换;而一个模型,就是这些乘法叠成的高高一摞,其中的数字由训练来调校。要跟上后面的内容,你不需要证明,也不需要什么巧妙的技巧——你需要的是这寥寥几招,松松地握着,常常地用着。