为了方便学习与体验,本文中选择的模型是蒸馏后 DeepSeek-R1-Distill-Qwen-7B 模型,显卡选择是 RTX4090 24G。
Deepseek 模型以及数据集均来源于魔塔社区 medical-o1-reasoning-SFT。
1. 微调教程复现
import torch
import matplotlib.pyplot as plt
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
TrainingArguments,
Trainer,
TrainerCallback
)
from peft import LoraConfig, get_peft_model
from datasets import load_dataset
import os
os.environ["CUDA_VISIBLE_DEVICES"] = "0" # 指定使用GPU
# 配置路径(根据实际路径修改)
model_path = "xxxx" # 模型路径
data_path = "xxxx" # 数据集路径
output_path = "xxxx" # 微调后模型保存路径
# 设置设备参数
DEVICE = "cuda" # 使用CUDA
DEVICE_ID = "0" # CUDA设备ID,如果未设置则为空
device = f"{DEVICE}:{DEVICE_ID}" if DEVICE_ID else DEVICE # 组合CUDA设备信息
# 自定义回调记录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"])
# 数据预处理函数
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=8
num_train_epochs=3,
learning_rate=3e-4,
fp16=True, # 开启混合精度
logging_steps=20,
save_strategy="no",
report_to="none",
optim="adamw_torch",
no_cuda=False, # 强制使用CUDA
dataloader_pin_memory=False, # 加速数据加载
remove_unused_columns=False, # 防止删除未使用的列
device="cuda:0" # 指定使用的GPU设备
)
def main():
# 创建输出目录
os.makedirs(output_path, exist_ok=True)
# 加载tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_path)
tokenizer.pad_token = tokenizer.eos_token
# 加载模型到GPU
model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.float16,
device_map=device
)
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
# 创建Trainer
trainer = 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()
我们看看 LOSS 曲线。
可以看到经过简单的微调,模型的 LOSS 值是有降低,说明 Deepseek 模型是对训练集的数据集有拟合的。
2.直观比较模型生成
模型微调完,生成的内容效果如何,怎么进行比较呢?
这个时候我们首先想到的是直接比较「微调模型」和「原始模型」对同一个问题生成的回答内容进行比较。
因此我们可以统一提示词,统一相关的问题,然后比较生成的答案。
具体代码如下:
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
import os
import json
from bert_score import score
from tqdm import tqdm
# 设置可见GPU设备(根据实际GPU情况调整)
os.environ["CUDA_VISIBLE_DEVICES"] = "0" # 指定仅使用GPU
# 路径配置 ------------------------------------------------------------------------
base_model_path = "xxxxx" # 原始预训练模型路径
peft_model_path = "xxxxx" # LoRA微调后保存的适配器路径
# 模型加载 ------------------------------------------------------------------------
# 初始化分词器(使用与训练时相同的tokenizer)
tokenizer = AutoTokenizer.from_pretrained(base_model_path)
# 加载基础模型(半精度加载节省显存)
base_model = AutoModelForCausalLM.from_pretrained(
base_model_path,
torch_dtype=torch.float16, # 使用float16精度
device_map="auto" # 自动分配设备(CPU/GPU)
)
# 加载LoRA适配器(在基础模型上加载微调参数)
lora_model = PeftModel.from_pretrained(
base_model,
peft_model_path,
torch_dtype=torch.float16,
device_map="auto"
)
# 合并LoRA权重到基础模型(提升推理速度,但会失去再次训练的能力)
lora_model = lora_model.merge_and_unload()
lora_model.eval() # 设置为评估模式
# 生成函数 ------------------------------------------------------------------------
def generate_response(model, prompt):
"""统一的生成函数
参数:
model : 要使用的模型实例
prompt : 符合格式要求的输入文本
返回:
清洗后的回答文本
"""
# 输入编码(保持与训练时相同的处理方式)
inputs = tokenizer(
prompt,
return_tensors="pt", # 返回PyTorch张量
max_length=1024, # 最大输入长度(与训练时一致)
truncation=True, # 启用截断
padding="max_length" # 填充到最大长度(保证batch一致性)
).to(model.device) # 确保输入与模型在同一设备
# 文本生成(关闭梯度计算以节省内存)
with torch.no_grad():
outputs = model.generate(
input_ids=inputs.input_ids,
attention_mask=inputs.attention_mask,
max_new_tokens=1024, # 生成内容的最大token数(控制回答长度)
temperature=0.7, # 温度参数(0.0-1.0,值越大随机性越强)
top_p=0.9, # 核采样参数(保留累积概率前90%的token)
repetition_penalty=1.1, # 重复惩罚系数(>1.0时抑制重复内容)
eos_token_id=tokenizer.eos_token_id, # 结束符ID
pad_token_id=tokenizer.pad_token_id, # 填充符ID
)
# 解码与清洗输出
full_text = tokenizer.decode(outputs[0], skip_special_tokens=True) # 跳过特殊token
answer = full_text.split("### 答案:\n")[-1].strip() # 提取答案部分
return answer
# 对比测试函数 --------------------------------------------------------------------
def compare_models(question):
"""模型对比函数
参数:
question : 自然语言形式的医疗问题
"""
# 构建符合训练格式的prompt(注意与训练时格式完全一致)
prompt = f"诊断问题:{question}\n详细分析:\n### 答案:\n"
# 双模型生成
base_answer = generate_response(base_model, prompt) # 原始模型
lora_answer = generate_response(lora_model, prompt) # 微调模型
# 终端彩色打印对比结果
print("\n" + "="*50) # 分隔线
print(f"问题:{question}")
print("-"*50)
print(f"\033[1;34m[原始模型]\033[0m\n{base_answer}") # 蓝色显示原始模型结果
print("-"*50)
print(f"\033[1;32m[LoRA模型]\033[0m\n{lora_answer}") # 绿色显示微调模型结果
print("="*50 + "\n")
# 主程序 ------------------------------------------------------------------------
if __name__ == "__main__":
# 测试问题集(可自由扩展)
test_questions = [
"根据描述,一个1岁的孩子在夏季头皮出现多处小结节,长期不愈合,且现在疮大如梅,溃破流脓,口不收敛,头皮下有空洞,患处皮肤增厚。这种病症在中医中诊断为什么病?"
]
# 遍历测试问题
for q in test_questions:
compare_models(q)
来看看模型对同一个问题输出结果的差异,这里为了凸显图像微调后与原始模型的差异,选择了训练集中的一条数据进行测试,读者可以根据自己的情况随机测试。
我们来看看生成的内容。
根据生成的内容,看起来 LoRA 微调后的模型好像还是和原始模型有些不同的,但是这个回答要比较的话就很抽象,毕竟作为学习者我们对医疗领域的问题可能了解的也不太多,能否通过一些比较直观的方法来体现微调后模型与原始模型的差异呢?
这个时候我们想到了能否通过文本的相似性来评估,可以使用 bertscore 对模型进行比较,那 bertscore 是什么呢?我们来看看 Deepseek 满血版给我的答复,输出的内容太多了,这里就不全部粘贴过来,主体来说就是衡量语意的相似性,那我们似乎可以通过 berscore 来比较训练集的答案和模型生成的答案,来比较直观的看看微调后的模型与原始模型的差异。
这里为了方便学习者进行学习,以下代码中选择的 bert 模型是最基础的 bert-base-chinese 模型,同样可以在魔塔社区进行下载。
需要说明的是,考虑到部分学习者可能无法访问 hugging face 的官网,这里的 bert-base-chinese 模型采用离线的模型进行加载。
温馨提示,模型的评估非常消耗资源,这里建议学习者只调用 10 条数据集即可。
ok,我们来看看代码:
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
import os
import json
from bert_score import score
from tqdm import tqdm
# 设置可见GPU设备(根据实际GPU情况调整)
os.environ["CUDA_VISIBLE_DEVICES"] = "0" # 指定仅使用GPU
# 路径配置 ------------------------------------------------------------------------
base_model_path = "xxxxxx/DeepSeek-R1-Distill-Qwen-7B" # 原始预训练模型路径
peft_model_path = "xxxxxx/output" # LoRA微调后保存的适配器路径
# 模型加载 ------------------------------------------------------------------------
# 初始化分词器(使用与训练时相同的tokenizer)
tokenizer = AutoTokenizer.from_pretrained(base_model_path)
# 加载基础模型(半精度加载节省显存)
base_model = AutoModelForCausalLM.from_pretrained(
base_model_path,
torch_dtype=torch.float16, # 使用float16精度
device_map="auto" # 自动分配设备(CPU/GPU)
)
# 加载LoRA适配器(在基础模型上加载微调参数)
lora_model = PeftModel.from_pretrained(
base_model,
peft_model_path,
torch_dtype=torch.float16,
device_map="auto"
)
# 合并LoRA权重到基础模型(提升推理速度,但会失去再次训练的能力)
lora_model = lora_model.merge_and_unload()
lora_model.eval() # 设置为评估模式
# 生成函数 ------------------------------------------------------------------------
def generate_response(model, prompt):
"""统一的生成函数
参数:
model : 要使用的模型实例
prompt : 符合格式要求的输入文本
返回:
清洗后的回答文本
"""
# 输入编码(保持与训练时相同的处理方式)
inputs = tokenizer(
prompt,
return_tensors="pt", # 返回PyTorch张量
max_length=1024, # 最大输入长度(与训练时一致)
truncation=True, # 启用截断
padding="max_length" # 填充到最大长度(保证batch一致性)
).to(model.device) # 确保输入与模型在同一设备
# 文本生成(关闭梯度计算以节省内存)
with torch.no_grad():
outputs = model.generate(
input_ids=inputs.input_ids,
attention_mask=inputs.attention_mask,
max_new_tokens=1024, # 生成内容的最大token数(控制回答长度)
temperature=0.7, # 温度参数(0.0-1.0,值越大随机性越强)
top_p=0.9, # 核采样参数(保留累积概率前90%的token)
repetition_penalty=1.1, # 重复惩罚系数(>1.0时抑制重复内容)
eos_token_id=tokenizer.eos_token_id, # 结束符ID
pad_token_id=tokenizer.pad_token_id, # 填充符ID
)
# 解码与清洗输出
full_text = tokenizer.decode(outputs[0], skip_special_tokens=True) # 跳过特殊token
answer = full_text.split("### 答案:\n")[-1].strip() # 提取答案部分
return answer
# 对比测试函数 --------------------------------------------------------------------
def compare_models(question):
"""模型对比函数
参数:
question : 自然语言形式的医疗问题(如"小孩感冒怎么办?")
"""
# 构建符合训练格式的prompt(注意与训练时格式完全一致)
prompt = f"诊断问题:{question}\n详细分析:\n### 答案:\n"
# 双模型生成
base_answer = generate_response(base_model, prompt) # 原始模型
lora_answer = generate_response(lora_model, prompt) # 微调模型
# 终端彩色打印对比结果
print("\n" + "="*50) # 分隔线
print(f"问题:{question}")
print("-"*50)
print(f"\033[1;34m[原始模型]\033[0m\n{base_answer}") # 蓝色显示原始模型结果
print("-"*50)
print(f"\033[1;32m[LoRA模型]\033[0m\n{lora_answer}") # 绿色显示微调模型结果
print("="*50 + "\n")
# 主程序 ------------------------------------------------------------------------
if __name__ == "__main__":
# 测试问题集(可自由扩展)
# test_questions = [
# "根据描述,一个1岁的孩子在夏季头皮出现多处小结节,长期不愈合,且现在疮大如梅,溃破流脓,口不收敛,头皮下有空洞,患处皮肤增厚。这种病症在中医中诊断为什么病?"
# ]
# # 遍历测试问题
# for q in test_questions:
# compare_models(q)
# 加载测试数据
####-----------批量测试---------------#
with open("xxxxxx/data/medical_o1_sft_Chinese.json") as f:
test_data = json.load(f)
# 数据量比较大,我们只选择10条数据进行测试
test_data=test_data[:10]
# 批量生成回答
def batch_generate(model, questions):
answers = []
for q in tqdm(questions):
prompt = f"诊断问题:{q}\n详细分析:\n### 答案:\n"
ans = generate_response(model, prompt)
answers.append(ans)
return answers
# 生成结果
base_answers = batch_generate(base_model, [d["Question"] for d in test_data])
lora_answers = batch_generate(lora_model, [d["Question"] for d in test_data])
ref_answers = [d["Response"] for d in test_data]
bert_model_path="xxxxx/model/bert-base-chinese"
# 计算BERTScore
_, _, base_bert = score(base_answers, ref_answers, lang="zh",model_type=bert_model_path,num_layers=12,device="cuda")
_, _, lora_bert = score(lora_answers, ref_answers, lang="zh",model_type=bert_model_path,num_layers=12,device="cuda")
print(f"BERTScore | 原始模型: {base_bert.mean().item():.3f} | LoRA模型: {lora_bert.mean().item():.3f}")
我们来看看结果:
结果
可以看到利用 bertscore 比较数据集的参考答案与模型生成答案的相似性来看,LoRA微调后的结果和原始模型相比还是有细微的差异,随着 LoRA 微调的训练轮次加深,甚至我们故意让大模型产生“过拟合”后,比较这个相似性,这个结果的差异应该会进一步加大,可以从一个相对定性的角度给学习者提供一个新的视角。
如何学习大模型 AI ?
由于新岗位的生产效率,要优于被取代岗位的生产效率,所以实际上整个社会的生产效率是提升的。
但是具体到个人,只能说是:
“最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。
这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。
我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
第一阶段(10天):初阶应用
该阶段让大家对大模型 AI有一个最前沿的认识,对大模型 AI 的理解超过 95% 的人,可以在相关讨论时发表高级、不跟风、又接地气的见解,别人只会和 AI 聊天,而你能调教 AI,并能用代码将大模型和业务衔接。
- 大模型 AI 能干什么?
- 大模型是怎样获得「智能」的?
- 用好 AI 的核心心法
- 大模型应用业务架构
- 大模型应用技术架构
- 代码示例:向 GPT-3.5 灌入新知识
- 提示工程的意义和核心思想
- Prompt 典型构成
- 指令调优方法论
- 思维链和思维树
- Prompt 攻击和防范
- …
第二阶段(30天):高阶应用
该阶段我们正式进入大模型 AI 进阶实战学习,学会构造私有知识库,扩展 AI 的能力。快速开发一个完整的基于 agent 对话机器人。掌握功能最强的大模型开发框架,抓住最新的技术进展,适合 Python 和 JavaScript 程序员。
- 为什么要做 RAG
- 搭建一个简单的 ChatPDF
- 检索的基础概念
- 什么是向量表示(Embeddings)
- 向量数据库与向量检索
- 基于向量检索的 RAG
- 搭建 RAG 系统的扩展知识
- 混合检索与 RAG-Fusion 简介
- 向量模型本地部署
- …
第三阶段(30天):模型训练
恭喜你,如果学到这里,你基本可以找到一份大模型 AI相关的工作,自己也能训练 GPT 了!通过微调,训练自己的垂直大模型,能独立训练开源多模态大模型,掌握更多技术方案。
到此为止,大概2个月的时间。你已经成为了一名“AI小子”。那么你还想往下探索吗?
- 为什么要做 RAG
- 什么是模型
- 什么是模型训练
- 求解器 & 损失函数简介
- 小实验2:手写一个简单的神经网络并训练它
- 什么是训练/预训练/微调/轻量化微调
- Transformer结构简介
- 轻量化微调
- 实验数据集的构建
- …
第四阶段(20天):商业闭环
对全球大模型从性能、吞吐量、成本等方面有一定的认知,可以在云端和本地等多种环境下部署大模型,找到适合自己的项目/创业方向,做一名被 AI 武装的产品经理。
- 硬件选型
- 带你了解全球大模型
- 使用国产大模型服务
- 搭建 OpenAI 代理
- 热身:基于阿里云 PAI 部署 Stable Diffusion
- 在本地计算机运行大模型
- 大模型的私有化部署
- 基于 vLLM 部署大模型
- 案例:如何优雅地在阿里云私有部署开源大模型
- 部署一套开源 LLM 项目
- 内容安全
- 互联网信息服务算法备案
- …
学习是一个过程,只要学习就会有挑战。天道酬勤,你越努力,就会成为越优秀的自己。
如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。