第四篇:Embedding 层——从 ID 到向量的第一步
系列文章:
第一篇:预训练模型——站在巨人的肩膀上
第二篇:分词——文字如何变成数字
第三篇:向量与矩阵——理解一切的基石
📖 本文目录
- 开篇
- ID 的问题:数字的大小没有意义
- 查表操作:Embedding 的本质
- 代码验证:在 PyTorch 中看到 Embedding
- 这些向量是怎么来的?
- 一个关键限制:此时向量还是"孤立的"
- Embedding 层的物理意义与参数量
- 解决孤立信息的两种机制
- 一个小实验:动手查看 Embedding 输出
- 参考资源
- 本篇小结
开篇
经过分词,我们得到了一串数字 ID:[101, 2769, 4263, 1962, ...]。但这些 ID 本身毫无意义——“2769"这个数字比"2768"大,并不代表任何语义上的"更多"或"更强”。
模型需要把这些孤立的 ID 变成富含语义的向量。这一步,就是Embedding(嵌入)层。
一句话总结:Embedding 层就是一个巨大的查找表,把每个离散的 token ID 映射成一个连续的、稠密的语义向量。这是 NLP 模型将"符号"转化为"数值"的第一步。
ID 的问题:数字的大小没有意义
想象我们给三个词编号:
苹果 → ID=1 香蕉 → ID=2 汽车 → ID=50在计算机看来,“香蕉”(2)比"苹果"(1)大,但比"汽车"(50)小。这种大小关系毫无语义意义——香蕉不是苹果的两倍,汽车也不是香蕉的 25 倍。如果直接把 ID 作为输入,模型学到的是"数字大小",而不是"语义关系"。
这就是One-hot 编码——每个词用一个很长的向量表示,向量的维度等于词表大小,只有对应 ID 的那个位置是 1,其余全是 0:
词表有 4 个词: "的" → ID=0 → One-hot: [1, 0, 0, 0] "我" → ID=1 → One-hot: [0, 1, 0, 0] "喜欢" → ID=2 → One-hot: [0, 0, 1, 0] "你" → ID=3 → One-hot: [0, 0, 0, 1]向量的"1"永远只有一个,而且位置固定,所以叫 One-hot(独热编码)。它解决了 ID 大小没有意义的问题(所有 One-hot 向量两两正交,相似度都是 0),但代价是向量极度稀疏——如果词表有 21,128 个词,每个向量就有 21,128 维,里面只有 1 个 1,其余 21,127 个全是 0,非常浪费。
| 方案 | 维度 | 优点 | 缺点 |
|---|---|---|---|
| One-hot 编码 | vocab_size(21,128) | 简单、无偏 | 稀疏、维度灾难 |
| Embedding | hidden_size(768) | 稠密、可学习、含语义 | 需要训练得到 |
Embedding 层不仅把维度从 21,128 压缩到了 768(27 倍压缩),更重要的是它还通过学习让语义相似的词在向量空间中距离更近。这就是所谓的"分布式表示"(Distributed Representation)[1]——语义被分布到多个维度中共同编码。
为什么 One-hot 不可行?
假设词表有 21,128 个词,One-hot 向量的维度就是 21,128。每个向量只有一个位置是 1,其余全是 0。这不仅浪费存储空间,更致命的是:任何两个不同的 One-hot 向量都是正交的,意味着模型无法从 One-hot 编码中学到任何词与词之间的相似性。"苹果"和"香蕉"的相似度与"苹果"和"汽车"的相似度完全一样——都是 0。Embedding 用 768 维稠密向量从根本上解决了这一问题。
查表操作:Embedding 的本质
Embedding 层本质上是一个巨大的查找表:
词表(vocab.txt)就像一个超厚的词典: ID 0: [0.001, -0.002, 0.001, ...] (共768个数) ID 1: [0.005, 0.001, -0.003, ...] ... ID 101: [0.012, -0.008, 0.015, ...] ← [CLS] ID 102: [0.003, 0.007, -0.011, ...] ← [SEP] ... ID 2769: [0.021, -0.015, 0.033, ...] ← "我" ID 4263: [0.113, 0.072, -0.224, ...] ← "喜" ...说白了就是一个二维数组,形状是[21128, 768]——21128 行,每行 768 个浮点数。每一行就是一个词的向量。所谓的"Embedding 层",没有任何魔法,就是这么一张表。所谓"查表",就是根据 ID 去表里取对应的那一行。
查表的数学本质
简单说:Embedding 就是一个二维数组,查表就是按下标取行。
Embedding 矩阵 W(4行 x 3列): dim0 dim1 dim2 ID 0: [ 0.1, 0.2, -0.1 ] ← "的" ID 1: [ 0.5, 0.3, 0.2 ] ← "我" ID 2: [-0.2, 0.7, 0.4 ] ← "喜欢" ID 3: [ 0.3, -0.1, 0.8 ] ← "你" 输入 ID=1 → W[1] → [0.5, 0.3, 0.2] ✅没有任何魔法,和你写arr[1]取数组第 1 个元素完全一样,只不过这里取的是一整行(一个 768 维的向量)。
实际 PyTorch 代码:
embedding=nn.Embedding(num_embeddings=4,embedding_dim=3)input_ids=torch.tensor([1])# 要查 ID=1result=embedding(input_ids)# 内部就是 embedding.weight[1]# 输出: [[0.5, 0.3, 0.2]]补充:从数学角度看,这个操作等价于 One-hot 向量乘以矩阵(One-hot 里只有一个 1,乘法结果刚好等于那一行)。但实际实现不会真的去做乘法,因为 One-hot 太长了(21128 维全是 0),直接用索引取行快得多。
查表过程可视化: 输入IDs: 101 2769 4263 1962 ... 102 ↓ ↓ ↓ ↓ ↓ 查词表 → 拿到对应的 768 维向量 ↓ ↓ ↓ ↓ ↓ [0.012] [0.021] [0.113] [-0.040] ... [0.003] [-0.008] [-0.015] [0.072] [0.018] ... [0.007] [0.015] [0.033] [-0.224][0.092] ... [-0.011] ... ... ... ... ... ... (768行) (768行) (768行) (768行) (768行) 结果是一个矩阵:形状 [9, 768] 9 = 序列长度(token个数) 768 = 每个向量的维度对比:查表 vs 矩阵乘法
从数学上看,Embedding 等价于一个nn.Linear(21128, 768)层对 One-hot 向量做矩阵乘法——但没人真这么做,因为 One-hot 太长太稀疏,直接查表快得多。
代码验证:在 PyTorch 中看到 Embedding
importtorchimporttorch.nnasnn# 模拟一个迷你 Embedding 层:词表=100,向量维度=768embedding_layer=nn.Embedding(num_embeddings=100,embedding_dim=768)# 输入:3个token IDinput_ids=torch.tensor([[1,5,23]])# shape: [1, 3]# 查表output=embedding_layer(input_ids)print(output.shape)# torch.Size([1, 3, 768])# 查看第1个token的向量print(output[0,0,:5])# 前5个维度# tensor([-0.0231, 0.0158, -0.0412, 0.0325, -0.0107], grad_fn=<SliceBackward0>)在真实 BERT 模型中:
# 在模型内部,这一行代码完成了查表embedding_output=model.bert.embeddings.word_embeddings(input_ids)print(embedding_output.shape)# torch.Size([1, 9, 768])加上 batch 维度后,数据的实际形状是[1, 9, 768]——前面的 1 是 batch_size(一次输入一个句子)。
这些向量是怎么来的?
这是初学者最常问的问题:这些向量里的数字是谁定的?
答案是:训练出来的。
初始化:训练前都是随机数
一开始(训练前),词表里所有向量都是随机数:
ID 2769 ("我"): [0.023, -0.015, 0.041, -0.032, ...] ← 随机 ID 4263 ("喜"): [-0.031, 0.018, -0.052, 0.045, ...] ← 随机MLM 训练:完形填空驱动学习
在预训练过程中,模型通过完形填空(Masked Language Model, MLM)任务不断调整这些数字:
训练目标:预测 "[MASK] 喜欢人工智能" 中 [MASK] 位置是 "我" 每步训练循环: ① 随机 mask 掉句子中的 15% 的 token ② 前向传播 → 模型预测被 mask 掉的词 ③ 算交叉熵损失(预测越错,损失越大) ④ 反向传播 → 计算 Embedding 层每个维度的梯度 ⑤ 梯度下降 → 微调向量数值 ⑥ 重复数百万步...训练后的语义空间
MLM 任务最早由 BERT 论文 [2] 提出,是预训练的核心机制。模型通过数亿次这样的"完形填空"练习,学会了把语义相似的字映射到向量空间中相近的位置。
训练结束后,"我"和"我们"的向量会很接近(语义相似),“我"和"苹果"的向量会相距较远。语义相似度被编码在了向量的几何距离中。这个性质被称为"语义空间的线性结构”——经典的例子是v("king") - v("man") + v("woman") ≈ v("queen")[1]。
一个关键限制:此时向量还是"孤立的"
在 Embedding 之后,每个字的向量是独立的:
"行"(银行)的向量 = [0.012, -0.008, 0.015, ...] "行"(行走)的向量 = [0.012, -0.008, 0.015, ...] ↑ 完全一样!同一个字,在不同语境下含义不同,但 Embedding 层的输出是一样的。模型此时还不知道上下文——它只看到了孤立的字,没有看到字与字之间的关系。
Embedding 层的定位:它解决的是"符号 → 数值"的映射问题,不是"语境理解"问题。后者将由后续的 Transformer 层(自注意力机制)来解决。
Embedding 层的物理意义与参数量
| 参数 | 数值 | 说明 |
|---|---|---|
| 词表大小 | 21,128 | 覆盖几乎所有常用汉字 + 特殊标记 |
| 向量维度 | 768 | 每个字的语义编码长度 |
| 参数量 | 21,128 × 768 =16,226,304 | 约 1623 万参数 |
| 占模型总量 | 约15% | BERT base 共 1.1 亿参数 |
Embedding 层是 BERT 中除了 FFN 之外参数量最大的单层。它的 1623 万参数通过预训练学来了每个汉字的基础语义表示——但这只是"基础",真正的"理解"发生在后续的 12 层 Transformer 中。
解决孤立信息的两种机制
后续的 Transformer 层提供了两种机制来解决"孤立"问题:
| 机制 | 解决的问题 | 方法 | 对应文章 |
|---|---|---|---|
| 位置编码 | 字在句子中的顺序 | Token向量 + 位置向量 | 下一篇 |
| 自注意力 | 字与字之间的语义关系 | QKV 交互 + 加权融合 | 第 6-8 篇 |
类比:Embedding 就像给了每个演员一张角色照——但这张照片是定妆照,和你演对手戏的演员是谁、剧情发展到第几幕都没有关系。真正的"表演"(上下文理解)要从注意力机制开始。
一个小实验:动手查看 Embedding 输出
fromtransformersimportAutoModel,AutoTokenizerimporttorch tokenizer=AutoTokenizer.from_pretrained("bert-base-chinese")model=AutoModel.from_pretrained("bert-base-chinese")text="我喜欢人工智能"inputs=tokenizer(text,return_tensors="pt")# 手动获取 Embedding 层的输出withtorch.no_grad():embedding_output=model.bert.embeddings.word_embeddings(inputs["input_ids"])print(embedding_output.shape)# torch.Size([1, 9, 768])# 查看不同字的向量是否不同print("[CLS] 前3维:",embedding_output[0,0,:3])print(" '我' 前3维:",embedding_output[0,1,:3])print(" '喜' 前3维:",embedding_output[0,2,:3])# 计算"我"和"喜"的余弦相似度cos=torch.nn.CosineSimilarity(dim=0)sim=cos(embedding_output[0,1],embedding_output[0,2])print(f"'我'和'喜'的余弦相似度:{sim:.4f}")# 此时它们还没有上下文信息——但即使这样,预训练已经让# 常一起出现的字向量更接近了。注意:Embedding 层的输出只是"原始原料"。真正的语义理解在后面的 Transformer Block 中完成。
参考资源
[1] Mikolov et al., “Efficient Estimation of Word Representations in Vector Space”, 2013. arXiv:1301.3781
[2] Devlin et al., “BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding”, 2019. NAACL 2019
[3] PyTorch Embedding 官方文档. nn.Embedding
[4] Hugging Face Transformers 文档. BERT 模型
本篇小结
- Embedding 层是一个查找表,将每个 token ID 映射为 768 维向量
- 这些向量是通过预训练(MLM 任务)学出来的,不是人为设计
- 语义相似的词在向量空间中距离更近
- 此时每个字的向量还是孤立的——不知道上下文
- Embedding 层的参数量约 1623 万,占 BERT 总参数的 15%
- Embedding 解决了"符号 → 数值"问题,"语境理解"交给后续注意力机制
下一篇:Embedding 后,每个字有了自己的向量,但模型还不知道这些字在句子中的先后顺序。“我爱你"和"你爱我"用的字完全一样,顺序不同含义截然相反。下一篇,我们将看到位置编码如何给模型注入"顺序感”。