LLM微调-训练垂类问答模型

目录

一、数据下载

二、模型下载

三、模型微调

1)数据处理:收集和清洗

[Step 1: 数据收集](#Step 1: 数据收集)

[Step 2: 数据清洗](#Step 2: 数据清洗)

[Step 3: 格式转换与保存](#Step 3: 格式转换与保存)

[Step 4: 均衡采样(用于快速实验)](#Step 4: 均衡采样(用于快速实验))

2)SFT模型微调

[Step 1:预训练模型加载,得到model 和 tokenizer](#Step 1:预训练模型加载,得到model 和 tokenizer)

[Step 2:配置lora,通过get_peft_model创建Lora的小模型,最开始这个小模型为空](#Step 2:配置lora,通过get_peft_model创建Lora的小模型,最开始这个小模型为空)

[Step 3:加载数据,读取数据集](#Step 3:加载数据,读取数据集)

[Step 4:SFTTrainer设定好参数,开始训练](#Step 4:SFTTrainer设定好参数,开始训练)

3)效果的验证

GPU版本补充


一、数据下载

Chinese medical dialogue data 中文医疗对话数据集,共有6个文件夹,数据格式都是问答对

Toyhom/Chinese-medical-dialogue-data: Chinese medical dialogue data 中文医疗对话数据集

二、模型下载

我选用的是Qwen/Qwen3.5-0.8B这个模型,以下流程是用CPU跑的。下载模型流程如下:

step 1:检测环境

python 复制代码
MODEL_ID = "Qwen/Qwen3.5-0.8B"

# AutoDL环境
AUTODL_CACHE_DIR = "/root/autodl-tmp/models"

# 本地Windows环境
LOCAL_CACHE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "models")


def detect_environment():
    """检测当前运行环境"""
    if sys.platform == "win32":
        return "windows", LOCAL_CACHE_DIR
    elif os.path.exists("/root/autodl-tmp"):
        return "autodl", AUTODL_CACHE_DIR
    else:
        return "linux", LOCAL_CACHE_DIR

step 2:使用ModelScop下载

python 复制代码
def download_with_modelscope(model_id, cache_dir):
    """使用ModelScope下载模型(国内推荐)"""
    from modelscope import snapshot_download

    print(f"使用ModelScope下载模型: {model_id}")
    print(f"下载目录: {cache_dir}")

    os.makedirs(cache_dir, exist_ok=True)
    model_dir = snapshot_download(model_id, cache_dir=cache_dir)

    print(f"模型下载完成: {model_dir}")
    return model_dir

在前面 HuggingFace介绍 里面,我们曾介绍了国内两种模型下载的方式:

1)使用HF-Mirror (镜像站)

2)使用ModelScope(魔搭社区)

还有一种直接从HuggingFace上直接下载模型的方式,今天一并介绍(几乎和上面代码是一样的):

python 复制代码
def download_with_huggingface(model_id, cache_dir):
    """使用HuggingFace下载模型"""
    from huggingface_hub import snapshot_download

    print(f"使用HuggingFace下载模型: {model_id}")
    print(f"下载目录: {cache_dir}")

    os.makedirs(cache_dir, exist_ok=True)
    model_dir = snapshot_download(
        model_id,
        cache_dir=cache_dir,
        local_dir=os.path.join(cache_dir, model_id.split("/")[-1]),
    )

    print(f"模型下载完成: {model_dir}")
    return model_dir

各位跑一下代码看看:

python 复制代码
env_name, cache_dir = detect_environment()
model_dir = download_with_modelscope(MODEL_ID, cache_dir)

三、模型微调

模型微调的全流程还要分为以下三个步骤:

1)数据处理:收集和清洗

Step 1: 数据收集

python 复制代码
 # 配置数据目录(根据实际路径修改)
 data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "【数据集】中文医疗数据")
 output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "processed_data")
    
 raw_data, collect_stats = collect_medical_data(data_dir)

collect_medical_data 函数遍历DEPARTMENTS,读取数据集中每个科室的所有的.csv文件(read_csv_with_encoding 函数为读取函数)。输出的raw_data为每个科室读出来的数据,stats为每个科室的数据行数。

Step 2: 数据清洗

python 复制代码
cleaned_data = clean_medical_data(raw_data)

将raw_data作为参数传给clean_medical_data 函数。遍历raw_data_list,提取问题和回答字段(extract_qa_fields),如果没有提取到问题和回答字段则跳过不解析。再遍历每个科室中的数据,

1.用clean_text来处理单个的问题和答案(strip 去首尾空白 + 压缩连续空白 + 去控制字符,文本规范化)

2.用is_valid_qa来处理问题和答案是否有效(过滤掉太长或太短的问题和答案,或者仅含标点符号的问题,再或者是谢谢,你好之类无意义的问题)

如上两个步骤将所有文档中的所有的数据全部遍历了,并将数据放到all_data 里,再调用deduplicate_by_question函数用MD5哈希对全部数据进行快速去重。

Step 3: 格式转换与保存

python 复制代码
os.makedirs(output_dir, exist_ok=True)
    
# 保存Alpaca格式(完整)
alpaca_data = to_alpaca_format(cleaned_data)
save_dataset(alpaca_data, os.path.join(output_dir, "medical_alpaca_full.jsonl"))
    
# 保存Chat格式(完整)
chat_data = to_chat_format(cleaned_data)
save_dataset(chat_data, os.path.join(output_dir, "medical_chat_full.jsonl"))

将清洗后的全量数据cleaned_data处理成Alpaca格式 或者Chat格式进行保存。

Alpaca格式在 LLM微调-工作准备 中有介绍,包含instruction,intput,output字段,我们现在的数据是个QA对,即有input和output,只需要设定instruction指令,相当于系统提示词。代码中默认给了系统提示词"你是一个专业的医疗助手。请根据患者的问题提供专业、准确的回答。"。

Chat模式要追溯到 大模型API使用 舆情分析案例中,介绍了messages 的结构:每个元素都包含role 和content 两个键,role的角色包含**'system' (系统指示)** 、'user' (用户提问) 或**'assistant' (AI回复)**。

save_dataset函数是将转换好格式的数据存储到指定的文件中。分为jsonl和json两种文件格式的写入。

Step 4: 均衡采样(用于快速实验)

python 复制代码
sampled_data = sample_balanced_data(cleaned_data, samples_per_dept=200)
sampled_alpaca = to_alpaca_format(sampled_data)
save_dataset(sampled_alpaca, os.path.join(output_dir, "medical_alpaca_sampled.jsonl"))
    
# 留出验证集(5%)
val_size = max(10, len(sampled_data) // 20)
val_data = to_alpaca_format(sampled_data[:val_size])
train_data = to_alpaca_format(sampled_data[val_size:])
save_dataset(val_data, os.path.join(output_dir, "medical_alpaca_val.jsonl"))
save_dataset(train_data, os.path.join(output_dir, "medical_alpaca_train.jsonl"))

sample_balanced_data 函数遍历清洗后的全量数据cleaned_data ,按照科室放到dept_data里,再对每个科室的数据进行抽样。对采样完成的数据转换成Alpaca格式 ,并保存。sampled_data为采样数据集

对采样的数据再抽取一部分,一部分为训练集(train_data) ,一部分为验证集(val_data),并保存。

2)SFT模型微调

python 复制代码
from transformers import AutoModelForCausalLM, AutoTokenizer
    from peft import LoraConfig, get_peft_model
    from trl import SFTTrainer

    # 1. 加载模型
    print("CPU模式: float32加载 (约需3.2GB内存)")
    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
    model = AutoModelForCausalLM.from_pretrained(
        MODEL_NAME,
        dtype=torch.float32,
        trust_remote_code=True,
    )
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token

    # 2. 配置LoRA
    lora_config = LoraConfig(
        r=8,   #lora 秩,建议8,16,32,64,128
        lora_alpha=16,
        target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
        lora_dropout=0.05,
        task_type="CAUSAL_LM",
    )
    model = get_peft_model(model, lora_config) #lora的小模型
    # PEFT 注入的 LoRA 参数可能仍为 bf16;
    # Intel CPU 上 DNNL 后端不支持 bf16 反向传播,必须强制转 fp32
    model = model.to(torch.float32)
    model.print_trainable_parameters()

    # 3. 准备数据并格式化为带EOS的文本
    data_list = prepare_training_data()
    eos_token = tokenizer.eos_token or ""

    def formatting_func(examples):
        texts = []
        for inp, out in zip(examples["input"], examples["output"]):
            text = MEDICAL_PROMPT.format(inp, out) + eos_token
            texts.append(text)
        return {"text": texts}

    dataset = Dataset.from_list(data_list)
    dataset = dataset.map(formatting_func, batched=True)
    print(f"训练数据: {len(dataset)} 条")

    # 4. SFT训练
    train_kwargs = dict(
        per_device_train_batch_size=2, #每个设备的训练批次大小
        gradient_accumulation_steps=2, #梯度累加步数
        max_steps=10,                  #最大训练步数
        learning_rate=2e-4,            #学习率
        fp16=False,                    #是否使用 fp16 训练
        bf16=False,                    #是否使用 bf16 训练
        optim="adamw_torch",            #优化器
        use_cpu=True,
        output_dir="outputs_medical",
        report_to="none",
        logging_steps=1,              #日志记录步数
    )
    # trl 0.24+ 新版 API:dataset_* 与 packing 都搬进 SFTConfig;
    # SFTTrainer 的 tokenizer 参数改名为 processing_class;
    # max_seq_length 改名为 max_length
    from trl import SFTConfig
    sft_config = SFTConfig(
        **train_kwargs,
        max_length=512,
        dataset_text_field="text",
        dataset_num_proc=1,
        packing=False,
    )
    trainer = SFTTrainer(
        model=model,
        processing_class=tokenizer,
        train_dataset=dataset,
        args=sft_config,
    )

    trainer.train()

Step 1:预训练模型加载,得到model 和 tokenizer

Step 2:配置lora,通过get_peft_model创建Lora的小模型,最开始这个小模型为空

Step 3:加载数据,读取数据集

Step 4:SFTTrainer设定好参数,开始训练

3)效果的验证

python 复制代码
# 5. 测试推理
    test_questions = [
        "我最近总是感觉头晕,应该怎么办?",
        "感冒发烧应该吃什么药?",
        "高血压患者需要注意什么?",
    ]
    model.eval()
    for q in test_questions:
        prompt = MEDICAL_PROMPT.format(q, "")
        inputs = tokenizer(prompt, return_tensors="pt")
        with torch.no_grad():
            outputs = model.generate(
                **inputs,
                max_new_tokens=128,
                temperature=0.7,
                top_p=0.9,
                do_sample=True,
            )
        response = tokenizer.decode(outputs[0], skip_special_tokens=True)
        if "### 回答:" in response:
            answer = response.split("### 回答:")[-1].strip()
        elif "### 回答:" in response:
            answer = response.split("### 回答:")[-1].strip()
        else:
            answer = response
        print(f"Q: {q}")
        print(f"A: {answer[:200]}")
        print()

    # 6. 保存LoRA模型
    save_dir = "lora_model_medical"
    os.makedirs(save_dir, exist_ok=True)
    model.save_pretrained(save_dir)
    tokenizer.save_pretrained(save_dir)
    print(f"LoRA模型已保存: {save_dir}")

先把输入转化成token,生成答案后再把Token转为文字回答。这里保存了微调后的模型lora_model_medical,下次加载模型时就可以加载这个模型 model = AutoModelForCausalLM.from_pretrained("lora_model_medical",.......

结果输出:

我这里只用了1000多条数据,跑了10轮,感觉训练结果不好。可以加大数据量或者加大训练轮数来看看。

GPU版本补充

上面用CPU版本的代码说明了模型微调的流程,这里再补充一下GPU版本的流程:

python 复制代码
def load_model():
    """使用Unsloth加载模型,4bit量化"""
    from unsloth import FastLanguageModel
    model, tokenizer = FastLanguageModel.from_pretrained(
        model_name=MODEL_NAME,
        max_seq_length=MAX_SEQ_LENGTH,
        dtype=None,
        load_in_4bit=True, #4bit量化,0.8B模型约需0.5GB显存
    )
    return model, tokenizer


def setup_lora(model):
    """配置LoRA适配器,使用unsloth梯度检查点"""
    from unsloth import FastLanguageModel
    model = FastLanguageModel.get_peft_model(
        model,
        r=LORA_R,
        target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                        "gate_proj", "up_proj", "down_proj"],
        lora_alpha=16,
        lora_dropout=0,
        bias="none",
        use_gradient_checkpointing="unsloth",
        random_state=3407,
        use_rslora=False,
        loftq_config=None,
    )
    return model

//step 3:准备数据

def train_sft(model, tokenizer, dataset):
    """使用SFTTrainer训练,SFTConfig优先,回退到TrainingArguments"""
    from unsloth import is_bfloat16_supported
    bf16_val = is_bfloat16_supported()
    fp16_val = not bf16_val

    train_kwargs = dict(
        per_device_train_batch_size=BATCH_SIZE,
        gradient_accumulation_steps=GRAD_ACCUM,
        warmup_steps=5,
        max_steps=MAX_STEPS,
        learning_rate=LR,
        fp16=fp16_val,
        bf16=bf16_val,
        logging_steps=1,
        optim="adamw_8bit",
        weight_decay=0.01,
        lr_scheduler_type="linear",
        seed=3407,
        output_dir=OUTPUT_DIR,
        report_to="none",
    )

    from trl import SFTTrainer
    try:
        from trl import SFTConfig
        sft_config = SFTConfig(
            **train_kwargs,
            max_seq_length=MAX_SEQ_LENGTH,
            dataset_text_field="text",
            dataset_num_proc=2,
            packing=False,
        )
        trainer = SFTTrainer(
            model=model,
            processing_class=tokenizer,
            train_dataset=dataset,
            args=sft_config,
        )
    except ImportError:
        from transformers import TrainingArguments
        training_args = TrainingArguments(**train_kwargs)
        trainer = SFTTrainer(
            model=model,
            tokenizer=tokenizer,
            train_dataset=dataset,
            dataset_text_field="text",
            max_seq_length=MAX_SEQ_LENGTH,
            dataset_num_proc=2,
            packing=False,
            args=training_args,
        )


    trainer.train()

#==========================================
#数据验证
test_questions = [
        "我最近总是感觉头晕,应该怎么办?",
        "感冒发烧应该吃什么药?",
        "高血压患者需要注意什么?",
    ]
    for q in test_questions:
        ans = generate_response(model, tokenizer, q)
        print(f"  Q: {q}")
        print(f"  A: {ans[:300]}...")
        print()

大家看下流程是一样的:

**Step 1(load_model):**预训练模型加载,得到model 和 tokenizer。区别是CPU是用Transformers加载的,而GPU是用UnSloth加载的

**Step 2(setup_lora):**配置Lora,通过get_peft_model创建Lora的小模型

**Step 3:**准备数据,和上面CPU版本代码一样

Step 4(train_sft):与上面CPU版本代码传参基本一致

结果输出:

这次跑了100轮,效果比上面CPU版本跑10轮的效果好多了。使用一下我们训练出来的模型:

python 复制代码
MEDICAL_PROMPT = """你是一个专业的医疗助手。请根据患者的问题提供专业、准确的回答。

### 问题:
{}

### 回答:
{}"""


def load_model():
    """使用Unsloth加载模型,4bit量化"""
    from unsloth import FastLanguageModel
    model, tokenizer = FastLanguageModel.from_pretrained(
        model_name="lora_model_medical_GPU",
        max_seq_length=2048,
        dtype=None,
        load_in_4bit=True,
    )
    return model, tokenizer

def generate_response(model, tokenizer, question, max_new_tokens=256):
    """推理前调用for_inference,张量置于cuda"""
    # 禁用torch dynamo编译:unsloth为Qwen3.5生成的编译缓存里,
    # _alloc_from_pool算子在torch 2.11 inductor下会抛
    # "output must not be the same as input"RuntimeError。
    # 仅在推理路径禁用,不影响训练时的unsloth加速编译。
    import torch._dynamo
    torch._dynamo.config.disable = True

    from unsloth import FastLanguageModel
    FastLanguageModel.for_inference(model)

    prompt = MEDICAL_PROMPT.format(question, "")
    # Qwen3.5是多模态模型,tokenizer实为Qwen3VLProcessor,
    # 第一个位置参数默认是images;必须用text=关键字显式指定文本输入,
    # 否则Processor会把字符串当图像路径走PIL解码而抛ValueError
    inputs = tokenizer(text=[prompt], return_tensors="pt").to("cuda")

    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            temperature=0.7,
            top_p=0.9,
            use_cache=True,
            do_sample=True,
        )

    response = tokenizer.decode(outputs[0], skip_special_tokens=True)
    if "### 回答:" in response:
        answer = response.split("### 回答:")[-1].strip()
    elif "### 回答:" in response:
        answer = response.split("### 回答:")[-1].strip()
    else:
        answer = response
    return answer

if __name__ == "__main__":
    model, tokenizer = load_model()

    q = "孩子肥胖懒惰,应该怎么办?"
    ans = generate_response(model, tokenizer, q)
    print(f"  Q: {q}")
    print(f"  A: {ans[:300]}...")

结果输出:

GPU和CPU版代码的对比:

组件 GPU版 CPU版
基座模型 Qwen3.5-0.8B (4bit量化) Qwen3.5-0.8B (float32)
微调框架 Unsloth + trl transformers + peft + trl
LoRA rank r=16 r=8
训练步数 100步 10步
序列长度 2048 512
batch配置 2 x 4 (梯度累积) 1 x 2
显存/内存 约2-4GB显存 约3.2GB内存

**知识补充:**TRL(Transformer Reinforcement Learning)是Hugging Face 推出的 库,用于大语言模型的训练和微调。主要功能包括:

• SFTTrainer:监督微调(SFT),用指令数据训练模型

• DPOTrainer/ RLHFTrainer:偏好对齐(DPO、RLHF 等)

• SFTConfig:配置SFT 训练参数(学习率、batch、序列长度等)

TRL 基于transformers 和peft,支持LoRA、4bit 量化等,是Hugging Face 生态里做LLM 微调的核心工具。

相关推荐
basketball6161 小时前
Python FastAPI 介绍以及常用方法
python·fastapi·vllm·ai infra
论文复现现场1 小时前
企业知识库 RAG 用什么模型便宜?GLM-5.3-Flash 长文本 API 实测
python·rag·企业知识库·大模型api·glm-5.3-flash
梦在远山后2 小时前
从手写 Loop 到可恢复 Runtime:用 LangGraph、PostgreSQL Checkpoint 与 AG-UI 跑通中断恢复
python·langchain·agent
Yolanda_20222 小时前
8.tensorboard的使用
python
用户0332126663672 小时前
使用 Python 添加、隐藏或删除 PowerPoint 幻灯片
python
言乐63 小时前
Python关键词抓取目标网站
python·django·virtualenv·pygame·tornado
用户7783366132113 小时前
搜索页的 URL 状态管理:可分享、可回退、可刷新
python·api
Liaiyang663 小时前
空圈容错视角下的无人机全链路审计:从理论框架到耦合式检验
人工智能·pytorch·python·深度学习·系统架构·自动驾驶·无人机
liuchangng3 小时前
Jev 模型研究:从生成式大模型到决策式模型——System One、RLCD 校准与采用边界
java·javascript·人工智能·python·深度学习