
1. 项目背景与核心需求为什么需要“剥离”微调参数在大型语言模型LLM的微调实践中我们常常会遇到一个看似简单、实则关键的场景你基于一个强大的开源基座模型比如 Qwen、Llama 或 ChatGLM使用自己的数据进行了全参数微调Full Parameter Fine-Tuning。训练完成后你得到了一个包含基座模型原始参数和你新增知识的完整模型文件。此时你可能想分享自己的训练成果但直接发布这个几十GB甚至上百GB的完整模型既不经济存储和传输成本高也不符合开源协作的精神——因为基座模型本身可能是有特定许可协议的。更常见的需求是你希望只发布你“新增”的那部分知识即微调带来的参数变化量。这样其他使用者可以轻松地将你的微调模块加载到他们本地的、同版本的基座模型上快速获得你赋予模型的能力而无需下载整个庞然大物。这个过程就是标题中所说的“从全参数微调后的模型参数中剔除基座模型参数冻结然后发布自己训练的这部分参数模块”。这不仅是模型分发的高效方式也是构建模块化、可组合AI能力生态的基础。然而标准的模型保存方式如model.save_pretrained()保存的是整个模型的状态字典。如何精准地“剥离”出我们新增的参数部分呢这正是 PEFTParameter-Efficient Fine-Tuning库的用武之地。虽然 PEFT 本身更常用于 LoRA、Prefix Tuning 等高效微调方法但其底层对模型参数的精细操作能力使其成为处理此类“参数手术”的绝佳工具。本文将深入探讨如何利用 PEFT 库及相关技巧完成从全参数微调模型中提取增量参数的任务。2. 理解参数结构基座模型、全量微调与增量Delta在动手之前我们必须从概念上厘清我们要操作的对象。一个经过全参数微调的模型其参数可以看作由两部分组成基座模型参数Base Model Parameters这是微调开始前预训练模型已有的参数。它包含了模型从海量通用数据中学到的语言知识、世界知识和推理能力。增量参数或参数变化量Parameter Delta / Incremental Weights这是微调过程中模型参数相对于基座模型所发生的变化。它编码了你的特定任务数据如领域知识、特殊指令格式、风格偏好所带来的知识更新。用公式可以粗略表示为微调后模型参数 基座模型参数 参数变化量Delta我们的目标就是从等号左边分离出右边的“参数变化量”。这里有一个关键认知对于全参数微调这个“变化量”并不是像 LoRA 那样以低秩适配器lora_A,lora_B的显式、结构化形式存在。它是弥散在整个模型权重张量中的、每个参数点上数值的微小改变。因此我们的“剥离”操作在数学上就是一次简单的减法参数变化量 微调后模型参数 - 基座模型参数但在工程上我们需要确保两个前提模型结构完全一致微调后的模型和作为减数的基座模型必须有完全相同的网络结构、层数和参数命名。参数精确对齐进行减法操作时必须保证每个参数张量都一一对应。如果微调过程中没有改变模型结构例如没有新增或删除层那么第一个条件通常自动满足。第二个条件则需要我们通过代码来保证精确的键值匹配。PEFT 库提供了强大的工具来加载、管理和操作模型参数状态字典使得这种精细的减法操作变得可行且相对安全。3. 实操准备环境、模型与工具链在开始编码之前我们需要搭建一个稳定且功能齐全的工作环境。以下是我在实际操作中验证过的配置方案它平衡了易用性和灵活性。3.1 核心依赖安装首先创建一个新的 Python 虚拟环境是良好的实践可以避免包版本冲突。然后安装核心库# 创建并激活虚拟环境以 conda 为例 conda create -n param_surgery python3.10 conda activate param_surgery # 安装 PyTorch请根据你的 CUDA 版本选择对应命令此处以 CUDA 11.8 为例 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # 安装 transformers 和 peft pip install transformers peft accelerate # 可选但推荐安装 huggingface-hub 用于模型下载safetensors 用于安全高效的模型保存 pip install huggingface-hub safetensors注意accelerate库对于高效地加载大模型至关重要。safetensors格式相比传统的pytorch_model.bin更安全避免恶意代码执行且加载速度更快是当前社区推荐的标准。3.2 模型准备获取基座与微调后模型假设我们已经有了两个模型基座模型Base Model例如Qwen/Qwen2-7B可以从 Hugging Face Hub 下载或者使用你微调前本地保存的原始模型副本。全参数微调后的模型Fine-tuned Model这是你训练完成后的完整模型保存在本地目录例如./my_finetuned_qwen7b。关键步骤确保基座模型版本一致这是整个操作中最容易出错的一环。你必须使用与微调起点完全相同的基座模型。如果你是从 Hugging Face Hub 拉取的模型请记录下确切的revision提交哈希例如Qwen/Qwen2-7Bmain在训练开始时的具体哈希值。如果微调过程中 Hub 上的模型有更新直接拉取main分支可能会导致参数结构对不上。最稳妥的方式是在训练开始时将基座模型完整地保存一份到本地。from transformers import AutoModelForCausalLM # 加载基座模型示例从本地路径 base_model AutoModelForCausalLM.from_pretrained( ./original_qwen2_7b_snapshot, # 你的基座模型本地路径 torch_dtypetorch.float16, # 通常使用半精度以节省内存 device_mapauto, # 使用 accelerate 自动分配设备CPU/GPU trust_remote_codeTrue # 对于 Qwen 等模型可能需要 ) # 加载微调后的完整模型 finetuned_model AutoModelForCausalLM.from_pretrained( ./my_finetuned_qwen7b, torch_dtypetorch.float16, device_mapauto, trust_remote_codeTrue )3.3 工具函数构思参数对比与校验在进行减法之前写一个简单的校验函数是很有必要的。这个函数可以快速检查两个模型的状态字典state_dict是否具有完全相同的键结构。def compare_state_dict_keys(state_dict1, state_dict2): 比较两个状态字典的键是否完全一致。 返回 (是否一致, 仅存在于第一个字典的键, 仅存在于第二个字典的键) keys1 set(state_dict1.keys()) keys2 set(state_dict2.keys()) common keys1 keys2 only_in_1 keys1 - keys2 only_in_2 keys2 - keys1 is_identical (len(only_in_1) 0 and len(only_in_2) 0) return is_identical, only_in_1, only_in_2 # 获取状态字典 base_sd base_model.state_dict() finetuned_sd finetuned_model.state_dict() # 进行比较 is_match, only_base, only_ft compare_state_dict_keys(base_sd, finetuned_sd) if is_match: print(✅ 模型参数键完全匹配可以进行减法操作。) else: print(❌ 参数键不匹配) if only_base: print(f 仅在基座模型中存在的键: {list(only_base)[:5]}...) # 只打印前5个 if only_ft: print(f 仅在微调模型中存在的键: {list(only_ft)[:5]}...) # 此时应停止操作检查模型版本或微调过程是否修改了结构如添加了分类头。如果校验通过我们就可以进入核心的“剥离”环节了。4. 核心手术计算并提取参数变化量Delta参数减法是概念上的核心但在实现时需要考虑精度、内存和实用性。下面我将分步拆解并分享几个实践中至关重要的技巧。4.1 基础减法实现最直观的方法就是遍历匹配的键执行张量减法。import torch from tqdm import tqdm # 用于显示进度条 def compute_parameter_delta(base_state_dict, finetuned_state_dict, save_pathNone): 计算微调模型与基座模型之间的参数差值Delta。 参数: base_state_dict: 基座模型的状态字典。 finetuned_state_dict: 全参数微调后模型的状态字典。 save_path: 可选计算完成后直接保存 Delta 的路径。 返回: delta_state_dict: 包含参数差值的新状态字典。 delta_state_dict {} # 确保键匹配这里假设已经通过 compare_state_dict_keys 校验过 common_keys set(base_state_dict.keys()) set(finetuned_state_dict.keys()) print(开始计算参数差值Delta...) for key in tqdm(common_keys): base_param base_state_dict[key] ft_param finetuned_state_dict[key] # 关键检查张量形状必须一致 if base_param.shape ! ft_param.shape: raise ValueError(f键 {key} 的参数形状不匹配: 基座 {base_param.shape}, 微调 {ft_param.shape}) # 执行减法得到 Delta # 注意确保它们在同一个设备上且数据类型兼容 if base_param.device ! ft_param.device: # 通常不会发生因为都通过 device_mapauto 加载。但安全起见。 ft_param ft_param.to(base_param.device) param_delta ft_param - base_param # 可选过滤掉变化极小的参数减少存储体积见下文4.2节讨论 # param_delta filter_small_delta(param_delta, epsilon1e-6) delta_state_dict[key] param_delta if save_path: # 使用 safetensors 格式保存更安全 from safetensors.torch import save_file save_file(delta_state_dict, f{save_path}/delta_weights.safetensors) # 同时保存一个配置文件记录基座模型信息 with open(f{save_path}/config.json, w) as f: import json json.dump({ base_model_name_or_path: ./original_qwen2_7b_snapshot, # 你的基座路径 delta_created_from: full_finetuning, parameter_keys: list(delta_state_dict.keys())[:5] # 示例存前5个键 }, f, indent2) print(f✅ 参数差值已保存至 {save_path}) return delta_state_dict # 使用函数 delta_sd compute_parameter_delta(base_sd, finetuned_sd, save_path./my_lora_like_delta)这段代码已经可以工作了。它会生成一个delta_weights.safetensors文件其中包含了所有参数的差值。然而这个文件可能仍然很大几乎和原模型一样大因为即使参数变化很小我们仍然存储了每个位置的浮点数。4.2 进阶技巧稀疏化与量化压缩直接保存全量 Delta 可能不是最优雅的方案。我们可以借鉴 LoRA 的思想对 Delta 进行压缩。这里介绍两种实用的后处理技巧技巧一阈值过滤稀疏化微调通常只改变一小部分参数。我们可以设定一个很小的阈值epsilon将绝对值小于该阈值的 Delta 值视为“未改变”并将其置为零。这样Delta 张量就变成了稀疏张量可以用稀疏矩阵格式存储体积大大减小。def filter_small_delta(delta_tensor, epsilon1e-6): 将绝对值小于 epsilon 的 Delta 值置零实现稀疏化。 # 创建一个掩码标记哪些位置的变化量“显著” significant_mask torch.abs(delta_tensor) epsilon # 将不显著的位置置零 filtered_delta delta_tensor * significant_mask # 计算稀疏度 sparsity 1.0 - (significant_mask.sum().item() / delta_tensor.numel()) if sparsity 0.5: # 如果超过一半的参数变化可忽略 print(f 稀疏度: {sparsity:.2%}) return filtered_delta # 在 compute_parameter_delta 循环中启用过滤 # param_delta filter_small_delta(param_delta, epsilon1e-6)技巧二低精度量化参数变化量Delta的数值范围通常很小。我们可以将其量化为更低精度的数据类型如torch.float16甚至torch.bfloat16以减半或更多的存储空间。在加载使用时再转换回所需精度。def quantize_delta(delta_state_dict, dtypetorch.float16): 将 Delta 状态字典中的张量转换为低精度数据类型。 quantized_sd {} for key, tensor in delta_state_dict.items(): quantized_sd[key] tensor.to(dtype) return quantized_sd quantized_delta_sd quantize_delta(delta_sd, dtypetorch.float16)结合稀疏化和量化最终的 Delta 文件大小可能只有原始模型的十分之一或更少极大地提高了分享的便利性。4.3 将 Delta 封装为 PEFT 适配器为了让提取出的 Delta 能够像标准的 LoRA 适配器一样通过 PEFT 库方便地加载和使用我们可以将其封装成一个 PEFT 配置。虽然它不是通过 LoRA 训练得到的但我们可以利用 PEFT 的加载机制。首先创建一个简单的配置文件adapter_config.json{ peft_type: DELTA, task_type: CAUSAL_LM, base_model_name_or_path: ./original_qwen2_7b_snapshot, dtype: float16, delta_weights_file: delta_weights.safetensors }然后我们需要编写一个自定义的 PEFT 模型类或者更简单一点写一个加载函数来模拟 PEFT 的加载行为from peft import PeftModel, PeftConfig import os class DeltaPeftModel: 一个简化的示例演示如何将 Delta 权重作为 PEFT 模块加载。 注意这不是官方 PEFT 类而是一个概念验证。 staticmethod def from_delta(base_model, delta_path): # 加载 Delta 权重 from safetensors.torch import load_file delta_weights load_file(os.path.join(delta_path, delta_weights.safetensors)) # 获取基座模型状态字典 base_sd base_model.state_dict() # 将 Delta 加到基座参数上 for key, delta in delta_weights.items(): if key in base_sd: # 确保数据类型和设备一致 target_param base_sd[key] if target_param.device ! delta.device: delta delta.to(target_param.device) if target_param.dtype ! delta.dtype: delta delta.to(target_param.dtype) # 执行加法base delta fine-tuned target_param.data.add_(delta) else: print(f警告: Delta 中的键 {key} 在基座模型中不存在已跳过。) print(Delta 权重已合并到基座模型中。) return base_model # 使用方式 # reloaded_base_model AutoModelForCausalLM.from_pretrained(...) # merged_model DeltaPeftModel.from_delta(reloaded_base_model, ./my_lora_like_delta)虽然这不是一个标准的PeftModel但它实现了核心功能将保存的 Delta 增量加载并应用到基座模型上。对于社区分享你可以同时提供delta_weights.safetensors、adapter_config.json和这个简易的加载脚本。5. 验证与测试确保剥离与合并的正确性“手术”完成后验证是必不可少的。我们需要确保提取的 Delta 能够准确地重建出微调后的模型。5.1 完整性验证数学恒等式最直接的验证是数学上的用基座模型参数加上提取的 Delta应该能完全复原微调后的模型参数。我们可以抽样检查几个关键层的参数或者计算整体差异的范数。def verify_delta_integrity(base_model, finetuned_model, delta_state_dict): 验证 Delta 的正确性对于随机采样的部分参数检查 (base delta) 是否等于 finetuned。 base_sd base_model.state_dict() ft_sd finetuned_model.state_dict() # 随机选择10个键进行验证避免全部检查太耗时 import random keys_to_check random.sample(list(delta_state_dict.keys()), min(10, len(delta_state_dict))) print(进行 Delta 完整性验证抽样检查...) for key in keys_to_check: if key in base_sd and key in ft_sd: reconstructed base_sd[key] delta_state_dict[key] original ft_sd[key] # 计算最大绝对误差和均方误差 max_error torch.max(torch.abs(reconstructed - original)).item() mse torch.mean((reconstructed - original) ** 2).item() # 使用一个宽松的容差因为可能存在数值精度误差 if max_error 1e-5 and mse 1e-10: print(f ✅ {key}: 匹配良好 (max_err{max_error:.2e}, mse{mse:.2e})) else: print(f ❌ {key}: 可能存在不匹配max_err{max_error:.2e}, mse{mse:.2e}) # 如果误差很大需要检查减法过程或模型是否对应。 else: print(f ⚠️ {key}: 键在基座或微调模型中缺失跳过。) print(抽样验证完成。)5.2 功能性验证推理结果对比数学上的相等是基础但模型最终要看输出。我们应该用相同的输入提示词分别让原始微调模型和“基座模型Delta”组合模型进行推理比较它们的生成结果。from transformers import AutoTokenizer def functional_verification(base_model, delta_path, finetuned_model, test_prompts): 功能验证对比原始微调模型与基座Delta模型的输出。 tokenizer AutoTokenizer.from_pretrained(base_model.config._name_or_path, trust_remote_codeTrue) tokenizer.pad_token tokenizer.eos_token # 为生成设置 # 加载 Delta 并合并到基座模型使用之前定义的类或函数 merged_model DeltaPeftModel.from_delta(base_model, delta_path) merged_model.eval() finetuned_model.eval() print(功能性验证生成文本对比) for i, prompt in enumerate(test_prompts): inputs tokenizer(prompt, return_tensorspt).to(base_model.device) with torch.no_grad(): # 使用合并模型生成 outputs_merged merged_model.generate(**inputs, max_new_tokens50, do_sampleFalse) text_merged tokenizer.decode(outputs_merged[0], skip_special_tokensTrue) # 使用原始微调模型生成 outputs_original finetuned_model.generate(**inputs, max_new_tokens50, do_sampleFalse) text_original tokenizer.decode(outputs_original[0], skip_special_tokensTrue) print(f\n--- 提示 {i1}: {prompt[:50]}... ---) print(f[原始微调模型]: {text_original}) print(f[基座Delta模型]: {text_merged}) # 简单判断如果生成的文本相同或高度相似则通过 if text_original text_merged: print( ✅ 输出完全一致。) else: # 对于文本生成完全一致可能要求过高可以计算相似度如BLEU、ROUGE print( ⚠️ 输出存在差异需人工评估语义是否一致。)选择3-5个有代表性的测试提示词涵盖微调任务的核心领域运行上述验证。如果输出在语义上高度一致那么我们的“剥离-合并”流程就基本成功了。6. 发布、分享与应用你的微调模块经过验证的 Delta 模块已经可以发布了。为了让他人能够轻松使用你需要提供一个完整的“使用包”。6.1 打包发布内容一个用户友好的发布包应该包含以下文件my_awesome_finetune_delta/ ├── README.md # 项目说明至关重要 ├── delta_weights.safetensors # 核心参数差值文件 ├── adapter_config.json # PEFT风格配置文件 ├── load_delta.py # 简易加载脚本如第4.3节所示 ├── requirements.txt # 依赖说明torch, transformers, peft等 └── verification.ipynb # 可选的验证笔记本展示效果README.md 编写要点标题与简介清晰说明这是什么任务的微调增量例如“用于法律文书生成的 Qwen2-7B Delta 权重”。基座模型明确指出必须使用的基座模型名称、版本和提交哈希如Qwen/Qwen2-7Babcdef1。快速开始提供最简单的代码示例展示如何加载并使用。训练详情简要说明训练数据、超参数学习率、epoch等增加可信度。效果评估提供在验证集上的关键指标如准确率、BLEU分数或示例输入/输出对比。许可协议明确你的 Delta 权重和代码的许可通常继承基座模型的许可并添加你的要求。6.2 加载与应用示例在你的 README 中提供傻瓜式的加载代码# 快速使用示例 from transformers import AutoModelForCausalLM, AutoTokenizer from load_delta import DeltaPeftModel # 假设这是你提供的加载脚本 import torch # 1. 加载基座模型必须与训练时完全一致 base_model_name Qwen/Qwen2-7B base_model AutoModelForCausalLM.from_pretrained( base_model_name, torch_dtypetorch.float16, device_mapauto, trust_remote_codeTrue ) tokenizer AutoTokenizer.from_pretrained(base_model_name, trust_remote_codeTrue) # 2. 加载并应用 Delta 权重 delta_path ./my_awesome_finetune_delta model_with_delta DeltaPeftModel.from_delta(base_model, delta_path) # 3. 使用合并后的模型进行推理 prompt 请根据以下事实生成一份起诉状... inputs tokenizer(prompt, return_tensorspt).to(model_with_delta.device) outputs model_with_delta.generate(**inputs, max_new_tokens200) print(tokenizer.decode(outputs[0], skip_special_tokensTrue))6.3 潜在问题与应对策略在实际分享和使用过程中可能会遇到以下问题基座模型版本漂移这是最常见的问题。如果用户使用的基座模型版本即使是同一个名字与你训练时不同参数结构可能微调导致加载失败或效果异常。解决方案在 README 中强烈强调版本并提供基座模型确切版本的下载链接或哈希值。甚至可以考虑将基座模型的分支快照上传到你的 Hub 空间。文件体积仍然过大即使经过稀疏化和量化全参数 Delta 可能还是很大。解决方案考虑是否真的需要发布全量 Delta。对于许多任务使用 LoRA 等高效微调方法得到的适配器通常只有几十MB可能是更优的分享选择。本文介绍的方法更适用于已经完成了全参数微调且需要最大限度保留性能的场景。性能损失由于数值精度误差减法、加法、量化合并后的模型与原始微调模型可能存在极细微的差异在极其敏感的任务上可能体现出来。解决方案在验证阶段使用更严格的测试集。如果发现差异可以尝试使用torch.float32精度进行计算和保存尽管这会增加文件大小。7. 延伸思考与标准PEFT方法如LoRA的协同你可能会问既然 LoRA 等 PEFT 方法本身就是为了得到轻量化的适配器为什么不直接用它们呢这是一个很好的问题。本文介绍的技术路径与标准 PEFT 是互补关系适用于不同的场景标准 LoRA 训练与分享这是首选。在微调开始时就直接使用 LoRA训练结束后自然得到一个小巧的adapter_model.safetensors文件可以直接通过PeftModel.from_pretrained加载。这是最规范、最省事的方式。本文的“后处理”路径适用于已经完成了全参数微调但当时没有使用 LoRA现在又想提取出增量部分进行分享的情况。它是一种“补救”或“转换”措施。此外对于某些研究可能需要精确分析全参数微调带来的参数变化分布这种方法也能提供原始数据。一个高级技巧将全量 Delta 近似转换为 LoRA理论上一个全参数微调产生的稠密 Delta 矩阵可以通过奇异值分解SVD近似分解为两个低秩矩阵A和B从而模拟 LoRA 的形式。这可以进一步压缩体积。不过这需要更复杂的数学操作并且是一种有损压缩可能会损失一些性能。对于大多数应用场景直接分享稀疏化/量化后的 Delta 或直接使用 LoRA 训练是更实用的选择。通过以上七个部分的详细拆解我们从需求背景、原理理解、环境准备、核心操作、验证测试到发布分享完整地走通了从全参数微调模型中“剥离”并发布自有参数模块的全流程。这套方法就像给模型做了一次精细的“外科手术”让你能将自己的训练成果以最经济、最合规的方式贡献给社区。