ARTICLE DETAIL

资讯详情

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

肝病患者智能诊断:从ANN训练到Flask部署的完整实践

肝病患者智能诊断:从ANN训练到Flask部署的完整实践 简介面向机器学习初学者与医疗数据挖掘开发者这份资源围绕印度肝病患者数据集共583条记录其中肝病患者416例、非肝病患者167例含441名男性与142名女性展开完整实现了基于ANN模型的肝病智能诊断并配套Flask框架搭建的Web应用界面支持输入特征数据后实时输出预测结果。压缩包共114个文件约8.13MB核心内容包括83个csv数据文件原始/预处理数据、8个Python脚本模型训练、预测及Flask服务、1个pkl模型文件、HTML/CSS/JS前端页面以及相关配置文件目录结构清晰便于直接运行与二次开发。已有594人学习下载适合作为机器学习分类项目、医疗数据分析课设或入门Flask部署的参考案例。通过该包可同时掌握数据清洗、特征处理、ANN模型调参与Web系统集成的完整链路并可直接替换数据集迁移至其他疾病预测场景。1. 肝病患者智能诊断不是黑匣子一份能跑的 ANN 系统资源一份 583 条记录的小数据集配上两层隐藏层的 MLP再套一个 Flask 页面就能把肝病患者智能诊断从数据清洗一路做到浏览器实时预测这正是这份资源最实用的地方。它不是论文里的模型也不是只有 ipynb 的算法演示而是包含了印度肝病患者数据集、ANN 训练脚本、Web 界面静态文件bootstrap.min.css、cover.css、style.css的完整闭环。适合三类人做机器学习课程设计、需要算法 系统双交付物的学生在 sklearn 上练过手但没把模型封成 Web 服务的数据工程师想找一个真实医疗数据集走完二分类全流程的算法从业者。接下来我会按数据读取、模型训练、Flask 部署、常见雷区、上线验证的顺序拆开讲每步都给可复现代码和参数说明。2. 把印度肝病数据集读对列名拼写、缺失值与标签语义2.1 先核对列名这个数据集的特征命名是拼错的Indian Liver Patient DatasetILPD在 UCI 上挂了十几年特征是 10 个生化指标加一个标签。大部分教程直接pd.read_csv()就开跑结果列名带出一堆问题原始 CSV 里Total_Protiens、Alamine_Aminotransferase、Alkaline_Phosphotase本身就是拼写错误。你按规范拼写Total_Proteins去读isnull().sum()会告诉你整列全是缺失值这就是数据读取期最常见的翻车点。我处理这个数据集的第一步永远是打印columns.tolist()按实际列名建特征列表而不是凭记忆拼。先把资源里的原始文件列出来CSV 列名按原始文件含义成人参考范围处理说明Age年龄20-80 岁数值型直接入模Gender性别Male / Female映射为 1 / 0Total_Bilirubin总胆红素0.1-1.2 mg/dL数值型Direct_Bilirubin直接胆红素0.1-0.4 mg/dL数值型Alkaline_Phosphotase碱性磷酸酶44-147 U/L注意原始列名少个字母Alamine_Aminotransferase谷丙转氨酶7-56 U/L拼写与规范不同Aspartate_Aminotransferase谷草转氨酶10-40 U/L数值型Total_Protiens总蛋白6.3-8.2 g/dL原始拼写为 ProtiensAlbumin白蛋白3.5-5.0 g/dL数值型A/G Ratio白球比1.0-2.5由 Albumin 与球蛋白计算label是否患肝病1 / 0语义需确认见 2.2表格里加粗的两行是我每次都要强调的。另外资源里给了一批切分好的 CSVB_1.csv、N_42.csv、N_23.csv 这一串命名看不出规律行数也对应不上原始文件。我一般不用这些切分文件做训练只拿它们核对行数正规做法是从原始 CSV 自己走一遍切分逻辑后面每一步都心里有数。2.2 标签语义先看 value_counts再动映射这个数据集的标签有个历史包袱原始 Selector 列里1代表肝病2代表非肝病。部分版本已经重映射成1/0但如果你拿到的是原始版本直接拿 1 和 2 当分类目标灌进模型训练过程不会报错部署时语义却可能是反的。我写过一次教训很深的代码训练完准确率看着还行一查 AUC 只有 0.2就是标签翻转导致的。所以读入后的第一个动作不是我之前习惯的head()而是import pandas as pd import numpy as np # 读取时把常见缺失标记都收进来否则空格会被当成字符串 df pd.read_csv(Indian_Liver_Patient_Dataset.csv, na_values[, NA, ?, ]) # 第一件事核对列名和标签分布 print(df.columns.tolist()) print(df.shape) print(df[label].value_counts()) # 统一为肝病1正常0。若已经是0/1则map不会命中安全 df[label] df[label].map({2: 0, 1: 1}) # 性别编码 df[Gender] df[Gender].map({Male: 1, Female: 0})这段代码的逻辑很简单但顺序不能乱先打印value_counts()确认原始标签分布再决定要不要map。map({2: 0, 1: 1})只变换数值如果资源里的 label 列已经是 0/1这个映射不会误伤但反过来如果你假设它已经是 0/1 而实际是 1/2后面所有评估指标都会失真。Gender同理先确认值是Male/Female文本还是已经编码过的数字。2.3 缺失值处理与标准化这步直接决定模型上限ILPD 的缺失集中在Albumin和A/G Ratio583 条里大约 4 条左右比例很低。处理这类稀疏缺失我的选择是直接dropna()而不是均值填充。原因有两个样本量本身小4 条用均值填充相当于凭空捏造 4 条标准人数据部署时如果真实输入缺了白蛋白后台会直接报缺失不会走到模型这一层所以训练时把缺失行清掉更贴近真实使用场景。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 删除缺失行索引重置避免后面行号错位 df df.dropna().reset_index(dropTrue) feature_cols [c for c in df.columns if c ! label] X df[feature_cols] y df[label] # 先切分再fit标准化杜绝信息泄露 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) print(训练集:, X_train_scaled.shape, 测试集:, X_test_scaled.shape) print(缩放后均值:, round(X_train_scaled.mean(), 6))这里有一个新手经常做错的关键点标准化必须放在切分之后。scaler.fit_transform(X_train)只从训练集学习均值和标准差X_test只transform。如果先把全量X做了fit_transform再切分测试集的均值方差已经被模型看见了验证指标会虚高部署后复现必然打折扣。打印缩放后均值接近 0是为了确认标准化真的生效不是走个形式。我看到有人用 MLP 训这个数据集时 loss 完全不降最后发现就是忘了标准化特征量纲差了几十个数量级梯度更新跟抽风一样。资源里的数据文件本身行数不多切分时用stratifyy能保证训练集和测试集里的肝病/正常比例和原始数据一致这个参数在类别不平衡时比随机切分稳得多。这一节做完你已经拿到了可入模的X_train_scaled和X_test_scaled下一步就是搭 ANN。3. ANN 模型设计与训练从网络结构到评估指标3.1 为什么这个场景选 ANN 而不是逻辑回归或树模型逻辑回归在这个数据集上不是不能用但它默认特征对数是线性加权。肝病的生化指标之间明显有交互关系总胆红素偏高本身有意义但总胆红素 直接胆红素比值异常、转氨酶和碱性磷酸酶的组合模式往往比单指标更能区分肝细胞损伤还是胆道阻塞。逻辑回归要表达这些关系得手工构造交叉特征MLP 的隐藏层本质上就在内部学习这些非线性组合省去人工特征工程这一步。树模型随机森林、XGBoost也能处理非线性而且对特征尺度不敏感但这份资源的交付物明确是 ANN所以主线就是 MLP。我的建议是课程设计或系统演示场景用 MLP 更贴合题目要求如果是真实筛查场景可以训练一个 MLP 和一个树模型做对比取两者predict_proba的平均值。不过那是后话先把 MLP 跑通。选择 MLP 的另一个现实原因它输出的是平滑的概率值predict_proba的结果天然适合后面做阈值调整。逻辑回归也是平滑概率但表达能力上限摆在那里树模型的概率输出是叶子节点的离散频率调阈值时经常出现跳变。3.2 网络结构与超参小数据集别贪大583 条样本、10 个特征这个体量决定了网络规模的上限。我见过有人在课程设计里直接上一个三层 256 神经元的大 MLP结果训练集准确率 0.99测试集 0.6典型的过拟合。小数据集上正确的做法是把网络压到够用输入 10 维第一层 16 个神经元第二层 8 个输出 1 个。这个形状是在表达能力和泛化能力之间折中的结果16→8 的降维过程本身就在强制模型提炼特征。参数取值选型理由hidden_layer_sizes(16, 8)两层隐藏层逐层降维避免过拟合activationrelu默认首选梯度消失风险远小于 tanhalpha0.001L2 正则强度略微约束权重增长batch_size16小数据集适合小批量更新更稳定learning_rate_init0.001过大导致 loss 震荡过小收敛太慢max_iter500给足迭代上限实际会因早停提前结束early_stoppingTrue自动切 20% 训练数据做验证防过拟合n_iter_no_change20验证分数连续 20 次不涨就停random_state42固定随机种子保证结果可复现alpha0.001这个值不是拍脑袋而是对应 sklearn 里MLPClassifier的默认 L2 惩罚系数。如果你发现验证曲线锯齿状、训练分数高测试分数低优先把alpha往上调到 0.01而不是去加层。加层在这个数据集上是自杀行为583 条样本喂 10 个特征两层 168 已经是上限偏保守的水平。3.3 训练脚本与三类评估指标from sklearn.neural_network import MLPClassifier from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score model MLPClassifier( hidden_layer_sizes(16, 8), activationrelu, alpha0.001, batch_size16, learning_rate_init0.001, max_iter500, early_stoppingTrue, n_iter_no_change20, validation_fraction0.2, random_state42, ) model.fit(X_train_scaled, y_train) y_pred model.predict(X_test_scaled) y_proba model.predict_proba(X_test_scaled)[:, 1] print(classification_report(y_test, y_pred, target_names[正常, 肝病])) print(AUC:, round(roc_auc_score(y_test, y_proba), 4))训练这一段的逻辑不复杂但有两个细节值得展开。第一early_stoppingTrue时 sklearn 会自动从你传入的X_train_scaled里再切 20% 当作验证集n_iter_no_change20表示如果验证分数连续 20 次迭代没有改善训练提前结束。这个机制在小数据集上比死跑max_iter500稳妥得多能显著压制过拟合。第二评估不能只看accuracy这个数据集类别本身不平衡肝病约 416 条、正常 167 条如果模型把全部样本都预测成肝病准确率也能到 71% 左右看着还行实际毫无价值。所以我在上面代码里同时打印classification_report和roc_auc_score。classification_report里真正要关注的是肝病那一行的recall如果漏检太多说明模型把很多真实肝病患者当成了正常人这在医疗场景里是比误报更严重的问题。AUC 反映的是模型对正负样本的排序能力0.5 是瞎猜0.7-0.8 在这个数据集上是常见水平超过 0.85 就要怀疑是不是发生了标签泄露或者数据处理顺序错了。4. Flask 封装上线模型三件套、路由与表单对接4.1 模型三件套模型权重、缩放器、特征顺序一个都不能少训练完模型只是完成了三分之一。部署阶段最常翻车的不是模型本身而是特征顺序错位和重新 fit 了 StandardScaler。我的习惯是训练结束后立刻保存三个文件模型、缩放器、特征名称列表。特征名称列表看起来多余但它是 Flask 端构造 DataFrame 的唯一依据没有它你只能凭记忆手写字段顺序一旦训练时列的顺序调整过部署端必然错位。import joblib import json # 模型三件套权重、缩放器、特征顺序 joblib.dump(model, liver_model.joblib) joblib.dump(scaler, liver_scaler.joblib) feature_cols [c for c in df.columns if c ! label] with open(feature_names.json, w, encodingutf-8) as f: json.dump(feature_cols, f, ensure_asciiFalse, indent2) print(模型文件已保存, feature_cols)这三个文件在 Flask 项目里要放在同一层目录下。feature_names.json的内容就是feature_cols的列表比如[Age, Gender, Total_Bilirubin, ...]后面 Flask 端读取后按这个顺序从表单取值就能保证进入模型的特征和训练时完全一致。千万别把模型和缩放器放在 static 目录下那会被浏览器直接访问下载你辛苦训的参数就成公开资源了。4.2 Flask 路由设计表单取值与字段顺序保真Flask 端我一般只写两个路由GET /渲染输入表单POST /predict接收表单数据、拼装 DataFrame、调用模型、返回结果。核心代码就几十行真正的要点在如何保证字段顺序和如何处理非法输入。from flask import Flask, request, render_template import pandas as pd import joblib import json app Flask(__name__) # 启动时加载三件套避免每次请求都重新读文件 model joblib.load(liver_model.joblib) scaler joblib.load(liver_scaler.joblib) FEATURES json.load(open(feature_names.json, encodingutf-8)) app.route(/) def index(): return render_template(index.html) app.route(/predict, methods[POST]) def predict(): # 按FEATURES顺序从表单取值缺项直接拦下 values [request.form.get(f) for f in FEATURES] if any(v is None or v for v in values): return 请填完整所有指标, 400 # 构造一行DataFrame列顺序严格等于训练时的feature_cols data pd.DataFrame([values], columnsFEATURES) for col in data.columns: data[col] pd.to_numeric(data[col], errorscoerce) scaled scaler.transform(data) proba model.predict_proba(scaled)[0][1] pred int(proba 0.5) result 疑似肝病请尽快就医检查 if pred 1 else 暂未见明显肝病迹象 return render_template(result.html, resultresult, probaf{proba:.1%}) if __name__ __main__: app.run(debugFalse, port5000)这段代码的写法适合直接抄进项目里但有几处要解释清楚。values [request.form.get(f) for f in FEATURES]是整个部署里最关键的一行它按feature_names.json的顺序从表单取值而不是按前端页面里 input 标签的书写顺序。前端的字段可以随便排后端只认这个列表的顺序这也是为什么feature_names.json必须和训练时的feature_cols严格一致。pd.to_numeric(errorscoerce)是把空字符串或非数字内容变成 NaN这里不主动报错但后续scaler.transform会因为 NaN 抛异常属于故意留的防呆口子方便调试时看出哪个字段传错了。model.predict_proba(scaled)[0][1]取的是正类概率也就是患肝病的概率。int(proba 0.5)用 0.5 当硬阈值这个值在大多数分类任务里是默认选择但在医疗筛查场景未必最优最后一章我会讲怎么调它。4.3 前端模板与启动方式资源里已经带了bootstrap.min.css、cover.css和style.css说明前端是基于 Bootstrap cover 模板改的。你要做的不是重写样式而是把index.html里的 form 指对位置、把 input 的 name 和feature_names.json里的字段对应上form action/predict methodpost input typetext nameAge placeholder年龄 input typetext nameGender placeholder性别 1/0 !-- 其余字段同理name严格等于feature_names.json里的每一项 -- button typesubmit开始诊断/button /form这里最容易出的问题是性别字段。后端已经把它编码成 1/0前端就不要让用户填 Male/Female否则pd.to_numeric会把 Male 变成 NaN。正确做法是前端用下拉框选项就是 1 和 0或者显示文本男/女但提交时后端再映射一次。我习惯在前端直接放数字选择省一道转换。启动方式没有特殊之处命令行进项目目录执行python app.py浏览器打开http://127.0.0.1:5000就能看到页面。但要注意两件事一是debugFalse必须保持关闭debugTrue在公网环境下有代码执行风险二是app.run()是 Flask 开发服务器只适合课程设计或局域网演示正式上线要用waitress或gunicorn起服务这类部署细节本资源没有涉及看你自己环境定。5. 排错与避坑读取、训练、部署阶段五个高频雷区5.1 数据读取期的三个隐蔽雷雷区一标签语义反了模型表现好得离谱现象训练完打印classification_report准确率 0.85AUC 却只有 0.2 左右预测结果和真实标签完全反向。原因原始 ILPD 数据集的标签是 1肝病和 2非肝病部分资源文件重映射为 1/0部分没有。如果你假设它已经是 0/1直接拿 2 当正类训练模型学到的决策边界就是对的但含义全反了。解决训练前强制打印df[label].value_counts()对照数据集说明确认 1 和 2 各自的含义再做map({2: 0, 1: 1})。我后来养成的习惯是不仅打印分布还会打印pd.crosstab(df[label], df[Gender])从业务角度交叉验证标签是否合理——肝病组里男性占比明显高才是正常的如果分布和流行病学常识矛盾八成是标签映射错了。雷区二列名拼写错误整列读成 NaN现象df.isnull().sum()显示一大半特征全空或者df.columns有十几个列但大部分是Unnamed: N。原因ILPD 官方 CSV 的列名本身就是错拼的Total_Protiens、Alamine_Aminotransferase、Alkaline_Phosphotase你不能拿规范拼写去匹配。解决读文件后第一行就print(df.columns.tolist())复制实际列名到代码里。如果要在多个脚本间复用建议把列名统一改成规范拼写并同步更新feature_names.json否则后面 Flask 端会多次踩同一个坑。雷区三缺失值隐形用 info() 看不出来现象df.info()显示非空数量正常但特征里其实混入了空字符串或占位符。原因原始 CSV 里缺失单元格可能是纯空格而pd.read_csv()默认不会把空格识别为 NaN除非显式传na_values。解决读取时统一加na_values[, NA, ?, ]然后用df.isnull().sum()再确认一次。ILPD 的缺失集中在Albumin和A/G Ratio大约 4 条直接dropna()即可不要均值填充。5.2 训练期的翻车现场标准化顺序错了现象训练时 AUC 0.98自我感觉良好部署后用真实新数据复现性能掉到 0.75 左右。原因你在切分之前对全量数据做了scaler.fit_transform()测试集的信息已经参与了均值和标准差的计算。这种情况和用全量数据做 PCA 再切分是同一类错误属于数据泄露的温和版指标虚高但不算完全无效所以很多教程都不会报错。解决坚持先切分、后缩放的顺序scaler只fit训练集。还要记得把训练好的scaler用joblib存下来部署端直接加载不能在请求里现场重新 fit——线上每次请求只有一条数据重新 fit 出来的均值和方差和训练期完全不同预测结果会失真。5.3 部署期的顺序坑特征顺序在 Flask 端悄悄改变现象Flask 启动正常页面也能输入但预测结果和训练时对不上换成测试集里的典型样本也预测错误。原因前端表单字段顺序和后端pd.DataFrame构造顺序不一致。如果后端手写成[Age, Total_Bilirubin, ...]而feature_names.json里实际是[Age, Gender, ...]模型接收到的特征值和字段名就对不上预测自然全错。解决Flask 端不手写特征顺序一律从feature_names.json读取FEATURESDataFrame构造和表单取值都基于这个列表。我当年在这上面吃过亏训练时重命名了列忘了同步部署脚本用户填了一整页数据模型拿到的却是错位特征最后花了一下午对比列名才发现问题。6. 上线前加一道保险混淆矩阵与阈值扫描6.1 先看混淆矩阵的四象限而不是准确率训练完不要急着保存模型先打印混淆矩阵。四象限里最要命的是右下角的 FN漏检也就是真实肝病患者被预测成了正常。在这个数据集场景下漏检的代价远高于误报漏检意味着患者错过干预窗口误报顶多让人多跑一趟医院复查。所以评估模型时我优先看肝病这一类的recall要求至少 0.85再回头看误报率能不能接受。from sklearn.metrics import confusion_matrix tn, fp, fn, tp confusion_matrix(y_test, y_pred).ravel() print(f真阴{tn} 假阳{fp} 假阴{fn} 真阳{tp}) print(f肝病召回率{tp/(tpfn):.3f})6.2 阈值扫描把默认 0.5 换成临床可接受的数MLP 的predict_proba输出的是连续概率默认 0.5 只是正负各一半的对称选择不代表它是医疗场景的最优解。肝病筛查这个场景下宁可让模型敏感一些也不能放过真实患者所以要调低阈值比如 0.35 或 0.4。做法很简单扫描候选阈值观察召回率和误报率的变化import numpy as np for t in np.arange(0.30, 0.66, 0.05): pred_t (y_proba t).astype(int) tn, fp, fn, tp confusion_matrix(y_test, pred_t).ravel() print(f阈值{t:.2f} 召回率{tp/(tpfn):.3f} 误报率{fp/(fptn):.3f})实际扫描时你会发现阈值压到 0.35 左右召回率明显上升误报率只增加几个百分点压到 0.30 以下召回率提升有限误报率却开始失控。我一般选召回率不低于 0.9 且误报率尽量低的那个阈值把选定的值替换进 Flask 代码里pred int(proba 阈值)这一行。这个习惯是血泪换来的。之前接过一个糖尿病风险模型AUC 做到 0.97所有人都觉得稳了结果真实使用时被医生反馈漏了好几个。后来排查发现全流程没人调过阈值0.5 的默认值把概率在 0.4 附近的患者全判成了阴性。从那以后我每次训练完模型都强制跑一遍混淆矩阵加阈值扫描阈值选好之前绝不动joblib.dump。这套流程也建议你加到自己的交付清单里模型权重、scaler、特征顺序、选定阈值四样东西齐了再谈上线。希望帮到你。本文还有配套的精品资源点击获取
返回列表