
简介这套基于多种机器学习算法实现肿瘤识别的Python项目整合了SVM、逻辑回归、决策树、K近邻、随机森林和梯度提升等六类模型的完整源码并配套Excel肿瘤数据集与超详细中文注释帮助读者一次性掌握同一数据集上的多种分类建模流程。项目面向计算机、数据科学、人工智能、通信、物联网等相关专业的在校学生和教师可直接用于毕业设计、课程设计、课程大作业、期末大作业或初期项目演示也适合作为机器学习入门进阶的练手案例。压缩包共8个文件包含6个Python算法脚本、1个Excel数据文件和1个Markdown项目说明文档整体约142KB目录结构清晰代码与说明分离、便于查阅。目前已有250人学习下载。所有代码均经过功能验证可直接运行并展示了不同算法的实现细节与对比思路便于在此基础上扩展特征工程、模型调参等二次开发。1. 肿瘤识别为什么要拿四类机器学习算法当基底肿瘤识别是机器学习入门到落地之间最好的跳板样本量几百到几千特征是带明确医学含义的数值指标标签只有良恶性两类。标题里这套带 Python 源码、数据集和超详细注释的工程核心是用 SVM、逻辑回归、决策树、K近邻这四类经典机器学习算法做同场对比把「数据怎么喂、参数怎么设、结果怎么评」这件事一次讲透。它适合两类人正在做课程设计或期末项目的学生以及刚入门机器学习、想把 sklearn 串成完整应用流程的工程师。这类小样本高维的表格型医学数据单机 CPU 几分钟就能全部跑完不需要 GPU也不依赖深度学习框架——四个算法给出四种判断逻辑恰好能覆盖一个分类问题从线性到非线性、从可解释到黑匣子的全部典型形态。2. 数据从哪来加载肿瘤数据集与预处理这一关这类肿瘤识别项目的数据集会以 CSV 形式随源码一起提供这是最常见的组织方式。不过对多数学习者来说更省事的起点是直接用 scikit-learn 内置的乳腺癌数据集它和 UCI 公开数据集是同一份数据的标准打包版569 个样本、30 个数值特征、二分类标签。先把数据加载和基本清洗跑通后面所有算法都在这一份干净数据上做对比结论才有意义。2.1 内置数据集还是 CSV 落盘加载数据的两种姿势内置数据集的优势是完全离线装好 scikit-learn 就能用。我一般先把数据加载出来看一眼结构同时顺手导出一份 CSV这样工程形态就和标题里「源码数据集」的组成对齐了。下面这段是第一个代码块注释写得很密后面排查问题时能省不少时间# 数据加载sklearn 内置的乳腺癌数据集装好 scikit-learn 就能用 from sklearn.datasets import load_breast_cancer import pandas as pd data load_breast_cancer() # Bunch 对象可以像字典一样取值 # 30 个特征半径、纹理、周长、面积、光滑度、紧凑度等 X pd.DataFrame(data.data, columnsdata.feature_names) # 原始标签0恶性, 1良性 y pd.Series(data.target, nametarget) # 工程惯例把标签重映射成 1恶性、0良性 # 这样 predict_proba 输出的第 1 列就直接是恶性风险后续不用绕 y 1 - y # 加一列样本编号后面排查异常样本时能直接定位到行 X.insert(0, sample_id, range(len(X))) # 合并后导出 CSV源码 数据集的工程结构就齐了 df X.join(y) df.to_csv(breast_cancer.csv, indexFalse) print(df.shape, y.value_counts().to_dict())这段逻辑的要点在标签重映射。原始数据里 0 是恶性、1 是良性但 sklearn 的评估指标默认把 1 当作正类。如果不做这步后面画 ROC 曲线、调决策阈值时很容易把「良性概率」当成「恶性概率」用结论完全反掉。我踩过一次这个坑从此拿到任何二分类数据第一件事就是确认正类语义。如果压缩包里直接给的是 CSV就走下面这条加载路径同时做相同的标签重映射保证两个入口得到的数据完全一致# 从 CSV 读进来时也要做同样的重映射保持标签语义一致 df pd.read_csv(breast_cancer.csv) print(df.isnull().sum().sum()) # 缺失值数量正规数据是 0 print(df.duplicated().sum()) # 重复行数量 print(df[target].value_counts()) # 1恶性 212 条, 0良性 357 条 # feature 列只有 30 个sample_id 和 target 不进入特征矩阵 X df.drop(columns[sample_id, target]) y df[target]这里的三行打印是这个流程的底线检查。缺失值多了要先决定填充策略而不是直接丢样本重复行在医学数据里可能意味着同一病人做了多次检查去不去重要看业务目标——单纯练手的话直接去重即可。需要注意如果读进来的 CSV 是从外部原始数据集转换来的target 列可能还是「0恶性、1良性」那就要再补一句y 1 - y。2.2 标准化和样本划分先缩放再分割的顺序不能反肿瘤识别这 30 个特征的量纲差异非常大mean radius 在个位数到几十之间mean area 能达到几百甚至上千而 smoothness 这类比值的取值只有零点几。SVM 的 RBF 核函数和 KNN 的距离计算对量纲极其敏感逻辑回归加了 L2 正则后同样受量纲影响。所以统一做法是先标准化再划分训练集和测试集。注意顺序先 split再 fit再 transform。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler seed 42 # 全项目统一的随机种子 # stratify 按类别比例分层抽样类别不平衡时几乎必选 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_stateseed, stratifyy ) # 标准化先 fit 训练集再 transform 测试集 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 验证标准化结果均值约 0标准差约 1 print(X_train_scaled.mean(axis0).round(2)) print(X_train_scaled.std(axis0).round(2))提示在划分训练集之前对整个 X 做 fit_transform是这类项目里最常见的数据泄漏写法。测试集只能在训练集 fit 好的 scaler 上做 transform不能参与 fit。stratify 参数在良恶性比例约 37:63 的不均衡数据上几乎是必选的它保证切分后两边比例和原始数据一致。random_state 固定后每次运行切分结果都一样这是复现的基础。标准化用的是 z-score也就是把每个特征变成均值为 0、方差为 1 的分布这一步直接决定 SVM 和 KNN 的结果可信度。2.3 按标签看特征分布哪些特征真正在区分良恶性建模前先按标签分组看特征均值差异这一步花五分钟却能避免后面很多无效调参。决策树和逻辑回归对特征可解释性的依赖最强提前知道哪些特征在两组间差异大能帮你判断模型学到的信号是否符合医学直觉import numpy as np # 把标准化后的训练数据转回 DataFrame保留列名 X_train_df pd.DataFrame(X_train_scaled, columnsX.columns) # 按标签分组统计均值1恶性0良性 summary X_train_df.groupby(y_train.to_numpy()).mean() summary.index [良性, 恶性] # 两组均值差距最大的特征通常就是区分信号最强的特征 diff (summary.loc[恶性] - summary.loc[良性]).abs().sort_values(ascendingFalse) print(diff.head(10))标准化之后不同特征的均值差异可以直接比较这是先缩放再做分布探查的好处。观察 diff 输出你会发现排名靠前的基本集中在 radius、perimeter、area 这几个几何特征上。接下来做一个相关性检查radius、perimeter、area 这三组特征内部相关性通常超过 0.9留一组代表即可否则 KNN 的距离计算会被重复信息带偏这个坑在第 5 章专门讲。如果确实想降维可以先跑一遍 PCA 保留 95% 方差再决定要不要用降维后的特征子集。3. SVM、逻辑回归、决策树、KNN四个模型的原理与选型边界标题把这四个算法放在一起不是随机组合它们恰好覆盖了分类问题的四种建模思路最大间隔分类、线性概率建模、递归划分、距离度量。同一份肿瘤数据上四个模型的边界完全不同对比着看才能理解每个算法的适用场景。3.1 SVM小样本高维下的首选重点调 C 和 gammaSVM 的核心思想是找一个最大间隔超平面把两类分开间隔越大泛化能力越强。肿瘤数据 569 条样本、30 维特征正是不折不扣的小样本高维场景而 SVM 在这个场景下公认表现稳定。实际使用时几乎不会用线性核而是用 RBF 核它能隐式地把数据映射到高维空间解决线性不可分问题。RBF 核有两个关键参数C 是误分类惩罚系数C 越大模型越倾向于把训练样本全分对容易过拟合C 越小间隔越宽但可能欠拟合。gamma 控制 RBF 核的宽度gamma 越大决策边界越细碎小到一定程度边界又过于平滑。我一般先在 C 取 0.1 到 100、gamma 取 0.001 到 1 的对数网格上粗扫一遍再在第 6 章讲的 GridSearchCV 里精调。有一点需要知道SVM 默认不输出概率要拿到概率得设probabilityTrue代价是训练速度变慢但几百条样本完全无感。调 SVM 这些年我的体会是C 和 gamma 的组合很大程度靠经验范围先框定再让网格搜索去细找纯手工盲目试参就是玄学。有这个初值范围后面网格搜索基本不会跑偏。3.2 逻辑回归概率输出和可解释性都过关的基线逻辑回归在肿瘤识别里的价值体现在两个地方一是直接输出 0 到 1 的概率天然对接「恶性风险有多大」这种业务问题二是权重系数可解释每个特征对应的系数正负和大小直接告诉医生这个指标是提高还是降低恶性概率。这在医疗场景里比黑匣子模型更容易被业务方信任。实现上有三个参数要关注。C 是正则化强度的倒数C 越小正则越强系数越向零收缩适合特征多、样本少的情况solver 用默认的 lbfgs 就够max_iter 默认 100 在标准化后的数据上偶尔不够用我习惯设为 1000省得看到警告再回来改。如果发现恶性类的召回率偏低给class_weightbalanced让模型按类别比例自动加权这条在第 5 章避坑里再展开。逻辑回归的短板是决策边界是线性的如果特征和标签之间是非线性关系它只能靠手动加交叉项或多项式特征来补救。但作为基线模型它的分数能告诉你数据线性可分的程度这个信息后面用来判断 SVM 提升空间有多大。3.3 决策树不依赖尺度输出业务能看懂的规则决策树通过递归切分特征空间来分类每次选一个特征和一个阈值把样本分成两个子集然后重复这个过程。它的优势很直接特征缩放对它没有影响所以前面标准化后的数据可以直接喂不标准化也能跑更重要的是树的结构可以画出来给业务方看「为什么这个样本被判定为恶性」——沿着树的路径读下去就是一条清晰的医学规则。防过拟合是决策树唯一的主题。不加限制的决策树可以把训练集完美分类但测试集上会明显掉点。我一般限制max_depth在 3 到 6 之间同时设min_samples_split防止在叶子节点样本太少时继续分裂。肿瘤数据只有 569 条深度超过 6 基本就是在记训练集了。训练完后打印feature_importances_能看到模型认为哪几个特征最主导判定这个排序和第 2 章数据探查的结果对照起来看能验证模型学得是否合理。3.4 KNN最简单但最容易栽在维度灾难上的模型K 近邻的思路朴素到几乎没有假设新样本来的时候找训练集里距离最近的 k 个样本投票决定类别。它不需要显式训练过程新样本的预测是即时计算距离完成的这在原型设计和快速验证阶段非常方便。但肿瘤数据 30 维特征对 KNN 来说是严苛环境。需要设的参数有三个n_neighbors用奇数避免平票肿瘤数据上 5 到 9 都合理weightsdistance让更近的样本权重更大能缓解距离扁平化的问题但要注意这会让模型对局部噪声更敏感p控制距离范数默认 p2 是欧氏距离但维度高的时候曼哈顿距离p1有时反而更稳。真正解决 KNN 维度灾难的办法是先做特征筛选或 PCA 降维再喂给它这比调参有用得多。在项目里 KNN 的定位是基线参考不追求它拿最高分但它给出的分数能让你看出距离类方法在这个特征空间里到底吃到多少亏。4. 把四个模型跑在同一套流程里训练、评估、可视化四个算法的口径统一才有对比意义。sklearn 的设计哲学就是统一的 fit/predict/predict_proba 接口所以模型可以随意替换流程代码几乎不用改。4.1 用一个循环训练四个模型统一接口的好处先给四个模型一份手工初值这份初值基于第 3 章的选型理由不是随机拍的。然后在一个循环里完成训练、预测和基础评估from sklearn.svm import SVC from sklearn.linear_model import LogisticRegression from sklearn.tree import DecisionTreeClassifier from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import confusion_matrix, roc_auc_score # 四个模型的手工初值网格搜索在最后一步统一精调 models { SVM: SVC(kernelrbf, C10, gamma0.01, probabilityTrue, random_state42), 逻辑回归: LogisticRegression(C1.0, solverlbfgs, max_iter1000, random_state42), 决策树: DecisionTreeClassifier(max_depth4, min_samples_split15, random_state42), KNN: KNeighborsClassifier(n_neighbors7, weightsdistance, p2), } for name, clf in models.items(): # 训练 clf.fit(X_train_scaled, y_train) # 因为标签重映射过第 1 列就是恶性概率 y_pred clf.predict(X_test_scaled) y_prob clf.predict_proba(X_test_scaled)[:, 1] # 核心指标AUC 和混淆矩阵 auc roc_auc_score(y_test, y_prob) cm confusion_matrix(y_test, y_pred, labels[1, 0]) # 第一行是恶性 print(f{name}: AUC{auc:.3f}) print(f 混淆矩阵(恶性/良性): {cm.tolist()})这里有两个容易写错的细节。第一predict_proba输出的列顺序对应classes_因为我们把标签重映射成 1恶性、0良性升序后第 1 列就是恶性概率取第 0 列会拿反。第二AUC 计算要求传概率而不是预测类别直接传y_pred进去会让 AUC 退化成和准确率等价的指标失去意义。四个模型在测试集上的参数设置速查如下后面对照翻车原因时方便回溯模型核心参数常见范围说明SVMC / gammaC 0.1~100gamma 0.001~1RBF 核小样本高维稳定逻辑回归C / solverC 0.01~100lbfgsC 是正则强度倒数决策树max_depth / min_samples_split3~6 / 10~30控制树复杂度防过拟合KNNn_neighbors / weights3~9 奇数 / uniform 或 distance维度高时优先降维4.2 评估指标怎么选准确率不够召回率和 AUC 才是关键肿瘤数据里良性 357 条、恶性 212 条类别并不均衡。如果只看准确率模型只要尽量把样本判成多数类良性就能拿到高分恶性样本的漏检完全被掩盖。所以在医疗场景里重点是恶性类的召回率——所有真实恶性样本里模型抓到了多少以及 AUC——模型区分良恶性的整体能力。from sklearn.metrics import classification_report # 单独输出 SVM 的详细报告其他模型同一套代码 report classification_report( y_test, models[SVM].predict(X_test_scaled), target_names[良性, 恶性], labels[0, 1] ) print(report)如果看报告发现恶性召回率不达标先不要急着换模型可以调决策阈值。模型默认把概率大于 0.5 判为恶性但对筛查场景来说阈值降到 0.3 是合理的业务选择——宁可让更多良性样本进入复查流程也不能漏掉一个恶性。代价是精确率下降误报增多这个权衡要写进结果分析里让决策者看得明白# 手动调整判定阈值概率大于 0.3 判为恶性 y_prob_svm models[SVM].predict_proba(X_test_scaled)[:, 1] y_pred_custom (y_prob_svm 0.3).astype(int)这里还要补一个容易被忽略的事实决策阈值调的是业务风险偏好不是模型能力。同一个模型阈值 0.5 和 0.3 会得到完全不同的混淆矩阵写报告时两个矩阵都要附上否则别人没法判断你是在汇报模型性能还是在汇报业务策略。4.3 可视化降维后的决策边界和树模型的特征重要性30 维空间没法直接画决策边界常见做法是先用 PCA 降到二维再用降维后的数据重训一个 SVM 画等值线。这只是一个可视化辅助手段不代表原始模型在 30 维空间里的真实边界但能直观看出非线性模型和线性模型的分界形态差异from sklearn.decomposition import PCA import matplotlib.pyplot as plt # PCA 只用训练集拟合测试集做 transform pca PCA(n_components2, random_state42).fit(X_train_scaled) X_train_pca pca.transform(X_train_scaled) X_test_pca pca.transform(X_test_scaled) # 用降维数据重训一个小 SVM仅用于画图 clf_plot SVC(kernelrbf, C10, gamma1, probabilityTrue).fit(X_train_pca, y_train) # 画网格预测等值面这一步把二维平面铺成网格逐点预测 xx, yy np.meshgrid( np.linspace(X_train_pca[:, 0].min()-1, X_train_pca[:, 0].max()1, 200), np.linspace(X_train_pca[:, 1].min()-1, X_train_pca[:, 1].max()1, 200) ) Z clf_plot.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:, 1].reshape(xx.shape) plt.contourf(xx, yy, Z, levels20, cmapRdBu, alpha0.6) plt.scatter(X_test_pca[:, 0], X_test_pca[:, 1], cy_test, cmapRdBu, edgecolork) plt.savefig(svm_boundary.png, dpi150)PCA 是全局线性投影降维后的边界会丢失一些局部细节但这不影响它帮你理解模型的大致分界形态。接着看树模型的特征重要性排序这一段是给业务解释用的# 决策树输出的特征重要性排序后找出主导特征 importances models[决策树].feature_importances_ feat_imp pd.Series(importances, indexX.columns).sort_values(ascendingFalse) print(feat_imp.head(10))把特征重要性排序和第 2 章的均值差异排序对照一下如果两者高度一致说明模型学到的信号和数据本身的分布规律吻合如果不一致先怀疑特征标准化或标签映射出了问题而不是急着调参。5. 避坑肿瘤识别项目最常见的四个翻车点这四类问题我在类似项目里反复见到每一条都是先看到诡异结果再花时间定位原因。写在这里你能直接绕过去。5.1 标准化泄漏先 fit 全量数据再划分分数直接虚高现象按「全量标准化 → 划分训练/测试」的顺序跑测试集准确率 97% 甚至更高但把模型拿去做真正的新样本预测时效果明显对不上。原因StandardScaler 在全量数据上 fit 时均值和方法已经包含了测试集的信息。训练过程中模型间接看到了测试集的分布统计等于考题答案在训练时漏给了模型。这个偏差在小数据集上非常明显因为样本越少统计量对单条样本的依赖越大。解决严格要求先 split 再 fit 再 transform也就是第 2 章的代码顺序。检查的时候看一点如果你在代码里找不到scaler.fit(X)出现在train_test_split之后、X_train_scaled之前那八成已经泄漏了。测试集上只允许出现scaler.transform。5.2 类别不平衡高准确率掩盖了恶性样本的低召回现象classification_report 里总体准确率 90% 以上看起来不错但仔细看混淆矩阵真实恶性样本有将近一半被判成了良性。原因是良性样本占 60% 以上模型发现「全猜良性」就能拿到不错的准确率于是偷懒了。原因默认的训练目标是最小化错误率多数类的错误对目标函数的贡献更大决策边界自然偏向多数类。这是优化目标的问题不是模型参数的问题。单看准确率评估模型就会得到一个完全误导的结论。解决三层一起改。划分数据时用stratifyy模型里给class_weightbalanced评估时把召回率、F1 和 AUC 作为主要指标准确率只作参考。肿瘤筛查场景的核心诉求是恶性样本漏检越少越好这个业务导向要体现在评估指标里而不是体现在字面准确率上。5.3 随机种子不固定今天能复现明天就翻车现象同一份代码昨天跑出 AUC 0.96今天跑出 0.94换台机器变 0.93。四个模型的高低排名也跟着变导致昨天说 SVM 最好今天变成逻辑回归最好。原因train_test_split 在切分数据时带随机性SVM 的求解、决策树的切分选择、KNN 的平票处理也都涉及随机过程。任何一个环节的随机种子不固定整个对比结果都无法复现。机器学习模型跑数得出的结论如果不带种子严格说不是一个可验证的实验。解决在项目开头统一定义seed 42然后所有带random_state参数的地方都传入这个值。包括train_test_split、SVC、LogisticRegression、DecisionTreeClassifier、PCA以及第 6 章网格搜索里的StratifiedKFold。我习惯把这个 seed 放在脚本头部集中定义避免散落在各处容易漏改。5.4 KNN 的维度灾难30 维特征下距离变得不可靠现象KNN 在小样本上跑出来的分数不算差但把n_neighbors从 3 调到 15AUC 几乎不动所有调整都像在碰运气。有时候去掉几个特征反而明显变好。原因维度升高后样本之间距离的差异会趋同最近邻和最远邻的区分度下降。这就是维度灾难。肿瘤数据 30 维半径、周长、面积这几个特征还高度相关等于把同一个信号重复算了三遍稀释了真正有用的距离信息。解决先做特征相关性检查把相关性超过 0.9 的特征组只留一个代表或者用 PCA 降到 10 到 15 维再跑 KNN。weightsdistance能缓解但不能根治问题。KNN 在这个项目里的定位是基线模型如果它的分数明显低于 SVM不需要花太多时间调参而是把精力放在分析特征和数据的结构上。6. 把模型调到能用的最后一公里网格搜索、落盘与复用6.1 用 GridSearchCV 把参数搜索范围一次性跑完手工初值只能说明模型跑通了要说出「这个数据上哪个模型最好」还得用交叉验证选参。网格搜索不是玄学是把经验范围交给系统去枚举。这里用 StratifiedKFold 保证每一折的类别比例和全量一致评分用 roc_auc 而不是 accuracy理由在第 5 章已经讲过from sklearn.model_selection import StratifiedKFold, GridSearchCV cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) # 两个计算量较大的模型先做网格搜索树和逻辑回归参数少可以手调 param_grid { SVM: {C: [0.1, 1, 10, 100], gamma: [0.001, 0.01, 0.1, 1]}, KNN: {n_neighbors: [3, 5, 7, 9], weights: [uniform, distance]}, } for name, params in param_grid.items(): gs GridSearchCV(models[name], param_gridparams, cvcv, scoringroc_auc, n_jobs-1) gs.fit(X_train_scaled, y_train) print(f{name} 最优参数: {gs.best_params_}, 最优 AUC: {gs.best_score_:.3f})n_jobs-1让网格搜索用满所有 CPU 核心这份数据量几十秒就能搜完。搜索过程用的是训练集内部的交叉验证分数搜索结束后还要用最优参数重新在测试集上评估一次——交叉验证分数和测试集分数要分开看前者说明参数选得如何后者说明模型真实泛化水平。这个双评估流程省不得。6.2 模型落盘与推理scaler 和模型一起保存训练和评估都完成后最常见的问题是新样本来了怎么用。很多人只保存模型不保存 scaler结果推理时手忙脚乱。标准做法是把模型和 scaler 一起序列化推理链路保持一致import joblib # 模型和标准化器一起保存推理时两者缺一不可 joblib.dump(gs.best_estimator_, tumor_model.pkl) joblib.dump(scaler, scaler.pkl) # 新样本推理链路先 transform 再 predict_proba loaded_model joblib.load(tumor_model.pkl) loaded_scaler joblib.load(scaler.pkl) new_x_scaled loaded_scaler.transform(new_patient_df[X.columns]) risk loaded_model.predict_proba(new_x_scaled)[:, 1] print(f恶性风险概率: {risk[0]:.3f})这个推理链路要和训练时的数据流完全一致特征列顺序、标准化参数、模型输入格式任何一个不一致都会在线上出问题。我自己的习惯是跑完网格搜索后就把三样东西固定下来seed、scaler、best_estimator_一起放进同一个文件夹备注上游数据版本。这样哪怕两周后回来看也能一眼还原当时跑的结果。在这类表格型分类项目上我的习惯是永远先跑一遍四个基线再谈调参和换模型。整套流程半小时内能给出上限估计如果四个模型都卡在 85% 左右问题多半出在特征工程和数据质量上而不是模型没调好——这时候去做特征筛选或数据清洗性价比远高于继续堆参数。这套源码最值得学的也正是这份比照流程而不是某个所谓最优解。希望帮到你。本文还有配套的精品资源点击获取