- SignalDesk2小时前
想要更好的阅读体验,欢迎到我的网站阅读: 《从零开始的OCR-0——基础篇[Fine-Tuning]》 。 从零开始的OCR-0——基础篇[Fine-Tuning] 学习目标 :理解 Pretraining 与 SFT 的区别 → 掌握 SFT 数据构造、Loss Mask 与训练流程 → 理解 Full Fine-Tuning 的参数更新及显存开销 → 掌握 LoRA、QLoRA 的原理与实现 → 了解 DoRA、AdaLoRA 等改进 → 手撕 LoRA + 简单 SFT 训练流程。 难度 : 重要性 : 昔日修行,皆为参悟前人遗留的万千法则。然而纵有通天之术,亦未必契合自身之道。如今,以微末之力重塑经脉,以寥寥参数撬动万千神通……无需推倒重来,亦可破境新生。 Fine-Tuning等概念是什么? 简单一点,就是模型已经有了一定的通用能力。但是我们想要他在某一个“特有领域”上发挥的更好,就需要用“特定领域”的数据进行 Fine-Tuning,以让模型适应这个“特有领域”。 例如:模型在训练后,通常需要指令微调,前面几章也提到了这一点,实际上模型通过这样的数据继续优化,就可以逐渐学习如何根据指令生成符合要求的回答。 例如:大模型在通用领域表现得还可以,但是在公司里面,他没有公司的内部资料,那么我们就可以利用内部资料构建数据集,让模型微调后,就可以更好的回答。 所以 Fine-Tuning 是一个很广泛的概念,其实就是让预训练大模型继续训练,以适应新的任务、数据分布或行为要求而已。 SFT(Supervised Fine-Tuning) 指:利用具有明确监督目标的数据训练模型,例如指令与标准回答。 Full Fine-Tuning 描述参数更新范围。它指训练时更新目标模型的全部参数,而不是只更新其中一小部分。 LoRA / QLoRA 等 PEFT 方法 同样描述参数更新与资源优化方式,通常冻结原始主权重,只训练少量额外参数。 SFT 如何训练? SFT 中,数据是具有监督信号的,我们以指令微调作为例子。一般的数据集就是 \{X,Y\} ,其中 X 表示输入的文本, Y 表示答案,例如: { "messages": [ { "role": "user", "content": "2 + 2 等于多少?" //这是 X }, { "role": "assistant", "content": "4。" //这是 Y } ] } 最后也是一样,做 CE Loss 即可(但是需要注意,这个 Loss 要根据不同的任务进行设计)。其他流程基本上和 Pretraining 的时候一致。 其中 Loss 的只能监督答案,所以要加上 Loss Mask: # Prompt 和正确答案的 Token IDs input_ids = prompt_ids + answer_ids # 只监督答案,不监督 Prompt labels = [-100] len(prompt_ids) + answer_ids Full Fine-Tuning 训练的时候还会有一个问题,假设我们使用的是一个 7B 参数的 LLM,那么它的 70 亿个参数是否都需要更新?如果全部更新,就是 Full Fine-Tuning。如果只选择一部分参数进行训练,或者额外引入少量可训练参数,就是另一类微调策略。 假设有一个已经预训练好的 Transformer,其中就会有 QKV 的矩阵,FFN 层的 W_{up},W_{down} ,等等的参数。Full Fine-Tuning 的意思就是允许这些模型参数在目标任务训练过程中继续更新。 这种微调的最大问题就是需要极其大量的显存: 假设有一个 7B 参数的模型,采用 BF16存储,每个参数就是要占用 2 Bytes,那么模型就需要 710^92 = 14G ,接下来计算保存梯度、优化器状态,以及反向传播需要的中间激活。 梯度保存:由于 Full Fine-Tuning 允许更新全部参数,在一种常见的 BF16 训练配置中,梯度同样使用 BF16 存储,所以又需要 14G 优化器状态:假设我们使用 AdamW,会为每一个参数维护一阶动量和二阶动量估计,如果使用FP32保存,那么每个参数会对应两份 4 Bytes 的状态,就需要56G。 LoRA 伟大的技术,使我的显存旋转 假设现在有一个 Linear 层,变化为: Y=XW^T ,我们进行 Full Fine-Tuning 的时候,就是把 W 进行参数的调整变成了 W' ,那么其实我们也可以看成是 W 增加了一个量 \Delta W ,满足$W’ = W + \Delta W$。此时我们完全可以是冻结原来的 W ,只更新 \Delta W ,那么也就是 Y = X (W+\Delta W)^T 。 但是这样有啥用呢??即使不更新 W ,这里的 \Delta W 的大小和 W 一样呀,更新它没用呀?? 那么我们考虑,是否可以用更少的参数来表示这个矩阵。 我们可以得到 对于一个 H \times W 的矩阵 \Delta W ,存在一种分解方案,使得 \Delta W = BA, \quad B\in \mathbb{R}^{H\times r},\quad A\in \mathbb{R}^{r\times W} 。这里的 r 理论上来说可以取1,2,3都行,但是实际上会影响参数量,以及可表示的矩阵变化,这里也是一个 trade-off。当 r 继续增加时,可以表示的矩阵变化也会更加丰富,但可训练参数量随之增加。 因此完整的分解就是: Y=XW^T+ \frac{\alpha}{r}(XA^T)B^T 这里前面其中 r 是 Rank, \alpha 是 LoRA Scaling Hyperparameter 后面会说 为什么可以这么分解? 这其实是 LoRA 论文最重要的研究假设。作者提出一个猜想,模型在适应特定下游任务时,所需的权重更新可能只集中在 相对低维的方向上 ,而不一定需要任意自由地修改全部权重。 矩阵 A 和 B 的初始化 这里不能随意的初始化,因为我们希望一开始初始化后不破坏原来矩阵的性质,也就是初始化时 BA = 0 ,所以我们采用: A\sim\mathcal N(0,\sigma^2),\qquad B=0 。 不能二者都为 0,不然这样梯度回传不到他们身上。 为什么需要缩放系数? 原因是,改变 Rank 时,LoRA 分支的参数规模、数值性质和优化行为也会改变。 例如,我们从 Rank=8 增加到 Rank=64,虽然 A、B 仍然能够正常训练,但 LoRA 更新分支的尺度可能随配置变化。 因此,经典 LoRA 引入 s=\frac{\alpha}{r} 用于调节 LoRA 分支对最终输出的贡献,并减少调整 Rank 时重新搜索某些超参数的需求。这也是原论文给出的设计动机。 QLoRA 虽然 LoRA 大幅减少了可训练参数,但原始的 7B、13B 甚至更大的模型权重,仍然需要保存在内存中。如果只剩一张显存有限的 GPU,能不能进一步降低这些冻结权重的存储成本? 这里就需要用到 从零开始的OCR-0——基础篇[大模型工程基础] 的量化知识了。 前面说到,LoRA 的权重更新如下面公式所示, W'=W_0+\frac{\alpha}{r}BA 实际上 W_0 矩阵是完全不更新的,那么我们能不能进一步将它们量化到 4-bit,从而降低显存占用?这就是 QLoRA 的思想 QLoRA 的整体架构 QLoRA 并没有重新设计 LoRA 的低秩更新方式,而是将冻结的预训练权重量化到 4-bit 存储,再通过较高精度的 LoRA 参数进行微调。 这里的 NF4 是普通 4-bit 量化的进化版本。 NF4 假设我们需要量化这样一组数: W=[-0.8,-0.2,-0.1,0,0.1,0.2,0.8] 一种简单的方法就是均匀量化。但是我们发现其实数值分布并不均匀,如果采用均匀分布,就会导致浪费,密集的中心区域也会导致获取的量化等级数量比较有限。 NF4 的设计动机就是 在权重经常出现的区域分配更合适的离散值,从而降低量化带来的信息损失。 QLoRA 论文观察并利用了 预训练模型权重分布常常近似正态分布 的特点,设计了针对正态分布的非均匀量化表示。 假设权重近似满足 W\sim\mathcal N(0,\sigma^2) NF4 根据正态分布的分位数构造 16 个代表值,并保留零值。这些代表值构成一个 Codebook(码本)。 整个过程可以理解为 W \xrightarrow{\text{NF4 Quantization}} \text{4-bit Codes} \xrightarrow{\text{Codebook + Scale}} \hat W Double Quantization 现在已经可以把原始的权重保存为 NF4 编码,但是我们不仅要保存原始权重,还需要保存 Scale 等量化参数用于恢复。实际在大模型中,通常整个模型不会只用一组 Scale,这些存储的 Scale 也会占用显存。就假设 64 个 NF4 权重使用一个 FP32 的 Scale ,那么平均还需要 0.5bit/param 的存储成本。Double Quantization 就是用来压缩这一部分的存储成本的。 例如: W \xrightarrow{\text{第一次量化}} (Q_W,S) 这里 Q_W 是权重的低比特编码, S 是 Scale。 然后继续: S \xrightarrow{\text{第二次量化}} (Q_S,S_2) 其中 Q_S 是 Scale 的低比特表示, S_2 是恢复这些 Scale 所需要的第二级量化参数。 原论文使用了一种配置:第一次量化每组 64 个权重,第二次使用 8-bit 量化 Scale,并以 256 个 Scale 为一个更大的分组。大约可以压缩到 0.13 bit/param. Paged Optimizers 训练过程中,显存占用可能是波动式的,一会大一会小。Paged Optimizers 利用 NVIDIA Unified Memory(统一内存)机制,在需要时让部分优化器状态在 GPU 显存与 CPU 内存之间进行页面迁移,从而缓解显存峰值带来的 OOM 问题。 正常情况下,CPU RAM 和 GPU VRAM 是不同的物理存储空间。但是使用统一内存机制后,可以通过 CUDA 的内存管理机制,让 CPU 和 GPU 使用统一的虚拟地址空间访问托管数据。这样在 Forward/Backward 显存紧张时,就可以将部分 AdamW 状态移到 CPU 内存,给激活腾地方。等 Optimizer Step 真正需要它们时,再移回来。 灾难性遗忘 指模型在学习新任务时,原本已经掌握的部分知识或能力出现明显退化。 一般来说,微调的时候,模型在新任务上表现会显著提升,但是原来的任务上可能就会下降。这是因为模型的参数被多个任务共同使用,我们在一个新的任务上进行微调,只保证在这个任务上的 Loss 降低,即参数的调整是为了这个任务的性能提升,而其他任务就不再保证了。 常见方法大概有三个维度: 数据层面:混入一定比例的通用数据或旧任务数据。这类方法与 Replay(经验回放)的思想有关。 参数层面:通过冻结部分参数、使用 LoRA 等方法限制更新范围,减少对原有参数的直接修改。但限制更新不保证一定消除遗忘。 优化层面:控制 Learning Rate、训练步数和更新强度,并在微调期间持续评测新旧任务,避免只关注新任务的训练 Loss。 多模态模型微调 因为多了 Vision Encoder 以及中间的一些部件(比如 Q-Former, Projector 等),那我们想让 VLM 适应一个新任务,应该更新什么地方? 这里没有唯一答案,这里提供三种典型方案。 方案 VisionEncoder Projector LLM 适用情况 A:视觉语言对齐 冻结 训练 冻结 新建连接层、学习视觉到语言的映射 B:多模态指令微调 冻结 训练或继续训练 LoRA/FullFT 视觉特征较好,需要适应新任务 C:视觉与语言联合适配 部分微调或LoRA 训练 LoRA/FullFT 视觉域变化较大,需要修改视觉表示 手撕 LoRA class LoRALinear(nn.Module): def __init__(self, base_layer, r=8, alpha=16): super().__init__() self.base = base_layer self.r = r self.scaling = alpha / r d_in = base_layer.in_features d_out = base_layer.out_features # 冻结预训练 Linear for p in self.base.parameters(): p.requires_grad = False # A: [r, d_in] self.A = nn.Parameter(torch.empty(r, d_in)) # B: [d_out, r] self.B = nn.Parameter(torch.zeros(d_out, r)) # A 随机初始化,B 初始化为 0 nn.init.kaiming_uniform_( self.A, a=math.sqrt(5) ) def forward(self, x): # 原始分支 base_output = self.base(x) # LoRA 分支 # [B, N, d_in] -> [B, N, r] h = F.linear(x, self.A) # [B, N, r] -> [B, N, d_out] delta = F.linear(h, self.B) return base_output + self.scaling delta def merged_weight(self): # [d_out, d_in] return ( self.base.weight + self.scaling * (self.B @ self.A) ) 1 个帖子 - 1 位参与者 阅读完整话题
- 情报分类:硬件与数码
- 分类依据:内容涉及硬件、数码产品或通信卡
- 信息来源:服务器 / LINUX DO - 最新话题
- 发布时间:2026/10/18 21:11:29
- 暂无回复