简介面向南京邮电大学通达学院《机器学习》课程期末大作业的需求这份文档围绕医疗保健智能系统设计展开适合需要完成课程报告、了解机器学习医学应用案例的本科生参考。压缩包内仅含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, keeplast) df patient.merge(exam, onpatient_id, howinner) df df.merge(label[label.target_name recurrence_5y], onpatient_id, howinner) # 派生特征肿瘤负荷用大小 × 多发的粗粒度组合比单独两个字段更能表达进展风险 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-hotdrop_first 避免共线性 df pd.get_dummies(df, columns[c for c in categorical_cols if c not in (grade, stage)], drop_firstTrue) 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(keeplast)配合前面的日期排序等价于「取最近一次检查」这是纳排规则在代码里的落地报告里要能对上。tumor_burden这种派生特征不是拍脑袋临床研究里肿瘤大小和多发性经常被合并成一个负荷指标写法简单但能体现对业务语义的理解。stage和grade手工映射成序数而不是 one-hot是因为分期本身有序one-hot 会把 T1 和 T4 当成毫无关系的两类白白丢掉信息。drop_firstTrue这一项在小样本场景下要留意。类别数多的时候去掉第一列能减少特征维度但被去掉的那一类就变成了基准组解释系数时要记得这件事。报告里如果写了「吸烟使风险提高多少」得先确认基准组是谁。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(strategymedian)), # 中位数填补稳健于极端值 (scaler, StandardScaler()), # 逻辑回归对量纲敏感必须标准化 (clf, LogisticRegression( penaltyl2, C1.0, # 正则强度倒数样本少时调到 0.1~1 之间比较稳 class_weightbalanced, # 阳性样本偏少时自动加权避免全预测为阴性 max_iter1000, random_state42, )), ])三个参数值得说明。class_weightbalanced在阳性率低于 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_drop0.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), # 输出 logitsigmoid 交给损失函数内部做 ) 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], dtypetorch.float32) criterion nn.BCEWithLogitsLoss(pos_weightpos_weight) optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4)pos_weight用阴性数除以阳性数是把「漏诊一个阳性」的代价抬高到与不平衡比例相称比在数据层面过采样更干净不会引入重复样本带来的过拟合。weight_decay1e-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_size0.2, stratifydf.drop_duplicates(patient_id).set_index(patient_id).loc[ df[patient_id].unique(), target_value], random_state42, ) # 第二刀只在训练集内部做 5 折折内再分层 skf StratifiedKFold(n_splits5, shuffleTrue, random_state42)按patient_id切分而不是按行切分是这一节唯一不能妥协的地方。同一患者的多次影像、多项检验如果被分到两侧模型等于提前见过这个人测试指标会虚高十几个点而且这种虚高在报告里完全看不出来。分层的对象也要选对。阳性率低的时候按标签分层如果阳性样本本身很少可以按「分期 标签」的组合分层保证每一折里各个分期的阳性数都不为零。折数选 5 不选 10是因为样本量小的时候 10 折每折只有几十例验证指标的方差反而更大5 折是偏差和方差之间比较平衡的选择。4.4 指标口径准确率之外必须报的四个数类别不平衡的场景下准确率是最容易骗人的指标。阳性率 20% 的数据集全部预测为阴性就能拿到 80% 的准确率这个数字放进报告毫无意义。所以指标表里至少要并排出现下面几项。指标计算口径在这个场景里说明什么敏感度TP / (TP FN)真正有病的人里查出多少漏诊率的高低特异度TN / (TN FP)没病的人里排除多少误诊带来的复查成本F12PR / (P R)敏感度与精确率的折中不平衡时的综合参考AUCROC 曲线下面积与阈值无关的排序能力便于和公开研究比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_boot1000, seed42): 对留出集做 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 是精度和耗时的折中几百例的数据几秒钟就能跑完。置信区间宽说明样本量不足这本身就是一条诚实的结论写进报告的局限性里比硬说「效果显著」可信得多。答辩还有一个常被忽略的点把失败案例放进去。挑两三例预测错误的样本说明是特征缺失、类别罕见还是标注本身存疑。愿意展示失败案例的人通常比只展示最好结果的人更被信任。从那以后我每次带人做这类机器学习期末作业都强制要求先跑通一条最丑的基线再谈架构和调参——基线跑不通后面全是空中楼阁。希望帮到你。本文还有配套的精品资源点击获取