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矩阵就是一个查找表。你给一个词的索引,它返回对应的稠密向量。但这个查找表不是固定的——它会在训练过程中不断更新,让语义相近的词向量越来越接近。

🔧 我的经验: 我在做机器翻译项目时,发现Embedding矩阵的初始化特别重要。如果随机初始化,模型前期训练会很慢。后来我改用预训练的GloVe或Word2Vec向量初始化,收敛速度快了将近一倍。嗯,这里要注意——预训练向量不一定适合你的任务,有时候从头训练反而更好,得试。

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层干的事——把离散的符号变成连续的向量。

💡 小技巧: 如果你在做Transformer模型,记得把 padding_idx 设好。因为注意力机制里,填充位置不应该参与计算。我一般把 padding_idx=0,然后在注意力掩码里把0位置遮掉。

最后说一句:Embedding矩阵的维度选择是个艺术。太小了,语义表达不够丰富;太大了,参数爆炸、训练变慢。我个人习惯从128开始试,如果任务简单就降到64,复杂任务就升到256或512。你想想看,这其实就是在"表达能力"和"计算效率"之间找平衡。

好了,这一章就到这里。下一章我们会深入多头注意力机制,看看Embedding的维度是怎么分配到各个注意力头的。到时候你会发现,维度分配这件事,比你想的要讲究得多。