模型微调(一)介绍

张开发
2026/4/21 19:13:16 15 分钟阅读

分享文章

模型微调(一)介绍
目标为什么要做大模型的微调两个目标第一个是让模型更切合自己的应用场景更加适应特定的下游任务第二个是模型能够变得更加轻便节省资源。方式针对这两个目标业界主要采用以下几种微调方式。此外基于模型部署的需求模型蒸馏、量化、剪枝等模型压缩方法也常常与微调方法配合使用以进一步减小模型体积、加速推理速度使大模型更容易在实际应用中落地。1全量微调Full Fine-tuning这是最直接的微调方法。即在下游任务的训练数据上对预训练模型的所有参数进行训练和更新。全量微调可以让模型充分适应新的任务但需要更新的参数量巨大对算力要求很高而且有可能导致过拟合、遗忘预训练知识等问题。2部分微调Partial Fine-tuning有选择地冻结一部分模型参数只微调其中的一小部分参数如最后几层。这种方法可以在一定程度上减少所需的计算资源但由于大部分参数没有更新模型的适应能力也相对有限。3提示微调Prompt-tuning通过学习输入文本的“软提示”可训练的连续向量来引导预训练模型执行目标任务而无需改变原模型的参数。提示微调只需训练软提示参数大大减少了训练开销。但生成的提示向量缺乏可解释性泛化能力也有待进一步研究。4Adapter 微调在预训练模型的每一层或部分层注入轻量级的 Adapter 模块。微调时只训练这些新加入的 Adapter 参数冻结原模型参数。Adapter 充当了任务适配器的角色以较小的参数量和计算代价实现了模型适配。5LoRA 微调以低秩分解的思想对预训练模型进行微调。在每个注意力模块中引入低秩矩阵在前向传播时与原矩阵相加得到适配后的权重。LoRA 只需训练新引入的低秩矩阵参数开销很小但能在下游任务上取得不错的效果。6P-tuning v2将连续的提示向量和 Adapter 的思想相结合在每个 Transformer 层基础上引入可训练的提示嵌入。这些提示嵌入在前向传播时会注入到注意力矩阵和前馈层中。P-tuning v2 在保留预训练知识的同时也能有效地进行任务适配。上面罗列的各种主流微调方法除去全量微调之外其实都可以称之为参数高效微调也就是 PEFT是针对大语言模型来说最为实用的方法。毕竟大语言模型的主要问题是参数数量过大做全量微调实在对资源的消耗太大已经不是一般研究人员或者普通企业所能做的了。PEFTPEFT 的具体方法非常多我们可以把它们分成下面几大类。基于提示的方法基于 LoRA 的方法其他轻量级的参数化方法HuggingFace 的 PEFT 框架这是一组工具和方法旨在使大语言模型LLMs的微调过程更加高效和资源友好。这个框架提供了一些主要的技术以减少微调所需的参数量和计算资源同时仍保持模型在特定任务上的高性能。PEFT · Hugging Face安装pip install peft不过要完全实现微调还需要安装其他一些支持库。安装好了所需要的包之后就加载模型和 PEFT 配置伪代码如下from transformers import AutoModelForCausalLM, AutoTokenizer from peft import get_peft_config, get_peft_model model_name 你要微调的模型名称 model AutoModelForCausalLM.from_pretrained(model_name) tokenizer AutoTokenizer.from_pretrained(model_name) peft_config get_peft_config(这里会有很多具体的内容) peft_model get_peft_model(model, peft_config)PEFT configurations and models · Hugging Face二、用 LoRA 进行参数高效微调在上面这么多五花八门的 PEFT 技巧中目前业界最为常用的主流微调方式是 LoRA。1、Alpaca 数据格式微调的第一步其实是准备数据。说到数据介绍一下 Alpaca 数据格式。这种数据是由 Meta AI 在发布 Llama 模型时一同提出的。Alpaca 数据集包含了 52K 个由 Llama 模型生成的指令 - 输出对涵盖了问答、总结、创意写作等多种任务类型。这个数据集的格式对于后续的指令微调任务具有重要意义成为了许多开源项目的基石。这些数据是通过自指令生成技术Self-Instruct生成的。每条数据都是一个字典包含以下字段instruction描述模型需要执行的任务。input任务的上下文或输入约 40% 的例子包含此字段。output由 text-davinci-003 生成的指令答案。因为这种格式的指令数据微调大模型很有效网络上就出现了很多类似的数据集和微调项目我在网络上找到了一个非常符合 Alpaca 风格的中药数据集。GitHub - liucann/CPMI-ChatGLM · GitHub这个数据我把它保存在 data 目录中。2、在微调之前先测试模型在开始 PEFT 微调之前我们先简单的测试一下 Qwen 这个模型看看它接受指令后会给出什么回答。# 导入所需要的库 from transformers import AutoModelForCausalLM, AutoTokenizer # 指定模型 MODEL Qwen/Qwen1.5-1.8B-Chat # 加载训练好的模型和分词器 tokenizer AutoTokenizer.from_pretrained(MODEL, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained(MODEL, trust_remote_codeTrue, device_mapauto) # 模型设为评估状态 model.eval() # 定义测试示例 test_examples [ { instruction: 使用中医知识正确回答适合这个病例的中成药。, input: 肛门疼痛痔疮肛裂。 }, { instruction: 使用中医知识正确回答适合这个病例的中成药。, input: 有没有能够滋养肝肾、清热明目的中药。 } ] # 生成回答 for example in test_examples: context fInstruction: {example[instruction]}\nInput: {example[input]}\nAnswer: inputs tokenizer(context, return_tensorspt) outputs model.generate(inputs.input_ids.to(model.device), max_length512, num_return_sequences1, no_repeat_ngram_size2) answer tokenizer.decode(outputs[0], skip_special_tokensTrue) print(fInput: {example[input]}) print(fOutput: {answer}\n)模型首先被下载到本机的 Cache 目录之后大模型当然可以根据自己已有的知识对任何中医中药相关的问题进行回答。注意这个输出中并不包含我们数据集中有效治疗肛裂的的地榆槐角丸因为这个答案是目前通义千问模型根据自己的训练情况提供的并不一定真实有效。3、定制可以被模型读入的数据集刚才下载的中药指令微调数据需要转换成大模型能够读入的格式。代码如下# 导入Dataset from torch.utils.data import Dataset import json # 自定义数据集类 class CustomDataset(Dataset): def __init__(self, data_path, tokenizer, device): self.data json.load(open(data_path)) self.tokenizer tokenizer self.device device def __len__(self): return len(self.data) def __getitem__(self, idx): example self.data[idx] formatted_example self.format_example(example) inputs self.tokenizer( formatted_example[context], max_length512, truncationTrue, paddingmax_length, return_tensorspt ) labels self.tokenizer( formatted_example[target], max_length512, truncationTrue, paddingmax_length, return_tensorspt ) inputs[labels] labels[input_ids] # 确保所有张量在同一个设备上 return {key: val.squeeze().to(self.device) for key, val in inputs.items()} def format_example(self, example: dict) - dict: context fInstruction: {example[instruction]}\n if example.get(input): context fInput: {example[input]}\n context Answer: target example[output] return {context: context, target: target}自定义的数据集类 CustomDataset 继承自 Dataset以下是对类的实现代码的简单说明。举例来说对于下面的数据[ { “instruction”: “Translate the following sentence to French.”, “input”: “Hello, how are you?”, “output”: “Bonjour, comment ça va?” }]_init_ 方法会加载 JSON 文件并初始化分词器和设备。_getitem_ 方法会获取并格式化这个数据样本使用分词器将其转换为张量格式并返回包含输入和标签的字典。format_example 方法会生成上下文和目标文本contextInstruction: Translate the following sentence to French.\nInput: Hello, how are you?\nAnswer: targetBonjour, comment ça va?4、准备微调模型下面指定模型并准备开始微调。# 导入微调模型所需的库 from transformers import AutoTokenizer, AutoModelForCausalLM import torch # 指定模型 MODEL Qwen/Qwen1.5-1.8B-Chat # 确定设备 device torch.device(cuda if torch.cuda.is_available() else cpu) # 加载分词器和模型 tokenizer AutoTokenizer.from_pretrained(MODEL, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained(MODEL, trust_remote_codeTrue) model model.to(device) # 把模型移到设备上 # 节省内存的一些配置 model.supports_gradient_checkpointing True # 支持梯度检查点功能减少显存使用 model.gradient_checkpointing_enable() # 启用梯度检查点功能减少训练时的显存占用 model.enable_input_require_grads() # 允许模型输入的张量需要梯度支持更灵活的梯度计算 model.is_parallelizable True # 指定模型可以并行化处理 model.model_parallel True # 启用模型并行化在多设备如多GPU上分布计算5、设置 LoRA 配置项这些配置项帮助用户在训练语言模型时应用 LoRA 技术通过低秩分解减少模型参数和内存占用并使用 dropout 技术防止过拟合同时仅在特定模块上应用 LoRA从而在计算效率和模型性能之间取得平衡。# 导入peft库 from peft import get_peft_model, LoraConfig, TaskType # 配置并应用 LoRA peft_config LoraConfig( task_typeTaskType.CAUSAL_LM, # 任务类型因果语言模型 (Causal LM) inference_modeFalse, # 模式训练模式 (inference_modeFalse) r8, # 低秩分解的秩8 (r8) lora_alpha32, # 缩放因子32 (lora_alpha32) lora_dropout0.1, # dropout 概率0.1 (lora_dropout0.1) target_modules[q_proj, v_proj] # 查询投影和值投影模块 ) model get_peft_model(model, peft_config) # 应用LoRA配置6、开启 LoRA 训练# 准备数据集 train_dataset CustomDataset(data/chinese_med.json, tokenizer, device) # 导入训练相关的库 from transformers import TrainingArguments, Trainer # 定义训练参数 training_args TrainingArguments( output_dir./results, # 训练结果保存的目录 num_train_epochs50, # 训练的总轮数 per_device_train_batch_size4, # 每个设备上的训练批次大小 gradient_accumulation_steps8, # 梯度累积步数在进行反向传播前累积多少步 evaluation_strategyno, # 评估策略这里设置为不评估 save_strategyepoch, # 保存策略每个 epoch 保存一次模型 learning_rate5e-5, # 学习率 fp16True, # 启用 16 位浮点数训练提高训练速度并减少显存使用 logging_dir./logs, # 日志保存目录 dataloader_pin_memoryFalse, # 禁用 pin_memory 以节省内存 ) # 自定义 Trainer class CustomTrainer(Trainer): def compute_loss(self, model, inputs, return_outputsFalse): labels inputs.pop(labels) # 从输入中取出标签 outputs model(**inputs) # 获取模型输出 logits outputs.logits # 获取模型输出的logits shift_logits logits[..., :-1, :].contiguous() # 对logits进行偏移准备计算交叉熵损失 shift_labels labels[..., 1:].contiguous() # 对标签进行偏移准备计算交叉熵损失 loss_fct torch.nn.CrossEntropyLoss() # 定义交叉熵损失函数 loss loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1)) # 计算损失 return (loss, outputs) if return_outputs else loss # 根据参数返回损失和输出 # 定义 Trainer trainer CustomTrainer( modelmodel, # 训练的模型 argstraining_args, # 训练参数 train_datasettrain_dataset, # 训练数据集 ) # 开始训练 trainer.train()

更多文章