LoRA 不动权重,只加一条旁路
一句话总结
LoRA 把训练好的权重 W 冻结起来,在它旁边挂上两个小矩阵(A:输入→r,B:r→输出),只训练 B·A。B 从 0 开始,所以一开始模型丝毫不变,训练结束后合并成 W + B·A,就可以不带旁支地使用。MiniMind 的 model_lora.py 用 60 行实现了它。这个模块会在 SFT 模型上叠加 LoRA 来改变口吻,并亲自确认 基础权重是否一个比特都没有变。
为什么需要它
LoRA 的论文以 GPT-3 175B 为例——如果每个任务都单独保留一份全量微调的模型,一个任务就需要 1,750 亿个参数。冻结预训练权重、只训练低秩矩阵,要训练的参数会减少到万分之一,GPU 内存减少到三分之一,而且与适配器不同,不会增加推理延迟。MiniMind 的 README 也举了同样的用法——保留基础模型的通用能力,把医疗之类的领域或“我是谁”这类自我认知,用 LoRA 叠加上去。数据足够的话,全量 SFT 也可以,但那时需要另外做一件事:混入别的数据,避免过拟合到领域数据而丢失通用能力。
工作原理
MiniMind 的实现分三个部分。
class LoRA(nn.Module):
def __init__(self, in_features, out_features, rank):
self.A = nn.Linear(in_features, rank, bias=False) # 정규분포(std 0.02)
self.B = nn.Linear(rank, out_features, bias=False) # 0
def forward(self, x): return self.B(self.A(x))
def apply_lora(model, rank=16):
for name, module in model.named_modules():
if isinstance(module, nn.Linear) and module.in_features == module.out_features:
module.lora = LoRA(...); module.forward = 원래 forward(x) + lora(x)
挂在哪里。只挂在输入和输出维度 相同 的 Linear 上。在我们的模型(128 维,Q 头 4×32)里,q_proj(128→128)和 o_proj(128→128)符合条件,而 k_proj、v_proj(128→64)和 FFN(128↔448)不符合。4 层 × 2 = 8 处,每处 A(8×128)+B(128×8) = 2,048 个,所以要训练的参数是 16,384 个,占全部的 1.6%。论文另外讨论了该把它挂到哪个矩阵上,而 MiniMind 用这条简单的规则做了决定。
起点。B 是 0,所以 B·A = 0。叠加之后的输出,与基础模型完全相同。训练是从 0 开始学习“该偏离基础模型多少”。
倍率。论文把 ΔW·x 乘以 α/r,使得改变 rank 时不必重新选学习率。MiniMind 的实现不带这个倍率,直接相加——所以改变 rank 时,学习率也得重新看。
冻结与保存。train_lora.py 把名称里没有 lora 的参数全都设为 requires_grad=False,只把 LoRA 参数交给优化器。保存时由 save_lora 只把 LoRA 权重以 fp16 单独取出来保存——只有几十 KB。使用时,在基础模型上 apply_lora 之后 load_lora,或者用 merge_lora(W + B·A)合并成一个模型。
在现场相遇的样子
如果给一个基础模型,为每个客户、每个任务各配一份不同的 LoRA,存储就是一个基础模型加上几个小文件,服务时只要按请求替换 LoRA 即可。这时如果基础权重被悄悄改掉,所有的 LoRA 都会同时出错——一个漏掉了冻结的训练脚本就能造成这种局面。所以训练结束后,把基础权重与原件逐比特比较,是便宜又可靠的确认方法。反过来,如果只用于一种用途,就合并(merge)之后部署,消除推理时旁支的计算。
本课程与 MiniMind 原版的不同之处
MiniMind 的默认 rank 是 16,train_lora.py 以 lr 1e-4 训练 10 个轮次。这门课程用 rank 8、lr 5e-3、150 步。模型小、任务(一种口吻)简单,rank 减小也够用,并且为了在很短时间内看到效果,把学习率调大了。由于这个实现没有 alpha 倍率,rank 和学习率是联动的——rank 加倍,B·A 的大小也随之变大,同样的学习率实际上就成了更大的步幅。另外,MiniMind 默认把 LoRA 叠加在 SFT 模型(full_sft)之上。如果直接叠在预训练模型上,得先学聊天格式,仅靠 16,384 个参数是不够的。
下一项实验要做什么
在 SFT 基准模型上叠加 rank 8 的 LoRA,确认它挂在哪里,以及叠加之后输出是否原样不变。把模型冻结成只训练 LoRA,用猫咪口吻的数据训练 150 步,然后测量基础权重是否保持原样、在留出的问题上口吻变了多少、合并后的模型是否与叠加了 LoRA 的模型给出相同的输出。