模型合并与融合:理论、算法与可运行实现—从损失曲面几何到多模型融合

目录

  • 第一部分:模型合并基础理论
    • 第一章:绪论------为什么需要模型合并
    • 第二章:损失曲面几何与线性模式连通性
    • 第三章:权重空间的对齐问题
  • 第二部分:经典合并方法
    • 第四章:简单平均与加权平均
    • 第五章:球面线性插值(SLERP)
    • 第六章:任务算术(Task Arithmetic)
  • 第三部分:高级合并方法
    • 第七章:TIES-Merging------修剪冗余符号
    • 第八章:DARE------随机丢弃与重缩放
    • 第九章:Model Soups 与多模型融合
  • 第四部分:理论分析与前沿
    • 第十章:合并的理论保证
    • 第十一章:合并与泛化
    • 第十二章:大语言模型的合并实践
  • 第五部分:完整可运行代码实现
    • 第十三章:从零实现权重对齐
    • 第十四章:从零实现 SLERP 与任务算术
    • 第十五章:从零实现 TIES 与 DARE
    • 第十六章:完整合并 Pipeline 与精度对比
  • 附录

第一部分:模型合并基础理论


第一章:绪论------为什么需要模型合并

1.1 模型合并的动机

1.1.1 单一模型的局限

在实际应用中,我们通常面临以下场景:

  1. 多个专家模型:针对不同任务微调了多个模型,每个模型在各自任务上表现优异
  2. 计算资源有限:无法部署多个大模型
  3. 多任务需求:需要一个模型同时擅长多种任务

模型合并(Model Merging) 的核心思想是:将多个模型的权重直接合并为一个模型,无需额外训练(或仅需极少训练)。

1.1.2 模型合并 vs 其他方法

方法 是否需要训练 是否需要数据 计算成本 多任务能力
多任务微调
模型集成 高(推理时)
知识蒸馏 中等
模型合并 否/极少

模型合并的独特优势

  • 零成本融合:不需要训练数据和计算资源
  • 即插即用:直接对权重进行操作
  • 可组合性:可以任意组合不同任务的模型
  • 理论优美:基于损失曲面的几何性质

1.2 模型合并的历史

1.2.1 早期工作

模型平均(Model Averaging):最简单的合并方法------对多个模型的权重取平均。

Polyak-Ruppert 平均:在 SGD 训练过程中,对多个检查点取平均以提高收敛性。

1.2.2 现代发展

Git Rebasin(Ainsworth et al., 2023):解决了权重空间的对齐问题。

Task Arithmetic(Ilharco et al., 2023):发现任务向量可以进行加减运算。

TIES-Merging(Yadav et al., 2023):修剪冗余符号,解决冲突。

DARE(Yu et al., 2024):随机丢弃 delta 参数并重缩放。

1.3 模型合并的分类

类别 说明 代表方法
线性插值 在权重空间中线性插值 平均、SLERP
任务向量 使用任务向量进行算术运算 Task Arithmetic
符号处理 处理权重的符号冲突 TIES-Merging
随机化 随机丢弃 delta 参数 DARE
对齐合并 先对齐再合并 Git Rebasin

第二章:损失曲面几何与线性模式连通性

2.1 损失曲面的基本概念

2.1.1 损失曲面

定义 2.1(损失曲面) :模型参数 θ∈Rd\theta \in \mathbb{R}^dθ∈Rd 的损失曲面定义为:

L(θ)=E(x,y)∼Dℓ(fθ(x),y)\mathcal{L}(\theta) = \mathbb{E}_{(x,y) \sim \mathcal{D}} \\ell(f_\\theta(x), y)L(θ)=E(x,y)∼Dℓ(fθ(x),y)

其中 fθf_\thetafθ 是参数为 θ\thetaθ 的模型,ℓ\ellℓ 是损失函数。

2.1.2 损失曲面的几何性质

定义 2.2(局部极小值) :θ∗\theta^*θ∗ 是局部极小值,如果存在 ϵ>0\epsilon > 0ϵ>0 使得:

L(θ)≥L(θ∗),∀∥θ−θ∗∥<ϵ\mathcal{L}(\theta) \geq \mathcal{L}(\theta^*), \quad \forall \|\theta - \theta^*\| < \epsilonL(θ)≥L(θ∗),∀∥θ−θ∗∥<ϵ

定义 2.3(全局极小值) :θ∗\theta^*θ∗ 是全局极小值,如果:

L(θ)≥L(θ∗),∀θ∈Rd\mathcal{L}(\theta) \geq \mathcal{L}(\theta^*), \quad \forall \theta \in \mathbb{R}^dL(θ)≥L(θ∗),∀θ∈Rd

2.2 线性模式连通性

2.2.1 定义

定义 2.4(线性模式连通性,Linear Mode Connectivity, LMC) :两个参数 θ1\theta_1θ1 和 θ2\theta_2θ2 是线性模式连通的,如果沿它们之间的线性插值路径,损失不增加:

L(αθ1+(1−α)θ2)≤max⁡(L(θ1),L(θ2)),∀α∈0,1\mathcal{L}(\alpha \theta_1 + (1-\alpha) \theta_2) \leq \max(\mathcal{L}(\theta_1), \mathcal{L}(\theta_2)), \quad \forall \alpha \in 0, 1L(αθ1+(1−α)θ2)≤max(L(θ1),L(θ2)),∀α∈0,1

2.2.2 LMC 的理论分析

定理 2.1(LMC 的充分条件) :如果损失函数 L\mathcal{L}L 在 θ1\theta_1θ1 和 θ2\theta_2θ2 之间的线段上是凸的 ,则 θ1\theta_1θ1 和 θ2\theta_2θ2 是线性模式连通的。

证明:凸性意味着:

L(αθ1+(1−α)θ2)≤αL(θ1)+(1−α)L(θ2)≤max⁡(L(θ1),L(θ2))\mathcal{L}(\alpha \theta_1 + (1-\alpha) \theta_2) \leq \alpha \mathcal{L}(\theta_1) + (1-\alpha) \mathcal{L}(\theta_2) \leq \max(\mathcal{L}(\theta_1), \mathcal{L}(\theta_2))L(αθ1+(1−α)θ2)≤αL(θ1)+(1−α)L(θ2)≤max(L(θ1),L(θ2))

□\square□

定理 2.2(神经网络的 LMC):对于过参数化的神经网络,在训练初期(SGD 的随机性使不同初始化的模型收敛到同一"盆地"),不同初始化训练得到的模型通常是线性模式连通的。

实验支持:Neyshabur et al. (2020) 发现,从同一预训练模型出发,不同微调得到的模型之间存在低损失的线性路径。

2.2.3 LMC 的几何解释

图示:考虑损失曲面的等高线:

复制代码
    θ₁*          θ₂*
     ·            ·
    /|\          /|\
   / | \   __   / | \
  /  |  \_/  \_/  |  \
 /   |   /    \   |   \
/    |  /      \  |    \

如果 θ1∗\theta_1^*θ1∗ 和 θ2∗\theta_2^*θ2∗ 在同一个"盆地"中,它们之间的线性路径不会穿过高损失区域。

2.3 损失曲面的曲率分析

2.3.1 Hessian 与曲率

定义 2.5(Hessian 矩阵)

H(θ)=∇2L(θ)H(\theta) = \nabla^2 \mathcal{L}(\theta)H(θ)=∇2L(θ)

定理 2.3(LMC 与 Hessian 的关系) :如果沿 θ1\theta_1θ1 到 θ2\theta_2θ2 的路径上,Hessian 的最大特征值 λmax⁡(H)\lambda_{\max}(H)λmax(H) 满足:

λmax⁡(H(θ))≤0,∀θ=αθ1+(1−α)θ2\lambda_{\max}(H(\theta)) \leq 0, \quad \forall \theta = \alpha \theta_1 + (1-\alpha) \theta_2λmax(H(θ))≤0,∀θ=αθ1+(1−α)θ2

则路径上损失是凸的,θ1\theta_1θ1 和 θ2\theta_2θ2 是线性模式连通的。

2.3.2 窄盆地假设

假设 2.1(窄盆地假设):神经网络的损失曲面由许多"窄而深"的盆地组成,每个盆地内的模型可以通过线性插值连接。

推论:如果两个模型在同一盆地中,它们的加权平均仍然在该盆地中------这就是模型合并有效的根本原因。


第三章:权重空间的对齐问题

3.1 权重空间的对称性

3.1.1 权重排列不变性

定理 3.1(权重排列不变性) :对于全连接层 y=Wxy = Wxy=Wx,将权重矩阵 WWW 的行(或列)重新排列,同时相应地调整下一层(或上一层)的权重,模型的输入-输出映射不变。

形式化 :设 PPP 是排列矩阵,则:

y=Wx=(WPT)(Px)y = Wx = (WP^T)(Px)y=Wx=(WPT)(Px)

因此 WWW 和 WPTWP^TWPT 表示同一个函数(只是隐藏单元的排列不同)。

3.1.2 对齐问题的定义

定义 3.1(对齐问题) :给定两个模型 θ1\theta_1θ1 和 θ2\theta_2θ2(从不同初始化训练得到),找到排列矩阵 {Pl}\{P_l\}{Pl} 使得变换后的模型 θ2′={Plθ2,l}\theta_2' = \{P_l \theta_{2,l}\}θ2′={Plθ2,l} 与 θ1\theta_1θ1 在权重空间中尽可能接近:

min⁡{Pl}∑l∥θ1,l−Plθ2,lPlT∥F2\min_{\{P_l\}} \sum_l \|\theta_{1,l} - P_l \theta_{2,l} P_l^T\|_F^2{Pl}minl∑∥θ1,l−Plθ2,lPlT∥F2

重要性:如果不对齐,直接平均两个模型的权重是没有意义的------因为隐藏单元的排列可能完全不同。

3.2 对齐算法

3.2.1 基于激活的对齐

算法 3.1(基于激活的对齐)

复制代码
输入:模型 θ₁, θ₂, 校准数据 X
输出:对齐后的模型 θ₂'

对每一层 l:
    1. 计算两个模型在该层的激活:
       A₁ = f₁⁽ˡ⁾(X), A₂ = f₂⁽ˡ⁾(X)
    2. 计算激活的协方差:
       C₁ = A₁ᵀA₁, C₂ = A₂ᵀA₂
    3. 找到排列 P 使 C₁ 和 C₂ 尽可能相似:
       P = argmin_P ||C₁ - P C₂ Pᵀ||_F
    4. 应用排列:θ₂' = P θ₂ Pᵀ

3.2.2 基于权重的对齐

算法 3.2(基于权重的对齐)

复制代码
输入:模型 θ₁, θ₂
输出:对齐后的模型 θ₂'

对每一层 l:
    1. 计算权重的相似度矩阵:
       S = |θ₁||θ₂ᵀ|
    2. 使用匈牙利算法找到最优匹配:
       P = hungarian(S)
    3. 应用排列:θ₂' = P θ₂

3.2.3 匈牙利算法

问题 :给定相似度矩阵 S∈Rn×nS \in \mathbb{R}^{n \times n}S∈Rn×n,找到排列 π\piπ 最大化:

max⁡π∑i=1nSi,π(i)\max_\pi \sum_{i=1}^n S_{i, \pi(i)}πmaxi=1∑nSi,π(i)

复杂度 :O(n3)O(n^3)O(n3)(Kuhn-Munkres 算法)

3.3 Git Rebasin

3.3.1 核心思想

Git Rebasin(Ainsworth et al., 2023)将模型合并类比为 Git 的 rebase 操作:

  1. 对齐 :将模型 θ2\theta_2θ2 的权重空间对齐到 θ1\theta_1θ1
  2. 合并:在对齐后的空间中进行线性插值

3.3.2 算法

算法 3.3(Git Rebasin)

复制代码
输入:模型 θ₁, θ₂, 校准数据 X
输出:合并后的模型 θ_merged

1. 对齐 θ₂ 到 θ₁:
   θ₂' = align(θ₂, θ₁, X)
2. 线性插值:
   θ_merged = α θ₁ + (1-α) θ₂'

3.3.3 理论分析

定理 3.2(对齐后的 LMC) :如果 θ1\theta_1θ1 和 θ2\theta_2θ2 是从同一预训练模型微调得到的,且 θ2\theta_2θ2 已对齐到 θ1\theta_1θ1,则它们通常是线性模式连通的。

证明思路 :对齐消除了隐藏单元排列的歧义,使得权重空间中的线性插值对应于函数空间中的有意义插值。□\square□


第二部分:经典合并方法


第四章:简单平均与加权平均

4.1 简单平均

4.1.1 定义

定义 4.1(简单平均) :对于 KKK 个模型 θ1,...,θK\theta_1, \dots, \theta_Kθ1,...,θK,简单平均为:

θˉ=1K∑k=1Kθk\bar{\theta} = \frac{1}{K} \sum_{k=1}^K \theta_kθˉ=K1k=1∑Kθk

4.1.2 理论分析

定理 4.1(简单平均的泛化界) :设每个模型 θk\theta_kθk 的测试损失为 Ltest(θk)\mathcal{L}_{\text{test}}(\theta_k)Ltest(θk),则平均模型的测试损失满足:

Ltest(θˉ)≤1K∑k=1KLtest(θk)+1K∑k=1K∥θk−θˉ∥2⋅λmax⁡(H)\mathcal{L}{\text{test}}(\bar{\theta}) \leq \frac{1}{K} \sum{k=1}^K \mathcal{L}{\text{test}}(\theta_k) + \frac{1}{K} \sum{k=1}^K \|\theta_k - \bar{\theta}\|^2 \cdot \lambda_{\max}(H)Ltest(θˉ)≤K1k=1∑KLtest(θk)+K1k=1∑K∥θk−θˉ∥2⋅λmax(H)

其中 λmax⁡(H)\lambda_{\max}(H)λmax(H) 是 Hessian 的最大特征值。

推论 4.1 :如果损失曲面在模型之间的区域是平坦的(λmax⁡(H)\lambda_{\max}(H)λmax(H) 小),则简单平均接近最优。

4.2 加权平均

4.2.1 定义

定义 4.2(加权平均)

θmerged=∑k=1Kwkθk,∑k=1Kwk=1\theta_{\text{merged}} = \sum_{k=1}^K w_k \theta_k, \quad \sum_{k=1}^K w_k = 1θmerged=k=1∑Kwkθk,k=1∑Kwk=1

4.2.2 最优权重

定理 4.2(最优权重):在二次损失假设下,使合并模型测试损失最小的权重为:

w∗=arg⁡min⁡wL(∑kwkθk)w^* = \arg\min_w \mathcal{L}\left(\sum_k w_k \theta_k\right)w∗=argwminL(k∑wkθk)

求解:在验证集上搜索最优权重。

4.2.3 模型平均的集成视角

定理 4.3(平均与集成的关系):在二次损失假设下,模型平均的输出等于模型集成的输出:

fθˉ(x)≈1K∑k=1Kfθk(x)f_{\bar{\theta}}(x) \approx \frac{1}{K} \sum_{k=1}^K f_{\theta_k}(x)fθˉ(x)≈K1k=1∑Kfθk(x)

证明 :设 fθ(x)=θTxf_\theta(x) = \theta^T xfθ(x)=θTx(线性模型),则:

fθˉ(x)=θˉTx=(1K∑kθk)Tx=1K∑kθkTx=1K∑kfθk(x)f_{\bar{\theta}}(x) = \bar{\theta}^T x = \left(\frac{1}{K}\sum_k \theta_k\right)^T x = \frac{1}{K} \sum_k \theta_k^T x = \frac{1}{K} \sum_k f_{\theta_k}(x)fθˉ(x)=θˉTx=(K1k∑θk)Tx=K1k∑θkTx=K1k∑fθk(x)

对于非线性模型,这个等式只在 θk\theta_kθk 接近时近似成立。□\square□


第五章:球面线性插值(SLERP)

5.1 动机

5.1.1 线性插值的问题

问题 :线性插值 θ=αθ1+(1−α)θ2\theta = \alpha \theta_1 + (1-\alpha) \theta_2θ=αθ1+(1−α)θ2 可能导致:

  1. 权重范数变化 :∥θ∥\|\theta\|∥θ∥ 可能不等于 ∥θ1∥\|\theta_1\|∥θ1∥ 或 ∥θ2∥\|\theta_2\|∥θ2∥
  2. 损失增加:穿过高损失区域

5.1.2 SLERP 的思想

球面线性插值(Spherical Linear Interpolation, SLERP) :在超球面上进行插值,保持权重范数不变。

5.2 SLERP 的数学定义

5.2.1 2D 球面插值

定义 5.1(2D SLERP) :对于单位圆上的两个点 v1,v2\mathbf{v}_1, \mathbf{v}_2v1,v2,它们之间的球面插值为:

SLERP(v1,v2;t)=sin⁡((1−t)Ω)sin⁡Ωv1+sin⁡(tΩ)sin⁡Ωv2\text{SLERP}(\mathbf{v}_1, \mathbf{v}_2; t) = \frac{\sin((1-t)\Omega)}{\sin \Omega} \mathbf{v}_1 + \frac{\sin(t\Omega)}{\sin \Omega} \mathbf{v}_2SLERP(v1,v2;t)=sinΩsin((1−t)Ω)v1+sinΩsin(tΩ)v2

其中 Ω=arccos⁡(v1⋅v2)\Omega = \arccos(\mathbf{v}_1 \cdot \mathbf{v}_2)Ω=arccos(v1⋅v2) 是两个向量之间的夹角。

5.2.2 高维推广

定义 5.2(高维 SLERP) :对于 Rn\mathbb{R}^nRn 中的两个向量 w1,w2\mathbf{w}_1, \mathbf{w}_2w1,w2:

  1. 归一化:w^i=wi/∥wi∥\hat{\mathbf{w}}_i = \mathbf{w}_i / \|\mathbf{w}_i\|w^i=wi/∥wi∥
  2. 计算夹角:Ω=arccos⁡(w^1⋅w^2)\Omega = \arccos(\hat{\mathbf{w}}_1 \cdot \hat{\mathbf{w}}_2)Ω=arccos(w^1⋅w^2)
  3. 球面插值:w^=sin⁡((1−t)Ω)sin⁡Ωw^1+sin⁡(tΩ)sin⁡Ωw^2\hat{\mathbf{w}} = \frac{\sin((1-t)\Omega)}{\sin \Omega} \hat{\mathbf{w}}_1 + \frac{\sin(t\Omega)}{\sin \Omega} \hat{\mathbf{w}}_2w^=sinΩsin((1−t)Ω)w^1+sinΩsin(tΩ)w^2
  4. 恢复范数:w=∥w1∥⋅w^\mathbf{w} = \|\mathbf{w}_1\| \cdot \hat{\mathbf{w}}w=∥w1∥⋅w^(或使用其他范数插值策略)

5.3 SLERP 的理论分析

5.3.1 几何解释

定理 5.1(SLERP 的几何性质) :SLERP 沿着连接两个点的大圆弧(geodesic) 进行插值,这是球面上两点之间的最短路径。

证明 :在单位球面上,两点之间的最短路径是大圆弧。SLERP 正是沿着这条弧线等速运动。□\square□

5.3.2 与线性插值的比较

定理 5.2(SLERP 保持范数) :对于单位向量 w^1,w^2\hat{\mathbf{w}}_1, \hat{\mathbf{w}}_2w^1,w^2:

∥SLERP(w^1,w^2;t)∥=1,∀t∈0,1\|\text{SLERP}(\hat{\mathbf{w}}_1, \hat{\mathbf{w}}_2; t)\| = 1, \quad \forall t \in 0, 1∥SLERP(w^1,w^2;t)∥=1,∀t∈0,1

证明 :由 SLERP 的定义,∥SLERP∥2=sin⁡2((1−t)Ω)sin⁡2Ω+sin⁡2(tΩ)sin⁡2Ω+2sin⁡((1−t)Ω)sin⁡(tΩ)sin⁡2Ωcos⁡Ω\|\text{SLERP}\|^2 = \frac{\sin^2((1-t)\Omega)}{\sin^2\Omega} + \frac{\sin^2(t\Omega)}{\sin^2\Omega} + 2\frac{\sin((1-t)\Omega)\sin(t\Omega)}{\sin^2\Omega}\cos\Omega∥SLERP∥2=sin2Ωsin2((1−t)Ω)+sin2Ωsin2(tΩ)+2sin2Ωsin((1−t)Ω)sin(tΩ)cosΩ

利用三角恒等式可以证明这个等于 1。□\square□

推论 5.1:SLERP 避免了线性插值中权重范数变化的问题。

5.3.3 SLERP 的适用条件

定理 5.3(SLERP 的最优性条件):SLERP 在以下条件下优于线性插值:

  1. 权重向量的范数包含重要信息
  2. 损失曲面在球面上更平坦
  3. 两个模型的权重范数接近

第六章:任务算术(Task Arithmetic)

6.1 任务向量

6.1.1 定义

定义 6.1(任务向量) :对于预训练模型 θpre\theta_{\text{pre}}θpre 和微调模型 θft\theta_{\text{ft}}θft,任务向量定义为:

τ=θft−θpre\tau = \theta_{\text{ft}} - \theta_{\text{pre}}τ=θft−θpre

物理含义 :任务向量 τ\tauτ 编码了"从预训练到微调"的知识增量------即模型为了适应特定任务所做的修改。

6.1.2 任务向量的性质

定理 6.1(任务向量的低秩性) :在一定假设下,任务向量 τ\tauτ 的有效秩远小于权重矩阵的维度:

reff(τ)≪min⁡(m,n)r_{\text{eff}}(\tau) \ll \min(m, n)reff(τ)≪min(m,n)

证明 :微调通常只调整模型的一小部分方向(对应于任务相关的低维子空间),因此 τ\tauτ 是近似低秩的。□\square□

推论 6.1:任务向量可以用低秩分解来近似------这与 LoRA 的思想一致。

6.2 任务向量的算术运算

6.2.1 加法------多任务合并

定义 6.2(任务向量加法) :对于多个任务的任务向量 τ1,...,τK\tau_1, \dots, \tau_Kτ1,...,τK,合并后的模型为:

θmerged=θpre+λ∑k=1Kτk\theta_{\text{merged}} = \theta_{\text{pre}} + \lambda \sum_{k=1}^K \tau_kθmerged=θpre+λk=1∑Kτk

其中 λ\lambdaλ 是缩放系数。

物理含义:将多个任务的知识叠加到预训练模型上。

6.2.2 减法------任务遗忘

定义 6.3(任务向量减法):从模型中"减去"某个任务的知识:

θnew=θft−λτforget\theta_{\text{new}} = \theta_{\text{ft}} - \lambda \tau_{\text{forget}}θnew=θft−λτforget

物理含义:让模型"忘记"某个任务,同时保留其他能力。

6.2.3 线性插值------任务混合

定义 6.4(任务向量插值)

θmixed=θpre+ατ1+(1−α)τ2\theta_{\text{mixed}} = \theta_{\text{pre}} + \alpha \tau_1 + (1-\alpha) \tau_2θmixed=θpre+ατ1+(1−α)τ2

6.3 任务算术的理论分析

6.3.1 加法的理论

定理 6.2(任务向量加法的近似保证) :如果各任务的任务向量 τk\tau_kτk 近似正交(即 τiTτj≈0\tau_i^T \tau_j \approx 0τiTτj≈0 对 i≠ji \neq ji=j),则:

Lk(θpre+∑jτj)≈Lk(θpre+τk)\mathcal{L}k(\theta{\text{pre}} + \sum_j \tau_j) \approx \mathcal{L}k(\theta{\text{pre}} + \tau_k)Lk(θpre+j∑τj)≈Lk(θpre+τk)

证明 :设损失函数在 θpre\theta_{\text{pre}}θpre 附近是二次的:

Lk(θ)≈Lk(θpre)+gkT(θ−θpre)+12(θ−θpre)THk(θ−θpre)\mathcal{L}k(\theta) \approx \mathcal{L}k(\theta{\text{pre}}) + g_k^T (\theta - \theta{\text{pre}}) + \frac{1}{2} (\theta - \theta_{\text{pre}})^T H_k (\theta - \theta_{\text{pre}})Lk(θ)≈Lk(θpre)+gkT(θ−θpre)+21(θ−θpre)THk(θ−θpre)

当 τi\tau_iτi 和 τj\tau_jτj 正交时,交叉项 τiTHkτj\tau_i^T H_k \tau_jτiTHkτj 很小,因此:

Lk(θpre+∑jτj)≈Lk(θpre)+gkTτk+12τkTHkτk=Lk(θpre+τk)\mathcal{L}k(\theta{\text{pre}} + \sum_j \tau_j) \approx \mathcal{L}k(\theta{\text{pre}}) + g_k^T \tau_k + \frac{1}{2} \tau_k^T H_k \tau_k = \mathcal{L}k(\theta{\text{pre}} + \tau_k)Lk(θpre+j∑τj)≈Lk(θpre)+gkTτk+21τkTHkτk=Lk(θpre+τk)

□\square□

6.3.2 缩放系数的选择

定理 6.3(最优缩放系数) :对于 KKK 个任务,最优缩放系数 λ∗\lambda^*λ∗ 满足:

λ∗=arg⁡min⁡λ1K∑k=1KLk(θpre+λ∑jτj)\lambda^* = \arg\min_\lambda \frac{1}{K} \sum_{k=1}^K \mathcal{L}k(\theta{\text{pre}} + \lambda \sum_j \tau_j)λ∗=argλminK1k=1∑KLk(θpre+λj∑τj)

经验 :λ\lambdaλ 通常在 0.1,1.00.1, 1.00.1,1.0 之间,需要在验证集上搜索。


第三部分:高级合并方法


第七章:TIES-Merging------修剪冗余符号

7.1 核心观察

7.1.1 冲突问题

问题 :当多个任务向量在同一参数上有相反的符号(一个增加,一个减少)时,简单平均会导致抵消------两个任务的知识都被丢失。

例子

  • 任务 A 的 τA=+0.1,−0.2,+0.3\tau_A = +0.1, -0.2, +0.3τA=+0.1,−0.2,+0.3
  • 任务 B 的 τB=−0.1,+0.3,−0.2\tau_B = -0.1, +0.3, -0.2τB=−0.1,+0.3,−0.2
  • 平均:τˉ=0,+0.05,+0.05\bar{\tau} = 0, +0.05, +0.05τˉ=0,+0.05,+0.05------大部分信息丢失

7.1.2 冗余问题

问题 :许多 delta 参数的值很小,对模型性能影响微小------它们是冗余的

7.2 TIES-Merging 的算法

7.2.1 三步流程

算法 7.1(TIES-Merging)

复制代码
输入:预训练模型 θ_pre, 任务向量 {τ_1, ..., τ_K}, 修剪比例 p
输出:合并后的模型 θ_merged

1. 修剪(Trim):移除绝对值最小的 p% delta 参数
   对每个 τ_k:
     τ_k[|τ_k| < threshold] = 0

2. 符号选举(Elect Sign):确定每个参数的符号
   对每个参数 i:
     sign_i = majority_sign({τ_k[i] : τ_k[i] ≠ 0})

3. 合并(Merge):只合并与主符号一致的 delta
   τ_merged[i] = mean({τ_k[i] : τ_k[i] ≠ 0, sign(τ_k[i]) == sign_i})

4. 返回:θ_merged = θ_pre + λ * τ_merged

7.2.2 符号选举

定义 7.1(符号选举) :对于参数位置 iii,定义符号选举函数:

signi=sgn(∑k=1Ksgn(τki)⋅∣τki∣)\text{sign}i = \text{sgn}\left(\sum{k=1}^K \text{sgn}(\tau_ki) \cdot |\tau_ki|\right)signi=sgn(k=1∑Ksgn(τki)⋅∣τki∣)

即:按幅度加权投票决定符号。

定理 7.1(符号选举的最优性):在一定假设下,符号选举最小化合并后的总损失。

7.2.3 修剪策略

定义 7.2(幅度修剪) :对每个任务向量,移除绝对值最小的 p%p\%p% 参数:

τk′=τk⋅1∣τk∣≥τ(p)\tau_k' = \tau_k \cdot \mathbb{1}\|\\tau_k\| \\geq \\tau_{(p)}τk′=τk⋅1∣τk∣≥τ(p)

其中 τ(p)\tau_{(p)}τ(p) 是 ∣τk∣|\tau_k|∣τk∣ 的第 ppp 百分位数。


第八章:DARE------随机丢弃与重缩放

8.1 核心思想

8.1.1 观察

观察 :任务向量中的大部分 delta 参数是冗余的------移除它们对模型性能影响很小。

DARE (Yu et al., 2024)利用这个观察:随机丢弃 大部分 delta 参数,然后重缩放剩余参数以补偿。

8.1.2 DARE 的算法

算法 8.1(DARE)

复制代码
输入:预训练模型 θ_pre, 任务向量 τ, 丢弃率 p
输出:合并后的模型 θ_merged

1. 随机丢弃:对每个参数 i,以概率 p 将 τ[i] 设为 0
   mask ~ Bernoulli(1-p)
   τ' = τ * mask

2. 重缩放:将剩余参数放大以补偿
   τ'' = τ' / (1-p)

3. 合并:θ_merged = θ_pre + λ * τ''

8.1.3 理论分析

定理 8.1(DARE 的无偏性):DARE 的重缩放确保了期望值不变:

Eτ′′=Eτ⋅mask1−p=τ⋅(1−p)1−p=τ\mathbb{E}\\tau'' = \mathbb{E}\left\\frac{\\tau \\cdot \\text{mask}}{1-p}\\right = \frac{\tau \cdot (1-p)}{1-p} = \tauEτ′′=E1−pτ⋅mask=1−pτ⋅(1−p)=τ

证明 :每个 mask 元素的期望为 Emaski=1−p\mathbb{E}\\text{mask}_i = 1-pEmaski=1−p,因此 Eτi′′=τi⋅(1−p)/(1−p)=τi\mathbb{E}\\tau_i'' = \tau_i \cdot (1-p) / (1-p) = \tau_iEτi′′=τi⋅(1−p)/(1−p)=τi。□\square□

定理 8.2(DARE 的方差增加):DARE 增加了参数的方差:

Varτi′′=p1−pτi2\text{Var}\\tau_i'' = \frac{p}{1-p} \tau_i^2Varτi′′=1−ppτi2

推论 8.1 :丢弃率 ppp 越大,方差增加越多。但当 delta 参数接近零时(冗余参数),方差增加的影响很小。

8.2 DARE 与 TIES 的结合

8.2.1 DARE + TIES

算法 8.2(DARE + TIES)

复制代码
1. 对每个任务向量应用 DARE(随机丢弃 + 重缩放)
2. 对 DARE 后的任务向量应用 TIES(符号选举 + 合并)

8.2.2 理论分析

定理 8.3(DARE + TIES 的优势):DARE 解决了冗余问题,TIES 解决了冲突问题。两者结合可以更有效地合并多个任务向量。


第九章:Model Soups 与多模型融合

9.1 Model Soups

9.1.1 核心思想

Model Soups(Wortsman et al., 2022):将多个在不同超参数或数据增强下训练的模型进行平均。

9.1.2 Uniform Soup

定义 9.1(Uniform Soup)

θsoup=1K∑k=1Kθk\theta_{\text{soup}} = \frac{1}{K} \sum_{k=1}^K \theta_kθsoup=K1k=1∑Kθk

9.1.3 Greedy Soup

算法 9.1(Greedy Soup)

复制代码
输入:候选模型 {θ_1, ..., θ_K}, 验证集
输出:Greedy Soup 模型

1. 初始化:选择验证集上最好的模型作为初始 soup
2. 重复:
   a. 对每个未加入的候选模型,尝试加入 soup
   b. 计算加入后的验证集性能
   c. 如果性能提升,永久加入该模型
3. 直到没有模型能提升性能

9.2 Task Vectors 的多模型融合

9.2.1 符号共识

定义 9.2(符号共识) :对于 KKK 个任务向量,符号共识定义为:

signi=majority(sign(τ1i),...,sign(τKi))\text{sign}_i = \text{majority}(\text{sign}(\tau_1i), \dots, \text{sign}(\tau_Ki))signi=majority(sign(τ1i),...,sign(τKi))

9.2.2 幅度加权合并

定义 9.3(幅度加权合并)

τmergedi=∑k=1K∣τki∣⋅1sign(τk\[i)=signi]⋅τki∑k=1K1sign(τk\[i)=signi]\tau_{\text{merged}}i = \frac{\sum_{k=1}^K |\tau_ki| \cdot \mathbb{1}\\text{sign}(\\tau_k\[i) = \text{sign}i] \cdot \tau_ki}{\sum{k=1}^K \mathbb{1}\\text{sign}(\\tau_k\[i) = \text{sign}_i]}τmergedi=∑k=1K1sign(τk\[i)=signi]∑k=1K∣τki∣⋅1sign(τk\[i)=signi]⋅τki


第四部分:理论分析与前沿


第十章:合并的理论保证

10.1 合并误差的上界

10.1.1 二次损失假设

定理 10.1(合并误差上界):在二次损失假设下,合并模型与各任务最优模型之间的差距为:

Lk(θmerged)−Lk(θk∗)≤λmax⁡(Hk)2∥θmerged−θk∗∥2\mathcal{L}k(\theta{\text{merged}}) - \mathcal{L}k(\theta_k^*) \leq \frac{\lambda{\max}(H_k)}{2} \|\theta_{\text{merged}} - \theta_k^*\|^2Lk(θmerged)−Lk(θk∗)≤2λmax(Hk)∥θmerged−θk∗∥2

推论 10.1:如果合并后的模型与各任务最优模型的距离很小,合并误差也很小。

10.1.2 正交任务假设

定理 10.2(正交任务的完美合并) :如果各任务的任务向量正交(τiTτj=0\tau_i^T \tau_j = 0τiTτj=0 对 i≠ji \neq ji=j),且损失函数是二次的,则存在缩放系数 λ\lambdaλ 使得:

Lk(θpre+λ∑jτj)=Lk(θpre+τk),∀k\mathcal{L}k(\theta{\text{pre}} + \lambda \sum_j \tau_j) = \mathcal{L}k(\theta{\text{pre}} + \tau_k), \quad \forall kLk(θpre+λj∑τj)=Lk(θpre+τk),∀k

证明 :在二次损失下,正交性确保了交叉项为零。□\square□

10.2 合并的不可能性定理

10.2.1 冲突任务

定理 10.3(冲突任务的合并下界):如果两个任务在某些参数上有相反的最优更新方向,则合并模型的性能不可能同时达到两个任务的最优。

证明 :设 τ1∗=+ϵei\tau_1^* = +\epsilon \mathbf{e}_iτ1∗=+ϵei,τ2∗=−ϵei\tau_2^* = -\epsilon \mathbf{e}iτ2∗=−ϵei(在参数 iii 上相反)。对任何合并 τmerged\tau{\text{merged}}τmerged:

∥τmerged−τ1∗∥2+∥τmerged−τ2∗∥2≥12∥τ1∗−τ2∗∥2=2ϵ2\|\tau_{\text{merged}} - \tau_1^*\|^2 + \|\tau_{\text{merged}} - \tau_2^*\|^2 \geq \frac{1}{2}\|\tau_1^* - \tau_2^*\|^2 = 2\epsilon^2∥τmerged−τ1∗∥2+∥τmerged−τ2∗∥2≥21∥τ1∗−τ2∗∥2=2ϵ2

因此不可能同时精确合并两个任务。□\square□


第十一章:合并与泛化

11.1 合并的正则化效应

11.1.1 平均作为正则化

定理 11.1(模型平均的正则化) :模型平均等价于在参数空间中施加 ℓ2\ell_2ℓ2 正则化:

θˉ=arg⁡min⁡θ∑k=1K∥θ−θk∥2\bar{\theta} = \arg\min_\theta \sum_{k=1}^K \|\theta - \theta_k\|^2θˉ=argθmink=1∑K∥θ−θk∥2

推论:模型平均倾向于找到各模型的"中心"------这可能在损失曲面的平坦区域,有利于泛化。

11.1.2 合并与平坦极小值

定理 11.2(合并与平坦性) :合并后的模型通常位于损失曲面的平坦区域------因为它是多个极小值的"平均"。

推论:平坦极小值通常具有更好的泛化性------这解释了为什么合并后的模型泛化性能好。


第十二章:大语言模型的合并实践

12.1 LLM 合并的特殊挑战

12.1.1 模型规模

问题:LLM 的参数量巨大(7B-70B),直接在权重空间中操作需要大量内存。

解决方案

  • 使用低精度存储(FP16/BF16)
  • 逐层处理,不需要同时加载所有模型
  • 使用任务向量而非完整权重

12.1.2 任务多样性

问题:LLM 的微调任务多种多样(指令跟随、代码生成、数学推理等),任务向量之间的冲突更严重。

解决方案

  • 使用 TIES 或 DARE 处理冲突
  • 对不同任务使用不同的缩放系数

12.2 实践建议

12.2.1 合并策略选择

场景 推荐方法 理由
2-3 个相似任务 SLERP 保持权重范数
3-10 个不同任务 TIES-Merging 处理冲突
10+ 个任务 DARE + TIES 处理冗余和冲突
需要精确控制 任务算术 可解释性强

12.2.2 超参数选择

超参数 推荐范围 说明
缩放系数 λ\lambdaλ 0.1 - 1.0 在验证集上搜索
修剪比例 ppp 50% - 90% DARE 的丢弃率
秩 rrr 16 - 64 LoRA 的秩

第五部分:完整可运行代码实现


第十三章:从零实现权重对齐

python 复制代码
"""
权重对齐的完整实现。
包含:基于激活的对齐、匈牙利算法。
"""

import numpy as np
from typing import Tuple, List


def compute_activation_similarity(
    A1: np.ndarray,
    A2: np.ndarray,
) -> np.ndarray:
    """计算两个模型激活的相似度矩阵。

    Args:
        A1: 模型 1 的激活 (T, n)
        A2: 模型 2 的激活 (T, n)

    Returns:
        S: 相似度矩阵 (n, n)
    """
    # 归一化
    A1_norm = A1 / (np.linalg.norm(A1, axis=0, keepdims=True) + 1e-8)
    A2_norm = A2 / (np.linalg.norm(A2, axis=0, keepdims=True) + 1e-8)

    # 余弦相似度
    S = A1_norm.T @ A2_norm  # (n, n)

    return S


def hungarian_algorithm(cost_matrix: np.ndarray) -> np.ndarray:
    """匈牙利算法(简化版)------贪心近似。

    Args:
        cost_matrix: 代价矩阵 (n, n)

    Returns:
        permutation: 最优排列
    """
    n = cost_matrix.shape[0]
    used_rows = set()
    used_cols = set()
    permutation = np.zeros(n, dtype=int)

    # 贪心匹配
    for _ in range(n):
        best_val = -np.inf
        best_i, best_j = -1, -1

        for i in range(n):
            if i in used_rows:
                continue
            for j in range(n):
                if j in used_cols:
                    continue
                if cost_matrix[i, j] > best_val:
                    best_val = cost_matrix[i, j]
                    best_i, best_j = i, j

        permutation[best_i] = best_j
        used_rows.add(best_i)
        used_cols.add(best_j)

    return permutation


def align_models_by_activation(
    W1: np.ndarray,
    W2: np.ndarray,
    X: np.ndarray,
) -> Tuple[np.ndarray, np.ndarray]:
    """基于激活的模型对齐。

    Args:
        W1: 模型 1 的权重 (m, n)
        W2: 模型 2 的权重 (m, n)
        X: 校准输入 (T, n)

    Returns:
        W2_aligned: 对齐后的模型 2 权重
        perm: 排列
    """
    # 计算激活
    A1 = X @ W1.T  # (T, m)
    A2 = X @ W2.T  # (T, m)

    # 计算相似度
    S = compute_activation_similarity(A1, A2)

    # 找到最优排列
    perm = hungarian_algorithm(S)

    # 应用排列
    W2_aligned = W2[perm, :]

    return W2_aligned, perm


def align_models_by_weight(
    W1: np.ndarray,
    W2: np.ndarray,
) -> Tuple[np.ndarray, np.ndarray]:
    """基于权重的模型对齐。

    Args:
        W1: 模型 1 的权重 (m, n)
        W2: 模型 2 的权重 (m, n)

    Returns:
        W2_aligned: 对齐后的模型 2 权重
        perm: 排列
    """
    # 计算权重相似度
    S = np.abs(W1 @ W2.T)  # (m, m)

    # 找到最优排列
    perm = hungarian_algorithm(S)

    # 应用排列
    W2_aligned = W2[perm, :]

    return W2_aligned, perm


def demonstrate_alignment():
    """演示权重对齐。"""
    np.random.seed(42)

    print("=" * 70)
    print("权重对齐演示")
    print("=" * 70)

    m, n = 32, 64
    T = 100

    # 创建两个"相关"的模型(模拟从同一预训练模型微调)
    W_pre = np.random.randn(m, n) * 0.02

    # 模型 1:微调方向 1
    delta1 = np.random.randn(m, n) * 0.005
    W1 = W_pre + delta1

    # 模型 2:微调方向 2,但排列不同
    perm_true = np.random.permutation(m)
    delta2 = np.random.randn(m, n) * 0.005
    W2 = W_pre[perm_true, :] + delta2

    X = np.random.randn(T, n) * 0.5

    print(f"\n  权重矩阵: {m}x{n}")
    print(f"  真实排列: {perm_true[:10]}...")

    # 对齐前的距离
    dist_before = np.linalg.norm(W1 - W2, 'fro')
    print(f"\n  对齐前距离: {dist_before:.6f}")

    # 基于权重的对齐
    W2_aligned_w, perm_w = align_models_by_weight(W1, W2)
    dist_after_w = np.linalg.norm(W1 - W2_aligned_w, 'fro')
    print(f"  权重对齐后距离: {dist_after_w:.6f}")
    print(f"  排列匹配率: {np.mean(perm_w == perm_true):.2%}")

    # 基于激活的对齐
    W2_aligned_a, perm_a = align_models_by_activation(W1, W2, X)
    dist_after_a = np.linalg.norm(W1 - W2_aligned_a, 'fro')
    print(f"  激活对齐后距离: {dist_after_a:.6f}")
    print(f"  排列匹配率: {np.mean(perm_a == perm_true):.2%}")

    # 对齐后合并
    print("\n  对齐后合并效果:")
    for alpha in [0.0, 0.25, 0.5, 0.75, 1.0]:
        # 不对齐的合并
        W_merge_no_align = alpha * W1 + (1 - alpha) * W2
        Y_ref = X @ W1.T
        Y_no_align = X @ W_merge_no_align.T
        mse_no_align = np.mean((Y_ref - Y_no_align) ** 2)

        # 对齐后的合并
        W_merge_align = alpha * W1 + (1 - alpha) * W2_aligned_a
        Y_align = X @ W_merge_align.T
        mse_align = np.mean((Y_ref - Y_align) ** 2)

        print(f"    α={alpha:.2f}: 不对齐 MSE={mse_no_align:.8f}, "
              f"对齐 MSE={mse_align:.8f}")


if __name__ == "__main__":
    demonstrate_alignment()

第十四章:从零实现 SLERP 与任务算术

python 复制代码
"""
SLERP 和任务算术的完整实现。
"""

import numpy as np
from typing import Tuple


def slerp(
    w1: np.ndarray,
    w2: np.ndarray,
    t: float,
) -> np.ndarray:
    """球面线性插值(SLERP)。

    Args:
        w1: 向量 1
        w2: 向量 2
        t: 插值系数 [0, 1]

    Returns:
        w: 插值结果
    """
    # 归一化
    norm1 = np.linalg.norm(w1)
    norm2 = np.linalg.norm(w2)

    if norm1 < 1e-10 or norm2 < 1e-10:
        return (1 - t) * w1 + t * w2

    w1_hat = w1 / norm1
    w2_hat = w2 / norm2

    # 计算夹角
    cos_omega = np.clip(np.dot(w1_hat, w2_hat), -1.0, 1.0)
    omega = np.arccos(cos_omega)

    if omega < 1e-6:
        # 夹角很小,退化为线性插值
        return (1 - t) * w1 + t * w2

    # SLERP
    sin_omega = np.sin(omega)
    w_hat = (np.sin((1 - t) * omega) / sin_omega) * w1_hat + \
            (np.sin(t * omega) / sin_omega) * w2_hat

    # 恢复范数(线性插值范数)
    norm = (1 - t) * norm1 + t * norm2
    w = w_hat * norm

    return w


def slerp_matrices(
    W1: np.ndarray,
    W2: np.ndarray,
    t: float,
) -> np.ndarray:
    """对矩阵的每一行进行 SLERP。

    Args:
        W1: 权重矩阵 1 (m, n)
        W2: 权重矩阵 2 (m, n)
        t: 插值系数

    Returns:
        W: 插值结果
    """
    m = W1.shape[0]
    W = np.zeros_like(W1)

    for i in range(m):
        W[i] = slerp(W1[i], W2[i], t)

    return W


def compute_task_vector(
    W_ft: np.ndarray,
    W_pre: np.ndarray,
) -> np.ndarray:
    """计算任务向量。

    Args:
        W_ft: 微调后的权重
        W_pre: 预训练权重

    Returns:
        tau: 任务向量
    """
    return W_ft - W_pre


def task_arithmetic_add(
    W_pre: np.ndarray,
    task_vectors: list,
    lambda_scale: float = 1.0,
) -> np.ndarray:
    """任务向量加法。

    Args:
        W_pre: 预训练权重
        task_vectors: 任务向量列表
        lambda_scale: 缩放系数

    Returns:
        W_merged: 合并后的权重
    """
    tau_sum = sum(task_vectors)
    return W_pre + lambda_scale * tau_sum


def task_arithmetic_subtract(
    W_ft: np.ndarray,
    tau_forget: np.ndarray,
    lambda_scale: float = 1.0,
) -> np.ndarray:
    """任务向量减法(遗忘)。

    Args:
        W_ft: 微调后的权重
        tau_forget: 要遗忘的任务向量
        lambda_scale: 缩放系数

    Returns:
        W_new: 新权重
    """
    return W_ft - lambda_scale * tau_forget


def demonstrate_slerp():
    """演示 SLERP 和任务算术。"""
    np.random.seed(42)

    print("=" * 70)
    print("SLERP 与任务算术演示")
    print("=" * 70)

    m, n = 64, 128
    T = 200

    # 预训练模型
    W_pre = np.random.randn(m, n) * 0.02

    # 任务 1 的微调模型
    tau1 = np.random.randn(m, n) * 0.005
    W_task1 = W_pre + tau1

    # 任务 2 的微调模型
    tau2 = np.random.randn(m, n) * 0.005
    W_task2 = W_pre + tau2

    # 测试数据
    X = np.random.randn(T, n) * 0.5

    # SLERP 演示
    print("\n  1. SLERP vs 线性插值")
    print("  " + "-" * 40)

    print(f"  {'t':>6} {'线性插值 MSE':>15} {'SLERP MSE':>15} {'范数变化(线性)':>15} {'范数变化(SLERP)':>15}")
    print(f"  {'-'*6} {'-'*15} {'-'*15} {'-'*15} {'-'*15}")

    Y1 = X @ W_task1.T
    Y2 = X @ W_task2.T

    for t in [0.0, 0.25, 0.5, 0.75, 1.0]:
        # 线性插值
        W_linear = (1 - t) * W_task1 + t * W_task2
        Y_linear = X @ W_linear.T
        mse_linear = np.mean((Y1 - Y_linear) ** 2)
        norm_change_linear = np.linalg.norm(W_linear) / np.linalg.norm(W_task1)

        # SLERP
        W_slerp = slerp_matrices(W_task1, W_task2, t)
        Y_slerp = X @ W_slerp.T
        mse_slerp = np.mean((Y1 - Y_slerp) ** 2)
        norm_change_slerp = np.linalg.norm(W_slerp) / np.linalg.norm(W_task1)

        print(f"  {t:>6.2f} {mse_linear:>15.8f} {mse_slerp:>15.8f} "
              f"{norm_change_linear:>15.4f} {norm_change_slerp:>15.4f}")

    # 任务算术演示
    print("\n  2. 任务算术")
    print("  " + "-" * 40)

    # 计算任务向量
    tau1 = compute_task_vector(W_task1, W_pre)
    tau2 = compute_task_vector(W_task2, W_pre)

    print(f"  任务向量 1 范数: {np.linalg.norm(tau1):.6f}")
    print(f"  任务向量 2 范数: {np.linalg.norm(tau2):.6f}")
    print(f"  任务向量余弦相似度: {np.dot(tau1.flatten(), tau2.flatten()) / (np.linalg.norm(tau1) * np.linalg.norm(tau2)):.4f}")

    # 任务向量加法
    print(f"\n  任务向量加法 (不同 λ):")
    print(f"  {'λ':>6} {'任务1 MSE':>12} {'任务2 MSE':>12} {'平均 MSE':>12}")
    print(f"  {'-'*6} {'-'*12} {'-'*12} {'-'*12}")

    Y_task1 = X @ W_task1.T
    Y_task2 = X @ W_task2.T

    for lam in [0.1, 0.3, 0.5, 0.7, 1.0, 1.5]:
        W_merged = task_arithmetic_add(W_pre, [tau1, tau2], lambda_scale=lam)
        Y_merged = X @ W_merged.T
        mse1 = np.mean((Y_task1 - Y_merged) ** 2)
        mse2 = np.mean((Y_task2 - Y_merged) ** 2)
        avg_mse = (mse1 + mse2) / 2

        print(f"  {lam:>6.1f} {mse1:>12.8f} {mse2:>12.8f} {avg_mse:>12.8f}")

    # 任务向量减法(遗忘)
    print(f"\n  任务向量减法 (遗忘任务 1):")
    for lam in [0.0, 0.5, 1.0, 1.5]:
        W_new = task_arithmetic_subtract(W_task1, tau1, lambda_scale=lam)
        Y_new = X @ W_new.T
        mse_vs_pre = np.mean((X @ W_pre.T - Y_new) ** 2)
        mse_vs_task1 = np.mean((Y_task1 - Y_new) ** 2)
        print(f"    λ={lam:.1f}: 与预训练距离={mse_vs_pre:.8f}, 与任务1距离={mse_vs_task1:.8f}")


if __name__ == "__main__":
    demonstrate_slerp()

第十五章:从零实现 TIES 与 DARE

python 复制代码
"""
TIES-Merging 和 DARE 的完整实现。
"""

import numpy as np
from typing import List, Tuple


def ties_trim(
    tau: np.ndarray,
    trim_ratio: float = 0.5,
) -> np.ndarray:
    """TIES 修剪:移除绝对值最小的 delta 参数。

    Args:
        tau: 任务向量
        trim_ratio: 修剪比例

    Returns:
        tau_trimmed: 修剪后的任务向量
    """
    threshold = np.percentile(np.abs(tau.flatten()), trim_ratio * 100)
    mask = np.abs(tau) >= threshold
    return tau * mask


def ties_elect_sign(
    task_vectors: List[np.ndarray],
) -> np.ndarray:
    """TIES 符号选举:确定每个参数的主符号。

    Args:
        task_vectors: 任务向量列表

    Returns:
        sign_mask: 主符号 (+1 或 -1)
    """
    # 计算每个参数位置的符号投票
    sign_votes = np.zeros_like(task_vectors[0])

    for tau in task_vectors:
        sign_votes += np.sign(tau)

    # 主符号
    sign_mask = np.sign(sign_votes)
    # 处理零值(默认为 +1)
    sign_mask[sign_mask == 0] = 1

    return sign_mask


def ties_merge(
    task_vectors: List[np.ndarray],
    trim_ratio: float = 0.5,
) -> np.ndarray:
    """TIES-Merging 完整流程。

    Args:
        task_vectors: 任务向量列表
        trim_ratio: 修剪比例

    Returns:
        tau_merged: 合并后的任务向量
    """
    # 步骤 1:修剪
    trimmed_vectors = [ties_trim(tau, trim_ratio) for tau in task_vectors]

    # 步骤 2:符号选举
    sign_mask = ties_elect_sign(trimmed_vectors)

    # 步骤 3:合并(只保留与主符号一致的参数)
    tau_merged = np.zeros_like(task_vectors[0])
    count = np.zeros_like(task_vectors[0])

    for tau in trimmed_vectors:
        # 只保留符号一致的参数
        consistent = (np.sign(tau) == sign_mask) | (tau == 0)
        tau_merged += tau * consistent
        count += consistent.astype(float)

    # 平均
    count = np.maximum(count, 1)
    tau_merged = tau_merged / count

    return tau_merged


def dare_drop(
    tau: np.ndarray,
    drop_rate: float = 0.9,
) -> np.ndarray:
    """DARE 随机丢弃。

    Args:
        tau: 任务向量
        drop_rate: 丢弃率

    Returns:
        tau_dropped: 丢弃后的任务向量
    """
    mask = np.random.random(tau.shape) >= drop_rate
    tau_dropped = tau * mask / (1 - drop_rate)
    return tau_dropped


def dare_merge(
    task_vectors: List[np.ndarray],
    drop_rate: float = 0.9,
) -> np.ndarray:
    """DARE 合并。

    Args:
        task_vectors: 任务向量列表
        drop_rate: 丢弃率

    Returns:
        tau_merged: 合并后的任务向量
    """
    # 对每个任务向量应用 DARE
    dare_vectors = [dare_drop(tau, drop_rate) for tau in task_vectors]

    # 平均
    tau_merged = np.mean(dare_vectors, axis=0)

    return tau_merged


def dare_ties_merge(
    task_vectors: List[np.ndarray],
    drop_rate: float = 0.9,
    trim_ratio: float = 0.5,
) -> np.ndarray:
    """DARE + TIES 合并。

    Args:
        task_vectors: 任务向量列表
        drop_rate: DARE 丢弃率
        trim_ratio: TIES 修剪比例

    Returns:
        tau_merged: 合并后的任务向量
    """
    # 步骤 1:DARE 随机丢弃
    dare_vectors = [dare_drop(tau, drop_rate) for tau in task_vectors]

    # 步骤 2:TIES 合并
    tau_merged = ties_merge(dare_vectors, trim_ratio)

    return tau_merged


def demonstrate_ties_dare():
    """演示 TIES 和 DARE。"""
    np.random.seed(42)

    print("=" * 70)
    print("TIES-Merging 与 DARE 演示")
    print("=" * 70)

    m, n = 64, 128
    K = 5  # 任务数量
    T = 200

    # 预训练模型
    W_pre = np.random.randn(m, n) * 0.02

    # 生成多个任务的任务向量
    task_vectors = []
    W_tasks = []
    for k in range(K):
        tau = np.random.randn(m, n) * 0.005
        task_vectors.append(tau)
        W_tasks.append(W_pre + tau)

    # 测试数据
    X = np.random.randn(T, n) * 0.5

    print(f"\n  权重矩阵: {m}x{n}")
    print(f"  任务数量: {K}")

    # 各方法对比
    print("\n  1. 不同合并方法对比")
    print("  " + "-" * 40)

    # 计算各任务的参考输出
    Y_refs = [X @ W.T for W in W_tasks]

    methods = {}

    # 简单平均
    tau_avg = np.mean(task_vectors, axis=0)
    W_avg = W_pre + tau_avg
    Y_avg = X @ W_avg.T
    mse_avg = np.mean([np.mean((Y_ref - Y_avg) ** 2) for Y_ref in Y_refs])
    methods["简单平均"] = mse_avg

    # TIES
    tau_ties = ties_merge(task_vectors, trim_ratio=0.5)
    W_ties = W_pre + tau_ties
    Y_ties = X @ W_ties.T
    mse_ties = np.mean([np.mean((Y_ref - Y_ties) ** 2) for Y_ref in Y_refs])
    methods["TIES (trim=0.5)"] = mse_ties

    # DARE
    tau_dare = dare_merge(task_vectors, drop_rate=0.9)
    W_dare = W_pre + tau_dare
    Y_dare = X @ W_dare.T
    mse_dare = np.mean([np.mean((Y_ref - Y_dare) ** 2) for Y_ref in Y_refs])
    methods["DARE (drop=0.9)"] = mse_dare

    # DARE + TIES
    tau_dt = dare_ties_merge(task_vectors, drop_rate=0.9, trim_ratio=0.5)
    W_dt = W_pre + tau_dt
    Y_dt = X @ W_dt.T
    mse_dt = np.mean([np.mean((Y_ref - Y_dt) ** 2) for Y_ref in Y_refs])
    methods["DARE + TIES"] = mse_dt

    print(f"\n  {'方法':>20} {'平均 MSE':>15}")
    print(f"  {'-'*20} {'-'*15}")
    for name, mse in methods.items():
        print(f"  {name:>20} {mse:>15.8f}")

    # 不同丢弃率的影响
    print("\n  2. DARE 丢弃率的影响")
    print("  " + "-" * 40)

    print(f"  {'丢弃率':>10} {'平均 MSE':>15} {'参数非零率':>12}")
    print(f"  {'-'*10} {'-'*15} {'-'*12}")

    for drop_rate in [0.0, 0.5, 0.7, 0.9, 0.95, 0.99]:
        tau_d = dare_merge(task_vectors, drop_rate=drop_rate)
        W_d = W_pre + tau_d
        Y_d = X @ W_d.T
        mse_d = np.mean([np.mean((Y_ref - Y_d) ** 2) for Y_ref in Y_refs])
        nnz = np.mean(tau_d != 0)

        print(f"  {drop_rate:>10.2f} {mse_d:>15.8f} {nnz:>12.2%}")

    # 不同修剪比例的影响
    print("\n  3. TIES 修剪比例的影响")
    print("  " + "-" * 40)

    print(f"  {'修剪比例':>10} {'平均 MSE':>15}")
    print(f"  {'-'*10} {'-'*15}")

    for trim_ratio in [0.0, 0.3, 0.5, 0.7, 0.9]:
        tau_t = ties_merge(task_vectors, trim_ratio=trim_ratio)
        W_t = W_pre + tau_t
        Y_t = X @ W_t.T
        mse_t = np.mean([np.mean((Y_ref - Y_t) ** 2) for Y_ref in Y_refs])

        print(f"  {trim_ratio:>10.2f} {mse_t:>15.8f}")


if __name__ == "__main__":
    demonstrate_ties_dare()

第十六章:完整合并 Pipeline 与精度对比

python 复制代码
"""
完整的模型合并 Pipeline。
对比所有合并方法。
"""

import numpy as np
from typing import List


def run_full_comparison():
    """运行完整的合并方法对比。"""
    np.random.seed(42)

    print("=" * 70)
    print("模型合并方法综合对比")
    print("=" * 70)

    # 设置
    m, n = 64, 128
    K = 5
    T = 300

    # 预训练模型
    W_pre = np.random.randn(m, n) * 0.02

    # 任务向量(模拟不同任务)
    task_vectors = []
    W_tasks = []
    for k in range(K):
        tau = np.random.randn(m, n) * 0.005
        task_vectors.append(tau)
        W_tasks.append(W_pre + tau)

    X = np.random.randn(T, n) * 0.5
    Y_refs = [X @ W.T for W in W_tasks]

    print(f"\n  权重: {m}x{n}, 任务数: {K}")

    # 定义所有方法
    def simple_average(taus, **kwargs):
        return np.mean(taus, axis=0)

    def slerp_average(taus, **kwargs):
        # 简化:对每个参数位置独立 SLERP
        result = taus[0].copy()
        for i in range(1, len(taus)):
            t = 1.0 / (i + 1)
            # 简化的 SLERP(使用线性插值近似)
            result = (1 - t) * result + t * taus[i]
        return result

    def ties_method(taus, trim_ratio=0.5, **kwargs):
        # 修剪
        trimmed = []
        for tau in taus:
            threshold = np.percentile(np.abs(tau.flatten()), trim_ratio * 100)
            trimmed.append(tau * (np.abs(tau) >= threshold))

        # 符号选举
        sign_votes = sum(np.sign(tau) for tau in trimmed)
        sign_mask = np.sign(sign_votes)
        sign_mask[sign_mask == 0] = 1

        # 合并
        result = np.zeros_like(taus[0])
        count = np.zeros_like(taus[0])
        for tau in trimmed:
            consistent = (np.sign(tau) == sign_mask) | (tau == 0)
            result += tau * consistent
            count += consistent.astype(float)
        count = np.maximum(count, 1)
        return result / count

    def dare_method(taus, drop_rate=0.9, **kwargs):
        dropped = []
        for tau in taus:
            mask = np.random.random(tau.shape) >= drop_rate
            dropped.append(tau * mask / (1 - drop_rate))
        return np.mean(dropped, axis=0)

    def dare_ties_method(taus, drop_rate=0.9, trim_ratio=0.5, **kwargs):
        dropped = []
        for tau in taus:
            mask = np.random.random(tau.shape) >= drop_rate
            dropped.append(tau * mask / (1 - drop_rate))
        return ties_method(dropped, trim_ratio=trim_ratio)

    # 测试所有方法
    methods = {
        "简单平均": simple_average,
        "TIES (trim=0.5)": lambda taus: ties_method(taus, trim_ratio=0.5),
        "DARE (drop=0.9)": lambda taus: dare_method(taus, drop_rate=0.9),
        "DARE + TIES": lambda taus: dare_ties_method(taus, drop_rate=0.9, trim_ratio=0.5),
    }

    print(f"\n  {'方法':>20} {'任务1':>10} {'任务2':>10} {'任务3':>10} {'任务4':>10} {'任务5':>10} {'平均':>10}")
    print(f"  {'-'*20} {'-'*10} {'-'*10} {'-'*10} {'-'*10} {'-'*10} {'-'*10}")

    for name, method_fn in methods.items():
        tau_merged = method_fn(task_vectors)
        W_merged = W_pre + tau_merged
        Y_merged = X @ W_merged.T

        mses = [np.mean((Y_ref - Y_merged) ** 2) for Y_ref in Y_refs]
        avg_mse = np.mean(mses)

        print(f"  {name:>20} ", end="")
        for mse in mses:
            print(f"{mse:>10.6f} ", end="")
        print(f"{avg_mse:>10.6f}")

    # 缩放系数的影响
    print(f"\n  缩放系数 λ 的影响 (简单平均):")
    print(f"  {'λ':>6} {'平均 MSE':>15}")
    print(f"  {'-'*6} {'-'*15}")

    for lam in [0.1, 0.3, 0.5, 0.7, 1.0, 1.5, 2.0]:
        tau_merged = lam * simple_average(task_vectors)
        W_merged = W_pre + tau_merged
        Y_merged = X @ W_merged.T
        avg_mse = np.mean([np.mean((Y_ref - Y_merged) ** 2) for Y_ref in Y_refs])
        print(f"  {lam:>6.1f} {avg_mse:>15.8f}")


if __name__ == "__main__":
    run_full_comparison()

附录:关键公式汇总

A.1 损失曲面与 LMC

公式 表达式
损失曲面 L(θ)=Eℓ(fθ(x),y)\mathcal{L}(\theta) = \mathbb{E}\\ell(f_\\theta(x), y)L(θ)=Eℓ(fθ(x),y)
LMC 条件 L(αθ1+(1−α)θ2)≤max⁡(L(θ1),L(θ2))\mathcal{L}(\alpha\theta_1 + (1-\alpha)\theta_2) \leq \max(\mathcal{L}(\theta_1), \mathcal{L}(\theta_2))L(αθ1+(1−α)θ2)≤max(L(θ1),L(θ2))
凸性条件 λmax⁡(H)≤0\lambda_{\max}(H) \leq 0λmax(H)≤0

A.2 任务算术

公式 表达式
任务向量 τ=θft−θpre\tau = \theta_{\text{ft}} - \theta_{\text{pre}}τ=θft−θpre
加法合并 θmerged=θpre+λ∑kτk\theta_{\text{merged}} = \theta_{\text{pre}} + \lambda \sum_k \tau_kθmerged=θpre+λ∑kτk
减法遗忘 θnew=θft−λτforget\theta_{\text{new}} = \theta_{\text{ft}} - \lambda \tau_{\text{forget}}θnew=θft−λτforget

A.3 SLERP

公式 表达式
SLERP SLERP(v1,v2;t)=sin⁡((1−t)Ω)sin⁡Ωv1+sin⁡(tΩ)sin⁡Ωv2\text{SLERP}(\mathbf{v}_1, \mathbf{v}_2; t) = \frac{\sin((1-t)\Omega)}{\sin\Omega}\mathbf{v}_1 + \frac{\sin(t\Omega)}{\sin\Omega}\mathbf{v}_2SLERP(v1,v2;t)=sinΩsin((1−t)Ω)v1+sinΩsin(tΩ)v2
夹角 Ω=arccos⁡(v^1⋅v^2)\Omega = \arccos(\hat{\mathbf{v}}_1 \cdot \hat{\mathbf{v}}_2)Ω=arccos(v^1⋅v^2)

A.4 TIES-Merging

步骤 操作
修剪 $\tau[
符号选举 signi=majority(sign(τki))\text{sign}_i = \text{majority}(\text{sign}(\tau_ki))signi=majority(sign(τki))
合并 τmergedi=mean(τki:sign(τki)=signi)\tau_{\text{merged}}i = \text{mean}(\tau_ki : \text{sign}(\tau_ki) = \text{sign}_i)τmergedi=mean(τki:sign(τki)=signi)

A.5 DARE

公式 表达式
随机丢弃 τ′=τ⋅mask\tau' = \tau \cdot \text{mask}τ′=τ⋅mask, mask ∼\sim∼ Bernoulli(1−p)(1-p)(1−p)
重缩放 τ′′=τ′/(1−p)\tau'' = \tau' / (1-p)τ′′=τ′/(1−p)
无偏性 Eτ′′=τ\mathbb{E}\\tau'' = \tauEτ′′

参考文献

  1. Wortsman, M., et al. (2022). Model soups: averaging weights of multiple fine-tuned models improves accuracy without increasing inference time. ICML.
  2. Ilharco, G., et al. (2023). Editing models with task arithmetic. ICLR.
  3. Ainsworth, S., et al. (2023). Git rebasin: Merging models modulo permutation symmetries. ICLR.
  4. Yadav, P., et al. (2023). TIES-Merging: Resolving interference when merging models. NeurIPS.
  5. Yu, L., et al. (2024). Language models are super mario: Absorbing abilities from homologous models as a free lunch. ICML.
  6. Neyshabur, B., et al. (2020). What is being transferred in transfer learning? NeurIPS.
  7. Frankle, J., & Carlin, M. (2019). The lottery ticket hypothesis. ICLR.
  8. Matena, M., & Raffel, C. (2022). Merging models with Fisher-weighted averaging. NeurIPS.
  9. Singh, S., & Jaggi, M. (2020). Model fusion via optimal transport. NeurIPS.
  10. Don-Yehiya, S., et al. (2022). ColD fusion: Collaborative descent for distributed multitask finetuning. ACL.
相关推荐
memcpy01 小时前
LeetCode 2144. 打折购买糖果的最小开销【贪心】
算法·leetcode·职场和发展
散峰而望2 小时前
【算法练习】算法练习精选:陶陶摘苹果(基础+升级)、Music Notes、字串变换,你能AC几道?
数据结构·c++·算法·leetcode·贪心算法·github·动态规划
暗夜猎手-大魔王3 小时前
转载--Hermes Agent 04 | Agent 主循环:一次对话背后发生了什么
人工智能·python·算法
手写码匠3 小时前
华为云Flexus+DeepSeek征文|基于华为云Flexus X实例 + Dify + DeepSeek 构建企业级智能知识库问答系统实战
人工智能·深度学习·算法·aigc
吴可可1233 小时前
Win7上开发CAD2004自定义实体全解析
c++·算法
YXXY3133 小时前
二叉树中的深搜算法介绍
算法
zz34572981133 小时前
C语言中字符串常量存储位置
c语言·开发语言·算法·青少年编程
noipp3 小时前
推荐题目:洛谷 P16510 [GKS 2015 #C] gRanks
java·c语言·开发语言·c++·python·算法
菜菜的顾清寒4 小时前
力扣HOT100(50)动态规划-零钱兑换
算法·leetcode·动态规划