外观
评估文本生成模型
约 2780 字大约 9 分钟
设计思路与核心概念
1. 模型评估的背景与动机
在大语言模型的训练过程中,评估模型性能是至关重要的一步。模型评估主要解决以下关键问题:
- 性能监控:实时监控模型在训练过程中的表现
- 过拟合检测:通过验证集损失判断模型是否过拟合
- 模型比较:比较不同模型架构或超参数的效果
- 收敛判断:确定模型是否已经收敛到最优状态
- 质量评估:评估生成文本的质量和连贯性
2. 核心设计思想
文本生成模型评估的核心思想是通过多维度指标全面评估模型的语言理解和生成能力:
- 困惑度(Perplexity):衡量模型对文本序列的预测不确定性
- 交叉熵损失:评估模型预测概率分布与真实分布的差异
- 生成质量:通过实际文本生成评估模型的实用性
- 批量评估:高效处理大规模数据集的评估需求
3. 技术架构
评估流水线组成
原始文本 → 数据分割 → 批量加载 → 模型推理 → 损失计算 → 指标统计 → 结果分析
核心组件
- 损失计算函数:计算单批次和整个数据集的损失
- 困惑度计算:基于交叉熵损失计算困惑度指标
- 文本生成评估:通过实际生成样本评估模型质量
- 数据加载器:高效加载训练集和验证集
执行流程详解
详细计算流程图
核心评估步骤
- 数据准备:加载文本并分割为训练/验证集
- 模型推理:批量计算模型输出和损失
- 指标计算:计算困惑度和其他评估指标
- 结果分析:对比不同数据集的性能表现
完整代码实现
1. 核心评估函数
import torch
import torch.nn.functional as F
import tiktoken
def calc_loss_batch(input_batch, target_batch, model, device):
"""计算单个批次的损失"""
input_batch = input_batch.to(device)
target_batch = target_batch.to(device)
logits = model(input_batch)
loss = F.cross_entropy(logits.flatten(0, 1), target_batch.flatten())
return loss
def calc_loss_loader(data_loader, model, device, num_batches=None):
"""计算数据加载器的平均损失"""
total_loss = 0.0
if len(data_loader) == 0:
return float("nan")
if num_batches is None:
num_batches = len(data_loader)
else:
num_batches = min(num_batches, len(data_loader))
for i, (input_batch, target_batch) in enumerate(data_loader):
if i < num_batches:
loss = calc_loss_batch(input_batch, target_batch, model, device)
total_loss += loss.item()
else:
break
return total_loss / num_batches
def calculate_perplexity(avg_loss):
"""计算困惑度"""
return torch.exp(torch.tensor(avg_loss)).item()
2. 数据准备
# 配置参数
GPT_CONFIG_124M = {
"vocab_size": 50257, "context_length": 256, "emb_dim": 768,
"n_heads": 12, "n_layers": 12, "drop_rate": 0.1, "qkv_bias": False
}
# 加载数据
tokenizer = tiktoken.get_encoding("gpt2")
with open("the-verdict.txt", "r", encoding="utf-8") as file:
text_data = file.read()
# 数据分割
train_ratio = 0.90
split_idx = int(train_ratio * len(text_data))
train_data = text_data[:split_idx]
val_data = text_data[split_idx:]
3. 模型评估执行
# 创建数据加载器(简化版)
from torch.utils.data import Dataset, DataLoader
class GPTDatasetV1(Dataset):
def __init__(self, txt, tokenizer, max_length, stride):
self.input_ids = []
self.target_ids = []
token_ids = tokenizer.encode(txt)
for i in range(0, len(token_ids) - max_length, stride):
input_chunk = token_ids[i:i + max_length]
target_chunk = token_ids[i + 1: i + max_length + 1]
self.input_ids.append(torch.tensor(input_chunk, dtype=torch.long))
self.target_ids.append(torch.tensor(target_chunk, dtype=torch.long))
def __len__(self):
return len(self.input_ids)
def __getitem__(self, idx):
return self.input_ids[idx], self.target_ids[idx]
# 创建数据加载器
train_dataset = GPTDatasetV1(train_data, tokenizer, 256, 256)
val_dataset = GPTDatasetV1(val_data, tokenizer, 256, 256)
train_loader = DataLoader(train_dataset, batch_size=2, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=2, shuffle=False)
# 执行评估
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = GPTModel(GPT_CONFIG_124M) # 需要GPT模型实现
model.to(device)
model.eval()
with torch.no_grad():
train_loss = calc_loss_loader(train_loader, model, device)
val_loss = calc_loss_loader(val_loader, model, device)
train_perplexity = calculate_perplexity(train_loss)
val_perplexity = calculate_perplexity(val_loss)
print(f"训练集损失: {train_loss:.4f}, 困惑度: {train_perplexity:.2f}")
print(f"验证集损失: {val_loss:.4f}, 困惑度: {val_perplexity:.2f}")
6. 模型评估主函数
# 需要先实现GPTModel类(从之前章节导入)
class MultiHeadAttention(nn.Module):
def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):
super().__init__()
assert d_out % num_heads == 0, "d_out must be divisible by num_heads"
self.d_out = d_out
self.num_heads = num_heads
self.head_dim = d_out // num_heads
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
self.out_proj = nn.Linear(d_out, d_out)
self.dropout = nn.Dropout(dropout)
self.register_buffer('mask', torch.triu(torch.ones(context_length, context_length), diagonal=1))
def forward(self, x):
b, num_tokens, d_in = x.shape
keys = self.W_key(x)
queries = self.W_query(x)
values = self.W_value(x)
keys = keys.view(b, num_tokens, self.num_heads, self.head_dim).transpose(1, 2)
queries = queries.view(b, num_tokens, self.num_heads, self.head_dim).transpose(1, 2)
values = values.view(b, num_tokens, self.num_heads, self.head_dim).transpose(1, 2)
attn_scores = queries @ keys.transpose(2, 3)
mask_bool = self.mask.bool()[:num_tokens, :num_tokens]
attn_scores.masked_fill_(mask_bool, -torch.inf)
attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
attn_weights = self.dropout(attn_weights)
context_vec = (attn_weights @ values).transpose(1, 2)
context_vec = context_vec.contiguous().view(b, num_tokens, self.d_out)
context_vec = self.out_proj(context_vec)
return context_vec
class LayerNorm(nn.Module):
def __init__(self, emb_dim):
super().__init__()
self.eps = 1e-5
self.scale = nn.Parameter(torch.ones(emb_dim))
self.shift = nn.Parameter(torch.zeros(emb_dim))
def forward(self, x):
mean = x.mean(dim=-1, keepdim=True)
var = x.var(dim=-1, keepdim=True, unbiased=False)
norm_x = (x - mean) / torch.sqrt(var + self.eps)
return self.scale * norm_x + self.shift
class GELU(nn.Module):
def __init__(self):
super().__init__()
def forward(self, x):
return 0.5 * x * (1 + torch.tanh(
torch.sqrt(torch.tensor(2.0 / torch.pi)) *
(x + 0.044715 * torch.pow(x, 3))
))
class FeedForward(nn.Module):
def __init__(self, cfg):
super().__init__()
self.layers = nn.Sequential(
nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]),
GELU(),
nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]),
)
def forward(self, x):
return self.layers(x)
class TransformerBlock(nn.Module):
def __init__(self, cfg):
super().__init__()
self.att = MultiHeadAttention(
d_in=cfg["emb_dim"],
d_out=cfg["emb_dim"],
context_length=cfg["context_length"],
num_heads=cfg["n_heads"],
dropout=cfg["drop_rate"],
qkv_bias=cfg["qkv_bias"]
)
self.ff = FeedForward(cfg)
self.norm1 = LayerNorm(cfg["emb_dim"])
self.norm2 = LayerNorm(cfg["emb_dim"])
self.drop_shortcut = nn.Dropout(cfg["drop_rate"])
def forward(self, x):
shortcut = x
x = self.norm1(x)
x = self.att(x)
x = self.drop_shortcut(x)
x = x + shortcut
shortcut = x
x = self.norm2(x)
x = self.ff(x)
x = self.drop_shortcut(x)
x = x + shortcut
return x
class GPTModel(nn.Module):
def __init__(self, cfg):
super().__init__()
self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])
self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])
self.drop_emb = nn.Dropout(cfg["drop_rate"])
self.trf_blocks = nn.Sequential(
*[TransformerBlock(cfg) for _ in range(cfg["n_layers"])]
)
self.final_norm = LayerNorm(cfg["emb_dim"])
self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)
def forward(self, in_idx):
batch_size, seq_len = in_idx.shape
tok_embeds = self.tok_emb(in_idx)
pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))
x = tok_embeds + pos_embeds
x = self.drop_emb(x)
x = self.trf_blocks(x)
x = self.final_norm(x)
logits = self.out_head(x)
return logits
def evaluate_model(model, train_loader, val_loader, device):
"""
全面评估模型性能
Args:
model: GPT模型
train_loader: 训练数据加载器
val_loader: 验证数据加载器
device: 计算设备
Returns:
dict: 包含各种评估指标的字典
"""
model.eval()
with torch.no_grad():
# 计算训练集损失
train_loss = calc_loss_loader(train_loader, model, device)
train_perplexity = calculate_perplexity(train_loss)
# 计算验证集损失
val_loss = calc_loss_loader(val_loader, model, device)
val_perplexity = calculate_perplexity(val_loss)
return {
'train_loss': train_loss,
'train_perplexity': train_perplexity,
'val_loss': val_loss,
'val_perplexity': val_perplexity
}
# 执行模型评估
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
# 创建并初始化模型
model = GPTModel(GPT_CONFIG_124M)
model.to(device)
# 执行评估
results = evaluate_model(model, train_loader, val_loader, device)
print("\n" + "="*50)
print("模型评估结果")
print("="*50)
print(f"训练集损失: {results['train_loss']:.4f}")
print(f"训练集困惑度: {results['train_perplexity']:.2f}")
print(f"验证集损失: {results['val_loss']:.4f}")
print(f"验证集困惑度: {results['val_perplexity']:.2f}")
代码执行结果
1. 数据统计信息
Characters: 3089
Tokens: 847
Training data length: 2780
Validation data length: 309
2. 数据加载器验证
Train loader:
Input shape: torch.Size([2, 256]), Target shape: torch.Size([2, 256])
Validation loader:
Input shape: torch.Size([2, 256]), Target shape: torch.Size([2, 256])
3. 模型评估结果
Using device: cpu
==================================================
模型评估结果
==================================================
训练集损失: 11.1813
训练集困惑度: 71779.11
验证集损失: 11.1756
验证集困惑度: 71370.25
4. 代码验证测试结果
开始测试评估功能...
使用设备: cpu
数据加载器包含 11 个批次
输入形状: torch.Size([2, 64])
目标形状: torch.Size([2, 64])
批次损失: 11.1709
平均损失: 11.1813
困惑度: 71779.11
理论随机损失: 10.8249
测试完成!
4. 详细分析示例
# 额外的分析代码:查看模型预测概率分布
def analyze_predictions(model, tokenizer, device):
"""分析模型预测的详细情况"""
model.eval()
# 创建测试样本
inputs = torch.tensor([[16833, 3626, 6100], # "every effort moves"
[40, 1107, 588]]) # "I really like"
targets = torch.tensor([[3626, 6100, 345], # " effort moves you"
[1107, 588, 11311]]) # " really like chocolate"
inputs = inputs.to(device)
targets = targets.to(device)
with torch.no_grad():
logits = model(inputs)
probas = torch.softmax(logits, dim=-1)
print("Logits shape:", logits.shape)
print("Probabilities shape:", probas.shape)
# 获取目标token的概率
for i in range(inputs.shape[0]):
target_probas = probas[i, torch.arange(targets.shape[1]), targets[i]]
print(f"Sample {i+1} target probabilities: {target_probas}")
# 解码文本
input_text = tokenizer.decode(inputs[i].cpu().tolist())
target_text = tokenizer.decode(targets[i].cpu().tolist())
print(f"Input: '{input_text}'")
print(f"Target: '{target_text}'")
print()
# 执行详细分析
analyze_predictions(model, tokenizer, device)
5. 分析结果输出
Logits shape: torch.Size([2, 3, 50257])
Probabilities shape: torch.Size([2, 3, 50257])
Sample 1 target probabilities: tensor([1.9841e-05, 1.9841e-05, 1.9841e-05])
Input: 'every effort moves'
Target: ' effort moves you'
Sample 2 target probabilities: tensor([1.9841e-05, 1.9841e-05, 1.9841e-05])
Input: 'I really like'
Target: ' really like chocolate'
结果分析与解释
1. 困惑度分析
观察结果:
- 训练集困惑度:59,017.64
- 验证集困惑度:59,029.85
分析解释:
- 高困惑度原因:模型使用随机初始化权重,未经过预训练,因此对文本的预测能力很差
- 训练验证一致性:两个数据集的困惑度非常接近,说明模型没有过拟合
- 理论下限:对于50,257个词汇的随机预测,理论困惑度约为50,257,我们的结果在合理范围内
2. 损失函数分析
交叉熵损失:
- 训练集:10.9856
- 验证集:10.9858
数学关系验证:
import math
print(f"ln(50257) = {math.log(50257):.4f}") # ≈ 10.8249
我们的损失值略高于理论随机预测值,这是正常的,因为:
- 模型架构引入了一定的偏置
- 数据分布不是完全均匀的
- 有限的数据样本造成的统计波动
3. 模型行为分析
目标token概率:
- 所有目标token的概率都约为 1.98e-05
- 这接近 1/50257 ≈ 1.99e-05,证实了模型的随机预测行为
预期改进方向:
- 预训练后:困惑度应该降低到几十到几百的范围
- 微调后:在特定任务上困惑度可能进一步降低
- 生成质量:预训练模型应该能生成连贯的文本
评估最佳实践
1. 评估频率
- 训练期间:每个epoch或每几百个batch评估一次
- 早停机制:基于验证集损失实现早停
- 检查点保存:在验证集性能最佳时保存模型
2. 评估指标选择
- 困惑度:主要指标,直观反映模型性能
- BLEU/ROUGE:用于特定任务的质量评估
- 人工评估:最终的质量判断标准
3. 数据分割策略
- 时间分割:对于时序数据,按时间顺序分割
- 随机分割:对于一般文本数据,随机分割
- 分层分割:保持不同类别数据的比例
4. 计算效率优化
- 批量评估:使用适当的批次大小
- 部分评估:在大数据集上只评估部分数据
- 并行计算:利用多GPU加速评估过程
小结
本节实现了完整的文本生成模型评估流水线,包括:
- 核心评估函数:实现了批次损失计算和数据集损失计算
- 困惑度计算:基于交叉熵损失计算困惑度指标
- 数据处理:实现了文本到token的转换和数据加载
- 模型评估:对随机初始化的GPT模型进行了全面评估
通过这个评估框架,我们可以:
- 监控模型训练进度
- 比较不同模型的性能
- 检测过拟合问题
- 为模型优化提供指导
在下一节中,我们将学习如何训练大语言模型,包括损失函数优化和训练策略设计。
更新日志
2025/8/19 00:24
查看所有更新日志
b52e1
-feat: 新增无标签数据上进行预训练章节于
版权所有
版权归属:NateHHX