简介:面向机器学习初学者与医疗数据挖掘开发者,这份资源围绕印度肝病患者数据集(共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 Dataset(ILPD)在 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 / 0 |
| Total_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 | 原始拼写为 Protiens |
| Albumin | 白蛋白 | 3.5-5.0 g/dL | 数值型 |
| A/G Ratio | 白球比 | 1.0-2.5 | 由 Albumin 与球蛋白计算 |
| label | 是否患肝病 | 1 / 0 | 语义需确认,见 2.2 |
表格里加粗的两行是我每次都要强调的。另外资源里给了一批切分好的 CSV(B_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 Ratio,583 条里大约 4 条左右,比例很低。处理这类稀疏缺失,我的选择是直接dropna(),而不是均值填充。原因有两个:样本量本身小,4 条用均值填充相当于凭空捏造 4 条"标准人"数据;部署时如果真实输入缺了白蛋白,后台会直接报缺失,不会走到模型这一层,所以训练时把缺失行清掉更贴近真实使用场景。
from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 删除缺失行,索引重置避免后面行号错位 df = df.dropna().reset_index(drop=True) 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_size=0.2, stratify=y, random_state=42 ) 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 完全不降,最后发现就是忘了标准化,特征量纲差了几十个数量级,梯度更新跟抽风一样。
资源里的数据文件本身行数不多,切分时用stratify=y能保证训练集和测试集里的肝病/正常比例和原始数据一致,这个参数在类别不平衡时比随机切分稳得多。这一节做完,你已经拿到了可入模的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) | 两层隐藏层,逐层降维,避免过拟合 |
| activation | relu | 默认首选,梯度消失风险远小于 tanh |
| alpha | 0.001 | L2 正则强度,略微约束权重增长 |
| batch_size | 16 | 小数据集适合小批量,更新更稳定 |
| learning_rate_init | 0.001 | 过大导致 loss 震荡,过小收敛太慢 |
| max_iter | 500 | 给足迭代上限,实际会因早停提前结束 |
| early_stopping | True | 自动切 20% 训练数据做验证,防过拟合 |
| n_iter_no_change | 20 | 验证分数连续 20 次不涨就停 |
| random_state | 42 | 固定随机种子,保证结果可复现 |
alpha=0.001这个值不是拍脑袋,而是对应 sklearn 里MLPClassifier的默认 L2 惩罚系数。如果你发现验证曲线锯齿状、训练分数高测试分数低,优先把alpha往上调到 0.01,而不是去加层。加层在这个数据集上是自杀行为,583 条样本喂 10 个特征,两层 16+8 已经是上限偏保守的水平。
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), activation="relu", alpha=0.001, batch_size=16, learning_rate_init=0.001, max_iter=500, early_stopping=True, n_iter_no_change=20, validation_fraction=0.2, random_state=42, ) 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_stopping=True时 sklearn 会自动从你传入的X_train_scaled里再切 20% 当作验证集,n_iter_no_change=20表示如果验证分数连续 20 次迭代没有改善,训练提前结束。这个机制在小数据集上比死跑max_iter=500稳妥得多,能显著压制过拟合。第二,评估不能只看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", encoding="utf-8") as f: json.dump(feature_cols, f, ensure_ascii=False, indent=2) 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", encoding="utf-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], columns=FEATURES) for col in data.columns: data[col] = pd.to_numeric(data[col], errors="coerce") 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", result=result, proba=f"{proba:.1%}") if __name__ == "__main__": app.run(debug=False, port=5000)这段代码的写法适合直接抄进项目里,但有几处要解释清楚。values = [request.form.get(f) for f in FEATURES]是整个部署里最关键的一行:它按feature_names.json的顺序从表单取值,而不是按前端页面里 input 标签的书写顺序。前端的字段可以随便排,后端只认这个列表的顺序,这也是为什么feature_names.json必须和训练时的feature_cols严格一致。pd.to_numeric(errors="coerce")是把空字符串或非数字内容变成 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" method="post"> <input type="text" name="Age" placeholder="年龄"> <input type="text" name="Gender" placeholder="性别 1/0"> <!-- 其余字段同理,name严格等于feature_names.json里的每一项 --> <button type="submit">开始诊断</button> </form>这里最容易出的问题是性别字段。后端已经把它编码成 1/0,前端就不要让用户填 Male/Female,否则pd.to_numeric会把 "Male" 变成 NaN。正确做法是前端用下拉框,选项就是 1 和 0,或者显示文本"男/女"但提交时后端再映射一次。我习惯在前端直接放数字选择,省一道转换。
启动方式没有特殊之处,命令行进项目目录执行python app.py,浏览器打开http://127.0.0.1:5000就能看到页面。但要注意两件事:一是debug=False必须保持关闭,debug=True在公网环境下有代码执行风险;二是app.run()是 Flask 开发服务器,只适合课程设计或局域网演示,正式上线要用waitress或gunicorn起服务,这类部署细节本资源没有涉及,看你自己环境定。
5. 排错与避坑:读取、训练、部署阶段五个高频雷区
5.1 数据读取期的三个隐蔽雷
雷区一:标签语义反了,模型表现"好"得离谱
现象:训练完打印classification_report,准确率 0.85,AUC 却只有 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读取FEATURES,DataFrame构造和表单取值都基于这个列表。我当年在这上面吃过亏:训练时重命名了列,忘了同步部署脚本,用户填了一整页数据,模型拿到的却是错位特征,最后花了一下午对比列名才发现问题。
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/(tp+fn):.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/(tp+fn):.3f} 误报率={fp/(fp+tn):.3f}")实际扫描时你会发现阈值压到 0.35 左右,召回率明显上升,误报率只增加几个百分点;压到 0.30 以下,召回率提升有限,误报率却开始失控。我一般选"召回率不低于 0.9 且误报率尽量低"的那个阈值,把选定的值替换进 Flask 代码里pred = int(proba >= 阈值)这一行。
这个习惯是血泪换来的。之前接过一个糖尿病风险模型,AUC 做到 0.97,所有人都觉得稳了,结果真实使用时被医生反馈"漏了好几个"。后来排查发现全流程没人调过阈值,0.5 的默认值把概率在 0.4 附近的患者全判成了阴性。从那以后,我每次训练完模型都强制跑一遍混淆矩阵加阈值扫描,阈值选好之前绝不动joblib.dump。这套流程也建议你加到自己的交付清单里:模型权重、scaler、特征顺序、选定阈值,四样东西齐了再谈上线。希望帮到你。
本文还有配套的精品资源,点击获取