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])。这个过程可以理解为:

  1. 将单词ID转换为one-hot向量([0,...,1,...,0])
  2. 与Embedding矩阵相乘(等效于查表)

2.2 Transformer的特殊处理

原始Transformer论文中,Embedding层有三个关键设计:

  1. 统一维度:输入输出Embedding共享权重(减少参数量)
  2. 位置编码:添加正弦位置信号(因为Transformer没有RNN的时序感知能力)
  3. 缩放因子: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需要:

  1. 大规模语料(至少百万级token)
  2. 合理的tokenization(英文用BPE,中文用分词)
  3. 上下文窗口设计(通常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()

典型问题排查:

  1. 所有点挤在一起 → 尝试调大TSNE的perplexity参数
  2. 出现异常离群点 → 检查对应单词的语料质量

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)  # 投影到共享空间

我在实际项目中发现,当处理形态丰富的语言(如阿拉伯语)时,建议:

  1. 对词根和词缀分别做Embedding
  2. 使用字符级CNN辅助单词表示
  3. 添加层归一化稳定训练过程
Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐