Pipeline 深入解讀及實驗

一、Pipeline基本知識

Pipeline 對象將不同計算步驟串聯(lián)起來執(zhí)行,方便、快速、統(tǒng)一、保險!

構(gòu)造一個Pipeline對象時,需要輸入一個由多個二元tuple構(gòu)成的list,每個tuple的第一個元素可以是任意自定義字符串,第二元素表示一個已經(jīng)存在的計算步驟,例:
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()根據(jù)輸入數(shù)據(jù)X計算參數(shù)S(并保存S),然后 transform()根據(jù)參數(shù)S對數(shù)據(jù)(X或X_new)做變換,并返回變換結(jié)果。
    (3)Pipeline對象中,多個變換器串聯(lián),其本質(zhì)是將每個變換器按照步驟(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)通常是機(jī)器學(xué)習(xí)算法(比如線性回歸、支持向量機(jī)、決策樹等),通常有fit()和predict()方法。
    (2)在一個Pipeline中估計器通常只有一個,且放在最后。

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

Pipeline的使用方法:

  • Pipeline對象(本文用pipe表示)可以像普通變換器一樣使用pipe.fit()、pipe.transform()、pipe.fit_transform()或像普通估計器一樣使用pipe.predict()方法,其前提是pipe內(nèi)部這些變換器都實現(xiàn)了前三種方法和估計器實現(xiàn)了第四種方法,否則出錯。
  • Pipeline的使用模式通常有2種:
    (1)模式1:只有n個變換器(transformer)沒有估計器(estimator)
    ?? 這種模式主要用于對數(shù)據(jù)進(jìn)行變換,但不包括對變換后的結(jié)果進(jìn)行分類或擬合等操作(這是估計器干的事)。
    模式1.png

    (2)模式2:前n個變換器(transformer)+ 最后一個估計器(estimator)
    ?? 這種模式就是在第(1)種模式上加了估計器,從而實現(xiàn)分類或擬合(估計器一般只有1個,且放在最后,否則會引起錯誤。)
    模式2.png

    (注意:上述只是偽代碼,實際使用時還有其他各種參數(shù),同時也還有很多其他方法!)
    (3)Pipline的高級使用方法:請搜索其他文檔!
    ?? 與GridSearchCV、make_union、FeatureUnion結(jié)合。

二、Pipeline內(nèi)部計算過程窺探(包括一些結(jié)論)

下面我們自定義兩個變換器(實際上也是估計器,因為我們都定義了predict()函數(shù)),并組成Pipeline,來窺探其內(nèi)部執(zhí)行過程!

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

定義變換器:

說明:我們用 self.flag 變量來代表‘計算參數(shù)S’,即fit(X)根據(jù)輸入X計算得到的。

# 自定義變換器1
class T1(BaseEstimator, TransformerMixin):
    def __init__(self):
        self.flag = 'T1原始參數(shù)S1_old!'

    def fit(self, X, y=None):
        self.flag = 'T1 fit() 計算的新參數(shù)S1_new!'
        print(f'Run T1 fit(), 輸入X={X},計算并保存新參數(shù)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 = '原始參數(shù)S2_old!'

    def fit(self, X, y=None):
        self.flag = 'T2 fit() 計算的新參數(shù)S2_new!'
        print(f'Run T2 fit(), 輸入X={X},計算并保存新參數(shù)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])  # 數(shù)據(jù)準(zhǔn)備

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],計算并保存新參數(shù)S1_new!
Run T1 transform(), 輸入X=[1 2 3 4 5], 用 T1 fit() 計算的新參數(shù)S1_new!
Run T2 fit(), 輸入X=[ 2  4  6  8 10],計算并保存新參數(shù)S2_new!
#######################################分割線1################################################
Run T1 transform(), 輸入X=[1 2 3 4 5], 用 T1 fit() 計算的新參數(shù)S1_new!
Run T2 transform(), 輸入X=[ 2  4  6  8 10], 用 T2 fit() 計算的新參數(shù)S2_new!
[ 8 16 24 32 40]
#######################################分割線2################################################
Run T1 transform(), 輸入X=[1 2 3 4 5], 用 T1 fit() 計算的新參數(shù)S1_new!
Run T2 predict(), 輸入X=[ 2  4  6  8 10], 用 T2 fit() 計算的新參數(shù)S2_new!
  • 結(jié)論1:執(zhí)行pipe.fit() 方法,實際上執(zhí)行了所有內(nèi)部變換器的fit()和transform(),但不會執(zhí)行最后一個變換器的 transform() 方法
    這一點從分割線1前面的輸出結(jié)果就可以看出!
    這是合理的,因為pipe.fit()階段的主要目的是根據(jù)輸入數(shù)據(jù)X計算所有步驟的參數(shù)S!這個過程在前面變換器(transformer)-(3)節(jié)已有詳細(xì)描述。
    根據(jù)這個邏輯,最后一個變換器只要利用前一個變換器的transform()輸出,再通過自己的fit()計算自己的參數(shù)S就可以了,它的transform()沒有執(zhí)行的必要了,因為后面沒有其他步驟需要計算參數(shù)了!
  • 結(jié)論2:使用pipe.fit() 方法后,會自動保存計算參數(shù),留作后用
    從所有輸出結(jié)果可知,并沒有出現(xiàn)原始參數(shù)S1_old!原始參數(shù)S2_old!,都是S1_newS2_new,說明在執(zhí)行pipe.fit(X)時,確實保留了新計算參數(shù)!
  • 結(jié)論3:在pipe.fit() 后調(diào)用pipe.transform(),則會把所有計算步驟的transform()都執(zhí)行一遍,并輸出最終結(jié)果,且每一步用的是每個計算步驟的fit()所計算和保存的參數(shù)S
    這點從分割線1和2之間的輸出結(jié)果可以看出!
  • 結(jié)論4:執(zhí)行pipe.predict()時,實際上執(zhí)行的是pipe中最后一個計算步驟的predict()(也就是說如果要用pipe.predict(),那么就要求最后一個計算步驟必須有predict()這個方法,否則報錯);且在執(zhí)行predict()之前,排在其前面的所有步驟的transform()方法都會被執(zhí)行一遍
    這個結(jié)論的前半部分可以通過將T2定義中的 def predict(self, X):部分全部刪除來驗證,結(jié)果會報錯;結(jié)論的后半部分,從分割線2下的輸出可以看出!很顯然,T2 的 transform()沒有被執(zhí)行,因為它與最后一個predict()處于同一個類中,pipeline首先將其視為估計器,自動忽略了T2 的 transform()。實際中,真實的估計器,都是SVM、LR等機(jī)器學(xué)習(xí)方法,通常不會出現(xiàn)transform()和predict()同時出現(xiàn)在一個類中的情況。

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

X = np.array([1,2,3,4,5])  # 數(shù)據(jù)準(zhǔn)備

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],計算并保存新參數(shù)S1_new!
Run T1 transform(), 輸入X=[1 2 3 4 5], 用 T1 fit() 計算的新參數(shù)S1_new!
Run T2 fit(), 輸入X=[ 2  4  6  8 10],計算并保存新參數(shù)S2_new!
Run T2 transform(), 輸入X=[ 2  4  6  8 10], 用 T2 fit() 計算的新參數(shù)S2_new!
[ 8 16 24 32 40]
#######################################分割線1################################################
Run T1 transform(), 輸入X=[1 2 3 4 5], 用 T1 fit() 計算的新參數(shù)S1_new!
Run T2 predict(), 輸入X=[ 2  4  6  8 10], 用 T2 fit() 計算的新參數(shù)S2_new!
  • 結(jié)論5:pipe.fit_transform(X)除了執(zhí)行了pipe.fit(X)和pipe.transform(X)所執(zhí)行的所有步驟外,還執(zhí)行了最后一個變換器的transform(),并輸出了最終結(jié)果
    比較例1和2兩種情況分割線1前面部分就知道了。
  • 結(jié)論6:pipe.fit()、pipe.transform()、pipe.fit_transform()這幾個方法要正常執(zhí)行,要求pipe中每個計算步驟內(nèi)部都實現(xiàn)了fit()和transform()方法,否則會報錯。
    這點可以通過刪除T1或T2中的一個或多個fit()或transform()函數(shù)得到驗證。
  • 一個pipeline對象可以作為另一個pipeline對象的一個計算步驟。
    例子省略。

三、一個比較完整的Pipeline例子

對鳶尾花數(shù)據(jù)集進(jìn)行分類:

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  # 自帶的樣本數(shù)據(jù)集
from sklearn.model_selection import train_test_split
iris = load_iris()  # 導(dǎo)入數(shù)據(jù)

X = iris.data  # 150個樣本,4個屬性
y = iris.target # 150個類標(biāo)號

# 將數(shù)據(jù)集劃分為訓(xùn)練集和驗證集
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對象訓(xùn)練模型
pipe.fit(X_train, y_train)
# 利用測試數(shù)據(jù)對模型進(jìn)行評價
pipe.score(X_test,y_test)
輸出:
0.9
# 利用訓(xùn)練好的模型對新數(shù)據(jù)進(jìn)行預(yù)測(這里用測試數(shù)據(jù)代替)
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個分別是標(biāo)準(zhǔn)化處理和PCA分解,都屬于變換器(transformer);第4個是邏輯回歸,屬于估計器(estimator)。

我們繼續(xù)做實驗,在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))])

上述兩種改變,在定義時都不會報錯,但在執(zhí)行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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容