直觉理解

语言模型就是给一段文本打分的模型:它估计一个词序列出现的概率有多大。「今天天气很好」应该比「好很天气今天」的概率高得多。

现代的大语言模型几乎都是自回归的:把「整句话的概率」拆成「每一步预测下一个 token 的概率」。训练时,模型反复练习一件事:给定前文,猜下一个 token。生成时,就一个接一个地往后猜,把猜出来的 token 接到输入末尾,再猜下一个。

这个任务看起来简单,但要把下一个词猜准,模型必须掌握语法、事实知识、上下文逻辑,甚至一定的推理能力。大语言模型的许多能力,都是在海量文本上做这个任务「顺带」学会的。

定义与公式

自回归分解

根据概率论的链式法则,长度为 的序列 的联合概率可以严格地分解为

语言模型用一个参数为 的神经网络来近似每一个条件概率 。在仅解码器 Transformer 中,输入位置 能看到自己及之前的 token,输出经线性层和 softmax 后预测 。因此,预测 使用的是前一个输入位置的输出,而不是让含有 的位置预测自身。

训练目标:下一个 token 预测

训练就是最大化训练文本的似然,等价于最小化每个位置上的交叉熵损失:

借助因果掩码,一次前向计算能并行计算所有有效位置的预测和损失。若只有 个 token 且不额外添加边界标签,通常用前 个位置预测后 个 token;若输入为 BOS、,标签为 ,就有 个训练目标。标签直接由原文本错位得到,不需要逐项人工标注,因此属于自监督学习。

训练时每一步都喂给模型真实的前文,而不是模型自己生成的内容,这种做法叫教师强制(teacher forcing)。

困惑度

困惑度(perplexity)是评价语言模型最常用的指标,定义为平均交叉熵的指数:

直观理解:困惑度为 ,相当于模型每一步平均在 个选项里犹豫不决。完全随机地从大小为 的词表里猜,困惑度就是 ;预测得越准,困惑度越接近 1。困惑度与信息论中的交叉熵直接对应。

注意:困惑度依赖分词方式,词表不同的模型之间不能直接比较困惑度。

解码策略

模型每一步给出的是一个概率分布,从中挑出下一个 token 的方法叫解码策略。设模型输出的分数(logits)为 :

  • 贪心解码:每一步都选概率最大的 token。结果确定,但容易陷入重复,文本单调。束搜索(beam search)同时保留多个候选序列,常用于翻译等有标准答案的任务。
  • 温度采样:按概率随机采样,采样前把 logits 除以温度 : 让分布更尖锐,输出更保守; 让分布更平坦,输出更多样也更容易出错; 退化为贪心解码。
  • top-k 采样:只在概率最高的 个 token 里采样,砍掉长尾中不靠谱的选项。
  • top-p 采样(核采样):按概率从高到低累加,只保留累计概率刚好达到 的最小集合,再在其中采样。与 top-k 相比,候选集合的大小会随分布的形状自动调整:模型很确定时只留少数几个,不确定时留更多。由 Holtzman 等人在 2019 年提出。

实际使用中,温度常与 top-p 或 top-k 组合使用。

代码示例

计算交叉熵与困惑度,并实现上面几种解码策略:

import math
import torch
import torch.nn.functional as F
 
torch.manual_seed(0)
vocab_size = 50
logits = torch.randn(1, 6, vocab_size)    # 模型对 6 个位置的输出
ids = torch.randint(0, vocab_size, (1, 7))  # 输入序列多一个 token
 
# 训练目标:位置 t 的输出预测第 t+1 个 token
loss = F.cross_entropy(logits.reshape(-1, vocab_size), ids[:, 1:].reshape(-1))
print("交叉熵", loss.item(), "困惑度", math.exp(loss.item()))
 
def sample_next(logits, temperature=1.0, top_k=None, top_p=None):
    """logits: (vocab_size,),返回下一个 token id"""
    if temperature == 0:
        return logits.argmax().item()     # 贪心解码
    logits = logits / temperature
    if top_k is not None:
        kth = torch.topk(logits, top_k).values[-1]
        logits = logits.masked_fill(logits < kth, float("-inf"))
    if top_p is not None:
        sorted_logits, idx = logits.sort(descending=True)
        cum = sorted_logits.softmax(-1).cumsum(-1)
        remove = cum >= top_p
        remove[1:] = remove[:-1].clone()   # 保留使累计概率首次达到 p 的那个 token
        remove[0] = False
        logits[idx[remove]] = float("-inf")
    return torch.multinomial(logits.softmax(-1), 1).item()
 
last = logits[0, -1]
print(sample_next(last, temperature=0))
print(sample_next(last, temperature=0.8, top_k=10))
print(sample_next(last, temperature=1.0, top_p=0.9))

在 AI 中的作用

「在海量文本上预测下一个 token」是大语言模型预训练的核心目标。模型规模和数据量扩大之后,仅靠这个目标训练出的模型就能通过提示完成翻译、问答、写代码等各种任务,见 GPT-3。但这样的模型只会「续写」,不一定会按人的意图回答问题,所以之后还要经过指令微调和基于人类反馈的强化学习等后训练,见 InstructGPT。

生成是逐个 token 进行的,每一步都要把整个模型算一遍,这让推理成为大模型部署的主要成本,催生了 KV 缓存、投机解码、量化等一系列推理优化技术。

参考资料

相关