在模型微调这件事上,学术界一直在试图找到一个平衡点——既要足够灵活,又要尽量节省参数。这也就解释了为什么像LoRA这类方法会受到如此多的关注。但即便LoRA已经足够优秀,它仍然存在一些结构性上的局限。而DoRA的出现,一定程度上补上了这个缺口。

DoRA的全称是Weight-Decomposed Low-Rank Adaptation。它最大的突破点在于:把预训练权重拆成了“幅度”和“方向”两个独立的部分,然后只对方向部分施加LoRA更新,幅度则单独调整。这样一来,模型在微调时的学习过程就被拆解得更精细——就像调节音量(幅度)和调音色(方向)分成了两个旋钮,可以分别微调。
LoRA回顾
要理解DoRA,先得清楚LoRA到底做了什么。简单来说,LoRA就是用一个低秩分解来近似大矩阵的更新——把一个大权重矩阵拆成两个小矩阵的乘积。
看段代码会更直观:
import torch
import torch.nn as nn
import torch.nn.functional as F
class LoRALayer(nn.Module):
def __init__(self, in_dim, out_dim, rank, alpha):
super().__init__()
std_dev = 1 / torch.sqrt(torch.tensor(rank).float())
self.A = nn.Parameter(torch.randn(in_dim, rank) * std_dev)
self.B = nn.Parameter(torch.zeros(rank, out_dim))
self.alpha = alpha
def forward(self, x):
x = self.alpha * torch.matmul(torch.matmul(x, self.A), self.B)
return x
class LinearWithLoRAMerged(nn.Module):
def __init__(self, linear, rank, alpha):
super().__init__()
self.linear = linear
self.lora = LoRALayer(linear.in_features, linear.out_features, rank, alpha)
def forward(self, x):
lora = torch.matmul(self.lora.A, self.lora.B)
combined_weight = self.linear.weight + self.lora.alpha * lora.T
return F.linear(x, combined_weight, self.linear.bias)
看上去很简洁对吧?但就是这种“拆小再合”的思路,大幅降低了全量微调的计算成本。
DoRA
DoRA的核心组成可以拆成以下几个层次:
权重分解:将每个预训练权重矩阵先分解成幅度向量(magnitude)和方向矩阵(direction)。这里的关键在于,分解后的两个组件在微调时可以各司其职。
幅度和方向分别调整:幅度决定权重整体的大小,方向决定权重的走向。DoRA对这两部分独立调整,互不干扰。
方向更新用LoRA:在更新方向部分时,DoRA沿用了LoRA的低秩适配手段,只更新方向相关的参数。
混合权重构建:最终的权重由调整后的幅度m和经过LoRA更新的方向V组合而成:W' = m * (V + ∆V) / ||V + ∆V||c。
模块选择策略:在微调过程中,DoRA并不是对所有模块都一视同仁。比如对QKV模块,它同时调整幅度和方向;而对MLP等线性层,则只微调幅度。这种粒度控制,既提升了性能,又减少了参与训练的参数数量。
综合来看,DoRA通过权重分解和定向低秩适配,在保持全量微调学习能力的同时,还做到了参数效率的优化。从NLP到跨模态任务,它的表现普遍优于传统LoRA。
具体实现上,代码也不复杂:
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(0)
class DoRALayer(nn.Module):
def __init__(self, d_in, d_out, rank=4, weight=None, bias=None):
super().__init__()
if weight is not None:
self.weight = nn.Parameter(weight, requires_grad=False)
else:
self.weight = nn.Parameter(torch.Tensor(d_out, d_in), requires_grad=False)
if bias is not None:
self.bias = nn.Parameter(bias, requires_grad=False)
else:
self.bias = nn.Parameter(torch.Tensor(d_out), requires_grad=False)
self.m = nn.Parameter(self.weight.norm(p=2, dim=0, keepdim=True))
std_dev = 1 / torch.sqrt(torch.tensor(rank).float())
self.lora_A = nn.Parameter(torch.randn(d_out, rank) * std_dev)
self.lora_B = nn.Parameter(torch.zeros(rank, d_in))
def forward(self, x):
lora = torch.matmul(self.lora_A, self.lora_B)
adapted = self.weight + lora
column_norm = adapted.norm(p=2, dim=0, keepdim=True)
norm_adapted = adapted / column_norm
calc_weights = self.m * norm_adapted
return F.linear(x, calc_weights, self.bias)
DoRA相比LoRA的优势
聊完原理,我们来看看DoRA在实际表现中到底比LoRA好在哪里。根据已有的实验数据,在常识推理、视觉指令调整、图像/视频文本理解等下游任务中,微调LLaMA、LLaVA、VL-BART等模型时,DoRA几乎全面优于LoRA。
具体优势可以归纳为四点:
更精准的控制:把权重拆成幅度和方向后,模型可以针对任务需求做更细颗粒度的适配。
学习能力更强:这种分解策略放大了模型在微调阶段的可塑性,无论是小模型还是大模型,DoRA的表现都更接近全参数微调。
维持高效性:虽然策略上做了创新,但并没有带来额外的推理成本——本质还是低秩更新的路子。
训练更稳定:通过将权重分解并单独对方向做低秩适应,DoRA有效抑制了过拟合和训练过程的不稳定因素。
说到底,DoRA并不是碘伏性的方法论创新,而是在LoRA这条被验证过的路径上做了一个非常巧妙的结构优化。正是这种“再精细一点”的思维,让它在兼顾效率的同时,把性能又往前推了一步。
