一、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_new或S2_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)。

