拿到《Python机器学习及实践:从零开始通往Kaggle竞赛之路》这本书的时候,我正处于一个比较尴尬的阶段:书能看懂,例子也都能跑通,但合上书自己写就卡壳。尤其到了特征工程和模型调参这两块,书里的代码一步一步看完全没毛病,真挪到自己的数据集上,就不知道从哪下手了。后来我下定决心,花了大概三周时间,把整本书的代码从头到尾重新整理了一遍——不是复制粘贴跑一遍,而是逐行理解、重新组织、换新版本API、加上注释和统一封装,最后整理出一套可以直接拿去Kaggle当baseline的代码骨架。这篇东西就是这次整理过程的完整复盘,包括我怎么分模块、怎么处理数据、踩了哪些坑、最后沉淀成了什么。
如果你也处于“学过机器学习但写不出完整项目”的阶段,或者想用最短时间建立一套自己的机器学习代码模板,这篇文章应该对你有用。我尽量把每个决策背后的理由也写清楚,不光是给你看结果,更重要的是让你知道我当时为什么这么选。
1. 为什么值得把这本书的代码重新整理一遍
1.1 从“读得懂”到“写得顺”之间隔着什么
很多人学机器学习都会经历这样一个过程:看教材时觉得逻辑清晰,每个模型的原理也能讲上几句,但一打开编辑器就不知道先import什么,写完数据处理又不知道下一步该干嘛。说白了,这是“知识”和“技能”之间的落差。
这本书的代码有个特点:短小、直接、每段代码只为说明一个知识点。比如讲逻辑回归,它就给你一个读取数据、划分训练集、训练模型、输出准确率的四步式代码。这种写法对教学是友好的,但对工程实践的指导性偏弱。真实比赛或项目里的数据处理流程远比书上复杂,你需要的不只是“能跑的demo”,而是一套有层次的、可复用的流程。
我整理代码的第一个目标,就是把这个落差补上:把书里零散的demo变成结构化的模块,让每一步都能对应到真实项目里应该做的事。
1.2 整理代码前先给自己定三条规矩
开始之前我给自己定了三条规矩,这三条规矩决定了后面所有工作的走向:
第一,所有代码必须逐行读懂才允许往下整理。看不懂的地方先去查官方文档,查完还不行就自己写个小实验验证。宁可慢一点,也要保证自己真的理解每个参数在干什么。
第二,所有代码统一接口风格。比如数据集放在什么位置、模型训练的函数长什么样、输出结果用什么样的格式,全都要统一。这样以后换数据、换模型时,改动的成本才会降到最低。
第三,保证可复现。每次运行固定随机种子、固定数据划分方式、记录依赖库的版本。机器学习里“这次能跑下次跑不了”的问题大多和不设随机种子、依赖版本漂移有关,这三件事做在前面能省掉很多麻烦。
1.3 整理完之后的成果长什么样
三周之后,我手里多了一套完整可运行的代码库。结构大概是这样的:
book_reproduction/ ├── data/ │ ├── raw/ # 原始数据,统一放这里 │ └── processed/ # 预处理后的数据 ├── notebooks/ # 调试用的notebook ├── src/ │ ├── data_preprocessing.py │ ├── feature_engineering.py │ ├── models.py │ ├── evaluate.py │ └── utils.py ├── results/ # 输出结果、模型指标 └── requirements.txt这套结构既是书里知识点的载体,也是一个可以直接起步的机器学习项目模板。后面遇到任何新的表格类数据集,我都能从这套代码框架上快速改出第一版baseline。这个经历让我确信:整理代码的意义不在于代码本身,而在于整理过程中建立起的对项目流程的整体感。
2. 整理前的准备工作:环境、数据与目录规划
2.1 环境配置的版本选择与依赖管理
环境是整理的第一道门槛。这本书比较典型,用的是主流科学计算栈,核心依赖有scikit-learn、pandas、numpy、matplotlib,后面讲深度学习时还会用到TensorFlow/Keras或者类似的框架。
我这里的环境用的是Python 3.8系列,虚拟环境单独建了一个,没有直接装到系统环境里。原因很现实:机器学习库的依赖关系比普通应用复杂,版本之间经常打架,特别是scikit-learn这种API经常变的库。我见过太多人装了新版库之后跑旧教程代码直接报错,就是因为没有做环境隔离。
requirements.txt里的核心依赖大概是这样的:
numpy>=1.19 pandas>=1.1 scikit-learn>=0.24 matplotlib>=3.2 seaborn>=0.11 jupyter如果你已经装了anaconda,直接建一个独立环境就好。安装的时候建议用国内镜像,不然下载大包真的很难受。
2.2 数据集的本地化处理与统一目录规划
书里的数据大多是自带的小数据集,比如经典的鸢尾花Iris、手写数字MNIST、泰坦尼克号Titanic这类。整理代码时我做的第一件事不是写代码,而是把所有数据集下载好,统一放进data/raw目录。
这一步很有必要。第一,机器学习教程里常用的数据集来源比较杂,有的在sklearn里内置,有的需要单独下载,有的还在一些开源仓库里。每次跑代码都联网下载,一旦网络不稳定就会中断,你根本分不清是代码问题还是网络问题。第二,比赛场景下数据就是一份csv文件放在本地,提前养成“数据先落地、代码只读本地路径”的习惯,后面进入真实比赛时会顺畅很多。
文件路径规划也有讲究。我用了两个级别的目录:原始数据raw和预处理数据processed。raw只放初始拿到的东西,任何清洗、加工后的结果都写到processed,不覆盖原文件。这样万一调试过程中把数据处理坏了,还能回退到原始状态重来。
2.3 统一接口约定:随机种子、路径管理与配置
整理代码时有几个细节值得单独说,因为它们看似不起眼,却直接决定代码能不能“二次使用”。
第一是随机种子。我在utils.py里写了一个简单函数:
def set_seed(seed=42): """统一设置随机种子,确保结果可复现""" import random import numpy as np random.seed(seed) np.random.seed(seed)为什么必须做这一步?因为机器学习里很多过程是随机的——数据划分、模型初始化、随机梯度下降的顺序等。不固定种子的话,同样的代码每次跑出来的结果都会有细微差异。初学者很容易被这种差异误导,以为模型“不稳定”,实际只是随机性在起作用。
第二是路径管理。我没有用一堆字符串拼接路径,而是用pathlib做了统一处理:
from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent RAW_DIR = BASE_DIR / "data" / "raw" PROCESSED_DIR = BASE_DIR / "data" / "processed" RESULT_DIR = BASE_DIR / "results"为什么不直接用硬编码路径?因为Notebook和脚本运行时的工作目录经常不一致,用绝对路径写死的话,换个机器、换个目录就得改代码。用相对项目根目录的方式,整个代码库不管放在哪里都能正常跑。
3. 全书代码模块化复盘:从入门demo到比赛baseline
3.1 数据加载与探索性分析:一切工作的起点
梳理书里的代码,我第一个整理的模块是数据加载和探索性分析EDA。这本书在讲每个模型前都会先加载数据、看看数据长什么样。代码本身很简单,但里面有个思路值得强化:建模前先花时间理解数据结构,这个习惯越早养成越好。
我把数据加载统一封装成了一个函数:
def load_data(filename: str, directory: Path = RAW_DIR) -> pd.DataFrame: """统一的数据加载入口,支持csv/tsv格式""" filepath = directory / filename if filepath.suffix == ".csv": return pd.read_csv(filepath) elif filepath.suffix == ".tsv": return pd.read_csv(filepath, sep="\t") else: raise ValueError(f"Unsupported file type: {filepath.suffix}")封装这个函数最大的好处是,项目中所有地方加载数据都走同一个入口。后面如果我换了数据存储方式,只需要改这一个函数,不用满项目翻。
探索性分析部分我用matplotlib和seaborn做了一些可视化:数值型特征画直方图看分布、类别型特征看频数、特征两两之间画散点图和相关性热力图。这一步在书里可能只是一小段代码,但在真实比赛里,决定特征怎么造、模型怎么选的信息,大部分都藏在这个环节。
3.2 数据预处理标准化:按“训练集/测试集分开”的方式做
数据预处理是整本书里最容易被初学者跳过细看的部分,但它恰恰是影响模型效果的关键点。书里有大量代码涉及标准化、归一化、缺失值填充、类别变量编码。我在整理时把这些操作统一放到了一个文件里,并特别强调了一件事:预处理参数只能在训练集上拟合,然后应用到测试集。
以标准化为例,最容易犯的错误是:
# 错误示范:先对全量数据fit再划分训练测试集 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2, random_state=42)这个写法的严重问题在于数据泄漏。测试集的信息在模型训练之前就参与了scaler的均值方差计算,模型在训练时就已经“见过”测试集的分布信息。比赛中这会让你得到虚高的线下分数,上线后效果打折扣。
正确的做法是:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) # 只在训练集上fit X_test_scaled = scaler.transform(X_test) # 对测试集只transform这个“fit/transform分离”的思路贯穿整个机器学习流程,不只是标准化。之后的PCA降维、缺失值填充器的参数、OneHotEncoder等所有从数据中学习参数的步骤,都要遵守这个原则。
整理这本书的时候,我强烈建议把这张图刻在脑子里:训练集和测试集在整个流程中要像两个世界的人,训练集可以做任何操作,测试集只能用训练集学到的规则来变换,不能参与学习。
3.3 分类与回归模型:把经典算法整理成一个“模型工厂”
这本书的核心内容就是各种经典机器学习算法:线性回归、逻辑回归、决策树、随机森林、支持向量机、K近邻、朴素贝叶斯等。书里对每个算法都有单独的小节,代码结构类似:加载数据→训练→评估。整理时我没有保留这种“每种算法一个小节”的组织方式,而是用统一接口把它们组装成了一个“模型工厂”。
思路很简单。我先定义了一个统一的训练评估函数:
def train_and_evaluate(model, X_train, X_test, y_train, y_test, model_name="model"): """统一的模型训练与评估入口""" model.fit(X_train, y_train) y_pred = model.predict(X_test) acc = accuracy_score(y_test, y_pred) f1 = f1_score(y_test, y_pred, average="weighted") print(f"[{model_name}] Accuracy: {acc:.4f}, F1: {f1:.4f}") return model, y_pred然后对不同模型建立了一个简单的字典式注册:
models = { "logistic_regression": LogisticRegression(max_iter=1000, random_state=42), "decision_tree": DecisionTreeClassifier(random_state=42), "random_forest": RandomForestClassifier(n_estimators=100, random_state=42), "svm": SVC(kernel="rbf", random_state=42), "knn": KNeighborsClassifier(n_neighbors=5), }用的时候直接遍历这个字典,同一套数据、同一个评估函数,所有算法一次性跑完。这么做有两个好处:
第一,算法对比的效率大幅提升。原来书里的方式是每个算法单独看准确率,你得手动翻前面的结果记下来再对比。整理后用循环一次跑完,表格里一列就是全部算法的表现。
第二,接口统一之后,换数据、加新算法都很容易。你不需要重复写加载数据、训练、评估的代码,只需要往字典里加一行新的模型就行。
用这种方式跑完整本书的分类算法之后,我得到了一张类似的对比表:
| 算法 | 准确率 | 训练时间 | 备注 |
|---|---|---|---|
| 逻辑回归 | 0.973 | 极短 | 线性模型在数据量小时表现稳定 |
| 决策树 | 0.947 | 短 | 容易过拟合,需要剪枝 |
| 随机森林 | 0.980 | 中等 | 默认参数下综合表现好 |
| SVM(RBF) | 0.980 | 中等 | 对小数据集效果好,参数敏感 |
| KNN | 0.973 | 短 | 特征缩放很重要 |
这张表不是为了论证哪个算法最好,而是帮你建立“对同一份数据,不同模型差异可能不大,但训练成本、可解释性、稳定性差异很大”的直观认识。这种认识只能来自亲手跑一遍,光看书是得不到的。
3.4 特征工程与降维:把“造特征”变成有章法的过程
书里特征工程的篇幅不算少,讲了标准化、归一化、PCA主成分分析、特征选择等。整理这部分代码时我体会最深的是:特征工程决定模型效果的上限,而模型只是在逼近这个上限。
PCA这块值得多说几句。书里的代码很简单:
from sklearn.decomposition import PCA pca = PCA(n_components=2) X_pca = pca.fit_transform(X)但如果只是让代码跑通,你永远理解不了PCA到底帮了什么忙。整理时我额外做了几件事:看explained_variance_ratio_、画出累积方差贡献率曲线、对比降维前后模型的性能变化。
pca = PCA() pca.fit(X_train_scaled) cumsum = np.cumsum(pca.explained_variance_ratio_)为什么要看累积方差贡献率?因为n_components这个参数不是拍脑袋定的。当你发现前两个主成分已经解释了90%以上的方差,说明数据在更低维空间里也能有不错的表达;如果解释率很低,说明原始特征之间相关性不强,强行降维可能丢掉关键信息。
特征选择部分书里给出了SelectKBest等方式。我在整理时加了一个自己的体会:先用简单模型跑一版baseline,再用特征重要性筛选特征,最后对比两者效果。这种“先粗后细”的做法比一开始就上复杂特征工程策略要稳妥得多。
3.5 参数调优:从手动试参到网格搜索/随机搜索
这本书关于调参的章节很实用,核心工具是GridSearchCV和RandomizedSearchCV。整理这部分代码时,我意识到调参的本质不是“碰运气”,而是理解模型超参数之间的协作关系。
以随机森林为例,我当时对n_estimators(树的数量)、max_depth(树的最大深度)、min_samples_split(分裂一个节点所需最小样本数)做了网格搜索:
from sklearn.model_selection import GridSearchCV param_grid = { "n_estimators": [50, 100, 200], "max_depth": [None, 5, 10], "min_samples_split": [2, 5, 10], } rf = RandomForestClassifier(random_state=42) grid_search = GridSearchCV(rf, param_grid, cv=5, scoring="accuracy", n_jobs=-1) grid_search.fit(X_train_scaled, y_train) print(grid_search.best_params_) print(grid_search.best_score_)这里有几个值得注意的细节。第一,cv=5是五折交叉验证,不是简单的一次训练集/测试集划分。它的作用是减少因为数据划分带来的偶然性,让调参结果更稳定。第二,n_jobs=-1表示使用所有CPU核心并行计算,网格搜索的耗时能明显降低。第三,scoring参数决定了优化目标,比赛里用的指标可能不是准确率,比如二分类比赛常用AUC,回归比赛常用RMSE或MAE,要根据目标改。
整理代码时我还对比了GridSearchCV和RandomizedSearchCV的差别。当参数组合数量大时,网格搜索是穷举,组合数会爆炸,比如上面那个例子是3×3×3=27组,每组还要做5折交叉验证,也就是要跑135次模型。如果参数再多几组,计算时间会非常难看。这时RandomizedSearchCV随机采样的效率优势就出来了,它不需要遍历所有组合,能更快找到差不多的参数区间。
3.6 深度学习部分:从MLP到简单网络结构的梳理
书后面有深度学习相关章节,主要是用神经网络的简单结构来做分类。整理代码时我最大的感触是:深度学习的代码看起来很玄,但核心骨架和经典机器学习算法是高度相似的——数据准备、模型定义、训练、评估。
这部分代码我额外强调了固定训练随机性的重要性。比如用Keras/TensorFlow训练网络时,如果不设置随机种子,即便同一个脚本,每次跑出来的结果都可能不一样。整理时我加了这样一段:
import tensorflow as tf def set_tf_seed(seed=42): tf.random.set_seed(seed) np.random.seed(seed)从机器学习到深度学习,随机种子都是一个“看不见但影响巨大”的变量。训练时模型不是确定性算法,初始化权重是随机的,数据读取顺序也可能打乱,这些都会影响最终效果。想在调试时判断到底是代码改了导致结果变化,还是随机性导致结果变化,先把种子固定住是第一要务。
3.7 Kaggle实战流程:把前面积累的模块拼接成完整参赛代码
这本书最后有Kaggle实战的章节,我印象比较深的是用了一个经典的表格类比赛数据来演示完整流程。整理到这里时,我意识到前面所有模块化工作的价值终于显现出来了。
真实的Kaggle参赛流程大概是这样的:
# 1. 加载数据 train = load_data("train.csv") test = load_data("test.csv") # 2. 快速探索 print(train.info()) print(train.describe()) print(train.isnull().sum()) # 3. 数据清洗与特征工程 train_processed, test_processed = preprocess_features(train, test) # 4. 划分训练集和验证集 X_train, X_val, y_train, y_val = train_test_split(...) # 5. 训练模型(多个模型对比) for name, model in models.items(): train_and_evaluate(model, X_train, X_val, y_train, y_val, name) # 6. 选择最优模型预测测试集并生成提交文件 best_model.fit(np.vstack([X_train, X_val]), np.hstack([y_train, y_val])) test_pred = best_model.predict(test_processed) submission = pd.DataFrame({"ID": test_id, "Prediction": test_pred}) submission.to_csv(RESULT_DIR / "submission.csv", index=False)整个流程环环相扣。之前每章单独看时都觉得简单,拼在一起才发现每个环节都在为下一步做铺垫:没有好的预处理,模型训练效果就不可靠;没有可靠的模型选择策略,提交结果就是在碰运气。
4. 整理过程中踩过的坑与排查记录
4.1 库版本升级导致的API变动
这是我在整理时遇到最多的问题。这本书的部分代码是基于相对早期的scikit-learn版本写的,一些工具函数的位置变了。最典型的是交叉验证相关的函数。
老版本的写法可能是:
from sklearn.cross_validation import train_test_split, cross_val_score新版本里这个模块已经被拆到了model_selection:
from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV解决办法很简单,但我当时花了不少时间排查。建议整理这本书时直接统一使用新版API,不要混用。遇到import报错时,先看是不是模块路径变了,再去网上搜当前版本正确的导入路径,而不是盲目降低依赖版本。
4.2 数据泄漏问题:fit_transform和transform只用错一次代价就很大
前面提过的数据泄漏问题,在整理过程中我踩过一次。当时修改一个预处理脚本,不小心在测试集上直接调用了fit_transform。第一版跑出来验证集分数特别高,我还以为是特征工程立功了,后来用同样的代码在另一个数据集上跑,分数一落千丈,才意识到问题出在预处理环节。
排查方法是把pipeline拆开,逐步检查scaler的fit和transform各自用在了哪些数据上。后来我学乖了:凡是数据标准化、PCA之类的操作,一律封装成自定义函数,里面严格区分训练和测试分支,不给人手动写错的机会。
4.3 pandas的SettingWithCopyWarning
整理数据处理代码时,我经常会遇到这样的警告:
SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame这个警告的意思是:你用链式索引(先切片再赋值)修改DataFrame时,可能改的不是原来那份数据,而是它的一个副本。书里某些代码直接train[col] = ...的写法在极简demo里没出问题,但一旦数据量变大、操作变多,warning就冒出来了。
我的习惯是:需要修改DataFrame时先用copy()显式复制,或者改用loc来操作:
# 推荐写法 df = raw_df.copy() df.loc[:, "new_feature"] = df["existing_feature"] * 24.4 其他小坑速查表
我在整理过程中把印象比较深的问题整理成了下面这个速查表:
| 问题现象 | 可能原因 | 排查方向 | 解决办法 |
|---|---|---|---|
| 模型效果忽高忽低 | 没有固定随机种子 | 检查是否有random_state | 在所有随机环节设固定seed |
| 测试集效果远低于训练集 | 数据泄漏 | 检查预处理是否在测试集上fit | fit/transform严格分离 |
| import sklearn时报错 | 版本兼容问题 | 查看当前sklearn版本 | 使用新版API或调整版本 |
| pandas警告影响判断 | 链式赋值 | 查看警告位置 | 使用loc和copy避免链式修改 |
| 中文路径导致文件读取失败 | 编码或路径问题 | 检查路径和编码 | 统一用UTF-8编码和路径对象 |
| 可视化中文乱码 | 字体缺失 | 查看matplotlib字体配置 | 指定中文字体或改为英文标签 |
4.5 我的调试策略:先用最小数据跑通全流程
这算是我整理这本书时最受益的一个习惯:先用小数据量快速验证全流程,再跑全量数据。具体做法是,调试阶段只取整个数据集的前1000行或者更少,跑通整个pipeline,确认没有报错、输出格式正确,再替换成全量数据重新运行。
这个习惯帮我节省了大量时间。因为机器学习项目链条长,某一步出错了,报错信息经常指向很远的地方。小数据量跑一遍能在几秒内定位问题所在,不用在每次报错后等很久才能看到下一个错误。
5. 从中得到的沉淀:如何把“整理代码”变成“项目模板”
5.1 整理完之后要做什么:沉淀自己的baseline模板
代码整理不是终点,重点是整理完之后你能带走什么。我把这次整理的成果分成了三层:
第一层是代码本身。上面说过的所有源文件,组成了一个可运行的机器学习项目模板。
第二层是流程认知。从数据加载到预处理、建模、调参、提交,整个pipeline的各个环节在真实项目中的顺序和依赖关系,已经内化成了我自己的“肌肉记忆”。以后拿到一个新数据集,我不用再想“下一步做什么”,流程自然就在脑子里展开了。
第三层是工具意识。我知道什么时候该用GridSearchCV、什么时候该用RandomizedSearchCV、什么时候该上PCA、什么时候该用集成模型。这些判断力光靠看书很难获得,只有在亲手整理过代码、亲手对比过结果差异之后才能真正建立起来。
5.2 给同样在整理代码的你的几点建议
如果你也想把这本经典书的代码整理成自己的项目模板,我的建议是:
第一,不要追求一次整理完。每天整理一个章节就行,留出时间消化和验证。逼得太紧反而容易变成“手动抄代码”,没有理解过程。
第二,每个章节整理完后,做一个小实验:不看书里的代码,自己从零开始实现一遍同样的流程。这个“关书重建”的过程比任何笔记都有效。
第三,整个项目做完之后,试着拿一个新数据集从头到尾跑一遍。这一步能快速检验你沉淀下来的模板是真的好用,还是只对书里那几个数据集有效。
我做完这件事最直接的收获,是后来参加一个实际比赛时,别人还在纠结数据怎么清洗、用什么模型,我直接把这套模板套上去,半天就出了第一版baseline。然后才有足够的时间去优化特征、调参、做模型融合。这种“先有baseline再优化”的节奏,和“从零开始边写边想”的节奏,心气完全不同。
5.3 后续还能怎么扩展
这套模板目前覆盖了表格数据的经典机器学习流程,但还远不是终点。我的想法是随着经验积累持续往里面加东西,比如加一段模型融合的代码框架、加一个更完善的特征选择模块、把调参部分的搜索策略升级成更高效的贝叶斯优化。
不过这些都是后话。目前这个版本,已经足够支撑我从“读完一本书”走向“完成一次真实竞赛”了。如果你也在整理代码,我建议你把重点放在“理解为什么这样做”上,而不是“代码跑通”上。代码跑通只是起点,真正沉淀下来的、那种拿到数据就知道怎么下手的手感,才是整理这本书最大的回报。