跳转到内容

第 5 章 从向量、矩阵到批处理与形状

5.1 同一种计算,怎样处理许多个文本位置?

Section titled “5.1 同一种计算,怎样处理许多个文本位置?”

第 3 章用 Token ID 查出一行数值。在那张教学表里,ID 4 对应 ␠France,查出的向量是 [2,1]。空格标记属于文本片段的拼写,不是多出来的一个坐标。这两个坐标也没有天然的含义,不能把其中一项直接叫作“国籍”或“重要程度”;它们只是为这一行给定的两个数。

解释查表,一行就够了。处理一句话需要许多行;训练一步,通常还要处理许多句话。怎样既保留这些行各自的身份,又对它们使用同一套计算?我们的标准不能只是“程序能运行”:对于任意一个结果,都应能指出它在哪里、由哪些输入算出,以及每一维代表什么。

先把同一样本中不同位置的向量竖着排起来。下面从原有表格中取两个短片段。它们用来演示计算,并不是两句完整的法国相关陈述。

表 5.1 — 从 ID 到两张小矩阵。 所有向量都沿用第 3 章手工给定的无量纲教学数值,下标从 0 开始。

样本位置Token ID 与文本片段坐标 0坐标 1
004,␠France21
015,␠is1−1
106,␠Paris22
117,.0−1

样本 0 的两行可以写成 X0=[211−1]X_0=\begin{bmatrix}2&1\\1&-1\end{bmatrix}。这样一张长方形数表叫作 矩阵(matrix)。它有两行两列,因此 形状(shape) 是 [2,2]。在这个例子中,第一维数文本位置,第二维数坐标。形状按约定的顺序记录各维有多长,却不会告诉我们行和列各自是什么意思。

再把两张矩阵按顺序放在一起,组成一个 批次(batch),也就是一次共同处理的一组样本。整个输入记作 X=[X0,X1]X=[X_0,X_1],其中 X1=[220−1]X_1=\begin{bmatrix}2&2\\0&-1\end{bmatrix}。它的形状是 [2,2,2]:两个样本,每个样本两个位置,每个位置两个坐标。用 [B,T,C] 来标记更容易看出职责:BB 是批次中的样本数,TT 是每个样本在数组中存放的位置数,CC 是每个位置的坐标数。这几个字母是约定的名称,不是运算。

怎样读 X1,0,1=2X_{1,0,1}=2?先找样本 1,再找它的位置 0,也就是 ␠Paris,最后取坐标 1。这个数组一共存了 B×T×C=8B\times T\times C=8 个数。相比之下,ID 数组的形状是 [B,T]=[2,2],里面只有四个标签。坐标这一维是查表后加上的,跟 ID 本身的数值大小没有关系。机器学习软件常把具有任意维数的数值数组叫作 张量(tensor)。本章不需要因此再学一套运算:向量有一维,矩阵有两维,而这批输入有三维。

5.2 先算一个输出格,再算整张表

Section titled “5.2 先算一个输出格,再算整张表”

假设每个输入向量都要产生三项分数,候选顺序固定为 巴黎、里昂、其他。“其他”在这个教学算例中合并了剩余候选;这里既不是完整词表,也不是训练好的预测器。输入有两个坐标,因此每项分数都要给定两个相乘的系数。

例 5.1 — 一行输入变成三项分数。 输入是 x=[2,1]x=[2,1],三列系数依次为 [1,0][1,0]、[0,1][0,1]、[−1,1][-1,1]。让对应坐标相乘,再相加:第一项是 2×1+1×0=22\times1+1\times0=2;第二项是 2×0+1×1=12\times0+1\times1=1;第三项是 2×(−1)+1×1=−12\times(-1)+1\times1=-1。分数可以是负数,因为我们还没有把它变成概率。

每一次这样的“对应相乘再相加”,就是第 3 章用过的 点积(dot product)。对于两个长度同为 CC 的数列 xx 和 vv,可以写成

x⋅v=∑c=0C−1xcvc.x\cdot v=\sum_{c=0}^{C-1}x_c v_c.

求和符号表示“每个坐标各算一个乘积,然后全部加起来”。下标 cc 从两边选出相同位置的坐标;编号从 0 开始,所以最后一个下标是 C−1C-1。代入 C=2C=2 和 v=[−1,1]v=[-1,1],公式就展开成刚才那两项:2(−1)+1(1)=−12(-1)+1(1)=-1。长度相同只是允许我们进行这项运算,对应位置的含义和单位还必须匹配。

把三列系数并排放好:

W=[10−1011].W=\begin{bmatrix}1&0&-1\\0&1&1\end{bmatrix}.

WW 的形状是 [C,K]=[2,3],KK 表示输出分数的通道数。它的行对应输入坐标,列对应候选。把每一列的计算规则依次用于所有输入行,就是 矩阵乘法(matrix multiplication,常简写为 MatMul)。样本 0 的完整结果如下:

X0W=[211−1][10−1011]=[21−11−1−2].X_0W= \begin{bmatrix}2&1\\1&-1\end{bmatrix} \begin{bmatrix}1&0&-1\\0&1&1\end{bmatrix} = \begin{bmatrix}2&1&-1\\1&-1&-2\end{bmatrix}.

检查右下角那一格:它取第二行输入和第三列系数,因此是 1(−1)+(−1)(1)=−21(-1)+(-1)(1)=-2。这项运算不是把整行逐项相乘后留下两个结果,而是沿坐标这一维相乘、求和,最后每行得到三项分数。

表 5.2 — 整个批次的输出,连同候选顺序一起列出。

样本位置巴黎分数里昂分数其他分数
0021−1
011−1−2
10220
110−1−1

本例的批量计算可以概括为

Zb,t,k=∑c=0C−1Xb,t,cWc,k,[B,T,C] @ [C,K] ⟶ [B,T,K].Z_{b,t,k}=\sum_{c=0}^{C-1}X_{b,t,c}W_{c,k},\qquad [B,T,C]\ @\ [C,K]\ \longrightarrow\ [B,T,K].

bb 选样本,tt 选位置,kk 选输出候选;只有 cc 参与求和。例如 Z1,0,2=2(−1)+2(1)=0Z_{1,0,2}=2(-1)+2(1)=0,它就是 ␠Paris 这一行的第三项分数。@ 是配套 Python 练习中的矩阵乘法运算符。每个样本、每个位置都复用同一个 WW;批处理不会为每句话另造一张参数矩阵,也不会让一个样本读到另一个样本的内容。NumPy 的 matmul 把最后两维当作矩阵的维度,并支持成批计算;本例右侧使用的是一张共享的二维矩阵。NumPy:matmul

输入 X:[B,T,C] = [2,2,2]
  1. 一组数字样本 0:[2,1];[1,−1]
  2. 一组数字样本 1:[2,2];[0,−1]

先选样本,再选位置,最后选坐标。

共享 W:[C,K] = [2,3]
  1. 模型保存的数值各行:[1,0,−1];[0,1,1]
  2. 处理步骤第一行:2×[1,0,−1] + 1×[0,1,1]

只沿 C 维相乘求和,不把不同样本或位置混在一起。

输出 Z:[B,T,K] = [2,2,3]
  1. 一组数字样本 0:[2,1,−1];[1,−1,−2]
  2. 一组数字样本 1:[2,2,0];[0,−1,−1]

K 对应有固定顺序的候选:巴黎、里昂、其他。这些分数还不是概率。

图 5.1 — 同一张矩阵把坐标变成分数通道;样本与位置仍各自保留。 本书原创图示与教学数值,CC BY-SA 4.0。

图 5.1 把被求和消去的坐标维,与保留下来的样本维、位置维分开画出。沿着第一行输入,就能追到中间的计算和最后的输出。双边框标出给定的参数数值,并不表示这些数已经通过训练学得。

5.3 调换行列,不等于随便换个形状

Section titled “5.3 调换行列,不等于随便换个形状”

刚才把每组系数写成列,是为了让输入的一行依次与各列相乘。另一个文件也可能把每个候选的系数存成一行。这时需要交换行和列的职责,同时保证每个数仍属于原来的那对坐标。这个操作叫作 转置(transpose):

WT=[1001−11],(WT)k,c=Wc,k.W^{\mathsf T}=\begin{bmatrix}1&0\\0&1\\-1&1\end{bmatrix}, \qquad (W^{\mathsf T})_{k,c}=W_{c,k}.

这里右上角的 T\mathsf T 表示转置,与 [B,T,C] 中表示位置数的 TT 不是一回事。原来的形状 [2,3] 变为 [3,2]。例如,第 0 行第 2 列的 −1 移到第 2 行第 0 列。再转置一次,就回到原表。

借助转置,还可以一次比较所有输入行与一组参照向量。假设矩阵 RR 把 KK 个参照向量存成行,形状为 [K,C],那么 X0RTX_0R^{\mathsf T} 的形状就是 [T,K]。其中第 (t,k)(t,k) 格,是输入行 tt 与参照行 kk 的点积。转置只负责让坐标维对齐,不会替我们确认参照向量是否用了正确的词表或计量方式。

数组有三维时,调换维度就必须说清楚换的是哪两维。把 [B,T,C] 改排为 [T,B,C],意味着交换样本和位置的职责。本例中这两维恰好都长 2,打印出的形状仍然是 [2,2,2],但 [0,1] 处的向量已经从 [1,−1] 变成 [2,2]。只检查形状,发现不了这个错误。 重塑形状(reshape) 则是把已有数值序列按新的维度长度重新分组,通常不能代替这种轴的调换。NumPy 练习既检查大小,也检查具体数值;转置的官方说明明确规定了各维如何对应。NumPy:transpose

点积大,可能是因为方向接近,也可能是因为向量本身长,还可能两者都有。第 3 章用两个长度作分母,把比较集中到夹角上。现在用刚才的向量,把分母也逐项算出来。

例 5.2 — 比较两个方向。 取 ␠France 的 u=[2,1]u=[2,1] 和 ␠Paris 的 v=[2,2]v=[2,2]。点积是 2(2)+1(2)=62(2)+1(2)=6。uu 的长度为 22+12=5\sqrt{2^2+1^2}=\sqrt5,vv 的长度为 22+22=8\sqrt{2^2+2^2}=\sqrt8。先把各坐标的平方相加,再开平方,得到向量的长度。因此,余弦相似度为

cos⁡(u,v)=u⋅v∥u∥∥v∥=658=640≈0.948683.\cos(u,v)=\frac{u\cdot v}{\|u\|\|v\|} =\frac6{\sqrt5\sqrt8} =\frac6{\sqrt{40}}\approx0.948683.

两边的竖线表示向量长度,不是词数。如果把第二个向量换成 r=[1,−1]r=[1,-1],那么 u⋅r=1u\cdot r=1,∥r∥=2\|r\|=\sqrt2,余弦便是 1/10≈0.3162281/\sqrt{10}\approx0.316228。在这套给定坐标下,第一对的方向更接近。计算本身却不能证明两个词等价;法国和巴黎甚至不是同一类事物。

再把 vv 乘以 10。点积变为 60,它的长度也变为 10810\sqrt8,余弦保持不变。如果任务不在乎整体大小,这个性质很有用;如果大小恰好携带了所需信息,余弦就会丢掉这部分区别。对于 [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,2/1]=[2,2][2/1,2/1]=[2,2]。如果把存储的两行相加后除以 2,就会得到 [1,1]。在这个例子里,零向量没有改变总和,错误的分母却把结果减半了。如果两个样本原本应有相同权重,这种做法可能改变它们对后续计算的相对贡献。

补齐行用于安排存储,不是第 3 章的未知 Token 行,尽管两者碰巧都是零向量。未知 Token 仍占据一个真实的文本位置,补齐项则不占。零输入经过计算后也未必还是零:若运算是 xW+bxW+b,加上的向量 bb 不为零,那么补齐行也会输出 bb。因此,“已经补了零”并不足以说明平均或损失计算会排除补齐项。还要保存长度或有效位置标记,并在需要的计算中使用它们。完整的注意力掩码机制留到第 16 章;这里先分清:存储槽位数与真实观测数可以不同。

表 5.3 — 形状匹配也拦不住的三种错误。

没变的形状变了的含义能观察到的后果
输出 [2,2,3]使用方把候选顺序读成里昂、巴黎、其他第一行的分数 2 被归给了错误候选
输入 [2,2,2]样本维与位置维交换后续计算把不同位置或不同样本混在一起
两个长度为 2 的向量一个来源用元,另一个用百元计量若未同步换算坐标与系数,点积就混用了尺度

这三种问题要分别处理:给候选映射标明版本,明确并检验每一维,或者附上单位并进行换算。再加一条形状检查,并不能解决任何一种。数组让数值运算更紧凑,数据契约才说明这些数是什么意思。

读到这里,不会编程也已经完成了本章核心内容。实践顺序固定为: 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 相同;它还会交换前两维,展示大小相同、内容却不同的情况。这些检查验证的是约定好的计算,不是模型质量。具体运行环境和保存的结果见配套实践记录。

批次在位置维、坐标维之外增加了样本维。矩阵乘法让一行输入与一列系数做点积,沿共同的坐标维求和,形成一个输出格。转置会连同各维的职责一起移动数值,不能只理解为改写形状。余弦用长度作分母,消去正数倍缩放的影响,但零向量没有定义好的余弦。补齐让存储变成长方形,却没有增加真实观测。

现在,我们能为每个样本的每个位置算出一组分数。分数可以为负,总和也不必是 1。第 6 章将回答:怎样把它们变成概率,再变成可以度量的预测误差?

  1. 换一张输入表。 取 A=[120−1]A=\begin{bmatrix}1&2\\0&-1\end{bmatrix},仍使用例 5.1 的 WW,算出全部六个输出格。如果 AA 是一个样本,B,T,C,KB,T,C,K 各是多少?加上批次维后,输出形状是什么?
  2. 修复候选顺序。 输出 [2,1,−1] 的顺序是巴黎、里昂、其他,但使用方预期的顺序是里昂、巴黎、其他。转置整个批次能修复这个接口吗?写出正确重排后的向量,并解释原来的形状检查为什么通过。
  3. 选择分母。 两个真实位置分别为 [2,0] 和 [0,2],第三个存储位置是补齐项 [0,0]。分别按真实位置数与存储位置数求平均。要区分这两种情况,还需要什么信息?
  4. 检验相似度结论。 将 [2,1] 分别与 [20,10]、[0,0] 比较。有人说,第一个结果证明两个向量所代表的文本可以互换。计算实际支持哪些结论?
  5. 检查维度调换。 对表 5.1 交换 BB 与 TT 后,写出第一维下标为 0 的那一片中的两个向量。为什么只检查 [2,2,2] 会漏掉这个变化?
  1. 第 0 行依次得到 1(1)+2(0)=11(1)+2(0)=1、1(0)+2(1)=21(0)+2(1)=2、1(−1)+2(1)=11(-1)+2(1)=1;第 1 行得到 0、−1、−1。所以 AW=[1210−1−1]AW=\begin{bmatrix}1&2&1\\0&-1&-1\end{bmatrix}。加上样本维后,B=1,T=2,C=2,K=3B=1,T=2,C=2,K=3,输出为 [1,2,3]。被求和消去的那个 2 数的是输入坐标,不是样本。
  2. 应当在候选这一维内重排为 [1,2,−1],并更新映射契约。转置交换的是维度,修复这里的问题则要交换同一维内的候选位置。两套候选表都长 3,所以形状检查无法区分。
  3. 总和是 [2,2]。除以两个真实位置得到 [1,1];除以三个存储位置得到 [2/3,2/3]。真实长度 2,或有效位置列表 [true,true,false],都能补上所缺的信息。仅凭零向量,不能判断它是真实数据还是补齐项。
  4. 第一对的点积为 50,长度分别为 5\sqrt5 与 10510\sqrt5,余弦为 1。这说明它们在当前表示中方向相同、大小不同。“文本能否互换”是另一项任务判断。与零向量比较时分母为零,余弦没有定义,不能据此给出第二个相似度结论。
  5. 调换后的第一片包含原来的 X0,0=[2,1]X_{0,0}=[2,1] 和 X1,0=[2,2]X_{1,0}=[2,2],也就是两个样本各自的位置 0。原来的第一片则包含样本 0 的两个位置。因为 B=T=2B=T=2,大小仍然相同;必须检查具体元素或维度契约,才能发现混淆。
  • NumPy:matmul。说明矩阵与成批数组相乘、维度匹配以及练习所用的 @ 运算。本章完整数值算例由本书原创。
  • NumPy:transpose。规定各维如何重排,也解释了为什么一维数组转置后不会自动变成列矩阵。实践单独固定依赖版本,与持续维护的在线文档版本分开记录。