Pipeline, ColumnTransformer和FeatureUnion

做者|Zolzaya Luvsandorj
编译|VK
来源|Towards Datas Sciencehtml

掌握sklearn必须知道这三个强大的工具。所以,在创建机器学习模型时,学习如何有效地使用这些方法是相当重要的。python

在深刻讨论以前,咱们先从两个方面着手:git

  • Transformer:Transformer是指具备fit()和transform()方法的对象,用于清理、减小、扩展或生成特征。简单地说,transformers帮助你将数据转换为机器学习模型所需的格式。OneHotEncoder和MinMaxScaler就是Transformer的例子。github

  • Estimator:Estimator是指机器学习模型。它是一个具备fit()和predict()方法的对象。咱们将交替使用模型和Estimator这2个术语。该连接是一些Estimator的例子:https://scikit-learn.org/stable/tutorial/machine_learning_map/index.html。sql

安装

若是你想在你电脑上运行代码,确保你已经安装了pandas,seaborn和sklearn。我在Jupyter notebook中在python3.7.1中编写脚本。app

让咱们导入所需的库和数据集。关于这个数据集(包括数据字典)的详细信息能够在这里找到(这个源其实是针对R的,可是它彷佛引用了相同的底层数据集):https://vincentarelbundock.github.io/Rdatasets/doc/reshape2/tips.html。dom

# 设置种子
seed = 123

# 为数据导入包/模块
import pandas as pd
from seaborn import load_dataset

# 为特征工程和建模导入模块
from sklearn.model_selection import train_test_split
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.preprocessing import OneHotEncoder, MinMaxScaler
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline, FeatureUnion
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LinearRegression

# 加载数据集
df = load_dataset('tips').drop(columns=['tip', 'sex']).sample(n=5, random_state=seed)

# 添加缺失的值
df.iloc[[1, 2, 4], [2, 4]] = np.nan
df

使用少许的记录能够很容易地监控每一个步骤的输入和输出。所以,咱们将只使用数据集中5条记录的样本。机器学习

管道

假设咱们想用smoker、day和time列来预测总的帐单。咱们将先删除size列并对数据进行划分:工具

# 划分数据
X_train, X_test, y_train, y_test = train_test_split(df.drop(columns=['total_bill', 'size']), 
                                                    df['total_bill'], 
                                                    test_size=.2, 
                                                    random_state=seed)

一般状况下,原始数据不是咱们能够直接将其输入机器学习模型的状态。所以,将数据转换为可接受且对模型有用的状态成为建模的必要先决条件。让咱们作如下转换做为准备:学习

  1. 用“missing”填充缺失值

  2. one-hot编码

如下完成这两个步骤:

# 输入训练数据
imputer = SimpleImputer(strategy='constant', fill_value='missing')
X_train_imputed = imputer.fit_transform(X_train)

# 编码训练数据
encoder = OneHotEncoder(handle_unknown='ignore', sparse=False)
X_train_encoded = encoder.fit_transform(X_train_imputed)

# 检查训练先后的数据
print("******************** Training data ********************")
display(X_train)
display(pd.DataFrame(X_train_imputed, columns=X_train.columns))
display(pd.DataFrame(X_train_encoded, columns=encoder.get_feature_names(X_train.columns)))

# 转换测试数据
X_test_imputed = imputer.transform(X_test)
X_test_encoded = encoder.transform(X_test_imputed)

# 检查测试先后的数据
print("******************** Test data ********************")
display(X_test)
display(pd.DataFrame(X_test_imputed, columns=X_train.columns))
display(pd.DataFrame(X_test_encoded, columns=encoder.get_feature_names(X_train.columns)))

你可能已经注意到,当映射回测试数据集的列名时,咱们使用了来自训练数据集的列名。这是由于我更喜欢使用来自于训练Transformer的数据的列名。可是,若是咱们使用测试数据集,它将给出相同的结果。

对于每一个数据集,咱们首先看到原始数据,而后是插补后的输出,最后是编码后的输出。

这种方法能够完成任务。可是,咱们将上一步的输出做为输入手动输入到下一步,而且有多个临时输出。咱们还必须在测试数据上重复每一步。随着步骤数的增长,维护将变得更加繁琐,更容易出错。

咱们可使用管道编写更精简和简洁的代码:

# 将管道与训练数据匹配
pipe = Pipeline([('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
                 ('encoder', OneHotEncoder(handle_unknown='ignore', sparse=False))])
pipe.fit(X_train)

# 检查训练先后的数据
print("******************** Training data ********************")
display(X_train)
display(pd.DataFrame(pipe.transform(X_train), columns=pipe['encoder'].get_feature_names(X_train.columns)))

# 检查测试先后的数据
print("******************** Test data ********************")
display(X_test)
display(pd.DataFrame(pipe.transform(X_test), columns=pipe['encoder'].get_feature_names(X_train.columns)))

使用管道时,每一个步骤都将其输出做为输入传递到下一个步骤。所以,咱们没必要手动跟踪数据的不一样版本。这种方法为咱们提供了彻底相同的最终输出,可是使用了更优雅的代码。

在查看了转换后的数据以后,如今是在咱们的示例中添加模型的时候了。让咱们从为第一种方法添加一个简单模型:

# 输入训练数据
imputer = SimpleImputer(strategy='constant', fill_value='missing')
X_train_imputed = imputer.fit_transform(X_train)

# 编码训练数据
encoder = OneHotEncoder(handle_unknown='ignore', sparse=False)
X_train_encoded = encoder.fit_transform(X_train_imputed)

# 使模型拟合训练数据
model = LinearRegression()
model.fit(X_train_encoded, y_train)

# 预测训练数据
y_train_pred = model.predict(X_train_encoded)
print(f"Predictions on training data: {y_train_pred}")

# 转换测试数据
X_test_imputed = imputer.transform(X_test)
X_test_encoded = encoder.transform(X_test_imputed)

# 预测测试数据
y_test_pred = model.predict(X_test_encoded)
print(f"Predictions on test data: {y_test_pred}")

咱们将对管道方法进行一样的处理:

# 将管道与训练数据匹配
pipe = Pipeline([('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
                 ('encoder', OneHotEncoder(handle_unknown='ignore', sparse=False)), 
                 ('model', LinearRegression())])
pipe.fit(X_train, y_train)

# 预测训练数据
y_train_pred = pipe.predict(X_train)
print(f"Predictions on training data: {y_train_pred}")

# 预测测试数据
y_test_pred = pipe.predict(X_test)
print(f"Predictions on test data: {y_test_pred}")

你可能已经注意到,一旦咱们训练了一条管道,进行预测是多么简单。pipe.predict(X)对原始数据进行转换,而后返回预测。也很容易看到步骤的顺序。让咱们直观地总结一下这两种方法:

使用管道不只能够组织和简化代码,并且还有许多其余好处,下面是其中一些好处:

  • 微调管道的能力:当构建一个模型时,你可能须要尝试不一样的方法来预处理数据并再次运行模型,看看预处理步骤中的调整是否能提升模型的泛化能力。在优化模型时,微调不只存在于模型的超参数中,并且存在于预处理步骤的实现中。考虑到这一点,当咱们有一个统一了Transformer和Estimator的管道对象时,咱们能够微调整个管道的超参数,包括使用GridSearchCV或RandomizedSearchCV的Estimator和两个Transformer。

  • 更容易部署:在训练模型时用于准备数据的全部转换步骤在进行预测时也能够应用于生产环境中的数据。当咱们训练管道时,咱们训练一个包含数据转换器和模型的对象。一旦通过训练,这个管道对象就能够用于更平滑的部署。

ColumnTransformer

在前面的例子中,咱们以相同的方式对全部列进行插补和编码。可是,咱们常常须要对不一样的列组应用不一样的transformer。例如,咱们但愿将OneHotEncoder仅应用于分类列,而不该用于数值列。这就是ColumnTransformer的用武之地。

这一次,咱们将对保留全部列的数据集进行分区,以便同时具备数值和类别特征。

# 划分数据
X_train, X_test, y_train, y_test = train_test_split(df.drop(columns=['total_bill']), 
                                                    df['total_bill'], 
                                                    test_size=.2, 
                                                    random_state=seed)

# 定义分类列
categorical = list(X_train.select_dtypes('category').columns)
print(f"Categorical columns are: {categorical}")

# 定义数字列
numerical = list(X_train.select_dtypes('number').columns)
print(f"Numerical columns are: {numerical}")

咱们根据数据类型将特征分为两组。列分组能够根据数据的适当状况进行。例如,若是不一样的预处理管道更适合分类列,则能够将它们进一步拆分为多个组。

上一节的代码如今将再也不工做,由于咱们有多个数据类型。让咱们看一个例子,其中咱们使用ColumnTransformer和Pipeline在存在多个数据类型的状况下执行与以前相同的转换。

# 定义分类管道
cat_pipe = Pipeline([('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
                     ('encoder', OneHotEncoder(handle_unknown='ignore', sparse=False))])

# 使ColumnTransformer拟合训练数据
preprocessor = ColumnTransformer(transformers=[('cat', cat_pipe, categorical)], 
                                 remainder='passthrough')
preprocessor.fit(X_train)

# 准备列名
cat_columns = preprocessor.named_transformers_['cat']['encoder'].get_feature_names(categorical)
columns = np.append(cat_columns, numerical)

# 检查训练先后的数据
print("******************** Training data ********************")
display(X_train)
display(pd.DataFrame(preprocessor.transform(X_train), columns=columns))

# 检查测试先后的数据
print("******************** Test data ********************")
display(X_test)
display(pd.DataFrame(preprocessor.transform(X_test), columns=columns))

分类列的输出与上一节的输出相同。惟一的区别是这个版本有一个额外的列:size。咱们已经将cat_pipe(在上一节中称为pipe)传递给ColumnTransformer来转换分类列,并指定remainment='passthrough'以保持其他列不变。

让咱们用中值填充缺失值,并将其缩放到0和1之间:

# 定义分类管道
cat_pipe = Pipeline([('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
                     ('encoder', OneHotEncoder(handle_unknown='ignore', sparse=False))])

# 定义数值管道
num_pipe = Pipeline([('imputer', SimpleImputer(strategy='median')),
                     ('scaler', MinMaxScaler())])

# 使ColumnTransformer拟合训练数据
preprocessor = ColumnTransformer(transformers=[('cat', cat_pipe, categorical),
                                               ('num', num_pipe, numerical)])
preprocessor.fit(X_train)

# 准备列名
cat_columns = preprocessor.named_transformers_['cat']['encoder'].get_feature_names(categorical)
columns = np.append(cat_columns, numerical)

# 检查训练先后的数据
print("******************** Training data ********************")
display(X_train)
display(pd.DataFrame(preprocessor.transform(X_train), columns=columns))

# 检查测试先后的数据
print("******************** Test data ********************")
display(X_test)
display(pd.DataFrame(preprocessor.transform(X_test), columns=columns))

如今全部列都被插补,范围在0到1之间。使用ColumnTransformer和Pipeline,咱们将数据分红两组,将不一样的管道和不一样的Transformer应用到每组,而后将结果粘贴在一块儿:

尽管在咱们的示例中,数值管道和分类管道中的步骤数相同,但管道中能够有任意数量的步骤,而且不一样列子集的步骤数没必要相同。如今咱们将一个模型添加到咱们的示例中:

# 定义分类管道
cat_pipe = Pipeline([('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
                     ('encoder', OneHotEncoder(handle_unknown='ignore', sparse=False))])

# 定义数值管道
num_pipe = Pipeline([('imputer', SimpleImputer(strategy='median')),
                     ('scaler', MinMaxScaler())])

# 组合分类管道和数值管道
preprocessor = ColumnTransformer(transformers=[('cat', cat_pipe, categorical),
                                               ('num', num_pipe, numerical)])

# 在管道上安装transformer和训练数据的estimator
pipe = Pipeline(steps=[('preprocessor', preprocessor),
                       ('model', LinearRegression())])
pipe.fit(X_train, y_train)

# 预测训练数据
y_train_pred = pipe.predict(X_train)
print(f"Predictions on training data: {y_train_pred}")

# 预测测试数据
y_test_pred = pipe.predict(X_test)
print(f"Predictions on test data: {y_test_pred}")

为了将ColumnTransformer中指定的预处理步骤与模型结合起来,咱们在外部使用了一个管道。如下是它的视觉表现:

当咱们须要对不一样的列子集执行不一样的操做时,ColumnTransformer很好地补充了管道。

FeatureUnion

如下代码的输出在本节中被省略,由于它们与ColumnTransformer章节的输出相同。

FeatureUnion是另外一个有用的工具。它能够作ColumnTransformer刚刚作过的事情,但要作得更远:

# 自定义管道
class ColumnSelector(BaseEstimator, TransformerMixin):
    """Select only specified columns."""
    def __init__(self, columns):
        self.columns = columns
        
    def fit(self, X, y=None):
        return self
    
    def transform(self, X):
        return X[self.columns]

# 定义分类管道
cat_pipe = Pipeline([('selector', ColumnSelector(categorical)),
                     ('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
                     ('encoder', OneHotEncoder(handle_unknown='ignore', sparse=False))])

# 定义数值管道
num_pipe = Pipeline([('selector', ColumnSelector(numerical)),
                     ('imputer', SimpleImputer(strategy='median')),
                     ('scaler', MinMaxScaler())])

# FeatureUnion拟合训练数据
preprocessor = FeatureUnion(transformer_list=[('cat', cat_pipe),
                                              ('num', num_pipe)])
preprocessor.fit(X_train)

# 准备列名
cat_columns = preprocessor.transformer_list[0][1][2].get_feature_names(categorical)
columns = np.append(cat_columns, numerical)

# 检查训练先后的数据
print("******************** Training data ********************")
display(X_train)
display(pd.DataFrame(preprocessor.transform(X_train), columns=columns))

# 检查测试先后的数据
print("******************** Test data ********************")
display(X_test)
display(pd.DataFrame(preprocessor.transform(X_test), columns=columns))

咱们能够将FeatureUnion视为建立数据的副本,并行地转换这些副本,而后将结果粘贴在一块儿。这里的术语副本更像是一种辅助概念化的类比,而不是实际采用的技术。

在每一个管道的开始,咱们添加了一个额外的步骤,在这里咱们使用一个定制的转换器来选择相关的列:第14行和第19行的ColumnSelector。下面是咱们可视化上面的脚本的图:

如今,是时候向脚本添加模型了:

# 定义分类管道
cat_pipe = Pipeline([('selector', ColumnSelector(categorical)),
                     ('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
                     ('encoder', OneHotEncoder(handle_unknown='ignore', sparse=False))])

# 定义数值管道
num_pipe = Pipeline([('selector', ColumnSelector(numerical)),
                     ('imputer', SimpleImputer(strategy='median')),
                     ('scaler', MinMaxScaler())])

# 组合分类管道和数值管道
preprocessor = FeatureUnion(transformer_list=[('cat', cat_pipe),
                                              ('num', num_pipe)])

# 组合分类管道和数值管道
pipe = Pipeline(steps=[('preprocessor', preprocessor),
                       ('model', LinearRegression())])
pipe.fit(X_train, y_train)

# 预测训练数据
y_train_pred = pipe.predict(X_train)
print(f"Predictions on training data: {y_train_pred}")

# 预测测试数据
y_test_pred = pipe.predict(X_test)
print(f"Predictions on test data: {y_test_pred}")

它看起来很像咱们用ColumnTransformer作的。

如本例所示,使用FeatureUnion比使用ColumnTransformer要复杂得多。所以,在我看来,在相似的状况下最好使用ColumnTransformer。

然而,FeatureUnion确定有它的位置。若是你须要以不一样的方式转换相同的输入数据并将它们用做特征,FeatureUnion就是其中之一。例如,若是你正在处理一个文本数据,而且但愿对数据进行tf-idf矢量化以及提取文本长度,FeatureUnion是一个完美的工具。

总结

你可能已经注意到,Pipeline是超级明星。ColumnTransformer和FeatureUnion是用于管道的附加工具。ColumnTransformer更适合于并行划分,而FeatureUnion容许咱们在同一个输入数据上并行应用多个转换器。下面是一个简单的总结:

谢谢你阅读个人帖子。但愿这篇文章能帮助你更多地了解这些有用的工具。我但愿你能在你的数据科学项目中使用它们。若是你感兴趣,如下是个人一些帖子的连接:

原文连接:https://towardsdatascience.com/vectorizing-code-matters-66c5f95ddfd5

欢迎关注磐创AI博客站:
http://panchuang.net/

sklearn机器学习中文官方文档:
http://sklearn123.com/

欢迎关注磐创博客资源汇总站:
http://docs.panchuang.net/