语言模型
语言模型(Language Model)是自然语言处理的核心技术之一,用于计算文本序列的概率或预测下一个词。从早期的统计方法到现代的大语言模型,语言模型的发展见证了人工智能领域的巨大进步。
什么是语言模型
语言模型的核心任务是估计文本序列的概率。给定一个词序列 ,语言模型计算该序列出现的概率:
根据概率论中的链式法则,可以将联合概率分解为条件概率的乘积:
这个公式告诉我们:一个句子出现的概率,等于每个词在给定前面所有词的条件下出现的概率的乘积。这正是语言模型预测下一个词的理论基础——给定历史上下文,预测下一个词的概率分布。
语言模型的核心应用
语言模型在自然语言处理中有着广泛的应用:
文本生成:根据前文预测下一个词,逐词生成文本。这是 GPT 系列模型的核心能力。
语音识别:在语音转文字时,语言模型可以帮助选择最合理的词序列。例如,"他是一名医生"比"他是一名一生"更合理。
机器翻译:在翻译过程中,语言模型确保生成的译文流畅自然。
拼写纠错:根据上下文判断哪个拼写更合理。例如,"我明天去学校"比"我明天去学笑"更可能是正确的。
输入法:智能输入法使用语言模型预测用户可能要输入的下一个词或短语。
N-gram 语言模型
N-gram 是最经典的统计语言模型,基于马尔可夫假设:当前词只依赖于前面 n-1 个词。这个假设大大简化了概率计算,虽然牺牲了一定的准确性,但在很长一段时间内都是主流方法。
原理与假设
对于 n-gram 模型,我们假设:
这个假设被称为"n-1 阶马尔可夫假设"。直观理解:当我们预测下一个词时,只需要看前面 n-1 个词就够了,更早的历史可以忽略。
常见的 n-gram 模型:
-
Unigram(1-gram):不考虑上下文,假设每个词独立出现,。这种方法过于简化,无法捕捉词之间的依赖关系。
-
Bigram(2-gram):只考虑前一个词,。可以捕捉相邻词之间的简单依赖。
-
Trigram(3-gram):考虑前两个词,。能捕捉更复杂的局部依赖。
n 越大,模型能捕捉的上下文信息越多,但同时面临更严重的数据稀疏问题——很多 n-gram 组合在训练语料中从未出现过。
实现
from collections import defaultdict
import numpy as np
class NGramLM:
"""N-gram 语言模型"""
def __init__(self, n=2):
"""
初始化 N-gram 模型
参数:
n: n-gram 的阶数,默认为 2(bigram)
"""
self.n = n
self.ngram_counts = defaultdict(int) # 存储 n-gram 的计数
self.context_counts = defaultdict(int) # 存储上下文的计数
self.vocab = set() # 词汇表
def train(self, corpus):
"""
训练 n-gram 模型
参数:
corpus: 语料库,每个元素是一个词列表
"""
for sentence in corpus:
# 添加句首和句尾标记
tokens = ['<s>'] * (self.n - 1) + sentence + ['</s>']
self.vocab.update(sentence)
# 统计 n-gram 和上下文
for i in range(self.n - 1, len(tokens)):
ngram = tuple(tokens[i-self.n+1:i+1]) # 当前 n-gram
context = tuple(tokens[i-self.n+1:i]) # 上下文(前 n-1 个词)
self.ngram_counts[ngram] += 1
self.context_counts[context] += 1
def probability(self, word, context):
"""
计算条件概率 P(word|context)
参数:
word: 当前词
context: 上下文(前 n-1 个词的元组)
返回:
条件概率
"""
ngram = context + (word,)
context_count = self.context_counts.get(context, 0)
if context_count == 0:
# 如果上下文从未出现,使用均匀分布
return 1 / len(self.vocab)
return self.ngram_counts.get(ngram, 0) / context_count
def predict_next(self, context, top_k=5):
"""
预测下一个词
参数:
context: 上下文词列表
top_k: 返回概率最高的 k 个候选词
返回:
(词, 概率) 列表
"""
# 只保留最后 n-1 个词作为上下文
context = tuple(context[-(self.n-1):])
probs = {}
# 计算每个词的条件概率
for word in self.vocab:
probs[word] = self.probability(word, context)
# 按概率降序排列
sorted_probs = sorted(probs.items(), key=lambda x: x[1], reverse=True)
return sorted_probs[:top_k]
def sentence_probability(self, sentence):
"""
计算句子的概率
参数:
sentence: 词列表
返回:
句子概率的对数(避免下溢)
"""
tokens = ['<s>'] * (self.n - 1) + sentence + ['</s>']
log_prob = 0.0
for i in range(self.n - 1, len(tokens)):
context = tuple(tokens[i-self.n+1:i])
word = tokens[i]
prob = self.probability(word, context)
if prob > 0:
log_prob += np.log(prob)
else:
# 处理零概率情况
log_prob += np.log(1e-10)
return log_prob
# 训练示例
corpus = [
['我', '喜欢', '学习', '自然语言处理'],
['我', '喜欢', '学习', '机器学习'],
['自然语言处理', '是', '人工智能', '的', '分支'],
['机器学习', '是', '人工智能', '的', '重要', '技术']
]
lm = NGramLM(n=2)
lm.train(corpus)
# 预测下一个词
context = ['我', '喜欢']
predictions = lm.predict_next(context)
print(f"给定 {' '.join(context)},预测下一个词:")
for word, prob in predictions:
print(f" {word}: {prob:.4f}")
平滑技术
N-gram 模型面临的核心问题是数据稀疏:训练语料中未出现的 n-gram 组合会被赋予零概率。这会导致"零概率问题"——即使一个完全合理的句子,只要包含一个未见过的 n-gram,整个句子的概率就变为零。
平滑技术通过从"已见事件"借出一些概率质量分配给"未见事件"来解决这个问题。
class NGramLMWithSmoothing(NGramLM):
"""带平滑的 N-gram 模型"""
def __init__(self, n=2, alpha=1.0):
"""
参数:
n: n-gram 的阶数
alpha: Laplace 平滑参数,也称为加 k 平滑的 k 值
"""
super().__init__(n)
self.alpha = alpha
def probability(self, word, context):
"""
使用 Laplace 平滑计算概率
Laplace 平滑公式:
P(w|c) = (count(c,w) + alpha) / (count(c) + alpha * |V|)
其中 |V| 是词汇表大小
"""
ngram = context + (word,)
context_count = self.context_counts.get(context, 0)
vocab_size = len(self.vocab)
ngram_count = self.ngram_counts.get(ngram, 0)
return (ngram_count + self.alpha) / (context_count + self.alpha * vocab_size)
常见的平滑方法对比:
| 方法 | 公式 | 优点 | 缺点 |
|---|---|---|---|
| Laplace 平滑 | 简单易实现 | 给未见事件分配过多概率 | |
| Add-k 平滑 | 可调节参数 | 需要调参 | |
| Good-Turing | 用低频计数估计未见事件 | 理论基础扎实 | 实现较复杂 |
| Kneser-Ney | 回退 + 折扣机制 | 效果最好 | 实现复杂 |
N-gram 的局限性
维度灾难:当 n 增大时,可能的 n-gram 组合呈指数增长。对于词汇表大小为 V、n-gram 阶数为 n 的情况,理论上有 种可能的组合。即使训练语料很大,绝大多数 n-gram 仍然不会出现。
长距离依赖:n-gram 只能捕捉局部依赖,无法建模长距离的词关系。例如在句子"那个穿红衣服的女孩昨天在公园里丢了钱包"中,"丢了"和"钱包"之间存在较远的距离,n-gram 无法有效捕捉这种关系。
泛化能力差:N-gram 无法利用词之间的语义相似性。"猫在睡觉"和"狗在睡觉"在语义上非常相似,但在 n-gram 模型中是完全独立的事件。
神经语言模型
神经语言模型使用神经网络来建模语言,能够克服 n-gram 的局限性,学习词之间的语义关系和长距离依赖。
前馈神经网络语言模型
2003 年,Bengio 等人提出了首个神经语言模型。其核心思想是:将每个词映射为一个低维稠密向量(词嵌入),然后使用神经网络预测下一个词。
import torch
import torch.nn as nn
class FNNLanguageModel(nn.Module):
"""
前馈神经网络语言模型
架构:
1. 词嵌入层:将词索引转换为词向量
2. 隐藏层:非线性变换
3. 输出层:预测下一个词的概率分布
"""
def __init__(self, vocab_size, embed_dim, hidden_dim, context_size):
"""
参数:
vocab_size: 词汇表大小
embed_dim: 词向量维度
hidden_dim: 隐藏层维度
context_size: 上下文窗口大小(使用前 context_size 个词预测)
"""
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.fc1 = nn.Linear(context_size * embed_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, vocab_size)
def forward(self, x):
"""
前向传播
参数:
x: 词索引张量,形状为 (batch_size, context_size)
返回:
logits,形状为 (batch_size, vocab_size)
"""
# 获取词嵌入并展平
embeds = self.embedding(x) # (batch_size, context_size, embed_dim)
embeds = embeds.view(embeds.size(0), -1) # (batch_size, context_size * embed_dim)
# 通过隐藏层
hidden = torch.relu(self.fc1(embeds))
# 输出层
output = self.fc2(hidden)
return output
# 使用示例
vocab_size = 10000
embed_dim = 128
hidden_dim = 256
context_size = 3 # 使用前 3 个词预测下一个词
model = FNNLanguageModel(vocab_size, embed_dim, hidden_dim, context_size)
print(f"模型参数量: {sum(p.numel() for p in model.parameters()):,}")
前馈神经网络语言模型的局限性在于只能使用固定长度的上下文,无法处理变长序列。
循环神经网络语言模型
循环神经网络(RNN)能够处理变长序列,通过隐藏状态传递历史信息,从而捕捉长距离依赖。
class RNNLanguageModel(nn.Module):
"""
RNN 语言模型
RNN 的核心思想:维护一个隐藏状态,在每个时间步更新这个状态,
使其包含之前所有时间步的信息。
"""
def __init__(self, vocab_size, embed_dim, hidden_dim, num_layers=1):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.rnn = nn.RNN(embed_dim, hidden_dim, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_dim, vocab_size)
self.hidden_dim = hidden_dim
def forward(self, x, hidden=None):
"""
前向传播
参数:
x: 输入序列,形状为 (batch_size, seq_len)
hidden: 初始隐藏状态
返回:
output: 输出 logits,形状为 (batch_size, seq_len, vocab_size)
hidden: 最终隐藏状态
"""
embeds = self.embedding(x)
output, hidden = self.rnn(embeds, hidden)
output = self.fc(output)
return output, hidden
class LSTMLanguageModel(nn.Module):
"""
LSTM 语言模型
LSTM 通过门控机制解决了普通 RNN 的梯度消失问题,
能够更好地捕捉长距离依赖。
LSTM 的三个门:
- 遗忘门:决定从细胞状态中丢弃什么信息
- 输入门:决定什么新信息将被存储
- 输出门:决定输出什么值
"""
def __init__(self, vocab_size, embed_dim, hidden_dim, num_layers=2, dropout=0.3):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.lstm = nn.LSTM(
embed_dim, hidden_dim, num_layers,
batch_first=True, dropout=dropout if num_layers > 1 else 0
)
self.fc = nn.Linear(hidden_dim, vocab_size)
def forward(self, x, hidden=None):
embeds = self.embedding(x)
output, hidden = self.lstm(embeds, hidden)
output = self.fc(output)
return output, hidden
# 使用示例
vocab_size = 10000
model = LSTMLanguageModel(vocab_size, embed_dim=256, hidden_dim=512, num_layers=2)
print(f"模型参数量: {sum(p.numel() for p in model.parameters()):,}")
训练神经语言模型
训练神经语言模型的核心是最小化负对数似然。对于每个位置,模型需要正确预测下一个词。
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
class TextDataset(Dataset):
"""文本数据集"""
def __init__(self, texts, vocab, seq_length=20):
"""
参数:
texts: 文本列表,每个元素是词列表
vocab: 词汇表字典,{词: 索引}
seq_length: 序列长度
"""
self.vocab = vocab
self.seq_length = seq_length
self.data = []
for text in texts:
# 将词转换为索引
tokens = [vocab.get(w, vocab['<UNK>']) for w in text]
# 滑动窗口生成训练样本
for i in range(0, len(tokens) - seq_length):
self.data.append((
tokens[i:i+seq_length], # 输入
tokens[i+1:i+seq_length+1] # 目标(下一个词)
))
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
x, y = self.data[idx]
return torch.tensor(x), torch.tensor(y)
def train_model(model, dataloader, epochs=10, lr=0.001):
"""训练模型"""
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=lr)
model.train()
for epoch in range(epochs):
total_loss = 0
for batch_x, batch_y in dataloader:
optimizer.zero_grad()
output, _ = model(batch_x)
# 重塑张量以计算损失
output = output.view(-1, output.size(-1))
batch_y = batch_y.view(-1)
loss = criterion(output, batch_y)
loss.backward()
# 梯度裁剪,防止梯度爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)
optimizer.step()
total_loss += loss.item()
avg_loss = total_loss / len(dataloader)
print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}")
def generate_text(model, vocab, start_tokens, max_length=50, temperature=1.0):
"""
生成文本
参数:
model: 训练好的语言模型
vocab: 词汇表
start_tokens: 起始词列表
max_length: 最大生成长度
temperature: 温度参数,控制生成的随机性
- temperature > 1: 更随机,更多样化
- temperature < 1: 更确定,更保守
- temperature = 1: 标准采样
"""
model.eval()
idx_to_word = {v: k for k, v in vocab.items()}
tokens = [vocab.get(w, vocab['<UNK>']) for w in start_tokens]
hidden = None
with torch.no_grad():
for _ in range(max_length):
x = torch.tensor([tokens[-20:]]) # 使用最后 20 个词作为输入
output, hidden = model(x, hidden)
# 获取最后一个位置的 logits
logits = output[0, -1] / temperature
probs = torch.softmax(logits, dim=-1)
# 从概率分布中采样
next_token = torch.multinomial(probs, 1).item()
tokens.append(next_token)
# 遇到结束符则停止
if idx_to_word.get(next_token) == '<EOS>':
break
return [idx_to_word.get(t, '<UNK>') for t in tokens]
Transformer 语言模型
Transformer 架构彻底改变了语言模型的发展轨迹。2017 年,Google 在论文《Attention Is All You Need》中提出的 Transformer,使用自注意力机制替代循环结构,能够并行计算并有效捕捉长距离依赖。
Transformer 语言模型架构
Transformer 语言模型(如 GPT)使用 Transformer 解码器,其核心组件包括:
- 掩码自注意力:每个位置只能关注之前的位置,防止信息泄露
- 前馈神经网络:对每个位置独立应用
- 层归一化和残差连接:稳定训练过程
import torch
import torch.nn as nn
import math
class MultiHeadAttention(nn.Module):
"""
多头自注意力机制
核心思想:将输入投影到多个子空间,在每个子空间独立计算注意力,
然后合并结果。这让模型能够同时关注不同位置的不同表示子空间。
"""
def __init__(self, embed_dim, num_heads):
super().__init__()
self.embed_dim = embed_dim
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads
assert embed_dim % num_heads == 0, "embed_dim 必须能被 num_heads 整除"
self.q_proj = nn.Linear(embed_dim, embed_dim)
self.k_proj = nn.Linear(embed_dim, embed_dim)
self.v_proj = nn.Linear(embed_dim, embed_dim)
self.out_proj = nn.Linear(embed_dim, embed_dim)
def forward(self, x, mask=None):
"""
前向传播
参数:
x: 输入张量,形状为 (batch_size, seq_len, embed_dim)
mask: 注意力掩码,用于语言模型时防止看到未来信息
返回:
output: 注意力输出
attention_weights: 注意力权重(可选)
"""
batch_size, seq_len, _ = x.shape
# 线性投影
q = self.q_proj(x)
k = self.k_proj(x)
v = self.v_proj(x)
# 分割成多个头
q = q.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
k = k.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
v = v.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
# 计算注意力分数
scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim)
# 应用因果掩码(关键:语言模型只能看到之前的位置)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
# Softmax 归一化
attention_weights = torch.softmax(scores, dim=-1)
# 加权求和
output = torch.matmul(attention_weights, v)
# 合并多头
output = output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.embed_dim)
return self.out_proj(output)
class TransformerBlock(nn.Module):
"""Transformer 解码器块"""
def __init__(self, embed_dim, num_heads, ff_dim, dropout=0.1):
super().__init__()
self.attention = MultiHeadAttention(embed_dim, num_heads)
self.norm1 = nn.LayerNorm(embed_dim)
self.norm2 = nn.LayerNorm(embed_dim)
self.ff = nn.Sequential(
nn.Linear(embed_dim, ff_dim),
nn.GELU(),
nn.Linear(ff_dim, embed_dim)
)
self.dropout = nn.Dropout(dropout)
def forward(self, x, mask=None):
# 自注意力 + 残差连接 + 层归一化
attn_out = self.attention(x, mask)
x = self.norm1(x + self.dropout(attn_out))
# 前馈网络 + 残差连接 + 层归一化
ff_out = self.ff(x)
x = self.norm2(x + self.dropout(ff_out))
return x
class TransformerLM(nn.Module):
"""
Transformer 语言模型
与 BERT 不同,GPT 风格的语言模型使用单向(从左到右)的自注意力,
每个位置只能看到之前的内容,适合自回归生成。
"""
def __init__(self, vocab_size, embed_dim, num_heads, num_layers, ff_dim, max_seq_len=512):
super().__init__()
self.token_embedding = nn.Embedding(vocab_size, embed_dim)
self.position_embedding = nn.Embedding(max_seq_len, embed_dim)
self.layers = nn.ModuleList([
TransformerBlock(embed_dim, num_heads, ff_dim)
for _ in range(num_layers)
])
self.fc_out = nn.Linear(embed_dim, vocab_size)
self.ln_final = nn.LayerNorm(embed_dim)
# 预计算因果掩码
self.register_buffer('causal_mask',
torch.tril(torch.ones(max_seq_len, max_seq_len)).unsqueeze(0).unsqueeze(0))
def forward(self, x):
"""
前向传播
参数:
x: 输入词索引,形状为 (batch_size, seq_len)
返回:
logits: 形状为 (batch_size, seq_len, vocab_size)
"""
seq_len = x.size(1)
# 词嵌入 + 位置嵌入
positions = torch.arange(seq_len, device=x.device).unsqueeze(0)
x = self.token_embedding(x) + self.position_embedding(positions)
# 因果掩码:只看当前位置之前的词
mask = self.causal_mask[:, :, :seq_len, :seq_len]
# 通过 Transformer 层
for layer in self.layers:
x = layer(x, mask)
# 最终层归一化
x = self.ln_final(x)
# 输出层(与词嵌入权重共享)
return self.fc_out(x)
# 创建模型
vocab_size = 10000
model = TransformerLM(
vocab_size=vocab_size,
embed_dim=512,
num_heads=8,
num_layers=6,
ff_dim=2048
)
print(f"模型参数量: {sum(p.numel() for p in model.parameters()):,}")
位置编码
Transformer 没有循环结构,无法自然感知位置信息。位置编码通过添加位置相关的向量来解决这个问题。原始 Transformer 使用正弦和余弦函数生成固定位置编码:
这种编码方式有一个重要特性:对于任意固定偏移量 , 可以表示为 的线性函数,这使得模型能够学习到相对位置信息。
class PositionalEncoding(nn.Module):
"""正弦位置编码"""
def __init__(self, d_model, max_len=5000, dropout=0.1):
super().__init__()
self.dropout = nn.Dropout(dropout)
# 预计算位置编码
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)
)
pe[:, 0::2] = torch.sin(position * div_term) # 偶数维度
pe[:, 1::2] = torch.cos(position * div_term) # 奇数维度
pe = pe.unsqueeze(0)
self.register_buffer('pe', pe)
def forward(self, x):
x = x + self.pe[:, :x.size(1), :]
return self.dropout(x)
使用 Hugging Face 语言模型
Hugging Face Transformers 库提供了大量预训练语言模型,可以快速应用于各种任务。
GPT-2 文本生成
GPT-2 是 OpenAI 发布的自回归语言模型,使用 Transformer 解码器架构。它通过预测下一个词进行预训练,因此在文本生成任务上表现出色。
from transformers import GPT2LMHeadModel, GPT2Tokenizer
# 加载模型和分词器
model_name = "gpt2" # 可选: "gpt2-medium", "gpt2-large", "gpt2-xl"
tokenizer = GPT2Tokenizer.from_pretrained(model_name)
model = GPT2LMHeadModel.from_pretrained(model_name)
def generate_text(prompt, max_length=100, temperature=0.7, top_p=0.9):
"""
使用 GPT-2 生成文本
参数:
prompt: 提示文本
max_length: 最大生成长度
temperature: 温度参数,控制随机性
top_p: 核采样阈值
返回:
生成的文本
"""
inputs = tokenizer.encode(prompt, return_tensors="pt")
outputs = model.generate(
inputs,
max_length=max_length,
num_return_sequences=1,
temperature=temperature,
top_k=50,
top_p=top_p,
do_sample=True,
pad_token_id=tokenizer.eos_token_id
)
return tokenizer.decode(outputs[0], skip_special_tokens=True)
# 生成文本
prompt = "Natural language processing is"
generated = generate_text(prompt)
print(generated)
BERT 掩码语言模型
BERT 使用掩码语言模型(Masked Language Model, MLM)进行预训练。在预训练时,随机将输入中 15% 的词替换为 [MASK] 标记,然后让模型预测这些被遮盖的词。
from transformers import BertForMaskedLM, BertTokenizer
import torch
# 加载中文 BERT 模型
model_name = "bert-base-chinese"
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertForMaskedLM.from_pretrained(model_name)
def fill_mask(text, top_k=5):
"""
使用 BERT 进行填空任务
参数:
text: 包含 [MASK] 标记的文本
top_k: 返回前 k 个预测结果
返回:
预测的词及其概率列表
"""
inputs = tokenizer(text, return_tensors="pt")
# 找到 [MASK] 标记的位置
mask_token_index = torch.where(inputs["input_ids"] == tokenizer.mask_token_id)[1]
with torch.no_grad():
outputs = model(**inputs)
# 获取 [MASK] 位置的预测 logits
mask_token_logits = outputs.logits[0, mask_token_index, :]
# 获取 top-k 预测
top_k_tokens = torch.topk(mask_token_logits, top_k, dim=1).indices[0].tolist()
print(f"原文: {text}")
print(f"\n预测结果 (前 {top_k} 个):")
for i, token in enumerate(top_k_tokens, 1):
predicted_word = tokenizer.decode([token])
score = torch.softmax(mask_token_logits, dim=1)[0, token].item()
predicted_text = text.replace(tokenizer.mask_token, predicted_word)
print(f" {i}. {predicted_word} (概率: {score:.4f})")
print(f" 完整句子: {predicted_text}")
return top_k_tokens
# 使用示例
print("=" * 60)
print("BERT 填空任务示例")
print("=" * 60)
# 示例 1
fill_mask("自然语言处理是人工[MASK]能的重要分支。")
print("\n" + "=" * 60)
# 示例 2
fill_mask("北京是中国的[MASK]都。")
print("\n" + "=" * 60)
# 示例 3
fill_mask("他每天早上都会喝一杯[MASK]奶。")
BERT 的填空能力源于其预训练任务。在预训练时,模型随机遮盖 15% 的输入词,其中:
- 80% 替换为
[MASK] - 10% 替换为随机词
- 10% 保持不变
这种设计让模型学习理解上下文语义,从而做出合理的预测。值得注意的是,BERT 是双向模型,预测时会同时考虑左右两边的上下文,这与 GPT 的单向自回归方式不同。
文本生成策略
使用语言模型生成文本时,有多种解码策略可供选择:
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "gpt2"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
prompt = "The future of artificial intelligence is"
inputs = tokenizer(prompt, return_tensors="pt")
# 1. 贪心解码:每步选择概率最高的词
print("贪心解码:")
outputs = model.generate(**inputs, max_length=50, do_sample=False)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
# 2. 束搜索:保留多个候选序列
print("\n束搜索 (beam_size=5):")
outputs = model.generate(**inputs, max_length=50, num_beams=5, early_stopping=True)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
# 3. 核采样 (Nucleus Sampling):从累积概率达到 top_p 的词中采样
print("\n核采样 (top_p=0.9):")
outputs = model.generate(**inputs, max_length=50, do_sample=True, top_p=0.9)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
# 4. Top-K 采样:从概率最高的 K 个词中采样
print("\nTop-K 采样 (top_k=50):")
outputs = model.generate(**inputs, max_length=50, do_sample=True, top_k=50)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
不同策略的对比:
| 策略 | 特点 | 适用场景 |
|---|---|---|
| 贪心解码 | 每步选最优,可能陷入局部最优 | 简单任务 |
| 束搜索 | 保留多个候选,更可能找到全局最优 | 翻译、摘要 |
| 核采样 | 平衡质量与多样性 | 创意写作 |
| Top-K | 限制候选数量,控制随机性 | 通用生成 |
语言模型评估
困惑度
困惑度(Perplexity)是评估语言模型最常用的指标。它衡量模型对测试数据的"惊讶程度"——困惑度越低,说明模型对测试数据的预测越准确。
困惑度的数学定义:
直观理解:困惑度可以看作模型在每个位置预测时的"平均分支因子"。如果困惑度为 100,意味着模型在每个位置平均有 100 个"等概率"的选择。
import math
def calculate_perplexity(model, dataloader, criterion, device='cpu'):
"""
计算困惑度
参数:
model: 语言模型
dataloader: 测试数据加载器
criterion: 损失函数
device: 计算设备
返回:
困惑度值
"""
model.eval()
total_loss = 0
total_tokens = 0
with torch.no_grad():
for batch_x, batch_y in dataloader:
batch_x = batch_x.to(device)
batch_y = batch_y.to(device)
output, _ = model(batch_x)
output = output.view(-1, output.size(-1))
batch_y = batch_y.view(-1)
# 忽略填充位置的损失
loss = criterion(output, batch_y)
total_loss += loss.item() * batch_y.size(0)
total_tokens += batch_y.size(0)
avg_loss = total_loss / total_tokens
perplexity = math.exp(avg_loss)
return perplexity
# 困惑度的解释
def interpret_perplexity(ppl):
"""解释困惑度数值"""
if ppl < 30:
return "优秀 - 模型对语言有很好的理解"
elif ppl < 50:
return "良好 - 模型表现不错"
elif ppl < 100:
return "一般 - 模型有一定理解能力"
else:
return "较差 - 模型需要改进"
困惑度的局限性:
- 只衡量预测准确性,不衡量生成质量
- 对不同领域的文本,困惑度难以直接比较
- 低困惑度不一定意味着生成文本更符合人类偏好
其他评估方法
随着大语言模型的发展,传统的困惑度评估已不足以衡量模型能力。现代评估方法包括:
基准测试集:如 GLUE、SuperGLUE、MMLU 等,包含多种任务,全面评估模型能力。
人工评估:让人类评估者对模型输出进行打分,包括流畅性、相关性、准确性等维度。
模型评估:使用强大的模型(如 GPT-4)评估其他模型的输出质量。
语言模型的发展历程
| 年份 | 模型 | 特点 | 意义 |
|---|---|---|---|
| 2003 | NNLM | 首个神经语言模型 | 开创性工作 |
| 2013 | Word2Vec | 高效词向量训练 | 词嵌入时代开始 |
| 2017 | Transformer | 自注意力机制 | 架构革命 |
| 2018 | BERT | 双向预训练 | 理解任务突破 |
| 2018 | GPT | 自回归生成 | 生成能力展示 |
| 2020 | GPT-3 | 1750 亿参数 | 涌现能力显现 |
| 2022 | ChatGPT | RLHF 对齐 | 对话能力突破 |
| 2023 | GPT-4 | 多模态能力 | 通用智能迈进 |
总结
语言模型是 NLP 的核心技术,本章系统介绍了:
- 语言模型基础:概率模型的基本原理和核心应用场景
- N-gram 模型:基于统计的经典方法,简单直观但能力有限
- 神经语言模型:使用 RNN/LSTM 建模序列,能够捕捉长距离依赖
- Transformer 语言模型:现代大语言模型的基础架构,使用自注意力实现并行计算
- 预训练语言模型:BERT、GPT 等模型的原理和使用方法
- 评估方法:困惑度等指标的计算和解释
语言模型的发展推动了整个 NLP 领域的进步。从简单的统计方法到如今的大语言模型,语言模型的能力不断提升,应用场景也越来越广泛。理解语言模型的原理,对于深入学习自然语言处理和大语言模型技术至关重要。
下一章将介绍注意力机制与 Transformer 架构的详细内容。