ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

任务向量合并:从参数平均到映射优化

任务向量合并:从参数平均到映射优化 1. 任务向量这东西加法本来就不该是默认操作我第一次在论文里看到任务向量Task Vector时说实话是带着怀疑的。把模型在目标任务上做微调然后拿微调后的权重减去预训练权重得到一个“向量”这个向量居然能通过简单的加减法去控制模型行为——减掉数学能力就能让模型在其它任务上更专注加上某个任务的向量就能让这个任务的能力凭空出现在别的模型里。这套操作跑在论文的图里确实很漂亮但我自己上手复现时问题马上来了。任务向量最早的用法非常朴素定义就是[ \tau_t \theta_t - \theta_{\text{pre}} ]其中 (\theta_{\text{pre}}) 是预训练权重(\theta_t) 是在任务 (t) 上微调后的权重。要把多个任务的能力合到一个模型里最常见做法就是参数平均[ \theta_{\text{new}} \theta_{\text{pre}} \sum_{t1}^{T} \lambda_t \tau_t ](\lambda_t) 是人工设置的缩放系数多数时候干脆全设成 1。这个做法几乎零成本不需要拿任何数据重新训练概念也足够直观所以在多任务合并、模型编辑、机器遗忘这些场景里被广泛使用。但我自己在三组不同任务上批量复现之后发现这个“默认加法”其实是一个相当强的假设很多时候它只是碰巧成立而不是理所当然成立。1.1 参数平均到底是在“平均”什么先说清楚参数平均在做什么。假设你有一个预训练模型在任务 A 上微调得到 (\theta_A)在任务 B 上微调得到 (\theta_B)。常规的模型合并思路是直接对权重取平均[ \theta_{\text{merge}} \frac{1}{2}(\theta_A \theta_B) ]这个思路来自模型集成里的权重平均Weight Averaging它在同一任务的多个 checkpoint 之间是有效的因为那些 checkpoint 基本落在同一个损失盆地平均之后损失不会变差。但任务向量加法不一样它是在预训练权重这个“原点”上做向量叠加[ \theta_{\text{new}} \theta_{\text{pre}} \tau_A \tau_B ]这里隐含了两个假设一是任务 A 和任务 B 在参数空间里的改变是相互独立的可以直接叠加二是模型从预训练权重出发沿着这两个方向同时走损失面仍然是平滑的不会突然出现一个“坑”把模型带偏。1.2 三个被默认成立、但其实很脆弱的假设实际使用中参数平均的生效依赖三个假设任何一个松动结果都会打折扣。第一个假设是“任务影响可以压缩成一个向量”。微调过程是一个轨迹从预训练权重出发一路走到任务权重不同阶段的更新方向很可能不一样。拿最终权重和预训练权重做差得到的任务向量只是这条轨迹的终点的位移它丢失了中间过程的信息。两个任务如果把模型往同一个方向推三千里和往一个方向推一千、再往另一个方向推两千最终任务向量可能长得完全一样但实际影响截然不同。第二个假设是“任务的更新方向可以交换”。向量加法是交换的但神经网络训练的动力学不交换。任务 A 的损失面在某个方向上很平缓任务 B 的损失面在同一个方向上很陡峭分开微调时各自都找到了合理的解但相加之后模型落到的位置可能同时踩在两个任务的高损失区域上。这个时候不是“平均”出了问题而是“加法”本身不适合作为跨任务的组合算子。第三个假设是“损失面在预训练到微调终点之间近似线性”。任务向量为什么在 CLIP 这类模型上表现特别好因为 CLIP 的预训练表征足够强微调只是做小幅移动线性插值路径上的损失基本可控。但你换成从头训练的小模型或者任务之间差异特别大这条路径上的损失曲线就完全不可控了。参数平均不是万灵药它只在“预训练足够强、微调位移足够小”的范围内好使。1.3 我在多任务复现中看到的真实翻车现场有次我把一个文本分类任务和一个生成式任务组合简单加权的结果让我一度怀疑是不是代码写错了。单独看两个任务向量模长都正常方向也不至于完全相反但相加之后模型在分类任务上的表现几乎退化到预训练状态生成任务也只保留了一半能力。我试了不同的 (\lambda) 组合总是一头好一点、另一头就垮下去。排查到最后发现两个任务在参数空间里的重要更新方向存在明显冲突一部分参数被任务 A 往左拉被任务 B 往右拉简单相加等于互相抵消。更隐蔽的是不同任务的微调超参数不一样任务向量的模长天然不在同一量纲上。直接相加时模长大的任务会单方面主导整个合并结果这不是“平均”出问题而是“平均的基准不一致”出问题。也就是从那次之后我开始认真想一个问题任务向量之间到底应该怎么“相加”才是对任务本身最忠实的方式2. 与其把任务向量看成点不如看成一条方向轨迹要回答怎么相加的问题首先得搞清楚一个任务向量在参数空间里到底代表了什么。我之前一直默认它是一个方向就像二维平面里从原点画出来的一条箭头。但后来我发现这个箭头只是某种压缩之后的结果真实信息远不止一个箭头能承载。2.1 微调不是一步到位而是一条轨迹微调过程中权重是从预训练起点逐步变化到任务终点的。如果你把中间 checkpoints 全部保存下来把每个 checkpoint 减去预训练权重得到的一串向量才能更完整地描述“这个任务究竟把模型往哪个方向推了多远”。举个例子一个任务在前 30% 的训练步里大范围调整底层特征后 70% 只微调顶层分类头另一个任务恰好相反前 30% 几乎不动后 70% 才开始大改。只看最终任务向量可能完全看不出这种差异。这影响的不是可视化而是组合时的行为前者在底层方向上的改变必须保留后者在底层上的更新几乎为零。简单相加时这些信息全部丢失模型自然会用错方向。2.2 用子空间替代单点信息量完全不同我在这里引入一个基础概念子空间。假设任务 (t) 在第 (k) 个 checkpoint 对应的任务向量为[ \tau_t^{(k)} \theta_t^{(k)} - \theta_{\text{pre}} ]把所有 checkpoint 向量拼成一个矩阵对矩阵做 SVD取前 (r) 个主成分就能得到一个子空间 (U_t)。(U_t) 表示的是这个任务在整个微调过程中的主要变化方向集合而不仅仅是终点的位移。表示方式可以写成[ U_t \text{span}(q_{t,1}, q_{t,2}, \dots, q_{t,r}) ]其中 (q_{t,j}) 是 SVD 得到的基向量。对于一个任务来说真正的关键信息是“它变到了哪些方向”而不是“它最终落在哪个点”。这个转换最大的价值在于它把任务向量从“一个方向向量”升级成了“一个方向集合”。两个任务之间是否冲突不再看两个向量夹角是否大于 90 度而是看两个子空间的交叠关系。子空间交叠大的任务大概率能兼容交叠小甚至正交的任务直接相加必然各走各的路。2.3 子空间距离比向量夹角更可靠两个向量之间的关系可以用余弦相似度描述但两个子空间之间的关系更适合用“距离”描述。对于某个组合向量 (\tau)它到任务 (t) 的子空间 (U_t) 的距离定义为[ d(\tau, U_t) | (I - P_t) \tau | ]其中 (P_t Q_t Q_t^T) 是投影到子空间 (U_t) 上的投影矩阵(Q_t) 是子空间的正交基。((I - P_t)\tau) 就是 (\tau) 中落在任务 (t) 子空间之外的部分。这个剩余部分越大说明 (\tau) 离任务 (t) 的真实更新方向越远。这一下就把问题变成了几何问题我们想要的组合结果应该尽量靠近所有任务的子空间同时又不能完全丢掉某个任务的方向。这个思路直接引出了下一步的优化框架也让“映射优化”这个词有了具体的几何含义。3. 把相加问题重写成映射优化既然简单加权不可靠那就把“任务向量相加”这个操作重写成一个带约束的优化问题。我说的映射优化指的是通过一个线性映射来调整任务向量的组合方式而不是给每个任务固定一个标量权重然后机械相加。3.1 目标函数贴近所有任务子空间别完全偏离平均我来用一个具体的目标函数说明。给定任务 1 到任务 (T)每个任务有对应的子空间 (U_t)我们希望找到一个组合向量 (\tau^*)让它同时满足两个条件尽量靠近每个任务的子空间同时不能离参数平均的结果太远否则会丢掉公共信息。写成数学形式就是[ \tau^* \arg\min_{\tau} \sum_{t1}^{T} w_t | (I - P_t) \tau |^2 \alpha | \tau - \tau_{\text{avg}} |^2 ]其中(P_t) 是任务 (t) 的子空间投影矩阵(w_t) 是任务权重默认可以取 (1/T)(\tau_{\text{avg}} \frac{1}{T}\sum_t \tau_t) 是参数平均的结果(\alpha) 是正则项系数控制对参数平均的信任程度。第一项作用是让组合结果尽量坐落在每个任务的子空间内不要轻易踩到某个任务完全没更新过的方向上去。第二项保证优化不会跑飞它把一个先验拽住如果没有任何冲突证据最终结果就回到参数平均。所以这不是全盘推翻参数平均而是给参数平均加了一层“基于子空间证据”的修正。3.2 求解从一个常系数变成一个矩阵映射这个目标函数是个标准的二次型可以直接求闭式解。令[ M \sum_{t1}^{T} w_t (I - P_t) ]那么目标函数等价于[ \tau^T M \tau \alpha | \tau - \tau_{\text{avg}} |^2 ]对 (\tau) 求导并令其等于 0得到[ (M \alpha I) \tau^* \alpha \tau_{\text{avg}} ]整理一下[ \tau^* \alpha (M \alpha I)^{-1} \tau_{\text{avg}} ]看这个形式参数平均得到的是 (\tau_{\text{avg}})而映射优化得到的是对 (\tau_{\text{avg}}) 做了一次矩阵变换的结果。换句话说它用矩阵 ((M \alpha I)^{-1}) 来重新分配任务向量在不同方向上的分量。那些离某个任务子空间特别远的分量会被矩阵拉回来那些和所有子空间都兼容的分量会被保留。这就是标题里“从参数平均到映射优化”的本质加权的标量系数变成矩阵映射。3.3 实际操作里的低秩表达直接构造 (M) 需要 (d \times d) 的矩阵其中 (d) 是参数量。对一个大模型来说这是不可能的。实际计算时我会把优化限制在基础任务向量张成的低秩子空间里。假设我们取 (K) 个基础向量作为列拼成矩阵 (B \in \mathbb{R}^{d \times K})令[ \tau B c ]其中 (c) 是维度为 (K) 的组合系数。代入目标函数得到关于 (c) 的线性系统[ (B^T M B \alpha B^T B) c \alpha B^T \tau_{\text{avg}} ]这里 (B^T M B) 是 (K \times K) 的小矩阵计算量完全可以接受。(B) 的列可以选择每个任务最终任务向量做 SVD 后的主方向。我实际测试下来(K) 取任务数量的 2 到 3 倍就够了多余的基向量对结果没有实质改善只会增加条件数。3.4 为什么这个优化能解决简单相加的冲突回到那个文本分类加生成式任务失败的场景。分类任务和生成任务的子空间在参数空间中交叠少(\tau_{\text{avg}}) 在两边的子空间之外都有大量残留。代入优化目标后第一项会强制把 (\tau^*) 拉向两个子空间都覆盖到的中间区域相当于在冲突方向上做了一次“裁判式协调”。正则项保证协调不会过度最终结果自然比直接平均更可靠。这也是我后来把它叫成“Real TA”的原因不是因为它用了多复杂的网络而是因为它终于正视了任务向量“不是一个普通向量而是一个更新方向的集合”这件事。4. 实验对比什么场景收益最大什么场景别折腾理论听起来顺但落到实验里还得看收益。我基于开源模型和公开数据集做了几组对比核心问题是在什么任务组合下映射优化明显优于简单参数平均在什么场景下简单平均其实已经够了。4.1 同域任务映射优化和平均的差异很小第一组测试是两个非常相似的任务比如两个文本分类任务数据分布接近微调过程也类似。这种情况下两个任务向量高度相关子空间交叠很大(I-P) 的分量很小映射优化给出的结果几乎就是参数平均本身。差异通常在验证集上只有零点几个点不值得为此增加计算量。这个结论其实很正面说明映射优化没有破坏参数平均在常规场景下的良好表现。它继承了参数平均的能力只是多了在冲突场景下的纠偏机制。4.2 异域任务差距拉大到五个百分点以上第二组测试把一个纯视觉任务和一个纯文本任务组合这是最容易出现冲突的场景。参数平均合并后的模型视觉任务掉点明显文本任务也只能算勉强保住。换成映射优化之后视觉任务基本回到了单任务微调的九成以上水平文本任务也有明显改善。在我自己的验证集上差异在五个百分点以上。这里的关键不是映射优化做了多复杂的变换而是它识别出两个任务的子空间几乎不交叠于是把组合向量调整到尽量兼顾两边。参数平均做不到这一点因为它只会僵硬地等比例相加。4.3 三任务叠加以及 (\alpha) 的敏感性我有一次把三个任务放进同一个模型参数平均后的结果非常微妙任务 A 和任务 B 表现还行任务 C 几乎报废。映射优化整体上是保住了三个任务的下限但也不是完全没有代价个别任务相对最优单任务仍然有掉点。(\alpha) 的取值很关键。我观察到任务之间差异越大、冲突越明显(\alpha) 就应该越小让矩阵映射有更大的调整空间任务相似时(\alpha) 接近 1 就足够。实际使用中我会用小部分验证集做一次网格搜索(\alpha) 的候选范围放在 0.05 到 0.5 之间通常就够了。4.4 什么场景下不推荐映射优化不是所有场景都推荐用映射优化。如果只有一个任务向量需要应用优化没有意义直接加权即可。如果任务数量非常多但每个任务只提供了一个最终 checkpoint没有中间轨迹子空间信息不足映射优化的收益也会非常有限。另外如果部署环境要求极致的低延迟和零额外计算简单平均的零成本优势仍然是不可替代的。映射优化的计算主要花在 SVD 和一次线性系统求解上对离线合并场景无所谓但如果你需要在端上实时合并模型那这些步骤就是负担。5. 代码级实现细节和踩坑记录我在实现这套流程时踩了不少坑这里挑几个最典型的讲。代码本身不复杂复杂的是边界条件。5.1 子空间构造不用最终权重用检查点轨迹第一步是保存微调过程中的中间 checkpoint。我建议保留最后 30% 训练过程中的 5 到 10 个 checkpoint而不是只保留最终权重。原因是微调后期权重接近收敛能较好地反映任务的最终方向同时还保留了一部分优化轨迹信息。每个 checkpoint 都减去预训练权重得到一组任务向量然后拼成矩阵做 SVD。核心代码大致如下import torch def build_task_subspace(checkpoints, pretrained_weight, rank8): # checkpoints: list of state_dict tensors, shape [num_params] # pretrained_weight: tensor, shape [num_params] vecs [] for ckpt in checkpoints: vec (ckpt - pretrained_weight).flatten() vecs.append(vec) mat torch.stack(vecs, dim1) # [num_params, num_ckpts] # 对列做中心化消除整体偏移影响 mat mat - mat.mean(dim1, keepdimTrue) U, S, Vt torch.linalg.svd(mat, full_matricesFalse) basis U[:, :rank] # [num_params, rank] return basis这里要注意中心化操作减掉的是所有 checkpoint 向量的均值而不是预训练权重。很多实现直接拿原始权重做归一化最后子空间方向会整体偏掉。5.2 投影矩阵的数值实现不要真去构造 d 乘 d 矩阵有人看到公式里的 ((I - P_t))会想在代码里构造一个 (d \times d) 的矩阵这是完全错误的做法。参数 d 动辄上亿你根本没地方放这个矩阵。正确的做法是永远保留子空间的基 (Q_t)计算投影时用矩阵乘法完成。投影的计算方式是def project_to_subspace(vector, basis): # basis: [num_params, rank] coeffs basis.T vector # [rank] return basis coeffs def residual(vector, basis): return vector - project_to_subspace(vector, basis)这样内存开销只是基矩阵本身rank 通常小于 16完全可控。5.3 线性系统求解条件数必须检查在低秩子空间里求解[ (B^T M B \alpha B^T B)c \alpha B^T\tau_{\text{avg}} ]这个系统的矩阵维度很小但条件数可能很糟糕尤其是任务向量之间相关性极高时。我在实验里见过条件数超过 (10^8) 的情况解出来的系数分布极不均匀个别系数会异常大。解决办法有两个一个是调大 (\alpha)给对角线加一个合适的偏置另一个是用最小二乘求解器而不是直接求逆。PyTorch 里推荐用torch.linalg.lstsq而不是torch.linalg.solve数值稳定性好很多。A Bt_M_B alpha * Bt_B rhs alpha * Bt_tau_avg c, _ torch.linalg.lstsq(A, rhs.unsqueeze(1))5.4 我踩过的一个隐蔽的坑任务向量尺度不一致不同任务的微调学习率、训练步数、batch size 不同得到的任务向量模长可能差别几倍到几十倍。如果直接把这些向量放进 B 矩阵模长大的任务会主导整个子空间的 SVD 结果子空间方向几乎全被它带偏。我建议在做 SVD 前先对每个任务向量做尺度归一化或者至少记录下每个任务的模长在后续系数求解时再还原。最简单的做法是把每个检查点向量除以对应任务的向量模长vec vec / (tau_final.norm() 1e-8)这样不会改变方向信息但能避免大模长任务在 SVD 里“一票否决”。5.5 验证时容易犯的错误验证时不要拿合并后的模型在所有任务上同时跑一遍然后简单看平均分。正确做法是分开记录每个任务的指标并且和单任务微调的上限做对比。映射优化能提高的是多任务合并场景下的折中质量不是每个任务都超过单任务微调那是另一回事。我一般会专门记录一个“冲突度”指标合并模型在每个任务上的分数除以该任务单独微调的分数。冲突度越接近 1说明合并没造成损失远低于 1 的任务就是需要映射优化重点照顾的对象。6. 从 Real TA 出发再往下走我打算做的事这套方法目前已经在我的多个项目里落地但说实话还只是第一步。任务向量的信息量远比现在大部分人用的“减法取差”要丰富映射优化的方向我很看好后面还有几个点值得继续插进去。第一是把全局的 (\alpha) 改成逐层自适应。Transformer 不同层对任务的敏感度差异很大底层更多是通用特征顶层更多是任务专属特征。全局用一个 (\alpha)等价于假设所有层的冲突程度一样这个假设显然粗糙。把映射优化拆到每一层去执行让底层更保守、顶层更激进应该能带来更细粒度的收益。第二是用 Fisher 信息矩阵做加权替代目前等权的 (w_t)。任务对参数的“重要性”不是均匀分布的有些参数对任务 A 至关重要对任务 B 则可有可无。用 Fisher 信息来估计每个任务对每个参数的依赖程度可以让 ((I - P_t)) 的惩罚更精准真正压制那些会互相干扰的参数移动。还有一个我最近在尝试的方向是把映射优化用在模型编辑上。模型编辑通常只希望改一个局部行为不希望破坏整体能力。把编辑方向当作一个任务子空间把预训练模型的原始行为当作另一个任务子空间两者的冲突天然适合用映射优化去权衡。最后再说一点实际心得不要一上来就上完整方案。先跑一个最简化的版本——只取最终任务向量、只取秩 1 子空间、固定一个 (\alpha)——你会立刻发现它都能比简单平均好一点。然后你再逐步加入检查点轨迹、增加子空间秩、搜索 (\alpha)每一步改进都能清晰看到收益来源。这种从简到繁的过程比直接落一版花哨方法更容易定位问题也更容易写出可复用的代码。任务向量这个方向还远没到玩法定型的时候我现在特别好奇的是当模型规模再大一个量级后子空间的几何结构会不会出现新的规律。
返回列表