提供了使用适配器模块微调预训练Transformer模型的动手指南。它采用了 adapter-transformers 库,该库是 Hugging Face transformers 库的一个扩展,专门设计用于便于适配器和其他PEFT方法的应用。我们的目标是通过仅训练插入模型中的轻量级适配器模块,使 bert-base-uncased 适应情感分类任务(使用 GLUE SST-2 数据集)。这种方法使原始模型绝大多数参数保持冻结状态,与完全微调相比,大大减少了计算和存储需求。环境设置首先,请确保已安装必要的库。我们需要 adapter-transformers (其中包含 transformers 和 torch),以及用于数据处理的 datasets。pip install -U adapter-transformers datasets加载模型和数据我们首先加载预训练模型和分词器,就像使用标准 transformers 库一样。我们还将加载斯坦福情感树库 (SST-2) 数据集。from transformers import AutoTokenizer from adapter_transformers import AutoAdapterModel # 使用 AutoAdapterModel from datasets import load_dataset # 加载分词器和模型 model_name = "bert-base-uncased" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoAdapterModel.from_pretrained(model_name) # 重要更改:使用 AutoAdapterModel # 加载数据集 dataset = load_dataset("glue", "sst2") # 预处理数据 def encode_batch(batch): """将句子分词。""" return tokenizer(batch["sentence"], max_length=80, truncation=True, padding="max_length") dataset = dataset.map(encode_batch, batched=True) dataset = dataset.rename_column("label", "labels") # 为 Trainer 兼容性重命名标签列 dataset.set_format(type="torch", columns=["input_ids", "attention_mask", "labels"]) print("数据集样本:", dataset["train"][0])使用 AutoAdapterModel 而非 AutoModelForSequenceClassification 很重要,因为它提供了管理适配器所需的各项方法。添加和配置适配器现在,我们将适配器模块添加到已加载的BERT模型中。 adapter-transformers 库使此操作变得简单直接。我们将为Transformer模型的每个层添加瓶颈适配器(经典的适配器类型)。from adapter_transformers.training import AdapterArguments from transformers import AdapterConfig # 配置适配器 # 使用 Pfeiffer 配置:瓶颈适配器,降维因子为16 adapter_config = AdapterConfig.load("pfeiffer", reduction_factor=16) # 将适配器添加到模型 # 给它一个独特名称,例如 "sentiment_adapter" adapter_name = "sentiment_adapter" model.add_adapter(adapter_name, config=adapter_config) # 为与此适配器关联的任务添加一个分类头 num_labels = dataset["train"].features["labels"].num_classes model.add_classification_head( adapter_name, num_labels=num_labels, id2label={ 0: "NEGATIVE", 1: "POSITIVE" } # 可选的标签映射 ) # 激活适配器进行训练 model.train_adapter(adapter_name) # 验证哪些参数是可训练的 total_params = sum(p.numel() for p in model.parameters()) trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad) print(f"总参数数量:{total_params}") print(f"可训练参数数量(适配器 + 头部):{trainable_params}") print(f"可训练参数占比:%:{100 * trainable_params / total_params:.4f}%")查看输出。您会注意到 trainable_params 仅占 total_params 的一小部分。add_adapter 方法会插入适配器模块(通常在注意力层和前馈层之后),而 add_classification_head 会为我们的特定任务添加一个新的最终层,该层也与适配器关联。重要的一点是,train_adapter(adapter_name) 会冻结整个预训练BERT模型,并且只解冻属于指定适配器 (sentiment_adapter) 及其关联分类头的参数。这种选择性的冻结/解冻是实现参数高效训练的核心机制。AdapterConfig.load("pfeiffer", reduction_factor=16) 定义了其架构。"pfeiffer" 指的是一种标准的瓶颈适配器配置,它带有层归一化和特定的激活函数。reduction_factor=16 表示瓶颈维度将是 $d_{model} / 16$,其中 $d_{model}$ 是BERT模型的隐藏维度(bert-base-uncased 为 768)。调整此因子可以直接控制参数数量和潜在性能之间的权衡。设置 Trainer我们使用标准的 Hugging Face Trainer 进行训练。设置过程与完全微调类似,但 Trainer 会自动处理只有适配器参数可训练这一事实。import numpy as np from transformers import TrainingArguments, Trainer, EvalPrediction from datasets import load_metric # 定义训练参数 # 注意:较小的批处理大小和较少的训练轮次适合演示 training_args = TrainingArguments( output_dir="./adapter_sst2_output", learning_rate=1e-4, # 适配器通常受益于稍高的学习率 num_train_epochs=3, per_device_train_batch_size=16, per_device_eval_batch_size=16, logging_steps=100, evaluation_strategy="epoch", save_strategy="epoch", # 每个训练轮次保存适配器检查点 load_best_model_at_end=True, metric_for_best_model="accuracy", remove_unused_columns=False, # 对适配器训练器很重要 ) # 定义评估指标 metric = load_metric("glue", "sst2") def compute_metrics(p: EvalPrediction): preds = np.argmax(p.predictions, axis=1) return metric.compute(predictions=preds, references=p.label_ids) # 实例化 Trainer trainer = Trainer( model=model, args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["validation"], tokenizer=tokenizer, compute_metrics=compute_metrics, )请注意,相较于完全微调(约 2e-5),学习率 (learning_rate) 可能会稍高一些(例如 1e-4),因为适配器有时使用较大的步长能更好地收敛。在 Trainer 中使用适配器模型时,通常需要设置 remove_unused_columns=False。训练适配器现在,我们可以开始训练过程。在此阶段,梯度将仅计算并应用于适配器和分类头的权重。# 开始训练 train_result = trainer.train() # 记录训练指标 metrics = train_result.metrics trainer.log_metrics("train", metrics) trainer.save_metrics("train", metrics) # 评估最佳模型 eval_metrics = trainer.evaluate(eval_dataset=dataset["validation"]) trainer.log_metrics("eval", eval_metrics) trainer.save_metrics("eval", eval_metrics)监控训练期间和训练后打印的训练进度和评估指标(本例中为准确率)。您应该会看到模型有效地学习了情感分类任务,尽管只更新了参数的一小部分。保存训练好的适配器适配器微调的一个主要优势是能够独立保存适配器权重。基础模型保持不变。# 定义适配器保存路径 output_adapter_dir = "./saved_adapters/sst2_adapter" # 保存适配器权重 model.save_adapter(output_adapter_dir, adapter_name) # 您也可以根据需要单独保存头部,或者它通常会与适配器一起保存。 # model.save_head(output_adapter_dir, adapter_name) print(f"适配器 '{adapter_name}' 已保存至 {output_adapter_dir}")定位到 output_adapter_dir 目录。您会发现配置文件 (adapter_config.json) 和权重文件(例如 pytorch_adapter.bin)。请注意这些文件与完整模型检查点(BERT-base 模型数百兆字节)相比是多么小。这说明了适配器在存储方面的效率。加载并使用适配器进行推理要使用训练好的适配器,您需要加载原始基础模型,然后再加载特定的适配器权重。from adapter_transformers import AutoAdapterModel # 使用相同的类 from transformers import TextClassificationPipeline # 再次加载基础模型(假设这是一个新的会话) inference_model = AutoAdapterModel.from_pretrained(model_name) inference_tokenizer = AutoTokenizer.from_pretrained(model_name) # 从保存的目录加载适配器权重 loaded_adapter_name = inference_model.load_adapter(output_adapter_dir) # 返回保存时的名称 # 重要:为推理设置活跃适配器 inference_model.set_active_adapters(loaded_adapter_name) # 如果头部未与适配器一同保存,或者您单独保存了它,则可能需要明确加载头部。 # 通常加载适配器会加载关联的头部。 # 如果出现问题,请查看 `adapter-transformers` 文档以了解头部加载的具体信息。 # 使用 pipeline 进行推理 classifier = TextClassificationPipeline(model=inference_model, tokenizer=inference_tokenizer, device=training_args.device.index if training_args.device else -1) # 示例句子 sentences = [ "This movie was absolutely fantastic!", "I was completely bored throughout the entire film.", "The acting was decent, but the plot was predictable." ] results = classifier(sentences) for sentence, result in zip(sentences, results): print(f"句子:{sentence}") print(f"预测标签:{result['label']},得分:{result['score']:.4f}\n")这说明了其模块化特性:大型基础模型可以加载一次,然后在其之上加载不同的轻量级适配器,以便在不同任务之间切换,而无需完整模型的多个副本。set_active_adapters 告知模型在推理期间的前向传播中使用哪个或哪些适配器。本次实践练习展示了适配器微调的核心工作流程:添加适配器模块、冻结基础模型、仅训练适配器、单独保存它们,以及加载它们以进行高效推理。您已成功为一个特定任务微调了一个强大的大型语言模型,同时只修改了其参数的一小部分,这体现了这种PEFT技术的效率和模块化优势。