词嵌入与语言模型详解

目录

  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

开源库


相关推荐
摩尔线程8 小时前
喜报|摩尔线程MTT S5000(PH100芯片)通过国家《安全可靠测评》
人工智能·摩尔线程
Ricky05538 小时前
RF-DETR:实时检测变换器(transformers)的神经架构搜索(美国2025.12研究)
图像处理·人工智能·算法
HZZSDSCYZ8 小时前
2026年杭州电商新趋势:专业公司如何引领未来市场
大数据·人工智能·python
心疼你的一切8 小时前
Llama.Cpp 本地大模型极速部署与调用指南
人工智能·ai·aigc·llama
程序员cxuan8 小时前
还在用 xigh 拉满跑?大错特错
人工智能·后端·程序员
城管不管8 小时前
什么是Prompt?
android·java·数据库·语言模型·llm·prompt
完成大叔8 小时前
模块二,Agent的推理模式是什么
人工智能
猫先生Mr.Mao8 小时前
一文梳理主流 LLM 架构技术演进
人工智能·架构·大模型·llm·transformer
土星云SaturnCloud8 小时前
基于 YOLOv8 + ByteTrack 的冷库叉车智能出入库监管系统设计
人工智能·ai·边缘计算