DeepSeek模型微调实战:打造专业心理医生AI模型

50

深入探索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模型。