1. Embedding基础:词嵌入与独热编码对比
各位同学,今天我们来聊聊Embedding。说实话,这个词在深度学习里太常见了,但很多人其实没真正搞懂它到底在干什么。我刚开始接触NLP时,也踩过不少坑,今天就把这些经验分享给你们。
1.1 独热编码:最朴素的想法
先说说独热编码。假设我们有一个词汇表,里面有10万个词。独热编码的做法很简单——每个词用一个10万维的向量表示,其中只有1个位置是1,其余全是0。
举个例子:
- "猫" → [1, 0, 0, 0, ...]
- "狗" → [0, 1, 0, 0, ...]
- "鱼" → [0, 0, 1, 0, ...]
听起来挺直观的对吧?但问题来了:
- 维度灾难:10万维的向量,存储和计算都是噩梦。我早期一个项目里,光存储词向量就占了几个GB,训练时GPU直接爆显存。
- 语义鸿沟:任何两个词之间的余弦相似度都是0。也就是说,"猫"和"狗"在数学上毫无关系——这显然不合理。
- 无法表达相似性:你没法说"猫"和"老虎"比"猫"和"汽车"更接近。
说白了,独热编码就是给每个词发了个身份证号,但完全没告诉我们这个词长什么样、有什么特性。
1.2 词嵌入:让词"活"起来
词嵌入(Word Embedding)要解决的就是这个问题。它的核心思想是:把每个词映射到一个低维稠密向量中,让语义相近的词在向量空间里也靠得近。
比如,我们可以把"猫"映射成 [0.2, 0.8, -0.3, 0.1, ...],"狗"映射成 [0.3, 0.7, -0.2, 0.0, ...]。你看,这两个向量就很接近了。
| 特性 | 独热编码 | 词嵌入 |
|---|---|---|
| 维度 | 词汇表大小(如10万) | 自定义(如128、256、512) |
| 稀疏性 | 极度稀疏(99.999%为0) | 稠密(所有值非零) |
| 语义表达 | 无 | 有(相似词距离近) |
| 可学习性 | 固定,不可学习 | 可训练,随任务优化 |
| 存储开销 | O(V) | O(V × d),d << V |
我个人习惯把词嵌入想象成一个"语义压缩器"。它把高维的、稀疏的、无意义的独热编码,压缩成低维的、稠密的、有意义的向量。你想想看,这多像我们人类理解语言的方式——我们不会记住每个词的"身份证号",而是记住它的"含义"。
1.3 Embedding矩阵的数学本质
好,现在我们来点硬核的。Embedding矩阵到底是什么?
假设词汇表大小是V,嵌入维度是d。那么Embedding矩阵就是一个 V × d 的矩阵。每一行对应一个词的嵌入向量。
数学上,词嵌入操作其实就是:
# 伪代码
embedding_matrix = [V, d] # 可训练的参数矩阵
one_hot_vector = [0, 0, ..., 1, ..., 0] # 长度为V,第i位为1
# 词嵌入 = 矩阵乘法
word_vector = one_hot_vector @ embedding_matrix # 结果是一个d维向量
# 实际上,这等价于查表
word_vector = embedding_matrix[i] # 直接取第i行
说白了,Embedding矩阵就是一个查找表。你给一个词的索引,它返回对应的稠密向量。但这个查找表不是固定的——它会在训练过程中不断更新,让语义相近的词向量越来越接近。
1.4 nn.Embedding模块参数详解
在PyTorch里,我们直接用 nn.Embedding 来实现词嵌入。它的参数其实不多,但每个都很关键。
import torch.nn as nn
# 基本用法
embedding = nn.Embedding(
num_embeddings=10000, # 词汇表大小 V
embedding_dim=256, # 嵌入维度 d
padding_idx=None, # 填充词的索引(可选)
max_norm=None, # 最大范数约束(可选)
norm_type=2.0, # 范数类型(配合max_norm使用)
scale_grad_by_freq=False, # 是否按词频缩放梯度
sparse=False # 是否使用稀疏梯度
)
来,我们逐个拆解:
| 参数 | 说明 | 我的建议 |
|---|---|---|
num_embeddings |
词汇表大小,即有多少个不同的词 | 一般设为实际词数+1(留一个给未知词) |
embedding_dim |
每个词向量的维度 | 常见值:128、256、512。小任务用128,大模型用512以上 |
padding_idx |
指定填充词的索引,该词向量在训练中不更新 | 强烈建议设置!我见过太多人忘了设,导致填充词也参与训练 |
max_norm |
如果设置,会对词向量进行L2范数裁剪 | 有助于稳定训练,但别设太小,否则信息丢失 |
sparse |
是否使用稀疏梯度更新 | 大词汇表时建议True,节省内存。但注意优化器要支持稀疏梯度 |
num_embeddings 设得比实际词数大很多,结果模型训练时出现了大量未使用的词向量,白白浪费了显存。后来我养成了一个习惯:先统计语料中的唯一词数,再加一个 <UNK> 和 <PAD> 的预留位置。
再来看一个实际使用的例子:
import torch
import torch.nn as nn
# 假设我们有3个句子,每个句子4个词,词汇表大小10
embedding = nn.Embedding(10, 8, padding_idx=0)
# 输入:batch_size=3, seq_len=4
input_ids = torch.tensor([
[1, 2, 3, 0], # 0是填充
[4, 5, 0, 0],
[6, 7, 8, 9]
])
# 输出:batch_size=3, seq_len=4, embedding_dim=8
output = embedding(input_ids)
print(output.shape) # torch.Size([3, 4, 8])
你看,输入是整数索引,输出是浮点数向量。这就是Embedding层干的事——把离散的符号变成连续的向量。
padding_idx 设好。因为注意力机制里,填充位置不应该参与计算。我一般把 padding_idx=0,然后在注意力掩码里把0位置遮掉。
最后说一句:Embedding矩阵的维度选择是个艺术。太小了,语义表达不够丰富;太大了,参数爆炸、训练变慢。我个人习惯从128开始试,如果任务简单就降到64,复杂任务就升到256或512。你想想看,这其实就是在"表达能力"和"计算效率"之间找平衡。
好了,这一章就到这里。下一章我们会深入多头注意力机制,看看Embedding的维度是怎么分配到各个注意力头的。到时候你会发现,维度分配这件事,比你想的要讲究得多。