ARTICLE DETAIL

资讯详情

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

Python多标签文本分类实战:基于JD智能分诊系统的完整工程解析

Python多标签文本分类实战:基于JD智能分诊系统的完整工程解析 简介面向医疗健康领域智能分诊场景的Python文本分类源码项目共分三阶段基础模型构建、优化策略应用、集成ResNet、BERT、XLNet等预训练模型进行特征提取用于提升分诊文本处理的效率与准确性适合有一定Python基础、关注NLP工程化的开发者或医疗信息化从业者参考。压缩包共58个文件约78.61MB包含30个CSV数据文件、TXT语料、6个Python源码文件、预训练模型文件夹及LDA主题模型、tfidf特征文件等。CSV与TXT构成训练/测试数据py源码覆盖特征工程、模型训练与评估模型文件用于推理或继续训练。已有296人学习下载。读者可从中获取完整项目源码、数据处理流程、模型集成思路与文件组织方式便于在此基础上复现实验、改进算法或迁移到相似的分诊分类任务同时也适合作为医疗领域智能问诊、预检分诊等场景的基线方案参考。1. 一份能直接跑的JD智能分诊文本分类源码到底装了什么“咳嗽三天、夜间加重、有痰”这句最普通的患者主诉放到分诊系统里应该推给呼吸内科还是感染科这种判断靠规则匹配很难稳定因为同一句话在不同上下文里指向完全不同。这份基于Python的JD智能分诊文本分类项目源码搭出了一条完整链路30个CSV数据文件做语料、7个TXT标注文件给标签、6个Python源文件覆盖数据预处理、特征工程、模型训练与集成预测最终把自由文本的主诉映射到科室。它解决的是多标签文本分类问题一段主诉可能同时命中消化内科与感染科所以要输出多个科室而非单一结果。适合正在做医疗文本挖掘、文本分类入门或多标签分类实战的人。不是竞赛Demo是能直接加载数据、跑通训练与评估的完整工程文件。2. 数据与标签先行从30个CSV到分类器能消费的训练集2.1 文件布局先把散落的CSV和标注文本对齐这个项目的数据组织方式和很多医疗文本项目类似CSV放原始样本、TXT放人工标注、Excel放辅助说明。我拿到这类资源的第一动作永远是先把文件清单理清而不是直接打开某个模型文件。项目里0.csv到29.csv这30个CSV是原始样本4-494.csv、4-503.csv这类带连字符的可能是按来源或批次切分的补充数据data_cat10_annotated_train.txt、data_cat10_annotated_test.txt、data_cat10_annotated_eval.txt则是带科室标签的标注集dict label2id.json负责科室名到数字id的映射。文件类型文件名示例在项目里的职责CSV样本0.csv、14.csv、4-494.csv原始分诊咨询文本及基础字段标注文本data_cat10_annotated_train.txt带科室标签的训练语料标签映射label2id.json、id_label.txt科室名称与数字id双向映射辅助说明explain.txt、domain2.xlsx字段含义与科室范围说明特征产物w2v.bin、tfidf、lda.*预训练好的词向量与主题模型看文件要先解决编码问题。医疗场景的CSV大概率是Excel导出的Excel存CSV时默认GBK或带BOM的UTF-8直接按纯UTF-8读很容易在第一条中文数据上就崩掉。我一般会写一个带编码回退的加载函数一次性把全部CSV读进来import glob import pandas as pd def read_csv_auto(path): for enc in (utf-8-sig, gbk, gb18030): try: return pd.read_csv(path, encodingenc) except UnicodeDecodeError: continue raise ValueError(f无法识别编码: {path}) csv_files sorted(glob.glob(*.csv)) print(f识别到 {len(csv_files)} 个CSV文件) frames [] for f in csv_files: df read_csv_auto(f) df.columns [c.strip() for c in df.columns] # 去掉列名里的空格 frames.append(df) data pd.concat(frames, ignore_indexTrue) print(f合并后总样本数: {len(data)})逻辑说明glob不用手动维护文件名单新增CSV也能被自动收进来utf-8-sig负责处理Excel的BOM头gb18030是GBK的超集能把一些生僻科室词也解出来。这里有个细节sorted按字符串排序时“14.csv”会排在“3.csv”前面如果样本本身有顺序要求建议改成按数字排序import re csv_files.sort(keylambda x: int(re.search(r\d, x).group()))2.2 标注文本解析科室标签不是单个而是列表data_cat10_annotated_train.txt这类文件是文本分类项目的核心资产。cat10说明标注集按10个科室类别组织每一行大概率是“主诉文本 一个或多个科室标签”的组合。多标签的设计思路在医疗分诊里很合理一位患者同时有胃痛和发热消化内科与感染科都可能被推到。我解析标注文本时会先打印前几行确认分隔符避免硬编码一个错误格式跑完整条流水线。with open(data_cat10_annotated_train.txt, encodingutf-8) as f: lines f.read().strip().split(\n) print(repr(lines[0])) # 先看原始格式再决定解析方式 rows [] for line in lines: parts line.strip().split(\t) # 常见为\t分隔可能是逗号或空格 if len(parts) 2: continue text parts[0] labels [lbl.strip() for lbl in parts[1].split(,) if lbl.strip()] rows.append((text, labels)) print(f有效训练样本: {len(rows)})参数说明split(\t)是首选的尝试方向因为主诉文本里大概率有逗号、句号但很少出现制表符如果某个标注文件实际用空格分隔用repr打印出来后立刻就能看出来把分隔符换掉即可。这段代码还过滤掉了len(parts) 2的坏行标注文件里偶尔有空行或缺失标签留着会在训练时报维度不匹配。2.3 标签分布检查先别急着训练看一眼类别天平分诊数据最后一定面临一个问题科室间样本量极度不均。感冒发烧的文本可能上万条职业病科或营养科可能只有几十条。如果在标签分布还没摸清的情况下直接塞进分类器少数科室会被完全淹没。我用一个简单Counter就能把分布打出来from collections import Counter all_labels [label for _, labels in rows for label in labels] counter Counter(all_labels) print(标签总数:, sum(counter.values())) print(类别数:, len(counter)) for label, cnt in counter.most_common(): print(f{label}: {cnt} ({cnt / sum(counter.values()) * 100:.2f}%))逻辑说明把全部样本的标签摊平统计能同时看出两个问题——哪些科室样本过少以及一个样本平均带几个标签。如果平均标签数明显大于1.5说明多标签情况很普遍单标签的softmax方案直接不用考虑。看到少数科室占比低于2%时后面训练BR分类器必须配class_weightbalanced这是后话但这一步的结论要用上。3. 特征工程三件套word2vec、TF-IDF与LDA在分诊场景的融合3.1 为什么分诊场景不能只用一种特征很多文本分类教程只教TF-IDF一把梭到分诊场景就翻车。原因很直接患者主诉平均只有二十来个字TF-IDF在这种超短文本上会变成极度稀疏的向量一个词对应一维大部分维度是0。但它又必须保留——科室关键词“吐血”“心悸”这类强信号词只有TF-IDF能直接命中。word2vec负责解决同义表达问题“拉肚子”和“腹泻”在字面上不共享词但在w2v空间里距离很近。问题在于w2v对词序不敏感“胃痛伴随发烧”和“发烧伴随胃痛”会得到几乎一样的向量。LDA主题模型则是第三路补充它能抓“这个文本整体在讨论哪个医学主题”相当于把短文本放进一个更大的语义上下文里。三路特征各有盲区拼接在一起才能覆盖短文本分类的主要失效模式。3.2 TF-IDF实现停用词表与n-gram是效果分水岭项目里带了stopwords.txt这说明原实现已经考虑了词频噪音。医疗文本里的“你好”“请问”“医生”这类词出现频率极高但分诊贡献为零必须过滤。分词环节中文场景我默认用jieba没有出现在项目清单里也不要紧这是分词工具不参与项目代码的学术成分。import jieba from sklearn.feature_extraction.text import TfidfVectorizer with open(stopwords.txt, encodingutf-8) as f: stopwords {w.strip() for w in f if w.strip()} def text_to_words(text): words jieba.lcut(text) return .join(w for w in words if w not in stopwords and len(w) 1) tfidf_vec TfidfVectorizer( token_patternr\S, # 因为分词结果已带空格按非空白切分即可 ngram_range(1, 2), # 保留“持续/发热”这类连续短语 max_features50000, sublinear_tfTrue, # 用1log(tf)平滑词频抑制高频词 ) X_tfidf tfidf_vec.fit_transform([text_to_words(t) for t in train_texts]) print(fTF-IDF特征维度: {X_tfidf.shape[1]})参数说明ngram_range(1, 2)对短文本提升很大因为“不退烧”切成bigram后比单独“不”“退烧”更有区分度sublinear_tfTrue是容易被忽略的一行它把所有词频取log防止“的”“了”这类残留词在长句里把数值顶得过高max_features50000在十类分诊任务里通常足够太大只会增加内存压力和过拟合风险。3.3 词向量与LDA读取预训练产物比重新训练更划算项目里已经有训练好的w2v.bin和lda系列文件这意味着不需要从头训练。我拿到这类资源时的原则是先试着加载再决定要不要重新训练。gensim加载w2v.bin的标准做法如下from gensim.models import KeyedVectors w2v KeyedVectors.load_word2vec_format(w2v.bin, binaryTrue, unicode_errorsignore) print(f词表大小: {len(w2v.key_to_index)}) for w in [咳嗽, 发热, 腹泻, 胸痛]: print(w, 在词表内 if w in w2v.key_to_index else 不在词表内)逻辑说明unicode_errorsignore是医疗文本场景必须加的患者输入的词里经常混着全角字符、表情符号不忽略会让加载直接报错。检查“胸痛”这类核心词是否在词表内也很关键如果不在说明w2v的训练语料覆盖不全后面做文本向量平均时会丢信息。LDA模型文件的加载比w2v麻烦因为gensim保存的是一组文件lda.model是主文件lda.id2word是词袋字典lda.expElogbeta.npy是主题词分布矩阵。加载时要把模型和字典都挂上from gensim.models import LdaModel from gensim.corpora import Dictionary lda_model LdaModel.load(lda.model) lda_dict Dictionary.load(lda.id2word) def lda_topic_vector(text, num_topics10): words text_to_words(text).split() bow lda_dict.doc2bow(words) # 词袋会自行忽略OOV词 vec [0.0] * num_topics for topic_id, prob in lda_model.get_document_topics(bow): vec[topic_id] prob return vec参数说明Dictionary.load必须和LdaModel.load配套使用单独加载其中一个都会在doc2bow时出错get_document_topics返回(id, prob)对转成定长向量后方便和TF-IDF、w2v特征直接numpy拼接。这里LDA主题数在项目里应该是10或附近恰好和cat10标注集对应主题数一旦和类别数差距过大主题特征就容易变成纯噪音。4. 多标签分诊模型为什么是Binary Relevance而不是softmax4.1 多标签问题的建模差异分诊任务里一个常见误区是把科室当成互斥类别直接拿softmax做十选一。真实场景中主诉“腹痛伴呕吐”可能同时命中消化内科和急诊科softmax的归一化迫使模型只挑一个概率最大的漏掉次优科室。项目文件里的clf_BR直接点明了建模方式——Binary Relevance翻译过来就是对每个标签单独训练一个二分类器预测时10个分类器各自独立输出0或1。BR的优点一是简单、每个子分类器都能用成熟的二分类策略二是可解释能单独看某个科室分类器的权重排查“模型为什么总把胸痛分给心内科”这类问题。缺点是忽略了标签相关性但分诊场景下科室间本来就不该过度关联BR的“缺点”在这里反而不太致命。4.2 BR分类器实现从训练到多标签评估我用MultiOutputClassifier把LogisticRegression包成BR模式。base estimator选LogisticRegression而不是SVM或树模型是因为高维稀疏特征下线性模型训练快、可解释性强还能配class_weight处理样本不平衡。from sklearn.multioutput import MultiOutputClassifier from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split X_all tfidf_vec.transform([text_to_words(t) for t in texts]) # 全量特征 y_all label_binarizer.transform(labels_list) X_tr, X_va, y_tr, y_va train_test_split( X_all, y_all, test_size0.2, random_state42, stratifylabel_primary ) base LogisticRegression( C1.0, max_iter800, class_weightbalanced, # 少数科室不直接判0 solverliblinear, ) clf_br MultiOutputClassifier(base, n_jobs-1) clf_br.fit(X_tr, y_tr) from sklearn.metrics import hamming_loss, f1_score y_pred clf_br.predict(X_va) print(Hamming Loss:, hamming_loss(y_va, y_pred)) print(Micro F1:, f1_score(y_va, y_pred, averagemicro))参数说明class_weightbalanced是这份代码里最关键的一行它让每个二分类器自动按样本占比给负样本降权否则少数科室的预测结果会全部是0solverliblinear适合几千到几万维的稀疏矩阵L-BFGS在高维稀疏场景容易收敛慢stratifylabel_primary保证切分时每个科室都出现在训练集和验证集里避免某个冷门科室恰好全落到验证集直接被判0。4.3 标签词向量embedding用语义相似度先筛候选科室w2v_label_embedding.pkl这个文件的作用我拆过之后才看懂它不是模型参数而是每个科室的标签词向量集合。用法是先算输入文本的词向量平均再和所有科室的标签向量做余弦相似度取top3到top5作为候选集最后只在候选集上跑BR分类。这样有两个好处减少预测时的计算量以及在样本极度不均衡时把少数科室“保护”起来别让它们在一开始就被全量分类器忽略。import pickle import numpy as np with open(w2v_label_embedding.pkl, rb) as f: label_emb pickle.load(f) # dict: {科室名: 词向量} def text_embedding(text): vecs [w2v[w] for w in text_to_words(text).split() if w in w2v.key_to_index] if not vecs: return np.zeros(w2v.vector_size) return np.mean(vecs, axis0) def top_k_labels(text, k5): t_vec text_embedding(text) scores {name: t_vec v / (np.linalg.norm(t_vec) * np.linalg.norm(v) 1e-8) for name, v in label_emb.items()} return sorted(scores, keyscores.get, reverseTrue)[:k]逻辑说明文本向量和标签向量都是w2v空间里的表示点积除以模长就是余弦相似度1e-8防止零向量除零报错。这里有个细节如果一个词不在w2v词表里会被直接跳过如果整句词都不在词表里就返回全零向量并在排序时被自然排到最后不会影响其他科室的分数。5. 避坑与排查分诊文本分类最常踩的5个坑5.1 CSV读出来全是乱码或直接报错现象pd.read_csv(0.csv)抛出UnicodeDecodeError或者读出来中文变成“鍝堝搱”这类乱码。原因Excel保存CSV时默认用了GBK编码而你按UTF-8去解码即使选了UTF-8Excel还会在文件头部加一个BOM标记。解决写一个带编码回退的read_csv_auto函数按utf-8-sig → gbk → gb18030的顺序逐个尝试。我见过不少项目因为这一步没做对直接在数据加载阶段就放弃了。5.2 少数科室的预测结果全是0现象BR分类器训练完验证集上部分科室标签的recall为0少数科室即使出现也永远不亮。原因样本分布严重倾斜少数科室占比可能不到2%LogisticRegression默认把一切都判为负类反而准确率更高但它牺牲了分诊最需要的召回能力。解决两个手段叠加。一是class_weightbalanced让少数科室的误判代价提高二是在预测时不直接用默认0.5阈值改成按预测概率排序取top1或top2。后者是把分类问题转成排序问题分诊场景下更实用。5.3 LDA加载后doc2bow报错或主题分布全是0现象LdaModel加载成功但调用get_document_topics时返回的主题概率全是0或者某些新词把doc2bow直接中断。原因lda.id2word是训练阶段保存的固定字典新文本里的词在字典里不存在时会被丢弃如果整个句子都是新词bow就变成空的主题分布自然全零。解决加载时必须Dictionary.load(lda.id2word)拿到原训练字典doc2bow本身就处理OOV。另外务必保留训练LDA时的preprocess函数分词和停用词规则不一致词表对不上主题特征就跟白噪声一样。5.4 标注集上F1很高真实数据上一塌糊涂现象在data_cat10_annotated_eval.txt上F1有0.85上线后真实分诊准确率掉到0.6。原因标注集是cat10模式按10个科室均衡采样而真实分诊流向是典型长尾分布呼吸内科和消化内科占了大头冷门科室样本极少。模型在均衡集上学到的阈值放到长尾分布上必然失配。解决收集一段时间的真实分诊日志按真实分布做评估以排序指标为主。预测阶段不再对所有科室用同一个0.5阈值而是按科室的历史先验概率动态调整。5.5 短文本的TF-IDF向量太稀疏分类器像在猜现象平均每个样本的非零特征维度不到50个分类器的决策边界非常不稳定换一个随机种子结果就差一大截。原因患者主诉只有十几个到二十几个字分词后有效token就那么几个TF-IDF向量往5万维空间里一放稀疏到不成样子。解决TF-IDF加上ngram_range(1, 2)甚至(1, 3)“持续发热”这类bigram能把短文本的有效特征翻倍。同时在特征拼接时把w2v平均向量和LDA主题向量一起拼进来向量的稠密部分正好弥补TF-IDF的稀疏。6. 预训练模型集成BERT、XLNet与ResNet在分诊里分别扛什么活6.1 三个模型的定位差异不是越大越复杂越好BERT负责抓住上下文里的精细语义“偶尔胸痛和剧烈胸痛”这种程度差异BERT通过注意力机制能感知到。XLNet在长文本和词序敏感的场景有优势它换了一种自回归的预训练方式对“先是腹痛后是发热”这类时序敏感的病历描述更友好。ResNet在文本里不是直接把患者照片喂进去而是把句子的词向量排列成二维特征图用残差卷积做一次深层特征提取再压成向量。三个模型从不同角度表征同一句主诉集成之后比任何单模型稳定。6.2 集成策略别把特征拼接当集成我第一次做这种融合时把三个模型的输出向量直接拼成一个长向量再训练分类器结果比单BERT还差。特征拼接让分类器面对一个高维混合空间没学过怎么对齐三个模型的语义尺度。后来改成先让每个模型独立给出预测概率再在概率层做加权平均import numpy as np bert_probs bert_predict(texts) # shape: (n, num_classes) xlnet_probs xlnet_predict(texts) resnet_probs resnet_predict(texts) weights np.array([0.5, 0.3, 0.2]) # 在验证集上搜索得到 final_probs (bert_probs * weights[0] xlnet_probs * weights[1] resnet_probs * weights[2]) pred_labels np.argsort(final_probs, axis1)[:, -2:] # 取概率最高的2个科室参数说明加权权重不要拍脑袋定死我会在验证集上跑一个简单网格搜索三个权重和为1步长0.1找到让micro F1最高的组合。常见结果就是BERT权重0.5左右、XLNet 0.3、ResNet 0.2不会有模型拿到0.7以上的权重——如果某一项占比异常高多半是其他两个模型的输入预处理有问题不是权重调的功劳。最后取top2而不是top1分诊场景本来就是多标签输出次优科室常常是患者实际需要的科室。那次踩完特征拼接的坑之后我每次做模型集成都强制先跑一遍单模型基线记录各自的F1和错误case再谈融合权重。希望帮到你。本文还有配套的精品资源点击获取
返回列表