全参数微调涉及理解其工作原理、超参数考量、正则化需求和资源要求。这个动手练习将指导您完成对一个相对较小的预训练语言模型进行全参数微调,以适应特定下游任务的过程。即使在LLM的背景下其规模较小,这个过程仍需计算资源,突出了资源管理的重要性。我们将使用Hugging Face的transformers和datasets库,这些库为许多常见的自然语言处理(NLP)任务和模型提供了便捷的抽象。本例主要关注文本分类任务,这是微调的一个常见应用。环境准备首先,请确保已安装必要的库。您主要需要transformers、datasets、torch(或tensorflow)以及accelerate来实现高效训练。pip install transformers datasets torch accelerate scikit-learn我们假设您在能使用GPU的环境中工作,因为即使对于较小的模型,全参数微调在CPU上也会非常慢。accelerate库有助于自动管理设备(CPU/GPU)分配。选择模型和数据集对于本次实践练习,我们将使用distilbert-base-uncased,它是BERT的一个蒸馏版本,在保持显著性能的同时,规模更小、速度更快。对于任务,我们将使用imdb数据集,它是二元情感分类(正面/负面电影评论)的标准基准。# 定义模型和数据集名称 model_checkpoint = "distilbert-base-uncased" dataset_name = "imdb"加载和准备数据datasets库使加载标准数据集变得简单。from datasets import load_dataset # 加载数据集 raw_datasets = load_dataset(dataset_name) # 显示数据集结构(可选) print(raw_datasets) # DatasetDict({ # train: Dataset({ # features: ['text', 'label'], # num_rows: 25000 # }) # test: Dataset({ # features: ['text', 'label'], # num_rows: 25000 # }) # unsupervised: Dataset({ # features: ['text', 'label'], # num_rows: 50000 # }) # })接下来,我们需要对文本数据进行分词,以便模型能够理解它。我们使用与我们选择的预训练模型对应的分词器。from transformers import AutoTokenizer # 加载分词器 tokenizer = AutoTokenizer.from_pretrained(model_checkpoint) # 定义分词函数 def tokenize_function(examples): # 截断长度超过模型最大输入尺寸的序列 return tokenizer(examples["text"], truncation=True, padding="max_length", max_length=512) # 对整个数据集应用分词(为提高效率,进行批量处理) tokenized_datasets = raw_datasets.map(tokenize_function, batched=True) # 移除原始的'text'列,因为它不再需要 tokenized_datasets = tokenized_datasets.remove_columns(["text"]) # 将'label'重命名为'labels',这是Trainer所期望的 tokenized_datasets = tokenized_datasets.rename_column("label", "labels") # 将格式设置为PyTorch张量 tokenized_datasets.set_format("torch") # 创建较小的子集以加快演示(可选) # 对于完整运行,请移除或调整这些行 small_train_dataset = tokenized_datasets["train"].shuffle(seed=42).select(range(1000)) small_eval_dataset = tokenized_datasets["test"].shuffle(seed=42).select(range(1000)) print("\n分词后的训练数据样本:") print(small_train_dataset[0])这个过程将原始文本转换为输入ID、注意力掩码,并包含监督学习所需的标签。加载模型我们加载配置用于序列分类的预训练DistilBERT模型。AutoModelForSequenceClassification会自动在基础DistilBERT模型之上添加一个分类头。标签数量从数据集中推断(在本例中为2:正面/负面)。from transformers import AutoModelForSequenceClassification # 加载用于序列分类的模型 # num_labels=2 表示二元分类(正面/负面) model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint, num_labels=2)此时,model包含预训练权重。在全参数微调期间,所有这些权重,包括基础模型和新添加的分类头,都将被更新。配置训练参数TrainingArguments类包含了训练过程的所有超参数和设置。这包括我们之前讨论过的参数,如学习率、批量大小、训练轮数和正则化(权重衰减)。from transformers import TrainingArguments # 定义检查点和日志的输出目录 output_dir = "./results/distilbert-imdb-full" training_args = TrainingArguments( output_dir=output_dir, evaluation_strategy="epoch", # 在每个 epoch 结束时评估性能 save_strategy="epoch", # 在每个 epoch 结束时保存检查点 num_train_epochs=3, # 训练 epoch 数量(根据需要调整) per_device_train_batch_size=8, # 每个 GPU 的批量大小 per_device_eval_batch_size=8, # 评估时的批量大小 learning_rate=2e-5, # 初始学习率(微调的常用值) weight_decay=0.01, # 应用权重衰减进行正则化 logging_dir='./logs', # 存储日志的目录 logging_steps=100, # 每 100 步记录训练损失 load_best_model_at_end=True, # 在结束时加载性能最好的模型检查点 metric_for_best_model="accuracy", # 用于确定“最佳”模型的指标 # 使用 push_to_hub=True 将结果上传到 Hugging Face Hub(需要登录) # push_to_hub=False, )这些参数直接控制了前面章节理论上讨论的微调过程。选择合适的值(如学习率和训练轮数)通常需要通过实验来确定。定义评估指标为了在训练期间监测性能,我们需要一个根据模型预测和真实标签计算指标的函数。我们将为此分类任务使用标准准确率。import numpy as np from datasets import load_metric # 加载准确率指标 metric = load_metric("accuracy") def compute_metrics(eval_pred): logits, labels = eval_pred predictions = np.argmax(logits, axis=-1) return metric.compute(predictions=predictions, references=labels)初始化TrainerTrainer类简化了训练循环。它根据提供的模型、参数、数据集、分词器和指标函数,协调数据加载、模型正向/反向传播、优化、评估和检查点保存。from transformers import Trainer trainer = Trainer( model=model, args=training_args, train_dataset=small_train_dataset, # 使用完整数据集进行实际训练 eval_dataset=small_eval_dataset, # 使用完整测试集进行正确评估 tokenizer=tokenizer, # 对于填充整理很有用 compute_metrics=compute_metrics, )启动微调过程一切设置就绪后,启动全参数微调过程只需一个命令:# 启动训练过程 train_result = trainer.train() # (可选)保存训练指标 trainer.log_metrics("train", train_result.metrics) trainer.save_metrics("train", train_result.metrics) # 保存最终微调好的模型和分词器 trainer.save_model(output_dir) # 由于 load_best_model_at_end=True,将保存最佳模型 trainer.save_state() # 保存 Trainer 状态,包括随机数生成器(RNG)状态在训练期间,您将看到输出日志,显示训练损失逐渐减小,并且评估指标(准确率)在每个epoch后可能有所提升。这直接对应于模型参数 $\theta$ 通过梯度下降,基于在imdb数据集上计算的损失而被更新。评估微调模型训练完成后,您可以在测试集(或指定的eval_dataset)上明确运行评估。# 评估最终模型 eval_results = trainer.evaluate() # 打印评估结果 print(f"评估结果: {eval_results}") trainer.log_metrics("eval", eval_results) trainer.save_metrics("eval", eval_results)输出将显示您的微调模型在保留评估数据上的性能表现。资源考量和后续步骤即使使用DistilBERT和部分数据,您可能也注意到训练需要相当多的时间和GPU内存。将此扩展到GPT-3变体或Llama模型等更大规模的模型,将需要更多资源,通常涉及多个高端GPU和分布式训练策略,正如第7章中所述。内存占用量大致随序列长度呈平方增长,并随更新参数的数量呈线性(或更高,取决于架构)增长。这个动手练习展示了使用标准工具进行全参数微调的完整流程。您加载了数据,进行了准备,配置了训练,执行了微调循环,并评估了结果。这个过程虽然有效,但强调了更新每个模型参数所涉及的计算成本。这种成本促使人们研究参数高效微调(PEFT)方法,我们将在下一章中介绍这些方法,目的是在大幅降低计算需求的情况下实现相似的适应结果。