Adapter Tuning代码实现示例
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.1M(2 * 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_size=768, bottleneck=64):
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_labels=2, adapter_bottleneck=64):
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_size=768, bottleneck=adapter_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_ids=input_ids,
attention_mask=attention_mask,
output_hidden_states=True # 获取所有层的隐藏状态(共13层:embedding+12 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_length=64):
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,
truncation=True,
padding='max_length',
max_length=self.max_length,
return_tensors='pt'
)
return {
'input_ids': encoding['input_ids'].flatten(),
'attention_mask': encoding['attention_mask'].flatten(),
'labels': torch.tensor(label, dtype=torch.long)
}
# -------------------------------
# 4. 主训练流程(无修改)
# -------------------------------
def train():
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Using 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_labels=2, adapter_bottleneck=64).to(device)
# 统计可训练参数(仅Adapter和分类头)
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Trainable 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_size=16, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=16)
# 优化器(分层设置学习率)
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_steps=100,
num_training_steps=num_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(f"Epoch {epoch+1}/{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, dim=1) # 取概率最大的类别
val_preds.extend(preds.cpu().numpy())
val_true.extend(labels.cpu().numpy())
# 计算验证准确率
val_acc = accuracy_score(val_true, val_preds)
print(f"Validation 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实现.py"
Using device: cpu
Trainable parameters: 1,191,170
Epoch 1/3, Average Loss: 0.7141
Validation Accuracy: 0.5650
Epoch 2/3, Average Loss: 0.6213
Validation Accuracy: 0.7950
Epoch 3/3, Average Loss: 0.4555
Validation 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实现。

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)