在此处可以看到很明显大模型进行微调后口吻已经发生了更改。据笔者使用下来的记录表示,微调后的大模型思考时间更加短暂。接下来,让我们一起逐步完成微调实践,共同优化模型性能!一、什么是大模型微调?微调就像给一个“学霸”补课,让它从“通才”变成某个领域的“专家”。此处以本文进行微调的医学数据进行举例:假设你有一个很聪明的朋友,他读过全世界的书(相当于大模型的预训练阶段),能和你聊历史、科学、文学等各种话题。但如果你需要他帮你看医学报告段),能和你聊历史、科学、文学等各种话题。但如果你需要他帮你看医学报告,虽然他懂一些基础知识,但可能不够专业。这时候,你给他一堆医学书籍和病例,让他专门学习这方面的知识(这就是微调),他就会变得更擅长医疗领域的问题。? 故事解释:想象你有一个会画小猫的机器人?(这就是预训练模型)。现在你想让它学会画戴帽子的小猫??。不需要从头教它画画,只需要给它看很多"戴帽子小猫"的图片,然后说:"保持原来的画画能力,但要学会加帽子哦!" 这就是微调!? 生活案例解释:基础版音箱只会普通话(预训练模型) 给它听 100 句四川话(微调数据) 现在能听懂"摆龙门阵"(方言理解能力↑)
原始相机拍所有场景(通用模型) 加载“美食滤镜”参数(微调后的模型) 拍食物时自动增强饱和度(专业能力强化)
加强版解释:乐高城堡改造成儿童医院第一步:原有结构 —— 通用乐高城堡▸ 比喻:就像网购的"标准款城堡积木套装",有城墙、塔楼、尖顶,能当普通房子用。▸ 对应技术:预训练模型(比如 ChatGPT),已经学会通用语言能力,但不够专业。第二步:局部改造 —— 低成本改装▸ 技术含义:微调模型顶层参数(比如修改分类头),让输出风格更适合儿童对话。▸ 操作:在门口插入一个可旋转的积木模块,不破坏原有门结构。▸ 技术含义:插入适配器模块(Adapter),让模型新增儿科医学术语理解能力,且不干扰原有知识。▸ 操作:在城堡外墙贴上"十字符号"和卡通动物贴纸。▸ 技术含义:特征空间偏移(Feature Shift),调整模型内部表示,让它更关注医疗相关词汇和童趣表达。第三步:新功能 —— 变身儿童医院▸ 成果:改装后的城堡能接待小患者,有玩具区、温和的医生(圆顶),还有专用医疗设备(旋转门)。▸ 技术含义:通过轻量改造,通用模型变成"儿科医疗问答机器人",专精儿童健康咨询。二、当前尝试过的硬件配置显卡:NVIDIA GeForce RTX 4060内存:16 G(因为家庭电脑所以日常状态是 8.8/15.7 GB)三、微调工作(1) 数据集准备本文数据集来源,魔搭社区的 medical-o1-reasoning-SFT。在 DeepSeek 的蒸馏模型微调过程中,数据集中引入 Complex_CoT(复杂思维链)是关键设计差异。若仅使用基础问答对进行训练,模型将难以充分习得深度推理能力,导致最终性能显著低于预期水平。这一特性与常规大模型微调的数据要求存在本质区别。(2) 模型微调代码(此处是无框架纯手搓)——直接上了,后面会有细节讲解需要引入的库:pipinstalltorchtransformerspeftdatasetsmatplotlibacceleratesafetensors import torchimport matplotlib.pyplot as pltfrom transformers import (AutoTokenizer,AutoModelForCausalLM,TrainingArguments,Trainer,TrainerCallback)from peft import LoraConfig, get_peft_modelfrom datasets import load_datasetimport os
# 配置路径(根据实际路径修改)model_path = r"你的模型路径"# 模型路径data_path = r"你的数据集路径"# 数据集路径output_path = r"你的保存微调后的模型路径"# 微调后模型保存路径
# 强制使用GPUassert torch.cuda.is_available(), //"必须使用GPU进行训练!"device = torch.device("cuda")
# 自定义回调记录Lossclass LossCallback(TrainerCallback):def __init__(self):self.losses = []
def on_log(self, args, state, control, logs=None, **kwargs):if "loss" in logs:self.losses.append(logs["loss"])
# 数据预处理函数def process_data(tokenizer):dataset = load_dataset("json", data_files=data_path, split="train[:1500]")
def format_example(example):instruction = f"诊断问题:{example['Question']}\n详细分析:{example['Complex_CoT']}"inputs = tokenizer(f"{instruction}\n### 答案:\n{example['Response']}<|endoftext|>",padding="max_length",truncation=True,max_length=512,return_tensors="pt")return {"input_ids": inputs["input_ids"].squeeze(0), "attention_mask": inputs["attention_mask"].squeeze(0)}
return dataset.map(format_example, remove_columns=dataset.column_names)
# LoRA配置peft_config = LoraConfig(r=16,lora_alpha=32,target_modules=["q_proj", "v_proj"],lora_dropout=0.05,bias="none",task_type="CAUSAL_LM")
# 训练参数配置training_args = TrainingArguments(output_dir=output_path,per_device_train_batch_size=2,# 显存优化设置gradient_accumulation_steps=4,# 累计梯度相当于batch_size=8num_train_epochs=3,learning_rate=3e-4,fp16=True,# 开启混合精度logging_steps=20,save_strategy="no",report_to="none",optim="adamw_torch",no_cuda=False,# 强制使用CUDAdataloader_pin_memory=False,# 加速数据加载remove_unused_columns=False# 防止删除未使用的列)
def main():# 创建输出目录os.makedirs(output_path, exist_ok=True)
# 加载tokenizertokenizer = AutoTokenizer.from_pretrained(model_path)tokenizer.pad_token = tokenizer.eos_token
# 加载模型到GPUmodel = AutoModelForCausalLM.from_pretrained(model_path,torch_dtype=torch.float16,device_map={"": device}# 强制使用指定GPU)model = get_peft_model(model, peft_config)model.print_trainable_parameters()
# 准备数据dataset = process_data(tokenizer)
# 训练回调loss_callback = LossCallback()
# 数据加载器def data_collator(data):batch = {"input_ids": torch.stack([torch.tensor(d["input_ids"]) for d in data]).to(device),"attention_mask": torch.stack([torch.tensor(d["attention_mask"]) for d in data]).to(device),"labels": torch.stack([torch.tensor(d["input_ids"]) for d in data]).to(device)# 使用input_ids作为labels}return batch
# 创建Trainertrainer = Trainer(model=model,args=training_args,train_dataset=dataset,data_collator=data_collator,callbacks=[loss_callback])
# 开始训练print("开始训练...")trainer.train()
# 保存最终模型trainer.model.save_pretrained(output_path)print(f"模型已保存至:{output_path}")
# 绘制训练集损失Loss曲线plt.figure(figsize=(10, 6))plt.plot(loss_callback.losses)plt.title("Training Loss Curve")plt.xlabel("Steps")plt.ylabel("Loss")plt.savefig(os.path.join(output_path, "loss_curve.png"))print("Loss曲线已保存")
if __name__ == "__main__":main()
(3) 代码详细讲解1. 导入必要的库和模块功能总结:导入项目依赖的第三方库,包括 PyTorch 基础库、HuggingFace 工具库、可视化库等。importtorchimportmatplotlib.pyplotaspltfromtransformersimport(#HuggingFaceTransformer模型工具AutoTokenizer,AutoModelForCausalLM,TrainingArguments,Trainer,TrainerCallback)frompeftimportLoraConfig,get_peft_model#参数高效微调库fromdatasetsimportload_dataset#数据集加载工具importos#系统路径操作 有关类库介绍:1. torch (PyTorch 库的核心模块)2. matplotlib.pyplot (Matplotlib 绘图库)3. transformers (HuggingFace Transformers 库)核心组件: AutoTokenizer:自动加载预训练模型对应的分词器 AutoModelForCausalLM:自动加载因果语言模型(如GPT系列) TrainingArguments:定义训练超参数 Trainer:封装训练流程的类 TrainerCallback:训练回调基类
4. peft (Parameter-Efficient Fine-Tuning)功能:实现参数高效微调方法的库。 核心组件: 代码中的作用:
5. datasets (HuggingFace Datasets 库)功能:高效数据集加载与处理工具。 核心方法: load_dataset:加载多种格式的数据 map:数据预处理流水线
代码中的作用: 从本地文件加载医疗问答数据集 将原始数据转换为模型需要的输入格式
6. os (操作系统接口)功能:提供操作系统相关功能。 代码中的作用: 创建输出目录 (os.makedirs) 处理文件路径相关操作 确保模型保存路径的有效性
2. 配置路径和硬件检查功能总结:配置模型/数据路径,强制检查GPU可用性# 配置路径(根据实际路径修改)model_path = r"你的模型路径"# 预训练模型存放路径data_path = r"你的数据集路径"# 训练数据路径(JSON格式)output_path = r"你的保存微调后的模型路径"# 微调后模型保存位置
# 强制使用GPU(确保CUDA可用)assert torch.cuda.is_available(), "必须使用GPU进行训练!"device = torch.device("cuda")# 指定使用CUDA设备
3.自定义训练回调类功能总结:实现自定义回调,在模型训练过程中,实时记录损失值(Loss)的变化。损失值是用来衡量模型预测结果与真实结果之间的差距的,损失值越小,说明模型的表现越好。class LossCallback(TrainerCallback):def __init__(self):self.losses = []# 存储损失值的列表
# 当训练过程中有日志输出时触发def on_log(self, args, state, control, logs=None, **kwargs):if "loss" in logs:# 过滤并记录损失值self.losses.append(logs["loss"])
4. 数据预处理函数功能总结:加载并格式化训练数据,将原始数据集转换为模型可以理解的格式。def process_data(tokenizer):# 从JSON文件加载数据集(仅取前1500条)dataset = load_dataset("json", data_files=data_path, split="train[:1500]")
# 单条数据格式化函数def format_example(example):# 拼接指令和答案(固定模板)instruction = f"诊断问题:{example['Question']}\n详细分析:{example['Complex_CoT']}"inputs = tokenizer(f"{instruction}\n### 答案:\n{example['Response']}<|endoftext|>",# 添加结束符padding="max_length",# 填充至最大长度truncation=True, # 超长截断max_length=512,# 最大序列长度return_tensors="pt"# 返回PyTorch张量)# 返回处理后的输入(移除batch维度)return {"input_ids": inputs["input_ids"].squeeze(0), "attention_mask": inputs["attention_mask"].squeeze(0)}
# 应用格式化函数并移除原始列return dataset.map(format_example, remove_columns=dataset.column_names)
关键代码1.拼接指令和答案instruction=f"诊断问题:{example['Question']}\n详细分析:{example['Complex_CoT']}"2.使用分词器处理文本作用:将拼接后的文本转换为模型可以理解的格式。 参数说明: padding="max_length":将文本填充到固定长度(512)。 truncation=True:如果文本超过 512 个 token,就截断。 max_length=512:最大长度为 512。 return_tensors="pt":返回 PyTorch 张量。
示例: 输入:"诊断问题:发烧怎么办?\n详细分析:可能是感冒引起的。\n### 答案:\n多喝水,休息。" 输出:input_ids=[101, 234, 345, ..., 102], attention_mask=[1, 1, 1, ..., 1]
类比:就像把文字翻译成机器能懂的数字。
inputs=tokenizer(f"{instruction}\n###答案:\n{example['Response']}<|endoftext|>",#添加结束符padding="max_length",#填充至最大长度truncation=True,#超长截断max_length=512,#最大序列长度return_tensors="pt"#返回PyTorch张量)3.返回处理后的输入作用:返回处理后的输入数据,并移除多余的维度。 参数说明: 类比:就像把翻译好的数字整理成一张表格。
return{"input_ids":inputs["input_ids"].squeeze(0),"attention_mask":inputs["attention_mask"].squeeze(0)}4.应用格式化函数returndataset.map(format_example,remove_columns=dataset.column_names) 5. LoRA微调配置功能总结:配置LoRA参数,指定要适配的模型模块。peft_config=LoraConfig(r=16,#LoRA秩(矩阵分解维度)lora_alpha=32,#缩放系数(控制适配器影响强度)target_modules=["q_proj","v_proj"],#要适配的注意力模块(查询/值投影)lora_dropout=0.05,#防止过拟合的Dropout率bias="none",#不训练偏置参数task_type="CAUSAL_LM"#任务类型(因果语言模型)) 1. r=16:LoRA 的秩"相当于给AI的‘学习笔记’设置 16 页的篇幅限制"2. lora_alpha=32:缩放系数作用:控制低秩矩阵对原始模型的影响强度。 解释: 影响: 比喻: 就像是,音量旋钮的大小决定了声音的响亮程度。如果旋钮转得太大,声音可能会震耳欲聋,甚至让人难以忍受;如果旋钮转得太小,声音又可能太小,听不清楚。过大的 lora_alpha 可能会导致模型的训练变得不稳定,就像声音太大可能会让人感到不适一样。可能会导致过拟合,因为模型对训练数据的细节调整过于敏感。较小的 lora_alpha 会导致模型在训练过程中会更保守地调整权重,训练过程更稳定,但适应新任务的速度可能会较慢。3. target_modules=["q_proj", "v_proj"]:目标模块作用:指定需要插入低秩矩阵的模型模块。 解释: 影响:
4. lora_dropout=0.05:Dropout 率5. bias="none":偏置参数6. task_type="CAUSAL_LM":任务类型训练参数配置training_args=TrainingArguments(output_dir=output_path,#输出目录(模型/日志)per_device_train_batch_size=2,#单GPU批次大小(显存优化)gradient_accumulation_steps=4,#梯度累积步数(等效batch_size=8)num_train_epochs=3,#训练轮次learning_rate=3e-4,#初始学习率fp16=True,#启用混合精度训练(节省显存)logging_steps=20,#每隔20步记录日志save_strategy="no",#不保存中间检查点report_to="none",#禁用第三方报告(如W&B)optim="adamw_torch",#优化器类型no_cuda=False,#强制使用CUDAdataloader_pin_memory=False,#禁用锁页内存(加速数据加载)remove_unused_columns=False#保留未使用的列(避免数据错误)) 1. output_dir=output_path:输出目录2. per_device_train_batch_size=2:单 GPU 批次大小作用:设置每个 GPU 上的训练批次大小。 解释: 示例:
3. gradient_accumulation_steps=4:梯度累积步数4. num_train_epochs=3:训练轮次作用:设置模型在整个数据集上训练的轮次。 解释: 示例:
5. learning_rate=3e-4:初始学习率6. fp16=True:混合精度训练作用:启用混合精度训练,节省显存并加速训练。 解释: 示例:
7. logging_steps=20:日志记录频率8. save_strategy="no":保存策略9. report_to="none":禁用第三方报告10. optim="adamw_torch":优化器类型11. no_cuda=False:强制使用 CUDA12. dataloader_pin_memory=False:禁用锁页内存13. remove_unused_columns=False:保留未使用的列作用:设置是否移除数据集中未使用的列。 解释: 示例:
主函数(训练流程)def main():# 创建输出目录(如果不存在)os.makedirs(output_path, exist_ok=True)
# 加载Tokenizer并设置填充符tokenizer = AutoTokenizer.from_pretrained(model_path)tokenizer.pad_token = tokenizer.eos_token# 使用EOS作为填充符
# 加载预训练模型(半精度+指定GPU)model = AutoModelForCausalLM.from_pretrained(model_path,torch_dtype=torch.float16, # 半精度加载(节省显存)device_map={"": device}# 指定使用的GPU设备)# 应用LoRA适配器model = get_peft_model(model, peft_config)model.print_trainable_parameters() # 打印可训练参数量
# 准备训练数据集dataset = process_data(tokenizer)
# 初始化损失记录回调loss_callback = LossCallback()
# 数据整理函数(构造批次)def data_collator(data):batch = {"input_ids": torch.stack([torch.tensor(d["input_ids"]) for d in data]).to(device),"attention_mask": torch.stack([torch.tensor(d["attention_mask"]) for d in data]).to(device),"labels": torch.stack([torch.tensor(d["input_ids"]) for d in data]).to(device)# 标签=输入(因果LM任务)}return batch
# 初始化Trainertrainer = Trainer(model=model,args=training_args,train_dataset=dataset,data_collator=data_collator,# 自定义数据整理callbacks=[loss_callback] # 添加回调)
# 执行训练print("开始训练...")trainer.train()
# 保存微调后的模型trainer.model.save_pretrained(output_path)print(f"模型已保存至:{output_path}")
# 绘制损失曲线plt.figure(figsize=(10, 6))plt.plot(loss_callback.losses)plt.title("Training Loss Curve")plt.xlabel("Steps")plt.ylabel("Loss")plt.savefig(os.path.join(output_path, "loss_curve.png"))# 保存为PNGprint("Loss曲线已保存")
if __name__ == "__main__":main()
关键代码:1. 加载 Tokenizer 并设置填充符作用:加载预训练模型的分词器,并设置填充符。 解释: 示例:
tokenizer=AutoTokenizer.from_pretrained(model_path)tokenizer.pad_token=tokenizer.eos_token#使用EOS作为填充符 2.加载预训练模型作用:加载预训练的语言模型,并配置硬件相关设置。 解释: AutoModelForCausalLM.from_pretrained:加载因果语言模型(如 GPT)。 torch_dtype=torch.float16:使用半精度(16 位浮点数)加载模型,节省显存。 device_map={"": device}:将模型加载到指定的 GPU 设备上。
示例:
model=AutoModelForCausalLM.from_pretrained(model_path,torch_dtype=torch.float16,#半精度加载(节省显存)device_map={"":device}#指定使用的GPU设备)3.数据整理函数作用:将多条数据整理成一个批次。 解释: input_ids:输入序列的 token ID。 attention_mask:标记有效 token 的位置。 labels:因果语言模型的标签与输入相同(模型需要预测下一个 token)。
示例:
defdata_collator(data):batch={"input_ids":torch.stack([torch.tensor(d["input_ids"])fordindata]).to(device),"attention_mask":torch.stack([torch.tensor(d["attention_mask"])fordindata]).to(device),"labels":torch.stack([torch.tensor(d["input_ids"])fordindata]).to(device)#标签=输入(因果LM任务)}returnbatch4.初始化Trainertrainer=Trainer(model=model,args=training_args,train_dataset=dataset,data_collator=data_collator,#自定义数据整理callbacks=[loss_callback]#添加回调) 四、完结感言非常感谢 Deepseek 官网满血版在本章的代码修改、资料收集以及文章润色方面提供的宝贵帮助!本章的微调部分目前还较为基础,导致损失函数的收敛效果不够理想,仍有较大的优化空间。例如,数据集构建可以更加精细化,代码结构也有待进一步优化和调整。我们非常期待各位小伙伴的宝贵建议和指正,让我们共同进步,一起在 AI 学习的道路上探索更多乐趣!↓ |