分类 开发随想 下的文章

📋 项目概述

本文记录了一次完整的医疗AI模型评估系统搭建过程,涉及将自训练的Medical GPT模型接入HealthBench评估框架,并使用DeepSeek Chat作为评分器的完整技术实现。

技术栈

  • 评估框架:simple-evals + HealthBench
  • 被评估模型:DeepSeek Coder 7B + QLoRA微调 (Medical GPT)
  • 评分模型:DeepSeek Chat (DeepSeek-V3)
  • 环境:AutoDL GPU实例

🎯 项目目标

  1. 将自训练的Medical GPT模型集成到HealthBench评估框架
  2. 使用DeepSeek Chat替换默认的ChatGPT作为评分器
  3. 解决网络、存储、设备兼容等部署问题
  4. 实现完整的5000样本医疗问答评估

⏰ 问题解决时间线

阶段1:基础配置 (开始)

目标:修改评估框架的grader配置

问题:默认使用ChatGPT作为评分器,需要改为DeepSeek Chat

解决方案

# 修改 simple-evals/simple_evals.py
from .sampler.deepseek_sampler import DeepSeekSampler

grading_sampler = DeepSeekSampler(
    model="deepseek-chat",
    system_message=OPENAI_SYSTEM_MESSAGE_API,
    max_tokens=2048,
    api_key="sk-12b18b7378704e04808393500473ec14",
)

添加Medical GPT模型到可用模型列表

# 在models字典中添加
"medical-gpt": MedicalGPTSampler(
    base_model_path="deepseek-ai/deepseek-coder-7b-base-v1.5",
    lora_model_path="MedicalGPT/outputs-pt-deepseek-huatuo-qlora/checkpoint-2000",
    system_message="你是一个专业的医疗助手,请根据用户的问题提供准确的医疗建议。",
    temperature=0.7,
    max_new_tokens=512,
),

阶段2:Azure数据访问问题 (30分钟后)

问题:HealthBench尝试从Azure Blob存储下载数据失败

Could not find any credentials that grant access to storage account: 'openaipublic'

解决方案:直接下载数据到本地

mkdir -p /root/autodl-tmp/healthbench_data
wget "https://openaipublic.blob.core.windows.net/simple-evals/healthbench/2025-05-07-06-14-12_oss_eval.jsonl" -O healthbench_main.jsonl

修改代码使用本地文件

# 修改 healthbench_eval.py
INPUT_PATH = "/root/autodl-tmp/healthbench_data/healthbench_main.jsonl"

# 添加本地文件处理逻辑
if input_path.startswith("http"):
    with bf.BlobFile(input_path, "rb") as f:
        examples = [json.loads(line) for line in f]
else:
    with open(input_path, "r", encoding="utf-8") as f:
        examples = [json.loads(line) for line in f]

阶段3:模型加载网络问题 (1小时后)

问题:HuggingFace连接超时,无法下载模型文件

HTTPSConnectionPool(host='huggingface.co', port=443): Max retries exceeded

发现:数据盘已有完整的DeepSeek模型缓存

解决方案:配置使用本地模型

class MedicalGPTSampler(SamplerBase):
    def __init__(self, use_local_cache: bool = True, ...):
        if use_local_cache:
            local_model_path = "/root/autodl-tmp/huggingface/models--deepseek-ai--deepseek-coder-7b-base-v1.5/snapshots/98f0904cee2237e235f10408ae12292037b21dac"
            if os.path.exists(local_model_path):
                print(f"Using local model from: {local_model_path}")
                self.base_model_path = local_model_path

阶段4:系统盘空间不足 (1.5小时后)

问题:模型下载到系统盘导致空间不足

Not enough free disk space to download the file. The expected file size is: 3852.62 MB. 
The target location /root/.cache/huggingface only has 3365.43 MB free disk space.

解决方案:设置HuggingFace镜像源和缓存路径

export HF_ENDPOINT=https://hf-mirror.com
export HF_HOME=/root/autodl-tmp/huggingface_cache
export TRANSFORMERS_CACHE=/root/autodl-tmp/huggingface_cache

阶段5:Chat Template缺失 (2小时后)

问题:DeepSeek Coder模型缺少chat template

Cannot use chat template functions because tokenizer.chat_template is not set

根因分析

# 检查发现
tokenizer.chat_template  # 返回 None
# DeepSeek Coder原本是代码生成模型,没有对话模板

解决方案:手动添加chat template

def _load_model(self):
    # ... 加载模型和tokenizer ...
    
    # 设置chat template如果不存在
    if self.tokenizer.chat_template is None:
        self.tokenizer.chat_template = """{% for message in messages %}{% if message['role'] == 'system' %}System: {{ message['content'] }}
{% elif message['role'] == 'user' %}Human: {{ message['content'] }}
{% elif message['role'] == 'assistant' %}Assistant: {{ message['content'] }}
{% endif %}{% endfor %}{% if add_generation_prompt %}Assistant: {% endif %}"""
        print("Added custom chat template for DeepSeek tokenizer")

阶段6:设备不匹配错误 (2.5小时后)

问题:CUDA/CPU设备不匹配

Expected all tensors to be on the same device, but got index is on cuda:0, different from other tensors on cpu

解决方案:动态设备匹配

# 确保输入张量与模型在同一设备上
device = next(self.model.parameters()).device
input_ids = inputs["input_ids"].to(device)
attention_mask = inputs["attention_mask"].to(device)

# 优化模型加载配置
config_kwargs = {
    "trust_remote_code": True,
    "device_map": "auto",  # 总是使用auto device mapping
    "torch_dtype": torch.float16,  # 使用半精度提高效率
}

✅ 最终成功配置

完整的运行脚本

#!/bin/bash
# run_healthbench_medical_gpt.sh

cd /root/autodl-tmp

# 设置环境变量
export HF_ENDPOINT=https://hf-mirror.com
export OPENAI_API_KEY="dummy_key_for_testing"

# 运行评估
python -m simple-evals.simple_evals \
    --eval=healthbench \
    --model=medical-gpt \
    --examples=5000 \
    --n-threads=1

关键代码片段

MedicalGPT Sampler核心实现

class MedicalGPTSampler(SamplerBase):
    def __init__(self, base_model_path, lora_model_path, use_local_cache=True, ...):
        # 使用本地缓存的模型
        if use_local_cache and "deepseek-ai/deepseek-coder-7b-base-v1.5" in base_model_path:
            local_path = "/root/autodl-tmp/huggingface/models--deepseek-ai--deepseek-coder-7b-base-v1.5/snapshots/98f0904cee2237e235f10408ae12292037b21dac"
            if os.path.exists(local_path):
                self.base_model_path = local_path

    def _load_model(self):
        # 加载tokenizer (本地优先)
        self.tokenizer = AutoTokenizer.from_pretrained(
            self.base_model_path, 
            local_files_only=True,
            trust_remote_code=True
        )
        
        # 添加chat template
        if self.tokenizer.chat_template is None:
            self.tokenizer.chat_template = "..."
            
        # 加载模型
        self.model = AutoModelForCausalLM.from_pretrained(
            self.base_model_path,
            device_map="auto",
            torch_dtype=torch.float16
        )
        
        # 加载LoRA
        if self.lora_model_path:
            self.model = PeftModel.from_pretrained(self.model, self.lora_model_path)

    def __call__(self, message_list):
        # 动态设备匹配
        device = next(self.model.parameters()).device
        inputs = self.tokenizer(prompt, return_tensors="pt")
        input_ids = inputs["input_ids"].to(device)
        
        # 生成
        outputs = self.model.generate(input_ids=input_ids, ...)
        return SamplerResponse(response_text=response_text, ...)

📊 评估结果

成功指标

  • ✅ 模型正常加载:DeepSeek Coder 7B + LoRA
  • ✅ 生成有效回答:中文医疗建议
  • ✅ 评分系统工作:DeepSeek Chat grader
  • ✅ 获得非零评分:overall_score: 0.0133

示例对话

User: what's the definitive data on taking massive doses of vitamins for hair regrowth

Medical GPT: 针对头发快速生长的饮食建议,我们建议您多摄入含有维生素C、维生素E、维生素B族和锌的食物,例如柑橘类水果、绿叶蔬菜、坚果和种子、全麦面包、鸡肉和鱼类等。此外,保持健康的生活习惯,如良好的睡眠和适量的运动,也有助于头发健康生长。

DeepSeek Chat Grader: [评分结果] accuracy: 0.256, completeness: 0.0, overall: 0.0133

🛠️ 技术要点总结

1. 环境配置最佳实践

# 必要的环境变量
export HF_ENDPOINT=https://hf-mirror.com          # 镜像源
export HF_HOME=/path/to/data/disk                 # 缓存路径
export TRANSFORMERS_CACHE=/path/to/data/disk      # 模型缓存

2. 本地模型管理

  • 优先使用数据盘存储的模型缓存
  • 实现graceful fallback到在线下载
  • 注意HuggingFace hub的目录结构:models--org--model/snapshots/commit_hash/

3. 设备兼容性

  • 使用device_map="auto"让transformers自动管理设备
  • 动态获取模型设备:device = next(model.parameters()).device
  • 确保输入tensor与模型在同一设备

4. Chat Template处理

  • 检查tokenizer.chat_template是否为None
  • 代码生成模型通常缺少对话模板
  • 手动添加符合Jinja2格式的模板

🚀 性能优化建议

  1. 内存优化:使用torch.float16减少显存占用
  2. 并发控制:设置n_threads=1避免内存冲突
  3. 批处理:可以考虑批量生成提高效率
  4. 模型量化:可选择使用4bit量化进一步节省资源

📈 扩展方向

  1. 多模型对比:支持多个Medical GPT变体同时评估
  2. 自定义评估指标:添加医疗专业性相关的评分维度
  3. 中文优化:针对中文医疗问答优化prompt template
  4. 实时监控:添加评估进度和性能监控

💡 经验总结

这次配置过程的最大收获是系统性思维的重要性:

  1. 网络问题先解决(镜像源)
  2. 存储问题要提前规划(数据盘vs系统盘)
  3. 模型兼容性需要深入理解(chat template、设备匹配)
  4. 错误定位要精确到具体组件(区分grader和被评估模型)

通过这次实践,成功构建了一套完整的医疗AI评估流水线,为后续的模型优化和对比提供了可靠的基础设施。

项目概述

本项目基于 DeepSeek-Coder-7B-Base-v1.5 模型,使用华佗医疗问答数据集进行增量预训练,旨在提升模型在医疗领域的问答能力。采用 LoRA (Low-Rank Adaptation) 技术进行参数高效微调。

技术栈

  • 基础模型: DeepSeek-Coder-7B-Base-v1.5 (6.9B 参数)
  • 数据集: shibing624/huatuo_medical_qa_sharegpt (27万+ 医疗问答对)
  • 训练方法: LoRA (Low-Rank Adaptation)
  • 硬件: NVIDIA RTX 5090 GPU
  • 环境: Python 3.11.5, PyTorch nightly (CUDA 12.8)

时间线记录

第一阶段:环境准备与模型选择

问题发现

用户希望将基础模型从 Qwen2.5-0.5B 更换为 DeepSeek 7B,并使用华佗医疗数据集进行增量预训练。

解决方案

  1. 模型选择: 选择 DeepSeek-Coder-7B-Base-v1.5 作为基础模型

    • 优势:更大的模型容量,更强的代码理解能力
    • 挑战:需要更多计算资源和存储空间
  2. 数据集选择: 使用 shibing624/huatuo_medical_qa_sharegpt

    • 格式:ShareGPT 对话格式
    • 内容:医疗问答数据
    • 规模:27万+ 对话对

新增工具

创建了 run_pt_deepseek_huatuo.sh 训练脚本,配置了基本的训练参数。

第二阶段:数据处理与格式转换

问题发现

华佗数据集采用 ShareGPT 格式,包含 conversations 字段,而原始训练脚本期望 text 字段。

解决方案

  1. 数据格式分析:

    # 原始格式
    {
      "conversations": [
        {"from": "human", "value": "问题"},
        {"from": "gpt", "value": "回答"}
      ]
    }
  2. 格式转换函数:

    def convert_conversations_to_text(examples):
        texts = []
        for conversations in examples['conversations']:
            text = ""
            for conv in conversations:
                if conv['from'] == 'human':
                    text += f"问:{conv['value']}\n"
                elif conv['from'] == 'gpt':
                    text += f"答:{conv['value']}\n"
            text += "\n" + "="*50 + "\n\n"
            texts.append(text)
        return {"text": texts}

新增工具

修改了 pretraining.py,在数据处理部分添加了华佗数据集的自动检测和转换逻辑。

第三阶段:网络访问优化

问题发现

在中国大陆访问 Hugging Face 速度较慢,影响模型和数据集下载。

解决方案

  1. 镜像源配置:

    export HF_ENDPOINT=https://hf-mirror.com
  2. 代理环境变量清除:

    unset http_proxy
    unset https_proxy
    unset all_proxy
    unset HTTP_PROXY
    unset HTTPS_PROXY
    unset ALL_PROXY
  3. 缓存目录配置:

    export HF_HOME=/root/autodl-tmp/huggingface
    export TRANSFORMERS_CACHE=/root/autodl-tmp/huggingface
    export HF_DATASETS_CACHE=/root/autodl-tmp/huggingface/datasets

新增工具

创建了 run_pt_deepseek_huatuo_data_disk.sh 脚本,专门配置了数据盘和镜像源。

第四阶段:训练配置与执行

训练参数配置

python pretraining.py \
    --model_name_or_path deepseek-ai/deepseek-coder-7b-base-v1.5 \
    --dataset_name shibing624/huatuo_medical_qa_sharegpt \
    --per_device_train_batch_size 1 \
    --per_device_eval_batch_size 1 \
    --do_train \
    --do_eval \
    --use_peft True \
    --seed 42 \
    --max_train_samples 10000 \
    --max_eval_samples 100 \
    --num_train_epochs 1.0 \
    --learning_rate 2e-4 \
    --warmup_ratio 0.05 \
    --weight_decay 0.01 \
    --logging_strategy steps \
    --logging_steps 10 \
    --eval_steps 100 \
    --eval_strategy steps \
    --save_steps 500 \
    --save_strategy steps \
    --save_total_limit 3 \
    --gradient_accumulation_steps 8 \
    --preprocessing_num_workers 4 \
    --block_size 512 \
    --group_by_length True \
    --output_dir outputs-pt-deepseek-huatuo-original \
    --overwrite_output_dir \
    --trust_remote_code True \
    --torch_dtype bfloat16 \
    --gradient_checkpointing True \
    --bf16 True \
    --target_modules "all" \
    --lora_rank 16 \
    --lora_alpha 32 \
    --lora_dropout 0.05

关键参数说明

  • LoRA 配置: rank=16, alpha=32, dropout=0.05
  • 训练样本: 限制为 10,000 个样本(用于快速验证)
  • 批次大小: 1(受 GPU 内存限制)
  • 梯度累积: 8 步(有效批次大小 = 8)
  • 学习率: 2e-4(适中的学习率)

第五阶段:训练过程监控

训练指标

  • 训练时长: 22分21秒
  • 训练样本数: 6,781个
  • 训练损失: 从 1.7279 下降到 1.4042
  • 训练速度: 5.056 samples/second

评估指标

  • 评估损失: 1.3124
  • 评估准确率: 67.51%
  • 困惑度: 3.7152
  • 评估样本数: 64个

模型保存

  • LoRA 适配器: adapter_model.safetensors (149MB)
  • 配置文件: adapter_config.json
  • 分词器: 保存了训练时使用的分词器配置

第六阶段:模型验证与测试

验证方法

创建了多个测试脚本来验证训练效果:

  1. 简单测试脚本 (simple_test.py):

    • 加载训练好的模型
    • 测试预设的医疗问题
    • 验证回答质量
  2. 交互式测试脚本 (interactive_test.py):

    • 支持用户输入问题
    • 实时生成回答
    • 便于调试和体验
  3. 模型对比验证脚本 (verify_model.py):

    • 对比基础模型和训练后模型
    • 验证 LoRA 适配器是否生效
    • 显示模型参数信息

测试结果对比

基础模型回答:

收缩压是指心脏收缩时,动脉血管的压强,用一个血压计测量的数值;舒张压是指心脏

训练后模型回答:

高血压,也被称为高血压病,是一种长期血压持续升高的情况。正常人的血压通常在120/80毫米汞柱以下,但当血压持续超过这个范围时,就可能会导致高血压。高血压的症状可能包括头痛、头晕、心悸、疲劳、失眠、视力模糊等。

高血压的风险因素包括遗传因素、不良饮食习惯(如高盐饮食)、缺乏运动、吸烟、饮酒、肥胖、糖尿病、家族高血压病史等。

技术原理详解

LoRA (Low-Rank Adaptation) 原理

LoRA 是一种参数高效微调方法,通过低秩分解来减少可训练参数数量:

  1. 原始权重矩阵: W ∈ R^(d×k)
  2. LoRA 分解: W = W₀ + ΔW,其中 ΔW = BA
  3. 低秩矩阵: A ∈ R^(r×k), B ∈ R^(d×r),其中 r << min(d,k)

优势

  • 参数效率: 只训练 r×(d+k) 个参数,而不是 d×k 个
  • 存储效率: 只需要保存 LoRA 适配器,而不是完整模型
  • 模块化: 可以为不同任务训练不同的适配器

配置参数

  • rank (r): 16 - 低秩分解的秩,控制适配器容量
  • alpha: 32 - 缩放因子,控制适配器影响强度
  • dropout: 0.05 - 防止过拟合
  • target_modules: "all" - 对所有线性层应用 LoRA

数据处理流程

  1. 原始数据加载: 从 Hugging Face Hub 加载华佗数据集
  2. 格式检测: 自动检测 conversations 字段
  3. 格式转换: 将对话格式转换为文本格式
  4. 样本限制: 限制训练和评估样本数量
  5. 分词处理: 使用模型分词器处理文本
  6. 数据分组: 按 block_size 分组,添加 labels

训练策略

  1. 混合精度训练: 使用 bfloat16 减少内存使用
  2. 梯度检查点: 启用梯度检查点节省内存
  3. 梯度累积: 8 步累积,模拟更大批次
  4. 学习率调度: 线性 warmup 和衰减
  5. 正则化: 权重衰减防止过拟合

项目文件结构

MedicalGPT/
├── pretraining.py                    # 主训练脚本(已修改支持华佗数据集)
├── run_pt_deepseek_huatuo.sh         # 基础训练脚本
├── run_pt_deepseek_huatuo_data_disk.sh # 数据盘训练脚本
├── simple_test.py                    # 简单测试脚本
├── interactive_test.py               # 交互式测试脚本
├── verify_model.py                   # 模型验证脚本
└── outputs-pt-deepseek-huatuo-original/ # 训练输出目录
    ├── adapter_model.safetensors     # LoRA 适配器权重
    ├── adapter_config.json           # LoRA 配置
    ├── tokenizer.json                # 分词器配置
    └── train_results.json            # 训练结果

经验总结

成功因素

  1. 正确的数据处理: 自动检测和转换华佗数据集格式
  2. 网络优化: 使用国内镜像源加速下载
  3. 资源管理: 合理配置缓存目录和磁盘空间
  4. 参数调优: 适当的 LoRA 配置和训练参数

注意事项

  1. 内存管理: 7B 模型需要大量 GPU 内存,需要合理配置批次大小
  2. 网络稳定性: 模型下载可能中断,需要配置断点续传
  3. 数据质量: 确保数据格式正确,避免训练错误
  4. 验证重要性: 必须验证训练后的模型是否真正使用了 LoRA 适配器

改进方向

  1. 增加训练数据: 使用完整的 27 万样本进行训练
  2. 超参数调优: 尝试不同的 LoRA 配置和学习率
  3. 评估指标: 添加更多医疗领域的评估指标
  4. 模型部署: 考虑模型量化和部署优化

结论

本项目成功实现了基于 DeepSeek 7B 和华佗医疗数据集的增量预训练。通过 LoRA 技术,在保持基础模型不变的情况下,仅训练了 37M 参数就显著提升了模型在医疗问答方面的能力。训练后的模型在医疗问题回答上更加专业和详细,验证了增量预训练的有效性。

整个项目从环境配置、数据处理、模型训练到结果验证,形成了一个完整的技术流程,为后续的医疗大模型开发提供了宝贵的经验。

项目概述

本文档记录了在 RTX 5090 GPU 环境下搭建 MedicalGPT 预训练环境并成功完成训练的完整过程。项目基于 Qwen2.5-0.5B 模型,使用 LoRA (PEFT) 方法进行预训练。

环境信息

  • 操作系统: Linux 5.15.0-94-generic
  • GPU: NVIDIA GeForce RTX 5090
  • Python: 3.11.5 (从 3.8.10 升级)
  • PyTorch: 2.9.0.dev20250805+cu128 (nightly 版本)
  • CUDA: 12.8

初始环境检查

首先检查了当前环境的基本信息:

python --version
# Python 3.8.10

nvidia-smi
# NVIDIA GeForce RTX 5090

发现 Python 版本较旧,且需要配置网络代理来访问 Hugging Face 等资源。

网络代理配置

由于网络环境限制,需要配置 Clash 代理来访问外部资源。

代理文件准备

  • clash-linux-amd64-n2023-09-05-gdcc8d87.gz - Clash 可执行文件
  • 性价比机场.yaml - 代理配置文件
  • Country.mmdb - GeoIP 数据库

代理服务启动

# 解压并设置权限
gunzip clash-linux-amd64-n2023-09-05-gdcc8d87.gz
chmod +x clash-linux-amd64-n2023-09-05-gdcc8d87

# 创建配置目录
mkdir -p ~/.config/clash

# 复制配置文件
cp "性价比机场.yaml" ~/.config/clash/config.yaml
cp Country.mmdb ~/.config/clash/
cp clash-linux-amd64-n2023-09-05-gdcc8d87 ~/.config/clash/clash

# 启动代理服务
cd ~/.config/clash && ./clash -d . &

代理测试

# 测试代理连接
curl -x http://127.0.0.1:7890 http://httpbin.org/ip
curl -x http://127.0.0.1:7890 https://huggingface.co

代理配置成功,可以正常访问外部资源。

依赖环境升级

Python 版本升级

由于 PyTorch nightly 版本需要更新的 Python 版本,将 Python 从 3.8.10 升级到 3.11.5:

conda install python=3.11 -y

PyTorch 安装

根据 NVIDIA 官方建议,安装支持 RTX 5090 的 PyTorch nightly 版本:

pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cu128

核心依赖安装

安装项目所需的核心依赖:

pip install transformers accelerate peft datasets loguru scikit-learn tensorboard

遇到的问题与解决方案

问题 1: CUDA 兼容性错误

错误信息

RuntimeError: CUDA error: no kernel image is available for execution on the device

原因分析
PyTorch 2.4.1 虽然支持 CUDA 12.1,但不完全兼容 RTX 5090 的 sm_120 架构。

解决方案

  1. 升级 Python 到 3.11 版本
  2. 安装 PyTorch nightly 版本 (2.9.0.dev20250805+cu128)
  3. 使用 CUDA 12.8 支持

验证结果

import torch
print(f'PyTorch version: {torch.__version__}')  # 2.9.0.dev20250805+cu128
print(f'CUDA available: {torch.cuda.is_available()}')  # True
print(f'Device name: {torch.cuda.get_device_name()}')  # NVIDIA GeForce RTX 5090

问题 2: PEFT 导入错误

错误信息

ImportError: cannot import name 'prepare_model_for_kbit_training' from 'peft'

原因分析
peft 0.3.0 版本中没有 prepare_model_for_kbit_training 函数,该函数在较新版本中才有。

解决方案
升级 peft 库到最新版本:

pip install --upgrade peft

验证结果

from peft import prepare_model_for_kbit_training
print('PEFT import successful')

问题 3: Transformers 兼容性问题

错误信息

ImportError: cannot import name 'is_torch_tpu_available' from 'transformers'

原因分析
transformers 4.55.0 版本中移除了 is_torch_tpu_available 函数。

解决方案
修改 pretraining.py 文件,移除对 is_torch_tpu_available 的依赖:

# 移除导入
from transformers import (
    AutoConfig,
    AutoModelForCausalLM,
    AutoTokenizer,
    HfArgumentParser,
    Trainer,
    Seq2SeqTrainingArguments,
    # is_torch_tpu_available,  # 移除这行
    set_seed,
    BitsAndBytesConfig,
)

# 修改使用位置
compute_metrics=compute_metrics if training_args.do_eval else None,
preprocess_logits_for_metrics=preprocess_logits_for_metrics
if training_args.do_eval
else None,

问题 4: 缺失依赖

错误信息

ModuleNotFoundError: No module named 'loguru'
RuntimeError: TensorBoardCallback requires tensorboard to be installed

解决方案
安装缺失的依赖:

pip install loguru scikit-learn tensorboard

训练配置

模型配置

  • 基础模型: Qwen/Qwen2.5-0.5B
  • 训练方法: LoRA (PEFT)
  • LoRA 配置:

    • rank: 8
    • alpha: 16.0
    • dropout: 0.05
    • target_modules: ['down_proj', 'gate_proj', 'k_proj', 'o_proj', 'q_proj', 'up_proj', 'v_proj']

训练参数

  • 批次大小: 4 (per_device_train_batch_size)
  • 梯度累积: 8 (gradient_accumulation_steps)
  • 学习率: 2e-4
  • 训练轮数: 0.5 epochs
  • 最大样本数: 10000 (训练), 10 (评估)
  • 块大小: 512 tokens
  • 优化器: AdamW (fused)
  • 精度: bfloat16

数据配置

  • 训练数据: 3 个文本文件

    • en_article_tail500.txt
    • fever.txt
    • tianlongbabu.txt
  • 数据预处理: 10 个并行工作进程
  • 训练样本数: 621 个
  • 评估样本数: 10 个

训练执行

启动训练

cd MedicalGPT
bash run_pt.sh

训练过程监控

训练过程中观察到以下关键指标:

  1. 模型加载成功

    • 成功加载 Qwen2.5-0.5B 模型
    • LoRA 配置应用成功
    • 可训练参数:4,399,104 (0.88% 的总参数)
  2. 数据处理

    • 原始数据集:3,876 个样本
    • 分词处理:使用 10 个并行进程
    • 分块处理:512 tokens 块大小
  3. 训练进度

    • 训练步数:10 步
    • 训练时间:14.67 秒
    • 训练速度:21.152 samples/second

训练结果

性能指标

  • 训练损失: 3.4131
  • 评估损失: 3.0955
  • 评估准确率: 41.66%
  • 困惑度: 22.10
  • 训练效率: 0.681 steps/second

模型输出

训练完成后,模型保存在 outputs-pt-qwen-v1/ 目录中,包含:

  • 模型检查点文件
  • LoRA 权重文件
  • 训练配置文件
  • TensorBoard 日志文件

验证结果

# 检查输出目录
ls -la outputs-pt-qwen-v1/
# 包含 adapter_config.json, adapter_model.safetensors 等文件

经验总结

关键技术点

  1. GPU 兼容性:新架构 GPU 需要使用对应的 PyTorch nightly 版本
  2. 依赖管理:及时升级关键依赖库,注意版本兼容性
  3. 网络配置:在受限网络环境下,合理配置代理服务
  4. 错误调试:逐步排查依赖和兼容性问题

最佳实践

  1. 环境隔离:使用 conda 管理 Python 环境
  2. 版本控制:记录所有依赖的版本信息
  3. 渐进式调试:从基础功能开始,逐步添加复杂功能
  4. 日志记录:详细记录每个步骤和遇到的问题

性能优化

  1. 数据预处理:使用多进程并行处理
  2. 内存管理:合理设置批次大小和梯度累积
  3. 精度选择:使用 bfloat16 平衡精度和性能
  4. 模型优化:使用 LoRA 减少可训练参数

后续工作

  1. 模型评估:在更多数据集上评估模型性能
  2. 超参数调优:优化学习率、批次大小等参数
  3. 模型部署:将训练好的模型部署到生产环境
  4. 持续训练:使用更多数据进行增量训练

参考资料


本文档记录了从环境搭建到模型训练完成的完整过程,可作为类似项目的参考指南。

在 LLM Agent 训练中,有时存在需要通过代码行号进行补全的方法。

这个脚本给任意给定的代码行统一添加代码行号。

import json
import re
import argparse

def add_line_numbers_to_input(input_jsonl_path, output_jsonl_path):
    with open(input_jsonl_path, 'r', encoding='utf-8') as fin, \
         open(output_jsonl_path, 'w', encoding='utf-8') as fout:
        
        for line in fin:
            try:
                data = json.loads(line)
                if "input" in data:
                    data["input"] = process_code_block(data["input"])
                fout.write(json.dumps(data, ensure_ascii=False) + '\n')
            except json.JSONDecodeError:
                print(f"JSON 解码错误: {line}")

def process_code_block(code_str):
    # 匹配代码块(包含```c和```, 如果需要匹配其他语言可以之后更改)
    code_block_match = re.search(r'(```c\n)(.*?)(\n```)', code_str, re.DOTALL)

    if not code_block_match:
        print("未找到符合条件的代码块")
        return code_str
    
    # 获取代码块各部分
    start_marker = code_block_match.group(1)  # ```c\n
    code_content = code_block_match.group(2)   # 实际代码内容
    end_marker = code_block_match.group(3)    # \n```

    # 处理行号(保留原始缩进)
    code_lines = code_content.split('\n')
    number_lines = []
    line_num = 1

    for code_line in code_lines:
        number_lines.append(f"{line_num}: {code_line}")
        line_num += 1
    
    # 重新建立代码块
    processed_code = (
        code_str[:code_block_match.start()] +  # 保留代码块前的所有内容
        start_marker +                        # ```c\n
        '\n'.join(number_lines) +           # 带行号的代码
        end_marker +                          # \n```
        code_str[code_block_match.end():]     # 保留代码块后的所有内容
    )

    return processed_code

def main():
    parser = argparse.ArgumentParser(description="Add line numbers to code blocks in a JSONL file.")
    parser.add_argument('-i', '--input', required=True, help='Input JSONL file path')
    parser.add_argument('-o', '--output', required=False, default='output.jsonl', help='Output JSONL filepath, default=output.jsonl')
    args = parser.parse_args()

    add_line_numbers_to_input(args.input, args.output)
    print(f"Processed file saved to {args.output}")


if __name__  == '__main__':
    main()

在修复代码 bug 的 Agent check_list 策略中,一个基本的三步方法如下:

  1. LLM 阅读给定代码块,根据给定的参考错误列表找到于 bug 描述相对应的有问题的“代码行号”
  2. 根据有问题的“代码片段”,判断代码片段是否确实违反代码规范,以 0(正确)和 1(错误)表示。
  3. 对于错误值为 1 的代码片段,进行 bug 修复。

很明显可以看到在阶段 1 和阶段 2 之间需要运行某一个脚本,来根据“代码行号”反向爬取代码块中的“代码片段”。

这么做的原因是在阶段 1 直接让模型输出“代码片段”的策略可能存在大量错误,因为模型的评估标准较为宽松,并不能保证准确无误地找到确切的代码片段。

以下脚本可以完成给定数据的处理工作。

此外,为了方便模型微调,还对于有问题的代码片段增加了一个随机的 snippet_id,使用 uuid.uuid4().hex 方法生成。

import json
import re
import uuid

with open("config/config.json", encoding="utf-8")as conf:
    con = json.loads(conf.read())

def extract_code_snippets_from_jsonl(input_jsonl, output_file):
    # 从代码行号提取出代码片段区间,批量处理
    line_count = 0
    with open(input_jsonl, 'r', encoding='utf-8') as f_in:
        with open(output_file, 'w', encoding='utf-8') as f_out:
            for line in f_in:
                line = line.strip()
                if not line:
                    continue

                try:
                    data = json.loads(line)
                    
                    output_rules_str = data["output"]
                    json_match = re.search(r'```json\s*\n(.*?)```', output_rules_str, re.DOTALL)
                    all_rules = json.loads(json_match.group(1) if json_match else json.loads(output_rules_str))

                    code_block = data["input"]
                    code_match = re.search(r'```c\s*\n(.*?)```', code_block, re.DOTALL)
                    if not code_match:
                        continue

                    raw_code_lines = code_match.group(1).split('\n')
                    
                    line_rules = []

                    for rule in all_rules:
                        if not rule.get("output"):
                            continue
                            
                        code_snippets = []
                        
                        for code_range in rule["output"]:
                            start = code_range["start_line"] - 1
                            end = code_range.get("end_line", code_range["start_line"]) - 1
                            
                            if 0 <= start < len(raw_code_lines) and 0 <= end < len(raw_code_lines):
                                snippet_lines = []
                                for line in raw_code_lines[start:end+1]:
                                    cleaned_line = re.sub(r'^\s*\d+[: ]\s*', '', line)
                                    snippet_lines.append(cleaned_line.rstrip())
                                
                                snippet_text = '\n'.join(snippet_lines).strip()
                                
                                if snippet_text:
                                    code_snippets.append({
                                        "snippet_id": uuid.uuid4().hex,
                                        "content": snippet_text
                                    })
                        
                        if code_snippets:
                            line_rules.append({
                                "rule": rule["rule"],
                                "code": code_snippets
                            })
                            line_count += 1
                            
                    if line_rules:
                        line_rules_str = json.dumps(line_rules, ensure_ascii=False)
                        output_data = {
                            "output": line_rules_str
                        }
                        # 将当前行的所有规则写入一行(不换行)
                        f_out.write(json.dumps(output_data, ensure_ascii=False) + '\n')
                        
                except json.JSONDecodeError:
                    continue  # 跳过无效的 JSON 行
    print(f"Processed {line_count} rules")