如何建立複雜的機器學習專案?

AI科技大本營發表於2019-02-10

640?wx_fmt=jpeg


翻譯 | 光城
責編 | 郭芮

轉載自CSDN(CSDNnews)


scikit-learn提供最先進的機器學習演算法。但是,這些演算法不能直接用於原始資料。原始資料需要事先進行預處理。因此,除了機器學習演算法之外,scikit-learn還提供了一套預處理方法。此外,scikit-learn提供用於流水線化這些估計器的聯結器(即變壓器,迴歸器,分類器,聚類器等)。


在本文中,將介紹scikit-learn功能集,允許流水線估計器、評估這些流水線、使用超引數優化調整這些流水線以及建立複雜的預處理步驟。


基本用例:訓練和測試分類器


對於第一個示例,我們將在資料集上訓練和測試一個分類器。我們將使用此示例來回憶scikit-learn的API。


我們將使用digits資料集,這是一個手寫數字的資料集。


# 完成資料集的載入
from sklearn.datasets import load_digits
# return_X_y預設為False,這種情況下則為一個Bunch物件,改為True,可以直接得到(data, target)
X, y = load_digits(return_X_y=True)


X中的每行包含64個影像畫素的強度,對於X中的每個樣本,我們得到表示所寫數字對應的y。


# 下面完成灰度圖的繪製
# 灰度顯示影像
plt.imshow(X[0].reshape(88), cmap='gray');
# 關閉座標軸
plt.axis('off')
# 格式化列印
print('The digit in the image is {}'.format(y[0]))


輸出:The digit in the image is 0


640?wx_fmt=png

在機器學習中,我們應該通過在不同的資料集上進行訓練和測試來評估我們的模型。train_test_split是一個用於將資料拆分為兩個獨立資料集的效用函式,stratify引數可強制將訓練和測試資料集的類分佈與整個資料集的類分佈相同。


# 劃分資料為訓練集與測試集,新增stratify引數,以使得訓練和測試資料集的類分佈與整個資料集的類分佈相同。
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=42)


一旦我們擁有獨立的培訓和測試集,我們就可以使用fit方法學習機器學習模型。我們將使用score方法來測試此方法,依賴於預設的準確度指標。


# 求出Logistic迴歸的精確度得分
from sklearn.linear_model import LogisticRegression

clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=5000, random_state=42)
clf.fit(X_train, y_train)
accuracy = clf.score(X_test, y_test)
print('Accuracy score of the {} is {:.2f}'.format(clf.__class__.__name__, accuracy))

Accuracy score of the LogisticRegression is 0.95


scikit-learn的API在分類器中是一致的。因此,我們可以通過RandomForestClassifier輕鬆替換LogisticRegression分類器。這些更改很小,僅與分類器例項的建立有關。


# RandomForestClassifier輕鬆替換LogisticRegression分類器
from sklearn.ensemble import RandomForestClassifier

clf = RandomForestClassifier(n_estimators=100, n_jobs=-1, random_state=42)
clf.fit(X_train, y_train)
accuracy = clf.score(X_test, y_test)
print('Accuracy score of the {} is {:.2f}'.format(clf.__class__.__name__, accuracy))


輸出:


Accuracy score of the RandomForestClassifier is 0.96


練習


完成接下來的練習:


  • 載入乳腺癌資料集,從sklearn.datasets匯入函式load_breast_cancer:

# %load solutions/01_1_solutions.py

  • 使用sklearn.model_selection.train_test_split拆分資料集並保留30%的資料集以進行測試。確保對資料進行分層(即使用stratify引數)並將random_state設定為0:

# %load solutions/01_2_solutions.py

  • 使用訓練資料訓練監督分類器:

# %load solutions/01_3_solutions.py

  • 使用擬合分類器預測測試集的分類標籤:

# %load solutions/01_4_solutions.py

  • 計算測試集的balanced精度,需要從sklearn.metrics匯入balanced_accuracy_score:

# %load solutions/01_5_solutions.py



更高階的用例:在訓練和測試分類器之前預處理資料


2.1 標準化資料


在學習模型之前可能需要預處理。例如,一個使用者可能對建立手工製作的特徵或者演算法感興趣,那麼他可能會對資料進行一些先驗假設。在我們的例子中,LogisticRegression使用的求解器期望資料被規範化。因此,我們需要在訓練模型之前標準化資料。為了觀察這個必要條件,我們將檢查訓練模型所需的迭代次數。


from sklearn.linear_model import LogisticRegression

clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=5000, random_state=42)
clf.fit(X_train, y_train)
print('{} required {} iterations to be fitted'.format(clf.__class__.__name__, clf.n_iter_[0]))


輸出:


LogisticRegression required 1841 iterations to be fitted


MinMaxScaler變換器用於規範化資料。該標量應該以下列方式應用:學習(即,fit方法)訓練集上的統計資料並標準化(即,transform方法)訓練集和測試集。 最後,我們將訓練和測試這個模型並得到歸一化後的資料集。


from sklearn.preprocessing import MinMaxScaler
from sklearn.linear_model import LogisticRegression

scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=1000, random_state=42)
clf.fit(X_train_scaled, y_train)
accuracy = clf.score(X_test_scaled, y_test)
print('Accuracy score of the {} is {:.2f}'.format(clf.__class__.__name__, accuracy))
print('{} required {} iterations to be fitted'.format(clf.__class__.__name__, clf.n_iter_[0]))


輸出:


Accuracy score of the LogisticRegression is 0.96
LogisticRegression required 190 iterations to be fitted


通過歸一化資料,模型的收斂速度要比未歸一化的資料快得多(迭代次數變少了)。


2.2 錯誤的預處理模式


我們強調了如何預處理和充分訓練機器學習模型。發現預處理資料的錯誤方法也很有趣。其中有兩個潛在的錯誤,易於犯錯但又很容易發現。


第一種模式是在整個資料集分成訓練和測試集之前標準化資料。


scaler = MinMaxScaler()
X_scaled = scaler.fit_transform(X)
X_train_prescaled, X_test_prescaled, y_train_prescaled, y_test_prescaled = train_test_split(
    X_scaled, y, stratify=y, random_state=42)

clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=1000, random_state=42)
clf.fit(X_train_prescaled, y_train_prescaled)
accuracy = clf.score(X_test_prescaled, y_test_prescaled)
print('Accuracy score of the {} is {:.2f}'.format(clf.__class__.__name__, accuracy))


輸出:


Accuracy score of the LogisticRegression is 0.96


第二種模式是獨立地標準化訓練和測試集。它回來在訓練和測試集上呼叫fit方法。因此,訓練和測試集的標準化不同。


scaler = MinMaxScaler()
X_train_prescaled = scaler.fit_transform(X_train)
# 這裡發生了變化(將transform替換為fit_transform)
X_test_prescaled = scaler.fit_transform(X_test)

clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=1000, random_state=42)
clf.fit(X_train_prescaled, y_train)
accuracy = clf.score(X_test_prescaled, y_test)
print('Accuracy score of the {} is {:.2f}'.format(clf.__class__.__name__, accuracy))


輸出:


Accuracy score of the LogisticRegression is 0.96


2.3 保持簡單,愚蠢:使用scikit-learn的管道聯結器


前面提到的兩個模式是資料洩漏的問題。然而,當必須手動進行預處理時,很難防止這種錯誤。因此,scikit-learn引入了Pipeline物件。它依次連線多個變壓器和分類器(或迴歸器)。我們可以建立一個如下管道:


from sklearn.pipeline import Pipeline

pipe = Pipeline(steps=[('scaler', MinMaxScaler()),
                       ('clf', LogisticRegression(solver='lbfgs', multi_class='auto', random_state=42))])


我們看到這個管道包含了縮放器(歸一化)和分類器的引數。有時,為管道中的每個估計器命名可能會很繁瑣,而make_pipeline將自動為每個估計器命名,這是類名的小寫。


from sklearn.pipeline import make_pipeline
pipe = make_pipeline(MinMaxScaler(),
                     LogisticRegression(solver='lbfgs', multi_class='auto', random_state=42, max_iter=1000))


管道將具有相同的API。我們使用fit來訓練分類器和socre來檢查準確性。然而,呼叫fit會呼叫管道中所有變換器的fit_transform方法。呼叫score(或predict和predict_proba)將呼叫管道中所有變換器的內部變換。它對應於本文2.1中的規範化過程。


pipe.fit(X_train, y_train)
accuracy = pipe.score(X_test, y_test)
print('Accuracy score of the {} is {:.2f}'.format(pipe.__class__.__name__, accuracy))

Accuracy score of the Pipeline is 0.96


我們可以使用get_params()檢查管道的所有引數。


pipe.get_params()


輸出:


{'logisticregression': LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
           intercept_scaling=1, max_iter=1000, multi_class='auto',
           n_jobs=None, penalty='l2', random_state=42, solver='lbfgs',
           tol=0.0001, verbose=0, warm_start=False),
 'logisticregression__C'1.0,
 ...
 ...
 ...}


練習


重用第一個練習的乳腺癌資料集來訓練,可以從linear_model匯入SGDClassifier。使用此分類器和從sklearn.preprocessing匯入的StandardScaler變換器來建立管道,然後訓練和測試這條管道。


# %load solutions/02_solutions.py


當更多優於更少時:交叉驗證而不是單獨拆分


分割資料對於評估統計模型效能是必要的。但是,它減少了可用於學習模型的樣本數量。因此,應儘可能使用交叉驗證。有多個拆分也會提供有關模型穩定性的資訊。


scikit-learn提供了三個函式:cross_val_score,cross_val_predict和cross_validate。 後者提供了有關擬合時間,訓練和測試分數的更多資訊。 我也可以一次返回多個分數。


from sklearn.model_selection import cross_validate

pipe = make_pipeline(MinMaxScaler(),
                     LogisticRegression(solver='lbfgs', multi_class='auto',
                                        max_iter=1000, random_state=42))
scores = cross_validate(pipe, X, y, cv=3, return_train_score=True)


使用交叉驗證函式,我們可以快速檢查訓練和測試分數,並使用pandas快速繪圖。


import pandas as pd

df_scores = pd.DataFrame(scores)
df_scores


輸出:


640?wx_fmt=png

# pandas繪製箱體圖
df_scores[['train_score''test_score']].boxplot()


輸出:


640?wx_fmt=png


練習


使用上一個練習的管道並進行交叉驗證,而不是單個拆分評估。


# %load solutions/03_solutions.py


超引數優化:微調管道內部


有時希望找到管道元件的引數,從而獲得最佳精度。我們已經看到我們可以使用get_params()檢查管道的引數。


pipe.get_params()


輸出:


{'logisticregression': LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
           intercept_scaling=1, max_iter=1000, multi_class='auto',
           n_jobs=None, penalty='l2', random_state=42, solver='lbfgs',
           tol=0.0001, verbose=0, warm_start=False),
 'logisticregression__C'1.0,
 ...
 ...
 ...}


可以通過窮舉搜尋來優化超引數。GridSearchCV 提供此類實用程式,並通過引數網格進行交叉驗證的網格搜尋。


如下例子,我們希望優化LogisticRegression分類器的C和penalty引數。


from sklearn.model_selection import GridSearchCV

pipe = make_pipeline(MinMaxScaler(),
                     LogisticRegression(solver='saga', multi_class='auto',
                                        random_state=42, max_iter=5000))
param_grid = {'logisticregression__C': [0.11.010],
              'logisticregression__penalty': ['l2''l1']}
grid = GridSearchCV(pipe, param_grid=param_grid, cv=3, n_jobs=-1, return_train_score=True)
grid.fit(X_train, y_train)


輸出:


GridSearchCV(cv=3, error_score='raise-deprecating',
       ...
       ...
       ...
       scoring=None, verbose=0)


在擬合網格搜尋物件時,它會在訓練集上找到最佳的引數組合(使用交叉驗證)。 我們可以通過訪問屬性cv_results_來得到網格搜尋的結果。 通過這個屬性允許我們可以檢查引數對模型效能的影響。


df_grid = pd.DataFrame(grid.cv_results_)
df_grid


輸出:


640?wx_fmt=png


預設情況下,網格搜尋物件也表現為估計器。一旦它被fit後,呼叫score將超引數固定為找到的最佳引數。


grid.best_params_


輸出:


{'logisticregression__C': 10, 'logisticregression__penalty''l2'}


此外,可以將網格搜尋稱為任何其他分類器以進行預測。


accuracy = grid.score(X_test, y_test)
print('Accuracy score of the {} is {:.2f}'.format(grid.__class__.__name__, accuracy))

Accuracy score of the GridSearchCV is 0.96


最重要的是,我們只對單個分割進行網格搜尋。 但是,如前所述,我們可能有興趣進行外部交叉驗證,以估計模型的效能和不同的資料樣本,並檢查效能的潛在變化。 由於網格搜尋是一個估計器,我們可以直接在cross_validate函式中使用它。


scores = cross_validate(grid, X, y, cv=3, n_jobs=-1, return_train_score=True)
df_scores = pd.DataFrame(scores)
df_scores


輸出:


640?wx_fmt=png


練習


重複使用乳腺癌資料集的先前管道並進行網格搜尋以評估hinge(鉸鏈) and log(對數)損失之間的差異。此外,微調penalty。


# %load solutions/04_solutions.py


總結:我的scikit-learn管道只有不到10行程式碼(跳過import語句)


import pandas as pd
from sklearn.preprocessing import MinMaxScaler
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import cross_validate

pipe = make_pipeline(MinMaxScaler(),
                     LogisticRegression(solver='saga', multi_class='auto', random_state=42, max_iter=5000))
param_grid = {'logisticregression__C': [0.11.010],
              'logisticregression__penalty': ['l2''l1']}
grid = GridSearchCV(pipe, param_grid=param_grid, cv=3, n_jobs=-1)
scores = pd.DataFrame(cross_validate(grid, X, y, cv=3, n_jobs=-1, return_train_score=True))
scores[['train_score''test_score']].boxplot()


輸出:


640?wx_fmt=png


異構資料:當您使用數字以外的資料時


到目前為止,我們使用scikit-learn來訓練使用數值資料的模型。


X


輸出:


array([[ 0.,  0.,  5....,  0.,  0.,  0.],
       [ 0.,  0.,  0....10.,  0.,  0.],
       [ 0.,  0.,  0....16.,  9.,  0.],
       ...,
       [ 0.,  0.,  1....,  6.,  0.,  0.],
       [ 0.,  0.,  2....12.,  0.,  0.],
       [ 0.,  0.10....12.,  1.,  0.]])


X是僅包含浮點值的NumPy陣列。但是,資料集可以包含混合型別。


import os
data = pd.read_csv(os.path.join('data''titanic_openml.csv'), na_values='?')
data.head()


輸出:


640?wx_fmt=png


泰坦尼克號資料集包含分類、文字和數字特徵。我們將使用此資料集來預測乘客是否在泰坦尼克號中倖存下來。


讓我們將資料拆分為訓練和測試集,並將倖存列用作目標。


y = data['survived']
X = data.drop(columns='survived')

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)


首先,可以嘗試使用LogisticRegression分類器,看看它的表現有多好。


clf = LogisticRegression()
clf.fit(X_train, y_train)


哎呀,大多數分類器都設計用於處理數值資料。因此,我們需要將分類資料轉換為數字特徵。最簡單的方法是使用OneHotEncoder對每個分類特徵進行讀熱編碼。讓我們以sex與embarked列為例。請注意,我們還會遇到一些缺失的資料。我們將使用SimpleImputer用常量值替換缺失值。


from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder
ohe = make_pipeline(SimpleImputer(strategy='constant'), OneHotEncoder())
X_encoded = ohe.fit_transform(X_train[['sex''embarked']])
X_encoded.toarray()


輸出:


array([[0., 1., 0., 0., 1., 0.],
       [0., 1., 1., 0., 0., 0.],
       [0., 1., 0., 0., 1., 0.],
       ...,
       [0., 1., 0., 0., 1., 0.],
       [1., 0., 0., 0., 1., 0.],
       [1., 0., 0., 0., 1., 0.]])


這樣,可以對分類特徵進行編碼。但是,我們也希望標準化數字特徵。因此,我們需要將原始資料分成2個子組並應用不同的預處理:(i)分類資料的獨熱編;(ii)數值資料的標準縮放(歸一化)。我們還需要處理兩種情況下的缺失值: 對於分類列,我們將字串'missing_values'替換為缺失值,該字串將自行解釋為類別。 對於數值資料,我們將用感興趣的特徵的平均值替換缺失的資料。


  • 分類資料的獨熱編:


col_cat = ['sex''embarked']
col_num = ['age''sibsp''parch''fare']

X_train_cat = X_train[col_cat]
X_train_num = X_train[col_num]
X_test_cat = X_test[col_cat]
X_test_num = X_test[col_num]


  • 數值資料的標準縮放(歸一化):


from sklearn.preprocessing import StandardScaler

scaler_cat = make_pipeline(SimpleImputer(strategy='constant'), OneHotEncoder())
X_train_cat_enc = scaler_cat.fit_transform(X_train_cat)
X_test_cat_enc = scaler_cat.transform(X_test_cat)

scaler_num = make_pipeline(SimpleImputer(strategy='mean'), StandardScaler())
X_train_num_scaled = scaler_num.fit_transform(X_train_num)
X_test_num_scaled = scaler_num.transform(X_test_num)


我們應該像在本文2.1中那樣在訓練和測試集上應用這些變換。


import numpy as np
from scipy import sparse

X_train_scaled = sparse.hstack((X_train_cat_enc,
                                sparse.csr_matrix(X_train_num_scaled)))
X_test_scaled = sparse.hstack((X_test_cat_enc,
                               sparse.csr_matrix(X_test_num_scaled)))


轉換完成後,我們現在可以組合所有數值的資訊。最後,我們使用LogisticRegression分類器作為模型。


clf = LogisticRegression(solver='lbfgs')
clf.fit(X_train_scaled, y_train)
accuracy = clf.score(X_test_scaled, y_test)
print('Accuracy score of the {} is {:.2f}'.format(clf.__class__.__name__, accuracy))


輸出:


Accuracy score of the LogisticRegression is 0.79


上面首先轉換資料然後擬合/評分分類器的模式恰好是本節2.1的模式之一。因此,我們希望為此目的使用管道。


但是,我們還希望對矩陣的不同列進行不同的處理。應使用ColumnTransformer轉換器或make_column_transformer函式。它用於在不同的列上自動應用不同的管道。


from sklearn.compose import make_column_transformer

pipe_cat = make_pipeline(SimpleImputer(strategy='constant'), OneHotEncoder(handle_unknown='ignore'))
pipe_num = make_pipeline(SimpleImputer(), StandardScaler())
preprocessor = make_column_transformer((col_cat, pipe_cat), (col_num, pipe_num))

pipe = make_pipeline(preprocessor, LogisticRegression(solver='lbfgs'))

pipe.fit(X_train, y_train)
accuracy = pipe.score(X_test, y_test)
print('Accuracy score of the {} is {:.2f}'.format(pipe.__class__.__name__, accuracy))


輸出:


Accuracy score of the Pipeline is 0.79


此外,它還可以被使用在另一個管道。 因此,我們將能夠使用所有scikit-learn實用程式作為cross_validate或GridSearchCV。


pipe.get_params()


輸出:


{'columntransformer': ColumnTransformer(n_jobs=None, remainder='drop', sparse_threshold=0.3,
          transformer_weights=None,
          transformers=[('pipeline-1', Pipeline(memory=None,
      ...]}


合併及視覺化:


pipe_cat = make_pipeline(SimpleImputer(strategy='constant'), OneHotEncoder(handle_unknown='ignore'))
pipe_num = make_pipeline(StandardScaler(), SimpleImputer())
preprocessor = make_column_transformer((col_cat, pipe_cat), (col_num, pipe_num))

pipe = make_pipeline(preprocessor, LogisticRegression(solver='lbfgs'))

param_grid = {'columntransformer__pipeline-2__simpleimputer__strategy': ['mean''median'],
              'logisticregression__C': [0.11.010]}
grid = GridSearchCV(pipe, param_grid=param_grid, cv=5, n_jobs=-1)
scores = pd.DataFrame(cross_validate(grid, X, y, scoring='balanced_accuracy', cv=5, n_jobs=-1, return_train_score=True))
scores[['train_score''test_score']].boxplot()


輸出:


640?wx_fmt=png


練習


完成接下來的練習:


載入位於./data/adult_openml.csv中的成人資料集,製作自己的ColumnTransformer前處理器,並用分類器管道化它,對其進行微調並在交叉驗證中檢查預測準確性。


  • 使用pd.read_csv讀取位於./data/adult_openml.csv中的成人資料集:

# %load solutions/05_1_solutions.py

  • 將資料集拆分為資料和目標,目標對應於類列。對於資料,刪除列fnlwgt,capitalgain和capitalloss:

# %load solutions/05_2_solutions.py

  • 目標未編碼,使用sklearn.preprocessing.LabelEncoder對類進行編碼:

# %load solutions/05_3_solutions.py

  • 建立一個包含分類列名稱的列表,同樣,對數值資料也一樣:

# %load solutions/05_4_solutions.py


  • 建立一個管道以對分類資料進行讀熱編碼,使用KBinsDiscretizer作為數值資料,從sklearn.preprocessing匯入它:

# %load solutions/05_5_solutions.py

  • 使用make_column_transformer建立前處理器,應該將好的管道應用於好的列:

# %load solutions/05_6_solutions.py

  • 使用LogisticRegression分類器對前處理器進行管道傳輸。隨後定義網格搜尋以找到最佳引數C.使用cross_validate在交叉驗證方案中訓練和測試此工作流程:


# %load solutions/05_7_solutions.py



原文連結:

https://github.com/glemaitre/pyparis-2018-sklearn。

譯者簡介:光城,研一,個人公眾號:guangcity,部落格:http://light-city.me/, 個人研究方向知識圖譜,正致力於將機器學習運用到KG當中。

(本文僅代表作者觀點,轉載請聯絡原作者)

徵稿

640?wx_fmt=png


推薦閱讀:

                         640?wx_fmt=png

點選“閱讀原文”,開啟CSDN APP 閱讀更貼心!

相關文章