4.6 Scikit-learn 与深度学习框架的结合 本节摘要:Scikit-learn 和 TensorFlow、PyTorch 不是二选一,而是各管一段。Scikit-learn 擅长表格数据的预处理、调参与评估,深度学习框架擅长图像、文本这类非结构化数据的表示学习。这一节讲清楚两者的边界,再给三条协作路径:用 Scikit-learn 预处理后交给深度学习训练、用深度学习提取特征后交给 Scikit-learn 下游模型、把深度学习模型封装成 Scikit-learn 估计器以复用网格搜索。核心接口就一句话——数据在 NumPy 数组层面交接。
本节摘要:Scikit-learn 和 TensorFlow、PyTorch 不是二选一,而是各管一段。Scikit-learn 擅长表格数据的预处理、调参与评估,深度学习框架擅长图像、文本这类非结构化数据的表示学习。这一节讲清楚两者的边界,再给三条协作路径:用 Scikit-learn 预处理后交给深度学习训练、用深度学习提取特征后交给 Scikit-learn 下游模型、把深度学习模型封装成 Scikit-learn 估计器以复用网格搜索。核心接口就一句话——数据在 NumPy 数组层面交接。
阅读完本节,你应当能够:
把这两个工具放一起之前,先划一条清楚的线。Scikit-learn 的根基是表格数据:每一行一个样本、每一列一个特征,矩阵进、矩阵出。它对这类数据做了极致打磨——预处理、特征选择、交叉验证、网格搜索、指标计算,一套接口全包。但它不擅长图像、音频、长文本这类原始信号,也不做 GPU 上的大规模反向传播。
深度学习框架正好补上这一段。TensorFlow、PyTorch 用自动微分和 GPU 把"从原始输入学出有用表示"这件事做到极致,代价是数据准备、调参、评估这些周边活计要么手动、要么工具分散。
所以问题不是"用哪个",而是"这一摊活该交给谁"。下面这张决策流是我常用的判断顺序。
一句话总结这条线:结构化数据先考虑 Scikit-learn;需要深度表示的非结构化数据,把表示学习交给深度学习框架,但评估与调参仍可以回到 Scikit-learn。两者是流水线的上下游,不是竞品。
💡 关键直觉:Scikit-learn 是"表格数据的瑞士军刀",深度学习框架是"表示学习的发动机"。协作的粘合剂只有一样——双方都认识 NumPy 数组。
把这条边界落成一张可执行的清单,就不会临场纠结。下面这些活,放心交给 Scikit-learn:表格数据的缺失值处理、编码、标准化、特征选择,以及交叉验证、网格搜索、指标计算这些评估周边——它已经把这几件事打磨得既快又少坑。下面这些活,则必须交给深度学习框架:图像卷积、文本的序列建模、音频频谱这类"从原始信号里学表示"的任务,以及需要 GPU 大批量反向传播的训练——这些是 Scikit-learn 压根没有的引擎。真正的灰色地带只有一个:结构化数据本身。小到几万行、特征几十列的表格,先别急着上神经网络,直方图梯度提升常常又快又稳;只有当数据大到需要批处理、或者特征之间存在需要学习才能捕获的复杂交互时,才值得把模型本体也换成神经网络,但预处理和评估照样可以留在 Scikit-learn。一句话:边界不是"数据结构"决定的,而是"这段计算需不需要深度表示和 GPU 引擎"决定的。
最常用的模式是正向的:数据先经过 Scikit-learn 的预处理,转成干净、标准化的 NumPy 数组,再喂给深度学习模型。下面这段用 StandardScaler 标准化,然后交给 Keras 训练。
from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split from tensorflow import keras from tensorflow.keras import layers X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) # 训练集上拟合并转换 X_test_s = scaler.transform(X_test) # 测试集用同一套统计量 model = keras.Sequential([ layers.Dense(64, activation='relu', input_shape=(X_train_s.shape[1],)), layers.Dense(32, activation='relu'), layers.Dense(1, activation='sigmoid'), ]) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) model.fit(X_train_s, y_train, epochs=10, batch_size=32, validation_split=0.1)
这里最要紧的一行是 fit_transform 和 transform 的区分。预处理器的统计量必须在训练集上算一次,测试集严格复用,否则测试集的信息就偷偷漏进了训练,评估会虚高。这条规则在纯 Scikit-learn 里成立,在 Scikit-learn 加深度学习的组合里同样成立,甚至更容易被忽略——因为中间隔了一层框架,很多人就忘了统计量是从哪来的。
PyTorch 的流程一模一样,只是数据要多一步转成 Tensor:
import torch X_train_t = torch.tensor(X_train_s, dtype=torch.float32) y_train_t = torch.tensor(y_train, dtype=torch.float32).unsqueeze(1) # 之后是标准的 Dataset、DataLoader、训练循环
PyTorch 比 Keras 要多写一个训练循环——前向传播、算损失、反向传播、更新权重,一步步手写。这套循环里最容易踩的是数据类型:PyTorch 默认要 float32,而 NumPy 数组常见 float64,不显式转换会报错或悄悄降速。所以 dtype=torch.float32 这行别省。
⚠️ 常见坑:预处理器的
fit只能在训练集上做。有人图省事,对全量数据先fit_transform再切分,测试集统计量混进训练集,交叉验证的分数全都不准。预处理和切分的顺序,永远不能反过来。
反向模式也很有用:让预训练的深度学习模型当一个"特征提取器",把非结构化输入转成紧凑的向量,然后把这些向量当普通特征,交给 Scikit-learn 的分类器或回归器。
典型场景是小数据的文本或图像任务。从头训练一个神经网络容易过拟合,但拿一个预训练模型抽出的语义向量,再套一个逻辑回归,常常又快又稳。
from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score # 假设 embed 是一个预训练模型,把每条文本变成 512 维向量 X_train_emb = embed(texts_train) # 得到 numpy 数组 X_test_emb = embed(texts_test) clf = LogisticRegression(max_iter=1000) clf.fit(X_train_emb, y_train) y_pred = clf.predict(X_test_emb) print(accuracy_score(y_test, y_pred))
这条路径的好处是:特征提取吃掉了"理解原始信号"的难活,Scikit-learn 只管在已经很好的特征上做线性或树模型,训练快、可解释、还不容易过拟合。代价是那批嵌入向量是黑箱里出来的,下游模型再透明,上游也还是黑的。
反向协作在"小标注数据"上尤其值钱。你手里可能只有几百条带标签的样本,从头训练深度学习必然过拟合,但预训练模型已经见过海量通用数据,它抽出的向量本身就带着语义。你只需要在这些向量上套一个逻辑回归,几百条样本就够拟合。这种"预训练特征加轻量下游模型"的组合,是迁移学习里性价比最高的一档——不用微调大模型,也不用担心训练不稳定,却常常比从头训练强一大截。
第三种模式最"混血":把深度学习模型包成 Scikit-learn 兼容的估计器,让它像内置模型一样接受 fit、predict、score,于是网格搜索、交叉验证、管道这些工具全都复用得上。
TensorFlow 侧常用 scikeras 的 KerasClassifier,PyTorch 侧常用 skorch 的 NeuralNetClassifier。核心都是一个"工厂函数"——每次实例化一个模型,剩下的交给封装器。
from sklearn.model_selection import GridSearchCV from scikeras.wrappers import KerasClassifier def build_model(units=64): m = keras.Sequential([ layers.Dense(units, activation='relu', input_shape=(X_train_s.shape[1],)), layers.Dense(1, activation='sigmoid'), ]) m.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) return m clf = KerasClassifier(model=build_model, units=64, epochs=10, batch_size=32, verbose=0) grid = GridSearchCV(clf, param_grid={'units': [32, 64, 128], 'batch_size': [16, 32]}, cv=3) grid.fit(X_train_s, y_train)
封装之后,GridSearchCV 会像对待随机森林一样对待这个神经网络:反复创建、训练、评分。省下的是一大段手写调参代码。
⚠️ 常见坑:封装器每次
fit都重新构建并训练一个模型,本身就很耗时,再乘上网格搜索的组合数和交叉验证折数,训练量会爆炸。先固定住大部分超参,只搜一两个关键项;能先在纯 Scikit-learn 模型上把流程跑通,再套深度学习模型。
PyTorch 侧对应的封装是 skorch 的 NeuralNetClassifier,用法和 scikeras 类似,只是参数命名走 skorch 那套——比如优化器的学习率要写成 optimizer__lr 这种双下划线形式,网格搜索才能正确识别。
from skorch import NeuralNetClassifier net = NeuralNetClassifier(module=MyNet, max_epochs=10, batch_size=32, verbose=0) # 之后 net.fit、net.predict、GridSearchCV(net, param_grid) 都能直接用
封装的意义不在"少写几行训练代码",而在"让深度学习模型获得和经典模型同等的工程待遇":同一个管道、同一个网格搜索、同一套评估指标。这一层抽象一旦建立,前面几章学的那些模型选择、交叉验证的套路,原封不动就能套到神经网络上。
把上面三条路径画在一张图里,就是双方的分工与衔接方式。左边是 Scikit-learn 的职责区,右边是深度学习框架的职责区,中间两条箭头是两种数据交接方向。

三条协作路径的取舍,看这张表更直接:
| 协作方向 | Scikit-learn 的角色 | 深度学习框架的角色 | 交接物 |
|---|---|---|---|
| 正向 | 预处理、特征工程 | 模型训练 | NumPy 数组 |
| 反向 | 下游分类回归 | 特征提取 | 嵌入向量 |
| 封装 | 调参、评估、管道 | 模型本体 | 估计器接口 |
举个具体的例子。做一个客服工单自动分类,文本量大、标注少:先走反向协作,用预训练模型把工单抽成向量,再用逻辑回归分类;如果效果不够、又想精细调,再走正向,把向量接进一个小神经网络微调。两条路径可以接力,不必二选一。
💡 关键直觉:哪种模式好,取决于你手里最缺的是哪块能力——缺干净特征就走正向,缺表示能力就走反向,想复用整套调参评估工具就走封装。别为了"看起来先进"硬套深度学习。
动手组合之前,对照这四问,能少走很多弯路。
第一问:数据是不是结构化表格?是,就先跑 Scikit-learn 基线,别急着上深度学习。第二问:预处理状态从哪来?fit 只能在训练集上,测试集和上线后的数据都要复用同一套统计量。第三问:数据类型对不对得上?交给 PyTorch 前确认是 float32,交给 Keras 前确认维度形状对。第四问:这次真的需要封装吗?如果只是训练一次拿个结果,直接写训练循环就够了,封装加网格搜索是为"反复调参"准备的。
把这四问过一遍,再决定走正向、反向还是封装,协作才不会变成互相添乱。
该不该为一个小表格任务上深度学习?
大多数情况不该。结构化表格数据上,梯度提升往往比神经网络更稳、更快、更好调,还没有调参地狱。先跑一个直方图梯度提升当基线,再决定要不要加深度学习。
封装成估计器后,嵌套参数怎么传?
scikeras 和 skorch 各有自己的命名规则,学习率、批次大小这类嵌套参数要用特定的前缀或双下划线暴露出来,网格搜索才认。具体以各自文档为准,但原则一致:把模型构建函数里的参数"暴露"给搜索空间。
两个框架能同时出现在一条管道里吗?
能,但要分层想清楚。数据在 NumPy 层面流动,Scikit-learn 的转换器管输入加工,深度学习封装器管模型,评估器管评分。只要每一步的输入输出都是 NumPy 数组或兼容对象,就能串成一条管道。
fit_transform 只用于训练集,测试集用同一套统计量 transform,防止数据泄露。协作是当下的活计,但一个库的价值也在于它往哪走。最后一节我们退一步,看看 Scikit-learn 近年的演进、GPU 加速和 AutoML 这些方向,判断这个"老牌工具"会怎么继续演化。