目录
- 概述
- 词表示的发展历程
- One-Hot编码
- TF-IDF与词袋模型
- Word2Vec详解
- GloVe详解
- FastText详解
- ELMo详解
- BERT详解
- GPT系列详解
- 语言模型数学基础
- 评估指标
- 完整代码实现
- 参考资料
1. 概述
1.1 什么是词嵌入
词嵌入(Word Embedding)是自然语言处理中最基础也是最重要的技术之一。它的核心目标是将离散的词语符号映射到连续的低维向量空间中,使得语义相近的词在向量空间中的距离也相近。这种映射不是随意的,而是通过大量文本数据学习得到的,能够捕捉词语之间的语义、语法和上下文关系。
举个直观的例子,在一个好的词嵌入空间中,"国王"和"女王"的向量应该很接近,因为它们都是君主;"国王"减去"男人"加上"女人"的结果应该接近"女王"的向量,这说明词嵌入能够捕捉到性别这种语义关系。
1.2 为什么需要词嵌入
在自然语言处理中,计算机无法直接理解文字。最简单的做法是用One-Hot编码表示每个词,但这种方式存在严重问题:第一,维度灾难------如果词汇表有10万个词,每个词就需要10万维的向量;第二,稀疏性------每个向量只有一个位置为1,其余都是0;第三,正交性------任意两个词的向量点积都为0,无法表示语义相似性。
词嵌入解决了这些问题:它将词表示为低维(通常100-300维)的稠密向量,语义相近的词向量距离近,语义不同的词向量距离远。更重要的是,词嵌入能够捕捉词语之间的复杂关系,如类比关系、上下位关系等。
1.3 发展历程概览
词嵌入和语言模型的发展可以分为几个阶段:
离散表示阶段(2000年之前):使用One-Hot编码、词袋模型、TF-IDF等方法,将词视为离散符号,无法捕捉语义关系。
分布式表示阶段(2013-2017):Word2Vec、GloVe、FastText等方法出现,通过神经网络学习词的分布式表示,首次实现了语义空间的构建。
预训练语言模型阶段(2018-2020):ELMo、BERT、GPT等模型出现,词表示从静态变为动态,同一个词在不同上下文中有不同的表示。
大语言模型阶段(2020至今):GPT-3、ChatGPT、GPT-4等模型出现,通过大规模预训练和指令微调,实现了强大的语言理解和生成能力。
2. 词表示的发展历程
2.1 离散表示的局限性
在分布式表示出现之前,NLP主要使用离散表示方法。这些方法将每个词视为一个独立的符号,词与词之间没有内在联系。这种假设与语言的实际情况严重不符------语言中的词是有丰富关系的,同义词、反义词、上下位词等关系无法被离散表示捕捉。
离散表示的另一个问题是维度灾难。对于一个包含10万个词的词汇表,One-Hot编码需要10万维的向量,而且每个向量中只有一个位置为1,其余都是0。这种极度稀疏的表示不仅浪费存储空间,而且无法进行有效的计算。
2.2 分布式假说
分布式假说(Distributional Hypothesis)是词嵌入的理论基础。该假说由语言学家John Rupert Firth在1957年提出,核心观点是:一个词的含义由其上下文决定。换句话说,如果两个词经常出现在相似的上下文中,那么它们的含义就是相近的。
这个假说可以从两个角度理解:共现角度 ------统计词与词的共现频率,构建共现矩阵,然后通过矩阵分解得到词向量;预测角度------训练神经网络根据上下文预测词(或根据词预测上下文),网络的权重就是词向量。
2.3 从静态到动态表示
早期的词嵌入方法(Word2Vec、GloVe)为每个词学习一个固定的向量,无论这个词出现在什么上下文中,向量都是一样的。这被称为静态词表示。
然而,自然语言中存在大量一词多义的现象。例如"bank"在"river bank"和"bank account"中的含义完全不同,但在静态词表示中,它们有相同的向量。为了解决这个问题,ELMo、BERT等模型提出了动态词表示,同一个词在不同上下文中有不同的向量表示。
3. One-Hot编码
3.1 原理
One-Hot编码是最简单的词表示方法。给定一个词汇表V,每个词用一个长度为|V|的向量表示,其中该词对应位置为1,其余位置为0。
例如,假设词汇表为"我", "喜欢", "学习", "机器", "深度",那么:
- "我" → 1, 0, 0, 0, 0
- "喜欢" → 0, 1, 0, 0, 0
- "学习" → 0, 0, 1, 0, 0
3.2 数学表示
设词汇表大小为V,词w的One-Hot编码为e_w ∈ R^V,其中:
e w i = { 1 if i = index ( w ) 0 otherwise e_wi = \begin{cases} 1 & \text{if } i = \text{index}(w) \\ 0 & \text{otherwise} \end{cases} ewi={10if i=index(w)otherwise
3.3 实现
python
import numpy as np
class OneHotEncoder:
def __init__(self, vocabulary):
self.vocab = vocabulary
self.word2idx = {w: i for i, w in enumerate(vocabulary)}
self.idx2word = {i: w for w, i in self.word2idx.items()}
def encode(self, word):
vector = np.zeros(len(self.vocab))
vector[self.word2idx[word]] = 1
return vector
def encode_batch(self, words):
return np.array([self.encode(w) for w in words])
# 示例
vocab = ["我", "喜欢", "学习", "机器", "深度"]
encoder = OneHotEncoder(vocab)
print(encoder.encode("喜欢")) # [0. 1. 0. 0. 0.]
3.4 局限性分析
维度灾难:词汇表越大,向量维度越高。现代语言模型的词汇表通常有3万-10万个词,这意味着每个词需要3万-10万维的向量。
稀疏性:每个向量中只有一个位置为1,其余都是0,存储和计算效率极低。
正交性:任意两个不同词的One-Hot向量的点积都为0,无法表示词之间的相似性。
泛化能力差:模型无法从一个词的经验泛化到语义相近的词。
4. TF-IDF与词袋模型
4.1 词袋模型
词袋模型(Bag of Words, BoW)是最简单的文本表示方法之一。它将文本看作一个词的集合,忽略词序和语法,只关注每个词出现的次数。
例如,句子"我 喜欢 学习 机器 学习"的词袋表示为{"我":1, "喜欢":1, "学习":2, "机器":1}。
4.2 TF-IDF详解
TF-IDF(Term Frequency-Inverse Document Frequency)是一种统计方法,用于评估一个词对于一个文档集合中的某篇文档的重要程度。
词频(TF):衡量一个词在文档中出现的频率。直觉上,一个词在文档中出现的次数越多,它对该文档就越重要。但简单的词频会高估常见词的重要性,因此需要进行归一化:
T F ( t , d ) = f ( t , d ) ∑ t ′ ∈ d f ( t ′ , d ) TF(t, d) = \frac{f(t, d)}{\sum_{t' \in d} f(t', d)} TF(t,d)=∑t′∈df(t′,d)f(t,d)
其中f(t,d)是词t在文档d中出现的次数,分母是文档d中所有词的总次数。
逆文档频率(IDF):衡量一个词的普遍重要性。如果一个词在很多文档中都出现,那么它的区分能力就弱,IDF值就低;反之,如果一个词只在少数文档中出现,它的区分能力强,IDF值就高:
I D F ( t , D ) = log ∣ D ∣ ∣ { d ∈ D : t ∈ d } ∣ IDF(t, D) = \log \frac{|D|}{|\{d \in D : t \in d\}|} IDF(t,D)=log∣{d∈D:t∈d}∣∣D∣
其中|D|是文档总数,分母是包含词t的文档数。
TF-IDF:将TF和IDF相乘,得到最终的权重:
T F I D F ( t , d , D ) = T F ( t , d ) × I D F ( t , D ) TFIDF(t, d, D) = TF(t, d) \times IDF(t, D) TFIDF(t,d,D)=TF(t,d)×IDF(t,D)
4.3 实现
python
import math
from collections import Counter
class TFIDF:
def __init__(self):
self.idf = {}
def fit(self, documents):
"""计算IDF"""
n = len(documents)
df = Counter() # 文档频率
for doc in documents:
unique_words = set(doc)
for word in unique_words:
df[word] += 1
self.idf = {word: math.log(n / count) for word, count in df.items()}
def transform(self, document):
"""计算TF-IDF"""
word_count = Counter(document)
total = len(document)
tfidf = {}
for word, count in word_count.items():
tf = count / total
idf = self.idf.get(word, math.log(len(document)))
tfidf[word] = tf * idf
return tfidf
# 示例
corpus = [
["我", "喜欢", "学习", "机器", "学习"],
["我", "喜欢", "深度", "学习"],
["机器", "学习", "很", "有趣"]
]
tfidf = TFIDF()
tfidf.fit(corpus)
print(tfidf.transform(["我", "喜欢", "机器", "学习"]))
4.4 TF-IDF的局限性
尽管TF-IDF在信息检索中广泛使用,但它仍然存在一些根本性的局限:第一,它无法捕捉词之间的语义相似性,"快乐"和"高兴"在TF-IDF看来是完全不同的词;第二,它忽略了词序信息,"我 爱 你"和"你 爱 我"的TF-IDF表示相同;第三,它仍然是稀疏的高维表示。
5. Word2Vec详解
5.1 背景与动机
2013年,Google的Tomas Mikolov等人提出了Word2Vec,这是词嵌入领域的里程碑式工作。Word2Vec的核心贡献是提出了一种高效的学习词向量的方法,使得在大规模语料上训练词向量成为可能。
Word2Vec基于分布式假说,通过神经网络学习词的分布式表示。与之前的矩阵分解方法不同,Word2Vec直接优化预测任务,训练效率更高,学到的词向量质量也更好。
5.2 CBOW模型
CBOW(Continuous Bag of Words)模型的核心思想是:给定一个词的上下文,预测这个词本身。具体来说,对于句子中的每个词w_t,CBOW使用其周围的词w_{t-c}, ..., w_{t-1}, w_{t+1}, ..., w_{t+c来预测w_t,其中c是窗口大小。
模型架构:
输入层:上下文词的One-Hot向量
隐藏层:将所有上下文词向量取平均
输出层:通过Softmax预测中心词
数学推导:
给定上下文词集合C = {w_{t-c}, ..., w_{t-1}, w_{t+1}, ..., w_{t+c}},中心词w_t的预测概率为:
P ( w t ∣ C ) = exp ( v w t ′ ⋅ v ˉ ) ∑ w ∈ V exp ( v w ′ ⋅ v ˉ ) P(w_t | C) = \frac{\exp(v'{w_t} \cdot \bar{v})}{\sum{w \in V} \exp(v'_w \cdot \bar{v})} P(wt∣C)=∑w∈Vexp(vw′⋅vˉ)exp(vwt′⋅vˉ)
其中v'_w是词w的输出向量,v̄是上下文词向量的平均值:
v ˉ = 1 2 c ∑ w ∈ C v w \bar{v} = \frac{1}{2c} \sum_{w \in C} v_w vˉ=2c1w∈C∑vw
损失函数:
CBOW的训练目标是最大化训练数据的对数似然:
L = − ∑ t = 1 T log P ( w t ∣ C t ) \mathcal{L} = -\sum_{t=1}^{T} \log P(w_t | C_t) L=−t=1∑TlogP(wt∣Ct)
实现:
python
import torch
import torch.nn as nn
import torch.nn.functional as F
class CBOWModel(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super().__init__()
# 输入嵌入矩阵
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
# 输出嵌入矩阵
self.output_embeddings = nn.Embedding(vocab_size, embedding_dim)
def forward(self, context_ids, target_ids):
"""
Args:
context_ids: 上下文词索引 [batch_size, 2*window_size]
target_ids: 目标词索引 [batch_size]
"""
# 获取上下文词向量并取平均
context_embeds = self.embeddings(context_ids) # [batch, 2c, dim]
context_mean = context_embeds.mean(dim=1) # [batch, dim]
# 获取目标词向量
target_embeds = self.output_embeddings(target_ids) # [batch, dim]
# 计算相似度分数
scores = torch.sum(context_mean * target_embeds, dim=1) # [batch]
return scores
5.3 Skip-gram模型
Skip-gram模型与CBOW相反:给定中心词,预测其上下文词。具体来说,对于句子中的每个词w_t,Skip-gram使用w_t来预测其周围的词w_{t+j}(-c ≤ j ≤ c, j ≠ 0)。
数学模型:
给定中心词w_t,预测上下文词w_{t+j}的概率为:
P ( w t + j ∣ w t ) = exp ( v w t + j ′ ⋅ v w t ) ∑ w ∈ V exp ( v w ′ ⋅ v w t ) P(w_{t+j} | w_t) = \frac{\exp(v'{w{t+j}} \cdot v_{w_t})}{\sum_{w \in V} \exp(v'w \cdot v{w_t})} P(wt+j∣wt)=∑w∈Vexp(vw′⋅vwt)exp(vwt+j′⋅vwt)
损失函数:
L = − ∑ t = 1 T ∑ − c ≤ j ≤ c , j ≠ 0 log P ( w t + j ∣ w t ) \mathcal{L} = -\sum_{t=1}^{T} \sum_{-c \leq j \leq c, j \neq 0} \log P(w_{t+j} | w_t) L=−t=1∑T−c≤j≤c,j=0∑logP(wt+j∣wt)
CBOW vs Skip-gram:
CBOW和Skip-gram各有优劣。CBOW训练速度快,适合大规模数据集,对高频词效果好。Skip-gram训练速度慢,但对低频词和罕见词效果更好,因为它为每个词-上下文对单独训练。实践中,Skip-gram在语义相似性任务上通常表现更好。
5.4 负采样
问题:Softmax的计算需要遍历整个词汇表,对于大词汇表来说计算量巨大。假设词汇表有10万个词,每次更新都需要计算10万个词的概率。
解决方案:负采样(Negative Sampling)。核心思想是将多分类问题转化为二分类问题:对于一个正样本(真实的词-上下文对),随机采样一些负样本(不真实的词-上下文对),然后训练模型区分正样本和负样本。
损失函数:
L = − log σ ( v w O ′ ⋅ v w I ) − ∑ i = 1 k E w i ∼ P n ( w ) log σ ( − v w i ′ ⋅ v w I ) \mathcal{L} = -\log \sigma(v'{w_O} \cdot v{w_I}) - \sum_{i=1}^{k} \mathbb{E}_{w_i \sim P_n(w)} \\log \\sigma(-v'_{w_i} \\cdot v_{w_I}) L=−logσ(vwO′⋅vwI)−i=1∑kEwi∼Pn(w)logσ(−vwi′⋅vwI)
其中σ是sigmoid函数,k是负样本数,P_n(w)是噪声分布。
噪声分布:论文中使用词频的3/4次方作为噪声分布:
P n ( w ) = f ( w ) 3 / 4 ∑ w ′ f ( w ′ ) 3 / 4 P_n(w) = \frac{f(w)^{3/4}}{\sum_{w'} f(w')^{3/4}} Pn(w)=∑w′f(w′)3/4f(w)3/4
为什么用3/4次方?因为这介于均匀分布和词频分布之间,对低频词有一定的提升,使得低频词也有机会被采样为负样本。
实现:
python
class SkipGramNegSampling(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super().__init__()
self.center_embeddings = nn.Embedding(vocab_size, embedding_dim)
self.context_embeddings = nn.Embedding(vocab_size, embedding_dim)
def forward(self, center, context, negatives):
"""
Args:
center: 中心词 [batch_size]
context: 正样本上下文词 [batch_size]
negatives: 负样本 [batch_size, num_neg]
"""
center_embed = self.center_embeddings(center) # [batch, dim]
context_embed = self.context_embeddings(context) # [batch, dim]
neg_embed = self.context_embeddings(negatives) # [batch, num_neg, dim]
# 正样本分数
pos_score = torch.sum(center_embed * context_embed, dim=1)
pos_loss = -F.logsigmoid(pos_score)
# 负样本分数
neg_score = torch.bmm(neg_embed, center_embed.unsqueeze(2)).squeeze(2)
neg_loss = -F.logsigmoid(-neg_score).sum(dim=1)
return (pos_loss + neg_loss).mean()
5.5 层次化Softmax
层次化Softmax(Hierarchical Softmax)是另一种加速训练的方法。它将词汇表组织成一棵二叉树,每个词对应一个叶子节点。预测一个词变成了从根到叶子的一系列二分类问题。
二叉树结构:
通常使用霍夫曼树构建二叉树,高频词离根节点近,低频词离根节点远。这样高频词只需要几次比较就能被预测到,而低频词需要更多比较。
数学模型:
对于词w,设从根到w的路径上有L(w)个节点,路径上的第j个节点的左右分支分别编码为ch(w,j) ∈ {0,1}。那么词w的概率为:
P ( w ) = ∏ j = 1 L ( w ) − 1 σ ( \[ c h ( w , j + 1 ) = right ] ⋅ v n ( w , j ) ′ ⋅ v w ) P(w) = \prod_{j=1}^{L(w)-1} \sigma(\\!\[ch(w,j+1) = \\text{right}\!] \cdot v'_{n(w,j)} \cdot v_w) P(w)=j=1∏L(w)−1σ(\[ch(w,j+1)=right]⋅vn(w,j)′⋅vw)
其中n(w,j)是路径上第j个内部节点,v'是内部节点的向量。
计算复杂度:从O(V)降低到O(log V),对于大词汇表来说提升显著。
5.6 词向量的性质
Word2Vec学到的词向量具有许多有趣的性质:
语义相似性:语义相近的词在向量空间中距离近。例如,"猫"和"狗"的距离比"猫"和"桌子"的距离近。
类比关系:词向量能够捕捉类比关系。最著名的例子是:king - man + woman ≈ queen。这说明词向量空间中存在线性结构,能够编码语义关系。
语法关系:词向量也能捕捉语法关系。例如:walking - walked + swam ≈ swimming,这说明词向量能够编码时态关系。
向量运算:
python
def analogy(word_vectors, a, b, c, top_k=5):
"""词类比:a - b + c ≈ ?"""
vec = word_vectors[a] - word_vectors[b] + word_vectors[c]
# 计算与所有词的相似度
similarities = {}
for word in word_vectors:
if word not in [a, b, c]:
sim = cosine_similarity(vec, word_vectors[word])
similarities[word] = sim
# 返回最相似的词
return sorted(similarities.items(), key=lambda x: x[1], reverse=True)[:top_k]
# 示例
# analogy(vectors, "国王", "男人", "女人") → [("女王", 0.85), ...]
6. GloVe详解
6.1 动机与思想
GloVe(Global Vectors for Word Representation)由Stanford的Jeffrey Pennington等人于2014年提出。GloVe的核心思想是结合全局统计信息和局部上下文窗口的优点。
Word2Vec基于局部上下文窗口,只考虑词周围的几个词,忽略了全局的共现统计信息。而基于矩阵分解的方法(如LSA)利用了全局统计信息,但计算复杂度高,且效果不如Word2Vec。
GloVe通过优化词向量使得它们的点积等于词的共现概率的对数,从而结合了两者的优势。
6.2 共现矩阵
GloVe首先构建词-词共现矩阵X,其中X_ij表示词j出现在词i的上下文窗口中的次数。
设词汇表大小为V,共现矩阵X ∈ R^{V×V}。对于给定的上下文窗口大小,我们可以统计整个语料库中所有词对的共现次数。
共现概率定义为:
P i j = P ( j ∣ i ) = X i j X i P_{ij} = P(j | i) = \frac{X_{ij}}{X_i} Pij=P(j∣i)=XiXij
其中 X i = ∑ k X i k X_i = \sum_k X_{ik} Xi=∑kXik 是词i的所有共现次数。
6.3 目标函数推导
GloVe的关键洞察是:词向量应该满足以下关系:
w i T w ~ j + b i + b ~ j = log X i j w_i^T \tilde{w}_j + b_i + \tilde{b}j = \log X{ij} wiTw~j+bi+b~j=logXij
其中 w i w_i wi 和 w ~ j \tilde{w}_j w~j 分别是词i和词j的向量, b i b_i bi 和 b ~ j \tilde{b}_j b~j 是偏置项。
为什么是log?因为共现计数的分布是高度偏斜的(少数词对共现次数很高,大多数很低),取对数可以压缩这种偏斜。
损失函数:
J = ∑ i , j = 1 V f ( X i j ) ( w i T w ~ j + b i + b ~ j − log X i j ) 2 J = \sum_{i,j=1}^{V} f(X_{ij}) (w_i^T \tilde{w}_j + b_i + \tilde{b}j - \log X{ij})^2 J=i,j=1∑Vf(Xij)(wiTw~j+bi+b~j−logXij)2
其中f是权重函数,用于处理不同共现次数的词对:
f ( x ) = { ( x / x m a x ) α if x < x m a x 1 otherwise f(x) = \begin{cases} (x/x_{max})^\alpha & \text{if } x < x_{max} \\ 1 & \text{otherwise} \end{cases} f(x)={(x/xmax)α1if x<xmaxotherwise
权重函数的作用:高频共现的词对权重被截断为1,避免它们主导损失函数;低频共现的词对权重较小,减少噪声的影响;零共现的词对不参与计算。
6.4 与Word2Vec的关系
论文证明了GloVe和Word2Vec在数学上是等价的。Skip-gram模型实际上在隐式地分解词的共现矩阵的对数。GloVe的优势在于它显式地利用了全局共现信息,训练效率更高。
6.5 实现
python
class GloVeModel(nn.Module):
def __init__(self, vocab_size, embedding_dim, x_max=100, alpha=0.75):
super().__init__()
self.embedding_dim = embedding_dim
self.x_max = x_max
self.alpha = alpha
# 词向量和偏置
self.wi = nn.Embedding(vocab_size, embedding_dim)
self.wj = nn.Embedding(vocab_size, embedding_dim)
self.bi = nn.Embedding(vocab_size, 1)
self.bj = nn.Embedding(vocab_size, 1)
# 初始化
nn.init.xavier_uniform_(self.wi.weight)
nn.init.xavier_uniform_(self.wj.weight)
nn.init.zeros_(self.bi.weight)
nn.init.zeros_(self.bj.weight)
def weight_function(self, x):
"""权重函数"""
return torch.where(
x < self.x_max,
(x / self.x_max) ** self.alpha,
torch.ones_like(x)
)
def forward(self, i_indices, j_indices, x_ij):
wi = self.wi(i_indices)
wj = self.wj(j_indices)
bi = self.bi(i_indices).squeeze()
bj = self.bj(j_indices).squeeze()
# 计算损失
weight = self.weight_function(x_ij)
prediction = torch.sum(wi * wj, dim=1) + bi + bj
loss = weight * (prediction - torch.log(x_ij)) ** 2
return loss.mean()
7. FastText详解
7.1 核心思想
FastText由Facebook AI Research于2017年提出。它的核心创新是将词视为字符n-gram的集合,而不是一个整体。这使得FastText能够处理未登录词(OOV)和形态丰富的语言。
例如,对于词"where",FastText会将其分解为字符trigram:"<wh", "whe", "her", "ere", "re>"。其中"<"和">"是特殊字符,标记词的开始和结束。
7.2 词向量计算
一个词的向量是其所有字符n-gram向量的和:
v w = ∑ g ∈ G w z g v_w = \sum_{g \in G_w} z_g vw=g∈Gw∑zg
其中G_w是词w的所有字符n-gram集合,z_g是n-gram g的向量。
7.3 处理未登录词
由于FastText使用字符n-gram,当遇到未登录词时,虽然这个词本身没有对应的向量,但它的字符n-gram可能已经在训练数据中出现过。因此,FastText可以通过这些已知的n-gram向量来构建未登录词的向量。
例如,如果"unhappiness"是未登录词,但"un", "happ", "iness"等n-gram已经在训练数据中出现过,FastText就可以组合这些n-gram向量来表示"unhappiness"。
7.4 优势
- 处理未登录词:通过字符n-gram,FastText能够为训练数据中没有出现的词生成向量。
- 适合形态丰富的语言:对于德语、芬兰语等有丰富词形变化的语言,FastText能够更好地捕捉词缀信息。
- 训练速度快:FastText的训练速度与Word2Vec相当。
- 支持多语言:FastText提供了157种语言的预训练词向量。
8. ELMo详解
8.1 核心创新
ELMo(Embeddings from Language Models)由Allen AI于2018年提出,是第一个成功的上下文化词表示方法。它的核心创新是:同一个词在不同上下文中有不同的向量表示。
例如,"bank"在"river bank"中应该表示"河岸",在"bank account"中应该表示"银行"。传统的静态词向量无法区分这两种情况,而ELMo可以。
8.2 架构
ELMo使用双向语言模型(biLM)来生成词表示。具体来说,它使用两层LSTM:一层从左到右处理句子,另一层从右到左处理句子。
前向语言模型:
P ( w t ∣ w 1 , . . . , w t − 1 ) P(w_t | w_1, ..., w_{t-1}) P(wt∣w1,...,wt−1)
后向语言模型:
P ( w t ∣ w t + 1 , . . . , w n ) P(w_t | w_{t+1}, ..., w_n) P(wt∣wt+1,...,wn)
双向语言模型:
L = ∑ t = 1 n ( log P ( w t ∣ w 1 , . . . , w t − 1 ) + log P ( w t ∣ w t + 1 , . . . , w n ) ) \mathcal{L} = \sum_{t=1}^{n} (\log P(w_t | w_1, ..., w_{t-1}) + \log P(w_t | w_{t+1}, ..., w_n)) L=t=1∑n(logP(wt∣w1,...,wt−1)+logP(wt∣wt+1,...,wn))
8.3 上下文化表示
ELMo为每个词生成L+1个表示(L层LSTM + 1层输入嵌入)。最终的ELMo向量是这些表示的加权和:
E L M o k = γ ∑ l = 0 L s l h k , l ELMo_k = \gamma \sum_{l=0}^{L} s_l h_{k,l} ELMok=γl=0∑Lslhk,l
其中h_{k,l}是第l层的隐藏状态,s_l是可学习的层权重,γ是缩放因子。
层权重的意义:不同的层捕捉不同的信息。底层更多捕捉语法信息,高层更多捕捉语义信息。通过学习层权重,ELMo可以自动选择对下游任务最有用的信息。
8.4 实现
python
class ELMo(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, num_layers):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
# 双向LSTM
self.forward_lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers,
batch_first=True)
self.backward_lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers,
batch_first=True)
# 层权重
self.layer_weights = nn.Parameter(torch.ones(num_layers + 1))
self.gamma = nn.Parameter(torch.ones(1))
def forward(self, input_ids):
embed = self.embedding(input_ids)
# 前向LSTM
forward_out, _ = self.forward_lstm(embed)
# 后向LSTM
backward_input = torch.flip(embed, [1])
backward_out, _ = self.backward_lstm(backward_input)
backward_out = torch.flip(backward_out, [1])
# 加权组合
weights = F.softmax(self.layer_weights, dim=0)
elmo = self.gamma * (weights[0] * embed +
weights[1] * forward_out +
weights[2] * backward_out)
return elmo
9. BERT详解
9.1 核心创新
BERT(Bidirectional Encoder Representations from Transformers)由Google于2018年提出,是NLP领域的里程碑式工作。BERT的核心创新包括:
- 双向上下文:与GPT的单向不同,BERT能够同时利用左侧和右侧的上下文信息。
- 掩码语言模型(MLM):随机掩盖部分词,让模型预测被掩盖的词,这是实现双向上下文的关键。
- 下一句预测(NSP):预测两个句子是否相邻,帮助模型理解句子间关系。
9.2 预训练任务
掩码语言模型:
输入序列中15%的词被随机选择进行处理:
- 80%替换为MASK
- 10%替换为随机词
- 10%保持不变
为什么这样处理?如果总是用MASK替换,模型在微调时不会看到MASK,导致训练和测试不一致。通过随机替换和保持不变,模型被迫学习每个位置的表示。
下一句预测:
给定句子对(A, B),预测B是否是A的下一句。标签为IsNext或NotNext。
9.3 架构
BERT使用Transformer编码器,核心是多头自注意力机制。
自注意力:
Attention ( Q , K , V ) = softmax ( Q K T d k ) V \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V Attention(Q,K,V)=softmax(dk QKT)V
多头注意力:
MultiHead ( Q , K , V ) = Concat ( head 1 , . . . , head h ) W O \text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, ..., \text{head}_h)W^O MultiHead(Q,K,V)=Concat(head1,...,headh)WO
其中每个头:
head i = Attention ( Q W i Q , K W i K , V W i V ) \text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V) headi=Attention(QWiQ,KWiK,VWiV)
9.4 微调
BERT的强大之处在于,预训练后只需在下游任务上添加简单的分类层,通过微调就能达到很好的效果。这大大减少了标注数据的需求。
10. GPT系列详解
10.1 GPT-1
GPT-1(Generative Pre-trained Transformer)由OpenAI于2018年提出,使用Transformer解码器进行自回归语言模型预训练。
自回归语言模型:
P ( w 1 , w 2 , . . . , w n ) = ∏ i = 1 n P ( w i ∣ w 1 , . . . , w i − 1 ) P(w_1, w_2, ..., w_n) = \prod_{i=1}^{n} P(w_i | w_1, ..., w_{i-1}) P(w1,w2,...,wn)=i=1∏nP(wi∣w1,...,wi−1)
GPT-1使用12层Transformer解码器,117M参数,在BooksCorpus上预训练。
10.2 GPT-2
GPT-2将模型规模扩展到1.5B参数,使用更大的WebText数据集(40GB文本)。GPT-2展示了强大的零样本学习能力,引发了"太危险而不能发布"的讨论。
10.3 GPT-3
GPT-3将规模扩展到175B参数,是当时最大的语言模型。GPT-3的主要贡献是展示了few-shot学习 和in-context学习的能力:只需在提示中给出几个示例,模型就能完成新任务,无需微调。
涌现能力:随着模型规模的增大,GPT-3展示了一些"涌现能力"------这些能力在小模型中不存在,但在大模型中突然出现。
10.4 ChatGPT与GPT-4
ChatGPT通过RLHF(Reinforcement Learning from Human Feedback)对GPT-3.5进行对齐训练,使其能够更好地遵循指令、生成有用的回答、拒绝有害的请求。
GPT-4进一步扩展了能力,支持多模态输入(文本+图像),推理能力更强,安全性更好。
11. 语言模型数学基础
11.1 概率语言模型
语言模型的核心任务是估计一个文本序列的概率:
P ( w 1 , w 2 , . . . , w n ) P(w_1, w_2, ..., w_n) P(w1,w2,...,wn)
通过链式法则分解:
P ( w 1 , w 2 , . . . , w n ) = ∏ i = 1 n P ( w i ∣ w 1 , . . . , w i − 1 ) P(w_1, w_2, ..., w_n) = \prod_{i=1}^{n} P(w_i | w_1, ..., w_{i-1}) P(w1,w2,...,wn)=i=1∏nP(wi∣w1,...,wi−1)
11.2 困惑度
困惑度(Perplexity)是评估语言模型的标准指标:
P P L = P ( w 1 , w 2 , . . . , w n ) − 1 / n = exp ( − 1 n ∑ i = 1 n log P ( w i ∣ w < i ) ) PPL = P(w_1, w_2, ..., w_n)^{-1/n} = \exp\left(-\frac{1}{n}\sum_{i=1}^{n} \log P(w_i | w_{<i})\right) PPL=P(w1,w2,...,wn)−1/n=exp(−n1i=1∑nlogP(wi∣w<i))
困惑度越低,语言模型越好。直觉上,困惑度表示模型在每个位置上平均有多少个等可能的选择。
11.3 Softmax
Softmax函数将logits转换为概率分布:
P ( y = k ∣ x ) = exp ( z k ) ∑ j = 1 V exp ( z j ) P(y=k|x) = \frac{\exp(z_k)}{\sum_{j=1}^{V} \exp(z_j)} P(y=k∣x)=∑j=1Vexp(zj)exp(zk)
数值稳定性优化:
softmax ( z ) k = exp ( z k − max ( z ) ) ∑ j exp ( z j − max ( z ) ) \text{softmax}(z)k = \frac{\exp(z_k - \max(z))}{\sum{j} \exp(z_j - \max(z))} softmax(z)k=∑jexp(zj−max(z))exp(zk−max(z))
11.4 交叉熵损失
语言模型训练使用交叉熵损失:
L = − 1 n ∑ i = 1 n log P ( w i ∣ w < i ) \mathcal{L} = -\frac{1}{n}\sum_{i=1}^{n} \log P(w_i | w_{<i}) L=−n1i=1∑nlogP(wi∣w<i)
这等价于最小化困惑度的对数。
12. 评估指标
12.1 困惑度
python
import math
def compute_perplexity(model, data):
total_loss = 0
total_tokens = 0
for batch in data:
logits = model(batch['input_ids'])
loss = F.cross_entropy(logits.view(-1, logits.size(-1)),
batch['labels'].view(-1), reduction='sum')
total_loss += loss.item()
total_tokens += batch['labels'].numel()
return math.exp(total_loss / total_tokens)
12.2 词向量评估
词相似度:计算词向量相似度与人类评分的相关性。
词类比:测试词向量的类比能力。
下游任务:在文本分类、命名实体识别等任务上评估。
13. 完整代码实现
13.1 Word2Vec完整实现
python
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from collections import Counter
class Word2VecTrainer:
def __init__(self, corpus, embedding_dim=100, window_size=5,
min_count=5, num_negatives=5, batch_size=512, epochs=10):
self.embedding_dim = embedding_dim
self.window_size = window_size
self.num_negatives = num_negatives
self.batch_size = batch_size
self.epochs = epochs
# 构建词汇表
word_counts = Counter(w for sent in corpus for w in sent)
self.vocab = [w for w, c in word_counts.items() if c >= min_count]
self.word2idx = {w: i for i, w in enumerate(self.vocab)}
self.vocab_size = len(self.vocab)
# 噪声分布
freqs = np.array([word_counts[w] for w in self.vocab])
self.noise_dist = torch.from_numpy(freqs ** 0.75 / (freqs ** 0.75).sum())
# 构建训练对
self.pairs = self._build_pairs(corpus)
# 模型
self.model = SkipGramNegSampling(self.vocab_size, embedding_dim)
self.optimizer = torch.optim.Adam(self.model.parameters(), lr=0.003)
def _build_pairs(self, corpus):
pairs = []
for sent in corpus:
indices = [self.word2idx[w] for w in sent if w in self.word2idx]
for i, center in enumerate(indices):
for j in range(max(0, i - self.window_size),
min(len(indices), i + self.window_size + 1)):
if i != j:
pairs.append((center, indices[j]))
return pairs
def train(self):
for epoch in range(self.epochs):
np.random.shuffle(self.pairs)
total_loss = 0
for i in range(0, len(self.pairs), self.batch_size):
batch = self.pairs[i:i + self.batch_size]
centers = torch.LongTensor([p[0] for p in batch])
contexts = torch.LongTensor([p[1] for p in batch])
negatives = torch.multinomial(
self.noise_dist, len(batch) * self.num_negatives
).view(len(batch), self.num_negatives)
loss = self.model(centers, contexts, negatives)
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch}, Loss: {total_loss:.4f}")
def get_embedding(self, word):
idx = self.word2idx.get(word)
if idx is None:
return None
return self.model.center_embeddings.weight[idx].detach().numpy()
13.2 使用Transformers库
python
from transformers import AutoTokenizer, AutoModel
import torch
# 加载预训练模型
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
model = AutoModel.from_pretrained("bert-base-uncased")
# 获取词向量
text = "The cat sat on the mat"
inputs = tokenizer(text, return_tensors="pt")
outputs = model(**inputs)
# 最后一层的隐藏状态
last_hidden_state = outputs.last_hidden_state # [1, seq_len, 768]
# [CLS]向量可用于句子分类
cls_vector = last_hidden_state[:, 0, :]
14. 参考资料
核心论文
- Word2Vec: Mikolov et al., "Efficient Estimation of Word Representations in Vector Space", 2013
- GloVe: Pennington et al., "GloVe: Global Vectors for Word Representation", 2014
- FastText: Bojanowski et al., "Enriching Word Vectors with Subword Information", 2017
- ELMo: Peters et al., "Deep contextualized word representations", 2018
- BERT: Devlin et al., "BERT: Pre-training of Deep Bidirectional Transformers", 2019
- GPT: Radford et al., "Improving Language Understanding by Generative Pre-Training", 2018
开源库
- Gensim: https://github.com/RaRe-Technologies/gensim
- Transformers: https://github.com/huggingface/transformers
- FastText: https://github.com/facebookresearch/fastText