一、背景
通用模型除了挂载知识库,去回答垂类问题以外,还有就是做 SFT 的微调,而大多数人其实是没有英伟达显卡的,但又挡不住学习的渴望,还想在老旧的电脑上去尝试微调,而我翻看了很多教程,都没有一个完整能够完全跑通的完整案例,决定一定要整一个出来。
二、目标
在没有专业显卡的普通笔记本上去做 Deepseek 的微调,将它由一个通用模型改造为能够回答专业医疗问题的模型。它的特点是:微调电脑只有集成显卡,纯 CPU 微调,SFT 模式,transformers+LoRA,医疗类的垂类数据集。
三、最终效果
微调前胡说,给定数据集微调后,回答相对靠谱了。
四、整体三个步骤
第一步是下载模型和做本地的基础配置,让模型部署在本地,还能能够跑起来,看看它对医疗类问题如何解答。
第二步是核心,准备好数据集,建立python工程,编码,调参数,投喂数据,让模型开始微调,并且对微调后的模型进行保存。这个过程非常麻烦,虽然最终能跑的参数设置已在代码中了,但这都是我屡试屡不爽的调出来的,未必是最优,但确实能跑了。
第三步是做拿微调后的模型做推理验证,看看与之前有没有不一样,是否能够能够回答专业问题了。
五、开整
心急的同学直接跳到第 5 步:
1)先自检一下本机的配置,建议至少要达到我这个五年前的配置,低了会出现什么情况,我也不知道,当然是越高越好。
留个 20G 左右的空闲存储空间,用于保存原始模型、数据集、微调的过程模型。
2)先把基础模型下载下来,我的电脑算下来,基本上只能用huggingface 上的deepseek R1 的 1.5b 模型,整体下载下来大概不到 4个 G,下载地址如下: https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B,相关文件都可以下,包括自带的数据集,考虑到科学上网的问题,我把这部分放到了百度网盘(包含一个小数据集),自行取用:
链接: https://pan.baidu.com/s/1DgF9iv62qAH9qxBN37G6cw?pwd=4twx 提取码: 4twx
3)下载完成后,存到自己指定的目录,如我的是/Users/facaishu/DeepSeek15B,再下个 ollama,把下载好的 deepseek基础模型run起来试一下(ollama挂载本地已有模型的方法如下:AI学习笔记 本地下载好的 deepseek模型如何导入 ollama 中_ollama打开本地下载好的大模型-CSDN博客)
问它一个专业问题,看它怎么回复:
4)居然说是中风,感觉它是不是一个庸医?一本正经的胡扯…..那我们就开始上科技,用 pycharm(或者vscode),新建一个 py 工程,建好虚拟环境。可能需要 pip 安装transformers,peft等,不过没关系,缺啥代码跑进来时,会有提示,到时候安装即可。
5)新建一个 python 文件,内容如下,相关参数是经几经调整后,确保可以在我的配置电脑上跑起来的,每行参数做了备注,当前进度也做了一定的输出,方便掌握大概到了哪一步,如有需要,可以酌情调整:
from transformers import AutoTokenizer, AutoModelForCausalLM
from datasets import load_dataset
from peft import get_peft_model, LoraConfig, TaskType
from transformers import TrainingArguments, Trainer
from peft import PeftModel
from transformers import pipeline
# 微调部分代码
print("------开始做各种准备-----")
model_name = "/Users/facaishu/DeepSeek15B"
tokenizer = AutoTokenizer.from_pretrained(model_name)
print("---模型ok----")
# 加载数据集
dataset = load_dataset(path="json", data_files={"train": "medical_sft.json"}, split="train")
print("------数据集加载完成,条数为:", len(dataset))
# 划分训练集和验证集
train_test_split = dataset.train_test_split(test_size=0.1)
train_dataset = train_test_split["train"]
eval_dataset = train_test_split["test"]
# 定义分词函数
def tokenizer_function(many_samples):
texts = [f"{Question}\n{Response}" for Question, Response in
zip(many_samples["Question"], many_samples["Response"])]
print("texts:", texts)
tokens = tokenizer(texts, truncation=True, max_length=512, padding="max_length")
tokens["labels"] = tokens["input_ids"].copy()
return tokens
# 对数据集进行分词处理
tokenized_train_dataset = train_dataset.map(tokenizer_function, batched=True)
tokenized_eval_dataset = eval_dataset.map(tokenizer_function, batched=True)
print("---------分词配置完成--------")
# 配置 8 位量化
# quantization_config = BitsAndBytesConfig(load_in_8bit=True)
# 将模型加载到 CPU 上
# model = AutoModelForCausalLM.from_pretrained(
# model_name, quantization_config=quantization_config, device_map="cpu")
model = AutoModelForCausalLM.from_pretrained(model_name, device_map="cpu")
print("-----完成模型加载-----------")
# 配置 LoRA
lora_config = LoraConfig(r=8, lora_alpha=16, lora_dropout=0.05,
task_type=TaskType.CAUSAL_LM)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
print("----lora设置完成-------")
# 配置训练参数
training_args = TrainingArguments(
output_dir="./finetuned_models",
num_train_epochs=10, # 增加训练轮数
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
fp16=False,
logging_steps=10,
save_steps=100,
eval_strategy="steps",
eval_steps=10,
learning_rate=3e-5,
logging_dir="./logs",
run_name="deepseek-r1-distill-finetune"
)
print("------训练参数设置完毕-----")
print("------准备完事,准备开始微调-----")
# 创建 Trainer 实例
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_train_dataset,
eval_dataset=tokenized_eval_dataset
)
print("------进行微调--------")
trainer.train() # 调用 trainer 实例的 train 方法
print("------微调完成--------")
print("------微调完成了,对结果进行保存------")
#----------微调完了,要对结果进行保存----------------
sft_save_path = "./sft_save_models"
# 设计保存路径
model.save_pretrained(sft_save_path)
tokenizer.save_pretrained(sft_save_path)
print("---保存 lora 模型成功---")
final_save_path = "./final_saved_path"
base_model = AutoModelForCausalLM.from_pretrained(model_name)
model = PeftModel.from_pretrained(base_model, sft_save_path)
model = model.merge_and_unload()
model.save_pretrained(final_save_path)
tokenizer.save_pretrained(final_save_path)
print("-----全量保存成功-----")
6)我这边微调大概用了 40 分钟左右,如果看到以下输出,说明微调完事了。
7)完成后,再新建一个 python 代码,用于调用微调后的模型进行推理验证,这里面要注意的是,包括分词器和 lora 等参数,最好是与微调时一致,相关代码如下:
from transformers import AutoModelForCausalLM
from transformers import AutoTokenizer
from transformers import pipeline
import os
print("------开始进行推理验证-----")
# 模型和分词器的路径
final_saved_path = "./final_saved_path"
# 检查路径是否正确
if not os.path.exists(final_saved_path):
print(f"模型保存路径 {final_saved_path} 不存在,请检查!")
# 使用 AutoModelForCausalLM 的 from_pretrained 方法从指定路径加载因果语言模型
try:
model = AutoModelForCausalLM.from_pretrained(final_saved_path)
except Exception as e:
print(f"模型加载失败,错误信息:{e}")
# 使用 AutoTokenizer 的 from_pretrained 方法从指定路径加载分词器
try:
tokenizer = AutoTokenizer.from_pretrained(final_saved_path)
except Exception as e:
print(f"分词器加载失败,错误信息:{e}")
# 检查分词器参数是否与训练时一致
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
if tokenizer.padding_side != 'right':
tokenizer.padding_side = 'right'
# 使用 pipeline 函数创建一个文本生成任务的管道,指定使用加载的模型和分词器
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
# 定义一个提示文本,这是用户输入的问题,用于让模型生成回答
prompt = "一个男孩,2岁,6小时前出现惊厥,其后昏迷,头CT加强显示:基底节显影增强,最可能的诊断是什么"
# 模拟微调时的格式拼接
formatted_prompt = f"[START] {prompt} [END]\n"
# 调用管道对象,传入提示文本,调整生成参数
generated_text = pipe(formatted_prompt, max_length=2048, num_return_sequences=1, truncation=True,
temperature=0.2, top_p=0.8, top_k=40)
# 打印开始回答的提示信息,并从生成的文本列表中取出第一个结果,
# 再从结果字典中获取生成的文本内容进行输出
print("开始回答:", generated_text[0]["generated_text"])
8)运行此python,对微调后的模型进行验证,看看相同的问题,这次会怎么回答
看结果还可以,至少没有说是中风。
至此,微调算是成功了,但整体看,效果不是特别好,分析与微调时的参数有很大的关系。