PEFT documentation

OSF(正交子空间微调)

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v0.21.0).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

OSF(正交子空间微调)

正交子空间微调(OSF)是一种专为持续学习设计的 PEFT 方法,它通过约束参数更新方向与先前重要方向保持正交来实现这一目标。该方法支持全量微调,同时无需引入额外参数或存储历史梯度即可防止灾难性遗忘。

该论文的摘要如下:

大语言模型(LLM)中的持续学习容易引发灾难性遗忘,即在适应新任务时,模型在先前已学习任务上的性能会显著下降。现有方法通常依赖低秩、参数高效的更新,这限制了模型的表现力,并为每个任务引入额外参数,从而导致可扩展性问题。为克服这些局限,我们提出了一种新颖的持续全量微调方法,利用自适应奇异值分解(SVD)。我们的方法动态识别任务特定的低秩参数子空间,并约束更新方向与先前任务相关的关键方向正交,从而在不增加额外参数开销或存储先前任务梯度的情况下,有效最小化任务间的干扰。我们在标准的持续学习基准上,使用编码器-解码器(T5-Large)和仅解码器(LLaMA-2 7B)模型,对包括分类、生成和推理在内的多种任务进行了广泛评估。实验表明,我们的方法取得了最先进的结果,平均准确率比 O-LoRA 等近期基线方法高出最多 7%,并且通过将遗忘降低到近乎可忽略的水平,在整个持续学习过程中显著保持了模型的通用语言能力、指令遵循准确率以及安全性。我们的自适应 SVD 框架有效平衡了模型的可塑性与知识保留,为大语言模型中的持续学习场景提供了一种实用、有理论依据且计算可扩展的解决方案。

OSF 的工作原理

OSF 使用 SVD 将每个权重矩阵分解为高秩(冻结)和低秩(可训练)分量:

W = U_high * S_high * V_high^T + U_low * S_low * V_low^T

其中:

  • U_high, S_high, V_high:保留来自先前任务的重要方向(冻结)
  • U_low, S_low, V_low:允许适应新任务(可训练)

在训练过程中,梯度被投影为与高秩子空间正交,从而确保更新不会干扰先前学到的知识。

基本用法

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import OSFConfig, get_peft_model

# Load base model
model = AutoModelForCausalLM.from_pretrained("facebook/opt-125m")

# Configure OSF
config = OSFConfig(
    target_modules=["q_proj", "v_proj"],  # Target attention layers
    effective_rank=8,                     # Default rank for decomposition
    rank_pattern={"q_proj": 16}           # Override rank for specific modules
)

# Apply OSF
model = get_peft_model(model, config)

# Train as usual
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)

tokenizer = AutoTokenizer.from_pretrained("facebook/opt-125m")

inputs = tokenizer("Hello world", return_tensors="pt", padding=True)
loss = model(**inputs, labels=inputs.input_ids).loss
loss.backward()
optimizer.step()
optimizer.zero_grad()

配置选项

目标模块

您可以通过以下几种方式指定目标模块:

# Specific module names
config = OSFConfig(target_modules=["q_proj", "k_proj", "v_proj", "o_proj"])

# All linear layers
config = OSFConfig(target_modules="all-linear")

# Model-specific defaults (automatically detected)
config = OSFConfig()  # Uses model-appropriate defaults

有效秩配置

控制保留/可训练的子空间:

# Global preserved rank (applies to all target modules)
config = OSFConfig(effective_rank=16)  # preserves top-16 singular directions; trains the rest

# Automatic preserved rank (50% of the smaller matrix dimension per target)
config = OSFConfig(effective_rank=None)

# Per-module preserved-rank overrides
config = OSFConfig(
    effective_rank=8,
    rank_pattern={
        "q_proj": 16,      # Higher rank for query projection
        "gate_proj": 4     # Lower rank for gate projection
    }
)

# Fractional preserved rank is supported (interpreted per-target as fraction * min_dim)
config = OSFConfig(effective_rank=0.8)  # preserve 80% of min_dim; train remaining 20%
config = OSFConfig(rank_pattern={"q_proj": 0.5})  # preserve 50% on q_proj, others use global/default

注意:OSF 的 effective_rank 是保留(冻结)的秩,而非可训练的秩。可训练的秩等于 min(weight.shape) - effective_rank。这与 LoRA 的 r 不同,后者直接指定可训练的秩。

持续学习的训练建议

顺序任务学习

OSF 专为顺序学习任务而设计。在任务之间,重新计算 SVD,使保留的子空间反映最新的权重。一种简单的方法是再次使用 OSF 重新包装更新后的基础模型:

# Task 1: train on domain A with initial preserved subspace
r = 8  # initial effective rank to preserve
model = get_peft_model(base_model, OSFConfig(effective_rank=r))
train_task(model, task_1_data)

# Task 2: recompute SVD on updated weights and increase preserved subspace
base_model = model.unload()  # unwrap base model without assuming internals
r += 4  # grow preserved subspace to include Task 1 knowledge
model = get_peft_model(base_model, OSFConfig(effective_rank=r))
train_task(model, task_2_data)

# Task 3: recompute again and expand preserved subspace further
base_model = model.unload()
r += 4
model = get_peft_model(base_model, OSFConfig(effective_rank=r))
train_task(model, task_3_data)

任务序列的预算分配

在针对已知的 n 个任务序列进行训练时,一种有效的策略是逐步分配模型容量,以平衡学习新任务与保留先前知识:

  • 任务 1:使用全部容量(训练所有参数)
  • 任务 2:冻结 1/n 的模型容量,训练剩余的 (n-1)/n 容量
  • 任务 3:冻结模型容量的 2/n,训练剩余的 (n-2)/n 容量
  • 任务 n:冻结模型容量的 (n-1)/n,使用 1/n 的容量用于最终任务

这种方法确保每个任务都能获得足够的学习能力,同时逐步保留更多来自先前任务的知识。

# Example: 4-task sequence with progressive budget allocation
n_tasks = 4
max_preserved_rank = 512  # Upper bound for preserved rank per target (heuristic)

for task_id in range(n_tasks):
    # Freeze increases over time; trainable capacity shrinks
    preserved_fraction = (task_id + 1) / n_tasks
    preserved_rank = int(max_preserved_rank * preserved_fraction)

    config = OSFConfig(
        target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
        effective_rank=preserved_rank,
    )

    print(
        f"Task {task_id + 1}: Preserving rank {preserved_rank} "
        f"({preserved_fraction:.1%} of max_preserved_rank - {max_preserved_rank} frozen); trainable rank = min_dim - preserved_rank"
    )

    model = get_peft_model(base_model, config)
    train_task(model, task_data[task_id])

最佳实践

  1. 有效秩选择:从 effective_rank=None 开始(自动将秩设置为每个目标模块中较小权重维度的 50%),并根据任务复杂度进行调整
  2. 学习率:与标准微调相比,使用更小的学习率(1e-5 到 1e-4)
  3. 任务重要性:使用 rank_pattern 为关键模块分配更多容量
  4. 模型架构:OSF 在具有清晰注意力机制和 MLP 分离的 Transformer 架构上表现最佳
  5. 容量规划:对于已知的任务序列,使用渐进式预算分配(1/n、2/n、…、(n-1)/n 冻结)来平衡可塑性与稳定性

内存注意事项

OSF 会就地修改权重,并且不会添加参数,因此具有内存效率:

# Memory usage remains close to base model
print(f"Base model parameters: {base_model.num_parameters():,}")
print(f"OSF model parameters: {osf_model.num_parameters():,}")  # Similar count

高级用法

自定义目标模块

对于具有非标准架构的模型:

config = OSFConfig(
    target_modules=["dense", "intermediate.dense"],  # Custom layer names
    effective_rank=12,
    rank_pattern={"dense": 8, "intermediate.dense": 16}
)

与其他方法的集成

OSF 可以与其他技术结合使用:

# Use with gradient checkpointing for memory efficiency
# note: use `model.gradient_checkpointing_enable()` for older Transformers versions
model.activation_checkpointing_enable()

# Apply weight decay selectively (regularizes low-rank factors to limit drift/overfitting in continual updates; keep small)
optimizer = torch.optim.AdamW([
    {"params": [p for n, p in model.named_parameters() if "U_low" in n], "weight_decay": 0.01},
    {"params": [p for n, p in model.named_parameters() if "S_low" in n], "weight_decay": 0.001},
    {"params": [p for n, p in model.named_parameters() if "V_low" in n], "weight_decay": 0.01},
], lr=1e-4)

基准测试概览

API

OSFConfig

类 peft.OSFConfig

< >

( task_type: Optional[Union[str, TaskType]] = Nonepeft_type: Optional[Union[str, PeftType]] = Noneauto_mapping: Optional[dict] = Nonepeft_version: Optional[str] = Nonebase_model_name_or_path: Optional[str] = Nonerevision: Optional[str] = Noneinference_mode: bool = Falseeffective_rank: Optional[Union[int, float]] = Nonetarget_modules: Optional[Union[list[str], str]] = Nonerank_pattern: Optional[dict[str, Union[int, float]]] = Noneinit_weights: Optional[bool] = Truemodules_to_save: Optional[list[str]] = Nonetarget_svd_config: Optional[dict[str, int]] = None )

参数

  • effective_rank (int 或 float,可选) — 保留的 SVD 秩(“高”子空间)。前 effective_rank 个奇异方向将被冻结,并在任务间保留;其余维度构成可训练的低秩子空间。如果为 None,则默认为每个目标模块中较小权重维度的 50%。注意:这与 LoRA 的 r(可训练秩)不同。在 OSF 中,可训练秩为 min(weight.shape) - effective_rank。
  • target_modules (Union[list[str], str], 可选) — 要应用 OSF 的模块名称。可以是模块名称列表或 “all-linear”。
  • rank_pattern (dict[str, int|float], 可选) — 一个正则表达式模式字典,用于覆盖特定模块的 effective_rank。值可以是绝对整数,也可以是 (0, 1] 区间内的分数,后者被解释为每个目标较小矩阵维度的比例。

正交子空间微调(OSF)的配置。

OSFModel

类 peft.OSFModel

< >

( modelconfigadapter_namelow_cpu_mem_usage: bool = Falsestate_dict: dict[str, torch.Tensor] | None = None )

一个实现了正交子空间微调的最小化调优器。

实用函数

权重分解

peft.tuners.osf.utils.decompose_weight_matrix

< >

( weight: torch.Tensortop_k: int )

对 weight 执行 SVD,并将其拆分为冻结部分和可训练部分。

梯度投影

peft.tuners.osf.utils.project_gradient_to_orthogonal_space

< >

( svd_dict: dict[str, Any] )

将U_low和V_low的梯度投影到与高秩空间正交的方向上。

Update on GitHub