ARTICLE DETAIL

资讯详情

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

llm-medical-data 拆包实战:从科室CSV到微调JSONL的完整流程

llm-medical-data 拆包实战:从科室CSV到微调JSONL的完整流程 简介llm-medical-data 是一份面向大模型微调训练场景的医疗数据集资源适合个人学习者、数据科学初学者及独立研究人员用于医疗 AI 方向的实验与练手。资源包共 34 个文件以 json、csv 为主辅以 py 脚本、zbak 备份、zip 压缩包及 txt、md 说明文档整体约 224.51MB涵盖妇产科、外科、儿科、肿瘤科、内科、男科等科室数据以及 Huatuo、GenMedGPT、iCliniq 等对话与问答语料并配有对话生成、格式转换等处理脚本。已有 206 人学习下载。借助这些多科室、多来源的医疗语料读者可实践大模型微调流程理解医疗问答与对话数据的组织方式并借助脚本完成 csv 与 json 之间的转换、中英翻译等预处理操作为后续模型训练与效果验证提供可复用的数据基础。1. 从一堆散装科室文件到可训练语料llm-medical-data 到底装了什么如果你最近在折腾大模型微调尤其是医疗垂直方向大概率会遇到一个很现实的问题公开的通用语料一抓一大把但真正带科室标签、带问答结构、能直接喂给 SFT 脚本的中文医疗数据少得可怜。llm-medical-data 这个包就是冲着这个缺口来的。它把妇产科、外科、儿科、肿瘤科、内科、男科这些科室的原始文件连同 Huatuo、GenMedGPT-5k、HealthCareMagic-100k、iCliniq 等公开医疗问答语料一起打包成了一个可以本地解压、按需取用的资源集合。我第一次拆开这个压缩包的时候最直观的感受是它不是一份“干净到可以直接训练”的成品数据集而是一个半成品原料库——有 CSV、有 JSON、有 zip 嵌套、有转换脚本、有种子任务文件甚至还有.zbak备份文件。这意味着它适合愿意动手清洗和转换的人而不是指望一键跑通的人。如果你正好在做医疗问答微调、想找带科室维度的中文语料做实验或者只是想看看一个真实医疗数据集长什么样这份资源值得你花时间拆一遍。2. 拆包先看目录文件结构与数据格式的对应关系2.1 顶层文件分类与用途拿到压缩包之后不要急着写训练脚本。先花十分钟把目录结构过一遍能省掉后面大量返工。这个包的顶层大致可以分成四类科室原始数据、公开问答语料、转换脚本、任务配置文件。科室原始数据这块obgyn_妇产科.zip、IM_内科.zip是压缩包形式surgical_外科.csv、pediatric_儿科.csv、oncology_肿瘤科.csv、andriatria_男科.csv是直接可读的 CSV。这种混搭本身就说明数据来源不统一有的是从某个系统导出的有的是二次整理的。公开问答语料这边GenMedGPT-5k.json、iCliniq.json、HealthCareMagic-100k.zip、huatuo_llama_med_chinese_data都是比较常见的医疗对话来源格式以 JSON 为主字段命名各有各的习惯。转换脚本集中在scripts目录下csv2json_opengpt_data.py、csv2json_chinese_medical_dialogue_data.py、en2zh_trans_google.py这三个是核心。任务配置这边dialogue_seed_task.json、dialogue_task.json、test_result.json看起来是对话生成流程的中间产物book_based_question_generation.py和book_data.json、book_based_qa.json则对应从书籍内容生成问答的链路。我一般会先跑一条命令把整体规模摸清楚# 统计各类型文件数量和总大小快速判断数据体量 find . -type f \( -name *.csv -o -name *.json -o -name *.zip \) -exec ls -lh {} \; | awk {print $5, $9} | sort -k2这条命令的逻辑很简单把所有 CSV、JSON、ZIP 文件列出来按文件名排序同时显示大小。跑完之后你会对“哪些文件大、哪些只是配置”有个直观判断。参数上没什么需要改的如果你只想看 CSV把-name条件收窄即可。2.2 CSV 与 JSON 的字段差异科室 CSV 和公开 JSON 语料在字段设计上差别很大。CSV 通常偏向结构化病历或问答对列名可能是department、question、answer这种直白命名而 JSON 语料往往嵌套更深比如GenMedGPT-5k.json里一条记录可能包含instruction、input、output三段式结构iCliniq.json则更接近医患对话的dialogue数组。先抽样看几条比读任何说明文档都快import json import pandas as pd # 抽样查看 CSV 结构 df pd.read_csv(surgical_外科.csv, nrows3) print(CSV columns:, df.columns.tolist()) print(df.head(3).to_string()) # 抽样查看 JSON 结构 with open(GenMedGPT-5k.json, r, encodingutf-8) as f: data json.load(f) print(JSON type:, type(data)) print(First record keys:, list(data[0].keys()) if isinstance(data, list) else list(data.keys()))这段代码做了两件事用 pandas 读 CSV 的前三行看列名和内容用 json 读 JSON 看顶层类型和第一条记录的键。参数上nrows3是为了快速预览正式处理时去掉即可。如果你发现 JSON 顶层是 dict 而不是 list那说明它可能是按科室或来源分组的嵌套结构后面转换脚本就得对应调整。提示.zbak后缀的文件是备份文件不要直接删。有些脚本在转换失败时会回退读取备份先留着等确认流程跑通再清理。3. 把 CSV 转成训练格式脚本参数与字段映射实操3.1 csv2json 脚本的调用方式与参数含义scripts目录下的csv2json_chinese_medical_dialogue_data.py和csv2json_opengpt_data.py是两个不同目标格式的转换入口。前者偏向中文医疗对话格式后者偏向 OpenGPT 风格的指令微调格式。我一般会先看脚本头部的 argparse 定义确认它接受哪些参数# 查看脚本支持的参数 python scripts/csv2json_chinese_medical_dialogue_data.py --help常见做法是脚本会接受--input_dir、--output_file、--department这类参数。如果脚本没有写--help直接打开文件看argparse.ArgumentParser()后面跟的add_argument调用。参数里最需要关注的是字段映射CSV 里的哪一列对应instruction哪一列对应output。有些脚本会硬编码列名比如默认找question和answer如果你的 CSV 列名是q和a就得改脚本或者先重命名列。# 如果脚本硬编码了列名可以先做一次列重命名再转换 import pandas as pd df pd.read_csv(pediatric_儿科.csv) # 假设原列名是 q_text / a_text统一改成脚本认识的 question / answer df df.rename(columns{q_text: question, a_text: answer}) df.to_csv(pediatric_儿科_renamed.csv, indexFalse)这段代码的逻辑是在调用转换脚本之前先把列名对齐到脚本期望的命名。参数上rename的字典左边是原列名右边是目标列名按你实际 CSV 的列名改。做完这一步再跑转换脚本成功率会高很多。3.2 多科室合并与去重策略科室文件是分开的但训练时通常希望合并成一个大的 JSON 或 JSONL。合并的时候有两个坑一是不同科室的字段可能不完全一致二是跨科室可能存在重复问答对。我一般会先做一次字段对齐再做一次基于 question 文本的去重。import pandas as pd import glob import json all_records [] for csv_file in glob.glob(*_*.csv): df pd.read_csv(csv_file) # 统一字段名缺失的补空 df df.rename(columns{q: question, a: answer}) if question not in df.columns or answer not in df.columns: print(f跳过 {csv_file}字段不匹配) continue df[source] csv_file all_records.append(df[[question, answer, source]]) merged pd.concat(all_records, ignore_indexTrue) # 基于 question 去重保留第一条 merged merged.drop_duplicates(subset[question], keepfirst) print(f合并后记录数{len(merged)}) # 输出为 JSONL每行一条方便后续流式读取 with open(merged_medical_qa.jsonl, w, encodingutf-8) as f: for _, row in merged.iterrows(): f.write(json.dumps({instruction: row[question], output: row[answer]}, ensure_asciiFalse) \n)这段代码做了合并、字段对齐、去重、输出 JSONL 四件事。参数上drop_duplicates的subset指定按哪一列判重keepfirst表示保留第一次出现的记录。输出用 JSONL 而不是单个大 JSON是因为后续训练框架通常按行读取JSONL 更省内存。如果你发现去重后记录数骤降说明跨科室重复率很高这时候可以考虑按科室分别保留而不是全局去重。注意en2zh_trans_google.py涉及翻译调用跑之前确认你的环境能正常访问对应服务否则会卡在请求超时。如果只是做本地实验可以先跳过翻译脚本直接用已有中文语料。4. 避坑与排查拆包和转换阶段最容易翻车的五件事4.1 解压后文件名乱码现象obgyn_妇产科.zip解压出来文件名变成乱码CSV 读进去列名也是乱码。原因压缩包在打包时用了非 UTF-8 编码常见于 Windows 环境下用默认编码压缩的文件。解决用unzip -O cp936指定编码解压或者用 Python 的zipfile模块手动指定编码读取。# 指定 GBK 编码解压解决中文文件名乱码 unzip -O cp936 obgyn_妇产科.zip -d obgyn_extracted/如果unzip不支持-O参数换用 Pythonimport zipfile with zipfile.ZipFile(obgyn_妇产科.zip) as z: for name in z.namelist(): # 尝试用 GBK 解码文件名 try: correct_name name.encode(cp437).decode(gbk) except Exception: correct_name name print(correct_name)4.2 JSON 文件不是标准数组现象json.load()报错提示Expecting value或Extra data。原因有些 JSON 文件是 JSONL 格式每行一个独立 JSON 对象不是单个大数组。解决先读第一行判断格式如果是 JSONL 就逐行解析。import json with open(llama_data.json, r, encodingutf-8) as f: first_line f.readline().strip() if first_line.startswith({): # 可能是 JSONL逐行解析 records [] with open(llama_data.json, r, encodingutf-8) as f: for line in f: line line.strip() if line: records.append(json.loads(line)) print(fJSONL 格式共 {len(records)} 条) else: # 标准 JSON with open(llama_data.json, r, encodingutf-8) as f: records json.load(f) print(f标准 JSON共 {len(records)} 条)4.3 字段缺失导致转换脚本中途崩溃现象转换脚本跑到一半报KeyError提示某个列名不存在。原因不同科室 CSV 的列名不统一脚本没有做容错。解决在转换前先做一次全量列名扫描找出所有 CSV 的列名并集和交集对缺失字段做填充。import pandas as pd import glob all_columns {} for csv_file in glob.glob(*.csv): df pd.read_csv(csv_file, nrows0) all_columns[csv_file] set(df.columns) # 打印每个文件的列名人工确认差异 for f, cols in all_columns.items(): print(f, -, cols)4.4 内存不够导致大 JSON 读取失败现象HealthCareMagic-100k.zip解压后的 JSON 文件好几个 G直接json.load()把内存吃满。原因一次性加载整个大文件。解决用ijson做流式解析或者先转成 JSONL 再逐行处理。# 如果安装了 ijson可以流式读取大 JSON 数组 import ijson with open(HealthCareMagic-100k.json, rb) as f: for record in ijson.items(f, item): # 逐条处理不占大内存 pass4.5 去重后训练集和验证集泄漏现象模型在验证集上表现异常好但实际推理效果差。原因去重只做了训练集内部去重没有检查训练集和验证集之间是否有相同 question。解决先划分数据集再在训练集内部去重同时确保验证集的 question 不在训练集中出现。from sklearn.model_selection import train_test_split train, val train_test_split(merged, test_size0.1, random_state42) # 去掉验证集中出现在训练集里的 question val val[~val[question].isin(train[question])] print(f训练集 {len(train)} 条验证集 {len(val)} 条)5. 从原始语料到微调输入一个可复现的最小流程5.1 用 dialogue_task.json 做种子任务扩增dialogue_seed_task.json和dialogue_task.json这两个文件容易被忽略但它们其实定义了一套对话生成的任务模板。dialogue_generation.py会读取种子任务结合book_based_qa.json或medicalgpt_data里的内容生成更多对话样本。我一般会先看种子任务的字段结构import json with open(dialogue_seed_task.json, r, encodingutf-8) as f: seed json.load(f) print(json.dumps(seed[:2] if isinstance(seed, list) else seed, ensure_asciiFalse, indent2))如果种子任务是列表每条通常包含task_type、prompt_template、source_field这类字段。dialogue_generation.py会按prompt_template去填充source_field的内容生成新的问答对。参数上你需要注意模板里的占位符是否和你的数据字段匹配不匹配就改模板或者改数据。5.2 构建训练用 JSONL 并校验 token 长度生成完对话数据之后最后一步是转成训练框架认识的 JSONL 格式并检查 token 长度分布。这一步不做的话训练时可能因为超长样本被截断导致模型学到不完整回答。import json from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(your_base_model_path) def format_sample(record): # 按你的训练模板拼接 instruction 和 output text f### 问题{record[instruction]}\n### 回答{record[output]} return text lengths [] with open(merged_medical_qa.jsonl, r, encodingutf-8) as f_in, \ open(train_ready.jsonl, w, encodingutf-8) as f_out: for line in f_in: record json.loads(line) text format_sample(record) token_len len(tokenizer.encode(text)) lengths.append(token_len) if token_len 2048: # 超过 2048 的样本先丢弃或截断 f_out.write(json.dumps({text: text}, ensure_asciiFalse) \n) print(f总样本 {len(lengths)}保留 {sum(1 for l in lengths if l 2048)}) print(ftoken 长度中位数{sorted(lengths)[len(lengths)//2]})这段代码的逻辑是逐行读取 JSONL按模板拼接成训练文本用 tokenizer 计算 token 长度只保留不超过 2048 的样本。参数上2048这个阈值要根据你实际使用的基座模型上下文长度来改不要照搬。format_sample里的模板也要和你训练时用的模板保持一致否则训练和推理会不一致。5.3 验证数据可用性的三个检查点在正式开训之前我习惯做三个快速检查。第一随机抽 20 条样本人工读一遍看问答是否语义通顺、有没有明显错位。第二统计各科室样本占比避免某个科室数据量过大导致模型偏科。第三用一个小规模基座模型跑 10 步训练确认 loss 能正常下降、没有 NaN。import json import random # 随机抽 20 条人工检查 with open(train_ready.jsonl, r, encodingutf-8) as f: lines f.readlines() for line in random.sample(lines, min(20, len(lines))): record json.loads(line) print(record[text][:200]) print(---)这三个检查点花不了多少时间但能帮你提前发现数据格式错位、标签泄漏、模板不匹配这些玄学问题。血泪经验是不要等到训练跑了半天才发现验证集里混了训练集的问题那时候后悔药没地方买。提示test_result.json和prepared_generated_data_for_medical_tasks.csv这两个文件看起来是流程中间产物如果你是从头构建可以忽略它们如果你想复现作者的处理链路可以对照它们的字段反推每一步做了什么。6. 进阶技巧把科室标签变成可控生成条件6.1 在 instruction 里注入科室前缀这份资源最值钱的地方不是语料本身而是它带了科室维度。很多公开医疗数据集是不分科室的你没法控制模型生成“儿科风格”还是“肿瘤科风格”的回答。llm-medical-data 的科室文件让你可以在 instruction 里显式注入科室前缀从而在推理时通过切换前缀来控制生成方向。具体做法是在构建训练样本时把科室名拼进 instructionimport json import pandas as pd def build_with_department(csv_file, department): df pd.read_csv(csv_file) records [] for _, row in df.iterrows(): instruction f[{department}] {row[question]} records.append({instruction: instruction, output: row[answer]}) return records # 分别处理不同科室保留科室标签 surgical build_with_department(surgical_外科.csv, 外科) pediatric build_with_department(pediatric_儿科.csv, 儿科) oncology build_with_department(oncology_肿瘤科.csv, 肿瘤科) all_data surgical pediatric oncology with open(department_tagged.jsonl, w, encodingutf-8) as f: for record in all_data: f.write(json.dumps(record, ensure_asciiFalse) \n) print(f带科室标签样本数{len(all_data)})这段代码的关键在instruction的拼接方式[{department}] {question}。参数上科室名你可以用中文也可以用英文缩写只要训练和推理时保持一致就行。这样做的好处是推理时你输入[儿科] 孩子发烧怎么办模型会更倾向于生成儿科视角的回答而不是泛泛的通用建议。6.2 用科室分布做采样权重合并多科室数据之后如果直接混合训练样本量大的科室会主导 loss。我一般会按科室样本量倒数的比例做采样权重让每个科室在训练中被见到的概率更均衡。import json from collections import Counter with open(department_tagged.jsonl, r, encodingutf-8) as f: records [json.loads(line) for line in f] # 统计各科室样本数 dept_counts Counter() for r in records: dept r[instruction].split(])[0].strip([) dept_counts[dept] 1 total sum(dept_counts.values()) # 计算每个科室的采样权重样本越少权重越高 weights {dept: total / (len(dept_counts) * count) for dept, count in dept_counts.items()} print(科室采样权重, weights)这段代码先统计各科室样本数再按total / (科室数 * 该科室样本数)计算权重。样本少的科室权重高样本多的科室权重低。参数上len(dept_counts)是科室总数如果你只想对部分科室做均衡可以手动指定科室列表。拿到权重之后在训练框架的 dataloader 里按权重采样即可。6.3 验证科室可控性的小实验训练完之后怎么验证科室标签真的起作用了我一般会做一组对照实验同一个问题分别加上不同科室前缀看模型输出是否有明显差异。# 伪代码示意实际调用你的推理接口 questions [高血压怎么调理, 术后恢复注意什么] departments [内科, 外科, 儿科, 肿瘤科] for q in questions: for dept in departments: prompt f[{dept}] {q} # response model.generate(prompt) # print(f{dept}: {response[:100]}) print(f输入{prompt})如果模型对[儿科] 高血压怎么调理和[内科] 高血压怎么调理给出了明显不同的回答风格说明科室标签被模型学到了。如果输出几乎一样那可能是科室前缀在训练数据里占比太低或者模板拼接方式有问题。这个实验不需要跑全量推理抽几条看看就行。从那以后我每次拿到带标签的医疗数据都会先做一次科室可控性对照确认标签真的进了模型而不是被当成噪声忽略。希望这份拆包笔记能帮你少走点弯路把这份资源真正用起来。本文还有配套的精品资源点击获取
返回列表