Skip to content

Tokenization 与 Embedding

文本到数字:为什么需要 Tokenization?

Section titled “文本到数字:为什么需要 Tokenization?”

大语言模型不认识文字,只能处理数字。Tokenization 就是把文本拆分成小单元(Token),再映射为数字 ID 的过程。

"我喜欢机器学习" → ["我", "喜欢", "机器", "学习"] → [8821, 35946, 9554, 15206]

GPT 系列使用的方法。核心思想:从单个字符开始,反复合并出现频率最高的相邻字符对。

训练过程示意:
初始词表: [a, b, c, d, ...]
第 1 轮: 合并 (t, h) → th (因为 "th" 出现最频繁)
第 2 轮: 合并 (th, e) → the ("the" 出现最频繁)
第 3 轮: 合并 (i, n) → in
...重复直到词表达到目标大小

BPE 的优势:能处理任何语言,罕见词自动拆分为子词,不会出现 OOV(Out of Vocabulary)。

BERT 使用的方法,与 BPE 类似,但合并依据是最大化语言模型的似然度,而非频率。

Google 开发的独立分词工具,将整个句子视为原始输入(包括空格),不依赖预分词。支持 BPE 和 Unigram 两种算法。LLaMA 等模型使用此方法。

特性BPEWordPieceSentencePiece
合并策略按字符对出现频率合并按语言模型似然度合并支持 BPE 和 Unigram 两种
预分词需要(先按空格/标点拆分)需要不需要(直接处理原始字节流)
多语言支持好,但依赖预分词规则最好(语言无关,对中日韩特别友好)
代表模型GPT 系列、ClaudeBERT、DistilBERTLLaMA、T5、ALBERT
OOV 处理回退到字节级别回退到字符级别(## 前缀)回退到字节级别
空格处理特殊前缀(如 Ġ## 标记延续 表示词首空格
flowchart LR
    A["原始文本\n'Hello world'"] --> B["Tokenizer\n['Hello', ' world']"]
    B --> C["Token IDs\n[9906, 1917]"]
    C --> D["Embedding 层\n高维向量"]
# 使用 tiktoken(OpenAI 的 tokenizer)
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4")
tokens = enc.encode("Hello, 大语言模型!")
print(tokens) # [9906, 11, 220, 28947, 99454, 39013, 0]
print(len(tokens)) # 7 个 token
# 解码回文本
text = enc.decode(tokens)
print(text) # "Hello, 大语言模型!"

Token 数量直接影响:

  • 成本:API 按 token 计费
  • 速度:token 越多,推理越慢
  • 上下文窗口:输入 + 输出的 token 总和不能超过模型的上下文窗口

经验法则:1 个英文单词 ≈ 1-1.5 个 token;1 个中文字 ≈ 1.5-2 个 token。

Token ID 只是一个编号,没有语义信息。Embedding 层将每个 ID 映射为一个高维向量(如 768 维或 4096 维),使得语义相近的词在向量空间中距离更近。

在实际应用中(尤其是 RAG 场景),Embedding 不只是”把一个词变成向量”这么简单。完整流程包括:

flowchart LR
    Doc["原始文档"] --> Chunk["分块\nChunking"]
    Chunk --> Emb["Embedding\n生成向量"]
    Emb --> Store["存入向量数据库"]
    Store --> Search["相似性检索"]
  1. 分块(Chunking):长文档需要切分成合适大小的文本块,太大检索不精确,太小丢失上下文
  2. Embedding 生成:用 Embedding 模型将每个文本块转换为高维向量
  3. 存储与索引:将向量存入向量数据库(如 Chroma、Pinecone),建立索引
  4. 相似性检索:用户提问时,将问题也转为向量,在数据库中找到最相似的文本块
🐾 动物区域
猫 🔵
狗 🔵
鸟 🔵
🚗 交通工具区域
🟠 汽车
🟠 卡车
🟠 火车

向量空间示意:语义相近的词聚集在一起,不同类别的词彼此远离

两个向量的相似程度通常用余弦相似度(Cosine Similarity) 来衡量。

import numpy as np
def cosine_similarity(a, b):
"""余弦相似度:衡量两个向量的方向是否一致"""
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# 示例(真实向量维度远高于此)
vec_cat = np.array([0.9, 0.1, 0.8])
vec_dog = np.array([0.85, 0.15, 0.75])
vec_car = np.array([0.1, 0.9, 0.2])
print(cosine_similarity(vec_cat, vec_dog)) # ≈ 0.99(猫和狗很相似)
print(cosine_similarity(vec_cat, vec_car)) # ≈ 0.45(猫和车差异大)

Embedding 是 RAG(检索增强生成) 的基石:

flowchart TD
    Q["用户问题: '如何部署 Kubernetes?'"] --> Emb["Embedding 模型将问题转为向量"]
    Emb --> Search["在向量数据库中检索最相似的文档"]
    Search --> Docs["找到相关文档作为上下文"]
    Docs --> LLM["LLM 基于检索到的内容生成回答"]

Agent 利用 Embedding 实现:

  1. 知识检索:从海量文档中快速找到相关信息
  2. 记忆管理:将历史对话编码存储,按语义检索
  3. 工具选择:将工具描述编码,根据任务语义匹配最合适的工具

常用 Embedding 模型:OpenAI text-embedding-3-smallBAAI/bge-m3Cohere embed-v3

自测题 1:为什么 BPE 不会遇到 OOV 问题?
BPE 的最小粒度是单个字节(或字符),这意味着任何文本都可以被表示。即使遇到从未见过的词(比如新造的品牌名"Xyloquz"),BPE 也会把它拆分为更小的已知子词或字符来表示。最极端的情况是每个字符各占一个 token,虽然效率低但绝不会出现"词表里没有"的情况。

相比之下,传统词表方法(如 Word2Vec)使用固定词表,遇到未登录词就只能用一个通用的 [UNK] 标记替代,丢失了所有信息。
自测题 2:为什么中文比英文消耗更多 token?
大多数 LLM 的 tokenizer 以英文语料为主训练,英文常见词(如"the"、"and")被高频合并为单个 token。而中文字符在训练语料中出现频率相对较低,往往需要多个 token 来表示一个汉字。

从编码层面看,UTF-8 下一个汉字占 3 个字节,而一个英文字母只占 1 个字节。这意味着同样的语义内容,中文在底层就需要更多的字节来表示。实际测试中,同样含义的一段话,中文版通常比英文版多消耗 30%-100% 的 token,直接影响 API 费用和上下文窗口利用率。
自测题 3:Embedding 向量的维度越高越好吗?
不一定。更高维度可以表达更丰富的语义细节,但也带来更大的存储和计算开销——向量数据库的索引大小、相似度计算的速度都会受影响。

例如,OpenAI 的 text-embedding-3-small 默认 1536 维,但通过 dimensions 参数降到 256 维后,在很多检索任务上性能只下降几个百分点,而存储和计算成本降低了 6 倍。实际项目中需要根据精度需求和资源预算做权衡,通常先用默认维度跑基线,再尝试降维看效果损失是否可接受。