第一次接触大语言模型时,很多人会被各种术语绕晕。其实从损失函数的角度来看,现代语言模型主要分为两大阵营:自编码式(如BERT)和自回归式(如GPT、GLM)。这两种模型在处理文本时,就像两种不同的阅读方式。
想象你在玩填字游戏。自编码模型就像同时看到谜题的左右提示,可以双向推理空白处的内容;而自回归模型则像只能看到左侧提示,必须按顺序逐个猜测。这种根本差异直接影响了它们的损失函数设计。
我在实际项目中使用BERT和GLM时发现,选择哪种模型往往取决于任务特性。比如需要理解完整上下文的任务(如文本分类),BERT的双向优势明显;而需要生成连贯文本的场景(如对话系统),自回归模型的表现更自然。这背后的关键就在于它们的损失函数如何引导模型学习。
BERT的核心创新是**Masked Language Model (MLM)**任务。具体实现时,它会随机遮盖输入文本中15%的token(比如把"city"变成"[MASK]"),然后让模型预测被遮盖的词。这个过程的损失函数计算非常讲究:
python复制def mlm_loss(masked_positions, logits, true_labels):
# 只计算被mask位置的损失
masked_logits = logits[masked_positions] # shape: [num_masked, vocab_size]
loss = cross_entropy(masked_logits, true_labels)
return loss
这种设计带来三个实战要点:
BERT的第二个损失项是Next Sentence Prediction (NSP)。这个二分类任务判断两个句子是否连续,其损失函数看似简单却暗藏玄机:
python复制def nsp_loss(sentence_embeddings, is_next_labels):
# sentence_embeddings: [batch_size, hidden_size]
logits = classifier(sentence_embeddings)
loss = binary_cross_entropy(logits, is_next_labels)
return loss
在实际预训练中,我们发现NSP任务有这些特点:
注意:后来的RoBERTa等模型移除了NSP任务,发现仅MLM也能达到更好效果。这说明损失函数设计需要根据数据特性不断优化。
清华大学提出的GLM模型创造性地融合了两种范式。它的核心是自回归式空白填充(Autoregressive Blank Infilling),通过独特的损失函数设计实现了双向理解和生成能力的平衡。
具体实现时,GLM会:
其损失函数可以表示为:
python复制def glm_loss(masked_spans, output_logits):
total_loss = 0
for span in shuffled(masked_spans):
# 使用前文和已预测片段信息
span_logits = output_logits[span.start:span.end]
loss = cross_entropy(span_logits, true_spans[span])
total_loss += loss
return total_loss
GLM-130B在实现时有个精妙设计:二维位置编码。这是因为:
这种设计使得损失函数计算时需要特殊处理位置信息:
python复制def get_positions(input_ids):
# 第一个维度表示片段编号
# 第二个维度表示片段内位置
position_ids = build_2d_positions(input_ids)
return position_ids
在实际使用中,这种设计让模型在生成长文本时能更好地保持一致性,避免了传统自回归模型容易出现的主题漂移问题。
观察BERT到GLM的损失函数变化,可以发现三个明显趋势:
从固定模式到灵活预测
早期BERT使用固定15%掩码率,后来模型逐渐采用动态长度掩码(如SpanBERT的随机长度掩码)
从单一任务到多任务融合
GLM的损失函数同时包含理解和生成目标,而最新模型甚至引入推理相关的损失项
从人工设计到自动学习
部分最新研究开始尝试用元学习等方式自动调整损失函数权重
根据我在多个项目的实测经验,给出以下建议:
提示:现在主流框架(如HuggingFace)已经实现了这些损失函数,建议先使用现成实现再考虑自定义。比如GLM的损失函数在transformers库中已经优化得很好。
在实现自回归损失时,温度系数(temperature)对结果影响巨大。我们可以这样调整:
python复制def scaled_softmax(logits, temperature=1.0):
scaled_logits = logits / temperature
return softmax(scaled_logits)
不同温度值的效果对比:
| 温度值 | 输出特点 | 适用场景 |
|---|---|---|
| <1.0 | 更确定性强,多样性低 | 事实性回答 |
| 1.0 | 标准设定 | 通用场景 |
| >1.0 | 多样性高,可能不连贯 | 创意写作 |
在多任务学习中,损失函数加权很有讲究。以GLM为例,我常用的策略是:
python复制class DynamicWeight:
def __init__(self, num_tasks):
self.log_vars = nn.Parameter(torch.zeros(num_tasks))
def __call__(self, losses):
return torch.sum(torch.exp(-self.log_vars) * losses + self.log_vars)
这种方法在GLM-130B的微调中特别有效,能自动平衡不同任务的贡献。