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

Logo

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

更多推荐