1. 从一次微调翻车说起:为什么我要死磕 AKG 分解
去年秋天,我接手了一个垂直领域的 LLM 微调任务。数据量不大,六万条左右,基座选的是 7B 级别的开源模型,硬件是单机八卡。按常规套路,LoRA 一挂,rank 设 16,学习率 2e-4,跑个三五轮就该收工了。结果第一轮 loss 曲线就把我看懵了——前 200 步下降得很漂亮,之后开始剧烈震荡,验证集指标不升反降,到第 800 步直接崩成了随机输出。
我一开始怀疑是数据清洗没做干净,回头查了一遍,没问题。又怀疑是学习率太大,降到 5e-5 重跑,震荡幅度小了点,但收敛依然很差。换了个基座模型,情况类似。那几天我几乎把能调的超参都试了一遍,效果都不理想。
后来我把训练过程中每一层的梯度范数打出来看,才发现问题所在:某些注意力层的权重更新矩阵,其奇异值分布极度不均衡,少数几个方向上的更新量比其余方向大了两三个数量级。换句话说,梯度在参数空间里被“挤压”到了极少数方向上,模型实际上只在那几个方向上学习,其余方向要么停滞,要么被噪声主导。这就是典型的病态更新问题。
要理解并解决这个问题,就得回到矩阵分解这个老话题上。而 AKG 分解,正是我在排查过程中重新捡起来的一个工具。它不是什么新潮概念,但在 LLM 微调的 dynamics 分析里,出奇地好用。
这篇文章,我想把这段踩坑经历完整地拆开讲。LLM 微调的动力学到底是怎么回事,AKG 分解又是什么,它怎么帮我们看清微调过程中参数更新的“形状”,以及怎么用它来指导实际的超参选择和训练策略调整。适合正在做大模型微调、被 loss 震荡和收敛问题困扰的同行参考,也适合对矩阵特征值分解、QR 分解这些线性代数工具在深度学习中应用感兴趣的朋友。
2. LLM 微调的 dynamics:参数到底在怎么动
2.1 微调不是“小调整”,而是高维空间里的受限漂移
很多人对微调有个直觉性的误解,觉得基座模型已经很强了,微调只是在原有能力上“微调”一下,参数变化应该很小、很温和。这个直觉在低维空间里成立,但在 LLM 这种动辄几十亿参数的高维空间里,完全不是这么回事。
我习惯用一个类比来解释:想象一个巨大的山谷,谷底是基座模型已经收敛到的位置。微调相当于在这个山谷的某个侧面施加一个持续的推力,让模型沿着某个方向滑向新的位置。问题在于,这个山谷不是光滑的碗形,而是布满了狭长的沟壑和鞍点。推力稍微大一点,模型就可能滑进一条窄沟里,卡住不动;推力方向偏一点,又可能翻过一道脊线,掉进完全无关的区域。
从数学上看,微调过程中的参数更新可以写成:
θ_{t+1} = θ_t - η * ∇L(θ_t)其中 η 是学习率,∇L 是损失函数对参数的梯度。在 LLM 里,θ 的维度是 10^9 到 10^12 量级,∇L 的每一个分量都在同时变化。我们看到的 loss 曲线,其实是这个超高维动力系统在一维投影上的表现。loss 震荡,本质上是高维空间里参数更新方向不稳定。
2.2 为什么 LoRA 能缓解但不能根治病态更新
LoRA 的思路很巧妙:不去直接更新原始权重矩阵 W,而是把更新量 ΔW 分解成两个低秩矩阵的乘积:
ΔW = B * A其中 A 是 r×d 的矩阵,B 是 d×r 的矩阵,r 远小于 d。这样一来,可训练参数量从 d² 降到了 2dr,显存和计算量都大幅下降。
但 LoRA 的低秩假设本身就是一个强约束。它假设微调所需的参数更新天然就是低秩的,也就是更新量主要集中在少数几个方向上。这个假设在很多任务上成立,但在一些需要模型学习全新能力或大幅调整行为模式的任务上,低秩约束会限制模型的表达能力。
更关键的是,LoRA 并没有改变底层优化问题的病态性。它只是把优化问题从 d² 维降到了 2dr 维,但在这个降维后的空间里,梯度方向的不均衡问题依然存在。我实测过,在某些任务上,LoRA 的 A 矩阵和 B 矩阵的奇异值分布同样会出现极端不均衡,只是表现没有全量微调那么剧烈。
2.3 梯度范数与损失曲面的几何关系
要诊断微调过程中的 dynamics,最直接的手段是监控每一层的梯度范数。但只看范数不够,因为范数只告诉你梯度有多大,不告诉你梯度在各个方向上的分布。
我通常会把每一层的权重更新矩阵 ΔW 拿出来,做一次奇异值分解(SVD),看奇异值的衰减速度。如果奇异值从大到小排列后,前几个奇异值占了总能量的 90% 以上,说明更新高度集中在少数方向上,这就是病态更新的信号。
但 SVD 有个问题:计算量大。对于 4096×4096 的权重矩阵,做一次完整 SVD 在 GPU 上也要几百毫秒。如果每一层、每一步都做,训练速度会被拖垮。所以我后来改用了一种更轻量的分析工具——AKG 分解。
3. AKG 分解到底是什么:从矩阵分解到微调诊断
3.1 AKG 分解的基本定义与数学形式
AKG 分解这个名字,在不同文献里指代的东西不太一样。我在这里说的 AKG 分解,是指一种基于列主元 QR 分解的矩阵分析框架,AKG 分别代表三个核心步骤:Approximation(近似)、Key-direction extraction(关键方向提取)、Gap analysis(间隙分析)。
它的基本思想是:对于一个权重更新矩阵 ΔW,我们不直接做 SVD,而是先通过列主元 QR 分解找到一个正交基,然后在这个基上分析更新量的分布。具体来说:
ΔW = Q * R其中 Q 是正交矩阵,R 是上三角矩阵。列主元 QR 分解的特点是,它会自动把矩阵中“最重要”的列排在前面,这样 R 的对角线元素就从大到小排列,反映了各个方向上的更新强度。
AKG 分解在此基础上做了三件事:
- Approximation:用前 k 个主元方向近似原始更新矩阵,k 远小于矩阵维度。
- Key-direction extraction:提取出更新量最集中的前 k 个方向,作为“关键方向”。
- Gap analysis:分析关键方向与非关键方向之间的能量间隙,判断更新是否病态。
3.2 为什么选 QR 分解而不是 SVD
你可能会问:既然 SVD 能直接给出奇异值和奇异向量,为什么还要绕道用 QR 分解?
原因有三个:
第一,计算效率。列主元 QR 分解的计算复杂度是 O(mn²),而完整 SVD 是 O(mn² + n³)。对于 LLM 里常见的几千维矩阵,QR 分解通常比 SVD 快 2 到 5 倍。在训练过程中做在线分析时,这个差距很关键。
第二,数值稳定性。QR 分解在数值上比 SVD 更稳定,尤其是在矩阵接近奇异或条件数很大的情况下。LLM 的权重更新矩阵经常出现极端条件数,用 SVD 分析时容易受到浮点误差的干扰。
第三,可解释性。QR 分解的 R 矩阵对角线元素直接对应各个方向的更新强度,而且列主元的选择过程本身就揭示了哪些方向是“主方向”。相比之下,SVD 的奇异向量是多个方向的线性组合,解释起来更抽象。
我实测下来,对于 4096×4096 的矩阵,列主元 QR 分解在 A100 上大约需要 80 毫秒,而完整 SVD 需要 350 毫秒左右。如果只计算前 64 个主元方向,QR 分解可以降到 15 毫秒以内,完全可以在训练循环里在线运行。
3.3 AKG 分解在微调分析中的三个核心指标
基于 AKG 分解,我定义了三个核心指标来监控微调的 dynamics:
| 指标名称 | 计算方式 | 含义 | 健康范围 |
|---|---|---|---|
| 能量集中度 | 前 k 个主元方向的能量占比 | 更新是否集中在少数方向 | 0.3 - 0.7 |
| 间隙比 | 第 k 个与第 k+1 个主元方向的能量比 | 关键方向与非关键方向的分离程度 | < 3.0 |
| 方向稳定性 | 相邻两步关键方向的余弦相似度 | 更新方向是否稳定 | > 0.8 |
能量集中度太高,说明模型只在少数方向上学习,表达能力受限;太低,说明更新太分散,可能是在拟合噪声。间隙比太大,说明存在明显的“主方向霸权”,病态更新风险高。方向稳定性太低,说明训练过程在震荡,需要降低学习率或增加 warmup。
这三个指标结合起来,基本能覆盖微调过程中大部分 dynamics 问题。
4. 实操:用 AKG 分解诊断一次真实的微调任务
4.1 环境准备与代码框架
我用的环境是 PyTorch 2.1 + CUDA 12.1,单机八卡 A100 80G。基座模型是 Qwen 系列的 7B 版本,微调方法用的是 LoRA,rank 设 32,alpha 设 64,dropout 设 0.05。
核心的分析代码不复杂,我把它封装成了一个回调函数,挂在 HuggingFace Trainer 的on_step_end钩子上。每次梯度更新后,抽取指定层的权重更新矩阵,做 AKG 分解,记录三个指标。
import torch import numpy as np from scipy.linalg import qr def akg_decompose(delta_w, k=64): """ 对权重更新矩阵做 AKG 分解 delta_w: 形状为 (m, n) 的 numpy 数组 k: 提取前 k 个主元方向 返回: 能量集中度, 间隙比, 关键方向矩阵 """ # 列主元 QR 分解 Q, R, P = qr(delta_w, pivoting=True, mode='economic') # 计算各方向的能量 diag_R = np.abs(np.diag(R)) energy = diag_R ** 2 total_energy = np.sum(energy) # 能量集中度 concentration = np.sum(energy[:k]) / total_energy # 间隙比 if k < len(energy) - 1: gap_ratio = energy[k-1] / (energy[k] + 1e-12) else: gap_ratio = float('inf') # 关键方向 key_directions = Q[:, :k] return concentration, gap_ratio, key_directions这段代码里,qr函数的pivoting=True参数启用了列主元,mode='economic'表示只返回经济型分解结果,节省内存。diag_R取的是 R 矩阵的对角线元素,它们的平方就是各个主元方向的能量。
4.2 第一次诊断:发现能量集中度异常
我把这个回调挂上去,跑了一次完整的微调。结果如下:
| 训练步数 | 能量集中度 | 间隙比 | 方向稳定性 |
|---|---|---|---|
| 100 | 0.42 | 1.8 | 0.92 |
| 300 | 0.58 | 2.4 | 0.85 |
| 500 | 0.76 | 4.1 | 0.63 |
| 700 | 0.89 | 7.3 | 0.41 |
| 900 | 0.94 | 12.6 | 0.28 |
从第 300 步开始,能量集中度快速上升,到第 700 步已经接近 0.9。间隙比从 1.8 涨到 12.6,方向稳定性从 0.92 跌到 0.28。这三个指标同时恶化,说明模型在训练后期已经陷入了病态更新——更新量高度集中在极少数方向上,而且这些方向还在剧烈变化。
这正好解释了 loss 曲线在第 800 步附近的崩溃。模型不是在“学习”,而是在几个方向上反复横跳,把之前学到的东西覆盖掉了。
4.3 调整策略:基于 AKG 指标的干预
发现问题后,我做了三件事:
第一,降低学习率并增加 warmup。把学习率从 2e-4 降到 8e-5,warmup 步数从 100 增加到 500。这样做的目的是让模型在训练初期有足够的时间探索参数空间,避免过早陷入少数方向。
第二,引入梯度裁剪。把梯度范数的裁剪阈值从默认的 1.0 降到 0.5。梯度裁剪能直接限制单步更新的幅度,防止某个方向上的更新量过大。
第三,调整 LoRA 的 rank 和 alpha。把 rank 从 32 降到 16,alpha 从 64 降到 32。降低 rank 相当于进一步约束更新的低秩性,但配合前面的学习率调整,反而能让更新更均匀地分布在低秩空间里。
调整后重新跑,AKG 指标的变化如下:
| 训练步数 | 能量集中度 | 间隙比 | 方向稳定性 |
|---|---|---|---|
| 100 | 0.38 | 1.5 | 0.94 |
| 300 | 0.45 | 1.9 | 0.91 |
| 500 | 0.52 | 2.2 | 0.88 |
| 700 | 0.56 | 2.5 | 0.86 |
| 900 | 0.59 | 2.7 | 0.84 |
能量集中度稳定在 0.6 以下,间隙比控制在 3.0 以内,方向稳定性保持在 0.8 以上。loss 曲线平滑下降,验证集指标稳步提升,最终比第一次尝试高了 4.2 个百分点。
5. 常见问题与排查技巧实录
5.1 AKG 指标异常时的排查顺序
在实际操作中,AKG 指标异常通常不是孤立出现的。我整理了一个排查顺序,按优先级从高到低:
先看方向稳定性。如果方向稳定性低于 0.7,说明训练过程在震荡,优先检查学习率和 batch size。学习率太大是最常见的原因,其次是 batch size 太小导致梯度噪声过大。
再看能量集中度。如果能量集中度持续上升超过 0.8,说明更新在向少数方向坍缩。这时候要检查 LoRA 的 rank 是否太低,或者模型是否已经过拟合。
最后看间隙比。间隙比突然跳升,通常是某个方向上的更新量异常增大。这时候要检查数据里是否有异常样本,或者某一层的梯度是否爆炸。
5.2 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 能量集中度 > 0.85 | LoRA rank 太低 | 检查 rank 设置 | 提高 rank 或改用全量微调 |
| 间隙比 > 5.0 | 梯度爆炸 | 打印各层梯度范数 | 降低学习率,增加梯度裁剪 |
| 方向稳定性 < 0.5 | 学习率过大 | 检查 loss 曲线 | 降低学习率,增加 warmup |
| 三个指标同时恶化 | 数据质量问题 | 检查数据清洗流程 | 重新清洗数据,去除异常样本 |
| 指标正常但 loss 不降 | 任务难度过高 | 检查任务定义 | 简化任务或增加数据量 |
5.3 几个容易踩的坑
坑一:在错误的层上做分析。LLM 里不同层的 dynamics 差异很大。嵌入层和输出层的更新模式跟中间的注意力层完全不同。我建议至少分别监控嵌入层、中间注意力层和输出层,不要只看一个全局平均。
坑二:k 值选得不对。AKG 分解里的 k 是提取的主元方向数。k 太小,能量集中度会虚高;k 太大,计算量上去了但信息增量有限。我的经验是 k 取矩阵维度的 1% 到 2% 比较合适。对于 4096 维的矩阵,k 取 64 左右。
坑三:忽略 warmup 阶段。warmup 阶段的 AKG 指标波动很大,这是正常的。分析时应该从 warmup 结束后开始看,否则容易被早期噪声误导。
坑四:把 AKG 指标当成绝对标准。不同任务、不同基座模型、不同微调方法,AKG 指标的健康范围是不一样的。我给的 0.3-0.7 能量集中度、<3.0 间隙比、>0.8 方向稳定性,是基于 7B 模型和 LoRA 微调的经验值。换成 13B 或 70B 模型,或者换成全量微调,这些阈值需要重新校准。
6. 从 AKG 分解延伸出去的几个方向
AKG 分解在 LLM 微调里的应用,远不止诊断 loss 震荡这一个场景。我后来把它用在了几个其他地方,效果也不错。
一是用于超参搜索。传统超参搜索靠最终指标反馈,一次实验要跑几个小时。用 AKG 指标做早期停止,可以在训练的前 20% 步数就判断这次超参组合是否靠谱,节省大量算力。
二是用于数据质量评估。如果某批数据导致的 AKG 指标异常,说明这批数据可能有问题。我试过用这个方法筛出了一批标注错误的样本,比人工抽查效率高得多。
三是用于微调方法对比。LoRA、Adapter、Prefix Tuning 这些方法,在 AKG 指标上的表现差异很明显。LoRA 的能量集中度通常最高,Adapter 居中,Prefix Tuning 最低。这个差异反映了不同方法对参数空间的约束强度。
四是用于训练过程的可视化。把 AKG 指标画成时间序列图,跟 loss 曲线放在一起看,能直观地看到模型在什么时候“学歪了”。我现在的训练监控面板上,AKG 三个指标跟 loss、学习率、梯度范数并列,是必看项。
最后分享一个我在实际操作中的体会:AKG 分解的价值不在于它有多精确,而在于它提供了一个轻量、可在线运行的诊断视角。你不需要等训练跑完才知道出了问题,在训练过程中就能看到参数更新的“形状”在怎么变化。这种实时反馈,对于 LLM 微调这种动辄几小时甚至几天的任务来说,能省下的时间和算力是相当可观的。