美文网首页
Pipeline 深入解读及实验

Pipeline 深入解读及实验

作者: 马尔代夫Maldives | 来源:发表于2024-01-01 22:24 被阅读0次

一、Pipeline基本知识

Pipeline 对象将不同计算步骤串联起来执行,方便、快速、统一、保险!

构造一个Pipeline对象时,需要输入一个由多个二元tuple构成的list,每个tuple的第一个元素可以是任意自定义字符串,第二元素表示一个已经存在的计算步骤,例:
my_steps = [(name1, algo1), (name2, algo2), (name3, algo3)...] # 定义计算步骤list
pipe = Pipline(step=my_steps) # 生成Pipline对象

这些计算步骤(algo)通常有3种类型:变换器(transformer)估计器(estimator)'passthrough'

  • 变换器(transformer)
    (1)变换器通常具有fit()、transform()、fit_transform()三个方法,StandardScaler()、PCA()等都属于变换器。
    (2)其中fit()根据输入数据X计算参数S(并保存S),然后 transform()根据参数S对数据(X或X_new)做变换,并返回变换结果。
    (3)Pipeline对象中,多个变换器串联,其本质是将每个变换器按照步骤(2)的方式串起来,即:
       第1个变换器T1:利用T1.fit(X)计算并保存S1,然后基于S1计算 result1 = T1.transform(X);
       第2个变换器T2:利用T2.fit(result1)计算并保存S2,然后基于S2计算 result2 = T2.transform(result1);
       ……
    (4)Pipeline对象中需要保证前一个变换器的输出可以作为后一个变换器的输入,否则报错

  • 估计器(estimator)
    (1)通常是机器学习算法(比如线性回归、支持向量机、决策树等),通常有fit()和predict()方法。
    (2)在一个Pipeline中估计器通常只有一个,且放在最后。

  • 'passthrough'
    表示这一步啥也不干!

Pipeline的使用方法:

  • Pipeline对象(本文用pipe表示)可以像普通变换器一样使用pipe.fit()、pipe.transform()、pipe.fit_transform()或像普通估计器一样使用pipe.predict()方法,其前提是pipe内部这些变换器都实现了前三种方法和估计器实现了第四种方法,否则出错。
  • Pipeline的使用模式通常有2种:
    (1)模式1:只有n个变换器(transformer)没有估计器(estimator)
       这种模式主要用于对数据进行变换,但不包括对变换后的结果进行分类或拟合等操作(这是估计器干的事)。
    模式1.png
    (2)模式2:前n个变换器(transformer)+ 最后一个估计器(estimator)
       这种模式就是在第(1)种模式上加了估计器,从而实现分类或拟合(估计器一般只有1个,且放在最后,否则会引起错误。)
    模式2.png
    (注意:上述只是伪代码,实际使用时还有其他各种参数,同时也还有很多其他方法!)
    (3)Pipline的高级使用方法:请搜索其他文档!
       与GridSearchCV、make_union、FeatureUnion结合。

二、Pipeline内部计算过程窥探(包括一些结论)

下面我们自定义两个变换器(实际上也是估计器,因为我们都定义了predict()函数),并组成Pipeline,来窥探其内部执行过程!

import numpy as np
from sklearn.base import TransformerMixin, BaseEstimator
from sklearn.pipeline import Pipeline

定义变换器:

说明:我们用 self.flag 变量来代表‘计算参数S’,即fit(X)根据输入X计算得到的。

# 自定义变换器1
class T1(BaseEstimator, TransformerMixin):
    def __init__(self):
        self.flag = 'T1原始参数S1_old!'

    def fit(self, X, y=None):
        self.flag = 'T1 fit() 计算的新参数S1_new!'
        print(f'Run T1 fit(), 输入X={X},计算并保存新参数S1_new!')
        return self

    def transform(self, X, y=None):
        print(f'Run T1 transform(), 输入X={X}, 用 {self.flag}')
        return X*2
    
    def predict(self, X):
         print(f'Run T1 predict(), 输入X={X}, 用 {self.flag}')

# 自定义变换器2
class T2(BaseEstimator, TransformerMixin):
    
    def __init__(self):
        self.flag = '原始参数S2_old!'

    def fit(self, X, y=None):
        self.flag = 'T2 fit() 计算的新参数S2_new!'
        print(f'Run T2 fit(), 输入X={X},计算并保存新参数S2_new!')
        return self

    def transform(self, X, y=None):
        print(f'Run T2 transform(), 输入X={X}, 用 {self.flag}')
        return X*4
    
    def predict(self, X):
         print(f'Run T2 predict(), 输入X={X}, 用 {self.flag}')

例1:pipe.fit()→pipe.transform()→pipe.predict()

X = np.array([1,2,3,4,5])  # 数据准备

pipe = Pipeline([('name1', T1()), ('name2', T2())]) # 生成Pipline对象

pipe.fit(X)
print('#######################################分割线1################################################')
result = pipe.transform(X)
print(result)
print('#######################################分割线2################################################')
pipe.predict(X)

输出:
Run T1 fit(), 输入X=[1 2 3 4 5],计算并保存新参数S1_new!
Run T1 transform(), 输入X=[1 2 3 4 5], 用 T1 fit() 计算的新参数S1_new!
Run T2 fit(), 输入X=[ 2  4  6  8 10],计算并保存新参数S2_new!
#######################################分割线1################################################
Run T1 transform(), 输入X=[1 2 3 4 5], 用 T1 fit() 计算的新参数S1_new!
Run T2 transform(), 输入X=[ 2  4  6  8 10], 用 T2 fit() 计算的新参数S2_new!
[ 8 16 24 32 40]
#######################################分割线2################################################
Run T1 transform(), 输入X=[1 2 3 4 5], 用 T1 fit() 计算的新参数S1_new!
Run T2 predict(), 输入X=[ 2  4  6  8 10], 用 T2 fit() 计算的新参数S2_new!
  • 结论1:执行pipe.fit() 方法,实际上执行了所有内部变换器的fit()和transform(),但不会执行最后一个变换器的 transform() 方法
    这一点从分割线1前面的输出结果就可以看出!
    这是合理的,因为pipe.fit()阶段的主要目的是根据输入数据X计算所有步骤的参数S!这个过程在前面变换器(transformer)-(3)节已有详细描述。
    根据这个逻辑,最后一个变换器只要利用前一个变换器的transform()输出,再通过自己的fit()计算自己的参数S就可以了,它的transform()没有执行的必要了,因为后面没有其他步骤需要计算参数了!
  • 结论2:使用pipe.fit() 方法后,会自动保存计算参数,留作后用
    从所有输出结果可知,并没有出现原始参数S1_old!原始参数S2_old!,都是S1_newS2_new,说明在执行pipe.fit(X)时,确实保留了新计算参数!
  • 结论3:在pipe.fit() 后调用pipe.transform(),则会把所有计算步骤的transform()都执行一遍,并输出最终结果,且每一步用的是每个计算步骤的fit()所计算和保存的参数S
    这点从分割线1和2之间的输出结果可以看出!
  • 结论4:执行pipe.predict()时,实际上执行的是pipe中最后一个计算步骤的predict()(也就是说如果要用pipe.predict(),那么就要求最后一个计算步骤必须有predict()这个方法,否则报错);且在执行predict()之前,排在其前面的所有步骤的transform()方法都会被执行一遍
    这个结论的前半部分可以通过将T2定义中的 def predict(self, X):部分全部删除来验证,结果会报错;结论的后半部分,从分割线2下的输出可以看出!很显然,T2 的 transform()没有被执行,因为它与最后一个predict()处于同一个类中,pipeline首先将其视为估计器,自动忽略了T2 的 transform()。实际中,真实的估计器,都是SVM、LR等机器学习方法,通常不会出现transform()和predict()同时出现在一个类中的情况。

例2:pipe.fit_transform()→pipe.predict()

X = np.array([1,2,3,4,5])  # 数据准备

pipe = Pipeline([('name1', T1()), ('name2', T2())]) # 生成Pipline对象

result = pipe.fit_transform(X)
print(result)
print('#######################################分割线1################################################')
pipe.predict(X)

输出:
Run T1 fit(), 输入X=[1 2 3 4 5],计算并保存新参数S1_new!
Run T1 transform(), 输入X=[1 2 3 4 5], 用 T1 fit() 计算的新参数S1_new!
Run T2 fit(), 输入X=[ 2  4  6  8 10],计算并保存新参数S2_new!
Run T2 transform(), 输入X=[ 2  4  6  8 10], 用 T2 fit() 计算的新参数S2_new!
[ 8 16 24 32 40]
#######################################分割线1################################################
Run T1 transform(), 输入X=[1 2 3 4 5], 用 T1 fit() 计算的新参数S1_new!
Run T2 predict(), 输入X=[ 2  4  6  8 10], 用 T2 fit() 计算的新参数S2_new!
  • 结论5:pipe.fit_transform(X)除了执行了pipe.fit(X)和pipe.transform(X)所执行的所有步骤外,还执行了最后一个变换器的transform(),并输出了最终结果
    比较例1和2两种情况分割线1前面部分就知道了。
  • 结论6:pipe.fit()、pipe.transform()、pipe.fit_transform()这几个方法要正常执行,要求pipe中每个计算步骤内部都实现了fit()和transform()方法,否则会报错。
    这点可以通过删除T1或T2中的一个或多个fit()或transform()函数得到验证。
  • 一个pipeline对象可以作为另一个pipeline对象的一个计算步骤。
    例子省略。

三、一个比较完整的Pipeline例子

对鸢尾花数据集进行分类:

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression

from sklearn.datasets import load_iris  # 自带的样本数据集
from sklearn.model_selection import train_test_split
iris = load_iris()  # 导入数据

X = iris.data  # 150个样本,4个属性
y = iris.target # 150个类标号

# 将数据集划分为训练集和验证集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 生成pipeline对象
pipe = Pipeline([('preprocessing','passthrough'),
                 ('sc', StandardScaler()),
                 ('pca', PCA(n_components=2)),
                 ('clf', LogisticRegression(random_state=1))])
# 利用pipeline对象训练模型
pipe.fit(X_train, y_train)
# 利用测试数据对模型进行评价
pipe.score(X_test,y_test)
输出:
0.9
# 利用训练好的模型对新数据进行预测(这里用测试数据代替)
pipe.predict(X_test)
输出:
array([1, 0, 2, 1, 2, 0, 1, 2, 2, 1, 2, 0, 0, 0, 0, 1, 2, 1, 1, 2, 0, 1, 0, 2, 2, 2, 2, 2, 0, 0])

上述例子中,在定义pipe对象时,用了四个计算步骤:其中第1个是'passthrough',啥也不做;第2、3个分别是标准化处理和PCA分解,都属于变换器(transformer);第4个是逻辑回归,属于估计器(estimator)。

我们继续做实验,在pipe步骤中做两种修改:1)估计器不放在最后位置;2)有两个估计器,如下所示:

# 修改1:把估计器(逻辑回归)放在非最后位置
pipe = Pipeline([('preprocessing','passthrough'),
                 ('sc', StandardScaler()),
                 ('clf', LogisticRegression(random_state=1)),
                 ('pca', PCA(n_components=2))])

# 修改2:把增加一个估计器(逻辑回归clf和clf2)
pipe = Pipeline([('preprocessing','passthrough'),
                 ('sc', StandardScaler()),
                 ('pca', PCA(n_components=2)),
                 ('clf', LogisticRegression(random_state=1)),
                 ('clf2', LogisticRegression(random_state=1))])

上述两种改变,在定义时都不会报错,但在执行pipe.fit(X_train, y_train)时都会报错:

pipe.fit(X_train, y_train)
输出:
 TypeError: All intermediate steps should be transformers and implement fit and transform or be the string 'passthrough' 'LogisticRegression(random_state=1)' (type <class 'sklearn.linear_model._logistic.LogisticRegression'>) doesn't

由此可见:

  • 估计器(estimator)通常需要放在Pipeline对象的最后位置;
  • 一个Pipeline对象中不能有多个估计器(estimator)。

参考:
https://blog.csdn.net/nkufang/article/details/129973061

相关文章

网友评论

      本文标题:Pipeline 深入解读及实验

      本文链接:https://www.haomeiwen.com/subject/elwrndtx.html