机器学习大作业:医疗保健智能系统设计与评估全流程
简介:面向南京邮电大学通达学院《机器学习》课程期末大作业的需求,这份文档围绕医疗保健智能系统设计展开,适合需要完成课程报告、了解机器学习医学应用案例的本科生参考。压缩包内仅含1个docx文件,大小约29KB,正文以膀胱癌诊断为主线,梳理影像学、尿细胞学、卡介苗治疗预后及术后预测等应用,并补充有监督学习、无监督学习与人工神经网络等基础概念,给出数据收集、特征处理、模型选择与实验评估的系统设计框架。已有1024人学习,可作为大作业选题、报告结构、案例论证和系统设计思路的对照材料,帮助读者把课堂知识落到具体医学场景中。
1. 从一道机器学习期末大作业说起:医疗保健智能系统到底要交什么
机器学习期末大作业的题目通常长这样:面向医疗保健领域,设计一个数据驱动的机器学习系统,报告覆盖数据收集、数据处理、实验评估三步,不少于一千五百字,最好配一张示意图。字面要求不复杂,真正难的是把手里的素材收成一套系统。膀胱癌影像诊断、尿脱落细胞形态学、卡介苗灌注后复发预测、根治性膀胱切除术后生存分析,这些方向各自都有一批公开研究,报道的准确率从 76% 到 97% 不等,但它们不是你的系统,只是你的参考文献。
这篇内容面向的读者很具体:要交这份机器学习大作业、期末作业的人,以及需要拿出一份可交付的医疗智能系统设计方案的从业者。下面按需求定位、总体架构、数据层、模型与评估、避坑排查、答辩验收的顺序,把一份能直接照着写的系统设计报告拆开。
2. 总体架构设计:三阶段流水线与四个模块的职责边界
拿到题目后,最先要确定的是报告的结构。很多人第一次写会把章节排成「SVM 介绍」「随机森林介绍」「神经网络介绍」,理由是「我的系统用到了这些算法」。这种排法在评审眼里等于交了一篇算法综述,系统在哪里看不出来。医疗保健领域的系统设计报告,主线应该是数据怎么从原始记录一步步变成可用于决策的输出,算法只是流水线上的一个工位。
2.1 按流程切分章节,而不是按算法切分
作业要求里写得清楚:数据收集、数据处理、实验评估三个步骤。这三步就是最稳妥的一级骨架,报告的章节顺序跟它对齐,评审对照评分表逐条找的时候不会迷路。剩下要补的是这三步之间传递什么、每一步内部拆成几个可交付的模块。
我在带人做这类题目时一般会要求先画出一条单向数据流:原始记录 → 清洗后的规范化表 → 特征矩阵 → 模型输出概率 → 评估报告。数据流单向、不可回退,意味着任何一步出错都能定位到具体模块,而不是「模型效果不好」这种没法排查的结论。反向依赖是设计里最忌讳的东西,比如评估阶段回头去改特征定义,那前面的实验全部作废。
按流程切还有一个隐性好处:报告字数好分配。数据收集 400 字、数据处理 600 字、实验评估 500 字,结构天然平衡,不会出现前三章写得洋洋洒洒、评估部分两句话带过的情况——而评估部分恰恰是评审最容易追问的地方。
2.2 四个模块的职责、输入输出与失败模式
把三阶段落到工程上,我通常拆成四个模块。拆分的依据不是技术栈,而是「谁出错、错长什么样」。
| 模块 | 输入 | 输出 | 典型失败模式 |
|---|---|---|---|
| 数据接入 | 原始影像报告、检验记录、随访表 | 患者主表 + 检查明细表 | 同一患者多条记录未去重,训练/测试集串号 |
| 特征处理 | 主表与明细表 | 数值化特征矩阵 + 标签 | 缺失填补用了全量数据,造成信息泄漏 |
| 建模训练 | 特征矩阵(仅训练折) | 模型参数 + 预测概率 | 超参数在测试集上调,指标虚高 |
| 评估报告 | 模型在留出集上的概率输出 | 指标表 + 混淆矩阵 + 图 | 只报准确率,类别不平衡时结论失真 |
这张表建议直接放进报告。它同时回答了两个评审常问的问题:系统由哪几部分组成,以及每一部分如果不按设计做会出什么问题。能把失败模式写清楚的方案,通常说明作者真的跑过。
2.3 工程目录与配置项:让方案看起来能跑
设计报告里放一段目录结构,比放十句「本系统具有良好的扩展性」有用得多。目录结构本身就是模块划分的证据。
project/ ├── config/ │ └── default.yaml # 所有可调参数集中在这里,报告里引用的数值都能对上 ├── data/ │ ├── raw/ # 原始导出,只读,永不修改 │ ├── interim/ # 清洗中间态,可随时重建 │ └── processed/ # 特征矩阵,训练直接读这一层 ├── src/ │ ├── ingest.py # 数据接入:去重、纳排、建表 │ ├── features.py # 特征工程:编码、缩放、派生特征 │ ├── train.py # 建模训练:交叉验证 + 模型保存 │ └── evaluate.py # 评估:指标、混淆矩阵、置信区间 ├── reports/ │ └── figures/ # 架构图、ROC 曲线、混淆矩阵 └── README.mdraw 目录只读这一条约束,是整个工程里最便宜也最有效的后悔药。很多人第一次跑通流程之后回头改清洗逻辑,发现原始文件已经被覆盖,只能重新导数据。数据分三层存放,任何一层的处理逻辑改了,删掉下游目录重跑即可,不会有「改了哪里导致结果变了」的黑匣子。
配置项则决定报告里的数字能不能复现。下面这份配置是我比较常用的形态:
data: raw_dir: data/raw id_col: patient_id # 划分必须按患者,不能按样本 label_col: recurrence_5y # 二分类标签:5 年内是否复发 missing_threshold: 0.3 # 单特征缺失率超过 30% 直接剔除 split: test_size: 0.2 # 留出集比例,全程只评估一次 n_splits: 5 # 分层 K 折的折数 stratify_col: stage_group # 按分期分层,保证各折阳性比例接近 random_state: 42 features: numeric: [age, tumor_size_cm, cd34_expression] categorical: [sex, stage, grade, cis, multiplicity, smoking] model: hidden_layers: [64, 32] dropout: 0.3 learning_rate: 0.001 batch_size: 32 max_epochs: 200 early_stop_patience: 20 # 验证集损失连续 20 轮不降就停逐项说一下取舍。missing_threshold设 0.3 是经验值:医疗表格数据里缺失率超过三成的特征,填补出来的值基本是编的,不如直接删。stratify_col用分期分组而不是原始分期,是因为早期分期的样本量往往只有个位数,按原始分期分层会出现某一折里一个阳性都没有。early_stop_patience在样本量只有几百例时非常重要,没有早停,训练损失能一路降到接近 0,验证损失却从第 20 轮开始反弹,这就是后面要讲的过拟合翻车现场。
2.4 一张架构图要交代的五件事
题目要求配示意图,很多人画成「数据 → 模型 → 结果」三根箭头,信息量几乎为零。一张合格的系统架构图至少要交代五件事:数据来源有几类、每类经过哪些处理节点、特征矩阵的形状、模型在流水线中的位置、评估结果回流到哪里。
我的画法是横向三段、纵向分层。横向从左到右是数据收集段、数据处理段、实验评估段;纵向从上到下是原始层、清洗层、特征层、模型层、报告层。跨段的箭头只有一条,就是从特征层指向模型层,其余箭头都在段内,这样图上不会出现交叉线。影像、尿细胞学、临床随访三类数据源分别用三个起点汇入清洗层,汇合点标注「按 patient_id 对齐」,这一步在图上写出来,评审就知道你考虑过同一患者多来源数据的对齐问题。
图里不要放具体算法名称太多,一份图上出现五六个缩写反而显得杂乱。主模型和基线模型各标一个即可,其余在正文里展开。
3. 数据收集与特征工程:三类数据源怎么落成一张可训练的表
数据这一章最容易写成「我们收集了患者的年龄、性别、影像资料等」这种一句话交代。评审看到这里会追问:纳排标准是什么、缺失怎么处理、标签怎么定义。这三件事写不清楚,后面的模型再漂亮也站不住。
3.1 三类数据源与纳排标准
医疗保健场景下,膀胱癌相关的可用数据一般分三类。第一类是影像资料,盆腔 MRI 或 CT,公开研究里常见几十例患者、上百个不同层面的图像;第二类是尿液细胞学与基因表达数据,包括细胞形态、密度、染色质结构的计量学指标,以及微阵列得到的基因表达量;第三类是临床随访表,字段涵盖年龄、性别、肿瘤分期与分级、原位癌、肿瘤大小、是否多发、吸烟史、标志物表达情况,以及术后复发或生存结局。
纳排标准要写成可执行的规则,不能写「选取合适的患者」。我一般定四条:病理确诊为膀胱癌;关键影像或检验在手术前若干周内完成,超窗的剔除;标签字段完整,缺失的剔除而不是填补;同一患者多次检查只保留最接近手术日的一次,避免同一人跨训练集和测试集。最后一条尤其重要,它直接决定评估结果真不真实,后面避坑章节还会展开。
样本量要在这一节明确写出来。数量少不是缺点,隐瞒才是。几百例的队列在单中心研究里很常见,报告里如实写,同时在局限性里说明,比含糊其辞强得多。
3.2 表结构设计:主表、明细表、标签表
三类数据源不能塞进一张宽表,否则影像有几个层面就得复制几行患者信息,改一个字段要改十几行。规范的做法是三张表。
-- 患者主表:一个患者一行,存放不随检查变化的基线信息 CREATE TABLE patient ( patient_id VARCHAR(16) PRIMARY KEY, age INT, sex CHAR(1), smoking BOOLEAN, surgery_date DATE ); -- 检查明细表:一个患者多次检查就多行,通过 patient_id 关联 CREATE TABLE exam ( exam_id VARCHAR(20) PRIMARY KEY, patient_id VARCHAR(16) REFERENCES patient(patient_id), exam_type VARCHAR(8), -- mri / ct / urine exam_date DATE, tumor_size_cm NUMERIC(4,1), stage VARCHAR(4), -- Ta / T1 / T2 ... grade VARCHAR(4), -- low / high cis BOOLEAN, -- 原位癌 multiplicity BOOLEAN, -- 是否多发 cd34_expression NUMERIC(6,2) ); -- 标签表独立存放,方便换预测目标时不动前两张表 CREATE TABLE label ( patient_id VARCHAR(16) REFERENCES patient(patient_id), target_name VARCHAR(32), -- recurrence_5y / survival_5y target_value SMALLINT, -- 1 阳性,0 阴性 follow_up_days INT );三张表的设计意图很直接。主表存基线,明细表存时间相关的变化,标签表独立,好处是同一批患者可以换预测目标——这次做五年复发,下次做五年生存,前面的数据完全复用。target_name这个字段看着多余,实际是给自己留的接口,一份数据支撑多个实验设计,报告里也能体现系统的可扩展性。
主键用字符串而不是自增整数,是为了在脱敏阶段能直接把编号替换掉,不用改外键。检查类型用短码而不是自由文本,是因为后面做 one-hot 时自由文本一定会出现「MRI」「mri」「磁共振」三种写法,清洗成本比建表时多花五分钟高得多。
3.3 特征工程:从原始字段到模型输入
特征处理模块要做的事情可以压缩成一句话:把三张表拼成一行一个患者、列是数值、最后一列是标签的矩阵。拼接逻辑用代码说明比文字快。
import pandas as pd import numpy as np def build_feature_matrix(patient: pd.DataFrame, exam: pd.DataFrame, label: pd.DataFrame, numeric_cols: list, categorical_cols: list) -> pd.DataFrame: """按患者聚合检查明细,与主表、标签表拼接,输出建模用特征矩阵""" # 同一患者多次检查只取最接近手术日的一次,避免信息重复计数 exam = exam.sort_values("exam_date").drop_duplicates("patient_id", keep="last") df = patient.merge(exam, on="patient_id", how="inner") df = df.merge(label[label.target_name == "recurrence_5y"], on="patient_id", how="inner") # 派生特征:肿瘤负荷用大小 × 多发的粗粒度组合,比单独两个字段更能表达进展风险 df["tumor_burden"] = df["tumor_size_cm"] * np.where(df["multiplicity"], 1.5, 1.0) # 有序变量手工编码,low/high 和 Ta/T1/T2 本身有强弱顺序,不能当无序类别处理 df["grade_ord"] = df["grade"].map({"low": 0, "high": 1}) df["stage_ord"] = df["stage"].map({"Ta": 0, "T1": 1, "T2": 2, "T3": 3, "T4": 4}) # 剩余无序类别做 one-hot,drop_first 避免共线性 df = pd.get_dummies(df, columns=[c for c in categorical_cols if c not in ("grade", "stage")], drop_first=True) keep = numeric_cols + ["tumor_burden", "grade_ord", "stage_ord"] keep += [c for c in df.columns if c.startswith(tuple(categorical_cols))] keep = [c for c in dict.fromkeys(keep) if c in df.columns] return df[keep + ["target_value"]]几处值得展开。drop_duplicates(keep="last")配合前面的日期排序,等价于「取最近一次检查」,这是纳排规则在代码里的落地,报告里要能对上。tumor_burden这种派生特征不是拍脑袋,临床研究里肿瘤大小和多发性经常被合并成一个负荷指标,写法简单但能体现对业务语义的理解。stage和grade手工映射成序数而不是 one-hot,是因为分期本身有序,one-hot 会把 T1 和 T4 当成毫无关系的两类,白白丢掉信息。
drop_first=True这一项在小样本场景下要留意。类别数多的时候去掉第一列能减少特征维度,但被去掉的那一类就变成了基准组,解释系数时要记得这件事。报告里如果写了「吸烟使风险提高多少」,得先确认基准组是谁。
3.4 数据泄漏的三个入口
特征工程阶段最容易埋雷,而且埋下去的时候完全看不出来,等指标高得离谱才发现。三个常见入口:一是在全量数据上算缺失值中位数再填补,测试集的信息通过中位数漏进了训练集;二是在划分之前做标准化,均值和方差同样来自全量;三是按样本而不是按患者划分,同一患者的多次检查分散到训练集和测试集,模型记住了这个人的特征。
规避办法只有一条:所有依赖统计量的操作都必须放在划分之后,用 Pipeline 包起来。标准化、缺失填补、方差过滤,一律作为 Pipeline 的中间步骤,fit只在训练折上调用。这一条写在报告的方法部分,评审一看就知道你懂规矩。
4. 模型实现与实验评估设计:从逻辑回归基线到 ANN 主模型
模型这一章最忌讳的是「我们尝试了 SVM、决策树、随机森林、XGBoost、神经网络,最终选择神经网络,因为效果最好」。这句话既没说明为什么好,也没说明好多少。合理的写法是先立基线,再讲主模型的结构和训练策略,最后用一套固定的划分和指标口径把两者放在同一张表里比。
4.1 基线先行:逻辑回归给出可解释的下限
任何医疗相关的建模任务,我都会先跑一个逻辑回归。原因不是它准,而是它给出的系数可以直接翻译成「这个特征方向上是升风险还是降风险」,医生能看懂,评审也能看懂。有了这个下限,后面主模型提升多少才有参照。
from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.impute import SimpleImputer from sklearn.linear_model import LogisticRegression # 预处理全部封进 Pipeline,保证 fit 只在训练折上发生 baseline = Pipeline([ ("imputer", SimpleImputer(strategy="median")), # 中位数填补,稳健于极端值 ("scaler", StandardScaler()), # 逻辑回归对量纲敏感,必须标准化 ("clf", LogisticRegression( penalty="l2", C=1.0, # 正则强度倒数,样本少时调到 0.1~1 之间比较稳 class_weight="balanced", # 阳性样本偏少时自动加权,避免全预测为阴性 max_iter=1000, random_state=42, )), ])三个参数值得说明。class_weight="balanced"在阳性率低于 30% 的数据上几乎是必选项,不加权的话模型很快学会「全预测为阴性」这条捷径。C控制正则强度,样本只有几百例、特征几十维的时候,C取 1.0 往往偏大,往下调到 0.1 附近通常更稳,具体值靠交叉验证定,不要凭感觉写。max_iter设 1000 是为了避免标准化后仍不收敛的警告,收敛失败时n_iter_会顶到上限,这个信号要盯着。
基线跑完,报告里应该有一行数字:准确率、敏感度、特异度、AUC。这行数字就是后面所有对比的锚点。
4.2 ANN 主模型:结构、正则与训练循环
主模型选人工神经网络,理由要写清楚,不能只说「效果好」。合理的理由有三条:输入是结构化表格,特征之间存在非线性交互,比如肿瘤大小对风险的影响在高分级人群中更陡;样本量虽然不大,但特征维度中等,两层隐藏层足够拟合;公开研究里 ANN 在这类任务上用得多,有可比对象。反过来,样本只有几十例的时候硬上深度学习就是给自己找麻烦,这种时候该退回逻辑回归或者带核函数的 SVM。
网络结构不用深,两层足够:
import torch from torch import nn class RiskNet(nn.Module): """表格输入的二分类网络:两层隐藏层 + Dropout,输出 logit""" def __init__(self, n_features: int, hidden=(64, 32), p_drop=0.3): super().__init__() self.net = nn.Sequential( nn.Linear(n_features, hidden[0]), nn.ReLU(), nn.Dropout(p_drop), # 小样本必备,抑制特征间的偶然相关 nn.Linear(hidden[0], hidden[1]), nn.ReLU(), nn.Dropout(p_drop), nn.Linear(hidden[1], 1), # 输出 logit,sigmoid 交给损失函数内部做 ) def forward(self, x): return self.net(x).squeeze(-1)输出层不做 sigmoid 是刻意的。配合BCEWithLogitsLoss,数值稳定性比先 sigmoid 再算交叉熵好,梯度不会在极端概率处消失。这个细节写进报告,是很实在的一笔。
训练循环里最关键的是类别权重和早停:
def train_epoch(model, loader, optimizer, criterion, device): model.train() total = 0.0 for xb, yb in loader: xb, yb = xb.to(device), yb.float().to(device) optimizer.zero_grad() loss = criterion(model(xb), yb) loss.backward() optimizer.step() total += loss.item() * xb.size(0) return total / len(loader.dataset) # pos_weight = 阴性数 / 阳性数,直接缓解类别不平衡 pos_weight = torch.tensor([neg_count / pos_count], dtype=torch.float32) criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)pos_weight用阴性数除以阳性数,是把「漏诊一个阳性」的代价抬高到与不平衡比例相称,比在数据层面过采样更干净,不会引入重复样本带来的过拟合。weight_decay=1e-4是 L2 正则,和 Dropout 一起构成双重约束。早停的判据用验证折的损失而不是准确率,因为准确率在不平衡数据上抖动大,容易在错误的轮次停下。
学习率 1e-3 配 Adam 是表格数据的常用起点,训练不收敛时先降到 1e-4 再看,不要一上来就调网络结构。
4.3 划分策略:分层 K 折加一次留出
评估设计的核心就一句话:测试集只能看一次。具体做法是先把数据按患者切出 20% 作为留出集锁起来,剩下的 80% 做分层 5 折交叉验证,用来选模型和调超参数。
from sklearn.model_selection import StratifiedKFold, train_test_split # 第一刀:按患者切留出集,分层保证阳性比例与全量一致 train_ids, test_ids = train_test_split( df["patient_id"].unique(), test_size=0.2, stratify=df.drop_duplicates("patient_id").set_index("patient_id").loc[ df["patient_id"].unique(), "target_value"], random_state=42, ) # 第二刀:只在训练集内部做 5 折,折内再分层 skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)按patient_id切分而不是按行切分,是这一节唯一不能妥协的地方。同一患者的多次影像、多项检验如果被分到两侧,模型等于提前见过这个人,测试指标会虚高十几个点,而且这种虚高在报告里完全看不出来。
分层的对象也要选对。阳性率低的时候按标签分层;如果阳性样本本身很少,可以按「分期 + 标签」的组合分层,保证每一折里各个分期的阳性数都不为零。折数选 5 不选 10,是因为样本量小的时候 10 折每折只有几十例,验证指标的方差反而更大,5 折是偏差和方差之间比较平衡的选择。
4.4 指标口径:准确率之外必须报的四个数
类别不平衡的场景下,准确率是最容易骗人的指标。阳性率 20% 的数据集,全部预测为阴性就能拿到 80% 的准确率,这个数字放进报告毫无意义。所以指标表里至少要并排出现下面几项。
| 指标 | 计算口径 | 在这个场景里说明什么 |
|---|---|---|
| 敏感度 | TP / (TP + FN) | 真正有病的人里查出多少,漏诊率的高低 |
| 特异度 | TN / (TN + FP) | 没病的人里排除多少,误诊带来的复查成本 |
| F1 | 2PR / (P + R) | 敏感度与精确率的折中,不平衡时的综合参考 |
| AUC | ROC 曲线下面积 | 与阈值无关的排序能力,便于和公开研究比 |
| PR-AUC | 精确率-召回率曲线下面积 | 阳性稀少时比 AUC 更敏感,能暴露模型的真实水平 |
除了指标本身,还有两件事要写进评估设计。一是阈值怎么定。模型输出的是概率,落到「阳性/阴性」需要切一刀,默认 0.5 在 unbalanced 数据上通常不是最优,我一般会在训练折内部用约登指数或者固定敏感度不低于某个值来选阈值,选定后锁死,拿到留出集上只评估不改。二是波动范围。单次划分得到的指标是一个点估计,样本量小的时候换个随机种子可能差五个点,报告里给出交叉验证的均值 ± 标准差,比只写一个最好看的数字诚实得多,也更经得起追问。
报告的结果章节建议用一张表把基线、主模型、以及去掉某个特征模块的消融版本并排列出。消融这一栏能说明每个模块的贡献,是设计报告区别于「跑了个模型」的关键证据。
5. 常见问题排查:五个最容易翻车的点
下面五条是我见过的、以及自己踩过的坑里复现率最高的。每条按现象、原因、解决三段写,对着排查基本能覆盖大部分翻车现场。
5.1 训练集 95%,留出集 62%
现象很典型:交叉验证的准确率漂亮得不像话,一碰留出集就掉三十个点,而且怎么调参都救不回来。
原因通常不是过拟合,而是泄漏。排查顺序固定:先看划分是按患者还是按行,再看标准化和缺失填补是不是在划分之前做的,最后看特征里有没有混进手术后才知道的信息。第三条最隐蔽,比如把「术后病理分级」当成了术前特征,或者把随访期内的复查结果放进了输入。这类特征在训练时和目标高度相关,模型当然学得又快又好,上线就废。
解决方式是建立一份特征白名单,每个特征标注「在预测时点是否已知」,未知的一律剔除。这份白名单建议直接写进报告的表格里,评审看到会认为你想清楚了时序问题。
5.2 准确率 0.82,但敏感度只有 0.15
现象是整体指标看着还行,拆开混淆矩阵发现模型几乎把所有样本都判成阴性。
原因就是类别不平衡加未加权。阳性率两成左右时,模型走「全判阴性」这条捷径的损失最低,梯度下降自然会往那儿走。不加class_weight、不加pos_weight,这个结果几乎必然出现。
解决办法分三层,从轻到重:损失函数里加类别权重;评估指标换成敏感度、F1、PR-AUC,不再盯着准确率;阈值从 0.5 往下调,用训练折内部的验证集选一个让敏感度达标的切点。三层一起用比单独用某一层稳。另外提一句,过采样容易在小样本上造成过拟合,我一般优先用权重而不是复制样本。
5.3 换台机器重跑,结果对不上
现象是同一份代码,两次运行的 AUC 差了两三个点,报告里的数字和代码跑出来的对不上。
原因就一个字:种子。权重初始化、Dropout 掩码、数据打乱、划分策略,每一处都引入了随机性,任何一处没固定,结果就不可复现。小样本场景下这个差异会被放大,两三个点足够让结论反转。
解决办法是把random_state当成配置项统一管理,Python、NumPy、框架三层都设,并在报告里注明「所有结果基于固定种子,重复三次取均值」。还有一条血泪经验:不要在看到留出集结果之后回头改种子,那就是另一种形式的测试集调参。
5.4 训练损失一路降到 0.05,验证损失从第 20 轮开始往上走
现象是训练跑得很顺,验证指标却在某个轮次之后持续变差,最后保存下来的模型还不如第 15 轮的。
原因是模型容量相对样本量太大,加上没有早停。几百例样本配两层 64/32 的网络已经接近上限,再宽就会开始记忆噪声。
解决办法是加早停,监控验证折损失,patience设 15 到 20 轮,同时保存验证损失最低那一轮的权重而不是最后一轮。配合 Dropout 和权重衰减,通常能把过拟合压到可接受范围。如果还是压不住,就减层、减宽度,先降到单层 32 再看。
5.5 报告写了一千八百字,评审说「没看到系统设计」
现象是字数够了,图也画了,但反馈是「像综述」「没有设计」。
原因往往在结构:前面大段引述公开研究的准确率,中间一段介绍算法原理,最后一段说「本系统采用神经网络」,通篇没有出现自己的数据流、自己的表结构、自己的评估口径。评审要找的是「你的系统」,不是「别人的研究」。
解决办法是把文献压缩到背景一节,控制在三百字以内,剩下的篇幅全部给自己的设计。每一节末尾加一句「这一步的产出是什么」,比如数据处理这一步的产出是「形状为 N×M 的特征矩阵和对应的标签向量」,让读者能顺着产出往下走。系统设计报告的评判标准从来不是字数,是有没有一条能走通的路。
6. 答辩验收:用一张混淆矩阵讲清你的系统到底行不行
到了验收环节,最有效的做法不是把报告从头念一遍,而是打开混淆矩阵,用三句话说清楚。第一句说测试集有多少例、阳性多少例;第二句说四个格子分别是什么数;第三句说这个数在临床场景下意味着什么——比如「敏感度 0.86 意味着每一百个真正会复发的患者里,系统能提前识别出八十六个,剩下十四个会漏掉」。这句话比任何 AUC 都更能让人记住你的系统。
如果时间允许,再补一个置信区间。小样本的指标必须带波动范围,否则一个 0.86 和一个 0.79 的差别可能完全在噪声里。
import numpy as np from sklearn.metrics import roc_auc_score def bootstrap_auc(y_true, y_prob, n_boot=1000, seed=42): """对留出集做 bootstrap 重采样,给出 AUC 的 95% 置信区间""" rng = np.random.default_rng(seed) scores = [] y_true, y_prob = np.asarray(y_true), np.asarray(y_prob) for _ in range(n_boot): idx = rng.integers(0, len(y_true), len(y_true)) if len(np.unique(y_true[idx])) < 2: continue # 重采样后只剩一个类别,跳过 scores.append(roc_auc_score(y_true[idx], y_prob[idx])) return np.percentile(scores, [2.5, 97.5])n_boot取 1000 是精度和耗时的折中,几百例的数据几秒钟就能跑完。置信区间宽,说明样本量不足,这本身就是一条诚实的结论,写进报告的局限性里,比硬说「效果显著」可信得多。
答辩还有一个常被忽略的点:把失败案例放进去。挑两三例预测错误的样本,说明是特征缺失、类别罕见还是标注本身存疑。愿意展示失败案例的人,通常比只展示最好结果的人更被信任。从那以后我每次带人做这类机器学习期末作业,都强制要求先跑通一条最丑的基线,再谈架构和调参——基线跑不通,后面全是空中楼阁。希望帮到你。
本文还有配套的精品资源,点击获取