Research Engineer / ML · ML Coding

NumPy Cross Entropy:稳定LogSumExp、Mask和Label Smoothing

从logits直接算损失,梯度归一化按有效样本数。

题目重点:从logits直接算损失,梯度归一化按有效样本数。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • logits为N×C有限实数,targets为N个合法整数类标;mask为N个bool,用于排除整条样本。训练不使用ignore_index负类标,也不是屏蔽类别候选。
  • 采用自然对数nats,smoothing ε∈[0,1],q=(1−ε)one_hot+ε/C,包含目标类的均匀分布。mean仅按有效样本数;全mask返回loss0及零梯度。

解题路线

  1. 每行先减最大logit,logp=shifted−logΣexp(shifted),避免大正数overflow。不要先softmax再log,因为很小p可能下溢成0,产生无穷损失。
  2. CE=−Σq_c logp_c。hard label时等价−logp_target;smoothing时把所有类纳入,不是只把正确类别权重从1改成1−ε就结束。两种常见平滑约定不能混用,ε/C与ε/(C−1)产生不同目标。
  3. 梯度∂L/∂x=(p−q)/有效N,屏蔽行全部置0。先对N总行mean再把mask乘上会让有效样本比例改变梯度尺度,训练速率随padding比例漂移。
  4. 把张量形状、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均值?
✓已复制微信号!