过去一个月我一直在折腾一个客户流失预警项目特征表拉出来几十列缺值、偏态、类别变量全凑一块儿了。模型效果卡在召回率上迟迟上不去当时的纠结是继续在Sklearn里换模型还是干脆把数据扔给PyTorch上一个大点的网络。两边的文档都翻了不少最后趟下来发现真正顺手的路子是把Sklearn和PyTorch当两个各司其职的工种来用——Sklearn管数据处理、基线模型、指标评估PyTorch负责定制化网络和训练过程。这篇《Sklearn 和 PyTorch 机器学习实用指南一》就把这条主链路完整捋一遍从环境准备到Pipeline预处理再到把干净的numpy数组送进PyTorch训练最后用sklearn的指标体系复盘模型。适合刚学完基础语法、想在真实结构化数据上把两者串起来用的人。1. 为什么同时用Sklearn和PyTorch而不是二选一很多初学者容易陷入一个误区学机器学习要先选一个框架然后所有事情都用它干。我见过有人为了用PyTorch连缺失值填充都手写也有人因为sklearn没有好用的神经网络API硬是把任务拆得七零八落。我的看法很直接——你手里应该有两套工具箱而不是一把锤子。1.1 Sklearn负责数据工程PyTorch负责模型工程Sklearn在数据处理和经典机器学习上的积累太完整了从Pipeline到ColumnTransformer从网格搜索到各种评估指标API设计得相当统一。而PyTorch的强项是灵活的动态计算图和完整的自动求导机制你想自定义一个loss、加一个特殊的layer、控制每个batch的更新逻辑都很顺手。在实际项目里我几乎不会用PyTorch去处理缺失值、做标准化编码也基本不会用sklearn去训练一个几百层的大网络。最合理的分工是Sklearn数据清洗、特征编码、特征选择、基线模型、交叉验证、指标计算。PyTorch深度网络搭建、自定义训练循环、复杂loss、批量训练、模型部署时的动态逻辑。这条分工不是拍脑袋定的而是因为两边各自的心智模型完全不同。Sklearn的Transformer API是拟合-转换的思维训练和推断的状态都封装在对象里PyTorch则是张量流动的思维你真正掌握的是数据和梯度的走向。硬要混用会让代码变得拧巴。1.2 我的选型边界什么任务留在Sklearn什么任务交给PyTorch给一个我自己的判断标准纯主观经验但踩了几年坑后挺管用任务类型推荐栈理由数据行数少于1万特征少于100Sklearn模型训练快调参成本低解释性好数据量大且主要靠特征工程取胜Sklearn PyTorch特征变换交给sklearn简单MLP即可数据量大特征本身需要深层交互建模PyTorchTabNet、DeepFM这类结构sklearn做不了需要频繁上线更新模型PyTorch训练脚本和推理脚本分离更方便这里我想强调一个经验之谈不要把深度学习当成默认选项。一个5000行、20个特征的表逻辑回归或梯度提升树往往比三层MLP更稳调参时间还少。PyTorch在这个链路里的真正价值是当传统模型表达能力不够时你可以无缝把它升级成网络模型而不是推倒重来。2. 环境准备版本冲突和GPU检测先搞定代码写得好不好三分靠水平七分靠环境是否干净。Sklearn和PyTorch的依赖链其实是独立的两套体系前者依赖numpy、scipy后者依赖自己的底层库和CUDA运行时。一旦混装到同一个全局环境里很容易出现版本冲突比如某个sklearn版本要求numpy小于等于1.x而新装的PyTorch已经把numpy升到2.x了。2.1 用独立虚拟环境隔离依赖我的固定做法是每个机器学习项目建一个独立虚拟环境。这里不推荐用Anaconda默认的base环境直接装东西因为那个环境通常是系统里很多脚本共用的大杂烩。python -m venv venv_ml source venv_ml/bin/activate pip install --upgrade pip如果你是Windows激活命令是venv_ml\Scripts\activate。这一步本身不复杂但很多人跳过了它后面在装包时遇到莫名奇妙的二进制依赖问题然后浪费一晚上排查。虚拟环境最大的好处是你随时可以rm -rf venv_ml重建成本极低。2.2 固定一套稳妥的版本组合自从PyTorch 2.x出来之后安装命令变得简洁了不少CPU版本直接pip装就行GPU版本则建议先去官网查一下自己的CUDA版本。我最近的固定组合是pip install scikit-learn pandas matplotlib pip install torch --index-url https://download.pytorch.org/whl/cu118装完之后不要急着写代码先跑一遍环境自检。这个自检脚本已经变成我每个新项目的固定开场了import sys import sklearn import torch import pandas as pd print(fPython 版本: {sys.version.split()[0]}) print(fSklearn 版本: {sklearn.__version__}) print(fPyTorch 版本: {torch.__version__}) print(fCUDA 是否可用: {torch.cuda.is_available()}) if torch.cuda.is_available(): print(fGPU 名称: {torch.cuda.get_device_name(0)})我遇到过一种很气人的状况代码逻辑完全没问题但训练速度慢得离谱检查了半天才发现PyTorch装的是CPU版本每轮迭代都在跑单核。所以torch.cuda.is_available()这个检查一定要在项目第一天做掉而不是等训练跑了三个小时才发现。3. 数据处理走Sklearn Pipeline为PyTorch铺路数据预处理是整个项目里最容易被低估的环节。很多人拿到脏数据之后一边用pandas手动处理一边又在代码里写各种临时变量结果最后根本分不清哪些列做了标准化哪些列做了缺失值填充。用Sklearn的Pipeline价值在于它把所有预处理步骤串联成一个对象fit在训练集上直接在验证集和测试集上复用保证转换逻辑完全一致。3.1 一个典型结构化任务的预处理思路假设我们要处理一个包含数值列和类别列的表格数据原始DataFrame长这样import pandas as pd import numpy as np from sklearn.model_selection import train_test_split df pd.DataFrame({ age: [25, np.nan, 34, 45, np.nan, 29], income: [50000, 60000, 80000, 70000, 65000, 72000], city: [A, B, A, C, B, A], education: [本科, 硕士, 本科, 博士, 硕士, 本科], churn: [1, 0, 0, 1, 0, 1] # 目标列 }) X df.drop(columns[churn]) y df[churn]数值列age有缺失值income量纲明显偏大类别列city和education需要编码。这种任务用手写代码当然也能做但用Sklearn的ColumnTransformer会更清晰from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline numeric_features [age, income] categorical_features [city, education] numeric_transformer Pipeline([ (imputer, SimpleImputer(strategymedian)), (scaler, StandardScaler()) ]) categorical_transformer Pipeline([ (imputer, SimpleImputer(strategymost_frequent)), (onehot, OneHotEncoder(handle_unknownignore)) ]) preprocessor ColumnTransformer([ (num, numeric_transformer, numeric_features), (cat, categorical_transformer, categorical_features) ])这一步写完后preprocessor就是一个完整的数据处理模块。调用fit_transform得到干净的特征矩阵然后直接切训练集和验证集X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42, stratifyy ) X_train_processed preprocessor.fit_transform(X_train) X_val_processed preprocessor.transform(X_val)注意这里必须是fit_transform训练集、transform验证集否则会在验证集上发生数据泄漏后面模型的指标会虚高。3.2 Sklearn输出与PyTorch输入的天然衔接点ColumnTransformer默认输出的是稀疏矩阵如果你的OneHotEncoder处理了很多高基数类别矩阵会非常稀疏。直接把稀疏矩阵丢给PyTorch会报错因为PyTorch的Tensor不直接接受scipy.sparse矩阵。所以这里要顺手做个转换X_train_dense X_train_processed.toarray().astype(np.float32) X_val_dense X_val_processed.toarray().astype(np.float32) y_train_np y_train.to_numpy().astype(np.float32) y_val_np y_val.to_numpy().astype(np.float32)我用float32而不是默认的float64是为了和PyTorch默认的dtype对齐。如果你用float64创建一个Tensor后面很多在GPU上跑的算子会自动补转换徒增内存消耗甚至直接警告。4. 从numpy到Tensor的桥梁Dataset与DataLoader的正确打开方式数据预处理完成后你会得到一个干净的numpy数组。很多新手到这里就想着直接写循环for epoch in range(100): for i in range(0, len(X_train), batch_size): batch_x torch.from_numpy(X_train[i:ibatch_size]) batch_y torch.from_numpy(y_train[i:ibatch_size])这个写法在小数据集上当然能跑但在真实项目里有几个隐患没有自动打乱机制无法平滑支持多进程数据加载代码可扩展性差。PyTorch提供了Dataset和DataLoader这套抽象我认为这就是专门为结构化数据训练设计的。4.1 为什么不要直接拿numpy数组训练直接拿numpy数组切片训练的问题在于你的训练循环会越来越像手写数据管线而不是模型训练管线。一旦你想加入数据增强、样本采样、处理类别不平衡这些逻辑都会挤在同一个循环里代码很快就没法看了。而Dataset这套抽象的价值在于它把每一条样本长什么样和模型怎么用这些样本训练解耦开。DataLoader的shuffle、num_workers、drop_last这些机制是经过大量项目验证的通用实现。只要你接触的数据量上了百万行手写一个高效的batch迭代器就会变得非常痛苦。4.2 手写一个足够好用的TabularDataset这里不建议用什么花哨的库PyTorch原生Dataset就够用了。下面这个类我几乎在每个结构化任务里都会复用只是字段名不同而已。import torch from torch.utils.data import Dataset, DataLoader class TabularDataset(Dataset): def __init__(self, X, y): self.X torch.from_numpy(X) self.y torch.from_numpy(y).unsqueeze(1) # 变成 (N, 1) def __len__(self): return len(self.X) def __getitem__(self, index): return self.X[index], self.y[index]然后创建DataLoadertrain_dataset TabularDataset(X_train_dense, y_train_np) val_dataset TabularDataset(X_val_dense, y_val_np) train_loader DataLoader(train_dataset, batch_size128, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size256, shuffleFalse, num_workers4)这里面几个参数值得细说一下batch_size128对结构化数据来说128是一个比较稳的起点既能利用GPU并行又不会让小数据集震荡得太厉害。shuffleTrue只有训练集需要打乱。验证集如果也打乱除了浪费时间没有任何好处。num_workers4四个子进程帮忙加载数据可以在数据预处理较重时显著提升吞吐。如果你在Windows上跑记得放到if __name__ __main__:块里否则子进程会递归启动你的脚本。另外有一个很容易被忽视的经验如果你的y是整数0/1分类创建Tensor时保留float32因为后面要算二分类交叉熵lossPyTorch的BCEWithLogitsLoss期望浮点标签而不是整数标签。5. 训练循环里蹭Sklearn的评估体系训练循环是PyTorch的主场但指标评估这件事我用Sklearn用得比PyTorch顺手得多。tensor.detach().numpy()这个双向接口让两边缝合起来毫无压力。5.1 训练/验证/测试三段的指标口径先统一一下指标口径。很多人训练时喜欢看loss验证时却看准确率阶段不同、指标不统一导致判断模型好坏时特别混乱。我的习惯是训练集看loss因为loss下降是模型学到的直接信号。验证集看业务指标比如精确率、召回率、AUC因为验证集是用来调参和早停的。测试集只看一次只看最终指标不看任何中间过程。正确的决策顺序是用验证集判断模型该不该继续训练、该不该换超参最终确定模型后再在测试集上评估。不要反复看测试集表现否则测试集的参考价值会被你看过太多遍而污染。5.2 手写验证函数指标交给sklearn梯度交给torch下面是一个我常用的评估函数。它的核心逻辑很简单把模型输出送进torch.sigmoid转成numpy再喂给sklearn的metrics。from sklearn.metrics import roc_auc_score, precision_recall_fscore_support def evaluate_model(model, val_loader, threshold0.5): model.eval() y_true [] y_pred [] y_prob [] with torch.no_grad(): for batch_x, batch_y in val_loader: batch_x batch_x.to(device) logits model(batch_x) prob torch.sigmoid(logits).cpu().numpy() y_true.extend(batch_y.cpu().numpy().ravel()) y_prob.extend(prob.ravel()) y_true np.array(y_true) y_prob np.array(y_prob) y_pred (y_prob threshold).astype(int) # 二分类指标 precision, recall, f1, _ precision_recall_fscore_support( y_true, y_pred, averagebinary ) auc roc_auc_score(y_true, y_prob) return { precision: precision, recall: recall, f1: f1, auc: auc }这个函数是我在项目里反复打磨出来的模板。注意几点模型开model.eval()并包一层torch.no_grad()否则验证时PyTorch会计算梯度白白消耗内存。概率值要先经历sigmoidlogits本身不是概率。用averagebinary是因为我的目标是二分类如果用macro或micro指标含义会变。阈值threshold默认0.5但如果类别不平衡严重这个阈值往往不是最优的后面会调。训练主循环里每个epoch结束就在验证集上跑一次评估记录指标的变化epochs 30 best_auc 0.0 best_state_dict None for epoch in range(epochs): model.train() total_loss 0.0 for batch_x, batch_y in train_loader: batch_x batch_x.to(device) batch_y batch_y.to(device) optimizer.zero_grad() logits model(batch_x) loss loss_fn(logits, batch_y) loss.backward() optimizer.step() total_loss loss.item() * batch_x.size(0) avg_train_loss total_loss / len(train_loader.dataset) val_metrics evaluate_model(model, val_loader) print(fEpoch {epoch1}/{epochs} | Loss: {avg_train_loss:.4f} | fAUC: {val_metrics[auc]:.4f} | Recall: {val_metrics[recall]:.4f}) if val_metrics[auc] best_auc: best_auc val_metrics[auc] best_state_dict model.state_dict().copy()这段代码里model.state_dict().copy()是为了保存最优模型权重。很多人喜欢在整个训练结束后才保存最后一轮的权重但最后一轮往往不是验证集表现最好的那一轮尤其当学习率设置得偏大、模型在训练集上过拟合时。保存最佳状态可以在内存上稍微多点开销但效果是纯赚的。6. 这条链路上我踩过的坑提前给你排掉最后这部分我特别想多说几句。上述主链路跑通之后问题往往不出在代码逻辑上而是一些边缘细节里。下面四个坑我几乎每次都能遇到索性一次性写清楚。6.1 随机种子不设全局永远复现不了结果PyTorch的模型初始化、DataLoader的shuffle、Sklearn的train_test_split都涉及随机数。如果你不设置全局种子昨天跑出来的AUC今天可能变了这会给迭代造成很大的干扰。我的固定写法是import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42)额外提醒一点DataLoader的shuffle用的是全局随机状态不是它自己的seed参数所以你每次创建DataLoader时无法单独固定它的打乱顺序。全局种子设置好在大多数情况下就够了。如果你还需要更细的控制可以在创建DataLoader时传入generatortorch.Generator().manual_seed(42)。6.2 标准化必须先fit在训练集上不能在全体数据上和稀泥这个坑看起来人人都知道但在实际代码里特别容易犯。有人为了省事在切分数据之前对全量X做了一次StandardScaler然后才切割训练集和验证集心里觉得反正标准化是无监督的但这是典型的泄漏路径。原因很简单全量数据的均值和方差是包含了验证集、测试集信息的当你用这个scaler去变换训练集时模型已经间接看到了未来的数据分布。正确顺序就是前面写的切分数据 -fit_transform训练集 -transform验证集和测试集。同样道理也适用于缺失值填充的median、SimpleImputer的most_frequent。只要是Sklearn的Transformer对象都要遵循这套逻辑。6.3 类别不平衡过采样做在PyTorch端更顺手当正样本比例只有5%时常规做法是在训练集上做SMOTE过采样或降低负样本权重。Sklearn端有现成的imbalanced-learn库可以做SMOTE但在sklearn端完成过采样后再喂给PyTorch容易在训练/验证集上造成泄漏因为你很难保证过采样只发生在训练集。更顺手的做法是在PyTorch端给每个batch的loss加权或者在创建DataLoader时直接用WeightedRandomSampler。from torch.utils.data import WeightedRandomSampler class_counts np.bincount(y_train_np.astype(int)) weights 1.0 / class_counts sample_weights weights[y_train_np.astype(int)] sampler WeightedRandomSampler( sample_weights, num_sampleslen(sample_weights), replacementTrue ) train_loader DataLoader( train_dataset, batch_size128, samplersampler, num_workers4 )这里用replacementTrue意味着每个epoch采样到的样本不一定相同小类别被抽到的概率变大了。用sampler之后DataLoader里的shuffle参数就不能同时设为True了这是PyTorch的既定规则。6.4 设备、dtype和numpy/tensor转换的隐式错误最后一个坑最隐蔽的。你在训练时把batch_x和batch_y都to(device)了但验证函数里忘记了或者你在计算指标时把tensor转numpy之前忘了卸载梯度。常见错误包括# 错误示例直接把带梯度的tensor转numpy y_prob torch.sigmoid(logits).numpy() # 如果logits需要梯度这里会报错正确的做法是torch.sigmoid(logits).detach().cpu().numpy()或者像我在评估函数里做的那样先把验证过程套进torch.no_grad()然后就不用担心梯度问题了。还有一个容易忽略的点在评估函数里拿到的是CPU复用后的结果如果你的验证集很大.cpu().numpy()会在内存里多copy一份数据量级很大时要注意内存峰值。我遇到过16G内存被验证集一次性吃满的情况后来改成分批算指标或直接累积在numpy数组里。设备管理也很重要如果你在GPU上训练所有输入和模型参数都要在同一个设备。一不留神把模型挪到GPU但数据还在CPUpyTorch会报错而且报错信息不太直观开头会给出一个Expected all tensors to be on the same device的提示。遇到这个提示直接检查你的batch_x和batch_y是否都to(device)了。最后再分享一个小经验这条链路走得越熟练越会觉得Sklearn和PyTorch的边界其实是模糊的。你不一定非要选一边站队重要的是手里有一条从脏数据到模型评估的通路。这篇文章是第一篇讲的就是这条主链路怎么打通等到下一篇我会接着聊如何在这条链路上做超参搜索和模型压模这同样是很多人在实际项目中卡住的地方。
阅读完成 · 觉得有帮助?