简介:面向南京邮电大学通达学院《机器学习》课程期末大作业的完成需求,这份文档给出了一套以医疗保健为背景的完整系统设计报告,选题聚焦膀胱癌的机器学习辅助诊断,适合正在准备课程设计、期末论文或同类数据驱动项目的高校学生参考。压缩包内仅含1个docx文件,大小约29KB,正文围绕数据收集、数据处理与实验评估三个环节展开,并梳理影像学、尿细胞学及预后预测等应用脉络,同时给出不少于1500字的报告写作框架。已有1024人学习下载。读者可从中获得选题切入、章节组织、特征工程与模型选择思路,以及交叉验证和准确率、召回率、F1分数等评估指标的组织方式,便于快速搭建课程作业骨架并对照完善自己的设计报告。
1. 从一份被打回的初稿说起:机器学习大作业为什么要按系统来设计
「机器学习大作业」最常见的一种翻车方式,是把它写成一篇文献综述:堆十几篇论文,讲 ANN 在 MRI 上把膀胱癌诊断准确率做到 95%,讲尿细胞学模型在测试组做到 94.4%,交上去被一句「你的系统在哪」打回。以膀胱癌辅助诊断这个题目为例,真正能拿分的不是复述别人的数字,而是把数据收集、数据处理、实验评估这三步做成一条能跑通的链路:哪些字段进表、特征怎么提、模型怎么选、指标怎么算、结果表怎么排。这套结构适合正在做机器学习课程设计的学生,也适合需要在医疗保健场景交付一份数据驱动系统方案的从业者,下面按可复现的顺序拆开讲。
2. 总体设计与技术选型:三步链路与四个候选模型
2.1 系统边界:辅助分诊,不替代病理
先把边界划清楚,后面的选型和指标才有依据。膀胱癌确诊的金标准是膀胱镜下取活体组织做病理检查,这个手段有创、有不适感,不适合做成定期筛查项目;影像学检查无创、费用低,但受成像原理限制,噪声斑点大、信噪比低,容易误诊漏诊;尿脱落细胞学检查虽然无创,结果却和检验师的经验水平强相关。三类检查各有短板,这正是机器学习能插进去的位置。
系统定位就一句话:在无创检查这一环做风险分层,输出两样东西,一是浸润风险概率 p,二是按阈值给出的二分类结果(<T2 非肌层浸润 / ≥T2 肌层浸润)。概率用于排序和随访优先级,二分类用于辅助用药与治疗方案讨论,最终诊断仍然回到病理。边界划清之后,很多设计细节就自动确定了:输出必须是概率而不是硬标签,模型必须能给出特征层面的解释,指标必须优先看敏感度而不是准确率。
这一点在代码里怎么落地,我一般会强制要求模型接口返回predict_proba,并在报告里附一张特征贡献图。医生不会因为 AUC 是 0.92 就信任一个黑匣子,但如果能指着图说「这个病例被判为高危,主要是因为核浆比和 GLCM 对比度同时偏高」,接受度完全是两回事。
2.2 四个候选模型与选型依据
候选模型不是越多越好,而是要和样本规模匹配。这类课题的真实数据量通常在几十到几百例之间,特征维度却在几十到上百,属于典型的小样本高维场景。
| 模型 | 适用样本规模 | 在本场景中的角色 | 主要风险 |
|---|---|---|---|
| 逻辑回归 LR | 100 至 1000 | 基线模型,系数方向可解释 | 非线性表达能力弱 |
| 支持向量机 SVM | 100 至 1000 | 小样本高维下表现稳定 | 核函数与惩罚因子调不好会过拟合 |
| 随机森林 RF | 200 以上 | 抗噪声、可输出特征重要性 | 无关特征多时重要性被稀释 |
| 多层感知机 MLP | 500 以上 | 拟合特征间的非线性交互 | 参数多、易过拟合、解释性差 |
不上卷积网络的理由要写进报告,这是答辩必问题。样本量几百例、阳性样本可能只有几十例,深度网络的参数量远大于样本量,训练出来的东西在验证集上好看、换一批数据就崩。更现实的一层是,临床侧对「高级语义特征」的接受度本来就有限,一个连特征方向都说不清的模型,很难进入实际讨论。原文里提到特征与医生描述肿瘤的语义差距较大、难以被理解和接受,这条限制在选型阶段就该体现出来,而不是等到写局限性时才补一句。
我的做法是把 LR 和 SVM 作为主力,RF 用来交叉验证特征重要性是否和 LR 系数方向一致,MLP 只在样本量过线时才加进来,并且必须配合早停和正则化。四个模型跑同一套划分、同一套特征,结果才有可比性。
2.3 三步链路的交付物与数据流
整套系统按四层组织,用文字描述比画图更省事:数据层放原始影像、检验单、测序或 qPCR 结果;特征层把每个病例压成一行,存成病例级特征表;模型层是预处理加分类器组成的 Pipeline;评估层负责划分、指标和结果表。层与层之间只通过表结构传递,不直接耦合文件路径,这样换一批数据只要重跑 ETL。
| 步骤 | 输入 | 输出交付物 | 验收口径 |
|---|---|---|---|
| 数据收集 | 影像、检验单、基因检测结果 | 病例主表、数据字典 | 关键字段缺失率低于 10%,标签与病理报告一致 |
| 数据处理 | 病例主表 | 特征宽表、可复现的 Pipeline | 同一份原始数据重跑结果一致,无信息泄漏 |
| 实验评估 | 特征宽表 | 指标表、ROC 曲线、消融结果 | 分层交叉验证加独立测试集,口径统一 |
三步里最容易糊弄也最容易翻车的是第一步。很多作业的数据字典只有字段名,没有取值范围、缺失编码和单位,等到建模时才发现「分期」一列里同时有T1、t1、T1期三种写法。数据字典里必须写清楚每个字段的口径、缺失怎么表示、什么时候采集,这份东西在答辩时的说服力比模型本身还大。
3. 数据收集与特征工程:从原始影像到病例级特征表
3.1 四类数据源与最小字段集
数据源按模态分四类,每类的处理方式不同,但最终都要汇到同一个病例编号上。影像走特征提取,检验和基因走数值化,临床字段直接入表。原则是宁可字段少而全,不要多而缺,一个缺失率 40% 的字段在建模时基本只能丢掉,前期收集时却可能花了不少工夫。
| 特征类别 | 具体特征 | 大致维度 | 说明 |
|---|---|---|---|
| 一阶统计 | 均值、方差、偏度、峰度、百分位数 | 8 | 反映 ROI 整体信号分布 |
| 纹理 | GLCM 对比度、同质性、能量、相关性 | 4 至 16 | 反映结构粗糙程度与异质性 |
| 形状 | 面积、周长、圆度、紧致度 | 4 | 反映病灶边界的规则性 |
| 细胞形态学 | 核面积、核浆比、染色质密度、细胞圆度 | 6 至 10 | 尿脱落细胞计量学指标 |
| 基因表达 | 面板基因归一化表达量 | 5 至 20 | qPCR 的 ΔCt 或芯片 z 值 |
| 临床结构化 | 年龄、性别、吸烟史、肿瘤大小、数目、分级 | 6 至 8 | 直接入表,缺失需标注 |
影像这块要区分增强序列和平扫,同一病例的多期影像不能混在一起算均值。细胞形态学指标的提取依赖分割质量,分割不稳的时候这些特征反而会引入噪声,所以我在特征表里会额外存一列seg_quality标记分割是否可靠,建模时先做一次过滤。
3.2 标注口径、纳入排除与病人级聚合
标签的定义必须一句话说死:以术后病理报告为准,t_stage中 T2 及以上记为 1,T1 及以下记为 0。不能用影像报告的分期当标签,那是医生的判断,用它训练等于让模型学医生的结论而不是学病理事实。标签来源要写进数据字典,评审时第一眼就看这个。
纳入排除标准同样要提前定。既往做过膀胱根治性切除的病例、合并其他恶性肿瘤的病例、影像缺失超过 30% 的病例、病理报告缺失的病例,一律排除。排除数量要记录,答辩时被问到「为什么最后只剩 187 例」能立刻答出来,比现场回忆强得多。这类课题的数据量本来就紧张,每排除一例都要有理由。
病人级聚合是最容易被忽略的一步。同一个病人可能有多期影像、多次入院记录,这些如果当成独立样本,就会造成同一个人同时出现在训练集和测试集里,指标虚高得离谱。我一般按病人编号聚合:连续型特征取中位数,极值类特征(比如最大肿瘤径)取最大值,分类字段取术前最近一次记录。聚合后的表一行对应一个病人,主键就是脱敏后的病人编号。
3.3 病例主表与特征长表的结构
存储上分两张表:主表存临床字段和标签,特征表用长表结构存,一行一个「病例-特征名-特征值」。长表的好处很实际,后续新增特征只要追加行,不用改表结构,ETL 脚本也不用动。
-- 病例主表:一行一个病人,主键为脱敏编号 CREATE TABLE case_master ( patient_id VARCHAR(16) PRIMARY KEY, -- 脱敏编号,不可反查 age SMALLINT, -- 年龄,缺失存 NULL 不存 0 sex TINYINT, -- 1=男 0=女 smoke TINYINT, -- 吸烟史 1=有 0=无 tumor_size_cm DECIMAL(4,1), -- 最大肿瘤径,单位厘米 tumor_count SMALLINT, -- 肿瘤数目 cis TINYINT, -- 原位癌 1=是 0=否 grade TINYINT, -- 分级 1=低级别 2=高级别 t_stage VARCHAR(8), -- 保留原始写法,便于人工核对 label_stage TINYINT NOT NULL, -- 标签:1=肌层浸润(>=T2) 0=非肌层浸润 dataset_ver VARCHAR(8) -- 数据版本号,用于结果复现 ); -- 特征长表:一行一个特征值,加特征不用改表结构 CREATE TABLE feat_long ( patient_id VARCHAR(16) NOT NULL, feat_name VARCHAR(64) NOT NULL, -- 如 glcm_contrast、ncr_ratio feat_value DOUBLE, -- 统一转为数值型 src_modality VARCHAR(8), -- MR / CT / URINE / GENE seg_quality TINYINT, -- 影像特征的提取质量标记 0/1 PRIMARY KEY (patient_id, feat_name) );label_stage设 NOT NULL,是为了让任何缺标签的样本在入库时直接失败,不流入下游。dataset_ver看着多余,实际很有用,改过特征提取参数之后如果结果变化,能立刻定位是哪一版数据。长表在导出宽表时用一次透视,pivot之后列名就是feat_name,和模型输入的列顺序对得上。
3.4 影像与细胞形态的特征提取
影像特征分两步:先把 ROI 归一化,再算纹理。归一化的方式是先把灰度值线性拉伸到 [0,1],再做灰度量化,量化级数取 32 而不是 256,这是小样本场景下的一个实用取舍,级数太多会让共生矩阵过于稀疏,特征方差大。
import numpy as np from skimage.feature import graycomatrix, graycoprops def extract_texture(roi, levels=32): """从已归一化的 ROI 中提取 GLCM 纹理特征 roi: 二维数组,取值已线性拉伸到 [0,1] levels: 灰度量化级数,样本量小时 32 级比 256 级更稳 """ quant = (roi * (levels - 1)).astype(np.uint8) # 四个方向、距离取 1,先算后取均值,避免特征维度爆炸 glcm = graycomatrix( quant, distances=[1], angles=[0, np.pi / 4, np.pi / 2, 3 * np.pi / 4], levels=levels, symmetric=True, normed=True, ) return { "glcm_contrast": float(graycoprops(glcm, "contrast").mean()), "glcm_homogeneity": float(graycoprops(glcm, "homogeneity").mean()), "glcm_energy": float(graycoprops(glcm, "energy").mean()), "glcm_corr": float(graycoprops(glcm, "correlation").mean()), }distances只取 1 是控制维度的结果,多距离会让特征数翻几倍,样本少的时候直接导致过拟合。symmetric=True让共生矩阵对称,normed=True做归一化,这两个参数不设,不同尺寸的 ROI 之间特征就没有可比性。
细胞形态学的特征走另一套逻辑,分割出细胞核之后算面积、周长、圆度、核浆比,染色质密度用核内灰度的一阶统计量近似。圆度用4π·面积/周长²,值越接近 1 越圆,恶性肿瘤细胞常见形态不规则,这个特征在文献里反复出现是有道理的。分割质量不行的样本,seg_quality标 0,建模时先剔除再补。
4. 模型训练与实验评估:四个分类器的训练链路与指标口径
4.1 防泄漏的预处理管道
预处理的每一步都必须能被 Pipeline 包住,这是整套方案里我最强调的一条。缺失值填补、标准化、特征筛选,任何一个步骤如果在全量数据上先做一遍再划分,测试集的信息就已经渗进训练过程,指标会虚高,而且虚高得毫无规律。
from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler from sklearn.feature_selection import SelectKBest, f_classif from sklearn.svm import SVC def build_pipe(clf, k_best=20): """组装预处理加分类器的完整管道 clf: 分类器实例 k_best: 保留的特征数,样本少于 200 时建议 15 至 25 """ return Pipeline([ ("impute", SimpleImputer(strategy="median")), # 中位数比均值抗异常值 ("scale", StandardScaler()), # 在折内 fit,杜绝泄漏 ("select", SelectKBest(f_classif, k=k_best)), # 单变量筛选,快且可解释 ("clf", clf), ])SimpleImputer用中位数而不是均值,是因为影像特征里存在少量极端值,均值会被拉偏。SelectKBest的 k 需要调,样本量 100 出头时 k 取 15 到 25 之间比较稳,取太大等于没筛,取太小会丢掉有效信号。这套管道在交叉验证里是逐折 fit 的,这一点决定了指标是不是可信。
4.2 四个分类器的训练与超参搜索
四个模型跑同一套划分、同一套管道,只有分类器部分不同。超参搜索用网格搜索,配合前面按病人分组的交叉验证器,保证同一个病人不会同时出现在训练折和验证折。
import numpy as np from sklearn.model_selection import StratifiedGroupKFold, GridSearchCV from sklearn.linear_model import LogisticRegression from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier from sklearn.neural_network import MLPClassifier from sklearn.pipeline import Pipeline X = feat_wide.drop(columns=["patient_id", "label_stage"]) y = feat_wide["label_stage"].values groups = feat_wide["patient_id"].values # 同一病人绝不跨折 cv = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=42) # 每个模型配一套网格,网格范围按样本量收窄,别铺太大 grids = { "lr": ( LogisticRegression(max_iter=2000, class_weight="balanced"), {"clf__C": [0.01, 0.1, 1.0, 10.0]}, ), "svm": ( SVC(kernel="rbf", probability=True, class_weight="balanced"), {"clf__C": [0.1, 1.0, 10.0], "clf__gamma": ["scale", 0.01, 0.1]}, ), "rf": ( RandomForestClassifier(n_estimators=500, class_weight="balanced", random_state=42), {"clf__max_depth": [3, 5, None], "clf__min_samples_leaf": [1, 3, 5]}, ), "mlp": ( MLPClassifier(max_iter=3000, early_stopping=True, random_state=42), {"clf__hidden_layer_sizes": [(16,), (32, 16)], "clf__alpha": [1e-4, 1e-3]}, ), } best_models = {} for name, (clf, grid) in grids.items(): pipe = build_pipe(clf) # 每个模型独立重建管道 gs = GridSearchCV(pipe, grid, cv=cv, scoring="roc_auc", n_jobs=-1) gs.fit(X, y, groups=groups) best_models[name] = gs.best_estimator_class_weight="balanced"在四个模型里都设了,因为阳性比例通常只有 15% 到 30%,不设的话模型会往多数类偏。scoring选roc_auc而不是准确率,是因为准确率在类别不平衡时几乎没有区分度。网格范围刻意收窄,C最大只到 10,gamma只给三个候选,样本量小的时候大范围搜索只会搜到过拟合的那组参数。
RF 的max_depth限制在 3 到 5,是为了让树的复杂度匹配样本量。MLP 的early_stopping=True是必须的,不设的话训练损失一路下降、验证损失早就反弹了,最后拿到的是一个把训练集背下来的模型。
4.3 划分方案与指标口径
划分按 6:2:2 走:60% 训练、20% 验证调参、20% 独立测试。独立测试集在调参阶段完全不碰,只在最后跑一次。这个纪律听起来简单,实际执行时最容易破,尤其是结果不好看的时候忍不住回去调两下。
指标不能只报准确率。这套系统面向筛查场景,漏诊一个肌层浸润病例的代价远大于误诊一个,所以敏感度是首要指标,特异度次之,然后才是 AUC 和 F1。四个指标要同时报,不能挑好看的写。
| 指标 | 计算口径 | 在本场景的意义 |
|---|---|---|
| 敏感度 | TP / (TP + FN) | 真正浸润病例被检出的比例,优先级最高 |
| 特异度 | TN / (TN + FP) | 非浸润病例不被误判的比例 |
| AUC | ROC 曲线下面积 | 与阈值无关的整体区分能力 |
| F1 | 2PR / (P + R) | 不平衡数据下的综合参考 |
| Brier | 预测概率与真实标签的均方误差 | 概率输出是否可校准,用于风险排序 |
阈值不用默认的 0.5,而是在验证集上按约登指数(敏感度减特异度最大)选一个,锁定之后再到测试集上跑。这个阈值本身也是交付物的一部分,要写进报告,不能只在代码里藏着。
4.4 结果表模板与消融实验设计
结果表按「模型 × 指标」排,一行一个模型,指标列固定顺序,最后一列写选定的阈值。表下面附 ROC 曲线,四条线画一张图,比四张分开的图更容易比较。
| 模型 | 敏感度 | 特异度 | AUC | F1 | 阈值 |
|---|---|---|---|---|---|
| LR | 待填 | 待填 | 待填 | 待填 | 待填 |
| SVM | 待填 | 待填 | 待填 | 待填 | 待填 |
| RF | 待填 | 待填 | 待填 | 待填 | 待填 |
| MLP | 待填 | 待填 | 待填 | 待填 | 待填 |
消融实验至少做三组,这是把方案讲深的关键。第一组比单模态和多模态,影像特征单独跑一次、临床字段单独跑一次、融合跑一次,证明多源数据确实有增益而不是凑数。第二组比有无特征筛选,去掉SelectKBest看指标怎么变,验证高维下的过拟合风险。第三组比阈值策略,0.5 和约登阈值各跑一次,说明为什么不用默认值。三组结果加起来的篇幅不大,但能回答评审八成的追问。
5. 避坑与排查:五个让准确率虚高的常见问题
5.1 数据泄漏类问题
现象:交叉验证里 AUC 稳定在 0.95 以上,换一个独立测试集直接掉到 0.7 附近,同一套代码同一批特征,结果差出一大截。
原因:预处理步骤在全量数据上先跑了一遍。标准化的均值和方差、特征筛选的 F 值,都是拿包含测试集的数据算出来的,测试集的分布信息已经渗进训练过程。另一种更隐蔽的情况是同一个病人有多条记录,被随机划分拆到了训练和测试两侧,模型其实在识别病人而不是识别疾病。
解决:所有预处理步骤塞进 Pipeline,交给交叉验证器逐折 fit。划分用StratifiedGroupKFold并传groups=patient_id,保证病人级隔离。检查方法很直接,把fit的调用位置打印出来,凡是出现在划分之前的预处理都要挪进去。
5.2 标签与分布类问题
现象:模型在测试集上的准确率有 0.83,看着不错,但混淆矩阵里 TN 占满,TP 是 0,敏感度为零。换个说法就是模型学会了永远预测「非浸润」。
原因:阳性样本占比低,损失函数被多数类主导,模型退化成常数预测器。这个问题在医学数据里几乎是默认存在的,不做处理就一定遇到。
解决:三层一起上。训练时设class_weight="balanced";重采样只用在上采样阶段,且必须放在训练折内部,绝不能在全量数据上做 SMOTE;阈值不用 0.5,按约登指数在验证集上选。三步做完再看混淆矩阵,敏感度应该能回到可用区间。
现象:训练集 AUC 是 1.0,验证集 0.6,特征数一百多,样本数不到两百。
原因:特征维度接近甚至超过样本量,模型有足够自由度把训练集完全记住。树模型在这种情况下尤其明显,深度不限制的话每片叶子可以只装一个样本。
解决:先降维再训练。SelectKBest把特征压到 20 以内,RF 的max_depth限制在 3 到 5,MLP 的alpha调大做 L2 正则,SVM 的C往下压。如果还想再稳一点,用嵌套交叉验证,把调参也放进内层循环,避免外层指标被调参过程污染。
5.3 指标口径类问题
现象:报告里写的敏感度 0.9,答辩时被问了一句「你算的是阳性检出率还是阴性排除率」,现场翻代码才发现分子分母写反了。
原因:敏感度和特异度的定义在不同文献里偶有混用,加上阳性标签定义为 1 还是 0 因项目而异,confusion_matrix输出的矩阵方向如果不核对,很容易错位。
解决:固定标签定义并在报告首页写明「1 表示肌层浸润」,混淆矩阵画出来之后手工核对一遍四个格子。指标统一用sklearn.metrics算,不手写公式。另外,测试集只跑一次,反复在测试集上调参等于把测试集变成了验证集,最后报出来的数字没有任何意义。
6. 验收与答辩:一页三步图加一张指标表的讲法
答辩时间通常只有八到十分钟,讲不完所有细节,能讲清楚的是三件事:系统由哪几层组成、每一步的输入输出是什么、指标是在什么口径下算出来的。我一般会准备一页图,四个横向色块从左到右排:数据层写四类数据源和病例数,特征层写特征维度和筛选后的维度,模型层写四个模型和选定的那个,评估层写划分方式和阈值。图下面压一行小字标注数据版本号,被问到结果可复现时直接指这行。
指标表放在第二页,就是前面那张「模型 × 指标」的表,四行五列,阈值单独一列。表下面用一句话说明阈值是怎么选的,这句话能挡掉一半的追问。如果消融实验做了,再补一张两列的小表,单模态和多模态的 AUC 对比,证明多源融合不是凑数。
必答题提前准备好答案。「为什么不用深度学习」按样本量和可解释性两条回答,不要只说「数据少」。「数据这么少结论可信吗」按交叉验证加独立测试集加病人级分组三条回答,说清楚每一层防的是什么。「模型怎么进临床」回答定位,辅助分诊、输出概率、医生复核,不做确诊,这句话说完通常就没人再追了。
有个血泪经验:答辩前一定把代码从头跑一遍,从原始表跑到指标表,中间不要手工改任何文件。我见过不止一次,报告里的数字和仓库里的输出对不上,原因是中间某次调参的结果没保存,报告写的是记忆里的数。从那以后我每次交方案前都强制走一遍「清空输出目录、一条命令全跑、比对指标表」的流程,跑不通就不交。希望帮到你。
本文还有配套的精品资源,点击获取