当前位置: 首页 > news >正文

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实现。

http://www.jsqmd.com/news/703105/

相关文章:

  • 2026年天津性价比高的记账专业公司,快来了解 - 工业品牌热点
  • Termux避坑实录:为什么你的Octave-Kernel在Jupyter里跑不起来?附Debian环境完美解决方案
  • 3步掌握Snap.Hutao原神工具箱:高效游戏数据管理终极指南
  • 重庆叛逆孩子管理学校哪家好,为你揭秘优质品牌 - 工业品网
  • gInk屏幕标注工具:免费高效的Windows屏幕批注终极指南
  • 【MCP 2026跨服务器编排终极指南】:20年SRE亲授7大高可用编排模式与3类生产级避坑清单
  • 终极Windows内存优化指南:Mem Reduct让你的电脑时刻保持最佳状态
  • Xbox成就解锁完整指南:免费工具助你轻松达成全成就目标
  • 2026年温州直播间装修全案服务观察:直播场景专业化跃迁中的老陈实践 - GrowthUME
  • K8s StatefulSet 存储卷自动挂载机制
  • 告别点阵字库!用U8g2库在0.96寸OLED上轻松显示中文和图标(附完整代码)
  • 聊聊DL苹果酸厂商推荐,山西恒强化工值得考虑不 - 工业推荐榜
  • Symphony:从管理代码到管理工作流的AI自动化框架实践
  • 保姆级教程:在CentOS 7上用BIND 9一步步搭建DNS正向代理(含防火墙配置)
  • 2026年收藏必备:降AI率工具汇总清单 - 降AI实验室
  • 哔咔漫画下载器:5分钟打造你的个人离线漫画图书馆
  • Moonlight TV:在智能电视上实现30ms低延迟游戏串流的完整指南
  • 全国DL苹果酸制造厂排名情况,前十的都有谁 - mypinpai
  • 告别客户端!用Python+noVNC在Windows 10上搭建Web版远程桌面(保姆级教程)
  • 天津安立财税记账公司性价比咋样,口碑好不好? - 工业推荐榜
  • 终极指南:5分钟快速配置Switch大气层系统,性能提升200%
  • 使用 LangGraph 进行并发任务分解:从串行到 DAG 的性能量化
  • 2026年DL苹果酸生产厂推荐,好用又实惠的品牌有哪些 - 工业设备
  • GTA:SA存档编辑器终极指南:如何轻松修改你的圣安地列斯游戏体验
  • MPC-HC:Windows平台最值得信赖的开源媒体播放器完整指南
  • 别再只pip install graphviz了!Jupyter里画决策树报错‘dot’找不到?试试这个两步走的解法
  • 73KB的键盘屏蔽神器:极简设计背后的高效按键管理方案
  • 探讨DL苹果酸选购要点,恒强化工产品值得选吗? - myqiye
  • 不止是加个头文件:深入理解uint32_t在嵌入式与网络编程中的实战意义
  • 纯前端PPTX转HTML:无需服务器的跨平台演示文稿转换方案