本章任务:从分数一直追到共享参数的梯度
上一章已经固定输入 X、移位目标 Y 和有效标记。本章要回答两件相连的事:一个位置输出若干实数后,怎样得到可优化的概率目标;一个字符在不同位置重复出现时,学习信号怎样共同修改同一行向量。我们会亲手推导交叉熵的分数梯度,再推导嵌入查表的反向累加。这里没有完整模型训练,最后的程序是检验这些局部机制的独立数值参考。
先修包括上一章的条件概率连乘、词表地址与矩阵形状,以及标量求导规则:指数的导数仍为指数,自然对数的导数为自变量的倒数,复合函数按链式法则相乘。向量梯度将在坐标上逐项建立。记 V 为候选 token 数,z=(z零,…,z末) 是一个位置的 logits,即未归一化实数分数;y 是真实下一 token 的编号,不是概率。
一、为什么输出分数需要归一化
分数可以为负、可以大于一,分数之和也没有约束。定义每类的非负权重为 exp(z_i),再除以所有权重之和 Z,得到 softmax 概率。由于指数为正且候选数有限,Z>0,各概率处于零到一之间且总和为一。这是选定的一种参数化,并非所有分类模型只能这样构造。它允许模型只通过改变相对分数来重新分配概率质量。
对所有分数同时加常量 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。若完整文档的条件概率相乘,取负对数就变为各位置损失相加;这由对数的乘积转求和恒等式得到。将多篇文档的似然相乘则还采用了文档在参数条件下独立的模型约定,不是从字符链式分解自动得到的事实。
这一区别在极端分数下很重要。若分数为 [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 是否等于真实目标”的指示量。
独热目标 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 二仍参与计分。
沿用上一章两篇短文,矩形共有六格而有效目标只有五个。若把总损失固定为十,正确均值为二,误除以六会变成六分之五乘以二,即约一点六六七。若对两篇先各求平均再等权平均,短文的每个 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 但无位置的模型完全不能感知顺序”;位置机制和信息可见性要分别研究。
五、独立数值验证
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
提示 2
对照参考解答
TRY IT YOURSELF
练习二|一个共享行被错误覆盖
输入 ids=[3,3,5],上游梯度依次为 [2,−1]、[−3,4]、[1,1]。实现把每次使用位置的梯度赋给对应嵌入行,而不是累加。计算正确与错误的第三行梯度,再设计一个一维差分探针。
提示 1
提示 2
对照参考解答
TRY IT YOURSELF
练习三|把单位置公式迁移到带填充批次
一个独立二分类算例有两个有效预测和一个填充位置,目标分别为 [0,0,−100]。两个有效位置的概率为 [1/2,1/2] 与 [1/4,3/4],填充位置概率任意。沿用项目 target≠−100 的规则,写出有效 token 平均损失与三处 logits 梯度。若误用总位置数三作分母,发生什么?
提示 1
提示 2
对照参考解答
本章增量与下一章依赖
本章为项目补上稳定 softmax、按有效 token 计分的交叉熵、解析分数梯度与共享嵌入反向规则。下一章从 H 中生成 Q、K、V,让不同位置根据内容读取彼此,并把损失梯度真正送回这些投影。第四章才将因果、多头和位置组合进同一结构。在此之前,不能把一个均值向量加分类器称为完整序列模型,也不能用预先给定概率的损失验证冒充模型已经训练。