2、大模型文本生成基础:自回归生成原理、与tokenization、温度与top-p采样、beam search与贪心解码
大模型(如、LLaMA、Qwen等)的文本生成能力,本质上是一个逐字逐词“接龙”的过程。理解这一过程的核心机制,是掌握推理优化、参数调优和工程部署的前提。本章将围绕四个关键主题展开:自回归生成原理、与tokenization、温度与top-p采样、beam search与贪心解码。我们将从数学直觉、工程实现到实战调参,系统梳理大模型文本生成的底层逻辑。
2.1 自回归生成原理:逐的条件概率分解
核心原理
自回归(Autoregressive)生成是大模型文本生成的基础范式。其数学本质是将一个长度为 \(N\) 的文本序列 \(X = (x_1, x_2, ..., x_N)\) 的联合概率 \(P(X)\) 分解为条件概率的连乘形式:
P(X) = P(x_1) × P(x_2|x_1) × P(x_3|x_1,x_2) × ... × P(x_N|x_1,...,x_{N-1})
在实际推理时,模型每一步只预测下一个的概率分布,然后从分布中采样或选择最优,将其拼接到已有序列末尾,再作为下一步的输入。这种“吃一个、吐一个”的循环过程,就是自回归生成。
工程实现上,Transformer解码器利用因果注意力掩码(Causal Mask)确保每个位置只能看到它之前的。同时,为了加速推理,业界普遍使用KV Cache技术:将已计算过的Key和Value向量缓存起来,避免每一步都重新计算整个序列的注意力,从而将时间复杂度从 \(O(n^2)\) 降为 \(O(n)\)(仅计算新的注意力)。
判断方法与操作流程
- 判断是否自回归:若模型生成下一个时依赖所有已生成的,且生成过程是串行的,则为自回归模型。BERT等双向编码器模型不属于自回归生成模型。
- 推理流程:初始化prompt → 模型前向计算得到logits → 对logits应用解码策略(如贪心/采样) → 得到新 → 更新KV Cache → 重复直到遇到EOS(结束符)或达到max_new_tokens。
案例拆解:KV Cache的显存优化
假设输入序列长度为1024,模型隐藏层维度为4096,层数为32,batch size为1。若不使用KV Cache,每一步生成都需要重新计算所有的Key/Value矩阵,计算量约为 \(1024^2 \times 4096 \times 32\) 次乘加运算。使用KV Cache后,仅需计算新的Key/Value,计算量降为 \(1024 \times 4096 \times 32\),推理速度提升约1000倍。但代价是显存占用增加:KV Cache需要存储 \(2 \times 32 \times 4096 \times 1024 \times 2\) 字节(FP16),约512MB。因此,长序列生成时需关注显存上限,必要时使用KV Cache量化或分块KV Cache。
--max-model-len参数控制最大序列长度,从而预估KV Cache峰值显存。
常见误区
- 误区1:认为自回归生成可以并行输出多个。实际上,自回归本质是串行的,但可以通过投机采样(Speculative Decoding)用一个小模型先草拟多个,再由大模型验证,实现“伪并行”加速。
- 误区2:忽略EOS(End of Sequence)的处理。若未正确设置停止条件,模型可能无限生成或提前截断。生产环境应同时设置
max_new_tokens和EOS判断。
2.2 与Tokenization:文本的原子单位
核心原理
是模型处理文本的最小单位。Tokenization(分词)是将原始字符串转换为 ID序列的过程。主流方法包括BPE(Byte Pair Encoding)、WordPiece和SentencePiece。BPE的核心思想是:从字符级开始,逐步合并出现频率最高的相邻字符对,直到达到预设的词表大小(如32K、64K或100K)。
例如,使用的BPE分词器:
输入: "I love NLP"
输出: ["I", " love", " NLP"] → IDs: [40, 3682, 12102]
注意,BPE通常会在英文单词前保留空格(如" love"),以便解码时恢复原始文本。中文分词则常以字或子词为单位,例如"今天天气很好"可能被切分为["今天", "天气", "很好"]。
判断方法与操作流程
| 分词方法 | 特点 | 适用场景 |
|---|---|---|
| BPE | 基于字节对合并,词表大小可控,能处理未登录词 | 系列、LLaMA、Qwen |
| WordPiece | 基于概率似然增加合并,类似BPE但合并标准不同 | BERT、DistilBERT |
| SentencePiece | 将空格视为普通字符,支持直接处理原始文本 | ALBERT、T5、LLaMA(部分) |
操作流程:加载分词器 → 调用tokenizer.encode(text) → 得到 IDs → 模型前向计算 → 调用tokenizer.decode(ids)恢复文本。注意,decode时需设置skip_special_tokens=True以过滤[PAD]、[CLS]等特殊。
案例拆解:中文分词的特殊性
中文没有天然的空格分隔符,因此分词策略直接影响模型效果。以Qwen2.5为例,其分词器基于SentencePiece,词表大小为151K,其中包含大量中文字符和常用词。实测对比:
- 输入"人工智能正在改变世界",Qwen分词结果:["人工智能", "正在", "改变", "世界"],共4个。
- 若按单字切分,则为["人","工","智","能","正","在","改","变","世","界"],共10个。
显然,基于词的分词能减少数量,降低计算量,但可能遇到未登录词(如"")。此时BPE会将其拆分为["Chat", ""]或["Chat", "G", "PT"],确保任何词都能被表示。
常见误区
- 误区1:认为数量等于字符数。实际上,英文中一个单词可能被拆成多个(如"unbelievable" → ["un", "belie", "vable"]),中文中一个词也可能被拆成多个字。
- 误区2:忽略数量对成本的影响。API计费通常按数计算,优化prompt长度(如精简指令、去除冗余)能显著降低成本。
2.3 温度与Top-p采样:控制生成随机性
核心原理
模型输出的logits(未归一化的分数)通过Softmax转换为概率分布。温度(Temperature)和Top-p是两种常用的采样策略,用于调节生成文本的多样性与确定性。
- 温度(T):对logits除以T后再做Softmax。当T→0时,概率分布趋于尖锐(近似one-hot),模型倾向于选择最高概率;当T→∞时,分布趋于均匀,生成更随机。典型取值范围:0.1~2.0。
- Top-p(核采样):将按概率降序排列,累计概率达到p时截断,只从保留的集合中采样。例如p=0.9,表示只从累计概率90%的中采样,丢弃尾部低概率。这能动态过滤“长尾”噪声。
两者可组合使用:先应用温度缩放,再进行Top-p截断。实际中,的默认使用temperature=0.7,top_p=1.0(即不截断)。
判断方法与操作流程
# 伪代码示例
def sample_with_temperature_and_top_p(logits, temperature=0.7, top_p=0.9):
# 1. 温度缩放
scaled_logits = logits / temperature
# 2. Softmax得到概率
probs = softmax(scaled_logits)
# 3. Top-p截断
sorted_probs, sorted_indices = torch.sort(probs, descending=True)
cumsum_probs = torch.cumsum(sorted_probs, dim=-1)
# 找到累计概率超过p的位置
mask = cumsum_probs - sorted_probs > top_p
sorted_probs[mask] = 0.0
# 4. 重新归一化并采样
sorted_probs = sorted_probs / sorted_probs.sum()
sampled_index = torch.multinomial(sorted_probs, 1)
return sorted_indices[sampled_index]
案例拆解:对话生成中的参数调优
假设我们在构建一个客服机器人,需要生成准确、稳定的回答。初始配置temperature=1.0,top_p=0.95,发现回答经常出现“嗯…”“可能吧”等含糊词汇,且偶尔产生事实错误。分析后调整:
- 将temperature降至0.3,回答变得确定,但略显机械。
- 将temperature设为0.6,top_p设为0.85,回答既流畅又保持一定多样性。
进一步对比:
| 参数组合 | 生成效果 | 适用场景 |
|---|---|---|
| T=0.1, p=1.0 | 几乎总是选择最高概率,回答固定 | 代码生成、数学推理 |
| T=0.7, p=0.9 | 平衡多样性和准确性 | 通用对话、内容创作 |
| T=1.2, p=0.95 | 高随机性,可能产生创意但易跑题 | 头脑风暴、故事生成 |
常见误区
- 误区1:认为temperature=0就是贪心解码。实际上,T=0时Softmax退化为argmax,但数值计算中可能出现除零或NaN,建议使用
do_sample=False直接走贪心路径。 - 误区2:忽略top_p与temperature的相互作用。当T很高时,概率分布变平,top_p截断可能保留过多,导致生成质量下降。建议先固定一个参数,再调另一个。
2.4 Beam Search与贪心解码:搜索最优序列
核心原理
贪心解码(Greedy Decoding)每一步选择概率最高的,计算效率高,但容易陷入局部最优。Beam Search则维护一个大小为beam_size的候选序列集合,每一步从所有候选的扩展中保留概率乘积最高的beam_size个序列,最终选择整体概率最高的序列。
例如,beam_size=2时:
Step 1: 候选 ["我"(0.6), "你"(0.4)]
Step 2: 扩展 "我" → ["我爱"(0.7), "我恨"(0.3)]; "你" → ["你好"(0.8), "你坏"(0.2)]
保留概率乘积最高的2条: ["你好"(0.4×0.8=0.32), "我爱"(0.6×0.7=0.42)]
Step 3: 继续扩展,直到EOS或达到最大长度。
Beam Search的优点是能找到全局更优解,但计算量随beam_size线性增长,且容易产生重复文本(尤其是长文本生成时)。
判断方法与操作流程
- 选择依据:若任务要求高准确性(如翻译、摘要),优先Beam Search;若任务要求多样性(如对话、故事),优先采样(temperature+top_p)。
- 参数设置:
num_beams(通常4~8)、no_repeat_ngram_size(防止重复,如设为3)、early_stopping(是否在遇到EOS时提前停止)。
案例拆解:机器翻译中的Beam Search调优
以英译中任务为例,使用HuggingFace Transformers库:
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
model = AutoModelForSeq2SeqLM.from_pretrained("Helsinki-NLP/opus-mt-en-zh")
tokenizer = AutoTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-zh")
inputs = tokenizer("The quick brown fox jumps over the lazy dog.", return_tensors="pt")
# 贪心解码
greedy_output = model.generate(**inputs, max_new_tokens=50)
# Beam Search
beam_output = model.generate(**inputs, max_new_tokens=50, num_beams=5, early_stopping=True)
print(tokenizer.decode(greedy_output[0], skip_special_tokens=True))
print(tokenizer.decode(beam_output[0], skip_special_tokens=True))
实测结果:贪心输出“敏捷的棕色狐狸跳过懒狗。”,Beam Search输出“敏捷的棕色狐狸跃过懒狗。”后者用词更准确。但若beam_size过大(如10),生成速度下降40%,且可能出现重复“的的”等噪声。因此,生产环境建议beam_size=4~6,并配合no_repeat_ngram_size=3。
常见误区
- 误区1:认为beam_size越大越好。实际上,beam_size超过8后收益递减,且显存和延迟显著增加。
- 误区2:忽略长度惩罚(length_penalty)。在长文本生成中,Beam Search倾向于短序列(因为概率乘积随长度递减),需设置
length_penalty(如1.0~2.0)来平衡。
2.5 综合对比与工程选型建议
| 解码策略 | 确定性 | 多样性 | 计算开销 | 典型应用 |
|---|---|---|---|---|
| 贪心解码 | 最高 | 最低 | 最低 | 代码补全、关键词提取 |
| Beam Search | 高 | 低 | 中(beam_size倍) | 机器翻译、文本摘要 |
| 温度采样 | 低 | 高 | 低 | 对话、创意写作 |
| Top-p采样 | 中 | 中高 | 低 | 通用生成、API服务 |
工程选型时,还需考虑批处理(batch)的影响。Beam Search在batch推理时需维护多个候选序列,显存占用为batch_size × num_beams × max_len,容易OOM。建议在长序列生成时使用采样策略,或采用分组Beam Search(如每batch内分多组)降低显存峰值。