在自然语言处理(NLP)乃至更广泛的机器学习领域中,“嵌入(Embedding)”这个词的出现频率相当之高。它似乎是处理类别型数据时绕不开的一步。然而,Embedding 层究竟是什么?它和我们常听到的 Word2Vec 之间又是什么关系?这些问题有时会给初学者带来一些困扰。今天,我就想顺着自己的思路,把这些概念彻底梳理一遍。
一、问题的起点:计算机如何理解“词”?
我们首先要面对一个根本性的问题:计算机本质上只懂得处理数字,但我们日常打交道的文本是由一个个离散的、非数值的“词”组成的。要让机器学习模型能够处理文本,第一步必然是将词语数值化。
最简单、最暴力的方法,莫过于独热编码(OneHot Encoding)。
这种方法的优点是简单,但缺点也同样致命:
- 维度灾难:当词典规模 变得非常大时(例如几十万),每个词的向量维度也随之膨胀,这会带来巨大的存储和计算开销。
- 高度稀疏:向量中绝大部分都是 0,信息密度极低。
- 语义鸿沟:从向量本身来看,任意两个词的 one-hot 向量都是正交的。这意味着我们无法从数学上表达“国王”与“男人”比“国王”与“香蕉”关系更近。词与词之间的语义关系完全丢失了。
既然 one-hot 编码有如此多的弊病,我们自然会思考:有没有一种更“聪明”的表示方法呢?我们希望新的表示法是一个低维、稠密的连续向量,并且这个向量本身就能蕴含词语的语义信息。
这就是“嵌入(Embedding)”思想的诞生。我们不再用一个稀疏的高维向量来“定位”一个词,而是将每个词“嵌入”到一个低维的、连续的向量空间中。在这个空间里,意思相近的词,其对应的向量在空间中的距离也应该更近。
最终,所有词的向量会构成一个巨大的查询表,也就是我们常说的“嵌入矩阵”。这个矩阵的大小通常是 ,其中 是词典大小,而 则是我们人为设定的嵌入维度(一个远小于 的超参数,比如 100 或 300)。
二、PyTorch 的实现:nn.Embedding
有了理论上的构想,我们来看看在 PyTorch 这个工具中是如何具体实现的。PyTorch 提供了一个非常方便的模块:torch.nn.Embedding。
我认为,理解 nn.Embedding 最直观的方式,就是把它看作一个可学习的查询表(Lookup Table)。它内部就维护着我们上面提到的那个 的嵌入矩阵。它的工作流程非常简单:你给它一个词的索引(一个整数),它就从矩阵里把对应索引的那一行向量返回给你。
为了让这个过程更具体,我们来设想一个简单的场景,还是用 {“king”, “queen”, “man”, “woman”} 这个迷你词典。
- 建立索引:我们先给每个词分配一个唯一的整数索引,比如:king → 0, queen → 1, man → 2, woman → 3。
- 创建嵌入层:我们想用一个 5 维的向量来表示每个词。于是,我们创建一个
nn.Embedding层,告诉它我们的词典大小是 4,每个词的嵌入维度是 5。
import torch.nn as nn
# 词典大小为 4,嵌入维度为 5
embedding_layer = nn.Embedding(num_embeddings=4, embedding_dim=5)- 进行查询:现在,如果我们想获取“king”这个词的向量,只需要把它的索引 0 传给
embedding_layer即可。
这个 embedding_layer 被创建时,里面的 矩阵是随机初始化的。但它的精髓在于,这个矩阵是可训练的。当我们将它作为神经网络的一部分进行训练时,这些向量会随着反向传播不断被微调,最终,模型会自发地学习到能最好地服务于当前任务的词向量表示。
三、nn.Embedding 与 Word2Vec 的纠葛
谈到词向量,就不能不提大名鼎鼎的 Word2Vec。那么,nn.Embedding 和 Word2Vec 到底是什么关系呢?
要厘清这一点,我们必须明白它们的角色定位是不同的:
nn.Embedding是一个机制或模块。它是一个容器,一个查询表,提供了一种将整数索引映射到稠密向量的方式。它本身并不关心这些向量是怎么来的,可以是随机初始化的,也可以是事先加载好的。- Word2Vec(包括其 CBOW 和 Skip-gram 模型)是一种算法或训练方法。它的目标就是专门用来训练词向量的。它通过“上下文预测”的方式,在海量的无监督文本上学习,最终得到的产物,就是那个包含了丰富语义信息的嵌入矩阵。
所以,它们的关系可以这样理解:Word2Vec 是一种生产优质“词向量”的强大算法,而 nn.Embedding 则是 PyTorch 中使用这些“词向量”的官方指定容器。
在实际应用中,我们常常不希望从零开始(随机初始化)训练词向量,因为这需要海量的训练数据和计算资源。一个更高效的做法是,直接加载由 Word2Vec 等算法在通用语料库上预训练好的词向量,然后在此基础上针对我们的特定任务进行微调,甚至不进行微调。
下面,我们就来看一下这个“加载预训练权重”的完整流程是怎样的。假设我们已经用 gensim 库加载了一个预训练好的 Word2Vec 模型。
第一步:获取预训练权重
首先,我们需要从加载的模型中把嵌入矩阵(也就是权重)提取出来。它本质上是一个 NumPy 数组。
import gensim
import numpy as np
# 假设已加载了一个 gensim 的 Word2Vec 模型
# word2vec_model = gensim.models.KeyedVectors.load_word2vec_format(...)
# 为了演示,我们手动创建一个模拟的权重
mock_vectors = np.random.rand(1000, 100) # 模拟一个1000词,100维的嵌入矩阵
weights = mock_vectors第二步:创建 Embedding 层并加载权重
我们创建一个尺寸与预训练权重完全匹配的 nn.Embedding 层,然后用预训练的权重来覆盖它内部随机初始化的权重。
import torch
import torch.nn as nn
vocab_size, embedding_dim = weights.shape
# 创建 Embedding 层
embedding_layer = nn.Embedding(vocab_size, embedding_dim)
# 将 NumPy 数组转为 Tensor,并加载到 Embedding 层的 weight 属性中
embedding_layer.weight.data.copy_(torch.from_numpy(weights))第三步:(可选)冻结权重
如果我们希望在后续的模型训练中,保持这些预训练的词向量不变,以免它们被我们规模不大的任务数据“带偏”,我们可以冻结这一层的梯度更新。
embedding_layer.weight.requires_grad = False为什么要冻结呢?因为预训练的向量是在通用的大规模语料上学到的,蕴含了丰富的、泛化的语义知识。对于一些小样本的任务,如果不冻结,这些珍贵的知识可能在训练初期就被任务的噪声数据破坏了。
至此,我们就完成了一次完整的知识迁移:将 Word2Vec 的训练成果,成功地“注入”到了 PyTorch 的 nn.Embedding 层中,让我们的模型能站在巨人的肩膀上。
文章小结
回顾全文,我们的思路是从一个最基本的问题——“如何向计算机表示词语”出发,剖析了 one-hot 编码的局限性,引出了 Embedding 这种低维、稠密的表示思想。接着,我们落地到具体的工具 PyTorch,把 nn.Embedding 层理解为一个可学习的查询表。最后,我们厘清了它与 Word2Vec 之间的关系:一个是通用的机制,一个是具体的算法,并演示了如何在实践中将二者结合,从而高效地利用预训练知识。