Scikit-learn 工作流
这一课你将能做到
- 用 ColumnTransformer 对数值列和类别列分别做不同的预处理,缺失值不再报错
- 用 Pipeline 把预处理和模型串成一条流水线,交叉验证时自动防数据泄漏
- 用 GridSearchCV 按「步骤名__参数名」调参,一行代码搜出最优超参数
- 用 joblib 把整条流水线保存成文件,推理端加载后直接吃原始数据
为什么要学
前几课的代码有个隐患:标准化、填充缺失值这些预处理是在训练集上手动做的,推理时你得凭记忆复刻同一套操作。特征口径差一点,线上效果就掉一截。
Scikit-learn 的答案是 Pipeline:把所有依赖数据的变换和模型绑成一个对象,fit 一起学、predict 一起算、保存一起存。这不是语法糖,而是把「训练与推理口径一致」从纪律变成结构。
先建立图像
原始 DataFrame Pipeline 内部
┌──────────────────────┐ ┌─────────────────────────────┐
│ accuracy attempts │ │ pre(ColumnTransformer) │
│ 0.62 3.5 │ ───► │ num: 填中位数 → 标准化 │
│ NaN 2.1 │ │ cat: OneHot(未知类别全0) │
│ 0.71 4.0 topic │ └──────────────┬──────────────┘
└──────────────────────┘ ▼
clf(LogisticRegression) → mastery
跟着做
步骤 1:造一份「脏」数据
import numpy as np
import pandas as pd
rng = np.random.default_rng(42)
n = 500
accuracy = rng.uniform(0.1, 0.95, n).round(3)
attempts = (rng.uniform(1, 8, n) + (0.6 - accuracy) * 5).round(1)
topic = rng.choice(["栈与队列", "动态规划", "图论", "字符串"], n)
mastery = (accuracy > 0.55) & (attempts > 3) # 标签先算好
attempts[rng.choice(n, 40, replace=False)] = np.nan # 再造 8% 缺失
df = pd.DataFrame({"accuracy": accuracy, "attempts": attempts,
"topic": topic, "mastery": mastery})
X, y = df.drop(columns="mastery"), df["mastery"]
print(X.isna().sum()) # attempts 有 40 个缺失
数值列有缺失、类别列是中文——手动预处理要写一堆判断,Pipeline 一步到位。
步骤 2:ColumnTransformer 分列处理
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
pre = ColumnTransformer([
("num", Pipeline([("imp", SimpleImputer(strategy="median")),
("sc", StandardScaler())]), ["accuracy", "attempts"]),
("cat", OneHotEncoder(handle_unknown="ignore"), ["topic"]),
])
handle_unknown="ignore":线上来了训练时没见过的题目分类,不报错,编码成全 0 向量。
步骤 3:Pipeline + 交叉验证
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
pipe = Pipeline([("pre", pre), ("clf", LogisticRegression(max_iter=1000))])
print(cross_val_score(pipe, X, y, cv=5, scoring="roc_auc").mean().round(3))
关键:cross_val_score 会对整条 pipe 在每一折重新 fit——填充用的中位数、标准化用的均值都只来自当折训练部分,验证折的信息绝无泄漏。
步骤 4:GridSearchCV 调参
from sklearn.model_selection import GridSearchCV
param_grid = {
"clf__C": [0.01, 0.1, 1, 10], # 正则强度
"pre__num__imp__strategy": ["mean", "median"], # 连预处理参数一起调
}
gs = GridSearchCV(pipe, param_grid, cv=5, scoring="roc_auc", n_jobs=-1)
gs.fit(X, y)
print(gs.best_params_, gs.best_score_.round(3))
双下划线是「往下钻」:clf__C 是 clf 步骤的 C 参数;嵌套子 Pipeline 继续点下去就是 pre__num__imp__strategy。
步骤 5:joblib 保存,推理端直接用
import joblib
joblib.dump(gs.best_estimator_, "mastery_pipe.joblib")
loaded = joblib.load("mastery_pipe.joblib")
new = pd.DataFrame([{"accuracy": 0.6, "attempts": np.nan, "topic": "图论"}])
print(loaded.predict_proba(new).round(3)) # 原始数据进,概率出
注意:加载的是整条流水线,推理端不需要、也不应该复刻任何预处理代码。
讲清楚要点
- Pipeline 防泄漏的原理:所有「从数据学统计量」的变换(均值、中位数、类别词表、PCA 主轴)都必须只见过训练集。绑进 Pipeline 后每折重 fit,结构上杜绝手工顺序错误。
- 命名规则是契约:
步骤名__参数名贯穿 GridSearchCV、learning_curve 等所有工具,写错直接报错,不会静默失效。 - handle_unknown="ignore" 是上线标配:真实系统总会出现新类别(新题目分类、新渠道),默认 strict 模式会让服务崩掉。
- joblib 是 sklearn 生态标配:对大 numpy 数组比 pickle 更高效且支持压缩;注意加载端 sklearn 版本需与保存端兼容。
你可能会懵的地方
Q1:Pipeline 到底防的是什么泄漏? 任何 fit 过的数据统计(均值、词表、主成分)都携带信息。若先在整个数据上 fit scaler 再交叉验证,验证折的分布信息就混进了训练——分数虚高,上线就掉。
Q2:双下划线参数看不懂怎么办?
用 pipe.named_steps 或 pipe.get_params() 打印全部参数路径,照着写准没错。名字就是你建 Pipeline/ColumnTransformer 时起的步骤名。
Q3:网格太大搜不动? 换 RandomizedSearchCV:从参数分布里随机采样,通常 60 次迭代就能逼近最优。参数空间超过百级组合时首选它。
Q4:joblib 和 pickle 选哪个?
都行,但 joblib 对 numpy 数组更高效、支持 compress=3 压缩,是 sklearn 官方推荐。别用 pickle 存新版 sklearn 模型再拿旧版加载,会不兼容。
Q5:所有变换都必须进 Pipeline 吗? 凡是「依赖数据统计」的都要进;纯逻辑映射(时间戳转小时段、字符串截断)可以放外面,但要保证训练和推理执行同一份代码。
错法对照
| 错法 | 后果 | 正确做法 |
|---|---|---|
| 全量 fit scaler 再交叉验证 | 验证信息泄漏,分数虚高 | pre 放进 Pipeline 每折重 fit |
| 类别列先 LabelEncoder 再喂模型 | 编码引入假序,线上新类别崩 | OneHotEncoder(handle_unknown="ignore") |
| 手写网格循环 + 手动切分 | 代码冗长且易泄漏 | GridSearchCV 一行完成 |
| 手工拼新样本特征 | 训练推理口径漂移 | 保存整条 pipeline,输入原始列 |
| 只 dump 模型参数不存流水线 | 复现困难 | joblib.dump(best_estimator_) |
卡点急救
- 报错
could not convert string to float→ 类别列没放进 ColumnTransformer,模型收到了字符串。 - 报错
X has N features, but ...→ 新样本列名或顺序与训练不一致;用与训练同结构的 DataFrame 传入。 - GridSearchCV 太慢 →
n_jobs=-1并行;先粗网格锁定量级,再细网格收敛。
面试常问
Q1:Pipeline 如何防止数据泄漏? 把预处理器和模型绑成单一估计器,交叉验证/学习曲线时每折整体重 fit,所有统计量只来自当折训练数据,结构上不可能偷看验证集。
Q2:GridSearchCV 与 RandomizedSearchCV 怎么选?
小空间(几十组合)用 Grid 穷举;大空间用 Random 采样,同样的算力探索面更广。两者都支持 n_jobs=-1 并行。
Q3:模型上线为什么保存 pipeline 而不只是模型? 推理端输入是原始数据,预处理必须和训练完全一致。保存整条流水线才能保证口径唯一,避免「训练一套、线上一套」的特征漂移。
自测三问
clf__C里的 clf 是什么?(Pipeline 中你给模型步骤起的名字)- 交叉验证时 SimpleImputer 的中位数来自哪?(当前折的训练部分)
- OneHotEncoder 遇到没见过的类别且设了 ignore,结果是?(该类别对应位置全 0,不报错)
小结一句话
Pipeline 的价值不是少写几行代码,而是把「训练与推理口径一致」和「无泄漏评估」从人工纪律变成结构保证。
延伸
- 官方文档:scikit-learn Pipeline 与复合估计器
- 下一课预告:当特征交互复杂到线性模型学不动,就该深度学习出场了——用 PyTorch 手写第一个神经网络。