好大夫2022在线的真实问诊数据集和简单分诊挑战赛

摘要

合集:AI案例-NLP-医疗
赛题:科大讯飞2022非标准化疾病诉求的简单分诊挑战赛2.0
AI问题:文本分类
数据集:好大夫在线的真实问诊数据,包括22,800条训练数据和7,600条测试数据。
数据集发布方:好大夫
数据集价值:优化医疗资源配置,找到合适的方向,进行分级诊疗。
解决方案:chinese-roberta-wwm-ext模型

一、赛题描述

背景

人民对于医疗健康的需求在不断增长,但社会现阶段医疗资源紧缺,往往排队一上午看病十分钟,时间和精神成本巨大。如何更好地优化医疗资源配置,找到合适的方向,进行分级诊疗,是当前社会的重要课题。大众自觉身体状态异常,有时不能准确判断自己是否患有疾病,需要寻求有专业知识的人进行判断,但是主诉者一般进行口语化表述,不容易进行精准高效的指引。

任务

进行简单分诊需要一定的数据和经验知识进行支撑。本次比赛提供了部分好大夫在线的真实问诊数据,经过严格脱敏,提供给参赛者进行单分类任务。具体为:通过处理文字诉求,给出20个常见的就诊方向之一和61个疾病方向之一。

二、数据集描述

数据说明

比赛共提供约22,800条训练数据,约7,600条测试数据。数据字段信息包括:

  • 年龄段:age
  • 主要诉求:diseaseName
  • 标题:title
  • 希望获得的帮助:hopeHelp
  • 其他描述字段文本conditionDesc
  • 就诊方向标签:label_i 取值范围 [0,19]
  • 疾病方向标签:label_j 取值范围 [0,60] (训练数据中疾病方向标签存在缺失,以-1标记,测试数据中没有)。

就诊方向

label_i就诊方向
0乳腺外科
1产前检查
2内科
3呼吸内科
4咽喉疾病
5妇产科
6小儿保健
7小儿呼吸系统疾病
8小儿消化疾病
9小儿耳鼻喉
10心内科
11消化内科
12甲状腺疾病
13皮肤科
14直肠肛管疾病
15眼科
16神经内科
17脊柱退行性变
18运动医学
19骨科

疾病方向

label_j疾病方向label_j疾病方向label_j疾病方向
0乳房囊肿21小儿支气管肺炎42皮肤瘙痒
1乳腺增生22小儿消化不良43皮肤科其他
2乳腺疾病23小儿消化疾病44直肠肛管疾病
3乳腺肿瘤24小儿耳鼻喉其他45眼部疾病
4产前检查25小儿肺炎46神经内科其他
5儿童保健26心内科其他47微量元素缺乏
6先兆流产27心脏病48羊水异常
7内科其他28扁桃体炎49肺部疾病
8剖腹产29早孕反应50胃病
9发育迟缓30月经失调51脊柱退行性变
10呼吸内科其他31桥本甲状腺炎52腰椎间盘突出
11咽喉疾病32消化不良53腹泻
12喉疾病33消化内科其他54腹痛
13围产保健34消化道出血55膝关节半月板损伤
14外阴疾病35甲减56膝关节损伤
15妇科病36甲状腺功能异常57膝关节韧带损伤
16宫腔镜37甲状腺疾病58运动医学
17小儿呼吸系统疾病38甲状腺瘤59韧带损伤
18小儿咳嗽39甲状腺结节60骨科其他
19小儿感冒40痔疮
20小儿支气管炎41皮肤病

训练数据data_train.xlsx

idagediseaseNameconditionDesctitlehopeHelplabel_ilabel_j
130+小红点是什么?四肢上部张图片中这样的小红疙瘩是怎么回事呢?特别痒。小红点是什么?请医生给我一些治疗上的建议1342
230+乳腺结节体检发现左侧乳腺有结节,13mm×8mm,自己没有任何症状左侧乳腺结节请医生给我一些治疗上的建议,目前病情是否需要手术?01
320+身体麻 身体坐左半面肢体有麻木感年初患有带状疱疹 在左腿脚踝上方 之后疱疹好了 但是左脚开始麻 随后左手麻 然后左腿和左胳膊陆续开始麻 直到左边脸也略有麻木感 同时右脚也麻左侧身体麻木什么原因导致的麻木以及如何治疗1647
410+生长缓慢,想再增长10厘米今年8月满15岁,身高165厘米,近半年生长缓慢,能否打生长激素再增高10厘米。想增高10厘米是否可打生长激素69
530+眼睛看东西突然变小,变远从小记事时就有此病,发病时看任何东西都变小,距离也判断不太好了,没有就过医。眼睛看东西突然变小,从小就有希望医生诊断这是什么病?需要就医吗?有什么注意事项?谢谢。1546
620+甲状腺嗓子疼,说话感觉里面有回音,先是由感冒引起,后来大量运动后天气变化着凉,疼痛加剧甲状腺肿大如何控制病情,是否还需做下一步检查1236
70+晚上磨牙,缺钙女,5岁4个月。骨密度测试部缺钙,验血又显示缺钙,不知道到底怎么回事孩子是否缺钙?希望医生解答一下孩子是否缺钙648
850+手指麻木一月前出现十个手指指尖麻木,现症状加重。手指麻木会是什么病?需要做什么检查?1647
920+便血,鲜红色的便血,鲜红色的,大便时还有点疼,就持续两天要不要做检查要不要做检查1134

测试数据data_test.xlsx

idagediseaseNameconditionDesctitlehopeHelp
030+胳膊内测浅白色印记男,30岁。胳膊内测有几处浅白色印记,绿豆大小,不痛不痒。不仔细看看不出来,不是很明显。印记处皮肤外表光滑。无皮屑。是不是白癜风希望医生给看一下是不是白癜风
150+心慌气短心里难受眉骨疼。口渴,不停打哈欠口干,身上没劲,心慌气短,眉骨疼,不想挣眼,想睡觉,平躺感觉舒服,饥饿后出现的上述症状,发病一周,吃护心丸那方面的病情?明天去医院具体挂什么科室?那方面的病情?明天去医院具体挂什么科室?
230+头痛失眠、手脚发麻大夫您好、感觉心慌失眠、手脚发麻请问是什么原因?给点意见、如何缓解症状给点意见、如何缓解症状
330+乳房内有一个硬块硬块已经发现有一个多月了,不疼,请问一下,这是什么样的症状啊!硬块要如何解决?你这一般会是什么情况想得到咨询。
40+五个月宝宝体重增长缓慢宝宝现在五个月,纯母乳,从四个月开始不好好吃奶,三个月到四个月长了一斤,四个月到五个月长了不到一斤,宝宝现在这情况是在经历厌奶期吗,需不需要采取什么措施,五个月可以加米粉了吗?五个月奶量一次120左右,少不少?五个月宝宝生长缓慢宝宝厌奶期生长缓慢需要采取什么措施

另提供14,000条相关知识文本,每条包含主体/(属性)/客体,供选手随意选用。

数据集版权许可协议

BY-NC-SA 4.0
https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans

三、解决方案样例

1、比较两种建模方式

解决方案采用TF-IDF(Term Frequency-Inverse Document Frequency)传统机器学习的建模方式和Chinese-ROBERTa-wwm-ext 大语言模型为基础的建模方式,并进行比较。

  • BERT处理:考虑词序和上下文,适合深度学习。
  • TF-IDF:词袋模型,适合传统机器学习。

2、TF-IDF建模

TF-IDF(Term Frequency-Inverse Document Frequency)是一种广泛应用于自然语言处理(NLP)和信息检索的技术,用于评估一个词语在文档集合或语料库中的重要性。TF-IDF建模方案存在的主要问题是无法支持文本中的语法和语义的学习问题,而大语言模型则能较好解决该问题。

TF(Term Frequency,词频)​

  • 定义:某个词在文档中出现的次数。
  • 公式:TF(t,d)=文档 d 中的总词数词 t 在文档 d 中出现的次数​
  • 作用:衡量词语在单篇文档中的常见程度。例如,在文档“苹果香蕉苹果”中,“苹果”的TF值为 32​。

IDF(Inverse Document Frequency,逆文档频率)​

  • 定义:衡量词语在文档集合中的稀有程度。罕见词具有更高的IDF值。
  • 公式:IDF(t)=log(包含词 t 的文档数总文档数​)
  • 作用:降低常见词的权重,突出重要词汇。例如,若“的”出现在所有文档中,则其IDF值趋近于0。

3、Bert建模

Chinese-ROBERTa-wwm-ext 是一个针对中文的预训练语言模型,基于 ​RoBERTa 架构,并针对中文特性进行了多项优化。其中,​wwm 表示 ​Whole Word Masked Modeling​(全词掩码建模),而 ​ext 通常指扩展版本(可能包含更长的上下文窗口、更大的模型规模或额外的训练策略)。

Chinese-ROBERTa 的改进

  • 分词适配:使用 WordPiece 或 BPE 分词,并针对中文设计词典。WordPiece 和 BPE(Byte Pair Encoding)是两种广泛使用的子词分词算法(Subword Tokenization),主要用于自然语言处理(NLP)中,将文本拆分为更小的、可处理的单元(子词或符号)。它们的核心目的是解决传统分词方法的局限性(如未登录词问题),同时平衡词汇表大小与语义表达能力。
  • 全词掩码(WWMM)​:将整个词作为掩码单位,而非单个字符,保留语义完整性。

4、运行环境

外部库名称版本号
python3.12.3
sklearn-compat0.1.3
torch2.5.1
transformers4.49.0
seaborn0.13.2

同时安装:

conda install openpyxl
conda install jieba

四、工作流程

1. 加载开发包

import pandas as pd
import seaborn as sns
import openpyxl
import jieba
import torch
from torch import nn
from torch.nn import CrossEntropyLoss
from torch.optim import AdamW
from torch.utils.data import Dataset, DataLoader
from transformers import AutoTokenizer, AutoConfig, AutoModel
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression

CrossEntropyLoss(交叉熵损失)用于衡量模型输出的概率分布与真实标签分布之间的差异。差异越小,损失值越低,说明模型的预测越准确。它主要应用于多分类问题,也可以用于二分类问题。

AdamW 是一种用于训练神经网络的优化算法。它的作用是根据损失函数的梯度(gradient)来更新模型的参数(weights),以使损失函数的值最小化,从而让模型的预测越来越准确。你可以把它想象成一个聪明的导航系统:损失函数告诉你当前的位置离目的地有多远(误差多大),梯度告诉你哪个方向是下坡路,而 AdamW 则负责计算沿着这个下坡路应该走多快的速度(学习率)和多大的步长(参数更新量)。

2. 数据读取

train_df = pd.read_excel('./data/data_train.xlsx')
test_df = pd.read_excel('./data/data_test.xlsx')
test_submit = pd.read_csv('./data/submit.csv')

# 数据集读取
class AppDataset(Dataset):
   def __init__(self, encodings, label_i, label_j):
       self.encodings = encodings
       self.label_i = label_i
       self.label_j = label_j
   
   # 读取单个样本
   def __getitem__(self, idx):
       item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
       item['label_i'] = torch.tensor(int(self.label_i[idx]))
       item['label_j'] = torch.tensor(int(self.label_j[idx]))
       return item
   
   def __len__(self):
       return len(self.label_i)

train_dataset = AppDataset(train_encoding,
                             train_df['label_i'].values[:-1000],
                             train_df['label_j'].values[:-1000])
val_dataset = AppDataset(val_encoding,
                             train_df['label_i'].values[-1000:],
                             train_df['label_j'].values[-1000:])
test_dataset = AppDataset(test_encoding, [0] * len(test_df), [0] * len(test_df))

3. 数据分析

train_df['label_i'].value_counts().plot(kind='barh')
train_df['label_j'].value_counts().plot(kind='barh')

4. TF-IDF 模型定义

a. 文本数据拼接

train_text = train_df['diseaseName'] + ' ' + train_df['conditionDesc'] + ' ' + train_df['title'] + ' ' + train_df['hopeHelp']
test_text = test_df['diseaseName'] + ' ' + test_df['conditionDesc'] + ' ' + test_df['title'] + ' ' + test_df['hopeHelp']
  • 将多个文本列用空格连接成一个字符串
  • 典型的多字段信息融合方式,帮助模型获取更全面的上下文

b. 空值处理

train_text = train_text.fillna('')
test_text = test_text.fillna('')

将NaN值替换为空字符串,避免后续处理出错

c. Tokenizer编码

train_encoding = tokenizer(train_text.tolist()[:-1000], truncation=True, padding=True, max_length=200)
val_encoding = tokenizer(train_text.tolist()[-1000:], truncation=True, padding=True, max_length=200)
test_encoding = tokenizer(test_text.tolist(), truncation=True, padding=True, max_length=200)
  • 将文本列表转换为TF-IDF模型可接受的输入格式
  • 关键参数:
    • truncation=True:超过max_length的部分会被截断
    • padding=True:不足max_length的会补零
    • max_length=200:设置最大序列长度
  • 数据划分:
    • 训练集:除最后1000条外的所有数据
    • 验证集:最后1000条数据
    • 测试集:全部测试数据

5、TF-IDF 模型训练和预测

TF-IDF建模和逻辑回归训练。

a. TF-IDF向量化

tfidf = TfidfVectorizer().fit(train_text)
train_tfidf = tfidf.fit_transform(train_text)
test_tfidf = tfidf.transform(test_text)
  • TfidfVectorizer():将文本转换为TF-IDF特征矩阵
  • fit_transform():训练+转换训练集
  • transform():仅转换测试集(使用训练集的词表)

b. 逻辑回归分类器

clf_i = LogisticRegression()
clf_i.fit(train_tfidf, train_df['label_i'])

clf_j = LogisticRegression()
clf_j.fit(train_tfidf, train_df['label_j'])
  • 为两个标签分别训练逻辑回归分类器
  • 使用相同的TF-IDF特征,预测不同的目标变量

c. 逻辑回归预测

进行逻辑回归预测,并保存预测结果到 tfidf_submit.csv。

test_submit['label_i'] = clf_i.predict(test_tfidf)
test_submit['label_j'] = clf_j.predict(test_tfidf)
test_submit.to_csv('tfidf_submit.csv', index=None)

运行结果如下:

输出数据样例:tfidf_submit.csv。其中id为测试数据项编号,label_i为一级分类,label_j为二级分类。

idlabel_ilabel_j
013-1
110-1
21646
30-1
469
53-1
62-1
71239
8725

6. Bert分类模型

AppBertModel为两级分类任务的模型。这种架构适合场景:

  • 需要同时预测两个相关标签的任务
  • 例如:情感分析(主情感+细粒度类别)
  • 或任何需要共享文本表示的双预测任务

类构造函数中的参数:

  • num_labels_i:第一个分类任务的类别数
  • num_labels_j:第二个分类任务的类别数

模型结构为:

  • 加载预训练的中文 RoBERTa 模型(wwm-ext 版本)
  • 10% 的 dropout 率,用于防止过拟合。
  • 两个分类器,都是简单的线性层。 输入维度为 768(RoBERTa 的隐藏层大小)。输出维度分别为两个任务的类别数。
  • 返回两个任务的 logits(未经过 softmax 的原始分数)。softmax() 函数将任意实数值向量转换为概率分布进行归一化。
class AppBertModel(nn.Module):
   def __init__(self, num_labels_i, num_labels_j):
       super(AppBertModel,self).__init__()

       #Load Model with given checkpoint and extract its body
       self.model = model = AutoModel.from_pretrained("./chinese-roberta-wwm-ext")
       self.dropout = nn.Dropout(0.1)
       self.classifier_i = nn.Linear(768, num_labels_i)
       self.classifier_j = nn.Linear(768, num_labels_j)

   def forward(self, input_ids=None, attention_mask=None,labels=None):
       outputs = self.model(input_ids=input_ids, attention_mask=attention_mask)
       sequence_output = self.dropout(outputs[0]) #outputs[0]=last hidden state
   
       logits_i = self.classifier_i(sequence_output[:,0,:].view(-1,768))
       logits_j = self.classifier_j(sequence_output[:,0,:].view(-1,768))
       
       return logits_i, logits_j

模型实例化:第一和第二个分类任务的类别数分别是20和61。

model = AppBertModel(20, 61)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# device = 'cpu'
model = model.to(device)

7. Bert模型训练和预测

以下是这段 PyTorch 训练代码的解析:

  1. 训练模式初始化
  • model.train():将模型切换到训练模式(启用 Dropout/BatchNorm 等训练专用层)。
  • iter_num:记录当前迭代次数。
  • total_iter:总批次数(train_loader 的长度)。
  1. 数据批次遍历
  • train_loader:数据加载器,按批次(batch)提供数据。
  • 条件打印:每 10 个批次打印一次进度(但跳过每 100 的倍数,避免冗余)。
  1. 梯度清零与数据准备
  2. 正向传播
  3. 损失计算
  4. 反向传播与梯度裁剪
  5. 参数更新
  6. 定期日志输出 每 100 次迭代打印:
  • 当前损失值:loss.item()
  • 训练进度:已完成迭代的百分比。
  • 双任务准确率:
  • pred_ilabel_i 的准确率。
  • pred_jlabel_j 的准确率(仅计算有效样本)。
def train():
   model.train()
   iter_num = 0
   total_iter = len(train_loader)

   for batch_idx, batch_data in enumerate(train_loader):
       if (batch_idx % 10 == 0 and batch_idx % 100 != 0):
           print("Training batch number: %d, total: %d" % (batch_idx, total_iter))

       # 正向传播
       optim.zero_grad()

       input_ids = batch_data['input_ids'].to(device)
       attention_mask = batch_data['attention_mask'].to(device)
       label_i = batch_data['label_i'].to(device)
       label_j = batch_data['label_j'].to(device)

       pred_i, pred_j = model(
           input_ids,
           attention_mask
      )

       valid = label_j != -1
       loss = loss_fn(pred_i, label_i)  + loss_fn(pred_j[valid], label_j[valid])

       # 反向梯度信息
       loss.backward()
       torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)

       # 参数更新
       optim.step()
       iter_num += 1

       if (iter_num % 100 == 0):
           print("Iterating Number: %d, Loss: %.4f, Progress: %.2f%%, Accuracy: %.4f / %.4f" % (
               iter_num, loss.item(), iter_num/total_iter*100,
              (pred_i.argmax(1) == label_i).float().data.cpu().numpy().mean(),
              (pred_j[valid].argmax(1) == label_j[valid]).float().data.cpu().numpy().mean()
          ))

训练和预测运行过程:

Epoch number: 0
Training batch number: 10, total: 1367
Training batch number: 20, total: 1367
...

Prediction batch number: 0, total: 475
Prediction batch number: 10, total: 475
Prediction batch number: 20, total: 475
Prediction batch number: 30, total: 475
...

输出数据样例:bert_submit.csv。其中id为测试数据项编号,label_i为一级分类,label_j为二级分类。

idlabel_ilabel_j
01341
11026
21646
302
469
5310
61341
71239
8725
91444

源码开源协议

GPL-v3

五、获取案例套装

需要登录后才允许下载文件包。登录

发表评论