Word Embeddings原理与Transformer实现详解
·
1. 为什么我们需要Word Embeddings?
在自然语言处理(NLP)领域,计算机无法直接理解人类语言中的单词和句子。传统方法使用one-hot编码表示单词,比如用[1,0,0]表示"猫",[0,1,0]表示"狗"。这种方法存在两个致命缺陷:一是维度灾难(vocabulary有多大向量就有多长),二是无法表达单词之间的语义关系("猫"和"狗"都是宠物,但它们的向量正交)。
Word Embeddings通过将单词映射到低维稠密向量空间(通常50-1000维),完美解决了这两个问题。在这个空间中:
- 语义相似的单词距离相近("猫"和"狗"的向量夹角小)
- 单词关系可以通过向量运算表达("国王"-"男"+"女"≈"女王")
- 上下文信息被编码在向量维度中(某些维度可能代表"性别"、"时态"等特征)
提示:Embedding维度不是越大越好。实践中,英语通常用300-768维,中文用200-512维。维度太高会导致过拟合,太低则无法充分表达语义。
2. Transformer中的Embedding层实现细节
2.1 基本Embedding结构
在PyTorch中,Embedding层本质是一个可训练的查找表:
import torch.nn as nn
# vocab_size: 词表大小(如50000)
# embedding_dim: 嵌入维度(如768)
embedding_layer = nn.Embedding(vocab_size, embedding_dim)
前向传播时,输入单词ID(如42),输出对应embedding向量(形状为[embedding_dim])。这个过程可以理解为:
- 将单词ID转换为one-hot向量([0,...,1,...,0])
- 与Embedding矩阵相乘(等效于查表)
2.2 Transformer的特殊处理
原始Transformer论文中,Embedding层有三个关键设计:
- 统一维度:输入输出Embedding共享权重(减少参数量)
- 位置编码:添加正弦位置信号(因为Transformer没有RNN的时序感知能力)
- 缩放因子:embedding值乘以√d_model(防止经过softmax后梯度太小)
实现示例:
class TransformerEmbedding(nn.Module):
def __init__(self, vocab_size, d_model):
super().__init__()
self.token_embed = nn.Embedding(vocab_size, d_model)
self.scale = torch.sqrt(torch.FloatTensor([d_model]))
def forward(self, x):
# x: [batch_size, seq_len]
embed = self.token_embed(x) * self.scale.to(x.device)
return embed # [batch_size, seq_len, d_model]
3. 从零训练Embedding的实战技巧
3.1 数据准备要点
训练优质Embedding需要:
- 大规模语料(至少百万级token)
- 合理的tokenization(英文用BPE,中文用分词)
- 上下文窗口设计(通常5-10个词)
以阿拉伯语为例的数据处理流程:
from sklearn.model_selection import train_test_split
class TextDataset:
def __init__(self, file_path):
with open(file_path) as f:
self.texts = [article['text'] for article in json.load(f)]
# 划分训练测试集
self.train_texts, self.test_texts = train_test_split(
self.texts, test_size=0.1, random_state=42)
# 构建词汇表
self.vocab = self._build_vocab(self.train_texts)
def _build_vocab(self, texts):
vocab = set()
for text in texts:
tokens = text.split() # 简单空格分词
vocab.update(tokens)
return sorted(vocab)
3.2 网络架构设计
推荐使用Skip-gram或CBOW架构。以下是PyTorch实现示例:
class SkipGramModel(nn.Module):
def __init__(self, vocab_size, embed_dim):
super().__init__()
self.in_embed = nn.Embedding(vocab_size, embed_dim)
self.out_embed = nn.Embedding(vocab_size, embed_dim)
def forward(self, target, context):
# target: [batch_size]
# context: [batch_size]
target_embed = self.in_embed(target) # [batch_size, embed_dim]
context_embed = self.out_embed(context) # [batch_size, embed_dim]
scores = torch.matmul(target_embed, context_embed.t()) # [batch_size, batch_size]
return scores
关键训练技巧:
- 使用负采样(negative sampling)加速训练
- 初始学习率设为0.025,线性衰减
- 采用Subsampling高频词(如"的"、"是")
4. 预训练Embedding的妙用
4.1 迁移学习实践
可以直接加载预训练Embedding提升模型性能:
import gensim.downloader as api
# 下载GloVe预训练向量
glove_vectors = api.load("glove-wiki-gigaword-300")
# 构建PyTorch Embedding层
embedding_matrix = torch.zeros((vocab_size, 300))
for word, idx in word2idx.items():
if word in glove_vectors:
embedding_matrix[idx] = torch.tensor(glove_vectors[word])
embedding_layer = nn.Embedding.from_pretrained(embedding_matrix, freeze=False)
4.2 可视化分析
使用TSNE降维可视化Embedding:
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
words = ["king", "queen", "man", "woman", "paris", "france"]
vectors = [embedding_layer(word2idx[w]) for w in words]
tsne = TSNE(n_components=2)
vectors_2d = tsne.fit_transform(torch.stack(vectors).detach().numpy())
plt.figure(figsize=(10,6))
for i, word in enumerate(words):
plt.scatter(vectors_2d[i,0], vectors_2d[i,1])
plt.annotate(word, xy=(vectors_2d[i,0], vectors_2d[i,1]))
plt.show()
典型问题排查:
- 所有点挤在一起 → 尝试调大TSNE的perplexity参数
- 出现异常离群点 → 检查对应单词的语料质量
5. Transformer中Embedding的进阶话题
5.1 动态vs静态Embedding
- 静态:Word2Vec/GloVe,训练后固定
- 动态:BERT/ELMo,根据上下文变化
Transformer使用的是动态Embedding,但与传统RNN不同:
- 位置信息通过Positional Encoding显式注入
- 每个位置的Embedding独立计算(可并行)
- 通过Self-Attention捕获长距离依赖
5.2 跨语言Embedding
通过共享部分参数实现多语言统一表示:
class MultilingualEmbedding(nn.Module):
def __init__(self, vocab_sizes, embed_dim):
super().__init__()
# 共享核心特征空间
self.shared_embed = nn.Parameter(torch.randn(embed_dim, embed_dim))
# 语言特定映射
self.lang_embeds = nn.ModuleList([
nn.Embedding(vsize, embed_dim) for vsize in vocab_sizes
])
def forward(self, x, lang_id):
lang_embed = self.lang_embeds[lang_id](x) # [batch, seq, dim]
return torch.matmul(lang_embed, self.shared_embed) # 投影到共享空间
我在实际项目中发现,当处理形态丰富的语言(如阿拉伯语)时,建议:
- 对词根和词缀分别做Embedding
- 使用字符级CNN辅助单词表示
- 添加层归一化稳定训练过程
更多推荐
所有评论(0)