【大模型语言基础(2)】文本如何变成数字 — 分词与嵌入

文章目录

参考:《图解大模型》第 2 章

核心问题:大模型只能算数字,怎么看得懂文字?这些数字又是怎么带上语义的? geeksforgeeks


一、模型其实看不懂文字

先把一个事实说清楚:神经网络本质上就是在做矩阵运算,只吃得下数字。 geeksforgeeks

所以一句人类写的文本,在进到模型之前,一定要变成一堆数字,大致是这两步:

text 复制代码
"Hello world"
    ↓ 分词(Tokenization)
[9906, 1917]          ← 每个 token 对应一个整数 ID
    ↓ 嵌入(Embedding)
[[0.23, -0.15, ...],  ← 每个 ID 映射成一个浮点向量
 [0.11,  0.88, ...]]

后面的 Transformer、自注意力,全都是在这些向量上折腾。

所以,「文本怎么变成数字 + 数字怎么带上语义 」,几乎是理解 LLM 的起点。 mbrenndoerfer


二、分词:为什么不能简单按「字/词」来切?

直觉上我们有三种朴素方案:

方案 听起来很自然,但有什么坑?
按整词切("hello" 算 1 个 token) 词汇表会炸掉几十万、上百万个词,新词和变形词(比如复数、时态)特别难搞。
按单个字符切("h","e","l","l","o") 序列会变得超长,模型很难直接从单字符里学到「单词级」语义。
按子词(subword)切,比如 BPE 在词汇量、序列长度、覆盖率之间找了一个不错的平衡,是现在主流做法。 mbrenndoerfer

BPE 的直觉:从字符出发,一点点「合并常见片段」

BPE(Byte Pair Encoding)一开始是做压缩的,后来被改造成了分词算法。 jonkrohn

它干的事可以简单理解为四步: codesignal

  1. 一开始只认识「字符级」token(包括空格、字节等)。
  2. 统计整个语料里,哪些相邻字符对最常一起出现
  3. 把最常见的一对合并成一个新 token。
  4. 重复「统计 → 合并」,直到词表大小达到你设定的上限。

举个极简例子(伪代码感):

text 复制代码
初始语料:
"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,冷门词就被拆成子词。

所以在一个 BPE 分词器眼里:

  • 「非常常见」的词,往往就是一个 token。
  • 「不太常见」的词,会被拆成几段可重用的子词。
  • 完全没见过的新词,可以回退到更小的单位(比如字符或字节),不会直接报废。

tiktoken 感受一下(GPT-4 系列使用的那套分词方案):

python 复制代码
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 不等于「词」也不等于「字」。

同一个词,可能是 1 个 token,也可能被拆成 2、3 个,取决于它在训练数据里出现得多不多。 rccchoudhury.github

三、嵌入:让数字真正「长出语义」

到目前为止,我们做的只是:

把一串文字 → 一串整数 ID。

这些 ID 本身是没有任何语义的,只是「字典页码」。
嵌入层要做的事情,是把每个 ID 映射成一个向量,并让「语义相近的词向量更靠近」。 blog.csdn

你可以把嵌入矩阵想象成一个巨大的「词向量表」: blog.csdn

text 复制代码
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]
"Dog"   (ID=39)    → [-0.8,  0.44, -0.12, ..., 0.77]

如果你算一下余弦相似度,大致会看到类似的现象:

text 复制代码
sim("Hello", "Hi")  ≈ 0.97   ← 很接近
sim("Hello", "Dog") ≈ 0.12   ← 很远

为什么训练完之后,相似词自然会「抱团」?

关键在预训练任务:预测下一个词(或者被 mask 掉的词)geeksforgeeks

看看这几句:

text 复制代码
"The cat sat on the [mat]"
"The dog sat on the [mat]"
"The animal sat on the [mat]"
  • 「cat」「dog」「animal」经常出现在类似的语境里:
    坐在某个地方、后面可能跟「mat」「sofa」这种词。
  • 如果「cat」和「dog」的向量离得很近,模型就能更容易「类比迁移」:
    学会了 cat 的某些用法,同时也更好地用在 dog 上。
  • 反过来,如果它们向量距离很远,模型每次都要「单独从头学」各自的用法,效果会更差。

在「损失函数 + 梯度下降」的共同作用下,最终会得到这么一种状态:

经常出现在相似上下文里的词,它们的向量会自动聚在一起。

这就是我们口中「语义相似度」的来源。

transformers 可以直接看一眼嵌入层的样子: geeksforgeeks

python 复制代码
from transformers import AutoTokenizer, AutoModel
import torch
from torch.nn.functional import cosine_similarity

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

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

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)))  # 会明显低很多

嵌入矩阵本身就是模型参数,会在预训练过程中不断被更新,慢慢「长成」一个有语义结构的空间。 blog.csdn


四、位置编码:告诉模型「我在第几个位置」

有了 token + 向量,我们好像就可以直接把这串向量丢进 Transformer 了?

答案是:还差一味关键调料------位置信息kazemnejad

自注意力有个特点:

一上来就能同时看到所有位置,但它不自带顺序感

如果不显式注入位置信息,模型眼里:

text 复制代码
「猫 追 狗」 和 「狗 追 猫」 只是三个向量的一个排列组合

为了让模型知道「谁在前、谁在后」,我们需要给每个位置加一个位置编码(positional encoding): kazemnejad

怎么加?就是「逐元素相加」

text 复制代码
"猫" 的词向量: [0.2, -0.1, 0.8]
位置 0 的编码: [0.1,  0.5, -0.3]
相加得到:     [0.3,  0.4, 0.5]  ← 真正喂进 Transformer 的向量

(维度要和模型隐藏维度一致,比如都是 768 维)

为什么这么加就能让模型感知位置?

因为注意力里的相似度是靠点积算的,位置编码会改变向量的方向

于是同样两个词,调换顺序之后,它们之间的注意力分数就会发生系统性变化。 kazemnejad

更进一步,位置编码不是随便瞎编的,而是精心设计过的。

绝对位置编码(原始 Transformer)

经典 Transformer 论文里用了一种基于正弦 / 余弦的公式: kazemnejad

  • 不同维度用不同频率的 sin/cos 函数。
  • 不同位置的编码之间有一定的数学关系,方便模型推理「相对距离」
  • 还具备一定的「长度泛化」能力:训练时见过 512 长度,推到稍微长一点也不至于完全崩掉。

RoPE(旋转位置编码)

后来的 LLaMA、Qwen 等模型使用了 RoPE(Rotary Position Embedding): emergentmind

  • 不是简单「加一个位置向量」,而是把 Q/K 看成在高维空间里做一次旋转
  • 旋转角度跟「位置」有关,结果是:
    点积里自然带上了「相对位置信息」
  • 对长上下文更友好,也更利于外推到比训练时更长的序列。 faculty.cc.gatech

可以简单记成:

位置编码的设计目标,是让注意力分数对「相对位置」变得敏感。 blog.csdn


五、*总结:三步串起来:从文本到「可计算的语义空间」

把本模块的主线串一下,就是这三步:

  1. 分词(Tokenization)

    • 把原始文本拆成 token 序列,
    • 通常用 BPE 这样的子词算法,在「词表大小」「覆盖率」「序列长度」之间找平衡。 jonkrohn
  2. 嵌入(Embedding)

    • 每个 token → 一个向量,
    • 预训练的目标让「语义相近、用法相似」的 token 向量自动靠得更近。 geeksforgeeks
  3. 位置编码(Positional Encoding / RoPE)

    • 把「我是第几个」和「我和别人相距多远」编码进向量空间,
    • 让注意力在计算相关性时,自然对顺序和距离变得敏感。 blog.csdn

做完这三步,一行纯文本就被翻译成了一串「带着语义和位置信息的向量」

后面所有的 Transformer Block、自注意力、多头机制,都是在这个向量世界里继续加工。


六、给你的三个小思考

这些问题很适合你边跑代码边想一想:

  1. 同一句中文,比起英文,token 数量往往更多,这对「按 token 计费」意味着什么?

    tiktoken 分别试试一段中英文,看看差异。

  2. 嵌入矩阵一开始是随机的,为什么训练后会自动长成「语义空间」?

    可以从「预测下一个词」的损失函数角度想一想。

  3. 如果我们把所有位置编码都去掉,"猫追狗" 和 "狗追猫" 在模型眼里会多像?

    自注意力本身有没有办法区分它们?

相关推荐
极客老王说Agent2 小时前
别被OpenClaw的30万Star晃了眼!AI产业逻辑重写后,打工人更该看清谁在“真干活”
人工智能·ai·chatgpt
AAI机器之心3 小时前
这个RAG框架绝了:无论多少跳,LLM只调用两次,成本暴降
人工智能·python·ai·llm·agent·产品经理·rag
xixixi777773 小时前
安全嵌入全链路:从模型训练到智能体交互,通信网络是AI安全的“地基”
人工智能·安全·ai·多模态·数据·通信·合规
Agent产品评测局3 小时前
企业 AI Agent 落地,如何保障数据安全与合规?——企业级智能体安全架构与合规路径深度盘点
人工智能·安全·ai·chatgpt·安全架构
VIP_CQCRE3 小时前
零成本 AI 副业:使用链接赚取你的第一桶金,手把手教程
ai
x-cmd3 小时前
[x-cmd] 终端里的飞书:lark-cli,让 AI Agent 拥有“实体办公”能力
java·人工智能·ai·飞书·agent·x-cmd
克里斯蒂亚诺·罗纳尔达4 小时前
智能体学习2——执行流的艺术与提示链模式
ai
金融RPA机器人丨实在智能5 小时前
OpenClaw正在重写AI产业逻辑:当“行动式AI”席卷全球,实在Agent如何定义商业新范式?
人工智能·ai