趋近智
大型语言模型训练部署后,其工作并未画上句号。信息环境不断变化,新数据持续生成,模型在实际运行中遇到的数据分布可能与其初始训练集产生偏差。简单地冻结模型会导致其随着时间推移变得陈旧,性能下降。将旧数据与新数据结合进行从头完全重新训练,通常在计算和时间上成本过高。持续预训练 (pre-training)提供了一个折衷方案:用新数据更新现有模型,同时尽量保持之前学到的知识。
持续预训练中的主要障碍是灾难性遗忘。当模型在不同数据分布上顺序训练时,随着其适应新数据分布,模型会迅速丢失对早期数据分布的性能,此现象即为灾难性遗忘。模型的参数 (parameter)会显著调整以最小化新数据上的损失,从而有效地覆盖了对旧数据性能重要的表示。有效地权衡学习新信息(可塑性)与保持旧知识(稳定性)是持续预训练策略要解决的核心难题。
最直接的方法是直接使用新数据集继续预训练 (pre-training)过程,从先前训练模型的权重 (weight)开始。这有时被称为在新数据上进行微调 (fine-tuning)。
# 示例:朴素持续学习设置
import torch
from torch.utils.data import DataLoader
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
AdamW,
get_linear_schedule_with_warmup,
)
# 假设 'model' 是从检查点加载的预训练LLM
# 假设 'new_dataset' 是 PyTorch 的新数据 Dataset 对象
model_path = "path/to/your/pretrained/llm"
model = AutoModelForCausalLM.from_pretrained(model_path)
tokenizer = AutoTokenizer.from_pretrained(model_path) # 确保分词器一致性
new_dataloader = DataLoader(new_dataset, batch_size=4, shuffle=True)
# 使用比初始预训练更小的学习率
optimizer = AdamW(model.parameters(), lr=1e-5) # 示例学习率
num_training_steps = len(new_dataloader) * num_epochs # 定义训练轮次(num_epochs)
scheduler = get_linear_schedule_with_warmup(
optimizer,
num_warmup_steps=0,
num_training_steps=num_training_steps
)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
model.train()
for epoch in range(num_epochs):
for batch in new_dataloader:
optimizer.zero_grad()
# 假设 batch 包含 input_ids, attention_mask, labels
inputs = {k: v.to(device) for k, v in batch.items()}
outputs = model(**inputs)
loss = outputs.loss
loss.backward()
optimizer.step()
scheduler.step()
print(f"Epoch {epoch}, Loss: {loss.item()}")
# 保存更新后的模型
model.save_pretrained("path/to/updated/llm")
尽管简单,但此方法极易受到灾难性遗忘的影响,特别是当新数据分布与旧数据分布差异很大,或者新数据量很大时。模型的参数 (parameter)会显著偏向最小化新数据上的损失,可能抹去早期学到的能力。这种方法可能只适用于新数据与原始训练数据非常相似或仅是小幅更新的情况。
重放策略通过在持续学习阶段将来自先前训练阶段的数据与新数据混合,明确地对抗遗忘。通过让模型重新接触旧样本,这些方法促使其保持在原始数据分布上的性能。
核心思想是创建包含新数据样本()和旧数据集样本()的训练批次。
# 示例:创建用于重放的混合 DataLoader
from torch.utils.data import (DataLoader, Dataset, ConcatDataset,
WeightedRandomSampler)
# 假设 'old_dataset' 和 'new_dataset' 是 PyTorch 的 Dataset 对象
# 如果原始数据集过大,old_dataset 可以是其一个代表性子集
# 选项1:简单拼接
# (如果数据集大小相同,则采样概率相等)
# combined_dataset = ConcatDataset([old_dataset, new_dataset])
# combined_dataloader = DataLoader(combined_dataset, batch_size=8, shuffle=True)
# 选项2:加权采样(控制混合比例)
# 示例:目标是每个批次中新数据占75%,旧数据占25%
new_data_weight = 0.75
old_data_weight = 0.25
weights = [new_data_weight / len(new_dataset)] * len(new_dataset) + \
[old_data_weight / len(old_dataset)] * len(old_dataset)
combined_dataset_for_sampler = ConcatDataset([new_dataset, old_dataset])
sampler = WeightedRandomSampler(
weights,
num_samples=len(combined_dataset_for_sampler),
replacement=True
)
# 注意:使用采样器时,shuffle 应设为 False
combined_dataloader = DataLoader(
combined_dataset_for_sampler,
batch_size=8,
sampler=sampler
)
# --- 训练循环将使用 combined_dataloader ---
# for batch in combined_dataloader:
# # ... 训练步骤的其余部分 ...
重放方法的考量包括:
正则化方法不依赖显式数据重放,而是修改损失函数 (loss function),惩罚对先前任务或数据分布而言被认为重要的模型参数 (parameter)的改变。
EWC 使用 Fisher 信息矩阵(FIM),即 ,来评估每个参数对于旧数据分布的重要性。FIM 中对角线值高的参数被认为更重要。在新数据()训练期间,EWC 会在标准损失()中添加一个二次惩罚项,该项会抑制这些重要参数()相对于它们在旧数据上训练后的值()的变化。
EWC 损失函数为:
这里, 控制正则化强度。更高的 值会优先保持旧知识。
计算精确的 FIM 对于 LLM 来说计算成本很高。实际实现中,通常使用 FIM 的对角线近似值,它基于旧数据分布的梯度计算。即便如此,为数十亿参数计算和存储这些对角线元素也需要细致的实现。
LwF 在新数据()上训练时,使用知识蒸馏 (knowledge distillation)来保持旧模型()的行为。其思想是促使新模型()在处理新数据时,产生与旧模型相似的输出(例如,logits 或词汇表 (vocabulary)上的概率分布)。
总损失结合了新数据真实标签()上的标准交叉熵损失()和蒸馏损失(),后者衡量 和 在 上的输出差异。
蒸馏损失通常使用两个模型软化概率分布(使用温度 )之间的 KL 散度来实施。
# 示例:LwF 损失计算
import torch.nn.functional as F
# 假设 'model' 是当前正在训练的模型 (M_new)
# 假设 'old_model' 是前一阶段的冻结模型 (M_old)
# 假设 'batch' 包含新数据的输入和 'labels'
inputs = {k: v.to(device) for k, v in batch.items() if k != 'labels'}
labels = batch['labels'].to(device)
# 新数据上的标准交叉熵损失
outputs_new = model(**inputs)
logits_new = outputs_new.logits
loss_ce = F.cross_entropy(
logits_new.view(-1, logits_new.size(-1)),
labels.view(-1)
)
# 蒸馏损失
temperature = 2.0 # 示例温度
lambda_distill = 0.5 # 示例权重
with torch.no_grad():
outputs_old = old_model(**inputs)
logits_old = outputs_old.logits
# 对两个模型进行带温度的 Softmax
prob_new_soft = F.softmax(logits_new / temperature, dim=-1)
prob_old_soft = F.softmax(logits_old / temperature, dim=-1)
# KL 散度损失(确保维度正确)
loss_distill = F.kl_div(
F.log_softmax(logits_new / temperature, dim=-1)
.view(-1, logits_new.size(-1)),
prob_old_soft.view(-1, logits_old.size(-1)),
reduction='batchmean'
) * (temperature**2) # 缩放因子
# 总损失
total_loss = (1.0 - lambda_distill) * loss_ce + \
lambda_distill * loss_distill
# --- 反向传播将使用 total_loss ---
# total_loss.backward()
# optimizer.step()
LwF 避免了存储旧数据或计算参数重要性矩阵的需要,这使得它与重放或 EWC 相比,在计算上更具吸引力,特别是对于非常大的模型。然而,它的效用取决于一个假设:即保持旧模型在新数据上的预测,是保留与旧数据分布相关知识的一个良好替代。
虽然对于单个整体式LLM的纯持续预训练 (pre-training)来说不那么常见,但架构方法涉及修改模型的结构。渐进式神经网络 (neural network)等技术会冻结旧的网络部分,并为新任务添加新的“列”。另一种方法是使用参数 (parameter)高效模块,例如适配器(在第14章中讨论)。新的适配器可以针对新增数据进行训练,目标是将新知识隔离在这些小型模块中,同时保持主干部分冻结。然而,确保模型能够有效整合跨不同适配器或增量数据的知识,这仍然是一个研究领域。
无论选择何种策略,有几个实际方面很重要:
持续预训练的流程图,突出了使用新数据应用策略以及在旧数据和新数据分布上进行评估的重要步骤。
通常,最有效的方法会结合不同策略的元素。例如,将小型重放缓冲区与 EWC 或 LwF 结合使用可以提供互补优势。最佳策略在很大程度上取决于具体的限制(计算预算、存储容量、数据特性)以及保持旧知识与适应新信息之间的所需平衡。持续预训练仍然是一个活跃的研究领域,尤其是在将这些技术有效扩展到具有数万亿参数的基础模型方面。
这部分内容有帮助吗?
© 2026 ApX Machine LearningAI伦理与透明度•