PyTorch 2.x 深度学习专题【左扬精讲】—— 词嵌入详解:从独热编码到上下文相关表示
PyTorch 2.x 深度学习专题【左扬精讲】—— 词嵌入详解:从独热编码到上下文相关表示
在自然语言处理(NLP)领域,如何让机器 "理解" 人类语言是一个核心问题。
词嵌入(Word Embedding)作为将离散符号映射为连续稠密向量的关键技术,是现代 NLP 深度学习模型的基石。
本文将系统讲解词嵌入的定义、前后关键词关系、生成方法,以及主流模型架构。
nn.Embedding ← PyTorch 嵌入层实现
nn.EmbeddingBag ← 变长序列的高效嵌入
Word2Vec ← 经典预训练词嵌入(CBOW + Skip-gram)
GloVe ← 基于全局共现统计的词嵌入
torch.nn.functional.embedding ← 低级嵌入查找接口
词嵌入Word Embedding独热编码嵌入层Word2VecGloVe上下文相关预训练
学习重点
- 必须掌握:词嵌入的定义、嵌入层的 PyTorch 实现、独热编码与词嵌入的区别
- 必须掌握:基于神经网络训练词嵌入的方法(CBOW、Skip-gram)
- 需要理解:上下文相关词嵌入的概念、BERT 等预训练模型的工作原理
- 需要理解:词嵌入的评估方法(内在评估与下游任务评估)
一、词嵌入的定义:什么是词嵌入?
What — 词嵌入是什么?
词嵌入(Word Embedding)是一种将 离散的语言符号(如单词、子词)映射为连续稠密实数向量 的技术手段。在嵌入空间中,语义相近的词距离更近,向量运算具有语义意义(如"king" - "man" + "woman" ≈ "queen")。
Why — 为什么需要词嵌入?
问题一:独热编码(One-Hot Encoding)无法表达语义相似性
独热编码是一种将词表示为"只有一个位置为 1、其余全为 0"的向量。例如词汇表 ["the", "cat", "dog"] 中,"cat" 表示为 [0, 1, 0],"dog" 表示为 [0, 0, 1]。这种表示方式有两个致命缺陷:
- 高维稀疏:vocab_size=50000 时,每个向量有 50000 个维度,但只有 1 个位置为 1,浪费大量存储和计算资源
- 词间正交:任意两个不同的词向量点积为 0,无法表达"cat"和"dog"都是动物这一语义相似性
问题二:无法处理未登录词和共享语义
独热编码对于每个词都是独立的 ID,无法捕捉词与词之间的语义关系,也无法泛化到相似词。
没有词嵌入会发生什么?
- 模型参数量巨大(每个词都对应独立参数)
- 无法利用词的语义相似性进行泛化
- 无法进行向量运算(如类比推理)
- 深度学习模型难以有效学习语言表示
在 PyTorch 中,nn.Embedding 是最常用的词嵌入层实现。它维护一个形状为 (num_embeddings, embedding_dim) 的嵌入矩阵,通过索引查找将离散 token 映射为连续向量。
import torch
import torch.nn as nn
# 定义一个词汇表大小为 10000、嵌入维度为 300 的嵌入层
embedding = nn.Embedding(num_embeddings=10000, embedding_dim=300)
# 创建一个形状为 (batch_size, seq_len) 的 token 索引张量
input_tokens = torch.randint(0, 10000, (32, 50)) # batch=32, seq_len=50
# 通过嵌入层得到形状为 (32, 50, 300) 的词向量序列
word_vectors = embedding(input_tokens)
print(word_vectors.shape) # torch.Size([32, 50, 300])
嵌入层的本质是一个可学习的查找表(Lookup Table):输入是 token 的整数索引,输出是对应行的向量。
核心总结
- 词嵌入将离散符号映射为连续稠密向量,捕捉语义相似性
- nn.Embedding 是 PyTorch 中的嵌入层实现
- 嵌入向量维度(embedding_dim)是一个超参数,通常选择 100~512
二、从独热编码到词嵌入的演进
词嵌入演进的三个阶段
词嵌入的发展经历了从稀疏到稠密、从静态到动态的演进过程,理解这一演进有助于把握 NLP 深度学习的发展脉络。
2.1 独热编码(One-Hot Encoding)
独热编码是最原始的文本表示方法。假设词汇表大小为 V,则每个词被表示为一个 V 维向量,其中只有一个位置为 1,其余为 0。
# 独热编码示例
import torch
# 假设词汇表:["the", "cat", "sat", "on", "mat"]
vocab = {"the": 0, "cat": 1, "sat": 2, "on": 3, "mat": 4}
vocab_size = len(vocab)
# 创建独热向量
def one_hot(token_idx, vocab_size):
vec = torch.zeros(vocab_size)
vec[token_idx] = 1
return vec
# "cat" 的独热编码:[0, 1, 0, 0, 0]
cat_onehot = one_hot(vocab["cat"], vocab_size)
print(cat_onehot) # tensor([0., 1., 0., 0., 0.])
独热编码的缺陷显而易见:高维稀疏(维度等于词表大小)、词与词之间无关联、无法泛化。
2.2 词袋模型(Bag of Words)与 TF-IDF
词袋模型将文本表示为词频向量,虽然解决了多词共存问题,但仍然是高维稀疏表示,无法捕捉词的语义。
2.3 分布式表示(Distributed Representation)
词嵌入采用"分布式表示"思想:一个词的语义由其上下文决定,语义相似的词会有相似的上下文,因此会有相似的向量表示。这与独热编码的"局部表示"形成鲜明对比。
分布式表示 vs 局部表示
"分布式"意味着信息分散存储在多个维度上。"cat"和"dog"的语义相似性会体现在它们的向量在多个维度上都有相似的值,而不是像独热编码那样完全正交。
核心总结
- 独热编码:高维稀疏、词间无关联
- 词嵌入:低维稠密、语义可计算
- 分布式表示是词嵌入的理论基础:词的语义由上下文决定
三、前后关键词关系梳理
词嵌入相关概念的关系图谱
理解词嵌入需要把握一系列相关概念之间的层级关系和依赖关系。
词嵌入(Word Embedding)
├── 静态词嵌入(Static Embeddings)
│ ├── 独热编码(One-Hot)→ 稀疏、无语义
│ ├── 词嵌入向量(Dense Vector)→ 稠密、有语义
│ ├── 预训练词嵌入
│ │ ├── Word2Vec(CBOW / Skip-gram)
│ │ ├── GloVe(全局共现矩阵分解)
│ │ └── FastText(子词嵌入)
│ └── 微调词嵌入(Fine-tuned Embeddings)
│
├── 上下文相关词嵌入(Contextual Embeddings)
│ ├── 基于 Transformer 的模型
│ │ ├── BERT 系列(Bidirectional)
│ │ ├── GPT 系列(Unidirectional)
│ │ └── ELMo(双层 LSTM)
│ └── 动态词嵌入:同一词在不同上下文有不同向量
│
└── 嵌入层实现
├── nn.Embedding(PyTorch)
├── nn.EmbeddingBag(高效变长序列)
├── keras.layers.Embedding(TensorFlow/Keras)
└── TensorFlow 基础实现
3.1 关键词详解
| 关键词 | 含义 | 特点 |
|---|---|---|
| 嵌入维度 |
词向量本质上是一个数字列表(向量),嵌入维度就是这个列表的长度。 例如维度为 300 意味着每个词被表示为一个包含 300 个数字的数组。维度越高,表达能力越强,但参数量也越大。 |
通常 100~512,太高过拟合,太低欠拟合 |
| 词汇表大小 |
训练词嵌入前,需要先构建一个词汇表,列出所有要学习的词(或子词)。词汇表大小就是这个列表中的词条数量。 词汇表越大,能覆盖的词越多,但嵌入矩阵(vocab_size × embedding_dim)的参数量也随之增大。 |
影响嵌入矩阵参数量(vocab_size x embedding_dim) |
| 上下文窗口 |
训练时,模型不仅看目标词本身,还会查看它周围的一些词来确定语义关系,上下文窗口就是左右各看多少个词。 例如窗口大小为 2 时,对于句子"The cat sat on the mat",预测"on"会参考"cat sat"和"the mat"共 4 个上下文词。 |
窗口越大越能捕捉全局语义,越小越关注局部 |
| 子词(Subword) |
不是把整个词作为一个不可拆分的单元,而是将词进一步拆分成更小的片段。 例如"running"可拆为"run"+"ning","unhappy"可拆为"un"+"happy"。这样即使遇到未见过的词(如"unrunnable"),也能通过已知的子词片段组合理解其含义。 |
解决 OOV 问题,基于 BPE/WordPiece/SentencePiece 等算法 |
| OOV / UNK |
未登录词(Out-of-Vocabulary),指训练词嵌入时词汇表中不存在的单词。 模型遇到这类词时无法给出有意义的向量,通常统一用一个特殊符号 |
词嵌入的痛点,子词方法可缓解 |
| 嵌入目标 |
训练词嵌入时,模型需要一个学习目标(Objective),它决定了什么样的词应该靠得更近、什么样的词应该离得更远。 不同的目标会产生不同性质的向量空间。例如语言模型目标让语义相似的词聚在一起,矩阵分解目标让共现频率高的词距离更近。 |
语言模型目标、对比学习目标、矩阵分解目标 |
3.2 嵌入目标(Embedding Objectives)
词嵌入的训练目标决定了学到的向量空间结构。常见的嵌入目标包括:
-
-
- 语言模型目标:根据上下文预测中心词(CBOW)或根据中心词预测上下文(Skip-gram)
- 共现矩阵分解目标:近似词-词共现矩阵的奇异值分解
- 对比学习目标:使相似词的向量接近,不相似词的向量远离
- 遮蔽语言模型目标:预测被遮蔽的词(BERT 的 [MASK] 目标)
-
核心总结
-
-
- 词嵌入体系包含静态嵌入和上下文相关嵌入两大类
- 嵌入维度、词汇表大小、上下文窗口是关键超参数
- 嵌入目标决定了学习到的向量空间性质
-
四、基于神经网络的词嵌入训练
What — 神经网络如何训练词嵌入?
基于神经网络的词嵌入训练,本质上是构建一个浅层神经网络,把词从稀疏的高维独热向量映射到稠密低维向量空间。
所谓"浅层",指网络只有少数几层。这里特指三类层:
- 输入层:把你看到的内容(句子里的每个词)送进模型。在词嵌入里,每个词被表示为独热向量——一个 vocab_size 维、只有一个位置为 1、其余为 0 的数组。
- 隐藏层(嵌入层):夹在输入层和输出层之间、用户不直接接触的"中间层",所以叫"隐藏"。这一层本质上是一个
vocab_size × embedding_dim的查找表(权重矩阵)。独热向量与它相乘,相当于按词索引取出对应的那一行,得到该词的嵌入向量——也就是我们最终想要的"低维稠密表示"。可以把它理解为:输入层负责"喂数据",隐藏层负责"加工数据",输出层负责"吐结果"。 - 输出层:根据任务设计不同的预测目标。例如根据上下文预测中心词(CBOW),或根据中心词预测上下文(Skip-gram),输出层再把隐藏层的嵌入向量映射回词汇表上的概率分布。
训练时,模型通过反向传播不断调整隐藏层(嵌入矩阵)的权重,让 "相关词的向量靠得更近" 这个隐含约束被自然学到。训练完成后,这个矩阵的每一行就是对应词的词向量。
Why — 为什么用神经网络训练词嵌入?
传统词嵌入(如 LSA)基于矩阵分解,虽然能捕捉全局统计信息,但表达能力有限。神经网络可以学习更复杂的非线性语义关系,并且可以与下游任务联合优化。
没有神经网络训练会发生什么?
-
-
- 无法端到端联合优化嵌入和下游任务
- 难以捕捉复杂的语义关系
- 无法利用 GPU 并行计算的优势
-
4.1 Word2Vec:CBOW 与 Skip-gram
Word2Vec 是最具影响力的词嵌入训练方法,由 Mikolov 等人于 2013 年提出。它包含两种模型架构:CBOW 和 Skip-gram。
4.1.1 CBOW(Continuous Bag-of-Words)
CBOW 根据上下文词预测中心词。例如对于句子"The cat sat on the mat",输入"cat"、"sat"、"on"、"the"、"mat"的独热向量,预测中心词"on"。
CBOW 模型结构简洁:输入层 → 投影层(嵌入) → 输出层(Softmax)。实际使用中会采用负采样(Negative Sampling)来加速训练。
import torch
import torch.nn as nn
import torch.optim as optim
class CBOWModel(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super(CBOWModel, self).__init__()
self.embeddings = nn.Embedding(vocab_size, embedding_dim) # 嵌入层:查找表
self.linear = nn.Linear(embedding_dim, vocab_size) # 输出层:预测中心词概率分布
def forward(self, context_words):
# context_words: (batch_size, window_size * 2),包含上下文词的索引
# 输出: (batch_size, vocab_size),每个词的预测概率
embeds = self.embeddings(context_words) # 查找上下文词的嵌入向量
sum_embeds = torch.mean(embeds, dim=1) # 对上下文向量取平均
out = self.linear(sum_embeds)
return out
# 示例:词汇表大小 10000,嵌入维度 300,窗口大小 2(左右各 2 个词)
model = CBOWModel(vocab_size=10000, embedding_dim=300)
loss_fn = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)
训练时使用交叉熵损失,让模型学习区分真实中心词和负采样词。
4.1.2 Skip-gram
Skip-gram 与 CBOW 相反:根据中心词预测上下文词。例如输入"on"的独热向量,预测周围词"cat"、"sat"、"the"、"mat"。
class SkipGramModel(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super(SkipGramModel, self).__init__()
self.embeddings = nn.Embedding(vocab_size, embedding_dim) # 输入嵌入
self.output_embeddings = nn.Embedding(vocab_size, embedding_dim) # 输出嵌入(用于负采样)
def forward(self, target_word, context_words, negative_samples):
# target_word: (batch_size,) 中心词索引
# context_words: (batch_size, num_negative) 正样本上下文词索引
# negative_samples: (batch_size, num_negative) 负采样词索引
target_emb = self.embeddings(target_word) # 中心词向量 (batch_size, embedding_dim)
context_emb = self.output_embeddings(context_words) # 正样本向量
neg_emb = self.output_embeddings(negative_samples) # 负采样向量
# 正样本得分:中心词与正样本的点积(越大越好)
pos_score = torch.sum(target_emb * context_emb, dim=1)
# 负样本得分:中心词与负样本的点积(越小越好)
neg_score = torch.sum(target_emb * neg_emb, dim=2)
# 对数损失:log(sigmoid(pos_score)) + sum(log(sigmoid(-neg_score)))
pos_loss = torch.nn.functional.logsigmoid(pos_score)
neg_loss = torch.nn.functional.logsigmoid(-neg_score).sum(dim=1)
return -(pos_loss + neg_loss).mean()
model = SkipGramModel(vocab_size=10000, embedding_dim=300)
4.2 负采样(Negative Sampling)
原始 Word2Vec 使用层次 Softmax 或负采样来加速训练。负采样的核心思想:将多分类问题转换为二分类问题(正样本 vs 负样本)。
| 优化方法 | 原理 | 优缺点 |
|---|---|---|
| 负采样 | 每次只更新正样本和 k 个负样本的权重 | 实现简单,训练快,适合小批量 |
| 层次 Softmax | 用霍夫曼树近似 Softmax,减少计算量 | 适合稀有词,但对常见词效率不如负采样 |
| 完全 Softmax | 标准多分类 | 计算量大,仅适合小词汇表 |
核心总结
- CBOW 根据上下文预测中心词,适合高频词
- Skip-gram 根据中心词预测上下文,适合稀有词
- 负采样是训练 Word2Vec 的关键加速技术
- nn.Embedding 初始化后可与下游任务联合训练
五、嵌入层详解(Embedding Layer)
What — 嵌入层是什么?
嵌入层(Embedding Layer)是深度学习模型中将离散 token 索引转换为连续向量表示的网络层。它本质上是一个形状为 (vocab_size, embedding_dim) 的可学习矩阵。
嵌入层支持多种高级特性: padding_idx、梯度稀疏化、分词嵌入、与下游任务联合训练等。
import torch
import torch.nn as nn
# 基础用法:创建嵌入层
embedding = nn.Embedding(num_embeddings=10000, embedding_dim=300)
# 带 padding 的嵌入层:PAD token 的嵌入向量保持为零
embedding_with_padding = nn.Embedding(
num_embeddings=10000,
embedding_dim=300,
padding_idx=0 # 索引 0 对应的嵌入向量始终为零
)
# 输入形状:(batch_size, seq_len)
input_indices = torch.LongTensor([[1, 5, 8, 12], [3, 7, 2, 0]]) # (2, 4)
output = embedding(input_indices)
print(output.shape) # torch.Size([2, 4, 300])
# input_indices[i][j] 查找的是嵌入矩阵第 input_indices[i][j] 行的向量
# 从预训练权重初始化
pretrained_weights = torch.randn(10000, 300) # 假设这是预训练的词向量
embedding = nn.Embedding.from_pretrained(pretrained_weights, freeze=False)
# freeze=True 时不更新嵌入层权重,freeze=False 时会微调
嵌入层的参数量 = num_embeddings x embedding_dim。10000 词 x 300 维 = 300 万参数,约 12MB(float32)。
5.1 嵌入层的数学原理
嵌入层的前向传播可以表示为矩阵乘法:
给定嵌入矩阵 W ∈ R^(V×D),输入索引 x ∈ N^L,输出嵌入为:
E = W[x] ∈ R^(L×D),即取出 W 的第 x[0], x[1], ..., x[L-1] 行
5.2 nn.EmbeddingBag 的高效用法
对于变长序列或需要直接聚合嵌入的场景(如 TextCNN、BERT 的 Pooler),nn.EmbeddingBag 可以直接计算嵌入的平均/最大/求和聚合,无需先查表再手动聚合。
# nn.EmbeddingBag:直接聚合,无需显式查表
embedding_bag = nn.EmbeddingBag(num_embeddings=10000, embedding_dim=300, mode='mean')
# 输入:token 索引和每个序列的偏移量
token_indices = torch.LongTensor([[1, 2, 3, 4], [5, 6, 7, 0, 0]]) # 两个变长序列
offsets = torch.LongTensor([0, 4]) # 每个序列在 token_indices 中的起始位置
# 输出:(2, 300),每个序列嵌入向量的平均值
bag_output = embedding_bag(token_indices, offsets)
print(bag_output.shape) # torch.Size([2, 300])
使用建议
- 固定序列长度用 nn.Embedding
- 变长序列或只需聚合向量时用 nn.EmbeddingBag
- 下游任务通常微调嵌入层(freeze=False)
- 大词汇表可用 Adaptive Embedding(高频词全维度,低频词低维度)
核心总结
- 嵌入层是可学习的查找表,参数量 = vocab_size x embedding_dim
- nn.Embedding 适用于固定长度序列
- nn.EmbeddingBag 适用于变长序列的高效聚合
- padding_idx 可保持 PAD token 嵌入为零向量
六、预训练词嵌入(Pre-trained Embeddings)
What — 什么是预训练词嵌入?
预训练词嵌入是在大规模语料上预先训练好的词向量,可以直接用于下游任务或作为初始化进行微调。常见方法包括 Word2Vec、GloVe、FastText 等。
6.1 Word2Vec 预训练词嵌入
Word2Vec 的训练已经高度工程化,实际应用中通常直接使用 Google 预训练模型或使用 Gensim 加载。
# 使用 Gensim 加载预训练 Word2Vec
# pip install gensim
from gensim.models import KeyedVectors
# 加载 Google News 预训练词向量(约 300 万词,维度 300)
# 预训练模型下载地址:https://code.google.com/archive/p/word2vec/
model = KeyedVectors.load_word2vec_format('GoogleNews-vectors-negative300.bin', binary=True)
# 获取词向量
vector = model['computer']
print(vector.shape) # (300,)
# 找相似词
similar = model.most_similar('computer')
print(similar[:5]) # [('computers', 0.78...), ('PC', 0.75...), ...]
# 词类比:king - man + woman = queen
result = model.most_similar(positive=['woman', 'king'], negative=['man'])
print(result[0]) # ('queen', 0.76...)
6.2 GloVe(Global Vectors)
GloVe 由 Stanford NLP 团队提出,结合了全局矩阵分解和局部上下文窗口的优点,通过最小化词-词共现概率比的损失函数来学习词向量。
# 加载预训练 GloVe 词向量
import numpy as np
# GloVe 预训练文件(6B tokens, 400K vocab, 300d)
# 下载地址:https://nlp.stanford.edu/projects/glove/
def load_glove(path):
embeddings = {}
with open(path, 'r', encoding='utf-8') as f:
for line in f:
values = line.strip().split()
word = values[0]
vector = np.asarray(values[1:], dtype='float32')
embeddings[word] = vector
return embeddings
glove = load_glove('glove.6B.300d.txt')
# 将 PyTorch 嵌入层初始化为 GloVe 向量
import torch.nn as nn
vocab = {'the': 0, 'cat': 1, 'dog': 2, 'on': 3} # 示例词汇表
embedding_dim = 300
embedding_matrix = np.random.randn(len(vocab), embedding_dim) * 0.01
for word, idx in vocab.items():
if word in glove:
embedding_matrix[idx] = glove[word]
embedding = nn.Embedding.from_pretrained(
torch.FloatTensor(embedding_matrix),
freeze=False # 微调:允许更新权重
)
6.3 FastText(子词嵌入)
FastText 由 Facebook 提出,核心思想是将词拆分为子词(subword),通过叠加子词的向量得到词的向量。这解决了 OOV 问题:遇到未见过的词时,可以从其子词推断向量。
from gensim.models import FastText
# 训练 FastText 模型
sentences = [['hello', 'world'], ['natural', 'language', 'processing']]
model = FastText(sentences, vector_size=100, window=5, min_count=1, epochs=10)
# 即使词不在训练集中,也可以通过子词组合得到向量
# FastText 使用字符级 n-gram(如 n=3 时,"where" 产生 "wh", "her", "ere", "re" 等子词)
vector = model.wv['embeddings']
print(vector.shape) # (100,)
| 预训练方法 | 训练目标 | 优点 | 缺点 |
|---|---|---|---|
| Word2Vec | CBOW / Skip-gram | 训练快,效果好,类比推理能力强 | 无法处理 OOV,不考虑词序 |
| GloVe | 全局共现矩阵分解 | 融合全局统计信息,训练稳定 | 无法处理 OOV,内存占用大 |
| FastText | Skip-gram + 子词 | 解决 OOV 问题,对稀有词效果好 | 子词粒度需要调优,向量可解释性差 |
核心总结
- 预训练词嵌入可大幅减少下游任务的训练数据需求
- Word2Vec、GloVe、FastText 各有优劣,需根据任务选择
- FastText 的子词机制是解决 OOV 的关键
- 加载预训练权重后,通常会微调(freeze=False)
七、上下文相关词嵌入(Contextual Embeddings)
What — 上下文相关词嵌入是什么?
传统的词嵌入(如 Word2Vec)为每个词分配一个固定的向量表示,与上下文无关。而上下文相关词嵌入根据词出现的上下文动态生成向量,同一个词在不同上下文中会有不同的向量表示。
Why — 为什么需要上下文相关词嵌入?
问题一:消歧问题
"bank" 可以指"银行"或"河岸",在传统词嵌入中只有一个向量,无法区分这两种含义。
问题二:上下文决定语义
"He saw a bat"中"bat"是"蝙蝠"还是"球拍"?这完全取决于上下文。传统词嵌入无法捕捉这种上下文依赖性。
没有上下文相关词嵌入会发生什么?
- 无法处理一词多义问题
- 语义消歧任务性能受限
- 无法充分利用上下文信息
7.1 ELMo:双层 LSTM 的上下文嵌入
ELMo(Embeddings from Language Model)由 AllenNLP 提出,使用双层双向 LSTM 从语言模型中提取上下文相关的词嵌入。
# ELMo 的 PyTorch 实现(简化版)
import torch
import torch.nn as nn
class ELMo(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim):
super(ELMo, self).__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
# 两层双向 LSTM,分别捕捉不同层级的语义
self.lstm1 = nn.LSTM(embedding_dim, hidden_dim, batch_first=True, bidirectional=True)
self.lstm2 = nn.LSTM(hidden_dim * 2, hidden_dim, batch_first=True, bidirectional=True)
def forward(self, x):
# x: (batch_size, seq_len)
x = self.embedding(x) # (batch_size, seq_len, embedding_dim)
# 第一层 BiLSTM
lstm1_out, _ = self.lstm1(x) # (batch_size, seq_len, hidden_dim * 2)
# 第二层 BiLSTM(使用第一层输出作为输入)
lstm2_out, _ = self.lstm2(lstm1_out) # (batch_size, seq_len, hidden_dim * 2)
# 加权组合两层输出(权重可学习)
return lstm1_out, lstm2_out
# ELMo 的关键:同一词在不同上下文中得到不同向量
# "bank" 在 "deposit money at the bank" 和 "river bank" 中有不同的 ELMo 向量
7.2 BERT:Transformer 的上下文嵌入
BERT(Bidirectional Encoder Representations from Transformers)使用 Transformer 编码器架构和遮蔽语言模型(MLM)目标,生成深度的双向上下文嵌入。
# BERT 嵌入层结构
import torch
import torch.nn as nn
import math
class BertEmbedding(nn.Module):
def __init__(self, vocab_size, hidden_size, max_position, type_vocab_size):
super().__init__()
self.word_embedding = nn.Embedding(vocab_size, hidden_size) # Token 嵌入
self.position_embedding = nn.Embedding(max_position, hidden_size) # 位置嵌入
self.token_type_embedding = nn.Embedding(type_vocab_size, hidden_size) # 句子类型嵌入
self.layer_norm = nn.LayerNorm(hidden_size)
self.dropout = nn.Dropout(0.1)
def forward(self, input_ids, token_type_ids=None):
seq_len = input_ids.size(1)
# Token 嵌入:每个词查表得到向量
word_embeds = self.word_embedding(input_ids)
# 位置嵌入:基于位置索引
position_ids = torch.arange(seq_len, dtype=torch.long, device=input_ids.device)
position_ids = position_ids.unsqueeze(0).expand_as(input_ids)
position_embeds = self.position_embedding(position_ids)
# 句子类型嵌入(区分句子 A 和句子 B,[CLS] 和 [SEP])
if token_type_ids is None:
token_type_ids = torch.zeros_like(input_ids)
token_type_embeds = self.token_type_embedding(token_type_ids)
# 三种嵌入相加后 LayerNorm + Dropout
embeddings = word_embeds + position_embeds + token_type_embeds
embeddings = self.layer_norm(embeddings)
embeddings = self.dropout(embeddings)
return embeddings
# BERT 的 MLM 目标:随机遮蔽 15% 的词,让模型预测被遮蔽的词
# 这使得 BERT 能同时利用左右上下文(双向),而非仅用左上下文(GPT)或右上下文(RNN)
7.3 静态嵌入 vs 上下文嵌入
| 特性 | 静态词嵌入(Word2Vec/GloVe) | 上下文相关嵌入(BERT/ELMo) |
|---|---|---|
| 向量维度 | 每个词一个固定向量 | 每个词的每个上下文一个向量 |
| 上下文利用 | 仅用局部窗口训练时不考虑上下文 | 直接建模完整上下文 |
| 一词多义 | 无法区分,同词同向量 | 可以区分,同词不同向量 |
| 计算成本 | 低(只需查表) | 高(需要完整模型前向传播) |
| 适用场景 | 资源受限、简单任务 | 复杂语义理解任务 |
注意
上下文相关嵌入的计算成本远高于静态嵌入。在实际应用中,可以根据任务复杂度选择:简单分类任务可能只需 Word2Vec 即可达到不错的效果,而阅读理解、问答等复杂任务则需要 BERT 等上下文嵌入。
核心总结
- 上下文相关词嵌入根据词的上下文动态生成向量
- ELMo 使用双层双向 LSTM,BERT 使用 Transformer 编码器
- BERT 的 MLM 目标使其能同时利用左右上下文
- 上下文嵌入解决了静态嵌入的一词多义问题
八、常用词嵌入模型一览
主流词嵌入模型全景图
经过多年发展,词嵌入技术已经形成完整体系。从静态到动态,从词级到子词级,从单一模型到预训练语言模型,下面梳理主要模型的特点和适用场景。
| 模型 | 发布年份 | 类型 | 核心特点 | 典型应用 |
|---|---|---|---|---|
| Word2Vec | 2013 | 静态 | CBOW + Skip-gram,负采样加速 | 词类比、语义相似度 |
| GloVe | 2014 | 静态 | 全局共现矩阵 + 局部窗口 | 语义相似度、情感分析 |
| FastText | 2016 | 静态 | 子词嵌入,解决 OOV | 稀有词处理、多语言 |
| ELMo | 2018 | 上下文 | 双层双向 LSTM,语言模型 | 序列标注、问答 |
| BERT | 2018 | 上下文 | Transformer 编码器,MLM + NSP | 分类、序列标注、问答 |
| GPT-2 | 2019 | 上下文 | Transformer 解码器,单向语言模型 | 文本生成、条件生成 |
| RoBERTa | 2019 | 上下文 | BERT 的优化版,动态遮蔽 | 各类 NLU 任务 |
| ALBERT | 2019 | 上下文 | 参数共享,句子顺序预测 | 资源受限场景 |
| DistilBERT | 2019 | 上下文 | BERT 的蒸馏版,6 层 66M 参数 | 实时推理、移动端 |
8.1 Hugging Face Transformers 的 Embedding 使用
# 使用 Hugging Face Transformers 加载预训练模型的嵌入
from transformers import AutoModel, AutoTokenizer
# 加载 BERT 模型和分词器
model_name = 'bert-base-uncased'
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
# 分词并获取嵌入
text = "The cat sat on the mat"
inputs = tokenizer(text, return_tensors='pt')
outputs = model(**inputs)
# 最后一层隐藏状态:每个 token 的上下文相关嵌入
last_hidden_state = outputs.last_hidden_state
print(last_hidden_state.shape) # (1, seq_len, 768)
# [CLS] 向量:可用于分类任务
cls_embedding = last_hidden_state[:, 0, :]
print(cls_embedding.shape) # (1, 768)
# 获取词嵌入矩阵(不含位置编码)
word_embeddings = model.embeddings.word_embedding.weight
print(word_embeddings.shape) # (30522, 768) = vocab_size x hidden_size
核心总结
- 静态嵌入模型(Word2Vec、GloVe、FastText):计算高效,适合简单任务
- 上下文嵌入模型(BERT、GPT):效果更好,适合复杂语义理解
- Transformer 架构已成为上下文嵌入的主流
- 预训练-微调范式是 NLP 深度学习的主流工作流
九、词嵌入的评估方法
词嵌入的质量如何衡量?
词嵌入的评估分为内在评估(intrinsic)和外在评估(extrinsic)。内在评估直接衡量向量空间的性质,外在评估通过下游任务性能间接评估。
9.1 内在评估(Intrinsic Evaluation)
内在评估直接测试词嵌入的语义和语法属性,通常使用人工标注的测试集。
- 词类比任务(Word Analogy):"king - man + woman = queen",测试向量运算能力
- 语义相似度任务:给定词对,预测人工标注的相似度分数,与余弦相似度比较
- 相关性任务:测试词向量是否捕捉词语之间的相关性(如 "dog" 与 "puppy" 的关系)
# 词嵌入内在评估示例
from scipy.stats import spearmanr
def evaluate_word_similarity(model, word_pairs, human_scores):
"""
评估词嵌入的语义相似度预测能力
model: 词嵌入模型(支持 most_similar 方法)
word_pairs: [(word1, word2), ...]
human_scores: [score1, score2, ...]
"""
predicted_scores = []
for w1, w2 in word_pairs:
if w1 in model and w2 in model:
sim = model.similarity(w1, w2) # 余弦相似度
predicted_scores.append(sim)
else:
predicted_scores.append(None)
# 过滤掉 OOV 词对
valid_pairs = [(p, h) for p, h in zip(predicted_scores, human_scores) if p is not None]
pred, true = zip(*valid_pairs)
# 计算 Spearman 相关系数
correlation, p_value = spearmanr(pred, true)
return correlation
def evaluate_word_analogy(model, analogies):
"""
评估词嵌入的类比推理能力
analogies: [["man", "woman", "king", "queen"], ...]
"""
correct = 0
total = 0
for analogy in analogies:
if len(analogy) != 4:
continue
a, b, c, expected = analogy
if all(w in model for w in analogy):
result = model.most_similar(positive=[b, c], negative=[a])
predicted = result[0][0]
if predicted == expected:
correct += 1
total += 1
return correct / total if total > 0 else 0
9.2 外在评估(Extrinsic Evaluation)
外在评估将词嵌入应用于实际下游任务,通过任务性能间接评估嵌入质量。
- 文本分类:情感分析、主题分类、垃圾邮件检测
- 序列标注:命名实体识别、词性标注
- 句法分析:依存句法分析、成分句法分析
- 语言理解:问答、文本蕴含、自然语言推理
评估建议
- 内在评估快速便宜,适合迭代调优嵌入参数
- 外在评估更接近实际应用,但计算成本高
- 最终模型选择应以目标任务的外在评估为准
- 内在评估提升但外在评估下降的情况时有发生
核心总结
- 内在评估直接测试向量空间性质(类比、相似度)
- 外在评估通过下游任务性能间接评估
- 词类比任务(Word Analogy)是最常用的内在评估方法
- 实际应用中外在评估更重要,但内在评估有助于快速迭代
FAQ(常见问题解答)
20 组常见问题
Q1. 词嵌入的维度如何选择?
一句话结论:通常选择 100~512 维,需要根据词汇表大小和任务复杂度调优。展开:词嵌入维度是偏差-方差权衡的结果。维度太低欠拟合,无法捕捉丰富语义;维度太高过拟合,计算成本增加。经验公式:D ≈ N^(0.25)(N 为词汇表大小)。对于 10000 词表,128~300 维通常足够。
Q2. 为什么词嵌入的初始化很重要?
一句话结论:好的初始化可以加速收敛,预训练初始化通常能提升下游任务性能。展开:随机初始化的嵌入向量没有语义意义,需要从随机噪声开始学习。预训练词嵌入(如 GloVe)提供了良好的语义初始化,可以让模型更快收敛,并在下游任务中取得更好的泛化效果。
Q3. 词嵌入和特征嵌入(Feature Embedding)有什么区别?
一句话结论:词嵌入是特征嵌入在 NLP 领域的特定形式,本质相同。展开:特征嵌入泛指将任意离散特征映射为连续向量的技术。在推荐系统中是物品嵌入(Item Embedding),在 CV 中是图像块嵌入。词嵌入强调的是语言符号的语义化表示。
Q4. 如何处理中文分词后的 OOV 问题?
一句话结论:使用子词嵌入(FastText)或基于 BPE/WordPiece 的分词器。展开:中文词汇表庞大且开放,未登录词众多。FastText 的子词机制可以将未见词拆分为已知子词的组合;BERT 使用的 WordPiece/BPE 分词器也能有效处理 OOV。
Q5. 词嵌入层是否需要梯度更新?
一句话结论:通常设置为可训练(freeze=False),但对于小数据集可考虑冻结。展开:预训练嵌入是否微调取决于任务和数据量。大数据集:微调可以学习任务特定语义。小数据集:冻结可防止过拟合。实践中通常从微调开始,根据验证集性能调整。
Q6. 位置嵌入和词嵌入有什么区别?
一句话结论:词嵌入编码词汇语义,位置嵌入编码序列位置。展开:词嵌入将 "cat" 映射为语义向量,位置嵌入将 "第 5 个位置" 映射为位置向量。BERT 使用可学习的位置嵌入,原始 Transformer 使用正弦位置编码(Sinusoidal PE)。
Q7. 词嵌入能否用于跨语言任务?
一句话结论:可以,通过多语言预训练模型(如 mBERT、XLM-R)实现跨语言词嵌入。展开:多语言 BERT 在 104 种语言上联合训练,共享词汇表和参数。不同语言的同义词在嵌入空间中距离较近,可以实现零样本跨语言迁移。
Q8. 如何判断词嵌入是否学到了有意义的语义?
一句话结论:可以通过词类比、相似词聚类、可视化等方法检验。展开:词类比任务验证向量运算能力;相似词列表检查语义聚类;t-SNE/PCA 可视化观察语义聚类效果。如果这些检验都不理想,可能需要更多数据或调整超参数。
Q9. 词嵌入是否可以用于文本生成任务?
一句话结论:可以作为初始输入,但文本生成通常需要完整的语言模型。展开:Word2Vec 只能查表,没有生成能力。GPT 等自回归语言模型可以生成文本,其内部嵌入层与词嵌入工作原理相同,但配合 Transformer 解码器和语言模型目标实现生成。
Q10. 为什么 BERT 的词嵌入包含 Token Type Embedding?
一句话结论:用于区分输入中的不同句子 segment(如句子 A 和句子 B)。展开:在 BERT 的 NSP(Next Sentence Prediction)任务和句子对分类任务中,模型需要知道哪个词属于哪个句子。Token Type Embedding(也叫 Segment Embedding)通过 0/1 标记实现这一区分。
Q11. 词嵌入训练时如何处理大小写?
一句话结论:通常统一转为小写以减少词汇表大小和稀疏性。展开:"Apple" 和 "apple" 在语义上通常相同,统一小写可以合并为同一词条。如果需要区分(如专有名词),可以使用 cased 模型或子词分词器。
Q12. 词嵌入能否替代语言模型?
一句话结论:不能,词嵌入只编码词的语义,语言模型还编码语序和语法。展开:词嵌入是静态的、无序的(仅考虑上下文窗口),无法完全替代需要完整语序信息的语言模型。上下文嵌入(BERT)更接近语言模型,但仍不同于自回归语言模型(GPT)。
Q13. 如何加速词嵌入的训练?
一句话结论:使用负采样、层次 Softmax、异步 SGD 或混合精度训练。展开:负采样将多分类转为二分类,大幅减少计算量。层次 Softmax 用二叉树替代扁平 Softmax。数据量大时可用多卡异步训练或混合精度(FP16)加速。
Q14. 词嵌入是否会导致性别偏见?
一句话结论:是的,预训练词嵌入会从语料中学习到社会偏见,需要去偏见处理。展开:Word2Vec 和 BERT 会从语料中学习到性别职业偏见(如 "doctor" 更接近男性)。去偏见方法包括:硬去偏见(强制特定词中性化)、软去偏见(对抗训练)、数据增强等。
Q15. 子词嵌入(FastText)和词嵌入(Word2Vec)的区别?
一句话结论:Word2Vec 以词为单位,FastText 以子词为单位,可以处理 OOV。展开:FastText 将词拆分为字符 n-gram(如 "where" → "wh", "her", "ere", "re"),词向量是子词向量的平均。这使得 FastText 可以为未见过的词生成向量,解决了 Word2Vec 的 OOV 问题。
Q16. 词嵌入和 One-Hot 的核心区别是什么?
一句话结论:One-Hot 高维稀疏无语义,词嵌入低维稠密有语义。展开:One-Hot 向量维度等于词汇表大小(10000+ 维),只有一个 1;词嵌入维度固定(100~512 维),每个维度都有值。One-Hot 词间正交,词嵌入词间可计算相似度。
Q17. BERT 的 [CLS] 向量如何用于分类?
一句话结论:[CLS] 向量经过 Pooler 层(全连接 + tanh)后作为序列表示。展开:[CLS] token 在 BERT 输入序列的最前端,其隐藏状态由 Transformer 编码器计算,融合了整个序列的信息。BERT 的 Pooler 将其转换为固定维度的分类向量,再接下游分类器。
Q18. 如何将预训练词嵌入用于 PyTorch 模型?
一句话结论:使用 nn.Embedding.from_pretrained() 加载预训练权重。展开:先将预训练向量(Word2Vec/GloVe)读取为 numpy array 或 torch tensor,然后作为 nn.Embedding 的初始权重传入。设置 freeze=False 可以微调,freeze=True 则冻结。
Q19. 词嵌入是否可以用于推荐系统?
一句话结论:可以,物品 ID 可以像词一样用嵌入表示,这就是 Embedding 在推荐中的核心应用。展开:推荐系统中的 item2vec(类比 word2vec)、Airbnb 的实时个性化搜索、阿里巴巴的DIN 都大量使用嵌入技术。用户行为序列被建模为"句子",物品被建模为"词"。
Q20. 为什么 Transformer 使用位置编码而不是位置嵌入?
一句话结论:位置编码(PE)使用确定性的正弦/余弦函数,位置嵌入(PE)是可学习的参数。展开:原始 Transformer 的正弦位置编码可以处理任意长度的序列(外推能力),且无需额外参数。BERT 等后续模型使用可学习的位置嵌入,通常效果更好,但受限于预训练的最大序列长度。
全篇总纲
词嵌入是 NLP 深度学习的基石技术,实现了从离散符号到连续向量的映射。独热编码到词嵌入的演进解决了语义表示问题;神经网络训练方法(Word2Vec)使嵌入可端到端学习;预训练范式(GloVe、FastText)提供了可复用的语义资源;上下文相关嵌入(BERT)解决了多义词问题。理解词嵌入的原理和实现,是掌握大语言模型和现代 NLP 系统的必经之路。

浙公网安备 33010602011771号