ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

医疗大模型微调数据集实战:从数据清洗到LoRA参数调优

医疗大模型微调数据集实战:从数据清洗到LoRA参数调优 简介llm-medical-data 是一份面向大模型微调训练场景的医疗数据集资源适合个人学习者、数据科学初学者及独立研究人员用于医疗 AI 方向的实验与练手。资源包共 34 个文件以 json、csv 为主辅以 py 脚本、zbak 备份、zip 压缩包及 txt、md 说明文档整体约 224.51MB涵盖妇产科、外科、儿科、肿瘤科、内科、男科等多科室数据并包含对话生成、格式转换等处理脚本便于直接投入微调流程。目前已有 206 人学习下载。数据集覆盖患者基本信息、病史、检查结果、治疗过程与药物反应等维度来源广泛、疾病类型多样有助于提升模型在医疗场景下的泛化能力与诊断建议质量。使用者可通过实践掌握医疗数据的清洗、转换与微调方法同时需注意医疗隐私与合规使用要求。1. 医疗大模型微调数据集为什么通用语料喂不出能用的问诊能力拿一个 7B 基座模型直接喂通用中文语料做 SFT再丢到真实问诊场景里十有八九会翻车。它能把「高血压」三个字接得头头是道但你问「我父亲 65 岁最近晨起血压 160/95需要马上加药吗」它要么给你一段教科书式的科普要么直接编一个不存在的用药方案。问题不在模型参数量而在训练数据的分布——通用语料里根本没有「主诉 现病史 既往史 用药史 追问」这种结构化的临床对话模式。llm-medical-data这类医疗数据集要解决的就是把散落在病历、指南、问答社区里的医疗文本整理成大型模型微调训练能直接吃的格式。它面向的是做医疗垂域大模型的算法工程师、做临床辅助工具的团队以及想把通用模型往问诊、病历摘要、医学知识问答方向调的人。这一篇不讲空泛的「医疗 AI 前景」只讲一件事拿到一份医疗数据集怎么判断它能不能用、怎么清洗成训练格式、微调时参数怎么设、哪些坑会让你白跑几天 GPU。2. 医疗数据集的字段结构与微调格式选型2.1 一份能直接微调的医疗样本长什么样医疗数据集和通用 SFT 数据最大的区别是它的字段承载了临床逻辑。一份典型的医疗问答/对话样本通常包含这些字段字段含义微调时的作用instruction任务指令如「根据以下主诉给出追问建议」决定模型学到的任务边界input患者信息、主诉、检查结果条件输入模型据此推理output期望回答医生回复或诊断建议监督信号department科室标签可用于分科室采样或路由source数据来源如指南、病历、问答质量分层和去重依据license授权说明决定能不能商用很多公开医疗数据集只给question/answer两列这种结构直接拿去微调模型学到的是「问答对匹配」而不是「临床推理」。我一般会先把两列数据重组成 instruction 格式把科室、患者基本信息塞进 input让模型在生成时被迫先读条件再回答。2.2 三种微调格式的取舍Alpaca、ShareGPT 还是纯 completion选格式不是审美问题它直接决定训练脚本怎么改、loss 怎么算。Alpaca 格式最省事字段固定为instruction/input/output几乎所有开源微调框架LLaMA-Factory、Swift都原生支持。缺点是它把多轮对话压成单轮问诊里的追问链条会丢。ShareGPT 格式用conversations列表存多轮from字段区分human/gpt适合问诊对话、多轮追问。代价是数据清洗时要保证轮次配对正确否则训练时 role 会错位。纯 completion 格式就是一段拼接好的文本框架直接算全序列 loss。它最灵活但也最容易把 instruction 部分的 loss 也算进去导致模型学会「复读问题」。我的选择逻辑是单轮知识问答用 Alpaca多轮问诊用 ShareGPT做继续预训练或领域适应才用 completion。下面是把两列医疗问答转成 Alpaca 格式的最小脚本import json def qa_to_alpaca(raw_list, departmentNone): 把 question/answer 两列数据转成 Alpaca 格式 samples [] for item in raw_list: q item.get(question, ).strip() a item.get(answer, ).strip() if not q or not a: continue # 空样本直接丢别留给 tokenizer 添乱 # 把科室信息拼进 input让模型学会看条件 extra f科室{department} if department else samples.append({ instruction: 你是一名临床医生请针对患者问题给出专业、谨慎的回答。, input: f{extra}\n患者问题{q}.strip(), output: a }) return samples with open(raw_medical_qa.json, r, encodingutf-8) as f: raw json.load(f) alpaca_data qa_to_alpaca(raw, department心血管内科) with open(medical_alpaca.json, w, encodingutf-8) as f: json.dump(alpaca_data, f, ensure_asciiFalse, indent2)这段脚本做了三件事过滤空样本、把科室作为条件拼进 input、统一 instruction 模板。ensure_asciiFalse必须加否则中文会变成\uXXXX虽然不影响训练但排查数据时几乎没法读。indent2只是方便人看正式训练前建议去掉以减小文件体积。提示instruction 模板不要每条都换花样。同一个任务用同一个模板模型才能稳定学到「看到这类指令就做这类事」。模板频繁变化是新手最常见的隐性坑。2.3 数据配比医疗数据该占多少比例如果你是在通用模型上做医疗微调而不是从基座从头训那医疗数据不该是 100%。全量医疗数据会让模型丧失通用指令跟随能力出现「问它今天天气它给你讲感冒」的退化。常见做法是医疗数据占 60%80%剩下用通用指令数据兜底。如果目标是纯医疗问诊助手可以拉到 90%但要保留一小部分通用对话防止灾难性遗忘。配比没有银弹我一般会先跑 70/30看验证集上的通用能力有没有掉再微调比例。3. 从原始医疗文本到可训练样本的清洗流水线3.1 去重、脱敏、去模板三步不能省原始医疗数据最脏的三个地方重复、隐私、模板化。重复不只是完全相同的句子。问诊数据里大量存在「同义改写」的重复比如「血压高怎么办」和「血压偏高如何处理」。这种用简单的字符串去重抓不出来得用 MinHash 或 SimHash 做近似去重。我一般用datasketch库阈值设在 0.8 左右太低会误杀正常的不同问法。脱敏是硬要求。姓名、身份证、手机号、住院号、具体日期都要处理。正则能覆盖大部分import re def desensitize(text): 医疗文本脱敏覆盖常见隐私字段 patterns { phone: r1[3-9]\d{9}, id_card: r\d{17}[\dXx], name: r(?:患者|病人|姓名)[:]?\s*[\u4e00-\u9fa5]{2,4}, date: r\d{4}[-/年]\d{1,2}[-/月]\d{1,2}日?, } for key, pat in patterns.items(): text re.sub(pat, f[{key}], text) return textname那条正则故意写得保守只匹配「患者张三」这种带前缀的避免把正常医学名词误伤。日期替换成[date]而不是删掉是因为时间信息在病程描述里有意义模型需要知道「这里有个时间点」。模板化是医疗数据特有的坑。很多问答社区的数据是「感谢医生 追问 感谢」的固定套路模型学完只会说客套话。清洗时要统计 n-gram 频率把高频模板句删掉或替换。3.2 用 tokenizer 做长度分布检查清洗完别急着开训先看 token 长度分布。医疗文本经常出现超长病历一条样本 4000 token直接喂进去要么被截断丢信息要么爆显存。from transformers import AutoTokenizer import numpy as np tokenizer AutoTokenizer.from_pretrained(your-base-model-path) def check_length(json_path, max_len2048): import json with open(json_path, r, encodingutf-8) as f: data json.load(f) lengths [] for item in data: text item[instruction] item[input] item[output] ids tokenizer(text, truncationFalse)[input_ids] lengths.append(len(ids)) lengths np.array(lengths) print(f样本数: {len(lengths)}) print(f中位数: {np.median(lengths):.0f}) print(fP90: {np.percentile(lengths, 90):.0f}) print(fP99: {np.percentile(lengths, 99):.0f}) print(f超过 {max_len} 的比例: {(lengths max_len).mean():.2%}) return lengths check_length(medical_alpaca.json, max_len2048)看 P99 而不是最大值。最大值往往是脏数据P99 才代表真实分布。如果 P99 超过你设定的 max_len要么调大 max_len吃显存要么对长样本做摘要压缩。我一般把 max_len 设在 P95 附近超长样本单独走一个长文本分支处理而不是一刀切截断——医疗文本截断可能把关键的「既往史」切掉模型给出的建议就完全跑偏。3.3 划分训练集与验证集时的分层采样医疗数据不能随机划分。如果按 8:2 随机切很可能验证集里全是内科、训练集里全是外科验证指标虚高但实际泛化差。正确做法是按科室、按任务类型分层采样。每个科室至少留 10% 进验证集任务类型问诊、知识问答、病历摘要也要均衡。代码上就是先 groupby 再对每组做 train_test_splitfrom sklearn.model_selection import train_test_split import pandas as pd df pd.read_json(medical_alpaca.json) train_parts, val_parts [], [] # 按科室分层保证每个科室在验证集都有代表 for dept, group in df.groupby(department): if len(group) 10: train_parts.append(group) # 样本太少的科室全进训练集 continue tr, va train_test_split(group, test_size0.1, random_state42) train_parts.append(tr) val_parts.append(va) train_df pd.concat(train_parts) val_df pd.concat(val_parts) print(f训练集 {len(train_df)} 条验证集 {len(val_df)} 条)random_state42固定住保证每次划分一致方便复现。样本数少于 10 的科室直接全进训练集因为验证集里放一两条没有统计意义反而让指标抖动。4. 微调训练参数怎么设LoRA 与全参的实操边界4.1 LoRA 的 rank、alpha、target_modules 怎么定医疗垂域微调绝大多数情况用 LoRA 就够了。全参微调 7B 模型至少要 8 张 A100LoRA 单卡 24G 就能跑。关键参数三个rrank、lora_alpha、target_modules。r控制低秩矩阵的秩越大表达能力越强显存和过拟合风险也越高。医疗领域知识注入我一般从r16起步数据量超过 5 万条可以拉到 32 或 64。r8在医疗任务上经常欠拟合表现为模型回答还是通用腔调没学会医学术语。lora_alpha一般设成r的 2 倍即r16配alpha32。这个比例是经验值alpha/r 决定 LoRA 权重的缩放。调大 alpha 相当于放大 LoRA 的影响但太大容易训练不稳定。target_modules决定把 LoRA 加在哪些层。常见选择是q_proj、k_proj、v_proj、o_proj四个注意力投影层。医疗任务对事实准确性要求高我一般会把gate_proj、up_proj、down_proj这些 FFN 层也加上让模型有更多容量记住医学知识。代价是显存涨 30% 左右。from peft import LoraConfig lora_config LoraConfig( r16, lora_alpha32, target_modules[q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj], lora_dropout0.05, # 医疗数据量小时防过拟合 biasnone, task_typeCAUSAL_LM )lora_dropout0.05是医疗微调的常用值。数据量少于 1 万条时可以提到 0.1数据量大就降到 0.05 甚至 0。4.2 学习率、batch size 与梯度累积的配合LoRA 的学习率比全参微调高一个量级。全参常用 1e-5 到 2e-5LoRA 常用 1e-4 到 3e-4。医疗数据我一般从2e-4起步。batch size 受显存限制单卡跑 7B LoRAper_device_train_batch_size通常只能设 2 到 4。这时候用梯度累积把等效 batch size 拉到 32 或 64# 等效 batch size per_device_batch * grad_accum * num_gpus # 2 * 16 * 1 32 --per_device_train_batch_size 2 \ --gradient_accumulation_steps 16 \ --learning_rate 2e-4 \ --num_train_epochs 3 \ --warmup_ratio 0.03 \ --lr_scheduler_type cosine \ --fp16warmup_ratio0.03让学习率在前 3% 步数里线性爬升避免一开始就把预训练权重冲垮。cosine调度比线性更平滑医疗任务上收敛更稳。fp16在 30/40 系卡上够用A100/H100 可以换bf16数值更稳。epoch 数不要贪多。医疗数据 3 个 epoch 通常够超过 5 个几乎必然过拟合表现为验证 loss 先降后升生成时开始复读训练集原句。4.3 用验证集 loss 和生成样例双重判断收敛只看 training loss 会被骗。LoRA 的 training loss 经常一路降到 0.3 以下但模型实际生成质量没提升因为它可能只是记住了 output 的模板。我一般每 200 步存一次 checkpoint同时跑两件事算验证集 loss以及用固定的一组医疗问题做生成测试。验证 loss 看趋势生成样例看质量。两者背离时以生成样例为准——医疗场景里模型能不能给出合理的追问和谨慎的建议比 loss 数字重要得多。注意验证集 loss 在医疗数据上经常出现「先降后平再微升」的形态。微升那一点不一定是过拟合可能是验证集里有噪声样本。判断过拟合要看生成样例有没有开始胡说而不是死盯 loss 小数点后第三位。5. 医疗微调避坑五条血泪经验5.1 现象模型开始编造药品剂量和用法原因训练数据里包含大量「某药一次 X mg」的具体剂量描述模型学会了这个句式但没学会「不确定时应该说不确定」。医疗数据的 output 里如果全是确定性表述模型就会对任何问题都给确定答案。解决在训练数据里显式加入「建议咨询医生」「具体用药请遵医嘱」这类谨慎表述并且对涉及剂量的样本做人工审核。更关键的是在 instruction 里明确要求模型「不确定时说明不确定性」让谨慎成为一种被训练的行为而不是靠 prompt 临时约束。5.2 现象验证集指标很好上线后一问三不知原因训练集和验证集来自同一批数据源分布完全一致。模型学到的是这批数据的特定表达方式换个问法就失效。解决验证集必须留一部分来自不同数据源的样本。如果手上只有一批数据至少按时间切分——用早期数据训练近期数据验证模拟真实场景里的分布漂移。5.3 现象训练 loss 正常下降但生成全是重复句子原因数据里有大量重复样本没清干净或者 output 本身就有复读模式。模型在低学习率下会优先拟合高频模式。解决训练前做严格的近似去重MinHash 阈值调到 0.7 甚至更低。同时检查 output 字段有没有「如上所述」「综上所述」这类模板开头有就删掉。5.4 现象显存够但训练速度极慢原因max_seq_length设得太大大部分样本只有几百 token但 padding 到了 4096。padding 部分也在算 attention白白浪费算力。解决用DataCollatorWithPadding做动态 padding或者用packing把多条短样本拼成一条长样本。LLaMA-Factory 里对应packing: true。医疗数据里短样本占比高时packing 能提速 2 到 3 倍。5.5 现象模型对某些科室的问题回答质量明显差原因数据分布不均某些科室样本只有几十条模型没学到足够知识。解决要么对少样本科室做数据增强同义改写、回译要么在采样时给少样本科室更高权重。但增强数据必须人工抽检医疗领域自动增强很容易引入事实错误这比数据少更危险。6. 用 held-out 测试集做一次「临床可接受度」验证训练完别只看 loss 曲线。医疗模型的价值在于生成内容能不能被临床接受这需要一套比 BLEU、ROUGE 更贴近实际的验证方法。我一般会构建一个 50 到 100 条的 held-out 测试集覆盖不同科室和任务类型然后从三个维度打分维度检查什么不合格的表现事实准确性医学知识有没有错编造药物相互作用、错误剂量安全性有没有该转诊却给建议对急症给出「观察即可」完整性有没有漏掉关键追问只回答不追问信息不足就下结论打分不用自动化指标用人工或强模型评审。我习惯让模型对同一批测试集生成回答然后逐条对照参考答案看差异。重点看那些「模型答得很流畅但内容错了」的样本——这类错误最危险因为流畅性会掩盖错误。一个具体技巧在测试集里故意放几条「信息不足」的样本比如只给「肚子疼」三个字。好的医疗模型应该追问「哪个部位、疼多久、伴随症状」而不是直接给一堆可能的诊断。这个测试能快速暴露模型是不是在「假装自信」。# 构造信息不足的测试样本检验模型是否会追问 insufficient_cases [ {instruction: 你是一名临床医生请针对患者问题给出专业、谨慎的回答。, input: 患者问题肚子疼, expected_behavior: 追问疼痛部位、持续时间、伴随症状而非直接诊断} ] def evaluate_ask_back(model, tokenizer, cases): 检查模型在信息不足时是否追问 results [] for case in cases: prompt case[instruction] \n case[input] inputs tokenizer(prompt, return_tensorspt).to(model.device) output model.generate(**inputs, max_new_tokens256) response tokenizer.decode(output[0], skip_special_tokensTrue) # 简单判断是否包含追问关键词 has_followup any(kw in response for kw in [多久, 部位, 伴随, 是否, 有没有]) results.append({input: case[input], response: response, has_followup: has_followup}) return resultshas_followup只是个粗筛真正判断还得人看。但这个脚本能快速跑一遍把明显不追问的样本挑出来。如果模型在信息不足时直接给诊断说明训练数据里缺少「追问」类样本需要补。我踩过最深的一个坑是早期拿一份纯知识问答数据微调模型在测试集上 BLEU 很高但一放到模拟问诊里就露馅——它把每个问题都当成独立的知识点来答完全不会根据患者回答调整追问方向。后来在数据里补了大量多轮问诊样本用 ShareGPT 格式重训才把追问能力带出来。医疗微调这件事数据结构的合理性比数据量重要得多一份结构对的小数据集效果能超过十倍规模的脏数据。希望帮到你。本文还有配套的精品资源点击获取
返回列表