AI Hero Club

Transformer 实战 · 第 2 章

已自行标记完成0 / 12

仅保存在当前浏览器;完成标记不代表通过项目验收。

本章提纲 10 个小节
全课目录 12 章
完整课程计划 ↗
Transformer 实战 / 课程计划 ↗

CHAPTER 02 / 12建议学习 120 分钟

从概率到向量表示

一组实数打分怎样变成概率和损失,同一字符出现多次时嵌入梯度又怎样累加?

本章要解决的 4 个问题
  • 从稳定 softmax 与似然逐步推导有效 token 平均交叉熵及其 p−独热目标梯度
  • 手算三类分数的概率、损失和更新方向,并用平移不变性及中心差分独立复核
  • 把嵌入查表写成独热矩阵乘法,计算重复 token 在同一参数行上的梯度累加
  • 用对称汇总反例定位次序信息丢失,区分字符嵌入与下一章的上下文读取
课程项目 ZIP ↓

本章任务:从分数一直追到共享参数的梯度

上一章已经固定输入 X、移位目标 Y 和有效标记。本章要回答两件相连的事:一个位置输出若干实数后,怎样得到可优化的概率目标;一个字符在不同位置重复出现时,学习信号怎样共同修改同一行向量。我们会亲手推导交叉熵的分数梯度,再推导嵌入查表的反向累加。这里没有完整模型训练,最后的程序是检验这些局部机制的独立数值参考。

先修包括上一章的条件概率连乘、词表地址与矩阵形状,以及标量求导规则:指数的导数仍为指数,自然对数的导数为自变量的倒数,复合函数按链式法则相乘。向量梯度将在坐标上逐项建立。记 V 为候选 token 数,z=(z零,…,z末) 是一个位置的 logits,即未归一化实数分数;y 是真实下一 token 的编号,不是概率。

一、为什么输出分数需要归一化

分数可以为负、可以大于一,分数之和也没有约束。定义每类的非负权重为 exp(z_i),再除以所有权重之和 Z,得到 softmax 概率。由于指数为正且候选数有限,Z>0,各概率处于零到一之间且总和为一。这是选定的一种参数化,并非所有分类模型只能这样构造。它允许模型只通过改变相对分数来重新分配概率质量。

Z=∑j=0V−1ezj,pi=eziZ,∑ipi=1Z=\sum_{j=0}^{V-1}e^{z_j},\qquad p_i=\frac{e^{z_i}}{Z},\qquad \sum_i p_i=1
先定义分数 z,再定义归一化常数 Z 与概率 p。

对所有分数同时加常量 c,分子变为 exp(c)exp(z_i),分母也乘相同 exp(c),因此相消后概率不变。取 m=max(z),把分数换成 z−m。最大的指数恰为一,其余不超过一,避免直接对很大的正数求指数。平移是精确的代数等价;机器上的舍入仍存在。我们推导导数时使用原式,避免把数值实现中的 max 在相等分数处的分支误当成新模型机制。

算例一:概率、似然与稳定损失算到同一组数

为手算使用一个三类局部分布,类别零、一、二仅是这个算例的索引,不对应工程中的 UNK/BOS/EOS;真实输出宽度仍由冻结词表决定。取三类分数 [ln 2,0,−ln 2],指数依次为 [2,1,1/2],Z=7/2,所以概率为 [4/7,2/7,1/7]。真实目标设为第二类,编号 y=1。本次观察在模型下的似然是 2/7。最大化这个真实目标的概率,比只看最高概率类别是否正确提供更细的反馈;概率从十分之一提高到十分之二时,argmax 可能未变,但模型已经为真实事件分配更多质量。

定义单位置损失 ℓ=−ln p_y。负号让最大化似然等价于最小化损失,因为 ln 严格递增。在本例中 ℓ=−ln(2/7)=ln(7/2),约 1.252763。若完整文档的条件概率相乘,取负对数就变为各位置损失相加;这由对数的乘积转求和恒等式得到。将多篇文档的似然相乘则还采用了文档在参数条件下独立的模型约定,不是从字符链式分解自动得到的事实。

ℓ=−log⁡py=log⁡ ⁣(∑jezj)−zy=log⁡ ⁣(∑jezj−m)+(m−zy)\ell=-\log p_y=\log\!\left(\sum_j e^{z_j}\right)-z_y=\log\!\left(\sum_j e^{z_j-m}\right)+(m-z_y)
稳定计算直接得到损失,不必先形成很小的 p_y 再取对数。

这一区别在极端分数下很重要。若分数为 [1000,−1000] 且目标是第二类,稳定指数为 [1,exp(−2000)],后者在普通双精度实现中可能下溢为零;对零取对数会失败。但右侧直接计算得到损失约两千,仍为有限数。某个概率在机器表示中成为零,并不表示数学 softmax 在有限分数下真的给它零概率。稳定损失和稳定概率要分别检查。

二、把交叉熵的梯度逐步推出来

先固定一个分数坐标 z_k,其他分数暂时不动。Z 中只有 exp(z_k) 这一项随它变化,因此 ∂Z/∂z_k=exp(z_k)。由对数链式法则,∂ln Z/∂z_k=(1/Z)exp(z_k)=p_k。再对 −z_y 求导:k=y 时为负一,否则为零。把两项相加,就得到 p_k 减去“k 是否等于真实目标”的指示量。

∂ℓ∂zk=1Z∂Z∂zk−∂zy∂zk=pk−1[k=y]\frac{\partial\ell}{\partial z_k}=\frac{1}{Z}\frac{\partial Z}{\partial z_k}-\frac{\partial z_y}{\partial z_k}=p_k-\mathbf 1[k=y]
这里没有跳过 softmax 的分母依赖,所有类别都会收到梯度。

独热目标 e_y 是一个 V 维向量:第 y 项为一,其他项为零。于是梯度向量可写成 p−e_y。本例得到 [4/7,−5/7,1/7],其和为零。执行梯度下降 z新=z旧−η梯度时,第二类分数提高,其余下降。若 η=0.1,变化为 [−2/35,1/14,−1/70]。这只是把分数暂时当独立变量的一步,不代表实际网络中所有分数能独立更新;网络参数还需接上后续链式法则。

梯度和为零提供第一条独立检查:同时平移全部分数不改损失,所以沿全一方向的方向导数必须为零。第二条检查是中心有限差分:只把第 k 项分别加减 h,用两个损失之差除以 2h,近似该坐标导数。它有截断误差与舍入误差,h 不能一味取小。用多个 h 观察误差变化,比只看到一次接近更可信;有限计算也不替代上述解析推导。

若使用分布目标 q,而非单个正确类,交叉熵定义为 −Σ_i q_i ln p_i,要求 q_i 非负且和为一。独热 q 正好退化成本章损失。对分布形式求导,ln Z 项因 Σq=1 保留一次,结果为 p−q。这说明 p−y 这一俗写必须先解释 y 指独热或分布向量,不能把整数类别编号直接从概率向量每项相减。

三、从单位置损失回到第一章的批次

把位置记为批次文档 b 与时间 t。真实项目以 y_bt≠−100 定义有效标记 m_bt,取值零或一,N=Σ_bt m_bt>0。−100 是忽略标记,不是词表类别,必须先筛出有效目标再按类别索引计算损失,不能先访问 p_负一百再乘零。批次采用有效 token 等权平均。m 和 N 是固定数据,所以有效位置梯度为 (p_bt−独热目标)/N,忽略位置的目标损失与对应 logits 梯度为零;真实目标 EOS 二仍参与计分。

L=1N∑(b,t): ybt≠−100ℓbt,∂L∂zbtk=mbtN(pbtk−1[k=ybt])L=\frac{1}{N}\sum_{(b,t):\,y_{bt}\ne-100}\ell_{bt},\qquad \frac{\partial L}{\partial z_{btk}}=\frac{m_{bt}}{N}\left(p_{btk}-\mathbf 1[k=y_{bt}]\right)
只对有效目标进行类别索引;−100 不入词表、不计损失、不入分母,真实 EOS 二仍有效。

沿用上一章两篇短文,矩形共有六格而有效目标只有五个。若把总损失固定为十,正确均值为二,误除以六会变成六分之五乘以二,即约一点六六七。若对两篇先各求平均再等权平均,短文的每个 token 权重又与长文不同。两种训练目标都能定义,不能在日志里使用同一个“平均损失”名称而隐去分母。空有效集合必须报错,不把分母随手改成一。

算例二:同一个字符为什么只有一行参数

嵌入表 E 是 V×D 的可学习矩阵,第 i 行就是 token 编号 i 的 D 维向量。对三个输入地址 [3,5,3],取 D=2,并设 E第三行=[1,0]、E第五行=[0,2],查表后 H 的三行为 [1,0]、[0,2]、[1,0]。两个编号三使用同一参数行,没有因为出现在不同时间就复制出两份独立权重。输入地址是离散数据,不对地址本身求连续导数。

为准确看见反向累加,先选一个线性探针损失 J=Σ_t Σ_d C_td H_td,其中 C 的三行为 [1,2]、[−1,1]、[3,−1]。先算三个位置贡献:第一项一,第二项二,第三项三,总 J=6。由标量乘法求导,每个 H 的上游梯度就是对应 C。E第三行被第零和第二位置共同使用,所以梯度为 [1,2]+[3,−1]=[4,1];E第五行为 [−1,1];其他行未被读取,梯度为零。

进一步用矩阵证明。定义独热选择矩阵 S,三行分别只在编号 3、5、3 处为一(全部按从 0 起的索引),其他为零。H=SE 的坐标式为 H_td=Σ_i S_ti E_id。对 E_kd 求导时,只有 i=k 的项保留,得到 Σ_t S_tk C_td,所以整体梯度为 S转置乘 C。因此索引 3 的梯度为 [4,1],索引 5 为 [−1,1],未读取的索引 4 为 [0,0]。这里的求和是共享参数反向规则,不是经验技巧。若程序采用赋值覆盖,索引 3 的行只保留最后 [3,−1],会漏掉第一个位置的贡献。

独立检查一:直接查表和独热矩阵乘法应给出完全相同的 H。检查二:只扰动 E第三行第零维,探针损失的中心差分应为四;如果得到三,说明反向覆盖。如果把编号三改成四而不换表,任务当然改变;只有同时交换词表地址、嵌入行和输出类别时,编号重排才只是同一模型的另一种表示。

共享嵌入怎样接到真实预测损失,还需要一条线性层的链式关系。令当前位置向量为 h,宽度 D;输出矩阵 W 为 D×V,分数 z_k=Σ_d h_d W_dk。记已经推导的分数梯度为 g_k。固定一个 h_d,逐个分数对它求导,得到 ∂z_k/∂h_d=W_dk,再把所有经过 z_k 的路径相加,得到 ∂ℓ/∂h_d=Σ_k g_k W_dk。固定参数 W_dk 时只有对应乘积项变化,因此 ∂ℓ/∂W_dk=h_d g_k。这个路径求和是把多变量问题还原成多个标量依赖,并没有把矩阵求导当成未解释的黑箱。

做一组可复算的数:h=[1,2],W 两行为 [1,0,−1] 和 [0,1,0],因此 z=[1,2,−1]。取目标第二类,平移后指数是 [exp(−1),1,exp(−3)],归一化得到约 [0.259496,0.705385,0.035119]。分数梯度为 [0.259496,−0.294615,0.035119]。于是向量第一维梯度为第一项减第三项,约 0.224377;第二维为中间项,约 −0.294615。W 第一行梯度等于 g,第二行等于 2g。若 h 来自某个嵌入行,这份向量梯度就加到该行;同一字符其他位置的贡献还会继续相加。

最后区分两种常被混淆的量。预测分布自身的熵是把负对数概率按模型自己的概率加权;本次监督交叉熵则按真实目标的独热分布加权。一个模型极度自信地选错类别,预测熵可以很小,监督损失却很大。降低不确定性并不自动等于提高正确性。本章的优化依据是观察目标,而不是单纯鼓励分布变尖;后续温度采样也不能靠让分布更尖来声称训练改善。

四、向量表示为什么还需要位置信息

查表矩阵保留行的排列,但相同 token 在进入上下文模块前具有同样的向量。如果后续仅对行求和或均值,那么 [春,风] 与 [风,春] 得到相同汇总:在上例的向量下都是 [1,2]。丢失来自对称汇总规则,不是“向量不能表示顺序”。对没有位置项、没有因果 mask 的自注意力也有对应的排列等变性质,第三章将直接计算它,而不凭直觉下结论。

主项目会采用可学习绝对位置表 P,把位置 t 的表示写成 E[编号_t]+P[t],两张表都有 D 维行向量,才能逐元素相加。P 按位置共享,E 按字符共享;它们获得的梯度由各自被使用的地方累加。第四章再实现这条路径。因果 mask 自身带有非对称信息,不能由上述均值反例推断“所有有 mask 但无位置的模型完全不能感知顺序”;位置机制和信息可见性要分别研究。

五、独立数值验证

独立运行仅用标准库验证稳定损失、完整分数梯度和重复 token 的共享梯度;没有网络训练。
import math

def softmax(z):
    m=max(z); e=[math.exp(v-m) for v in z]
    return [v/sum(e) for v in e]
def cross_entropy(z,y):
    m=max(z)
    return math.log(sum(math.exp(v-m) for v in z)) + (m-z[y])
def finite_difference(f,x,h):
    out=[]
    for k in range(len(x)):
        plus=x[:]; minus=x[:]
        plus[k]+=h; minus[k]-=h
        out.append((f(plus)-f(minus))/(2*h))
    return out
z=[math.log(2),0.,-math.log(2)]; y=1
prob=softmax(z); grad=[v-(i==y) for i,v in enumerate(prob)]
assert all(abs(a-b)<1e-12 for a,b in zip(prob,[4/7,2/7,1/7]))
assert abs(sum(grad))<1e-12
assert abs(cross_entropy(z,y)-math.log(3.5))<1e-12
assert abs(cross_entropy([1000.,-1000.],1)-2000.)<1e-12
assert max(abs(a-b) for a,b in zip(softmax(z),softmax([v+1000 for v in z])))<1e-12
errors=[]
for h in (1e-4,1e-5,1e-6):
    numeric=finite_difference(lambda v:cross_entropy(v,y),z,h)
    errors.append(max(abs(a-b) for a,b in zip(numeric,grad)))
assert max(errors)<1e-7

ids=[3,5,3]; V=6; D=2
E=[[0.,0.] for _ in range(V)]; E[3]=[1.,0.]; E[5]=[0.,2.]
C=[[1.,2.],[-1.,1.],[3.,-1.]]
H=[E[i][:] for i in ids]
S=[[float(i==k) for k in range(V)] for i in ids]
matmul=[[sum(S[t][i]*E[i][d] for i in range(V)) for d in range(D)] for t in range(3)]
assert H==matmul
analytic=[[0.,0.] for _ in range(V)]
for i,g in zip(ids,C):
    for d in range(D): analytic[i][d]+=g[d]
assert analytic[3]==[4.,1.] and analytic[5]==[-1.,1.]
def probe(value):
    table=[row[:] for row in E]; table[3][0]=value
    return sum(table[i][d]*C[t][d] for t,i in enumerate(ids) for d in range(D))
h=1e-6; embedding_fd=(probe(1+h)-probe(1-h))/(2*h)
assert abs(embedding_fd-4.)<1e-8
# 与 data.py 的 collate 相同:输入右填 EOS,目标右填 -100。
UNK, BOS, EOS, IGNORE = 0, 1, 2, -100
batch_inputs=[[1,3,5],[1,4,2]]
batch_targets=[[3,5,2],[4,2,-100]]
flat_targets=[target for row in batch_targets for target in row]
flat_logits=[[0.]*6 for _ in flat_targets]
valid_indices=[i for i,target in enumerate(flat_targets) if target != IGNORE]
assert len(valid_indices)==5
assert sum(flat_targets[i]==EOS for i in valid_indices)==2
batch_loss=sum(cross_entropy(flat_logits[i],flat_targets[i]) for i in valid_indices)/len(valid_indices)
assert abs(batch_loss-math.log(6))<1e-12
batch_gradient=[]
for logits,target in zip(flat_logits,flat_targets):
    if target == IGNORE:
        batch_gradient.append([0.]*6)
    else:
        batch_gradient.append([(p-(k==target))/len(valid_indices) for k,p in enumerate(softmax(logits))])
assert batch_gradient[-1]==[0.]*6
assert batch_gradient[2][EOS]<0 and batch_gradient[4][EOS]<0
print("project_batch_valid_targets",len(valid_indices),"scored_EOS",2,"loss",batch_loss)
print("probabilities",[round(v,8) for v in prob])
print("loss",round(cross_entropy(z,y),8))
print("logit_gradient",[round(v,8) for v in grad])
print("finite_difference_max_errors",errors)
print("embedding_row3_gradient",analytic[3],"numeric",embedding_fd)
print("all_probability_and_embedding_checks_passed")

这些检查覆盖正常分数、极端分数、公共平移和共享索引,分别触及不同故障机制。若有限差分失败,先检查解析式、扰动位置、原参数是否恢复及 loss 的分母;不要立刻把容差调大。若所有公式都正确但训练仍不改善,还要检查数据、模型连接和优化器。本章尚未引入注意力,局部数值正确不能证明完整网络已工作。

不要用随意给概率加一个小常数来替代稳定损失。若把目标改成负对数的“概率加常数”,当真实目标概率非常小时,梯度会被这一常数显著改变,优化的就不再是前面推导的似然。稳定的代数改写保持数学目标,额外截断或平滑则需要另行定义目标及导数。错误结果能变成有限数,并不表示实现已经正确。

六、递进练习与解答

TRY IT YOURSELF

练习一|从似然算出更新方向

将目标改成第一类 y=0,仍用 z=[ln2,0,−ln2]。写出似然、损失、三个分数导数和学习率 0.07 时的分数改变量。用一个与重新求导不同的性质检查答案。

提示 1
目标变化不影响softmax概率,只改变独热目标向量。
提示 2
逐项计算p−one_hot(y),再乘负学习率;检查全部梯度之和与公共平移的关系。
对照参考解答
概率不变,似然为 4/7。损失为 −ln(4/7)=ln(7/4),约 0.559616。逐项减去独热 [1,0,0],梯度为 [−3/7,2/7,1/7]。梯度下降改变量是负学习率乘梯度,得到 [0.03,−0.02,−0.01],正确类相对分数提高。检查三项梯度和为零、三项改变量也和为零,与公共平移不影响损失一致。这里更新的是独立 logits 的示意变量,还未将它当成真实网络参数。

TRY IT YOURSELF

练习二|一个共享行被错误覆盖

输入 ids=[3,3,5],上游梯度依次为 [2,−1]、[−3,4]、[1,1]。实现把每次使用位置的梯度赋给对应嵌入行,而不是累加。计算正确与错误的第三行梯度,再设计一个一维差分探针。

提示 1
列出第三行被读取的所有位置,共享行收到的是这些位置导数之和。
提示 2
固定其他坐标,令第三行第一维为a;把两个读取位置对J的贡献都写成a的函数,再做中心差分。
对照参考解答
正确的第三行应加前两个位置,得到 [−1,3];第五行为 [1,1]。覆盖实现先写 [2,−1],再改写为 [−3,4],丢失第一次贡献。令其余坐标固定,第三行第一维为 a,构造 J(a)=2a−3a+常数,所以导数为 −1。分别计算 J(a+h) 和 J(a−h),差除以 2h,应接近 −1,而不是覆盖版本的 −3。这个探针只扰动一个共享参数,却同时影响两个读取位置,能直接检验共享语义。

TRY IT YOURSELF

练习三|把单位置公式迁移到带填充批次

一个独立二分类算例有两个有效预测和一个填充位置,目标分别为 [0,0,−100]。两个有效位置的概率为 [1/2,1/2] 与 [1/4,3/4],填充位置概率任意。沿用项目 target≠−100 的规则,写出有效 token 平均损失与三处 logits 梯度。若误用总位置数三作分母,发生什么?

提示 1
先由target定义有效标记与有效总数,−100不作为类别索引。
提示 2
分别对两处有效位置写p−one_hot,并统一除有效数;填充位置乘零,再比较误用三个位置的尺度。
对照参考解答
先由目标得到 m=[1,1,0]、N=2;−100 不是类别编号,忽略项不进行类别索引。两项有效损失总和为 −ln(1/2)−ln(1/4)=ln8,均值 ln8/2≈1.039721。第一位置梯度 [−1/2,1/2] 除以二,得 [−1/4,1/4];第二位置梯度 [−3/4,3/4] 除以二,得 [−3/8,3/8];忽略位置梯度为 [0,0]。误除以三会把均值和有效梯度缩成正确值的三分之二,改变优化尺度。该二类算例只是通用公式验证;项目实际词表有六项,EOS 二是有效类别,只有 −100 被忽略,输入是否为 EOS 不作为判据。

本章增量与下一章依赖

本章为项目补上稳定 softmax、按有效 token 计分的交叉熵、解析分数梯度与共享嵌入反向规则。下一章从 H 中生成 Q、K、V,让不同位置根据内容读取彼此,并把损失梯度真正送回这些投影。第四章才将因果、多头和位置组合进同一结构。在此之前,不能把一个均值向量加分类器称为完整序列模型,也不能用预先给定概率的损失验证冒充模型已经训练。

CHECK YOUR UNDERSTANDING

检验理解

先作答,再决定是否展开解析。选择题用于发现理解缺口;独立实现与迁移任务仍需另行检验。

1. 有限 logits 同加常量后,softmax 怎样变化?

查看解析分子分母同时乘 exp(c) 后相消;这是稳定最大值平移的代数依据。
2. 真实目标是编号 y,单位置 CE 的第 k 项分数导数是什么?

查看解析ln Z 求导贡献 p_k,−z_y 求导贡献 −1[k=y];整数编号不能从每个分量直接相减。
3. 同一 token 在两个位置的上游梯度分别是 a 和 b,该嵌入行得到什么?

查看解析地址不求导,但被查到的参数行可求导;共享使用的贡献需要相加。
4. 均值汇总无法区分两种 token 排列,可以推出什么?

查看解析反例针对具体对称操作;因果 mask 与位置机制的影响须分开研究。

自行标记用于记录学习进度,不代表通过项目验收。