深入探索DeepSeek模型微调:实战指南与性能优化
随着人工智能领域的飞速发展,大型语言模型(LLM)如DeepSeek,正日益成为研究和应用的核心。本文将深入探讨DeepSeek模型的微调技术,以deepseek-llm-7b-chat模型为基础,结合EmoLLM数据集,旨在实现一个具备心理医生对话风格的AI模型。我们将详细介绍transformers和openMind两种框架下的微调实践,并提供实战代码和结果演示,助您掌握高效的模型调优方法。
1. 环境搭建与基础配置
在开始微调之前,我们需要搭建一个合适的实验环境。由于本次实验涉及到多个参数的对比,对显存有一定的要求。推荐配置如下:
- GPU:40GB 左右
- Python:>=3.8
接下来,安装必要的Python包:
conda install pytorch torchvision torchaudio pytorch-cuda=11.8 -c pytorch -c nvidia
pip install transformers -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install openmind -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install datasets -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install peft -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install bitsandbytes -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install -U swanlab -i https://pypi.tuna.tsinghua.edu.cn/simple
2. 数据准备与预处理
为了使模型具备心理医生的对话能力,我们需要一个包含心理健康相关对话的数据集。本文使用EmoLLM-datasets,该数据集已经过清洗,保证了数据质量。
数据集的格式如下:
[
{
"conversation": [
{
"system": "现在你是一个心理专家,我有一些心理问题,请你用专业的知识帮我解决。",
"input": "医生,我最近在社交场合总是感到非常紧张和焦虑,尤其是当有很多人的时候,我甚至会出汗、心跳加速。\n\n",
"output": "我理解你的感受,社交焦虑是很常见的。首先,我想告诉你,你并不孤单。请问你能告诉我,在哪些具体的社交场景中,你感到最不舒服或焦虑呢?\n\n"
},
{
"input": "主要是参加聚会或者需要公开发言的场合。我总是担心自己会说错话,或者别人会对我有不好的评价。\n\n",
"output": "明白了。在这种情况下,我们首先要认识到,每个人都有可能说错话或遇到尴尬的情况,这是正常的。我建议你可以尝试一些放松技巧,比如深呼吸、正念冥想,或者在参加社交活动前进行一些轻松的身体活动,以减轻紧张感。\n\n"
},
{
"input": "我试过深呼吸,但感觉效果并不明显。\n\n",
"output": "深呼吸确实需要一定的练习才能看到效果。除了深呼吸,你还可以尝试将自己的注意力转移到当下,而不是过分担忧未来可能发生的事情。此外,你能告诉我,你在担心别人对你有不好的评价时,具体是在担心些什么吗?\n\n"
},
...
]
}
]
预处理的目标是将数据集转换为模型训练所需的格式,包括input_ids、attention_mask和labels。
数据映射
from transformers import AutoTokenizer,AutoModelForCausalLM,DataCollatorForSeq2Seq
from datasets import Dataset
import pandas as pd
model_path = "your_model_path" # 替换为你的模型路径
tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False)
data_path = "./data/medical_multi_data.json"
data = pd.read_json(data_path)
train_ds = Dataset.from_pandas(data)
def process_data(data, tokenizer, max_seq_length):
input_ids, attention_mask, labels = [], [], []
conversations = data["conversation"]
for i,conv in enumerate(conversations):
if "instruction" in conv:
instruction_text = conv['instruction']
else:
instruction_text = ""
human_text = conv["input"]
assistant_text = conv["output"]
input_text = f"{tokenizer.bos_token}{instruction_text}\n\nUser:{human_text}\n\nAssistant:"
input_tokenizer = tokenizer(
input_text,
add_special_tokens=False,
truncation=True,
padding=False,
return_tensors=None,
)
output_tokenizer = tokenizer(
assistant_text,
add_special_tokens=False,
truncation=True,
padding=False,
return_tensors=None,
)
input_ids += (
input_tokenizer["input_ids"] + output_tokenizer["input_ids"] + [tokenizer.eos_token_id]
)
attention_mask += input_tokenizer["attention_mask"] + output_tokenizer["attention_mask"] + [1]
labels += ([-100] * len(input_tokenizer["input_ids"]) + output_tokenizer["input_ids"] + [tokenizer.eos_token_id]
)
if len(input_ids) > max_seq_length: # 截断
input_ids = input_ids[:max_seq_length]
attention_mask = attention_mask[:max_seq_length]
labels = labels[:max_seq_length]
return {
"input_ids": input_ids,
"attention_mask": attention_mask,
"labels": labels
}
train_dataset = train_ds.map(process_data,
fn_kwargs={"tokenizer": tokenizer, "max_seq_length": tokenizer.model_max_length},
remove_columns=train_ds.column_names)
需要注意的是,input_text
的格式必须与模型的tokenizer_config文件中的chat_template
一致,否则可能导致生成结果异常。
数据封装
transformers:
from transformers import DataCollatorForSeq2Seq
data_collator = DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True, return_tensors="pt")
openMind:
由于openMind缺少数据封装函数,需要手动添加:
import torch
class DataCollatorForSeq2SeqCustom:
def __init__(self, tokenizer, padding=True, return_tensors="pt"):
self.tokenizer = tokenizer
self.padding = padding # 是否填充到最大长度
self.return_tensors = return_tensors # 返回格式,默认为 pytorch tensor
def __call__(self, batch):
# 从 batch 中提取 input_ids, attention_mask, 和 labels
input_ids = [example['input_ids'] for example in batch]
attention_mask = [example['attention_mask'] for example in batch]
labels = [example['labels'] for example in batch]
# 填充所有 sequences 到最大长度
input_ids = self.pad_sequence(input_ids)
attention_mask = self.pad_sequence(attention_mask)
labels = self.pad_sequence(labels)
# 如果需要返回 pytorch tensor,则将数据转换为 tensor 格式
if self.return_tensors == "pt":
input_ids = torch.tensor(input_ids)
attention_mask = torch.tensor(attention_mask)
labels = torch.tensor(labels)
return {"input_ids": input_ids, "attention_mask": attention_mask, "labels": labels}
def pad_sequence(self, sequences):
# 填充序列到最大长度
max_length = max(len(seq) for seq in sequences)
padded_sequences = [
seq + [self.tokenizer.pad_token_id] * (max_length - len(seq)) for seq in sequences
]
return padded_sequences
data_collator = DataCollatorForSeq2SeqCustom(tokenizer=tokenizer, padding=True, return_tensors="pt")
3. Lora配置与训练参数设置
LoRA (Low-Rank Adaptation) 是一种高效的微调技术,通过引入低秩矩阵来更新模型参数,显著减少了计算资源的需求。以下是LoRA的参数配置:
from peft import LoraConfig, TaskType
lora_config = LoraConfig(
r=64,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
target_modules=['up_proj', 'gate_proj', 'q_proj', 'o_proj', 'down_proj', 'v_proj', 'k_proj'],
task_type=TaskType.CAUSAL_LM,
inference_mode=False # 训练模式
)
接下来,设置训练参数:
try:
from openmind import TrainingArguments
except:
from transformers import TrainingArguments
output_dir="./output/deepseek-mutil-test"
train_args = TrainingArguments(
output_dir=output_dir,
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
logging_steps=1,
num_train_epochs=3,
save_steps=5000,
learning_rate=2e-5,
save_on_each_node=True,
gradient_checkpointing=True,
report_to=None,
seed=42,
optim="adamw_torch",
fp16=True,
bf16=False,
remove_unused_columns=False,
)
4. SwanLab集成与可视化
为了更好地监控训练过程,我们集成了SwanLab,一个开源的机器学习日志跟踪和实验管理工具。
from swanlab.integration.transformers import SwanLabCallback
import os
swanlab_config = {
"dataset": data_path,
"peft":"lora"
}
swanlab_callback = SwanLabCallback(
project="deepseek-finetune-test",
experiment_name="first-test",
description="微调多轮对话",
workspace=None,
config=swanlab_config,
)
5. 模型训练与保存
现在,我们可以开始训练模型了。
from peft import get_peft_model
try:
from openmind import Trainer
except:
from transformers import Trainer
model.enable_input_require_grads()
model = get_peft_model(model,lora_config)
model.print_trainable_parameters()
trainer = Trainer(
model=model,
args=train_args,
train_dataset=train_dataset,
data_collator=data_collator,
callbacks=[swanlab_callback],
)
trainer.train()
from os.path import join
final_save_path = join(output_dir)
trainer.save_model(final_save_path)
final_save_path = join(output_dir)
trainer.save_state()
trainer.save_model(final_save_path)
6. 模型合并与推理
微调后,需要将LoRA权重合并到原始模型中,以便进行推理。
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
import os
import shutil
def copy_files_not_in_B(A_path, B_path):
if not os.path.exists(A_path):
raise FileNotFoundError(f"The directory {A_path} does not exist.")
if not os.path.exists(B_path):
os.makedirs(B_path)
# 获取路径A中所有非权重文件
files_in_A = os.listdir(A_path)
files_in_A = set([file for file in files_in_A if not (".bin" in file or "safetensors" in file)])
files_in_B = set(os.listdir(B_path))
# 找到所有A中存在但B中不存在的文件
files_to_copy = files_in_A - files_in_B
# 将文件或文件夹复制到B路径下
for file in files_to_copy:
src_path = os.path.join(A_path, file)
dst_path = os.path.join(B_path, file)
if os.path.isdir(src_path):
# 复制目录及其内容
shutil.copytree(src_path, dst_path)
else:
# 复制文件
shutil.copy2(src_path, dst_path)
def merge_lora_to_base_model(model_name_or_path,adapter_name_or_path,save_path):
# 如果文件夹不存在,就创建
if not os.path.exists(save_path):
os.makedirs(save_path)
tokenizer = AutoTokenizer.from_pretrained(model_name_or_path,trust_remote_code=True,)
model = AutoModelForCausalLM.from_pretrained(
model_name_or_path,
trust_remote_code=True,
low_cpu_mem_usage=True,
torch_dtype=torch.float16,
device_map="auto"
)
# 加载保存的 Adapter
model = PeftModel.from_pretrained(model, adapter_name_or_path, device_map="auto",trust_remote_code=True)
# 将 Adapter 合并到基础模型中
merged_model = model.merge_and_unload() # PEFT 的方法将 Adapter 权重合并到基础模型
# 保存合并后的模型
tokenizer.save_pretrained(save_path)
merged_model.save_pretrained(save_path, safe_serialization=False)
copy_files_not_in_B(model_name_or_path, save_path)
print(f"合并后的模型已保存至: {save_path}")
7. SwanLab结果分析与参数调优
通过SwanLab,我们可以详细分析训练过程中的各项指标,如学习率、LoRA秩、缩放因子、dropout、微调层和训练周期等,从而优化模型性能。
学习率 (lr)
学习率是影响模型收敛速度和稳定性的关键参数。合适的学习率能够加快收敛,避免梯度爆炸或消失。
- 当lr=2e-6时,损失和梯度范数变化缓慢,模型更新不足。
- 当lr=1e-3时,损失和梯度范数波动较大,可能导致训练不稳定。
- 推荐的学习率范围为2e-4~1e-3。
Lora的秩 (r)
LoRA的秩越高,模型容量越大,但计算成本也会增加。选择合适的秩需要在性能和效率之间进行权衡。
Lora的缩放因子 (α)
缩放因子用于调整低秩矩阵更新的幅度,影响模型的稳定性和收敛性。
Lora的正则化参数 (dropout)
Dropout用于防止过拟合,但对LoRA微调的影响相对较小。
Lora微调模型层
微调不同的模型层会影响训练时长和模型性能。微调所有线性层通常能获得更好的效果,但训练时间也会相应增加。
训练周期 (epoch)
过多的训练周期可能导致过拟合,应选择合适的epoch数量。
批次大小 (batch_size)
较大的batch_size可以提高训练速度,但也需要更多的显存。合适的batch_size能够提高模型收敛效果。
梯度累计步数 (gradient_accumulation_steps)
梯度累计步数用于在显存有限的情况下模拟更大的batch_size。梯度累计步数越大,每次参数更新越精确,但显存需求也越高。
结论
通过本文的详细介绍,相信您已经掌握了DeepSeek模型微调的基本流程和关键技术。希望您能够在实践中不断探索,找到最适合自己应用场景的参数配置,从而构建出高效、强大的AI模型。