参考:《图解大模型》第2章
核心问题:LLM 只能处理数字,文本怎么变成数字?这些数字怎么携带语义?


一、问题:模型看不懂文字

神经网络本质上是矩阵乘法,只能处理数字。所以文本进入模型之前,需要经历两步转换:

"Hello world"
    ↓ 分词(Tokenization)
[9906, 1917]          ← 整数 ID
    ↓ 嵌入(Embedding)
[[0.23, -0.15, ...],  ← 每个 ID 变成一个浮点向量
 [0.11,  0.88, ...]]

二、分词:为什么不按字/词切?

直觉上有三种方案:

方案 问题
按整词切("hello " = 1个token) 词汇表爆炸(几十万词),遇到新词/变形词无法处理
按字符切(“h”,“e”,“l”,“l”,“o”) 序列极长,模型难以学习词语级语义
按子词切(BPE) 平衡词汇量和覆盖率,主流方案

BPE(字节对编码)的直觉:从字符开始,反复把最高频的相邻对合并成一个新 token,直到词汇表达到预设大小。

初始:  "l o w e r"  "l o w"  "n e w e r"
合并1:最高频对 e+r → er
       "l o w er"   "l o w"  "n e w er"
合并2:最高频对 l+o → lo
       "lo w er"    "lo w"   "n e w er"
...最终:常见词变成整体 token,罕见词被拆成子词

用 tiktoken 直接感受:

import tiktoken

enc = tiktoken.get_encoding("cl100k_base")  # GPT-4 使用的分词器

# 普通英文:通常整词就是一个 token
print(enc.encode("hello"))        # [15339]  — 1个token
print(enc.encode("Hello"))        # [9906]   — 大小写是不同 token!

# 罕见词被拆开
print(enc.encode("Unbelievable")) # [1844, 14110, 481] — 拆成3个子词
print(enc.encode("未知词语"))      # 中文每个字甚至被拆成多个字节token

# encode + decode 是互逆的
ids = enc.encode("Large Language Models")
print(ids)                        # [35, 4876, 16688, 27992]
print(enc.decode(ids))            # "Large Language Models"

关键直觉:token 不等于词,也不等于字符。"tokenization"可能是 1 个 token,也可能是 3 个,取决于它在训练数据里出现的频率。

运行 python tokenizer.py 可以交互式探索任意文本的分词结果。


三、嵌入:让数字携带语义

分词只是给每个 token 分配了一个整数 ID(像字典的页码),ID 本身没有语义。嵌入层做的事是:把 ID 映射到一个高维空间里的向量,让语义相近的词向量也相近

为什么训练后语义相近的词向量会聚拢?

关键在于预训练任务:预测下一个词

"The cat sat on the [mat]"
   ↓ 预测 "mat"
"The dog sat on the [mat]"  ← "cat" 和 "dog" 出现在几乎相同的上下文中
   ↓ 预测 "mat"
"The animal sat on the [mat]" ← "dog" 和 "animal" 也能互相替换

训练过程中:

  • “cat” 和 “dog” 经常出现在相似的上下文里(能互相替换预测出后面的词)
  • 模型发现:把 “cat” 和 “dog” 的向量靠在一起,对预测更有利
  • 因为:这样 attention 层可以从对方的"经验"中获益

结果是:经常能互相替换的词,向量自然聚拢——这就是语义相似度的本质。

Token ID → 嵌入矩阵查表 → 浮点向量

"Hello" (ID=9906) → [0.23, -0.15, 0.88, ..., 0.05]  (768维)
"Hi"    (ID=15338) → [0.21, -0.13, 0.85, ..., 0.04]  (768维)
"Dog"   (ID=39)    → [-0.8,  0.44, -0.12, ..., 0.77]  (768维)

余弦相似度:
  sim("Hello", "Hi")  ≈ 0.97  ← 很接近
  sim("Hello", "Dog") ≈ 0.12  ← 很远

这个嵌入矩阵是可学习的参数,在预训练过程中不断更新,使得语义相近的词向量自然聚拢。

用 transformers 感受一下:

from transformers import AutoTokenizer, AutoModel
import torch

tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
model = AutoModel.from_pretrained("bert-base-uncased")

# 只看嵌入层(不经过 Transformer)
inputs = tokenizer(["hello", "hi", "dog"], return_tensors="pt", padding=True)
with torch.no_grad():
    embeddings = model.embeddings.word_embeddings(inputs["input_ids"])

# 计算相似度
from torch.nn.functional import cosine_similarity
e_hello = embeddings[0, 1]  # "hello" 的嵌入向量
e_hi    = embeddings[1, 1]  # "hi"
e_dog   = embeddings[2, 1]  # "dog"

print(cosine_similarity(e_hello.unsqueeze(0), e_hi.unsqueeze(0)))   # 接近 1
print(cosine_similarity(e_hello.unsqueeze(0), e_dog.unsqueeze(0)))  # 较低

四、位置编码:告诉模型"我是第几个词"

Transformer 的自注意力是全局并行的——它同时看所有词,本身不知道词的顺序。"猫追狗"和"狗追猫"在没有位置信息时对模型来说是一样的。

实现方式:逐元素相加(不是拼接,是两个向量每个维度相加):

"猫"的词向量:   [0.2, -0.1, 0.8]
位置0的编码:     [0.1,  0.5, -0.3]
相加:           [0.3,  0.4, 0.5]  ← 最终输入

维度必须相同(都是 d_model 维)

模型如何"理解"这个位置信息?

关键在注意力计算。注意力用点积衡量两个词的相关性,而点积对向量方向敏感:

"猫"(位置0): [0.3, 0.4, 0.5]
"追"(位置1): [0.4, 0.5, 0.4]  ← 位置编码不同,向量方向不同

点积("猫", "追") = 0.3×0.4 + 0.4×0.5 + 0.5×0.4 = 0.54

如果"猫"在位置1、"追"在位置0,由于位置编码变了,点积结果也会不同。模型在训练中学会:同样的语义关系,在不同相对位置会有不同的注意力分数模式

不是让模型记住"这是第5个词",而是让注意力机制能感知"这两个词相距多远"。

关键:位置编码不是任意向量,而是精心设计的

原始 Transformer 用正弦/余弦函数生成位置编码:

位置 pos 的第 i 维 = sin(pos / 10000^(2i/d_model))

这种设计的妙处:

  • 不同位置的编码有固定模式,模型能泛化到训练时没见过的长度
  • 相对位置可以通过线性变换推导出来(PE(pos+k) 可以从 PE(pos) 变换得到)

RoPE(旋转位置编码)更进一步:把位置信息编码成旋转角度,点积自然体现相对距离。

简单说:不是"加了位置向量就自动有位置信息",而是位置编码的设计让注意力计算能感知相对位置

两种主流方案:

方案 代表模型 特点
绝对位置编码(可学习) GPT-2、BERT 简单,但长度固定(训练时定死)
旋转位置编码(RoPE) LLaMA、Qwen 泛化性好,支持更长的上下文

直觉:位置编码不是单独记"我是第5个词",而是把位置信息编码进向量方向,让注意力计算时自然感知到相对位置关系。


五、思考题

  1. “tokenization” 被拆成 3 个 token,“the” 是 1 个 token。这意味着处理中文时 token 消耗会更多吗? 用 tiktoken 验证一下。
  2. 嵌入矩阵在预训练前是随机初始化的,为什么训练后语义相近的词向量会自动聚拢? 提示:想想预测下一个词的任务如何迫使模型学出这个结构。
  3. 如果去掉位置编码,"猫追狗"和"狗追猫"的模型输出会完全一样吗? 提示:注意力机制本身能感知顺序吗?

检验:能用 tiktoken 对任意文本分词并解码;能解释为什么词嵌入能表示语义相似度。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐