损失函数:训练信号的来源

损失函数定义了模型的学习目标。在LLM训练的不同阶段——预训练、指令微调、对齐——使用的损失函数截然不同。理解每种损失函数的设计意图和数学特性,是构建高质量LLM的基础。

预训练:交叉熵损失

基本形式

预训练的核心是下一个token预测,使用交叉熵损失:

def cross_entropy_loss(logits, labels, ignore_index=-100):
    """
    logits: [batch, seq_len, vocab_size]
    labels: [batch, seq_len]
    """
    batch_size, seq_len, vocab_size = logits.shape
    
    # 展平
    logits_flat = logits.view(-1, vocab_size)
    labels_flat = labels.view(-1)
    
    # 计算交叉熵(忽略padding位置)
    loss = F.cross_entropy(
        logits_flat, labels_flat,
        ignore_index=ignore_index,
        reduction='mean'
    )
    
    return loss

Z-Loss

Chinchilla引入的Z-Loss用于稳定训练。它通过对logits的log-partition-function进行正则化,防止logits过大:

def z_loss(logits, coefficient=1e-4):
    """Z-Loss: 正则化logits的规模"""
    log_z = torch.logsumexp(logits, dim=-1)  # [batch, seq_len]
    return coefficient * (log_z ** 2).mean()

# 总损失
total_loss = cross_entropy_loss(logits, labels) + z_loss(logits)

Z-Loss的作用:

  • 防止某些logits变得过大(导致softmax饱和)
  • 稳定混合精度训练
  • 对最终性能几乎没有负面影响

Label Smoothing

标签平滑将硬标签(one-hot)软化为软标签,防止模型过度自信:

def label_smoothing_cross_entropy(logits, labels, smoothing=0.1, ignore_index=-100):
    """带标签平滑的交叉熵"""
    n_classes = logits.size(-1)
    
    # 创建软标签
    log_probs = F.log_softmax(logits, dim=-1)
    
    # 均匀分布的平滑
    smooth_loss = -log_probs.mean(dim=-1)  # 对所有类的平均log prob
    
    # 标准交叉熵
    nll_loss = -log_probs.gather(dim=-1, index=labels.unsqueeze(-1)).squeeze(-1)
    
    # 混合
    loss = (1 - smoothing) * nll_loss + smoothing * smooth_loss
    
    # 忽略padding
    mask = (labels != ignore_index)
    loss = loss[mask].mean()
    
    return loss

标签平滑的效果:

  • 减少过拟合
  • 改善模型校准(calibration)
  • 在预训练中通常不使用,在微调中偶尔使用

指令微调:加权损失

多任务加权

指令微调通常涉及多种任务(对话、QA、推理等),不同任务的损失需要加权:

class MultiTaskLoss(nn.Module):
    def __init__(self, task_weights=None):
        super().__init__()
        self.task_weights = task_weights or {}
    
    def forward(self, logits, labels, task_ids):
        """
        task_ids: [batch] 标识每个样本的任务类型
        """
        total_loss = 0
        task_losses = {}
        
        for task_id in task_ids.unique():
            mask = (task_ids == task_id)
            task_logits = logits[mask]
            task_labels = labels[mask]
            
            task_loss = F.cross_entropy(
                task_logits.view(-1, task_logits.size(-1)),
                task_labels.view(-1),
                ignore_index=-100
            )
            
            weight = self.task_weights.get(task_id.item(), 1.0)
            total_loss += weight * task_loss * mask.sum()
            task_losses[task_id.item()] = task_loss.item()
        
        return total_loss / labels.numel(), task_losses

RLHF:PPO损失

PPO的目标函数

RLHF(Reinforcement Learning from Human Feedback)使用PPO算法优化模型:

def ppo_loss(
    policy_logits,      # 策略模型的logits
    reference_logits,    # 参考模型的logits
    rewards,            # 奖励模型的打分
    values,             # 价值函数的预测
    old_log_probs,      # 旧策略的log概率
    advantages,         # 优势函数
    clip_ratio=0.2,
    kl_coeff=0.05,
    value_coeff=0.5
):
    """PPO损失函数"""
    
    # 1. 策略损失(Clipped Surrogate)
    new_log_probs = F.log_softmax(policy_logits, dim=-1)
    ratio = torch.exp(new_log_probs - old_log_probs)
    
    clipped_ratio = torch.clamp(ratio, 1 - clip_ratio, 1 + clip_ratio)
    policy_loss = -torch.min(ratio * advantages, clipped_ratio * advantages).mean()
    
    # 2. KL散度惩罚
    kl_loss = F.kl_div(
        new_log_probs,
        F.softmax(reference_logits, dim=-1),
        reduction='batchmean'
    )
    
    # 3. 价值函数损失
    value_loss = F.mse_loss(values, rewards)
    
    # 总损失
    total_loss = policy_loss + kl_coeff * kl_loss + value_coeff * value_loss
    
    return total_loss

DPO损失

DPO(Direct Preference Optimization)直接优化偏好,无需训练奖励模型:

def dpo_loss(
    policy_chosen_logps,     # 策略模型对chosen的log概率
    policy_rejected_logps,    # 策略模型对rejected的log概率
    reference_chosen_logps,   # 参考模型对chosen的log概率
    reference_rejected_logps, # 参考模型对rejected的log概率
    beta=0.1
):
    """DPO损失"""
    # 计算相对log概率差
    chosen_logps_diff = policy_chosen_logps - reference_chosen_logps
    rejected_logps_diff = policy_rejected_logps - reference_rejected_logps
    
    # Bradley-Terry模型
    logits = beta * (chosen_logps_diff - rejected_logps_diff)
    
    # 损失 = -log(sigmoid(logits))
    loss = -F.logsigmoid(logits).mean()
    
    return loss

DPO的优势:

  • 无需训练独立的奖励模型
  • 无需复杂的PPO训练循环
  • 训练更稳定,调参更简单

对比学习损失

InfoNCE

用于学习文本表示的对比损失:

def info_nce_loss(
    anchor,       # [batch, d]
    positive,     # [batch, d]
    negatives,    # [batch, n_neg, d] 或 [batch_size-1, d]
    temperature=0.07
):
    """InfoNCE对比损失"""
    # 归一化
    anchor = F.normalize(anchor, dim=-1)
    positive = F.normalize(positive, dim=-1)
    
    # 正样本相似度
    pos_sim = (anchor * positive).sum(dim=-1, keepdim=True) / temperature  # [batch, 1]
    
    # 负样本相似度
    neg_sim = torch.bmm(negatives, anchor.unsqueeze(-1)).squeeze(-1) / temperature  # [batch, n_neg]
    
    # 拼接正负样本
    logits = torch.cat([pos_sim, neg_sim], dim=-1)  # [batch, 1 + n_neg]
    
    # 标签:第0个是正样本
    labels = torch.zeros(anchor.size(0), dtype=torch.long, device=anchor.device)
    
    return F.cross_entropy(logits, labels)

辅助损失

MoE负载均衡损失

def moe_load_balancing_loss(router_probs, expert_indices, top_k=2):
    """MoE负载均衡损失"""
    n_experts = router_probs.size(-1)
    batch_size = router_probs.size(0)
    
    # 每个专家被选中的比例 f_i
    expert_mask = F.one_hot(expert_indices, n_experts).float()  # [batch, top_k, n_experts]
    f = expert_mask.sum(dim=1).mean(dim=0)  # [n_experts]
    
    # 路由器对每个专家的平均概率 P_i
    P = router_probs.mean(dim=0)  # [n_experts]
    
    # 负载均衡损失 = n_experts * Σ(f_i * P_i)
    loss = n_experts * (f * P).sum()
    
    return loss

正则化损失

def regularization_loss(model, l1_coeff=0.0, l2_coeff=0.01):
    """L1/L2正则化"""
    l1 = sum(p.abs().sum() for p in model.parameters())
    l2 = sum((p ** 2).sum() for p in model.parameters())
    return l1_coeff * l1 + l2_coeff * l2

2026年损失函数前沿

ORPO(Odds Ratio Preference Optimization)

不需要参考模型的偏好优化方法:

def orpo_loss(policy_chosen_logps, policy_rejected_logps, beta=0.1):
    """ORPO: 无参考模型的偏好优化"""
    # 对数几率比
    log_odds_chosen = policy_chosen_logps - torch.log(1 - torch.exp(policy_chosen_logps))
    log_odds_rejected = policy_rejected_logps - torch.log(1 - torch.exp(policy_rejected_logps))
    
    # 对数几率比差异
    odds_ratio = log_odds_chosen - log_odds_rejected
    
    # 偏好损失
    preference_loss = -F.logsigmoid(odds_ratio).mean()
    
    return preference_loss

SimPO(Simple Preference Optimization)

进一步简化DPO,移除参考模型和beta参数:

def simpo_loss(policy_chosen_logps, policy_rejected_logps, 
               gamma=0.5, beta=2.0):
    """SimPO: 更简单的偏好优化"""
    # 直接使用策略模型的log概率
    logits = beta * (policy_chosen_logps - policy_rejected_logps) - gamma
    
    return -F.logsigmoid(logits).mean()

损失函数的选择指南

训练阶段主要损失辅助损失备注
预训练交叉熵Z-Loss基础语言能力
指令微调加权交叉熵多任务平衡指令跟随
SFT交叉熵Label Smoothing监督学习
RLHFPPOKL + Value复杂但灵活
DPODPO Loss简单有效
对齐ORPO/SimPO无需参考模型

实践建议

  1. Z-Loss必加:在预训练中添加Z-Loss,系数1e-4
  2. 梯度监控:监控不同损失项的梯度规模,确保没有一项主导
  3. 损失加权:多任务训练时,根据任务重要性调整权重
  4. KL系数调度:RLHF中动态调整KL系数——前期大(防偏移),后期小(允许探索)
  5. 评估指标:不要只看loss,还要看下游任务指标

结语

损失函数定义了模型"学什么"和"怎么学"。从基础的交叉熵到复杂的PPO/DPO,每种损失函数都承载着特定的训练目标。理解损失函数的数学原理和实践特性,是高效训练高质量LLM的关键。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。