引子

因为项目需要一个大模型嵌入到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模型,其他模型均可,这里重点解释一下后面的数字7Bxx-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("测试函数已定义")

调用这个函数输入问题就能打印结果啦~

本文到此结束,有任何问题欢迎留言

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐