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,无法捕捉词与词之间的语义关系,也无法泛化到相似词。

没有词嵌入会发生什么?

  • 模型参数量巨大(每个词都对应独立参数)
  • 无法利用词的语义相似性进行泛化
  • 无法进行向量运算(如类比推理)
  • 深度学习模型难以有效学习语言表示
How — PyTorch 中定义一个嵌入层

在 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),指训练词嵌入时词汇表中不存在的单词。

模型遇到这类词时无法给出有意义的向量,通常统一用一个特殊符号 [UNK](unknown)代替,导致语义信息丢失。

词嵌入的痛点,子词方法可缓解
嵌入目标

训练词嵌入时,模型需要一个学习目标(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 模型的 PyTorch 实现

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) 的可学习矩阵。

标准写法 — PyTorch 嵌入层的完整用法

嵌入层支持多种高级特性: 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 系统的必经之路。


posted @ 2026-07-28 15:12  左扬  阅读(18)  评论(0)    收藏  举报