前言

很多数据科学家在做特征工程的时候都会遇到这样困惑,当样本的特征变量很多的时候,既有连续型的数值变量,或许其中还有空值,亦或许需要归一化处理,又有离散型的类别变量,或许其中有些类别很多很多,有些类别又只有几个,假使这些问题都能被耐心的你一步一步处理好,但是在部署时,针对全新的预测样本,又如何保证在线预测时候要做的特征处理与离线训练所做的特征处理保持一致性呢?这个时候就需要用到Pipeline了。

pipeline中文意思是管道,流水线,顾名思义就是让机器学习像流水线作业一样,一个典型的机器学习一般会包含如下几个步骤

  • 获取数据
  • 特征工程
  • 模型训练与调参
  • 模型部署与预测

pipeline
获取数据主要是指获取真实的数据供后续训练模型使用,特征工程是机器学习的重心所在,需要应对各种各样的情形分情况进行处理,或筛选或降维或重新组合,选出一些重要的特征参与模型训练,Pipeline与ColumnTransformer以及FeatureUnion结合能玩出什么花样呢?在训练模型的时候有需要对超参进行调节选定,利用最佳的超参进行模型训练,Pipeline与GridSearch结合能玩出什么把戏呢?训练评估好的模型,在后来的部署与在线预测时, Pipeline与pickle结合可以擦出什么火花呢?首先,看Pipeline与ColumnTransformer结合。

Pipeline与ColumnTransformer

ColumnTransformer顾名思义就是对特征列进行变换,将其转为模型可接受的形式,所有的特征列首先可以分为数值型列和类别型列两类,针对数值型列,往往需要对缺失值用或常数或均值或其他统计量进行填充,处理了缺失值然后可能还需要进行归一化处理;针对类别型列也需要缺失值填充,需要进行编码处理等,类别型列又可以分少类别列和多类别列,为了防止类别多的列编码时候维度爆炸需要用到OrdinaryEncoder编码,类别少的列可以OnehotEncoder编码,这样,又可以把所有列一分为三。缺失值填充和编码本是两个独立过程,因此可以用Pipeline将他们打包起来,按先后放入Pipeline的steps里面,最后把这些处理都打包放入ColumnTransformer。

from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import  OneHotEncoder, OrdinalEncoder
.........
.........
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, shuffle=True, random_state=0) #划分训练测试集
less_cat_col = [col_name for col_name in X_train.columns if X_train[col_name].dtype=='object' and X_train[col_name].nunique()<10] #少类别型变量
more_cat_col = [col_name for col_name in X_train.columns if X_train[col_name].dtype=='object' and X_train[col_name].nunique()>=10] #多类别型变量
num_col = [col_name for col_name in X_train.columns if X_train[col_name].dtype in ['int64', 'float64']] #数值型特征

# print(less_cat_col, more_cat_col, num_col)

less_cat_transform = Pipeline(steps = [('imputer', SimpleImputer(strategy='most_frequent')),
                                    ('encoder', OneHotEncoder(handle_unknown='ignore'))]
                            ) #类别型变量先用众数填充再独热编码
more_cat_transform = Pipeline(steps = [('imputer', SimpleImputer(strategy='most_frequent')),
                                    ('encoder', OrdinalEncoder(handle_unknown='use_encoded_value', unknown_value=-1))]
                            ) #类别型变量先用众数填充再普通编码

num_transform = SimpleImputer(strategy='mean') #数值型变量采用均值填充
preprocessor = ColumnTransformer(transformers = [('less_cat', less_cat_transform, less_cat_col),
                                            ('more_cat', more_cat_transform, more_cat_col),
                                        ('num', num_transform, num_col)]
                                ) #不同的预处理步骤打包到一起

从上面的代码可以看到我们对所有列分为三类less_cat_col,more_cat_col和num_col,针对这三类列,分别定义less_cat_transform ,more_cat_transform 和num_transform三种变换方式,少类别列less_cat_col前后分别采用最频繁的值填充空值,然后采用OneHotEncoder编码,多类别列more_cat_col先后分别采用最频繁的值填充空值,然后采有OrdinalEncoder编码,而数值型特征列按均值填充缺失值,最后将三类列和其对应的变换方式以三元组形式放入ColumnTransformer里的transformers列表 ,这样,该填充的填充了,该编码的编码了。

Pipeline与FeatureUnion

有时候我们会发现模型拟合的并不好,可能是因为目前使用的特征空间太单薄了,需要对特征列重构再组合,构造新的更加丰富的复合特征空间再参与模型训练。

from sklearn.pipeline import FeatureUnion
from sklearn.decomposition import PCA,KernelPCA,TruncatedSVD

combined= FeatureUnion(transformer_list = [('linear_pca',PCA(n_components = 3)),
('kernel_pca',KernelPCA(n_components = 5)),
("svd", TruncatedSVD(n_components=2))])
combined_X = combined.fit_transform(X)

从上面代码可以看到FeatureUnion合并了一个名叫linear_pca,一个名叫kernel_pca,一个名叫svd一共3个转换器,形成一个新的转换器combined,然后将新的转换器作用到原特征空间X上,相当于这3个转换器分别独立的作用在原来的特征空间X上,然后将他们的输出进行合并,输出的特征向量被横向连接成更多维的特征向量,这里由一个3维的特征向量,一个5维的特征向量和一个2维的特征向量进行合并成一个3+5+2=10维的特征向量,从而达到特征向量空间的扩维。

Pipeline与GridSearchCV

在训练模型的时候往往需要对一些超参数进行调节设定,好的超参会让模型事半功倍,超参的调节便成了很重要的一个环节,正因为Pipeline具有独立链接性,可以与GridSearchCV结合来自动寻参。

from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import  OneHotEncoder, OrdinalEncoder
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
.........
.........
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, shuffle=True, random_state=0) #划分训练测试集
less_cat_col = [col_name for col_name in X_train.columns if X_train[col_name].dtype=='object' and X_train[col_name].nunique()<10] #少类别型变量
more_cat_col = [col_name for col_name in X_train.columns if X_train[col_name].dtype=='object' and X_train[col_name].nunique()>=10] #多类别型变量
num_col = [col_name for col_name in X_train.columns if X_train[col_name].dtype in ['int64', 'float64']] #数值型特征
    # print(less_cat_col, more_cat_col, num_col)

less_cat_transform = Pipeline(steps = [('imputer', SimpleImputer(strategy='most_frequent')),
                                    ('encoder', OneHotEncoder(handle_unknown='ignore'))]
                            ) #类别型变量先用众数填充再独热编码
more_cat_transform = Pipeline(steps = [('imputer', SimpleImputer(strategy='most_frequent')),
                                    ('encoder', OrdinalEncoder(handle_unknown='use_encoded_value', unknown_value=-1))]
                            ) #类别型变量先用众数填充再普通编码

num_transform = SimpleImputer(strategy='mean') #数值型变量采用均值填充
preprocessor = ColumnTransformer(transformers = [('less_cat', less_cat_transform, less_cat_col),
                                            ('more_cat', more_cat_transform, more_cat_col),
                                        ('num', num_transform, num_col)]
                                ) #不同的预处理步骤打包到一起
 
model = GradientBoostingRegressor(random_state=0) # 
pipe = Pipeline(steps=[('preprocessing', preprocessor),
                    ('model', model)]
                )
params = {
        'model__n_estimators':[100, 200, 300],
        'model__learning_rate':[0.01, 0.05, 0.1],
        'model__max_depth': [3, 5, 7, 9,],
        'model__max_features':[5, 7, 11,  14],
        'model__min_samples_leaf': [1, 2, 3]
    }
gs = GridSearchCV(pipe, param_grid = params)
gs.fit(X_train, y_train)
print(gs.best_params_)
y_pred = gs.best_estimator_.predict(X_test)
MAE = mean_absolute_error(y_test, y_pred) #平均绝对误差
score = gs.score(X_test, y_test)
print("平均绝对误差和得分", MAE, score)

从上面代码可以看到,首先把预处理和模型放入一个Pipeline里面,然后构造一个超参数网params ,这里用到GradientBoostingRegressor模型,主要要对其中5个重要的超参进行设定,紧接着调用GridSearchCV,传入超参数网,然后对模型进行训练打印出模型最佳拟合的超参数列表如下

n_estimators : 300, learning_rate : 0.1, max_depth : 5, max_features : 14, min_samples_leaf : 3

Pipeline与pickle

精心训练好的模型如何永久保存下来呢,比较简单的就是对其进行序列化,保存为本地的pickle文件。

from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import  OneHotEncoder, OrdinalEncoder
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.metrics import mean_absolute_error
import pickle
.........
.........
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, shuffle=True, random_state=0) #划分训练测试集
less_cat_col = [col_name for col_name in X_train.columns if X_train[col_name].dtype=='object' and X_train[col_name].nunique()<10] #少类别型变量
more_cat_col = [col_name for col_name in X_train.columns if X_train[col_name].dtype=='object' and X_train[col_name].nunique()>=10] #多类别型变量
num_col = [col_name for col_name in X_train.columns if X_train[col_name].dtype in ['int64', 'float64']] #数值型特征
    # print(less_cat_col, more_cat_col, num_col)

less_cat_transform = Pipeline(steps = [('imputer', SimpleImputer(strategy='most_frequent')),
                                    ('encoder', OneHotEncoder(handle_unknown='ignore'))]
                            ) #类别型变量先用众数填充再独热编码
more_cat_transform = Pipeline(steps = [('imputer', SimpleImputer(strategy='most_frequent')),
                                    ('encoder', OrdinalEncoder(handle_unknown='use_encoded_value', unknown_value=-1))]
                            ) #类别型变量先用众数填充再普通编码

num_transform = SimpleImputer(strategy='mean') #数值型变量采用均值填充
preprocessor = ColumnTransformer(transformers = [('less_cat', less_cat_transform, less_cat_col),
                                            ('more_cat', more_cat_transform, more_cat_col),
                                        ('num', num_transform, num_col)]
                                ) #不同的预处理步骤打包到一起
model = GradientBoostingRegressor(n_estimators = 300, learning_rate = 0.1, max_depth = 5, min_samples_leaf= 3, random_state=0) # 模型初始化
pipe = Pipeline(steps=[('preprocessing', preprocessor),
                    ('model', model)]
                )
pipe.fit(X_train, y_train)
y_pred = pipe.predict(X_test)
MAE = mean_absolute_error(y_test, y_pred) #平均绝对误差
score = pipe.score(X_test, y_test)
print(pipe.named_steps['preprocessing']._feature_names_in)
print("mean_absolute_error: {}, and model score: {}".format(MAE, score))
with open(r'D:\项目\psg_melt_strategy.pickle', "wb") as model_file: #保存模型
        pickle.dump(pipe, model_file)

从上面的代码可以看到,我们将训练好的模型保存为psg_melt_strategy.pickle,该序列化文件不仅保存了模型,而且还保存了对应的特征工程处理,在线预测的时候只需要将其加载起来进行预测即可,这段代码可以作为模板套到很多机器学习里面去,囊括了从特征工程到模型的保存,流水作业,行云流水一气呵成。

Pipeline的优缺点

  • 优点

1,实现了对机器学习多个环节的封装和管理,使其井然有序,代码更加简洁;
2,用Pipeline封装的机器学习过程更像堆积木,彼此独立又链接,可以单独对某一个环节进行调整,提高了灵活性;
3,保存的序列化文件保证离线训练和在线预测的一致性,免去在线预测时繁琐的特征处理工作;

  • 缺点

1,在用FeatureUnion并联多个变换器的时候,没办法检查两个变换器是否会产出相同的特征或者高度关联的特征,可能带来共线性的副作用。
2,Pipeline只是一种流程的优化方案,不是算法本质的提升,欲提高精准度还需要深耕算法本身。

参考文献

1,https://zhuanlan.zhihu.com/p/395777245
2,https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html
3,https://blog.csdn.net/zengbowengood/article/details/120778942?spm=1001.2014.3001.5501
4,https://www.cnblogs.com/xiaoyunbowen/p/15312580.html
5,https://zhuanlan.zhihu.com/p/399100131?utm_id=0

在这里插入图片描述

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐