系列文章导航:AI系列文章导航目录-持续更新中
第04课:Tokenization与模型推理流程
📝 本文摘要:本文详解Tokenization(BPE/WordPiece/Unigram三种主流方法)及其对模型"字数限制"、"数错字"和成本的实际影响,并逐步拆解从用户输入到模型输出的完整推理流程(Tokenization→特殊Token→Embedding→位置编码→Transformer层→采样→自回归→Detokenization),涵盖采样参数(Temperature/Top-p/Top-k)和KV Cache加速原理,附OpenAI API实战代码。
Tokenization是文本到模型的桥梁,理解它你才能理解:为什么模型会"数错字"?为什么不同模型的最大上下文不同?为什么Prompt要精炼?
一、什么是Tokenization
核心问题:模型不能直接处理文本,必须先把文本切分成一个个"Token",再映射为数字ID。
"Hello World" → Tokenize → ["Hello", " World"] → [15496, 995]
Token不等于字符,也不等于词。它是介于字符和词之间的一个单位。
二、主流Tokenization方法
2.1 演进历史
词级别分词 (Word-level)
问题: 词表太大(英语就有几十万词),OOV问题(未登录词无法处理)
字符级别分词 (Character-level)
问题: 序列太长,训练慢,语义信息弱(单个字符没意义)
子词分词 (Subword-level) ← 当前主流
核心思想: 高频词保留,低频词拆分
"unfriendly" → ["un", "friend", "ly"]
2.2 BPE(Byte Pair Encoding,字节对编码)⭐最主流
算法直觉:
初始: 每个字符(包括字节)是一个Token
反复: 找到最频繁的相邻Token对,合并为新Token
直到: 达到目标词表大小
示例:
初始: h e l l o w o r l d
第1轮: 合并 "l"+"l" → "ll"
h e ll o w o r ll d
第2轮: 合并 "h"+"e" → "he"
he ll o w o r ll d
...最终可能: "hello" "world" 各为一个Token
核心思想:高频词保留完整(如"hello"),低频词拆成常见子词(如"unfriendly"→"un"+"friend"+"ly")。这样词表不需要包含所有词,也能处理未登录词(OOV,Out-of-Vocabulary,词表中没有的新词)。
被谁用:GPT系列、LLaMA、Qwen、DeepSeek
2.3 WordPiece(词片分词)
与BPE类似,但合并标准不同:
- BPE:选频率最高的对
- WordPiece:选使得语言模型似然(LM Likelihood,语言模型概率)增加最多的对
被谁用:BERT、DistilBERT
2.4 SentencePiece / Unigram(句子片段/单字分词)
SentencePiece(Google开发的开源分词工具):一个分词框架,支持BPE和Unigram两种算法,特别适合多语言场景。
Unigram Language Model(单字语言模型分词):与BPE相反的方向:
- BPE:从字符逐步合并(自底向上)
- Unigram:从大词表逐步裁剪(自顶向下),删减使似然减少最少的子词
被谁用:T5(Text-to-Text Transfer Transformer)、ALBERT(A Lite BERT)、多语言模型
三、Tokenization的实际影响
3.1 为什么不同模型的"字数限制"不同
同样1万字中文:
GPT-4 (cl100k_base): ~5000 tokens
Llama 3 (128K词表): ~3000 tokens
Qwen2.5 (151K词表): ~2500 tokens
词表越大 → 编码越高效 → 同样字数消耗更少token → 上下文能装更多内容
3.2 为什么模型会"数错字"
"strawberry" → GPT-4 tokenize → ["str", "aw", "berry"]
↑
"berry"是一个Token,模型看不到里面的3个r
所以模型说strawberry有2个r------因为Token内部对模型是不可见的
解决方案:让模型逐字符检查,或用代码执行来计数。
3.3 Token与成本
API计费: 按1000 tokens收费
1个汉字 ≈ 1-2 tokens (取决于模型)
1个英文单词 ≈ 1-1.5 tokens
一段1000字中文 ≈ 1500 tokens
GPT-4o: $5/1M input tokens → 约0.75分钱
DeepSeek-V3: ¥1/1M input tokens → 约0.15分钱
四、完整推理流程详解
4.1 从用户输入到模型输出
Step 1: 用户输入
"讲一个笑话"
Step 2: Tokenization
["讲", "一个", "笑话"] → [2523, 10387, 33111]
Step 3: 添加特殊Token
[BOS, 2523, 10387, 33111, EOS]
BOS=Beginning of Sequence(序列起始符)
EOS=End of Sequence(序列结束符)
这些特殊Token告诉模型"文本开始了"和"文本结束了"
Step 4: Embedding
每个Token ID → 查Embedding表 → d_model维向量
[2523] → [0.12, -0.34, 0.56, ...] (4096维)
Step 5: 位置编码
加上位置信息: embedding + positional_encoding
Step 6: Transformer层(N层)
每层: Causal Attention → Add&Norm → FFN → Add&Norm
Step 7: 输出logits
最后一层 → 线性层 → 词表大小的向量 (如151936维)
每个维度代表对应Token的概率(未归一化)
Step 8: 采样(Sampling,从概率分布中选择Token的过程)
logits → softmax → 概率分布 → 选择下一个Token
采样策略(Sampling Strategy):
- Greedy(贪心解码): 选概率最高的,输出确定性最高,但多样性最低
- Top-k: 从概率最高的k个Token中按概率采样
- Top-p (Nucleus Sampling,核采样): 从累积概率达到p的最少Token集合中采样
- Temperature(温度参数): 调整概率分布的"锐度"
T→0: 分布更尖锐(几乎确定性),输出更确定但无聊
T→∞: 分布更平坦(完全均匀),输出更随机但多样
T=1: 原始概率分布
Step 9: 自回归
把选中的Token加入输入,重复Step 4-8
直到生成EOS或达到max_tokens
Step 10: Detokenization
Token IDs → 文本
[2523, 10387, 33111, 456, 789, ...] → "讲一个笑话:..."
4.2 采样参数详解
python
# Temperature: 控制随机性
# T→0: 几乎确定性的输出(总是选概率最高的)
# T→∞: 完全随机
# T=1: 原始概率分布
# 推荐: 代码生成0.0-0.2, 创意写作0.7-1.0
# Top-p: 核采样
# 只从累积概率达到p的最少Token集合中采样
# top_p=0.9 → 选最少的Token使其概率和≥0.9
# Top-k: 只从概率最高的k个Token中采样
# top_k=50 → 只看前50个候选
# Frequency/Presence Penalty: 惩罚重复
# 减少"车轱辘话"的效果
4.3 KV Cache加速
无KV Cache:
生成第1个Token: 计算Token1的Q,K,V
生成第2个Token: 重新计算Token1,2的Q,K,V ← 浪费!
生成第3个Token: 重新计算Token1,2,3的Q,K,V ← 更浪费!
有KV Cache:
生成第1个Token: 计算Token1的Q,K,V,缓存K,V
生成第2个Token: 只计算Token2的Q,K,V,Token1的K,V从缓存取
生成第N个Token: 只计算TokenN的Q,K,V,前面N-1个从缓存取
效果: 推理从O(n²)降到O(n)
五、OpenAI API推理实战
5.1 基本调用
python
from openai import OpenAI
client = OpenAI(api_key="your-key")
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "你是一个有帮助的助手。"},
{"role": "user", "content": "讲一个笑话"}
],
temperature=0.7,
max_tokens=500,
top_p=0.9
)
print(response.choices[0].message.content)
5.2 流式输出
python
stream = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "写一首诗"}],
stream=True # 关键参数
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
5.3 Token使用量
python
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "你好"}]
)
print(f"Prompt tokens: {response.usage.prompt_tokens}")
print(f"Completion tokens: {response.usage.completion_tokens}")
print(f"Total tokens: {response.usage.total_tokens}")
📝 作业
作业1:用tiktoken计算Token数量
安装tiktoken,计算以下文本在不同编码下的token数量,观察差异。
python
import tiktoken
text = "Hello World! 你好世界!这是一个测试。"
for encoding_name in ["cl100k_base", "o200k_base", "p50k_base"]:
enc = tiktoken.get_encoding(encoding_name)
tokens = enc.encode(text)
print(f"{encoding_name}: {len(tokens)} tokens")
print(f" Token IDs: {tokens}")
print(f" Decoded: {[enc.decode([t]) for t in tokens]}")
print()
参考答案(预期输出):
cl100k_base: 约14 tokens
中文每个字约1-2 tokens,英文每个词约1 token
o200k_base: 约10 tokens
GPT-4o的新编码,中文编码更高效
p50k_base: 约16 tokens
旧版编码,中文编码效率低
作业2:用OpenAI API(或兼容API)完成一次完整的对话
要求:
- 设置system prompt
- 进行多轮对话
- 开启流式输出
- 打印token使用量
参考答案:
python
from openai import OpenAI
# 也支持任何OpenAI兼容API,只需修改base_url
client = OpenAI(
api_key="your-key",
# base_url="https://api.deepseek.com" # DeepSeek
)
messages = [
{"role": "system", "content": "你是一个Python编程专家,回答简洁。"},
{"role": "user", "content": "什么是列表推导式?"}
]
# 第一轮
response = client.chat.completions.create(
model="gpt-4o-mini", # 便宜的模型用于练习
messages=messages,
temperature=0.3,
max_tokens=300
)
assistant_msg = response.choices[0].message.content
print(f"助手: {assistant_msg}")
print(f"Tokens: {response.usage}\n")
# 第二轮
messages.append({"role": "assistant", "content": assistant_msg})
messages.append({"role": "user", "content": "给一个嵌套列表推导式的例子"})
# 流式输出
stream = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
stream=True
)
print("助手: ", end="")
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
print()
下一篇文章见:AI系列文章导航目录-持续更新中