词嵌入与语言模型详解

目录

  1. 概述
  2. 词表示的发展历程
  3. One-Hot编码
  4. TF-IDF与词袋模型
  5. Word2Vec详解
  6. GloVe详解
  7. FastText详解
  8. ELMo详解
  9. BERT详解
  10. GPT系列详解
  11. 语言模型数学基础
  12. 评估指标
  13. 完整代码实现
  14. 参考资料

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 优势

  1. 处理未登录词:通过字符n-gram,FastText能够为训练数据中没有出现的词生成向量。
  2. 适合形态丰富的语言:对于德语、芬兰语等有丰富词形变化的语言,FastText能够更好地捕捉词缀信息。
  3. 训练速度快:FastText的训练速度与Word2Vec相当。
  4. 支持多语言: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的核心创新包括:

  1. 双向上下文:与GPT的单向不同,BERT能够同时利用左侧和右侧的上下文信息。
  2. 掩码语言模型(MLM):随机掩盖部分词,让模型预测被掩盖的词,这是实现双向上下文的关键。
  3. 下一句预测(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. 参考资料

核心论文

  1. Word2Vec: Mikolov et al., "Efficient Estimation of Word Representations in Vector Space", 2013
  2. GloVe: Pennington et al., "GloVe: Global Vectors for Word Representation", 2014
  3. FastText: Bojanowski et al., "Enriching Word Vectors with Subword Information", 2017
  4. ELMo: Peters et al., "Deep contextualized word representations", 2018
  5. BERT: Devlin et al., "BERT: Pre-training of Deep Bidirectional Transformers", 2019
  6. GPT: Radford et al., "Improving Language Understanding by Generative Pre-Training", 2018

开源库


相关推荐
Lei活在当下14 小时前
【AI手记系列-2026/6/18】iSparto & Harness,Caveman 以及AI时代的生存指南
人工智能·llm·openai
冬奇Lab15 小时前
每日一个开源项目(第134篇):Zvec - 阿里开源的嵌入式向量数据库,向量搜索界的 SQLite
数据库·人工智能·llm
冬奇Lab15 小时前
Agent 系列(22):Context Engineering 深度——三种上下文管理策略的量化对比
人工智能·agent
hboot15 小时前
AI工程师第二课 - 数据处理
人工智能·python·数据分析
程序员cxuan16 小时前
DeepSeek 杀入多模态,识图功能正式上线!
人工智能·后端·程序员
米小虾17 小时前
告别单打独斗:2026年多Agent协作架构实战指南
人工智能·agent
IT_陈寒18 小时前
SpringBoot这个自动配置坑我跳了三次
前端·人工智能·后端
Larcher19 小时前
AI Loop:让AI像人一样自主完成任务的核心机制
javascript·人工智能·设计模式
牧艺19 小时前
从零到协同:构建类飞书在线文档系统的五个技术重难点
前端·人工智能