第 5 章 从向量、矩阵到批处理与形状
5.1 同一种计算,怎样处理许多个文本位置?
Section titled “5.1 同一种计算,怎样处理许多个文本位置?”第 3 章用 Token ID 查出一行数值。在那张教学表里,ID 4 对应 ␠France,查出的向量是 [2,1]。空格标记属于文本片段的拼写,不是多出来的一个坐标。这两个坐标也没有天然的含义,不能把其中一项直接叫作“国籍”或“重要程度”;它们只是为这一行给定的两个数。
解释查表,一行就够了。处理一句话需要许多行;训练一步,通常还要处理许多句话。怎样既保留这些行各自的身份,又对它们使用同一套计算?我们的标准不能只是“程序能运行”:对于任意一个结果,都应能指出它在哪里、由哪些输入算出,以及每一维代表什么。
先把同一样本中不同位置的向量竖着排起来。下面从原有表格中取两个短片段。它们用来演示计算,并不是两句完整的法国相关陈述。
表 5.1 — 从 ID 到两张小矩阵。 所有向量都沿用第 3 章手工给定的无量纲教学数值,下标从 0 开始。
| 样本 | 位置 | Token ID 与文本片段 | 坐标 0 | 坐标 1 |
|---|---|---|---|---|
| 0 | 0 | 4,␠France | 2 | 1 |
| 0 | 1 | 5,␠is | 1 | −1 |
| 1 | 0 | 6,␠Paris | 2 | 2 |
| 1 | 1 | 7,. | 0 | −1 |
样本 0 的两行可以写成 。这样一张长方形数表叫作 矩阵(matrix)。它有两行两列,因此 形状(shape) 是 [2,2]。在这个例子中,第一维数文本位置,第二维数坐标。形状按约定的顺序记录各维有多长,却不会告诉我们行和列各自是什么意思。
再把两张矩阵按顺序放在一起,组成一个 批次(batch),也就是一次共同处理的一组样本。整个输入记作 ,其中 。它的形状是 [2,2,2]:两个样本,每个样本两个位置,每个位置两个坐标。用 [B,T,C] 来标记更容易看出职责: 是批次中的样本数, 是每个样本在数组中存放的位置数, 是每个位置的坐标数。这几个字母是约定的名称,不是运算。
怎样读 ?先找样本 1,再找它的位置 0,也就是 ␠Paris,最后取坐标 1。这个数组一共存了 个数。相比之下,ID 数组的形状是 [B,T]=[2,2],里面只有四个标签。坐标这一维是查表后加上的,跟 ID 本身的数值大小没有关系。机器学习软件常把具有任意维数的数值数组叫作 张量(tensor)。本章不需要因此再学一套运算:向量有一维,矩阵有两维,而这批输入有三维。
5.2 先算一个输出格,再算整张表
Section titled “5.2 先算一个输出格,再算整张表”假设每个输入向量都要产生三项分数,候选顺序固定为 巴黎、里昂、其他。“其他”在这个教学算例中合并了剩余候选;这里既不是完整词表,也不是训练好的预测器。输入有两个坐标,因此每项分数都要给定两个相乘的系数。
例 5.1 — 一行输入变成三项分数。 输入是 ,三列系数依次为 、、。让对应坐标相乘,再相加:第一项是 ;第二项是 ;第三项是 。分数可以是负数,因为我们还没有把它变成概率。
每一次这样的“对应相乘再相加”,就是第 3 章用过的 点积(dot product)。对于两个长度同为 的数列 和 ,可以写成
求和符号表示“每个坐标各算一个乘积,然后全部加起来”。下标 从两边选出相同位置的坐标;编号从 0 开始,所以最后一个下标是 。代入 和 ,公式就展开成刚才那两项:。长度相同只是允许我们进行这项运算,对应位置的含义和单位还必须匹配。
把三列系数并排放好:
的形状是 [C,K]=[2,3], 表示输出分数的通道数。它的行对应输入坐标,列对应候选。把每一列的计算规则依次用于所有输入行,就是 矩阵乘法(matrix multiplication,常简写为 MatMul)。样本 0 的完整结果如下:
检查右下角那一格:它取第二行输入和第三列系数,因此是 。这项运算不是把整行逐项相乘后留下两个结果,而是沿坐标这一维相乘、求和,最后每行得到三项分数。
表 5.2 — 整个批次的输出,连同候选顺序一起列出。
| 样本 | 位置 | 巴黎分数 | 里昂分数 | 其他分数 |
|---|---|---|---|---|
| 0 | 0 | 2 | 1 | −1 |
| 0 | 1 | 1 | −1 | −2 |
| 1 | 0 | 2 | 2 | 0 |
| 1 | 1 | 0 | −1 | −1 |
本例的批量计算可以概括为
选样本, 选位置, 选输出候选;只有 参与求和。例如 ,它就是 ␠Paris 这一行的第三项分数。@ 是配套 Python 练习中的矩阵乘法运算符。每个样本、每个位置都复用同一个 ;批处理不会为每句话另造一张参数矩阵,也不会让一个样本读到另一个样本的内容。NumPy 的 matmul 把最后两维当作矩阵的维度,并支持成批计算;本例右侧使用的是一张共享的二维矩阵。NumPy:matmul
- 一组数字样本 0:[2,1];[1,−1]
- 一组数字样本 1:[2,2];[0,−1]
先选样本,再选位置,最后选坐标。
- 模型保存的数值各行:[1,0,−1];[0,1,1]
- 处理步骤第一行:2×[1,0,−1] + 1×[0,1,1]
只沿 C 维相乘求和,不把不同样本或位置混在一起。
- 一组数字样本 0:[2,1,−1];[1,−1,−2]
- 一组数字样本 1:[2,2,0];[0,−1,−1]
K 对应有固定顺序的候选:巴黎、里昂、其他。这些分数还不是概率。
图 5.1 把被求和消去的坐标维,与保留下来的样本维、位置维分开画出。沿着第一行输入,就能追到中间的计算和最后的输出。双边框标出给定的参数数值,并不表示这些数已经通过训练学得。
5.3 调换行列,不等于随便换个形状
Section titled “5.3 调换行列,不等于随便换个形状”刚才把每组系数写成列,是为了让输入的一行依次与各列相乘。另一个文件也可能把每个候选的系数存成一行。这时需要交换行和列的职责,同时保证每个数仍属于原来的那对坐标。这个操作叫作 转置(transpose):
这里右上角的 表示转置,与 [B,T,C] 中表示位置数的 不是一回事。原来的形状 [2,3] 变为 [3,2]。例如,第 0 行第 2 列的 −1 移到第 2 行第 0 列。再转置一次,就回到原表。
借助转置,还可以一次比较所有输入行与一组参照向量。假设矩阵 把 个参照向量存成行,形状为 [K,C],那么 的形状就是 [T,K]。其中第 格,是输入行 与参照行 的点积。转置只负责让坐标维对齐,不会替我们确认参照向量是否用了正确的词表或计量方式。
数组有三维时,调换维度就必须说清楚换的是哪两维。把 [B,T,C] 改排为 [T,B,C],意味着交换样本和位置的职责。本例中这两维恰好都长 2,打印出的形状仍然是 [2,2,2],但 [0,1] 处的向量已经从 [1,−1] 变成 [2,2]。只检查形状,发现不了这个错误。 重塑形状(reshape) 则是把已有数值序列按新的维度长度重新分组,通常不能代替这种轴的调换。NumPy 练习既检查大小,也检查具体数值;转置的官方说明明确规定了各维如何对应。NumPy:transpose
5.4 相似度为什么要除以长度?
Section titled “5.4 相似度为什么要除以长度?”点积大,可能是因为方向接近,也可能是因为向量本身长,还可能两者都有。第 3 章用两个长度作分母,把比较集中到夹角上。现在用刚才的向量,把分母也逐项算出来。
例 5.2 — 比较两个方向。 取 ␠France 的 和 ␠Paris 的 。点积是 。 的长度为 , 的长度为 。先把各坐标的平方相加,再开平方,得到向量的长度。因此,余弦相似度为
两边的竖线表示向量长度,不是词数。如果把第二个向量换成 ,那么 ,,余弦便是 。在这套给定坐标下,第一对的方向更接近。计算本身却不能证明两个词等价;法国和巴黎甚至不是同一类事物。
再把 乘以 10。点积变为 60,它的长度也变为 ,余弦保持不变。如果任务不在乎整体大小,这个性质很有用;如果大小恰好携带了所需信息,余弦就会丢掉这部分区别。对于 [0,0],长度为零,分母也为零,夹角没有定义。配套通用计算函数在这种情况下返回 None。检索应用应另行决定是否放弃给出结果,不能悄悄把一个没有定义的角度当成语义一致的证据。
5.5 存储补齐了,不代表文本变长了
Section titled “5.5 存储补齐了,不代表文本变长了”真实样本往往长短不一。现在把样本 1 缩短为只有 ␠Paris,样本 0 保持原样。长方形数组仍然需要为样本 1 留出第二行。一种存储约定是在那里放入零向量,并另外记录真实长度 [2,1]。插入的这一行叫作 补齐项(padding)。
例 5.3 — 多出的一行,怎样改变平均值? 样本 1 现在存为 [[2,2],[0,0]]。真正的文本只有一行,所以平均表示是 。如果把存储的两行相加后除以 2,就会得到 [1,1]。在这个例子里,零向量没有改变总和,错误的分母却把结果减半了。如果两个样本原本应有相同权重,这种做法可能改变它们对后续计算的相对贡献。
补齐行用于安排存储,不是第 3 章的未知 Token 行,尽管两者碰巧都是零向量。未知 Token 仍占据一个真实的文本位置,补齐项则不占。零输入经过计算后也未必还是零:若运算是 ,加上的向量 不为零,那么补齐行也会输出 。因此,“已经补了零”并不足以说明平均或损失计算会排除补齐项。还要保存长度或有效位置标记,并在需要的计算中使用它们。完整的注意力掩码机制留到第 16 章;这里先分清:存储槽位数与真实观测数可以不同。
表 5.3 — 形状匹配也拦不住的三种错误。
| 没变的形状 | 变了的含义 | 能观察到的后果 |
|---|---|---|
输出 [2,2,3] | 使用方把候选顺序读成里昂、巴黎、其他 | 第一行的分数 2 被归给了错误候选 |
输入 [2,2,2] | 样本维与位置维交换 | 后续计算把不同位置或不同样本混在一起 |
| 两个长度为 2 的向量 | 一个来源用元,另一个用百元计量 | 若未同步换算坐标与系数,点积就混用了尺度 |
这三种问题要分别处理:给候选映射标明版本,明确并检验每一维,或者附上单位并进行换算。再加一条形状检查,并不能解决任何一种。数组让数值运算更紧凑,数据契约才说明这些数是什么意思。
5.6 先补 Python 基础,再做练习
Section titled “5.6 先补 Python 基础,再做练习”读到这里,不会编程也已经完成了本章核心内容。实践顺序固定为: Python 与 Notebook 基础 → 第 4 章参数化字符 N-gram Notebook → NumPy 形状练习 → 第 7 章训练实践。Primer 会补上阅读这些文件所需的语法。第 4 章的纸笔论证与 MG-0 项目里程碑仍然不依赖 Python。
配套文件是 notebooks/part-ii/01-primer-and-ngram.ipynb、02-shapes-and-loss.ipynb 和 code/part-ii/shape_practice.py。它们读取已有的 Part I 数据及 data/part-ii/arithmetic.json。形状练习得到 [2,2,2] @ [2,3] → [2,2,3],完整输出与表 5.2 相同;它还会交换前两维,展示大小相同、内容却不同的情况。这些检查验证的是约定好的计算,不是模型质量。具体运行环境和保存的结果见配套实践记录。
5.7 本章小结
Section titled “5.7 本章小结”批次在位置维、坐标维之外增加了样本维。矩阵乘法让一行输入与一列系数做点积,沿共同的坐标维求和,形成一个输出格。转置会连同各维的职责一起移动数值,不能只理解为改写形状。余弦用长度作分母,消去正数倍缩放的影响,但零向量没有定义好的余弦。补齐让存储变成长方形,却没有增加真实观测。
现在,我们能为每个样本的每个位置算出一组分数。分数可以为负,总和也不必是 1。第 6 章将回答:怎样把它们变成概率,再变成可以度量的预测误差?
5.8 习题
Section titled “5.8 习题”- 换一张输入表。 取 ,仍使用例 5.1 的 ,算出全部六个输出格。如果 是一个样本, 各是多少?加上批次维后,输出形状是什么?
- 修复候选顺序。 输出
[2,1,−1]的顺序是巴黎、里昂、其他,但使用方预期的顺序是里昂、巴黎、其他。转置整个批次能修复这个接口吗?写出正确重排后的向量,并解释原来的形状检查为什么通过。 - 选择分母。 两个真实位置分别为
[2,0]和[0,2],第三个存储位置是补齐项[0,0]。分别按真实位置数与存储位置数求平均。要区分这两种情况,还需要什么信息? - 检验相似度结论。 将
[2,1]分别与[20,10]、[0,0]比较。有人说,第一个结果证明两个向量所代表的文本可以互换。计算实际支持哪些结论? - 检查维度调换。 对表 5.1 交换 与 后,写出第一维下标为 0 的那一片中的两个向量。为什么只检查
[2,2,2]会漏掉这个变化?
5.9 参考解答
Section titled “5.9 参考解答”- 第 0 行依次得到 、、;第 1 行得到 0、−1、−1。所以 。加上样本维后,,输出为
[1,2,3]。被求和消去的那个 2 数的是输入坐标,不是样本。 - 应当在候选这一维内重排为
[1,2,−1],并更新映射契约。转置交换的是维度,修复这里的问题则要交换同一维内的候选位置。两套候选表都长 3,所以形状检查无法区分。 - 总和是
[2,2]。除以两个真实位置得到[1,1];除以三个存储位置得到[2/3,2/3]。真实长度 2,或有效位置列表[true,true,false],都能补上所缺的信息。仅凭零向量,不能判断它是真实数据还是补齐项。 - 第一对的点积为 50,长度分别为 与 ,余弦为 1。这说明它们在当前表示中方向相同、大小不同。“文本能否互换”是另一项任务判断。与零向量比较时分母为零,余弦没有定义,不能据此给出第二个相似度结论。
- 调换后的第一片包含原来的 和 ,也就是两个样本各自的位置 0。原来的第一片则包含样本 0 的两个位置。因为 ,大小仍然相同;必须检查具体元素或维度契约,才能发现混淆。
5.10 参考文献
Section titled “5.10 参考文献”- NumPy:
matmul。说明矩阵与成批数组相乘、维度匹配以及练习所用的@运算。本章完整数值算例由本书原创。 - NumPy:
transpose。规定各维如何重排,也解释了为什么一维数组转置后不会自动变成列矩阵。实践单独固定依赖版本,与持续维护的在线文档版本分开记录。