大模型是怎样「一个字一个字」生成文本的
Atomind4 分钟阅读--- 次阅读
On this page
和大模型聊天时,回复是一点点「流」出来的。这不是前端特效,而是模型真实的工作方式:每一步只预测下一个 token,再把它拼回输入,继续预测下一个。这个过程叫自回归生成(autoregressive generation)。
第一步:文本变成 token
模型看不到汉字,只看得到整数。Tokenizer 负责把文本切成 token,再映射为词表里的编号:
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")ids = tok.encode("原子思维")print(ids) # 一串整数print([tok.decode([i]) for i in ids]) # 每个整数对应的片段一个汉字可能是一个 token,也可能被拆成多个字节级片段;常见词组也可能合并成一个 token。计费、上下文长度,统计的都是 token 数,而不是字数。
第二步:模型输出 logits
把 token 序列送进模型,最后一层会为词表中的每一个 token 打一个分数,叫 logits。词表通常有十几万项,所以每一步的输出就是一个十几万维的向量。
import torchfrom transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")inputs = torch.tensor([tok.encode("天空是")])with torch.no_grad(): logits = model(inputs).logits[0, -1] # 只看最后一个位置print(logits.shape) # torch.Size([词表大小])第三步:从 logits 采样下一个 token
logits 经过 softmax 变成概率分布,再从中选出一个 token。选法决定了生成的「性格」:
| 参数 | 作用 | 直觉 |
|---|---|---|
temperature | 先把 logits 除以它再做 softmax | 越小越保守,越大越发散;趋近 0 时等价于每次选最大值(贪心) |
top_k | 只在概率最高的 k 个里采样 | 砍掉长尾的离谱选项 |
top_p | 只在累计概率达到 p 的最小集合里采样 | 根据分布形状自适应地截断 |
def sample(logits, temperature=0.7, top_p=0.9): probs = torch.softmax(logits / temperature, dim=-1) sorted_probs, idx = probs.sort(descending=True) cutoff = sorted_probs.cumsum(-1) > top_p cutoff[1:] = cutoff[:-1].clone() # 保留刚好越过阈值的那一个 cutoff[0] = False sorted_probs[cutoff] = 0 choice = torch.multinomial(sorted_probs / sorted_probs.sum(), 1) return idx[choice].item()第四步:循环
把选出的 token 追加到输入末尾,重复上面的过程,直到生成结束符或达到长度上限:
ids = tok.encode("用一句话解释什么是大模型:")for _ in range(60): with torch.no_grad(): logits = model(torch.tensor([ids])).logits[0, -1] next_id = sample(logits) if next_id == tok.eos_token_id: break ids.append(next_id)print(tok.decode(ids))这就是一个完整的(虽然很慢的)生成循环。实际的推理框架会用 KV Cache 把前面算过的注意力结果缓存起来,每一步只计算新 token,速度能快上一个数量级——这是后续文章的主题。
小结
- 模型每一步只做一件事:给下一个 token 的所有候选打分。
- 「创造力」主要来自采样策略,而不是模型本身在「思考」。
- 理解了这个循环,流式输出、停止词、最大长度、KV Cache 这些概念就都有了落脚点。