简介:南邮通达学院《机器学习》期末大作业资源,内含一份完整的系统设计报告,面向需要完成医疗保健数据驱动系统设计任务的同学。报告以膀胱癌诊断为核心案例,严格遵循作业要求,系统阐述了数据收集、数据处理(包含特征提取、模型选择与具体任务)以及实验评估设计三大步骤,脉络清晰、格式规范。内容涉及机器学习在医学影像、尿脱落细胞学、卡介苗治疗预后评估和术后复发预测等真实场景的应用,不仅解释了有监督学习(如ANN、逻辑回归)和无监督学习的基本原理,还列举了多项研究数据(如ANN将MRI诊断准确率提升至95%、CT分级准确度达97%等),并给出了系统示意图的设计建议。资源仅包含1个docx文档,压缩包大小29KB,下载后可直接阅读与编辑,省去自己构思框架的时间。目前已有1021人学习,特别适合南京邮电大学通达学院选修《机器学习》课程、正在准备期末大作业的学生参考借鉴。
1. 通达学院机器学习期末大作业:别把“跑通代码”当成“完成作业”
通达学院的机器学习期末大作业,每年都有一批同学卡在同一个地方:代码在教程里跑通了、模型也训练完了、报告交上去了,分数却远低于预期。原因是大多数同学把“跑通了别人的代码”当成了“完成了自己的项目”。期末大作业真正考的不是你会不会调参,而是你能不能围绕一个数据集讲完一个完整的问题闭环——数据怎么来的、特征怎么构造、模型为什么这么选、结论凭什么站得住。这篇笔记就照着这个闭环来拆,从选题到答辩,每一步给出能直接复现的做法和参数,再把老师一眼能看穿的坑挑明。适合两类人:一类是还没定题、想少走弯路的新手,另一类是代码已经跑完、但报告和答辩还讲不清楚的老手。
2. 选题与数据准备:先用一个能讲完整故事的数据集锁定方向
选题是整个大作业里最影响体验的一步。很多同学一开始就扎进某个花哨的深度学习框架,想着图像分类、情感分析看起来很厉害,结果数据下载不动、预处理做不完、模型训练要跑几个小时,最后报告只能堆截图。课程大作业的评分逻辑通常不是“谁的模型准”,而是“谁把问题讲完整了”。一个经典的结构化二分类任务,往往比一个半吊子的图像任务更容易拿高分。
2.1 选题的三条边界:可下载、可复现、可收尾
我一般会给准备做作业的同学三个筛选条件。第一,数据要能一次下载完,最好是一个 CSV 文件,而不是需要爬虫、需要解压、需要拼接的多文件数据集。第二,基线要能在一台普通笔记本上几分钟内跑完,这意味着数据量最好在几万行以内,特征维度不超过几十个。第三,故事要能收尾,也就是说你能用一句话说清楚“我要预测什么、对谁有用”。
最符合这三条的数据集类型是金融营销、用户流失、信用风险这类表格数据。以银行营销数据集为例,每一行是一个客户在一次营销活动中的记录,目标字段是客户有没有认购定期存款。这个题目天然包含类别不平衡、特征工程、业务解释这几个课程核心考点,而且字段都是可以直接写进报告的业务变量,比如年龄、婚姻状况、教育水平、上次联系间隔天数、联系次数等。相比之下,如果选一个“预测房价”的题目,特征之间高度线性相关,反而讲不出太多模型对比的故事。选定数据集之后,下一步就是把它处理成能喂给模型的样子。
2.2 数据清洗与特征工程:字段处理决定了报告能不能写满
公开的银行营销数据集拿到手之后,直接训练会遇到几个问题:里面有重复记录,有些分类字段存在“unknown”取值,目标列是 yes/no 字符串。直接删除带 unknown 的行会损失信息,但把这些值单独保留成一类,反而能让模型学出“缺失本身也有规律”。下面的代码是清洗阶段最常见的操作:
import pandas as pd df = pd.read_csv('bank_marketing.csv', sep=';') # 去重:同一客户在营销系统里被重复记录是常见脏数据 df = df.drop_duplicates() # poutcome 里的 'unknown' 单独保成一类,不直接删行 df['poutcome'] = df['poutcome'].replace('unknown', 'unknown_class') # 分类字段转哑变量,drop_first 防止产生完全共线列 cat_cols = ['job', 'marital', 'education', 'default', 'housing', 'loan', 'contact', 'month', 'poutcome'] df = pd.get_dummies(df, columns=cat_cols, drop_first=True) # 目标列转为 0/1 df['y'] = (df['y'] == 'yes').astype(int)这段代码里有三个关键决策。drop_duplicates()去重是为了防止同一个客户同时出现在训练集和测试集里,这一点在后面的避坑章节会展开。replace('unknown', 'unknown_class')是把未知类别当成一种真实分布,而不是简单删除,因为“未知”在营销数据里往往意味着某种渠道特征。drop_first=True对逻辑回归尤其重要,它去掉每个分类字段的第一个虚拟变量,避免设计矩阵出现完全多重共线性,否则逻辑回归的系数解释会变得不稳定。
做完这些之后,还要看数值字段的分布。duration(最近一次联系的通话时长)通常是最强的预测特征,但它有一个业务陷阱:通话时长只有在营销结束后才知道,如果用它做预测,等于在“结果已经发生之后”预测结果。课程报告里如果不讨论这个字段的因果含义,答辩时基本会被追问到。常见的处理方式是保留它但明确讨论,或者干脆先跑一版不含 duration 的模型做对照。
2.3 划分训练/验证/测试:随机种子与分层抽样是作业的“后悔药”
数据划分的顺序是作业里最常见的隐藏分水岭。错误的做法是先用全量数据做标准化、做哑变量,然后再划训练集和测试集;正确的做法是先划开,再让任何预处理只学习训练集的信息。另一个容易被忽略的点是随机种子。不设random_state,每次运行划分结果都不一样,后面调参得到的分数根本没法复现,写报告的时候自己都说不清哪个结果对应哪次运行。
from sklearn.model_selection import train_test_split X = df.drop('y', axis=1) y = df['y'] # 第一刀:先切出测试集,保证它完全没参与后续任何 fit X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 第二刀:从训练集里切验证集,用于调参 X_train, X_val, y_train, y_val = train_test_split( X_train, y_train, test_size=0.25, random_state=42, stratify=y_train )stratify=y是分层抽样参数,在类别不平衡时保证训练集和测试集里正负样本的比例与全量数据一致。如果不加这个参数,随机划分有可能让测试集里正样本比例跟训练集相差很多,最终分数忽高忽低,成了别人口中的“玄学”。random_state=42不是固定要求,但一旦定下来就不要改,因为后面网格搜索、模型初始化都要沿用同一个随机种子,整套结果才能复现。验证集比例取 0.25 是常见做法,因为它是从已切出的训练集里再切,实际占全量数据的比例是 0.2×0.25=0.05 乘上训练集,整体上训练、验证、测试大约是 60:20:20。
到这一步,数据集已经可以进入建模了。但很多同学会在这里直接跳到模型训练,跳过了最该做的一步:先跑一个最简单、最不可解释的基线,记住它的分数。否则后面模型调好了,你也不知道好在哪里。
3. 基线模型:把逻辑回归和随机森林跑成可复现的对照
模型部分最容易犯的错误是一上来就上 XGBoost、LightGBM,跑出一个很高的分数,然后报告里写“因为 XGBoost 效果好所以我选了它”。这句话在答辩时几乎站不住脚,因为老师会接着问“那逻辑回归多少分?随机森林多少分?你的数据有什么特性导致树模型必然更好?”如果答不上来,分数就会打折扣。正确顺序是先用逻辑回归建立一个基线,再用随机森林做对比,最后才考虑集成模型。
3.1 Pipeline:把预处理和模型绑成一个整体
建模的第一个动作是定义 Pipeline。很多同学喜欢先手动做标准化,再拿去训练,这样做的风险在于每次交叉验证时标准化器都是在全量训练集上拟合的,逻辑上已经包含了数据泄漏的影子。用 Pipeline 可以把标准化、模型训练绑成一个整体,交叉验证时每个折都会重新拟合标准化器,这是最稳妥的做法。
from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression pipe_lr = Pipeline([ ('scaler', StandardScaler()), ('clf', LogisticRegression(max_iter=1000, class_weight='balanced')) ]) pipe_lr.fit(X_train, y_train)max_iter=1000是为了防止逻辑回归在特征维度较高时达到迭代上限而报警。class_weight='balanced'是类别不平衡处理的第一道防线,它让模型自动把少数类的权重调高。课程作业里用这个参数比手动过采样更省事,而且不会改变训练集的样本分布,报告里也更容易解释。跑完这个基线,用验证集算一下 ROC-AUC,记住这个数字,它就是后面所有调参的参照物。
3.2 逻辑回归基线的参数与第一次成绩
逻辑回归在这个数据集上通常能跑到 0.90 以上的 ROC-AUC,前提是包含 duration 字段;如果排除 duration,会降到 0.80 左右。这两个数字本身就是报告里很好的分析素材。逻辑回归的优势在于系数可解释,你可以直接把系数输出成表格,看哪些字段正向影响认购。这一步不要追求最高分,而是追求“我理解我的模型”。
import numpy as np coefs = pd.DataFrame({ 'feature': X_train.columns, 'coef': pipe_lr.named_steps['clf'].coef_[0] }).sort_values('coef', ascending=False) print(coefs.head(10))这里用pipe_lr.named_steps['clf']取出管道里的逻辑回归模型,拿到系数后按绝对值排序。需要注意的是,因为管道里做了标准化,系数是在标准化尺度上的,不能直接解读成“原始字段每增加一个单位,对数几率增加多少”,只能看方向和相对大小。报告里如果写到系数解释,必须先说明这一点,否则就是严谨性漏洞。
3.3 随机森林对比:特征重要性与过拟合信号
随机森林是课程作业里的“安全牌”,它对特征尺度不敏感、不需要标准化、对非线性关系有天然的拟合能力。它的缺点是黑盒,所以要用特征重要性来打开它。特征重要性分数表示每个特征在整个森林中被用作分裂点时带来的不纯度下降总量,值越大说明模型越依赖它。
from sklearn.ensemble import RandomForestClassifier rf = RandomForestClassifier( n_estimators=200, max_depth=8, min_samples_split=5, class_weight='balanced', random_state=42, n_jobs=-1 ) rf.fit(X_train, y_train) importance = pd.DataFrame({ 'feature': X_train.columns, 'importance': rf.feature_importances_ }).sort_values('importance', ascending=False) print(importance.head(10))n_estimators=200是树的数量,再往上加收益很小但训练时间线性增长。max_depth=8限制单棵树深度,是防止过拟合最有效的参数。min_samples_split=5要求内部节点至少有 5 个样本才继续分裂,进一步抑制树往极端方向生长。n_jobs=-1意思是使用所有 CPU 核心。随机森林在这个数据集上的 ROC-AUC 通常比逻辑回归略高或持平,如果高出一大截,要怀疑是不是数据泄漏或者验证集划分出了问题。
到这里,基线和对照模型都有了。下一步才进入调参环节。但调参不是漫无目的地试,而是带着问题去搜索:树深度对过拟合的影响是什么、类别权重该不该开、用哪个指标做评分。这些决策最后都要写进报告,成为老师判断“你是真做了还是抄了代码”的依据。
4. 调参与模型对比:用网格搜索给报告造一条“证据链”
调参是作业里最容易投入产出倒挂的部分。很多同学拿 GridSearchCV 一次性搜索十几个参数,跑了半小时出结果,然后只抄一个 best_params_ 到报告里。这种做法既浪费时间,又没法回答老师“为什么这些参数最优”。正确的调参路径是先判断模型缺什么,再针对性地设置搜索网格,最后把每一次搜索的分数变化记录下来,这才是报告里真正值钱的东西。
4.1 网格搜索的边界:先树深度、再正则化、最后类别权重
网格搜索不是参数越多越好。参数一多,组合数爆炸,训练时间成倍增长,而且容易过拟合验证集。我一般遵循三个原则:第一,每次只搜一个维度的两组到三组取值;第二,评分指标用 ROC-AUC 而不是 accuracy;第三,交叉验证折数固定为 5,不要为了省时间改成 3。下面是针对随机森林的搜索示例,注意搜索的对象是一个新定义的管道:
from sklearn.model_selection import GridSearchCV from sklearn.metrics import roc_auc_score pipe_rf = Pipeline([ ('clf', RandomForestClassifier(random_state=42, n_jobs=-1)) ]) param_grid = { 'clf__n_estimators': [200, 300], 'clf__max_depth': [6, 8, None], 'clf__min_samples_split': [2, 5], 'clf__class_weight': ['balanced', None] } grid = GridSearchCV( pipe_rf, param_grid, scoring='roc_auc', cv=5, verbose=1 ) grid.fit(X_train, y_train) print(grid.best_params_) print(grid.best_score_)参数名里的clf__前缀对应管道中名为clf的组件,双下划线是 sklearn 里嵌套参数的标准写法。scoring='roc_auc'是关键,因为默认的 accuracy 在类别不平衡时会虚高,正样本占 11% 的话,全预测负类也有 89% 的准确率,但它一点用都没有。cv=5表示五折交叉验证,每个参数组合都要训练 5 个模型,所以网格规模要克制。跑完之后,如果best_params_里class_weight选中了'balanced',说明类别不平衡确实影响了模型;如果选中None,说明这个数据集上加权没有带来提升,这个结论也可以写进报告。
4.2 类别不平衡:别让 accuracy 骗了你
课堂作业中很多同学会在报告里写“模型准确率 91%”,看上去很高,但测试集里正样本比例只有 11%,全预测“不认购”也能到 89%。所以课程作业里必须同时报告 ROC-AUC、精确率、召回率、F1,而不是只挑一个好看的数字。ROC-AUC 对类别不平衡不那么敏感,它衡量的是模型把正样本排在负样本前面的能力,更适合作为这个题目的主指标。精确率和召回率要放在一起看,因为营销场景里,你联系 1000 个预测会认购的客户,最终真正认购的有多少,这是业务上更真实的成本收益问题。
下面这组代码计算并输出测试集上的完整指标:
from sklearn.metrics import classification_report, roc_auc_score y_pred_prob = grid.predict_proba(X_test)[:, 1] y_pred = grid.predict(X_test) print('ROC-AUC:', roc_auc_score(y_test, y_pred_prob)) print(classification_report(y_test, y_pred, target_names=['no', 'yes']))predict_proba(X_test)[:, 1]取的是正类的预测概率,ROC-AUC 需要的是概率而不是硬分类结果。classification_report会一次性输出每个类别的精确率、召回率、F1 和样本数,写报告时直接复制这段输出,比手画表格更严谨。如果发现召回率很低,比如正类的召回率只有 0.2,说明模型把绝大多数真实认购客户漏掉了,这时候再去调 class_weight 或者换阈值。
4.3 模型对比表:报告里最值钱也最能抗问的部分
模型对比表是课程报告里最该认真做的一张表。它不一定要展示最好的成绩,而是要展示“我做过哪些尝试、每一步提升来自哪里”。一张合格的对比表至少包含四行:逻辑回归基线、随机森林默认参数、随机森林调参后、类别权重调整后。每一行都要写清楚验证集 ROC-AUC 和测试集 ROC-AUC,两者的差距能直接反映过拟合程度。
| 模型 | 验证集 ROC-AUC | 测试集 ROC-AUC | 备注 |
|---|---|---|---|
| 逻辑回归基线 | 0.912 | 0.905 | 含 duration 字段 |
| 随机森林默认参数 | 0.924 | 0.898 | 出现过拟合信号 |
| 随机森林调参后 | 0.921 | 0.909 | 限制深度后测试集提升 |
| 调参后排除 duration | 0.831 | 0.818 | 字段因果性讨论 |
注意看第二行到第三行的变化:验证集分数下降了,但测试集分数反而上升了,这说明限制树深度抑制了过拟合。这种“验证集降、测试集升”的现象,在报告里解释清楚,比单纯追求最高分更能体现建模能力。如果只写一个最终结果,老师看不到你的思考过程,分数自然上不去。
到这里,模型的训练、评价和对比都做完了。接下来要面对的问题是:这套流程里有太多地方可能出错,而且有些错误不会让程序报错,只会让分数虚高。第 5 章把这些错误集中拆开。
5. 期末作业避坑指南:五个让老师一眼识破的翻车现场
课程作业里最糟糕的情况不是模型分数低,而是代码和报告里存在数据泄漏、随机种子不固定、重复数据混入测试集这类问题。这些问题通常不会让程序报错,甚至会给出一个漂亮的分数,但在答辩时一问就露馅。下面五条是出现频率最高的踩坑记录,每条都按“现象、原因、解决”来写。
5.1 StandardScaler 泄漏:先 fit 全集还是先 fit 训练集
现象:训练集和测试集的标准差几乎一致,测试集 ROC-AUC 比验证集还高,模型在报告里“好得不像真的”。
原因:用StandardScaler().fit_transform(X)对全量数据做了标准化,然后才划分训练集和测试集。标准化器在计算均值和方差时已经看过了测试集的数据,相当于测试集信息提前进入了训练流程。这在课堂上就是数据泄漏的典型例子,属于老师必查的坑。
解决:把标准化放进 Pipeline,或者严格按照“先划分、再 fit 训练集、再 transform 测试集”的顺序。
# 错误写法:先对全量做标准化再划分 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) X_train, X_test = train_test_split(X_scaled, y, test_size=0.2, random_state=42) # 正确写法:划分之后,scaler 只 fit 训练集 scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test)两种写法的分数差异通常很小,但第二种才经得起追问。如果答辩时老师问“你的标准化是怎么做的”,回答“在划分之后 fit 训练集”是标准答案;回答“先 fit 全量再划分”,基本等于承认泄漏。
5.2 random_state 不固定:同一份代码为什么会跑出两个成绩
现象:同一个 notebook 从头再跑一遍,测试集 ROC-AUC 从 0.91 变成了 0.89,而且每次都不一样。
原因:train_test_split、RandomForestClassifier、GridSearchCV内部都有随机过程。如果不固定随机种子,每次运行时数据划分不同、树的分裂点不同、网格搜索的训练折不同,结果自然无法复现。作业报告里如果写“模型 AUC 0.91”,但代码里一个random_state都没设,老师复跑一次对不上,整个报告可信度都会下降。
解决:在所有引入随机性的地方固定random_state=42。划分数据时设、初始化模型时设、网格搜索的管道组件里也要设。常见做法是定义一个全局变量:
SEED = 42然后在所有相关位置传入。这样一个实验跑完,过几天重跑还能得到一模一样的数字。写报告的时候,这个种子值本身不需要解释,但它保证了你报告里的表格不是“只出现过一次的运气”。
5.3 重复数据没去重:测试集里混进了训练样本
现象:测试集分数量然高,但仔细检查发现某些样本和训练集中的记录完全相同,特征一致、目标也一致。
原因:原始数据里同一个客户在同一次活动中有重复记录,清洗阶段没有做drop_duplicates(),划分数据时一部分重复样本被分到了训练集,另一部分被分到了测试集。模型相当于“背下了”训练集里的样本,测试时碰到一模一样的内容直接输出正确答案,虚高得分。
解决:在划分之前做全量去重,并结合业务含义判断。去重时只看特征列还是看全部列,需要根据数据实际情况决定。对于营销数据,同一客户在同一天同一渠道的记录一般只保留一条,这个判断写进报告里,也是数据清洗的工作量证明。
5.4 报告里的“太完美”曲线:越光滑越可疑
现象:报告里的 ROC 曲线几乎是一条完美弧线,精确率-召回率曲线没有任何抖动,或者训练集准确率 99%、测试集 85%,两者差距巨大。
原因:第一种情况是论文里常见的人工美化痕迹;第二种情况是典型的过拟合信号,说明模型把训练集的特征记住了,没有泛化能力。课程作业里很多同学喜欢把图做得“好看”,但老师看过的图比我们做过的多得多,曲线太过完美反而会被重点检查。
解决:训练集和测试集的指标都要写出来,让差距自然呈现。如果过拟合严重,优先做两件事:降低树模型的max_depth、增加min_samples_split;对线性模型则增加正则化强度。图要保留真实曲线,而不是只挑好看的部分。特征重要性图里如果出现某个特征占比超过 0.5,也要警惕是不是这个特征本身包含了未来信息。
5.5 答辩第一问“为什么选这个模型”:背不下来的动机
现象:答辩被问“逻辑回归和随机森林你为什么不选那个?”时,只能回答“因为随机森林分数高”。
原因:选模型只看了最终分数,没有从数据特性出发思考。逻辑回归适合需要系数解释的场景,随机森林适合特征与目标关系非线性、特征尺度不一的场景。如果回答不出这两者的差异,就说明建模过程缺少思考环节。
解决:在报告里写一段“模型选型依据”,套路是:先说明数据是什么类型、有多少特征、是否存在非线性关系;再说明逻辑回归适合用来做基线,因为它可解释、训练快;然后说明随机森林能捕捉交互作用,但可能过拟合;最后测试集上的对比印证了哪个更适用。这段话不用很长,但能让老师看到选择背后的逻辑链,比背十个模型的参数有用得多。
6. 把作业做成能讲的作品:报告结构、图表与答辩自测
报告是大作业的最后一步,但也是最容易拉分的一步。老师不可能一行行读代码,他主要通过报告里的文字、图表、结果表来判断你做没做、懂没懂。所以报告不是代码的附件,而是作品的说明书。结构上建议按“业务问题、数据、方法、结果、讨论”五个部分组织,每一部分都要回答“为什么”而不是只堆“做了什么”。
图表方面,三个图基本够用:特征相关性或特征重要性图、ROC 曲线、训练集和测试集分数对比柱状图。表格方面,第 4 章的模型对比表一定要放,它是最能抗追问的部分。注意 ROC 曲线不要单独画一张光秃秃的图,要标注 AUC 数值,最好把逻辑回归和随机森林两条曲线画在一起,一眼能看出哪个模型更优。所有图的坐标轴要有标签、曲线要有图例,这属于最基本的专业感,不需要额外技巧。
答辩前的自测,我习惯用五个问题来复盘:第一,为什么选这个数据集,它对谁有用?第二,哪个特征对预测最重要,为什么?第三,逻辑回归和随机森林的差异在你的数据上表现为什么?第四,测试集上的结果有没有可能存在数据泄漏?第五,如果只能用一个指标向业务方汇报,你选哪个、为什么?这五个问题全部能用报告里的内容回答出来,答辩基本不会冷场。如果某个问题答不上来,说明报告对应章节还需要补,而不是临场发挥能解决的。
我自己每次做完一个课程项目,都会把整套代码用固定种子从头跑一遍,确认每个数字都对得上,再开始写报告。这个习惯救过我很多次,因为作业里的翻车往往不是算法不行,而是过程经不起追问。这篇笔记里提到的坑,几乎都是往届同学踩过的,希望帮到你。
本文还有配套的精品资源,点击获取