LoRA微调大模型LoRA全流程 实现定制化ai助手
引子
因为项目需要一个大模型嵌入到app中实现ai助手,如果app的场景是定制化的,直接调用各家大模型的 api接口实现ai对话功能未免也有点太普通了。基于这样开发ai助手的例子网上也很多了。正好最近微调大模型挺流行,通过微调大模型能实现私人化ai助手。
此外,要调教大模型就要用到性能电脑,没有的话只能去租云服务器开发,使用云服务器价格偏高所以如果只是玩玩还行,但是如果要个人长期使用,肯定是直接调用各大模型官方api便宜的多。
写在前面
本文主要是为了记录整个从0到1配置全流程,作者本身是借助整个流程学习相关知识,微调采用的是LoRA
微调过程参考教程DeepSeek-7B-chat Lora 微调
整个操作流程大致可以这样划分:
1.找一个开源大模型
2.准备数据集
2.使用AutoDL平台微调大模型
3.测试
搭建整个系统你需要准备:
autoDL服务器

在正式开始之前,你还需要选择一个基座大模型,也就是你平时用到了chatgpt,豆包,gemini,copliot,deepseek等等这些大家耳熟能详的大模型。你要以这个大模型为基础,在它的上层构建属于你的大模型。如何选择大模型呢?首先这个模型必须是开源的,其次主要还是看你的成本和需求。它要求既能达到优秀的效果,又能降低部署的成本。这样就可以方便地在私有数据上进行微调,并且实现低成本的部署。
本文选用的是DeepSeek-R1-Distill-Qwen-7B模型,其他模型均可,这里重点解释一下后面的数字7B。xx-B 数字部分代表大模型参数的量级,B就是十亿billion的意思。这里7B就代表这个大模型的参数有70亿。大模型的参数不是它训练用了多少参数的意思,简单的说,现阶段不深究原理只需要知道数量级越大,参数越多,模型越聪明,任务处理能力越好。
所以你会在不少厂商开发的AI应用上看到“满血版”字样,其实说白了满血版和普通版区别就是在模型数量级上。但是这是有副作用的,成本随着参数量增大而增大,耗费算力越大,成本越高,也就是你的显卡要越多、还要越好。这也成为DeepSeek为啥那么火的原因之一,众所周知ChatGPT是最先火起来的生成式人工智能,但是它是闭源的。以chatgpt-4举例,根据媒体报道,它的数据参数已经达到万亿级别以上。而DeepSeek在降低成本的基础上还达到和几乎与chatgpt o1持平的推理效果,还开源。毕竟谁不希望降本增效呢......好了扯远了,如果你想使用DeepSeek的R1模型,在预算不高的情况下建议可以选择1.5B,7B和14B。
❤️实测在autoDL平台上微调DeepSeekR1大模型1.5B和7B至少需要一张RTX3090显卡,1.5B模型只需要一张3090显卡足矣,如果你的笔记本或者台式机显卡够好,内存够足(1.5B模型至少16G以上,7B模型至少32G以上),你甚至可以在本地微调大模型。这些网上教程很多,就不再细说了。
作者这边使用7B模型,为了速度更快,使用两张RTX3090显卡。
正式开始
打开AutoDL平台,选择租用新实例

镜像选择方面,推荐直接选择datawhale下的社区镜像-datawhalechina/self-llm/Deepseek-coder-v2,简化配置环境的过程

点击创建,创建成功后,在控制台-容器实例就能看到你创建的实例

点击开机,等待一段时间开机后,点击快捷工具-JupyterLab进入JupyterNotebook。
接下来我们就要正式开始微调大模型了。
1 准备模型
打开modelscope魔塔社区,直接在右上角搜索deepseek

在组织目录下找到deepseek,进入后找到模型,点进去,找到7B模型。或者直接https://www.modelscope.cn/models/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B,在这里进入。
点击“下载模型”按钮

弹出详细页面保持备用,回到autoDL平台。
新建一个文件夹命名为 ‘deepseek7b’ ,在该文件夹下打开终端,输入
pip install modelscope
安装完成后,复制刚刚魔塔社区中的下载命令,耐心等待下载完成

下载完成后继续下载依赖,先升级pip到最新:
python -m pip install -U pip
下载pytorch:
直接访问https://pytorch.org/get-started/locally/ ,注意版本。

其他需要用到的一些包
pip install pandas scikit-learn numpy
pip install transformers==4.35.2
pip install peft==0.4.0
pip install datasets==2.10.1
pip install accelerate==0.20.3
pip install tiktoken
2 微调
首先准备一个数据集,可以在网上找开源数据集,

其中,instruction 是用户指令,告知模型其需要完成的任务;input 是用户输入,是完成用户指令所必须的输入内容;output 是模型应该给出的输出。
打开新建一个jupyterNotebook,开始正式写代码,导入包:
from datasets import Dataset, DatasetDict
import pandas as pd
from transformers import (
AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq,
TrainingArguments, Trainer, GenerationConfig, EarlyStoppingCallback
)
import torch
from peft import LoraConfig, TaskType, get_peft_model
import numpy as np
from sklearn.model_selection import train_test_split
print("包加载成功")
2.1 定义数据预处理函数
1.系统提示词prompt
2.对每条数据,构造一段对话格式的字符串:
conversation:
<|begin▁of▁sentence|>System: ...\n\nUser: {user_question}\n\nAssistant:
response:
{assistant_answer}<|end▁of▁sentence|>
这样训练时的输入格式,和推理时调用使用的格式完全一致,模型更容易对齐助手角色,方便进行后续开发。
3.token 化
4.长度截断
最后,这个函数返回的是一个标准的字典{"input_ids", "attention_mask", "labels"},方便后续训练
# 定义数据预处理函数
def preprocess_chat_example(example, tokenizer):
"""把单条 {input, output} 样本转换为训练所需的 {input_ids, attention_mask, labels}(对话格式)。"""
MAX_LENGTH = 1024 # 序列长度
# 系统提示词
system_prompt = """你是一个专业的医药咨询助手,具有丰富的医学知识和临床经验。请根据用户的问题提供准确、专业、全面的医药知识回答。
专业要求:
1. 提供科学准确的医学信息
2. 使用专业但易懂的医学术语
3. 包含详细的作用机制说明
4. 提供必要的用药注意事项和禁忌症
5. 强调个体差异和专业医生指导的重要性
6. 如适用,提供相关的药物相互作用信息
7. 包含适当的剂量和用法指导(仅作参考)
请确保回答全面、准确、具有临床实用价值。"""
user_question = example["input"]
assistant_answer = example["output"]
# 使用 Chat 对话格式
conversation = f"<|begin▁of▁sentence|>System: {system_prompt}\n\nUser: {user_question}\n\nAssistant: "
response = f"{assistant_answer}<|end▁of▁sentence|>"
# Tokenization
instruction_tokens = tokenizer(conversation, add_special_tokens=False)
response_tokens = tokenizer(response, add_special_tokens=False)
input_ids = instruction_tokens["input_ids"] + response_tokens["input_ids"]
attention_mask = instruction_tokens["attention_mask"] + response_tokens["attention_mask"]
# 标签:只在回答部分计算 loss
labels = [-100] * len(instruction_tokens["input_ids"]) + response_tokens["input_ids"]
# 截断处理
if len(input_ids) > MAX_LENGTH:
input_ids = input_ids[:MAX_LENGTH]
attention_mask = attention_mask[:MAX_LENGTH]
labels = labels[:MAX_LENGTH]
return {
"input_ids": input_ids,
"attention_mask": attention_mask,
"labels": labels,
}
2.2 数据准备:从JSON到Dataset
划分了训练集和数据集,train_df和val_df
用 Dataset.from_pandas(train_df) / Dataset.from_pandas(val_df) 把pandas表格转换为Dataset,后面才能配合 map 做高效预处理。
df = pd.read_json('./dataset/data.json') #这里设置数据集路径
# 设置训练集和验证集
train_df, val_df = train_test_split(df, test_size=0.1, random_state=42) # 这里设置验证集大小
print(f"总数据量: {len(df)}")
print(f"训练集: {len(train_df)}")
print(f"验证集: {len(val_df)}")
# 创建数据集
train_dataset = Dataset.from_pandas(train_df)
val_dataset = Dataset.from_pandas(val_df)
# 加载tokenizer
model_path = './model/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B/' #这里写原模型路径
tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False, trust_remote_code=True)
tokenizer.padding_side = 'right'
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
print(f"词汇表大小: {len(tokenizer)}")
2.3 构建训练/验证数据集
把原始问答数据转换成模型可学习的对话格式样本,具体操作是把原始input / output列删掉
# 处理训练和验证数据集
train_tokenized = train_dataset.map(
lambda x: preprocess_chat_example(x, tokenizer),
remove_columns=train_dataset.column_names,
)
print("训练集处理完成")
val_tokenized = val_dataset.map(
lambda x: preprocess_chat_example(x, tokenizer),
remove_columns=val_dataset.column_names,
)
print("验证集处理完成")
# 检查样本
sample = train_tokenized[0]
print(f"\n样本长度: {len(sample['input_ids'])}")
print(f"预览: {tokenizer.decode(sample['input_ids'][:300])}...")
2.4 加载DeepSeek-R1-7B预训练模型
# 加载原模型
model = AutoModelForCausalLM.from_pretrained(
model_path,
trust_remote_code=True,
torch_dtype=torch.bfloat16, #bfloat16增加数值稳定性
device_map="auto", #重新启用自动设备映射
use_cache=False,
)
model.generation_config = GenerationConfig.from_pretrained(model_path)
model.generation_config.pad_token_id = tokenizer.pad_token_id
model.enable_input_require_grads()
print(f"模型加载完成,参数类型: {model.dtype}")
print(f"模型参数量: {sum(p.numel() for p in model.parameters()) / 1e9:.2f}B")
2.5 注入 LoRA
为了在 7B 模型上做定制化医学问答,使用了peft中的LoRA。
设置LoRA 超参数
r=64, lora_alpha=128: r=LoRA秩,数值偏大,代表更强的表达能力。lora_alpha=缩放系数,一起调节实际更新强度。
lora_dropout=0.05: 使用较低的dropout,防止过拟合。
inference_mode=False: 设为训练模式,允许参数参与梯度更新。
bias="none": 不对偏置项应用LoRA,减少冗余参数。
target_modules: 全部attention(q_proj, k_proj, v_proj, o_proj)、MLP(gate_proj, up_proj, down_proj)、embedding(embed_tokens)
modules_to_save=["embed_tokens", "lm_head"]: 进行全参数微调
用 get_peft_model(model, config) 把 LoRA 结构注入到原始模型,然后使用 print_trainable_parameters() 查看当前可训练参数占比。
config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj", # 所有attention层
"gate_proj", "up_proj", "down_proj", # 所有MLP层
"embed_tokens", "lm_head" # embedding和输出层
],
inference_mode=False,
r=64, # 大幅增加LoRA秩
lora_alpha=128, # 对应增加alpha
lora_dropout=0.05, # 较低的dropout
bias="none",
modules_to_save=["embed_tokens", "lm_head"], # 完全微调这些层
)
model = get_peft_model(model, config)
model.print_trainable_parameters()
print("LoRA配置完成")
2.6 设置其他训练超参数
这里不过多解释,详细配置方法移步https://hugging-face.cn/docs/peft/developer_guides/lora#lora-fa-optimizer
training_args = TrainingArguments(
output_dir="./output/DeepSeek_improved", # 设置模型输出目录
# 大批次设置
per_device_train_batch_size=8, # 增加批次大小
gradient_accumulation_steps=16, # 保持累积步数,等效批次大小=128
# 学习率和调度器
learning_rate=2e-5, # 适中的学习率
lr_scheduler_type="cosine_with_restarts", # 使用余弦退火重启
warmup_ratio=0.1, # 10%的步数用于预热
# 训练轮数和保存
num_train_epochs=10, # 增加训练轮数,充分训练
save_steps=100, # 保存频率
save_total_limit=5, # 保留最近5个检查点
save_strategy="steps", # 按步数保存
# 日志设置
logging_steps=25, # 更频繁的日志
logging_dir="./logs",
report_to="none", # 不使用外部报告工具
# 优化器设置
optim="adamw_torch", # AdamW优化器
adam_beta1=0.9,
adam_beta2=0.95,
adam_epsilon=1e-8,
weight_decay=0.01, # 权重衰减
max_grad_norm=1.0, # 梯度裁剪
# 数据加载设置 - 充分利用内存
dataloader_num_workers=8, # 增加数据加载worker数量
dataloader_pin_memory=True, # 使用固定内存
# 精度和性能设置
fp16=True, # 使用混合精度
gradient_checkpointing=True, # 梯度检查点
# 其他设置
remove_unused_columns=False,
push_to_hub=False,
resume_from_checkpoint=None,
# 种子设置
seed=42,
data_seed=42,
)
print("训练参数设置完成")
2.7 训练
在准备好数据与模型后,核心训练逻辑由Trainer接管。
# 数据收集器
data_collator = DataCollatorForSeq2Seq(
tokenizer=tokenizer,
padding=True,
max_length=1024, # 支持长序列
pad_to_multiple_of=8, # 优化GPU利用率
return_tensors="pt"
)
# 早停回调
early_stopping = EarlyStoppingCallback(
early_stopping_patience=3, # 3轮
early_stopping_threshold=0.001, # 改善阈值
)
# 创建训练器
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_tokenized,
eval_dataset=val_tokenized,
data_collator=data_collator,
callbacks=[early_stopping],
)
print("训练器创建完成")
print(f"有效批次大小: {training_args.per_device_train_batch_size * training_args.gradient_accumulation_steps}")
print(f"总训练步数预估: {len(train_tokenized) // (training_args.per_device_train_batch_size * training_args.gradient_accumulation_steps) * training_args.num_train_epochs}")
训练过程由 Trainer 自动完成,前向传播、反向传播、梯度裁剪、优化器更新、学习率调度等都不需要手写。
# 数据收集器
data_collator = DataCollatorForSeq2Seq(
tokenizer=tokenizer,
padding=True,
max_length=1024, # 匹配数据预处理的长度
pad_to_multiple_of=8,
return_tensors="pt"
)
# 创建训练器
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_tokenized, # 使用之前生成的训练集
eval_dataset=val_tokenized, # 可选
data_collator=data_collator,
)
# 开始训练
print("开始训练...")
trainer.train()
print("训练完成!")
# 保存最终模型
model.save_pretrained("./output/DeepSeek_improved/final_model")
tokenizer.save_pretrained("./output/DeepSeek_improved/final_model")
print("最终模型已保存到 ./output/DeepSeek_improved/final_model")
3 最终训练结果/模型推理
训练的模型文件

想要测试模型效果,可以定义一个测试函数,实现与刚刚导入原模型相似
# 测试函数
def test_model(question, max_new_tokens=300):
"""测试模型:输入问题,返回回答"""
model.eval()
system_prompt = """你是一个专业的医药咨询助手,具有丰富的医学知识和临床经验。请根据用户的问题提供准确、专业、全面的医药知识回答。
专业要求:
1. 提供科学准确的医学信息
2. 使用专业但易懂的医学术语
3. 包含详细的作用机制说明
4. 提供必要的用药注意事项和禁忌症
5. 强调个体差异和专业医生指导的重要性
6. 如适用,提供相关的药物相互作用信息
7. 包含适当的剂量和用法指导(仅作参考)
请确保回答全面、准确、具有临床实用价值。"""
input_text = f"<|begin▁of▁sentence|>System: {system_prompt}\n\nUser: {question}\n\nAssistant: "
inputs = tokenizer(input_text, return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
temperature=0.7,
do_sample=True,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id
)
generated_text = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
return generated_text.strip()
print("测试函数已定义")
调用这个函数输入问题就能打印结果啦~
本文到此结束,有任何问题欢迎留言

更多推荐

所有评论(0)