
简介这份中文医疗对话数据集面向医疗NLP研究者、问答系统开发者及医学AI方向的学生提供覆盖多科室的真实医患问答语料可用于构建智能问诊、意图识别与对话生成模型。资源包共13个文件以7个csv数据表为主另含txt说明、Python数据处理脚本、license与md文档压缩包约144.11MB按科室分目录组织便于按需加载与二次清洗。数据涵盖男科、内科、妇产科、肿瘤科、儿科与外科六大科室累计约79万条问答对单科室规模从7万余条到22万余条不等每条记录包含部门、标题、问题与回答字段语义完整、贴近临床表达。已有150人学习下载适合用于模型微调、检索增强问答或医学对话评测等场景能帮助读者快速获得结构化医疗语料省去从零采集与标注的成本。1. 中文医疗对话数据集从解压到跑通第一轮意图识别的落地路径如果你正在做医疗方向的对话系统大概率绕不开一个尴尬公开的中文医疗对话语料要么太干净、像教科书问答要么太脏、连说话人和科室都分不清。这份「中文医疗对话数据集.zip」解决的就是中间那段——它给的是贴近真实问诊场景的多轮对话带症状描述、追问、初步建议这类结构能直接喂给意图分类、槽位抽取或者检索式问答的流程。它适合三类人想快速验证医疗 NLU 模型的学生、需要冷启动语料的产品原型开发者、以及拿它做数据清洗练手的工程师。下面按「解压看结构 → 清洗成训练格式 → 跑通意图识别 → 避开几个血泪坑」的顺序走一遍Windows 环境下就能全程复现。2. 先看清压缩包里到底装了什么目录结构与字段含义拿到一个 zip 最忌讳直接pd.read_csv一把梭。医疗对话数据的字段命名往往带业务黑话先摸清结构再动手能省掉后面反复改列名的返工。2.1 解压与目录探查Windows 下用资源管理器右键解压即可但如果你要批量处理或者后续接脚本建议用命令行方便记录路径。假设解压到D:\data\medical_dialog# 进入解压目录先看整体结构 cd /d D:\data\medical_dialog dir /s /bdir /s /b会递归列出所有文件的完整路径比一层层点进去快得多。常见做法是你会看到类似dialog/、raw/、README这样的组合对话主体通常是一个或多个.json/.csv/.txt。如果目录里出现train、dev、test的划分说明作者已经做过切分可以直接沿用如果只有一个大文件那切分逻辑得自己补。提示解压前先确认磁盘剩余空间。对话类语料动辄几百 MB 到数 GB解压后体积通常是压缩包的 3 到 5 倍别解到一半卡住。2.2 字段含义与对话轮次结构医疗对话的核心不是单句而是「轮次」。一份合格的问诊对话至少包含说话人角色患者/医生、轮次序号、话语内容有时还有科室、症状标签、对话 ID。用 Python 快速抽样看几条import json # 假设主文件是 dialog.json按行存储JSONL 常见 with open(dialog.json, r, encodingutf-8) as f: for i, line in enumerate(f): if i 3: # 只看前 3 条避免刷屏 break sample json.loads(line) print(json.dumps(sample, ensure_asciiFalse, indent2))这段代码的关键在ensure_asciiFalse不加的话中文会变成\uXXXX转义根本没法肉眼核对。打印出来后重点看三件事一是对话是不是按dialogue_id分组的多轮数组二是角色字段叫role、speaker还是from三是内容字段是text、content还是utterance。这三个名字对不上后面所有脚本都得改。如果文件是 CSV字段确认方式换成import pandas as pd df pd.read_csv(dialog.csv, encodingutf-8) print(df.columns.tolist()) # 先看列名 print(df.head(3).to_dict(records)) # 再看前 3 条实际内容df.columns.tolist()能一次性把列名摊开比df.info()更直观。参数上encoding在 Windows 上偶尔要试gbk如果报UnicodeDecodeError先换编码再怀疑文件损坏。2.3 判断数据能不能直接用的三个信号不是所有医疗对话数据都值得投入。抽样之后用三个信号快速判断第一看患者话语里有没有口语化表达「肚子疼得厉害」而不是「腹痛」有口语才说明贴近真实第二看医生回复是不是模板化「建议您去医院就诊」反复出现就是低质第三看多轮之间有没有逻辑承接追问症状、确认病史如果每轮都是独立问答那它本质是 FAQ 而不是对话。这三个信号决定了你后面是拿它做意图分类还是只能做单句文本分类。3. 把原始对话清洗成可训练格式角色对齐与轮次切分原始数据能看不等于能训。医疗对话最常见的脏点是角色错位、轮次断裂、以及长短句极度不均衡。这一章把清洗流程拆成可复现的步骤。3.1 角色对齐别让患者的话被标成医生角色字段错位是医疗对话里最隐蔽的坑。有些数据用0/1表示患者/医生但不同文件里 0 的含义可能相反。清洗第一步就是统一成可读标签import pandas as pd df pd.read_csv(dialog.csv, encodingutf-8) # 把数字角色映射成文字映射关系以实际抽样为准 role_map {0: patient, 1: doctor} df[role_label] df[role].map(role_map) # 校验统计每个对话里患者和医生的轮次数量 turn_count df.groupby([dialogue_id, role_label]).size().unstack(fill_value0) print(turn_count.head()) # 异常信号某个对话里医生轮次为 0或患者轮次为 0 bad turn_count[(turn_count.get(doctor, 0) 0) | (turn_count.get(patient, 0) 0)] print(异常对话数, len(bad))map的作用是把离散编码翻译成语义标签方便后续按角色过滤。unstack(fill_value0)把长表转成「对话 × 角色」的宽表一眼就能看出哪些对话只有单方发言。如果异常对话占比超过 5%说明映射关系可能反了把role_map对调再跑一遍即可。这一步不做后面训练出来的意图分类会把医生的话也当成患者意图准确率虚高但线上全崩。3.2 轮次切分把长对话拆成「上下文 当前句」意图识别模型通常吃的是「前几轮 当前句」这样的样本而不是整段对话。所以要把多轮对话滑窗切分def build_samples(df, window2): window 表示当前句之前保留几轮作为上下文 samples [] for did, group in df.groupby(dialogue_id): group group.sort_values(turn_index) # 确保按轮次排序 turns group.to_dict(records) for i in range(len(turns)): if turns[i][role_label] ! patient: continue # 只对患者话语打意图标签 start max(0, i - window) context .join(t[content] for t in turns[start:i]) samples.append({ dialogue_id: did, context: context, query: turns[i][content], label: turns[i].get(intent, unknown) }) return pd.DataFrame(samples) samples build_samples(df, window2) print(samples.shape) print(samples.head(3))window2是常见起点表示当前患者话语往前保留两轮作为上下文。窗口太小学不到追问逻辑太大则引入无关信息、还拖慢训练。sort_values(turn_index)不能省很多原始文件是按写入顺序存的不排序会导致上下文错乱。label取intent字段如果原始数据没有意图标注那这份数据只能做无监督聚类或检索不能直接做分类——这是选型前必须确认的边界。3.3 文本归一化与长度过滤医疗对话里夹杂大量全角标点、多余空格、以及「嗯」「啊」这类填充词。归一化不是可选项import re def normalize(text): text str(text) text re.sub(r\s, , text) # 合并连续空白 text re.sub(r[。], lambda m: m.group(), text) # 保留中文标点 text text.strip() return text samples[query] samples[query].apply(normalize) samples[context] samples[context].apply(normalize) # 过滤过短和过长的样本 samples samples[(samples[query].str.len() 4) (samples[query].str.len() 200)] print(清洗后样本数, len(samples))长度下限设 4 是为了滤掉「哦」「好」这种无意图价值的短句上限 200 是防止个别超长描述拖慢 tokenizer。这两个阈值不是死的如果你的数据里患者描述普遍偏长上限可以放到 300。归一化之后建议再抽样看几条确认没有把有意义的标点误删。4. 跑通第一轮意图识别从词袋基线到轻量分类器清洗完就该验证数据到底能不能训出东西。别一上来就上大模型先用轻量方案跑通闭环确认数据本身有信号。4.1 用 TF-IDF 逻辑回归做基线基线的作用是告诉你「这份数据的意图是否可分」。如果连基线都跑不出像样的准确率说明标注质量有问题换模型也救不回来。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # 只保留有明确意图标签的样本 labeled samples[samples[label] ! unknown].copy() print(有标签样本数, len(labeled)) X_train, X_test, y_train, y_test train_test_split( labeled[query], labeled[label], test_size0.2, random_state42, stratifylabeled[label] ) vectorizer TfidfVectorizer(max_features5000, ngram_range(1, 2)) X_train_vec vectorizer.fit_transform(X_train) X_test_vec vectorizer.transform(X_test) clf LogisticRegression(max_iter1000, C1.0) clf.fit(X_train_vec, y_train) pred clf.predict(X_test_vec) print(classification_report(y_test, pred))ngram_range(1, 2)让模型同时看单字词和双字词组合医疗场景里「肚子疼」和「肚子 疼」的区分就靠这个。stratify保证训练测试集里各类意图比例一致否则小类别可能全被分到一边。C1.0是正则强度如果发现过拟合训练集远高于测试集把它调小到 0.1 试试。跑完看classification_report里的f1-score如果多数类别都在 0.7 以上说明数据可用如果全是 0.3 左右先回去查标签是不是错的。4.2 上下文拼接能不能提升效果上一章切分出的context字段别浪费把它和query拼起来再训一版对比效果labeled[full_text] labeled[context] [SEP] labeled[query] X_train2, X_test2, y_train2, y_test2 train_test_split( labeled[full_text], labeled[label], test_size0.2, random_state42, stratifylabeled[label] ) X_train_vec2 vectorizer.fit_transform(X_train2) X_test_vec2 vectorizer.transform(X_test2) clf2 LogisticRegression(max_iter1000, C1.0) clf2.fit(X_train_vec2, y_train2) print(classification_report(y_test2, clf2.predict(X_test_vec2)))[SEP]是人为插入的分隔符让模型知道上下文和当前句的边界。对比两版的macro avg f1如果拼接后提升明显比如 3 个点以上说明这份数据的多轮信息确实有价值后续可以上 BERT 类模型如果几乎没变化说明上下文对意图判断贡献有限把精力放在单句特征上更划算。这一步是选型的分水岭别跳过。4.3 保存可复用的向量器和模型跑通之后立刻固化别每次重训import joblib joblib.dump(vectorizer, tfidf_vectorizer.pkl) joblib.dump(clf, intent_clf.pkl) print(模型已保存)joblib比pickle更适合存 sklearn 对象内部对 numpy 数组有优化。保存后写个最小推理脚本验证vec joblib.load(tfidf_vectorizer.pkl) model joblib.load(intent_clf.pkl) test_query 最近总是头晕需要做什么检查 pred model.predict(vec.transform([test_query])) print(预测意图, pred[0])能跑出结果说明整条链路闭环了。这时候再考虑要不要换更强的模型而不是一开始就纠结架构。5. 避坑与排查医疗对话数据处理的五个翻车现场这一章全是踩过的坑每条按「现象 → 原因 → 解决」写照着排查能省下大量调试时间。5.1 现象训练准确率 0.95测试只有 0.4原因同一对话的样本被同时分到训练集和测试集模型记住了对话而非意图。医疗对话里同一dialogue_id的句子高度相似随机切分必然泄漏。解决按dialogue_id分组切分而不是按行切分。用GroupShuffleSplitfrom sklearn.model_selection import GroupShuffleSplit gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(labeled, groupslabeled[dialogue_id])) train_df labeled.iloc[train_idx] test_df labeled.iloc[test_idx]groups参数指定分组键保证同一对话只出现在一边。这是医疗对话数据最容易翻车的地方没有之一。5.2 现象中文全部变成乱码或问号原因Windows 默认编码是gbk而数据文件多为utf-8读取时不指定编码就会乱。解决所有文件读取都显式加encodingutf-8。如果仍报错用chardet探测真实编码import chardet with open(dialog.csv, rb) as f: raw f.read(10000) print(chardet.detect(raw))拿到encoding结果后再传给read_csv。别用errorsignore硬吞那会静默丢字符后面查都查不出来。5.3 现象意图类别极度不均衡小类别 f1 为 0原因医疗对话里「问诊」类占绝大多数「投诉」「复诊」类样本极少模型直接偏向多数类。解决先统计分布再决定策略print(labeled[label].value_counts())如果最小类别少于 50 条优先合并语义相近的类别而不是盲目上过采样。class_weightbalanced可以作为临时手段clf LogisticRegression(max_iter1000, class_weightbalanced)但它只是调整损失权重样本太少时依然学不到有效特征合并类别往往更实在。5.4 现象分词后「肚子疼」被切成「肚子」「疼」语义丢失原因通用分词器不认识医疗口语词切碎后 TF-IDF 特征失效。解决加自定义词典或改用字符级 n-gram。字符级更省事vectorizer TfidfVectorizer(analyzerchar, ngram_range(1, 3), max_features8000)analyzerchar按字切分ngram_range(1, 3)覆盖单字到三字组合对医疗口语这种新词多的场景反而更稳。代价是特征维度上升用max_features压住即可。5.5 现象推理时新句子预测结果全是同一个类别原因新句子里的词不在训练词表里TF-IDF 向量全为 0模型只能输出先验概率最高的类。解决检查vectorizer.transform后的非零元素数量vec_result vec.transform([最近总是头晕]) print(非零特征数, vec_result.nnz)如果nnz为 0说明词表覆盖不足。要么扩大训练语料的词汇覆盖要么在推理前做同义词归一把「头晕」映射到训练集里出现过的表达。这个坑在冷启动阶段特别常见别等到上线才发现。6. 进阶技巧用对话结构做意图纠错与置信度过滤基线跑通之后真正拉开效果差距的往往不是换模型而是利用对话本身的结构信息。医疗问诊有个特点患者的意图常常在下一轮才明确。比如第一句「肚子不舒服」单看是「症状描述」但如果医生追问后患者说「吃了海鲜之后开始的」那意图就偏向「病因询问」。利用这种前后轮关系做纠错比单纯堆特征更有效。一个具体做法是对每个对话取所有患者轮次的预测结果如果相邻两轮预测的意图高度相关比如「症状描述」后接「病程询问」就保留如果出现突兀跳变「症状描述」直接跳到「费用咨询」就用上下文重新打分。实现上可以维护一个意图转移矩阵统计训练集里真实标签的相邻转移频率import numpy as np from collections import defaultdict # 统计相邻患者轮次的意图转移 trans defaultdict(lambda: defaultdict(int)) for did, group in labeled.groupby(dialogue_id): group group.sort_values(turn_index) labels group[label].tolist() for a, b in zip(labels, labels[1:]): trans[a][b] 1 # 归一化成概率 for a in trans: total sum(trans[a].values()) for b in trans[a]: trans[a][b] / total # 推理时对低置信度样本做转移校验 def correct_with_transition(prev_label, cur_label, prob, threshold0.6): if prob threshold: return cur_label # 置信度低时看转移概率里哪个候选更高 candidates trans.get(prev_label, {}) if not candidates: return cur_label return max(candidates, keycandidates.get)trans记录的是「前一个意图 → 后一个意图」的频率threshold0.6是置信度门槛低于它才触发纠错。这个方法的边界在于它依赖训练集里意图转移的统计规律如果测试场景的对话流程和训练集差异大纠错反而会引入错误。所以上线前一定要在留出集上验证纠错前后的 f1 变化别凭感觉开。另一个实用技巧是置信度过滤。医疗场景对误判容忍度低与其硬猜不如把低置信度样本转人工或转兜底回复probs clf.predict_proba(X_test_vec) max_probs probs.max(axis1) confident max_probs 0.7 print(高置信度占比, confident.mean())predict_proba返回每个类别的概率max取最高那个作为置信度。0.7这个阈值可以按业务调宁可少答也别答错。我自己的习惯是任何医疗意图模型上线前都强制跑一遍「按对话分组切分 置信度分布 转移矩阵校验」这三件套缺一个都不敢发版。这套流程不复杂但能挡掉大部分线上翻车。希望帮到你。本文还有配套的精品资源点击获取