第三章:高效微调篇

前言

在第二章:实战演练篇,我们已经接触了各种各样的实战例子,在训练过程中或许大家因为设备性能、使用GPU卡的问题从而想要得到结果变得非常漫长,本篇就来给大家介绍下如何使用参数高效微调加快模型训练速度。

参数高效微调简介

什么是参数高效微调

在传统的深度学习中,针对下游任务进行微调通常采用全量微调 (Full Fine-Tuning),即更新模型网络中的每一层权重。但在大语言模型(LLM)时代,模型参数动辄数十亿甚至上千亿,全量微调面临着难以逾越的硬件鸿沟(尤其是优化器状态带来的显存黑洞)。

PEFT 是一种全新的微调范式。它的核心理念是:冻结预训练模型的大部分或全部主体权重,仅在模型中引入极少量的可训练参数(通常不到总参数量的 1%)进行更新

使用 PEFT 作为高效微调模型的库,能够通过小参数量撬动大模型 。这种方式不仅极大地节省了显存,降低了硬件门槛 ,还能有效缓解模型在学习新领域知识时产生的“灾难性遗忘”,保留预训练基座强大的通用能力。

常见的参数高校微调方法

image-20260519170817326

工业界和学术界目前演化出了多种 PEFT 路线,主要可以分为以下三大类:

重参数化方法 (Reparameterization-based)

  • LoRA (Low-Rank Adaptation): 这是目前工业界最火热、应用最广的方法 。它冻结了原有 Transformer 层的权重矩阵,而是在其旁路注入两个低秩矩阵(降维矩阵 $A$ 和升维矩阵 $B$)。训练时只更新这两个小矩阵,推理时再将它们与原矩阵合并,不增加任何推理延迟。QLoRA: LoRA 的极限显存优化版 。它将预训练基座模型量化为 4-bit 精度加载,然后配合 16-bit 精度(如 BF16)的 LoRA 矩阵进行微调。这使得在消费级显卡(如单张 24GB 显存)上微调数百亿参数的模型成为可能。

增加额外参数方法 (Addition-based)

  • Adapter Tuning: 在 Transformer 的注意力层(Attention)和前馈神经网络层(FFN)之后,串联插入小型的多层感知机(MLP)模块。微调时只训练这些 Adapter 模块。
  • Soft Prompts (Prompt Tuning / Prefix Tuning / P-Tuning): 将微调的重点放在输入端 。不再硬性修改输入的文本提示词,而是在输入的 Token Embedding 前面,拼接一段连续的、可训练的虚拟向量(Virtual Tokens)。模型通过梯度反向传播自动寻找最适合当前任务的 Prompt 向量。

指定训练特定参数方法 (Specification-based)

  • BitFit: 仅解冻并训练模型网络内部的偏置项 (Bias),而冻结所有庞大的权重矩阵 (Weights)。

BitFit代码实战

BitFit (Bias-term Fine-tuning) 是由 Ben-Zaken 等人在 2021 年提出的一种极其轻量级的 PEFT 方法。

  • 为什么有效? 在神经网络的线性层公式 $y = Wx + b$ 中,$W$ 是庞大的权重矩阵,而 $b$ 是极小的偏置向量。研究发现,当预训练模型已经具备了非常强大的特征提取能力时,适配下游新任务并不一定需要对特征空间($W$)进行剧烈的旋转或缩放,往往只需要在特征空间中进行微小的平移即可。而更新偏置项 $b$ 正好对应着特征空间的线性平移。
  • 极度轻量: 在标准的 BERT 或 Llama 模型中,偏置项的参数量通常仅占模型总参数量的 0.05% 左右。这意味着几乎不需要额外的显存来存储庞大的优化器状态。

在 Hugging Face 的生态中,实现 BitFit 不需要引入任何外部的 PEFT 库,纯靠原生的 PyTorch 参数冻结机制即可完成。下面是完整的代码实战逻辑:

from transformers import AutoModelForSequenceClassification

# Step 1: 加载预训练模型 (以经典的 BERT 为例)
model_name = "bert-base-uncased"
model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2)

# Step 2: 冻结模型的所有参数
for param in model.parameters():
    param.requires_grad = False

# Step 3: 遍历所有参数,仅解冻名称中包含 'bias' 的偏置项
for name, param in model.named_parameters():
    # 只要参数名里带有 bias,就将其梯度计算重新打开
    if "bias" in name:
        param.requires_grad = True

# Step 4: (可选) 解冻分类头 (Classifier Head)
# 在做特定任务微调时,模型顶层用于输出分类结果的线性层是随机初始化的,通常也需要解冻参与训练
for name, param in model.named_parameters():
    if "classifier" in name:
        param.requires_grad = True

# Step 5: 验证可训练参数量的比例
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
total_params = sum(p.numel() for p in model.parameters())

print(f"总参数量: {total_params}")
print(f"可训练参数量: {trainable_params}")
print(f"可训练比例: {100 * trainable_params / total_params:.4f}%")

# 预期输出类似于: 
# 总参数量: 109483778
# 可训练参数量: 104450 (包含 Bias 和顶层分类器)
# 可训练比例: 0.0954%

在完成上述代码后,你可以直接将这个被高度冻结的 model 传入 Hugging Face 的 Trainer 组件中,按照标准流程启动训练。底层的优化器在初始化时,将只会为这不到 0.1% 的偏置项分配显存空间。

Prompt_Tuning原理与微调实战

Prompt-Tuning原理介绍

在了解 Prompt Tuning 之前,我们需要区分传统的“硬提示”与微调阶段的“软提示”。

从 Hard Prompts 到 Soft Prompts

在使用大语言模型时,我们通常会手动编写提示词(例如:“请将以下英文翻译为中文:…”),这被称为 Hard Prompts (硬提示)。硬提示是离散的文本,依赖人工经验,且无法通过数学梯度进行精确的连续优化。

Prompt Tuning 引入了 Soft Prompts (软提示) 的概念。将微调的重点放在输入端。不再硬性修改输入的文本提示词,而是在输入的 Token Embedding 前面,拼接一段连续的、可训练的虚拟向量(Virtual Tokens)。

底层数学逻辑

image-20260519173126802

假设原始输入的文本经过分词和嵌入后,形成一个矩阵 $E \in \mathbb{R}^{n \times d}$,其中 $n$ 是序列长度,$d$ 是模型的隐藏层维度。

在 Prompt Tuning 中,我们会初始化一个极小的可训练参数矩阵 $P \in \mathbb{R}^{p \times d}$,其中 $p$ 是我们设定的虚拟 Token 的数量(例如 8 个或 16 个)。

模型真正的输入变成了这两个矩阵在序列维度上的拼接:$[P; E]$。

在训练过程中,预训练大模型的所有主体权重全部冻结,没有任何参数更新。模型通过梯度反向传播自动寻找最适合当前任务的 Prompt 向量。

扩展知识:Prompt Tuning 的家族变体

  1. Prompt Tuning (Lester et al., 2021): 最基础的做法,仅仅在模型的输入层 (Input Layer) 拼接可训练的虚拟 Tokens。参数量极小,但对模型的规模有要求(通常在模型参数量大于 10B 时,效果才能媲美全量微调)。
  2. Prefix-Tuning (Li & Liang, 2021): 不仅在输入层,而是在 Transformer 的每一层 (Every Layer) 的 Key 和 Value 矩阵前都拼接可训练的前缀向量。它的参数量比 Prompt Tuning 稍大,但在小规模模型上表现更好。
  3. P-Tuning (Liu et al., 2021): 为了解决虚拟 Token 之间缺乏上下文关联的问题,引入了一个小型的 LSTM 或 MLP 编码器来生成这些虚拟 Token 的嵌入向量。

PEFT库安装与简介

为了方便开发者快速调用上述各种高效微调算法,Hugging Face 官方推出了 PEFT (Parameter-Efficient Fine-Tuning) 库。

安装方式

在你的 Python 环境中,通过以下命令即可安装最新版本:

pip install peft

PEFT 库核心架构理念

PEFT 库采用了“即插即用”的设计模式,它完美兼容 transformers 库。它的核心工作流非常标准化,通常包含三个关键对象:

  • Base Model: 你通过 AutoModel 从 Hugging Face 加载的、处于冻结状态的基础预训练模型。
  • Config: 对应微调算法的配置类(如 PromptTuningConfig, LoraConfig),用于设定需要引入的额外参数的规模和任务类型。
  • PeftModel: 封装类。通过 get_peft_model(base_model, config) 函数,PEFT 会自动修改基础模型的结构,注入你设定的软提示或旁路矩阵,并自动冻结基础模型的梯度,只开放新注入参数的训练权限。

Prompt-Tuning代码实战

下面是一套基于 Hugging Face 生态的 Prompt Tuning 完整代码实战。我们将以因果语言模型(Causal LM)为例,展示如何向模型注入软提示。

核心扩展知识:文本初始化 (Text Initialization) 在实战中,如果我们对虚拟 Token 的向量进行纯随机初始化,模型在初期会非常困惑,收敛极慢。工业界最佳实践是使用 prompt_tuning_init="TEXT",即使用与下游任务相关的一段自然语言文本(例如 “Classify if this text is positive or negative:”)来初始化这些虚拟向量,这能成倍提升训练初期的稳定性。

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PromptTuningConfig, PromptTuningInit, get_peft_model, TaskType

# 1. 加载基础模型与分词器 (以轻量级的 GPT-2 或 Bloom 为例)
model_name_or_path = "gpt2"
tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)
base_model = AutoModelForCausalLM.from_pretrained(model_name_or_path)

# 如果 tokenizer 没有 pad token,需要手动设置,防止批处理时报错
if tokenizer.pad_token_id is None:
    tokenizer.pad_token_id = tokenizer.eos_token_id

# 2. 核心:配置 Prompt Tuning 参数
peft_config = PromptTuningConfig(
    task_type=TaskType.CAUSAL_LM, # 任务类型:因果语言生成
    prompt_tuning_init=PromptTuningInit.TEXT, # 使用具体文本进行初始化
    prompt_tuning_init_text="Classify the sentiment of the following text:", # 初始化文本
    num_virtual_tokens=8, # 虚拟 Token 的数量 (通常根据初始化文本的长度来定)
    tokenizer_name_or_path=model_name_or_path, # 传入 tokenizer 路径以正确映射初始化文本
)

# 3. 将基础模型转换为 PEFT 模型
peft_model = get_peft_model(base_model, peft_config)

# 4. 打印可训练参数比例,验证是否极度轻量化
peft_model.print_trainable_parameters()
# 预期输出类似: trainable params: 6,144 || all params: 124,445,952 || trainable%: 0.0049%

# 5. 模型推理测试 (查看注入 Soft Prompts 后的结构)
# 你可以直接将 peft_model 传入 Transformers 的 Trainer 中进行标准训练
inputs = tokenizer("Text: The movie was fantastic. Sentiment: ", return_tensors="pt")

# 模型在 forward 时,会自动把我们配置的 8 个虚拟 Token 的 Embedding 拼接到输入序列的最前面
with torch.no_grad():
    outputs = peft_model.generate(**inputs, max_new_tokens=10)

print(tokenizer.decode(outputs[0], skip_special_tokens=True))

经过上述步骤,你只需更新那微乎其微的 6,144 个参数,就能让底层模型适应全新的任务。

P-Tuning原理与微调实战

在前面的学习中,我们了解了 Prompt Tuning 是如何在输入端加入连续的“软提示”(Soft Prompts)的。但基础的 Prompt Tuning 存在一个明显的缺陷:那些被随机初始化的虚拟 Token 向量之间是相互独立的,缺乏内在的语义关联,这导致模型在训练初期收敛困难,且对模型规模要求极高。

为了解决这个问题,清华大学的研究团队提出了 P-Tuning。下面我将为你详细拆解 P-Tuning 的底层原理、演进过程(P-Tuning v2),并带你完成使用 Hugging Face PEFT 库的代码实战。

P-Tuning原理介绍

P-Tuning 的核心思想依然是使用可训练的连续向量(Soft Prompts)来引导预训练模型,但它在生成这些向量的方式上做出了重大创新。

核心痛点与解决方案

在基础的 Prompt Tuning 中,如果我们插入 5 个虚拟 Token,系统会直接在特征空间中随机初始化 5 个独立的向量。由于 Transformer 的输入 Embedding 空间高度离散且复杂,直接通过梯度下降去寻找最优的独立向量就像在大海捞针,模型很容易陷入局部最优解。

P-Tuning 引入了一个 Prompt Encoder(提示编码器) 来解决这个问题。它不再直接优化虚拟 Token 的 Embedding 矩阵,而是使用一个极其轻量级的神经网络(通常是双层 MLP 或是双向 LSTM)来“生成”这些虚拟 Token 的向量表示。

底层数学逻辑

假设我们要插入序列长度为 $m$ 的虚拟 Token,其初始的独立嵌入向量序列为 $E = [e_1, e_2, …, e_m]$。

在 P-Tuning 中,我们将这个序列送入 Prompt Encoder(以 LSTM 为例):

$$[h_1, h_2, …, h_m] = \text{Bi-LSTM}(e_1, e_2, …, e_m)$$

随后,通常还会经过一个多层感知机(MLP)进行特征映射:

$$v_i = \text{MLP}(h_i)$$

最终生成的连续提示向量 $V = [v_1, v_2, …, v_m]$ 才会被拼接到真实的输入文本前,送入冻结的大模型主体。

为什么引入 Encoder 更好?

  • 建立上下文依赖: LSTM 或 MLP 能够让不同的虚拟 Token 之间产生信息交互。比如第一个虚拟向量能够感知到后面虚拟向量的存在,这使得生成的提示序列更加连贯,更符合自然语言的潜在逻辑。
  • 平滑优化空间: 引入一层神经网络相当于在离散的词表空间和连续的语义空间之间加了一个缓冲器,大幅降低了优化的难度,使得小规模模型也能通过这种方式取得很好的微调效果。

扩展知识:从 P-Tuning 到 P-Tuning v2

在工业界落地时,你可能经常听到 P-Tuning v2。了解基础版本后,必须了解 v2 版本的进化,因为这是目前应用更广泛的形态。

  • 基础 P-Tuning 的局限: 它仅仅在模型的输入层(Input Layer)注入生成的虚拟 Token。当基础模型的参数量小于 100 亿(10B)时,仅靠输入层的微小扰动,很难让模型在复杂的自然语言理解(NLU)任务(如序列标注、复杂阅读理解)上比肩全量微调。
  • P-Tuning v2 的突破 (Deep Prompt Tuning): 借鉴了 Prefix-Tuning 的思想,P-Tuning v2 不再只将虚拟 Token 放在输入层,而是在 Transformer 每一层的注意力机制(Self-Attention)前都注入特定的虚拟向量(Prefixes)
  • 收益: 这种深度注入极大地增加了可训练参数的容量(虽然依然远小于全量微调),使得即便是 3 亿(300M)参数的小模型,也能在各项任务上达到与全量微调完全一致的性能,彻底解决了软提示技术在小模型上的规模魔咒。

P-Tuning代码实战

在 Hugging Face 的 PEFT 库中,实现 P-Tuning 非常便捷。PEFT 官方提供了 PromptEncoderConfig 来专门处理这种带有编码器的软提示网络。

下面是一套基于因果语言模型(Causal LM)的 P-Tuning 完整实战代码逻辑:

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PromptEncoderConfig, get_peft_model, TaskType, PromptEncoderReparameterizationType

# Step 1: 加载基础模型与分词器 (以 GPT-2 为例,实际业务中可替换为 Qwen、Llama 等)
model_name_or_path = "gpt2"
tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)
base_model = AutoModelForCausalLM.from_pretrained(model_name_or_path)

if tokenizer.pad_token_id is None:
    tokenizer.pad_token_id = tokenizer.eos_token_id

# Step 2: 核心配置 - P-Tuning 专属参数
# 注意这里使用的是 PromptEncoderConfig,而不是基础的 PromptTuningConfig
peft_config = PromptEncoderConfig(
    task_type=TaskType.CAUSAL_LM,
    num_virtual_tokens=20, # 设定虚拟 Token 的数量
    encoder_reparameterization_type=PromptEncoderReparameterizationType.MLP, # 编码器类型,可选 MLP 或 LSTM
    encoder_hidden_size=128, # 编码器的隐藏层维度
    encoder_num_layers=2, # 编码器的层数
)

# Step 3: 包装为 P-Tuning 模型
peft_model = get_peft_model(base_model, peft_config)

# Step 4: 查看可训练参数比例
peft_model.print_trainable_parameters()
# 预期输出将显示一个极低的可训练比例,参数量由虚拟 Token 矩阵和 MLP/LSTM 网络共同组成
# 例如:trainable params: 304,128 || all params: 124,743,936 || trainable%: 0.2438%

# Step 5: 前向传播测试
inputs = tokenizer("Question: What is the capital of France? Answer: ", return_tensors="pt")

# 模型在生成时,会先通过内部配置好的 MLP/LSTM 生成这 20 个虚拟 Token 的连续向量,
# 然后将其拼接到 "Question: What is..." 前面,再送入冻结的底层 GPT-2 模型进行推理。
with torch.no_grad():
    outputs = peft_model.generate(**inputs, max_new_tokens=10)

print(tokenizer.decode(outputs[0], skip_special_tokens=True))

实战工程避坑指南: 在执行上述代码训练时,通常需要比基础的 Prompt Tuning 稍微大一点的学习率(例如 1e-35e-3)。因为你不仅在优化虚拟 Token 的底层嵌入,还在训练一个完整的微型神经网络(Encoder)。给足优化器“学习空间”,它才能为你编织出最符合上下文逻辑的提示向量序列。

Prefix-Tuning原理与微调实战

Prefix-Tuning原理介绍

Prefix-Tuning 由 Li & Liang 在 2021 年提出,最初是专门为了解决自然语言生成(NLG)任务(如文本摘要、机器翻译)而在 GPT-2 和 BART 上设计的。

image-20260521131847376

image-20260521131901320

image-20260521131925731

核心痛点与突破

基础的 Prompt Tuning 仅仅在模型的输入层(Input Layer)拼接可训练的虚拟 Tokens。对于动辄几十层的深层 Transformer 模型来说,输入层的微小扰动在经过多层网络的正向传播后,其影响力会被严重稀释。这也是为什么基础 Prompt Tuning 只有在百亿参数(10B)以上的超大模型上才能媲美全量微调的原因。

Prefix-Tuning 打破了这个局限:它不仅在输入层,而是在 Transformer 的每一层(Every Layer)的 Key 和 Value 矩阵前都拼接可训练的前缀向量 。

底层数学逻辑

在标准的 Transformer 自注意力机制(Self-Attention)中,计算公式为:

$$Attention(Q, K, V) = \text{softmax}(\frac{QK^T}{\sqrt{d}})V$$

在 Prefix-Tuning 中,我们冻结了预训练模型的原始参数,而是为每一层的注意力机制初始化了两个可训练的低秩矩阵:前缀键($P_K$)和前缀值($P_V$)。

在进行注意力计算时,模型真正的 Key 和 Value 变成了拼接后的矩阵:

$$K_{new} = [P_K; K]$$

$$V_{new} = [P_V; V]$$

Query($Q$)保持不变。这意味着,输入序列的每一个 Token,在计算注意力权重时,不仅会关注序列中的其他真实 Token,还会强制关注我们注入的这些“虚拟前缀”。

为什么不拼接 Query?

学术界的研究发现,如果同时改变 $Q$、$K$、$V$,会严重破坏预训练模型原有的注意力分布,导致模型极难收敛。仅修改 $K$ 和 $V$ 相当于在模型原有的知识检索系统中加入了特定的“背景线索”,而保留了 $Q$ 作为原本“检索意图”的纯粹性。

三大 Soft Prompts 路线横向对比

为了帮你彻底理清这些容易混淆的软提示技术,总结了它们的核心差异:

| 微调方法 | 虚拟向量注入位置 | 参数生成方式 | 适用场景与优势 |
| —————– | ————————- | ————————————- | ———————————————————— |
| Prompt Tuning | 仅输入层 (Input Layer) | 随机初始化,直接作为 Embedding 训练 | 参数量极小,适用于 10B 以上的超大模型。 |
| P-Tuning (v1) | 仅输入层 (Input Layer) | 引入 LSTM/MLP 编码器生成向量 | 解决了虚拟 Token 间缺乏上下文关联的问题。 |
| Prefix-Tuning | 每一层的注意力机制 (K, V) | 引入 MLP 生成前缀参数,训练后丢弃 MLP | 深度干预,极大地提升了小模型的表达能力,尤其擅长生成类任务。 |

注:我们之前提到的 P-Tuning v2,在核心架构上就是完全借鉴了 Prefix-Tuning 的“深层注入”思想,并将其推广到了自然语言理解(NLU)任务中。

Prefix-Tuning代码实战

在 Hugging Face 的 PEFT 库中,实现 Prefix-Tuning 同样非常标准化。我们需要使用 PrefixTuningConfig 类。

核心工程技巧:前缀重参数化 (Prefix Projection)

直接优化每一层的 $P_K$ 和 $P_V$ 矩阵是非常不稳定的。工业界的标准做法是:在训练阶段,不直接训练前缀矩阵,而是像 P-Tuning 一样,训练一个一个小型的 MLP 网络,让这个 MLP 来生成每一层的前缀矩阵。训练结束后,我们将 MLP 丢弃,只保存它生成的静态前缀矩阵用于推理。PEFT 库底层已经自动为你处理好了这个重参数化过程。

下面是基于因果语言模型(Causal LM)的完整实战代码:

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PrefixTuningConfig, get_peft_model, TaskType

# 1. 加载基础模型与分词器
model_name_or_path = "gpt2"
tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)
base_model = AutoModelForCausalLM.from_pretrained(model_name_or_path)

if tokenizer.pad_token_id is None:
    tokenizer.pad_token_id = tokenizer.eos_token_id

# 2. 核心:配置 Prefix-Tuning 参数
peft_config = PrefixTuningConfig(
    task_type=TaskType.CAUSAL_LM,
    num_virtual_tokens=20, # 虚拟前缀的长度
    # Prefix-Tuning 默认会在底层使用 MLP 进行重参数化以稳定训练
    prefix_projection=True, # 强烈建议保持开启
)

# 3. 将基础模型转换为 Prefix-Tuning 模型
peft_model = get_peft_model(base_model, peft_config)

# 4. 打印可训练参数比例,验证轻量化
peft_model.print_trainable_parameters()
# 预期输出将显示,由于每一层都注入了参数,其可训练参数量通常会略多于基础的 Prompt Tuning
# 例如: trainable params: 983,040 || all params: 125,422,848 || trainable%: 0.7837%

# 5. 模型推理测试
inputs = tokenizer("Translate English to French: The weather is good. -> ", return_tensors="pt")

# 模型在前向传播时,会自动将我们训练好的前缀矩阵拼接到每一层的 K 和 V 之前
with torch.no_grad():
    outputs = peft_model.generate(**inputs, max_new_tokens=20)

print(tokenizer.decode(outputs[0], skip_special_tokens=True))

Lora原理与微调实战

Lora原理介绍

核心痛点与突破

在传统的全量微调中,模型在学习新任务时需要更新所有庞大的权重矩阵。而 LoRA (Low-Rank Adaptation): 这是目前工业界最火热、应用最广的方法。它冻结了原有 Transformer 层的权重矩阵,而是在其旁路注入两个低秩矩阵(降维矩阵 $A$ 和升维矩阵 $B$)。

底层数学逻辑

假设原始预训练的线性层权重矩阵为 $W_0 \in \mathbb{R}^{d \times k}$。在全量微调中,我们会学习一个更新增量 $\Delta W$。LoRA 的研究团队发现,模型在适配特定下游任务时,权重的更新量其实具有“极低的内在秩”。

因此,LoRA 将原本巨大的更新矩阵 $\Delta W$ 强行分解为两个极小矩阵的乘积:$\Delta W = B \times A$。

其中,$A \in \mathbb{R}^{r \times k}$ 负责将高维特征降维,$B \in \mathbb{R}^{d \times r}$ 负责再将其升维。关键在于,这里的秩 $r$(Rank)被设置得非常小(通常远小于 $d$ 和 $k$)。

在前向传播时,输入特征 $x$ 会分别通过原本被冻结的大矩阵和旁路的小矩阵,然后相加:

$$h = W_0 x + \Delta W x = W_0 x + B A x$$

image-20260521133450055

推理零延迟的魔法

除了极大地节省了训练显存,LoRA 还有一个相比于 Prompt Tuning 等软提示方法独一无二的优势:训练时只更新这两个小矩阵,推理时再将它们与原矩阵合并,不增加任何推理延迟。

在训练结束后,我们可以直接通过矩阵加法($W_{new} = W_0 + B \times A$),将旁路学到的新知识永远地“烙印”在基础模型中,随后就可以像调用原始模型一样丝滑地进行推理。

LoRA 核心超参数解析

在使用 PEFT 库配置 LoRA 时,你会频繁遇到以下三个决定模型成败的超参数:

秩 (Rank, r)

它决定了旁路矩阵的参数容量。r 越大,模型能记住的特定领域细节越多,但同时会消耗更多显存并增加过拟合的风险。在文本分类等简单任务中,r 设为 8 即可;在写代码、复杂逻辑推理等任务中,工业界倾向于将其设置为 64 甚至 128。

缩放系数 (Alpha, $\alpha$)

LoRA 矩阵在与原输出相加时,并不是直接加,而是会乘以一个缩放因子 $\frac{\alpha}{r}$。这个系数控制着“新学到的知识”在整个模型决策中的话语权。一条黄金经验是:通常将 $\alpha$ 设置为 r 的 1 倍或 2 倍(例如 r=8 时,alpha 设为 16)。

目标模块 (Target Modules)

决定将降维升维的旁路矩阵挂载到神经网络的哪些层。对于 Transformer,最常挂载的是注意力机制中的 Query ($W_q$) 和 Value ($W_v$) 矩阵。在最新的实践中,为了最大化微调效果,人们越来越倾向于将全部线性层(包含 MLP 层)都作为目标模块。

Lora代码实战

在 Hugging Face 生态中,借助 PEFT 库,只需要增加简单的几行配置代码,就能让原生大模型瞬间获得 LoRA 能力。

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model, TaskType

# Step 1: 加载冻结的基础模型与分词器 (以轻量级 GPT-2 为例)
model_name_or_path = "gpt2" 
tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)
base_model = AutoModelForCausalLM.from_pretrained(model_name_or_path)

if tokenizer.pad_token_id is None:
    tokenizer.pad_token_id = tokenizer.eos_token_id

# Step 2: 核心 —— 实例化 LoRA 配置类
lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM, # 任务类型
    r=8,                          # 低秩矩阵的秩
    lora_alpha=16,                # 缩放系数
    lora_dropout=0.05,            # 防止过拟合的丢弃率
    target_modules=["c_attn"]     # GPT-2 的注意力机制层名称 
    # 注: 如果微调 Llama/Qwen 等现代大模型,通常写成 ["q_proj", "k_proj", "v_proj", "o_proj"]
)

# Step 3: 魔法时刻 —— 将基础模型转换为注入了旁路矩阵的 LoRA 模型
peft_model = get_peft_model(base_model, lora_config)

# Step 4: 打印可训练参数比例,验证显存压缩效果
peft_model.print_trainable_parameters()
# 预期输出将显示极低的可训练比例,例如: trainable params: 294,912 || all params: 124,734,720 || trainable%: 0.2364%

# Step 5: (训练结束后的可选操作) 将学到的 LoRA 权重永久合并入基础模型
# merged_model = peft_model.merge_and_unload()
# merged_model.save_pretrained("./my_merged_lora_model")

扩展知识:从 LoRA 到 QLoRA

当我们需要在消费级硬件上微调数百亿参数(如 70B)的大模型时,普通的 LoRA 依然会因为基础模型本身的加载体积而导致显存溢出。此时必须引入量化技术。

QLoRA: LoRA 的极限显存优化版。它将预训练基座模型量化为 4-bit 精度加载,然后配合 16-bit 精度(如 BF16)的 LoRA 矩阵进行微调。这使得在消费级显卡(如单张 24GB 显存)上微调数百亿参数的模型成为可能。QLoRA 的出现,彻底扫清了阻碍开发者探索大模型的硬件屏障。

IA3原理与微调实战

在探讨了 BitFit、Prompt Tuning 系列以及 LoRA 之后,今天我们将聚焦于一种更加极致、在少样本(Few-Shot)学习上表现极其惊艳的技术:IA3 (Infused Adapter by Inhibiting and Amplifying Inner Activations)。

IA3原理介绍

核心痛点与突破

虽然 LoRA 已经将微调参数量降到了极低的水平,但在模型规模动辄数百亿的今天,对于一些需要同时处理极多独立下游任务的场景,LoRA 矩阵的参数量仍然是一笔不小的开销。此外,学术界发现在少样本学习(Few-Shot ICL)场景下,直接改变模型的内部激活分布往往比做低秩加法更加有效。

IA3 是由 T-Few 团队提出的一种方法。它的核心思想是:不引入新的庞大权重矩阵,而是通过“抑制(Inhibiting)”和“放大(Amplifying)”模型内部的激活值(Activations)来进行微调。

底层数学逻辑

在 Transformer 架构中,IA3 主要针对三个关键部分进行深度干预:自注意力机制中的 Key(键)、Value(值),以及前馈神经网络(FFN)的中间隐藏层。

它为这三个位置分别引入了三个极小的可训练向量(Vector,注意是单维向量而不是矩阵):$l_k$、$l_v$ 和 $l_{ff}$。在计算时,这些向量会与原始的输出进行逐元素相乘(Element-wise Multiplication,通常用 $\odot$ 表示):

自注意力机制中:

$K_{new} = K \odot l_k$

$V_{new} = V \odot l_v$

(注:Query 保持不变)

前馈神经网络中:

$FFN(x) = ( \text{Activation}(x W_1) \odot l_{ff} ) W_2$

这种逐元素相乘的操作相当于对特征的某些维度进行缩放。如果向量中的某个值为 0,就相当于抑制了该维度的特征;如果大于 1,则放大了该维度的特征。

极致的轻量与推理零延迟

由于引入的仅仅是三个一维向量,IA3 的参数量比 LoRA 还要小一个数量级。同时,因为逐元素相乘在数学上等价于对原始权重矩阵的列进行线性缩放,所以在微调结束后,这些可训练向量可以完美地被融合(Merge)进原始的预训练权重矩阵中。这意味着在实际生产环境部署时,它不会增加哪怕一毫秒的额外推理延迟。

IA3代码实战

在 Hugging Face 的 PEFT 库中,实现 IA3 非常标准化。我们需要使用 IA3Config 类。配置 IA3 时,最关键的工程细节是要明确指出哪些模块是目标模块,并且要特别标明哪些模块属于前馈神经网络(FFN),因为 IA3 对 FFN 的缩放处理逻辑与注意力层截然不同。

下面是基于因果语言模型(如 Llama 架构,这里以 Llama 的常见层命名为例)的完整实战代码:

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import IA3Config, get_peft_model, TaskType

# Step 1: 加载基础模型与分词器
model_name_or_path = "meta-llama/Llama-2-7b-hf" 
tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)
base_model = AutoModelForCausalLM.from_pretrained(model_name_or_path)

if tokenizer.pad_token_id is None:
    tokenizer.pad_token_id = tokenizer.eos_token_id

# Step 2: 核心配置 - IA3 专属参数
# 注意:target_modules 需要根据你具体使用的基础模型的内部网络层名称进行修改
peft_config = IA3Config(
    task_type=TaskType.CAUSAL_LM,
    target_modules=["k_proj", "v_proj", "down_proj"], # Llama 模型中的 K、V 投影层以及 FFN 下采样层
    feedforward_modules=["down_proj"], # 工程避坑:必须明确指出 target_modules 中哪些是 FFN 层
)

# Step 3: 将基础模型转换为注入了可训练向量的 IA3 模型
peft_model = get_peft_model(base_model, peft_config)

# Step 4: 打印可训练参数比例,验证极致轻量化
peft_model.print_trainable_parameters()
# 预期输出将显示极低的可训练比例,例如大约 0.01%

# Step 5: (训练结束后的可选操作) 将学到的 IA3 向量永久合并入基础模型权重中
# merged_model = peft_model.merge_and_unload()
# merged_model.save_pretrained("./my_merged_ia3_model")

实战工程避坑指南: 在 IA3Config 中,feedforward_modules 的列表必须是 target_modules 的子集。也就是说,你必须先在 target_modules 里声明你要修改这个模块,然后再告诉系统“这个特定模块属于 FFN”。如果没有正确设置 feedforward_modules,模型在训练时可能会试图去缩放错误的张量维度,导致维度不匹配的严重报错。

PEFT进阶操作

当我们掌握了基础的 PEFT 训练流程后,在真实的工业级应用中,往往会遇到更复杂的工程场景:比如使用非主流的自定义模型、在同一个基座模型上挂载多个不同垂直任务的微调权重、或者在推理时动态比对底层模型与微调模型的效果。

下面我将为你详细拆解这些 PEFT 的进阶 API 操作,并为你扩展生产环境中必不可少的“权重合并”技术。

自定义模型适配

核心痛点

默认情况下,Hugging Face 的 PEFT 库自动内置了对主流大模型(如 Llama、Qwen、ChatGLM 等)的网络层映射关系。它能自动识别哪些层是注意力机制中的投影层,并自动将旁路矩阵注入进去。但是,如果你使用的是一个最新发布还没被官方支持的模型,或者是你自己使用 PyTorch 从头魔改的自定义模型,PEFT 就会因为找不到目标层而报错。

解决方案:

精准指定 target_modules 在初始化 LoraConfig 时,你需要显式地通过 target_modules 参数告诉 PEFT 库,你希望把适配器具体挂载到哪些命名的网络层上。

代码实战

from peft import LoraConfig, get_peft_model

# 假设你的自定义模型中,线性层被命名为 "custom_q_layer" 和 "custom_v_layer"
lora_config = LoraConfig(
    r=16,
    lora_alpha=32,
    # 方式一:精确指定模块名称的列表
    target_modules=["custom_q_layer", "custom_v_layer"], 

    # 方式二:使用正则表达式匹配(例如匹配所有以 proj 结尾的层)
    # target_modules=".*proj$",

    # 方式三:终极黑科技(匹配模型中所有的线性层)
    # target_modules="all-linear",

    lora_dropout=0.05,
    bias="none"
)

# 将配置注入你的自定义模型
peft_model = get_peft_model(custom_base_model, lora_config)

扩展心得:在现代大模型的高级微调中,将 target_modules 设置为 "all-linear"(即将适配器注入所有的线性层,不仅仅是注意力层,还包括前馈神经网络)已经成为了提升微调精度和知识吸收能力的行业标配。

多适配器加载与切换

核心痛点

在企业级 AI 平台中,我们可能会面对不同的垂直业务请求(例如:既需要大模型生成代码,又需要它做医疗诊断,还需要它进行法律文本摘要)。如果为每个任务都单独在显卡上部署一个几十 GB 的全量大模型,硬件成本将是极其高昂的。

解决方案:一基多挂

PEFT 的绝妙之处在于基座模型的权重是冻结且共享的。这意味着,我们可以在显存中只加载一份庞大的基座模型权重,然后同时加载多个不同任务的 LoRA 适配器(每个仅占几十 MB)。在推理时,可以根据用户的请求类型,通过极其轻量级的 API 瞬间动态切换激活的适配器。

代码实战

from transformers import AutoModelForCausalLM
from peft import PeftModel

# 1. 正常加载庞大的底层基座模型 (仅占用一份显存)
base_model = AutoModelForCausalLM.from_pretrained("base_model_path")

# 2. 加载第一个适配器(如代码生成 LoRA),并将其命名为 "coder"
model = PeftModel.from_pretrained(base_model, "./lora_coder", adapter_name="coder")

# 3. 在同一个基座上,动态追加加载第二个适配器(如医疗 LoRA),命名为 "medical"
model.load_adapter("./lora_medical", adapter_name="medical")

# --- 动态路由与推理阶段 ---

# 场景 A:系统识别到用户提问了代码问题,切换到 coder 适配器
model.set_adapter("coder")
output_code = model.generate(**code_inputs)

# 场景 B:系统识别到用户提问了医疗问题,瞬间切换到 medical 适配器
model.set_adapter("medical")
output_med = model.generate(**medical_inputs)

禁用适配器

核心痛点

在进行模型测试或 A/B 评估时,你可能想立即对比“挂载了 LoRA 的模型”与“原始未微调的基座模型”在同一个问题上的回答差异。如果你通过手动卸载适配器或者重新加载一次基座模型来实现,会极其浪费时间和 I/O 资源。

解决方案:上下文管理器

PEFT 提供了一个优雅的上下文管理器 disable_adapter()。在这个作用域内,模型的前向传播会暂时忽略所有的微调旁路权重,直接使用底层基座的原始权重进行计算。一旦出了这个作用域,适配器立刻恢复生效。

代码实战

# 正常状态:使用激活的 LoRA 适配器进行前向推理
print("--- 微调模型输出 ---")
lora_outputs = model.generate(**inputs)
print(tokenizer.decode(lora_outputs[0]))

# 隔离状态:临时禁用适配器,直接逼出基座模型的原始回答
print("--- 原始基座模型输出 ---")
with model.disable_adapter():
    base_outputs = model.generate(**inputs)
    print(tokenizer.decode(base_outputs[0]))

# 出了 with 语句块后,之前的 LoRA 适配器自动恢复工作

扩展知识:适配器权重合并 (Merge and Unload)

在训练和研发测试阶段,保持基座和 LoRA 权重的分离是非常方便的。但是,当你准备将模型正式部署到生产环境供高并发访问时,每次前向传播都需要同时经过基础矩阵和旁路小矩阵然后再相加,这会带来微小的推理延迟计算开销。

工业级部署标准操作: 利用矩阵分配律的数学特性,我们可以将训练好的 LoRA 权重永久性地加进基座模型的权重矩阵中。合并后,它就变成了一个原生结构的单体大模型,推理速度达到最优。

# 将挂载的 LoRA 权重永久融合进底层基座,并从显存中卸载独立的 PEFT 结构
merged_model = model.merge_and_unload()

# 将融合后的完整模型保存到本地,后续可以像加载原生开源模型一样直接加载它
merged_model.save_pretrained("./final_production_model")

注意:合并操作在物理层面上是不可逆的。执行合并后,该模型实例将变回普通的 Transformers 模型,你将无法再对它使用 set_adapterdisable_adapter 方法。

版权声明:除特殊说明,博客文章均为Mark原创,依据CC BY-SA 4.0许可证进行授权,转载请附上出处链接及本声明。VIP内容严禁转载! | 广告招租请留言
暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇