题目重点:从logits直接算损失,梯度归一化按有效样本数。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- logits为N×C有限实数,targets为N个合法整数类标;mask为N个bool,用于排除整条样本。训练不使用ignore_index负类标,也不是屏蔽类别候选。
- 采用自然对数nats,smoothing ε∈[0,1],q=(1−ε)one_hot+ε/C,包含目标类的均匀分布。mean仅按有效样本数;全mask返回loss0及零梯度。
解题路线
- 每行先减最大logit,logp=shifted−logΣexp(shifted),避免大正数overflow。不要先softmax再log,因为很小p可能下溢成0,产生无穷损失。
- CE=−Σq_c logp_c。hard label时等价−logp_target;smoothing时把所有类纳入,不是只把正确类别权重从1改成1−ε就结束。两种常见平滑约定不能混用,ε/C与ε/(C−1)产生不同目标。
- 梯度∂L/∂x=(p−q)/有效N,屏蔽行全部置0。先对N总行mean再把mask乘上会让有效样本比例改变梯度尺度,训练速率随padding比例漂移。
- 把张量形状、dtype、目标范围和空mask验证放在计算前。token序列可把B×T×C展成(BT)×C,但mask平均单位是有效token;若题目要求每句均值再batch均值,长度不等时是另一个loss。
手动走一遍
logits[1000,999]、目标0,减max后[0,−1],loss约0.3133,有限。两行只有第一行有效,梯度与只输入第一行完全一致;全无效不出现0/0。C4、ε0.2,正确类q0.85,其余各0.05;不是正确0.8、其余0.0667。
Python 参考实现
下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。
代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。
import numpy as np
def cross_entropy(logits,targets,mask=None,smoothing=0.0):
x=np.asarray(logits,dtype=float);y=np.asarray(targets)
if x.ndim!=2 or x.shape[1]<1 or y.shape!=(len(x),) or not np.issubdtype(y.dtype,np.integer):raise ValueError('logits N,C / integer targets N')
if not np.isfinite(x).all() or not 0<=smoothing<=1 or np.any(y<0) or np.any(y>=x.shape[1]):raise ValueError('values')
active=np.ones(len(x),dtype=bool) if mask is None else np.asarray(mask,dtype=bool)
if active.shape!=(len(x),):raise ValueError('mask')
count=int(active.sum())
if not count:return 0.0,np.zeros_like(x)
shifted=x-x.max(axis=1,keepdims=True);exp=np.exp(shifted);prob=exp/exp.sum(axis=1,keepdims=True)
logprob=shifted-np.log(exp.sum(axis=1,keepdims=True))
q=np.full_like(x,smoothing/x.shape[1]);q[np.arange(len(x)),y]+=1-smoothing
loss=float((-(q*logprob).sum(axis=1))[active].mean())
gradient=(prob-q)/count;gradient[~active]=0
return loss,gradient
def kl_divergence(q,p):
q=np.asarray(q,dtype=float);p=np.asarray(p,dtype=float)
if q.ndim!=1 or q.shape!=p.shape or np.any(q<0) or np.any(p<0) or not np.isfinite(q).all() or not np.isfinite(p).all():raise ValueError('finite probability vectors')
if not np.isclose(q.sum(),1) or not np.isclose(p.sum(),1):raise ValueError('normalized')
if np.any((q>0)&(p==0)):return float('inf')
active=q>0
return float(np.sum(q[active]*(np.log(q[active])-np.log(p[active]))))
复杂度
每步O(NC)时间/空间;示例构建q/prob/logprob,因此不声称常数空间。chunk reduction可少存中间矩阵,但返回全部logits梯度仍占O(NC)。
易错点与追问
容易失分的边界
- logsumexp漏减max,极端logits溢出。
- mask用于有效样本,却错误把logits中的某些类设−∞。
- 全部mask时直接mean空数组导致NaN。
继续深挖
- 类别mask要求无法选择padding类时,softmax归一化域怎样改变?
- 有效token分布在不同设备,怎样用全局分母合并梯度而非平均各rank均值?
