Adapter Tuning 是一种“原模型参数冻结 插入小型适配模块”​ 的微调策略。它的核心思想是不动大模型的“基座”只在特定层间插入极小的“转换头”Adapter仅训练这极少部分新增参数从而在接近全量微调效果的同时极大降低计算和存储成本。Adapter 的本质是在 Transformer 层通常是 Feed-Forward Network 之后嵌入一个微小的神经网络模块。训练时原模型的所有参数被冻结Freeze只有这些新增的 Adapter 参数会被更新。典型应用场景多任务学习在同一个基座模型上为不同任务如情感分析、实体识别训练不同的 Adapter运行时灵活切换。领域自适应在医疗、法律等垂直领域插入领域特定的 Adapter无需改动通用基座。资源受限环境在消费级 GPU 或边缘设备上实现对 LLM 的高效定制。潜在局限推理延迟虽然参数少但增加了网络层数可能带来轻微的速度下降可通过模型编译优化。超参数敏感瓶颈维度 d的选择对性能有影响需要轻量级调优。总之Adapter Tuning 是 PEFT 工具箱中的一把“手术刀”它通过极致的参数隔离实现了大模型定制化的高性价比部署。需求分析高效参数微调的轻量级解决方案下面示例代码的核心需求是实现一种高效的参数高效微调Parameter-Efficient Fine-Tuning, PEFT方法即Adapter Tuning。随着大模型参数量的爆炸式增长传统的全参数微调在计算资源和存储空间上面临巨大挑战。具体需求包括需要一种方法能够在保持预训练模型参数冻结的前提下通过插入少量可训练参数来实现下游任务的适应要求在SST-2斯坦福情感树库情感分类任务上验证效果这是一个经典的二分类NLP任务需要大幅减少可训练参数数量传统BERT微调需要训练1.1亿参数而Adapter Tuning的目标是仅训练原始参数的0.1%-1%代码还需要兼容现有的Transformer架构特别是与Hugging Face库的良好集成同时要保证训练过程的稳定性和收敛性避免因参数更新不均衡导致的训练不稳定。这些需求源于实际部署中对计算资源、内存占用和训练效率的严格要求。架构设计插入式适配层的模块化设计示例代码采用层次化的模块化设计核心是插入式的Adapter层。Adapter模块作为基本构建块采用瓶颈结构设计首先通过降维投影down_project将768维的隐藏层压缩到瓶颈维度默认64维然后经过非线性激活GELU再通过升维投影up_project恢复原始维度最后添加残差连接。这种设计在计算效率和表达能力间取得平衡总参数量仅约0.1M2 * 768 * 64。BertWithAdapter类封装了整个微调架构其设计关键是完全冻结原始的BERT主干参数param.requires_grad False只为每个Transformer层BERT base共12层添加一个独立的Adapter模块最后添加分类头。在信息流动路径上模型在正向传播时依次获取12个Transformer层的隐藏状态对每一层独立应用对应的Adapter然后取最后一层的[CLS]标记作为整体表示。这种设计确保了Adapter能够干预每一层的表示学习同时残差连接保证了原始BERT知识的保留。优化器设计采用分层学习率为Adapter层3e-4和分类头2e-5设置不同的学习率这是针对不同参数特性的精细调优。代码实现# -*- coding: utf-8 -*- Created on Thu Jul 3 11:21:48 2025 author: liguo # adapter_tuning.py import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset from transformers import BertTokenizer, BertModel # 修正AdamW导入路径适配transformers 4.x版本 from transformers import get_linear_schedule_with_warmup from torch.optim import AdamW from sklearn.metrics import accuracy_score from datasets import load_dataset import numpy as np # ------------------------------- # 1. 定义 Adapter 模块无修改 # ------------------------------- class Adapter(nn.Module): def __init__(self, hidden_size768, bottleneck64): super(Adapter, self).__init__() self.down_project nn.Linear(hidden_size, bottleneck) self.non_linear nn.GELU() self.up_project nn.Linear(bottleneck, hidden_size) self.dropout nn.Dropout(0.1) def forward(self, x): residual x x self.down_project(x) x self.non_linear(x) x self.up_project(x) x self.dropout(x) return x residual # 残差连接 # ------------------------------- # 2. 修正带 Adapter 的 BERT 模型核心逻辑修复 # ------------------------------- class BertWithAdapter(nn.Module): def __init__(self, num_labels2, adapter_bottleneck64): super(BertWithAdapter, self).__init__() self.bert BertModel.from_pretrained(bert-base-uncased) self.num_labels num_labels self.adapter_bottleneck adapter_bottleneck # 冻结 BERT 主干参数 for param in self.bert.parameters(): param.requires_grad False # 为每一层 Transformer 配置一个 Adapter self.adapters nn.ModuleList([ Adapter(hidden_size768, bottleneckadapter_bottleneck) for _ in range(12) # BERT base 共12层 ]) # 分类头 self.classifier nn.Linear(768, num_labels) self.dropout nn.Dropout(0.1) def forward(self, input_ids, attention_mask): outputs self.bert( input_idsinput_ids, attention_maskattention_mask, output_hidden_statesTrue # 获取所有层的隐藏状态共13层embedding12 transformer ) # 提取12层 Transformer 的隐藏状态跳过第0层embedding transformer_hidden_states outputs.hidden_states[1:] # 列表长度12每层 shape [batch, seq_len, 768] # 核心修复逐层应用对应的 Adapter adapted_hidden [] for idx, (hidden_state, adapter) in enumerate(zip(transformer_hidden_states, self.adapters)): layer_hidden, layer_adapter hidden_state, adapter adapted_hidden.append(layer_adapter(layer_hidden)) # 取最后一层 Adapter 处理后的结果 final_hidden adapted_hidden[-1] # [batch, seq_len, 768] # 取 [CLS] token 表示 pooled_output final_hidden[:, 0] # [batch, 768] pooled_output self.dropout(pooled_output) logits self.classifier(pooled_output) return logits # ------------------------------- # 3. 数据集处理无修改 # ------------------------------- class SSTDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_length64): self.texts texts self.labels labels self.tokenizer tokenizer self.max_length max_length def __len__(self): return len(self.texts) def __getitem__(self, idx): text str(self.texts[idx]) label self.labels[idx] encoding self.tokenizer( text, truncationTrue, paddingmax_length, max_lengthself.max_length, return_tensorspt ) return { input_ids: encoding[input_ids].flatten(), attention_mask: encoding[attention_mask].flatten(), labels: torch.tensor(label, dtypetorch.long) } # ------------------------------- # 4. 主训练流程无修改 # ------------------------------- def train(): device torch.device(cuda if torch.cuda.is_available() else cpu) print(fUsing device: {device}) # 加载小样本数据集避免CPU训练过慢 dataset load_dataset(glue, sst2) train_data dataset[train].select(range(1000)) # 1000条训练样本 val_data dataset[validation].select(range(200)) # 200条验证样本 tokenizer BertTokenizer.from_pretrained(bert-base-uncased) model BertWithAdapter(num_labels2, adapter_bottleneck64).to(device) # 统计可训练参数仅Adapter和分类头 trainable_params sum(p.numel() for p in model.parameters() if p.requires_grad) print(fTrainable parameters: {trainable_params:,}) # 构建数据加载器 train_dataset SSTDataset(train_data[sentence], train_data[label], tokenizer) val_dataset SSTDataset(val_data[sentence], val_data[label], tokenizer) train_loader DataLoader(train_dataset, batch_size16, shuffleTrue) val_loader DataLoader(val_dataset, batch_size16) # 优化器分层设置学习率 optimizer AdamW([ {params: model.adapters.parameters(), lr: 3e-4}, # Adapter学习率更高 {params: model.classifier.parameters(), lr: 2e-5} # 分类头学习率与BERT微调一致 ]) # 学习率调度器 num_epochs 3 num_training_steps len(train_loader) * num_epochs scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_steps100, num_training_stepsnum_training_steps ) # 训练循环 model.train() for epoch in range(num_epochs): total_loss 0 for batch in train_loader: optimizer.zero_grad() # 张量移至目标设备 input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[labels].to(device) # 模型前向传播 logits model(input_ids, attention_mask) # 计算损失 loss nn.CrossEntropyLoss()(logits, labels) # 反向传播与参数更新 loss.backward() optimizer.step() scheduler.step() total_loss loss.item() # 打印epoch损失 avg_loss total_loss / len(train_loader) print(fEpoch {epoch1}/{num_epochs}, Average Loss: {avg_loss:.4f}) # 验证阶段 model.eval() val_preds, val_true [], [] with torch.no_grad(): # 禁用梯度计算 for batch in val_loader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[labels].to(device) logits model(input_ids, attention_mask) preds torch.argmax(logits, dim1) # 取概率最大的类别 val_preds.extend(preds.cpu().numpy()) val_true.extend(labels.cpu().numpy()) # 计算验证准确率 val_acc accuracy_score(val_true, val_preds) print(fValidation Accuracy: {val_acc:.4f}\n) model.train() # 回到训练模式 print(✅ Adapter Tuning Training Finished!) if __name__ __main__: train()代码结果C:\Users\xiayu\PyCharmMiscProject\AI-Agent-Dev-Practices-Code\langchain03\python.exe C:\Users\xiayu\PyCharmMiscProject\AI-Agent-Dev-Practices-Code\第3章代码\3.5-基于PyTorch的Adapter Tuning实现.pyUsing device: cpuTrainable parameters: 1,191,170Epoch 1/3, Average Loss: 0.7141Validation Accuracy: 0.5650Epoch 2/3, Average Loss: 0.6213Validation Accuracy: 0.7950Epoch 3/3, Average Loss: 0.4555Validation Accuracy: 0.8050✅ Adapter Tuning Training Finished!Process finished with exit code 0代码解析从数据流到梯度计算的完整训练流程在实现细节上代码展现了清晰的数据处理流程和训练循环。SSTDataset类负责将原始文本转换为BERT可接受的输入格式包括分词、填充、截断等预处理。核心修正体现在BertWithAdapter.forward()方法中原来的实现错误地将所有隐藏状态传递给同一个Adapter修正后的版本通过zip(transformer_hidden_states, self.adapters)确保每个Transformer层有对应的Adapter处理。训练流程分为设备检测与模型加载、数据集采样使用1000条训练200条验证样本以降低计算成本、优化器配置、训练循环和验证阶段。优化器配置时使用AdamW并搭配线性预热调度器get_linear_schedule_with_warmup这是Transformers微调的标准实践。训练循环中每个批次的计算图构建流程为输入文本→BERT编码仅前向传播→获取12层隐藏状态→逐层Adapter处理→取最后一层[CLS]标记→分类头→交叉熵损失。梯度回传时由于BERT主干参数被冻结梯度仅传播到Adapter和分类头参数这是实现参数高效的关键。验证阶段通过torch.no_grad()上下文管理器禁用梯度计算减少内存占用。整个代码结构完整涵盖了从数据加载、模型定义、训练优化到评估测试的完整流程是可实际运行的Adapter Tuning实现。