《Scikit-Learn與TensorFlow機器學(xué)習(xí)實用指南》 第02章 一個完整的機器學(xué)習(xí)項目(下)


(第一部分 機器學(xué)習(xí)基礎(chǔ))
第01章 機器學(xué)習(xí)概覽
第02章 一個完整的機器學(xué)習(xí)項目(上)
第02章 一個完整的機器學(xué)習(xí)項目(下)
第03章 分類
第04章 訓(xùn)練模型
第05章 支持向量機
第06章 決策樹
第07章 集成學(xué)習(xí)和隨機森林
第08章 降維
(第二部分 神經(jīng)網(wǎng)絡(luò)和深度學(xué)習(xí))
第9章 啟動和運行TensorFlow
第10章 人工神經(jīng)網(wǎng)絡(luò)
第11章 訓(xùn)練深度神經(jīng)網(wǎng)絡(luò)(上)
第11章 訓(xùn)練深度神經(jīng)網(wǎng)絡(luò)(下)
第12章 設(shè)備和服務(wù)器上的分布式 TensorFlow
第13章 卷積神經(jīng)網(wǎng)絡(luò)
第14章 循環(huán)神經(jīng)網(wǎng)絡(luò)
第15章 自編碼器
第16章 強化學(xué)習(xí)(上)
第16章 強化學(xué)習(xí)(下)


數(shù)據(jù)清洗

大多機器學(xué)習(xí)算法不能處理特征丟失,因此先創(chuàng)建一些函數(shù)來處理特征丟失的問題。前面,你應(yīng)該注意到了屬性total_bedrooms有一些缺失值。有三個解決選項:

  • 去掉對應(yīng)的分區(qū);

  • 去掉整個屬性;

  • 進(jìn)行賦值(0、平均值、中位數(shù)等等)。

用DataFrame的dropna(), drop(),和 fillna()方法,可以方便地實現(xiàn):

housing.dropna(subset=["total_bedrooms"])    # 選項1
housing.drop("total_bedrooms", axis=1)       # 選項2
median = housing["total_bedrooms"].median()
housing["total_bedrooms"].fillna(median)     # 選項3

如果選擇選項3,你需要計算訓(xùn)練集的中位數(shù),用中位數(shù)填充訓(xùn)練集的缺失值,不要忘記保存該中位數(shù)。后面用測試集評估系統(tǒng)時,需要替換測試集中的缺失值,也可以用來實時替換新數(shù)據(jù)中的缺失值。

Scikit-Learn提供了一個方便的類來處理缺失值:Imputer。下面是其使用方法:首先,需要創(chuàng)建一個Imputer實例,指定用該屬性的中位數(shù)替換它的每個缺失值:

from sklearn.preprocessing import Imputer

imputer = Imputer(strategy="median")

因為只有數(shù)值屬性才能算出中位數(shù),我們需要創(chuàng)建一份不包括文本屬性ocean_proximity的數(shù)據(jù)副本:

housing_num = housing.drop("ocean_proximity", axis=1)

現(xiàn)在,就可以用fit()方法將imputer實例擬合到訓(xùn)練數(shù)據(jù):

imputer.fit(housing_num)

imputer計算出了每個屬性的中位數(shù),并將結(jié)果保存在了實例變量statistics_中。只有屬性total_bedrooms有缺失值,但是我們要確保一旦系統(tǒng)運行起來,新的數(shù)據(jù)中沒有缺失值,所以安全的做法是將imputer應(yīng)用到每個數(shù)值:

>>> imputer.statistics_
array([ -118.51 , 34.26 , 29. , 2119. , 433. , 1164. , 408. , 3.5414])
>>> housing_num.median().values
array([ -118.51 , 34.26 , 29. , 2119. , 433. , 1164. , 408. , 3.5414])

現(xiàn)在,你就可以使用這個“訓(xùn)練過的”imputer來對訓(xùn)練集進(jìn)行轉(zhuǎn)換,通過將缺失值替換為中位數(shù):

X = imputer.transform(housing_num)

結(jié)果是一個普通的Numpy數(shù)組,包含有轉(zhuǎn)換后的特征。如果你想將其放回到Pandas DataFrame中,也很簡單:

housing_tr = pd.DataFrame(X, columns=housing_num.columns)

Scikit-Learn設(shè)計

Scikit-Learn設(shè)計的API設(shè)計的非常好。它的主要設(shè)計原則是:

  • 一致性:所有對象的接口一致且簡單:
    估計量(estimator)。任何可以基于數(shù)據(jù)集而對一些參數(shù)進(jìn)行估計的對象都被設(shè)計成估計量(比如,imputer就是個估計量)。估計本身是通過fit()方法,只需要一個數(shù)據(jù)集作為參數(shù)(對于監(jiān)督學(xué)習(xí)算法,需要兩個數(shù)據(jù)集;第二個數(shù)據(jù)集包含標(biāo)簽)。任何其它用來指導(dǎo)估計過程的參數(shù)都被當(dāng)做超參數(shù)(比如imputerstrategy),并且超參數(shù)要被設(shè)置成實例變量(通常是通過構(gòu)造器參數(shù))。
    轉(zhuǎn)換量(transformer)。一些估計量(比如imputer)也可以轉(zhuǎn)換數(shù)據(jù)集,這些估計量被稱為轉(zhuǎn)換量。API也是相當(dāng)簡單:轉(zhuǎn)換是通過transform()方法,被轉(zhuǎn)換的數(shù)據(jù)集作為參數(shù)。返回的是經(jīng)過轉(zhuǎn)換的數(shù)據(jù)集。轉(zhuǎn)換過程依賴學(xué)習(xí)到的參數(shù),比如imputer的例子。所有的轉(zhuǎn)換都有一個便捷的方法fit_transform(),等同于調(diào)用fit()transform()(但有時fit_transform()經(jīng)過優(yōu)化,運行的更快)。
    預(yù)測量(predictor)。最后,一些估計量可以根據(jù)給出的數(shù)據(jù)集做預(yù)測,這些估計量稱為預(yù)測量。例如,上一章的LinearRegression模型就是一個預(yù)測量:它根據(jù)一個國家的人均GDP預(yù)測生活滿意度。預(yù)測量有一個predict()方法,可以用新實例的數(shù)據(jù)集做出相應(yīng)的預(yù)測。預(yù)測量還有一個score()方法,可以根據(jù)測試集(和相應(yīng)的標(biāo)簽,如果是監(jiān)督學(xué)習(xí)算法的話)對預(yù)測進(jìn)行衡量。

  • 可檢驗。所有估計量的超參數(shù)都可以通過公共實例變量直接訪問(比如,imputer.strategy),并且所有估計量學(xué)習(xí)到的參數(shù)也可以通過公共實例變量添加下劃線后綴訪問(比如,imputer.statistics_)。

  • 類不可擴(kuò)散。數(shù)據(jù)集被表示成NumPy數(shù)組或SciPy稀疏矩陣,而不是自制的類。超參數(shù)只是普通的Python字符串或數(shù)字。

  • 可組合。盡可能使用現(xiàn)存的模塊。例如,用任意的轉(zhuǎn)換量序列加上一個估計量,就可以做成一個Pipeline,后面會看到例子。

  • 合理的默認(rèn)值。Scikit-Learn給大多數(shù)參數(shù)提供了合理的默認(rèn)值,很容易就能創(chuàng)建一個系統(tǒng)。

處理文本和分類屬性

前面,我們丟棄了分類屬性ocean_proximity,因為它是一個文本屬性,不能計算出中位數(shù)。大多數(shù)機器學(xué)習(xí)算法更喜歡和數(shù)字打交道,所以讓我們把這些文本標(biāo)簽轉(zhuǎn)換為數(shù)字。

Scikit-Learn為這個任務(wù)提供了一個轉(zhuǎn)換量LabelEncoder

>>> from sklearn.preprocessing import LabelEncoder
>>> encoder = LabelEncoder()
>>> housing_cat = housing["ocean_proximity"]
>>> housing_cat_encoded = encoder.fit_transform(housing_cat)
>>> housing_cat_encoded
array([1, 1, 4, ..., 1, 0, 3])

好了一些,現(xiàn)在就可以在任何ML算法里用這個數(shù)值數(shù)據(jù)了。你可以查看映射表,編碼器是通過屬性classes_來學(xué)習(xí)的(“<1H OCEAN”被映射為0,“INLAND”被映射為1,等等):

>>> print(encoder.classes_)
['<1H OCEAN' 'INLAND' 'ISLAND' 'NEAR BAY' 'NEAR OCEAN']

這種做法的問題是,ML算法會認(rèn)為兩個臨近的值比兩個疏遠(yuǎn)的值要更相似。顯然這樣不對(比如,分類0和4比0和1更相似)。要解決這個問題,一個常見的方法是給每個分類創(chuàng)建一個二元屬性:當(dāng)分類是“<1H OCEAN”,該屬性為1(否則為0),當(dāng)分類是“INLAND”,另一個屬性等于1(否則為0),以此類推。這稱作獨熱編碼(One-Hot Encoding),因為只有一個屬性會等于1(熱),其余會是0(冷)。

Scikit-Learn提供了一個編碼器OneHotEncoder,用于將整書分類值轉(zhuǎn)變?yōu)楠殶崾噶?。注?code>fit_transform()用于2D數(shù)組,而housing_cat_encoded是一個1D數(shù)組,所以需要將其變形:

>>> from sklearn.preprocessing import OneHotEncoder
>>> encoder = OneHotEncoder()
>>> housing_cat_1hot = encoder.fit_transform(housing_cat_encoded.reshape(-1,1))
>>> housing_cat_1hot
<16513x5 sparse matrix of type '<class 'numpy.float64'>'
    with 16513 stored elements in Compressed Sparse Row format>

注意輸出結(jié)果是一個SciPy稀疏矩陣,而不是NumPy數(shù)組。當(dāng)分類屬性有數(shù)千個分類時,這樣非常有用。經(jīng)過獨熱編碼,我們得到了一個有數(shù)千列的矩陣,這個矩陣每行只有一個1,其余都是0。使用大量內(nèi)存來存儲這些0非常浪費,所以稀疏矩陣只存儲非零元素的位置。你可以像一個2D數(shù)據(jù)那樣進(jìn)行使用,但是如果你真的想將其轉(zhuǎn)變成一個(密集的)NumPy數(shù)組,只需調(diào)用toarray()方法:

>>> housing_cat_1hot.toarray()
array([[ 0.,  1.,  0.,  0.,  0.],
       [ 0.,  1.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  1.],
       ...,
       [ 0.,  1.,  0.,  0.,  0.],
       [ 1.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  1.,  0.]])

使用類LabelBinarizer,我們可以用一步執(zhí)行這兩個轉(zhuǎn)換(從文本分類到整數(shù)分類,再從整數(shù)分類到獨熱矢量):

>>> from sklearn.preprocessing import LabelBinarizer
>>> encoder = LabelBinarizer()
>>> housing_cat_1hot = encoder.fit_transform(housing_cat)
>>> housing_cat_1hot
array([[0, 1, 0, 0, 0],
       [0, 1, 0, 0, 0],
       [0, 0, 0, 0, 1],
       ...,
       [0, 1, 0, 0, 0],
       [1, 0, 0, 0, 0],
       [0, 0, 0, 1, 0]])

注意默認(rèn)返回的結(jié)果是一個密集NumPy數(shù)組。向構(gòu)造器LabelBinarizer傳遞sparse_output=True,就可以得到一個稀疏矩陣。

自定義轉(zhuǎn)換量

盡管Scikit-Learn提供了許多有用的轉(zhuǎn)換量,你還是需要自己動手寫轉(zhuǎn)換量執(zhí)行任務(wù),比如自定義的清理操作,或?qū)傩越M合。你需要讓自制的轉(zhuǎn)換量與Scikit-Learn組件(比如pipeline)無縫銜接工作,因為Scikit-Learn是依賴鴨子類型的(而不是繼承),你所需要做的是創(chuàng)建一個類并執(zhí)行三個方法:fit()(返回self),transform(),和fit_transform()。通過添加TransformerMixin作為基類,可以很容易地得到最后一個。另外,如果你添加BaseEstimator作為基類(且構(gòu)造器中避免使用*args**kargs),你就能得到兩個額外的方法(get_params()set_params()),二者可以方便地進(jìn)行超參數(shù)自動微調(diào)。例如,一個小轉(zhuǎn)換量類添加了上面討論的屬性:

from sklearn.base import BaseEstimator, TransformerMixin
rooms_ix, bedrooms_ix, population_ix, household_ix = 3, 4, 5, 6

class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
    def __init__(self, add_bedrooms_per_room = True): # no *args or **kargs
        self.add_bedrooms_per_room = add_bedrooms_per_room
    def fit(self, X, y=None):
        return self  # nothing else to do
    def transform(self, X, y=None):
        rooms_per_household = X[:, rooms_ix] / X[:, household_ix]
        population_per_household = X[:, population_ix] / X[:, household_ix]
        if self.add_bedrooms_per_room:
            bedrooms_per_room = X[:, bedrooms_ix] / X[:, rooms_ix]
            return np.c_[X, rooms_per_household, population_per_household,
                         bedrooms_per_room]
        else:
            return np.c_[X, rooms_per_household, population_per_household]

attr_adder = CombinedAttributesAdder(add_bedrooms_per_room=False)
housing_extra_attribs = attr_adder.transform(housing.values)

在這個例子中,轉(zhuǎn)換量有一個超參數(shù)add_bedrooms_per_room,默認(rèn)設(shè)為True(提供一個合理的默認(rèn)值很有幫助)。這個超參數(shù)可以讓你方便地發(fā)現(xiàn)添加了這個屬性是否對機器學(xué)習(xí)算法有幫助。更一般地,你可以為每個不能完全確保的數(shù)據(jù)準(zhǔn)備步驟添加一個超參數(shù)。數(shù)據(jù)準(zhǔn)備步驟越自動化,可以自動化的操作組合就越多,越容易發(fā)現(xiàn)更好用的組合(并能節(jié)省大量時間)。

特征縮放

數(shù)據(jù)要做的最重要的轉(zhuǎn)換之一是特征縮放。除了個別情況,當(dāng)輸入的數(shù)值屬性量度不同時,機器學(xué)習(xí)算法的性能都不會好。這個規(guī)律也適用于房產(chǎn)數(shù)據(jù):總房間數(shù)分布范圍是6到39320,而收入中位數(shù)只分布在0到15。不需要對目標(biāo)值進(jìn)行縮放。

有兩種常見的方法可以讓所有的屬性有相同的量度:線性函數(shù)歸一化(Min-Max scaling)和標(biāo)準(zhǔn)化(standardization)。

線性函數(shù)歸一化(許多人稱其為歸一化(normalization))很簡單:值被轉(zhuǎn)變、重新縮放,直到范圍變成0到1。我們通過減去最小值,然后再除以最大值與最小值的差值,來進(jìn)行歸一化。Scikit-Learn提供了一個轉(zhuǎn)換量MinMaxScaler來實現(xiàn)這個功能。它有一個超參數(shù)feature_range,可以讓你改變范圍,如果不希望范圍是0到1。

標(biāo)準(zhǔn)化就很不同:首先減去平均值(所以標(biāo)準(zhǔn)化值的平均值總是0),然后除以方差,使得到的分布具有單位方差。與歸一化不同,標(biāo)準(zhǔn)化不會限定值到某個特定的范圍,這對某些算法可能構(gòu)成問題(比如,神經(jīng)網(wǎng)絡(luò)常需要輸入值得范圍是0到1)。但是,標(biāo)準(zhǔn)化受到異常值的影響很小。例如,假設(shè)一個分區(qū)的收入中位數(shù)是100。歸一化會將其它范圍是0到15的值變?yōu)?-0.15,但是標(biāo)準(zhǔn)化不會受什么影響。Scikit-Learn提供了一個轉(zhuǎn)換量StandardScaler來進(jìn)行標(biāo)準(zhǔn)化。

警告:與所有的轉(zhuǎn)換一樣,縮放器只能向訓(xùn)練集擬合,而不是向完整的數(shù)據(jù)集(包括測試集)。只有這樣,才能用縮放器轉(zhuǎn)換訓(xùn)練集和測試集(和新數(shù)據(jù))。

轉(zhuǎn)換Pipeline

你已經(jīng)看到,存在許多數(shù)據(jù)轉(zhuǎn)換步驟,需要按一定的順序執(zhí)行。幸運的是,Scikit-Learn提供了類Pipeline,來進(jìn)行這一系列的轉(zhuǎn)換。下面是一個數(shù)值屬性的小pipeline:

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

num_pipeline = Pipeline([
        ('imputer', Imputer(strategy="median")),
        ('attribs_adder', CombinedAttributesAdder()),
        ('std_scaler', StandardScaler()),
        ])

housing_num_tr = num_pipeline.fit_transform(housing_num)

Pipeline構(gòu)造器需要一個定義步驟順序的名字/估計量對的列表。除了最后一個估計量,其余都要是轉(zhuǎn)換量(即,它們都要有fit_transform()方法)。名字可以隨意起。

當(dāng)你調(diào)用pipeline的fit()方法,就會對所有轉(zhuǎn)換量順序調(diào)用fit_transform()方法,將每次調(diào)用的輸出作為參數(shù)傳遞給下一個調(diào)用,一直到最后一個評估量,它只執(zhí)行fit()方法。

pipeline暴露相同的方法作為最終的評估量。在這個例子中,最后的評估量是一個StandardScaler,它是一個轉(zhuǎn)換量,因此這個pipeline有一個transform()方法,可以順序?qū)?shù)據(jù)做所有轉(zhuǎn)換(它還有一個fit_transform方法可以使用,就不必先調(diào)用fit()再進(jìn)行transform())。

你現(xiàn)在就有了一個對數(shù)值的pipeline,你還需要對分類值應(yīng)用LabelBinarizer:如何將這些轉(zhuǎn)換寫成一個pipeline呢?Scikit-Learn提供了一個類FeatureUnion實現(xiàn)這個功能。你給它一列轉(zhuǎn)換量(可以是所有的轉(zhuǎn)換量),當(dāng)調(diào)用它的transform()方法,每個轉(zhuǎn)換量的transform()會被并行執(zhí)行,等待輸出,然后將輸出合并起來,并返回結(jié)果(當(dāng)然,調(diào)用它的fit()方法就會調(diào)用每個轉(zhuǎn)換量的fit())。一個完整的處理數(shù)值和分類屬性的pipeline如下所示:

from sklearn.pipeline import FeatureUnion

num_attribs = list(housing_num)
cat_attribs = ["ocean_proximity"]

num_pipeline = Pipeline([
        ('selector', DataFrameSelector(num_attribs)),
        ('imputer', Imputer(strategy="median")),
        ('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler()),
    ])

cat_pipeline = Pipeline([
        ('selector', DataFrameSelector(cat_attribs)),
        ('label_binarizer', LabelBinarizer()),
    ])

full_pipeline = FeatureUnion(transformer_list=[
        ("num_pipeline", num_pipeline),
        ("cat_pipeline", cat_pipeline),
    ])

你可以很簡單地運行整個pipeline:

>>> housing_prepared = full_pipeline.fit_transform(housing)
>>> housing_prepared
array([[ 0.73225807, -0.67331551,  0.58426443, ...,  0.        ,
         0.        ,  0.        ],
       [-0.99102923,  1.63234656, -0.92655887, ...,  0.        ,
         0.        ,  0.        ],
       [...]
>>> housing_prepared.shape
(16513, 17)

每個子pipeline都以一個選擇轉(zhuǎn)換量開始:通過選擇對應(yīng)的屬性(數(shù)值或分類)、丟棄其它的,來轉(zhuǎn)換數(shù)據(jù),并將輸出DataFrame轉(zhuǎn)變成一個NumPy數(shù)組。Scikit-Learn沒有工具來處理Pandas DataFrame,因此我們需要寫一個簡單的自定義轉(zhuǎn)換量來做這項工作:

from sklearn.base import BaseEstimator, TransformerMixin

class DataFrameSelector(BaseEstimator, TransformerMixin):
    def __init__(self, attribute_names):
        self.attribute_names = attribute_names
    def fit(self, X, y=None):
        return self
    def transform(self, X):
        return X[self.attribute_names].values

選擇并訓(xùn)練模型

可到這一步了!你在前面限定了問題、獲得了數(shù)據(jù)、探索了數(shù)據(jù)、采樣了一個測試集、寫了自動化的轉(zhuǎn)換pipeline來清理和為算法準(zhǔn)備數(shù)據(jù)?,F(xiàn)在,你已經(jīng)準(zhǔn)備好選擇并訓(xùn)練一個機器學(xué)習(xí)模型了。

在訓(xùn)練集上訓(xùn)練和評估

好消息是基于前面的工作,接下來要做的比你想的要簡單許多。像前一章那樣,我們先來訓(xùn)練一個線性回歸模型:

from sklearn.linear_model import LinearRegression

lin_reg = LinearRegression()
lin_reg.fit(housing_prepared, housing_labels)

完畢!你現(xiàn)在就有了一個可用的線性回歸模型。用一些訓(xùn)練集中的實例做下驗證:

>>> some_data = housing.iloc[:5]
>>> some_labels = housing_labels.iloc[:5]
>>> some_data_prepared = full_pipeline.transform(some_data)
>>> print("Predictions:\t", lin_reg.predict(some_data_prepared))
Predictions:     [ 303104.   44800.  308928.  294208.  368704.]
>>> print("Labels:\t\t", list(some_labels))
Labels:      [359400.0, 69700.0, 302100.0, 301300.0, 351900.0]

行的通,盡管預(yù)測并不怎么準(zhǔn)確(比如,第二個預(yù)測偏離了50%!)。讓我們使用Scikit-Learn的mean_squared_error函數(shù),用全部訓(xùn)練集來計算下這個回歸模型的RMSE:

>>> from sklearn.metrics import mean_squared_error
>>> housing_predictions = lin_reg.predict(housing_prepared)
>>> lin_mse = mean_squared_error(housing_labels, housing_predictions)
>>> lin_rmse = np.sqrt(lin_mse)
>>> lin_rmse
68628.413493824875

okay,有總比沒有強,但顯然結(jié)果并不好:大多數(shù)分區(qū)的median_housing_values位于120000美元到265000美元之間,因此預(yù)測誤差$68628不能讓人滿意。這是一個模型欠擬合訓(xùn)練數(shù)據(jù)的例子。當(dāng)這種情況發(fā)生時,意味著特征沒有提供足夠多的信息來做出一個好的預(yù)測,或者模型并不強大。就像前一章看到的,修復(fù)欠擬合的主要方法是選擇一個更強大的模型,給訓(xùn)練算法提供更好的特征,或去掉模型上的限制。這個模型還沒有正則化,所以排除了最后一個選項。你可以嘗試添加更多特征(比如,人口的對數(shù)值),但是首先讓我們嘗試一個更為復(fù)雜的模型,看看效果。

來訓(xùn)練一個DecisionTreeRegressor。這是一個強大的模型,可以發(fā)現(xiàn)數(shù)據(jù)中復(fù)雜的非線性關(guān)系(決策樹會在第6章詳細(xì)講解)。代碼看起來很熟悉:

from sklearn.tree import DecisionTreeRegressor

tree_reg = DecisionTreeRegressor()
tree_reg.fit(housing_prepared, housing_labels)

現(xiàn)在模型就訓(xùn)練好了,用訓(xùn)練集評估下:

>>> housing_predictions = tree_reg.predict(housing_prepared)
>>> tree_mse = mean_squared_error(housing_labels, housing_predictions)
>>> tree_rmse = np.sqrt(tree_mse)
>>> tree_rmse
0.0

等一下,發(fā)生了什么?沒有誤差?這個模型可能是絕對完美的嗎?當(dāng)然,更大可能性是這個模型嚴(yán)重過擬合數(shù)據(jù)。如何確定呢?如前所述,直到你準(zhǔn)備運行一個具備足夠信心的模型,都不要碰測試集,因此你需要使用訓(xùn)練集的部分?jǐn)?shù)據(jù)來做訓(xùn)練,用一部分來做模型驗證。

使用交叉驗證做更佳的評估

評估決策樹模型的一種方法是用函數(shù)train_test_split來分割訓(xùn)練集,得到一個更小的訓(xùn)練集和一個驗證集,然后用更小的訓(xùn)練集來訓(xùn)練模型,用驗證集來評估。這需要一定工作量,并不難而且也可行。

另一種更好的方法是使用Scikit-Learn的交叉驗證功能。下面的代碼采用了K折交叉驗證(K-fold cross-validation):它隨機地將訓(xùn)練集分成十個不同的子集,成為“折”,然后訓(xùn)練評估決策樹模型10次,每次選一個不用的折來做評估,用其它9個來做訓(xùn)練。結(jié)果是一個包含10個評分的數(shù)組:

from sklearn.model_selection import cross_val_score
scores = cross_val_score(tree_reg, housing_prepared, housing_labels,
                         scoring="neg_mean_squared_error", cv=10)
rmse_scores = np.sqrt(-scores)

警告:Scikit-Learn交叉驗證功能期望的是效用函數(shù)(越大越好)而不是成本函數(shù)(越低越好),因此得分函數(shù)實際上與MSE相反(即負(fù)值),這就是為什么前面的代碼在計算平方根之前先計算-scores

來看下結(jié)果:

>>> def display_scores(scores):
...     print("Scores:", scores)
...     print("Mean:", scores.mean())
...     print("Standard deviation:", scores.std())
...
>>> display_scores(tree_rmse_scores)
Scores: [ 74678.4916885   64766.2398337   69632.86942005  69166.67693232
          71486.76507766  73321.65695983  71860.04741226  71086.32691692
          76934.2726093   69060.93319262]
Mean: 71199.4280043
Standard deviation: 3202.70522793

現(xiàn)在決策樹就不像前面看起來那么好了。實際上,它看起來比線性回歸模型還糟!注意到交叉驗證不僅可以讓你得到模型性能的評估,還能測量評估的準(zhǔn)確性(即,它的標(biāo)準(zhǔn)差)。決策樹的評分大約是71200,通常波動有±3200。如果只有一個驗證集,就得不到這些信息。但是交叉驗證的代價是訓(xùn)練了模型多次,不可能總是這樣。

讓我們計算下線性回歸模型的的相同分?jǐn)?shù),以做確保:

>>> lin_scores = cross_val_score(lin_reg, housing_prepared, housing_labels,
...                              scoring="neg_mean_squared_error", cv=10)
...
>>> lin_rmse_scores = np.sqrt(-lin_scores)
>>> display_scores(lin_rmse_scores)
Scores: [ 70423.5893262   65804.84913139  66620.84314068  72510.11362141
          66414.74423281  71958.89083606  67624.90198297  67825.36117664
          72512.36533141  68028.11688067]
Mean: 68972.377566
Standard deviation: 2493.98819069

判斷沒錯:決策樹模型過擬合很嚴(yán)重,它的性能比線性回歸模型還差。

現(xiàn)在再嘗試最后一個模型:RandomForestRegressor。第7章我們會看到,隨機森林是通過用特征的隨機子集訓(xùn)練許多決策樹。在其它多個模型之上建立模型成為集成學(xué)習(xí)(Ensemble Learning),它是推進(jìn)ML算法的一種好方法。我們會跳過大部分的代碼,因為代碼本質(zhì)上和其它模型一樣:

>>> from sklearn.ensemble import RandomForestRegressor
>>> forest_reg = RandomForestRegressor()
>>> forest_reg.fit(housing_prepared, housing_labels)
>>> [...]
>>> forest_rmse
22542.396440343684
>>> display_scores(forest_rmse_scores)
Scores: [ 53789.2879722   50256.19806622  52521.55342602  53237.44937943
          52428.82176158  55854.61222549  52158.02291609  50093.66125649
          53240.80406125  52761.50852822]
Mean: 52634.1919593
Standard deviation: 1576.20472269

現(xiàn)在好多了:隨機森林看起來很有希望。但是,訓(xùn)練集的評分仍然比驗證集的評分低很多。解決過擬合可以通過簡化模型,給模型加限制(即,規(guī)整化),或用更多的訓(xùn)練數(shù)據(jù)。在深入隨機森林之前,你應(yīng)該嘗試下機器學(xué)習(xí)算法的其它類型模型(不同核心的支持向量機,神經(jīng)網(wǎng)絡(luò),等等),不要在調(diào)節(jié)超參數(shù)上花費太多時間。目標(biāo)是列出一個可能模型的列表(兩到五個)。

提示:你要保存每個試驗過的模型,以便后續(xù)可以再用。要確保有超參數(shù)和訓(xùn)練參數(shù),以及交叉驗證評分,和實際的預(yù)測值。這可以讓你比較不同類型模型的評分,還可以比較誤差種類。你可以用Python的模塊pickle,非常方便地保存Scikit-Learn模型,或使用sklearn.externals.joblib,后者序列化大NumPy數(shù)組更有效率:

from sklearn.externals import joblib

joblib.dump(my_model, "my_model.pkl")
# 然后
my_model_loaded = joblib.load("my_model.pkl")

模型微調(diào)

假設(shè)你現(xiàn)在有了一個列表,列表里有幾個有希望的模型。你現(xiàn)在需要對它們進(jìn)行微調(diào)。讓我們來看幾種微調(diào)的方法。

網(wǎng)格搜索

微調(diào)的一種方法是手工調(diào)整超參數(shù),直到找到一個好的超參數(shù)組合。這么做的話會非常冗長,你也可能沒有時間探索多種組合。

你應(yīng)該使用Scikit-Learn的GridSearchCV來做這項搜索工作。你所需要做的是告訴GridSearchCV要試驗有哪些超參數(shù),要試驗什么值,GridSearchCV就能用交叉驗證試驗所有可能超參數(shù)值的組合。例如,下面的代碼搜索了RandomForestRegressor超參數(shù)值的最佳組合:

from sklearn.model_selection import GridSearchCV

param_grid = [
    {'n_estimators': [3, 10, 30], 'max_features': [2, 4, 6, 8]},
    {'bootstrap': [False], 'n_estimators': [3, 10], 'max_features': [2, 3, 4]},
  ]

forest_reg = RandomForestRegressor()

grid_search = GridSearchCV(forest_reg, param_grid, cv=5,
                           scoring='neg_mean_squared_error')

grid_search.fit(housing_prepared, housing_labels)

當(dāng)你不能確定超參數(shù)該有什么值,一個簡單的方法是嘗試連續(xù)的10的次方(如果想要一個粒度更小的搜尋,可以用更小的數(shù),就像在這個例子中對超參數(shù)n_estimators做的)。

param_grid告訴Scikit-Learn首先評估所有的列在第一個dict中的n_estimatorsmax_features的3 × 4 = 12種組合(不用擔(dān)心這些超參數(shù)的含義,會在第7章中解釋)。然后嘗試第二個dict中超參數(shù)的2 × 3 = 6種組合,這次會將超參數(shù)bootstrap設(shè)為False而不是True(后者是該超參數(shù)的默認(rèn)值)。

總之,網(wǎng)格搜索會探索12 + 6 = 18種RandomForestRegressor的超參數(shù)組合,會訓(xùn)練每個模型五次(因為用的是五折交叉驗證)。換句話說,訓(xùn)練總共有18 × 5 = 90輪!折將要花費大量時間,完成后,你就能獲得參數(shù)的最佳組合,如下所示:

>>> grid_search.best_params_
{'max_features': 6, 'n_estimators': 30}

提示:因為30是n_estimators的最大值,你也應(yīng)該估計更高的值,因為這個值會持續(xù)提升。

你還能直接得到最佳的估計量:

>>> grid_search.best_estimator_
RandomForestRegressor(bootstrap=True, criterion='mse', max_depth=None,
           max_features=6, max_leaf_nodes=None, min_samples_leaf=1,
           min_samples_split=2, min_weight_fraction_leaf=0.0,
           n_estimators=30, n_jobs=1, oob_score=False, random_state=None,
           verbose=0, warm_start=False)

注意:如果GridSearchCV是以(默認(rèn)值)refit=True開始運行的,則一旦用交叉驗證找到了最佳的估計量,就會在整個訓(xùn)練集上重新訓(xùn)練。這是一個好方法,因為用更多數(shù)據(jù)訓(xùn)練會提高性能。

當(dāng)然,也可以得到評估值:

>>> cvres = grid_search.cv_results_
... for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
...     print(np.sqrt(-mean_score), params)
...
64912.0351358 {'max_features': 2, 'n_estimators': 3}
55535.2786524 {'max_features': 2, 'n_estimators': 10}
52940.2696165 {'max_features': 2, 'n_estimators': 30}
60384.0908354 {'max_features': 4, 'n_estimators': 3}
52709.9199934 {'max_features': 4, 'n_estimators': 10}
50503.5985321 {'max_features': 4, 'n_estimators': 30}
59058.1153485 {'max_features': 6, 'n_estimators': 3}
52172.0292957 {'max_features': 6, 'n_estimators': 10}
49958.9555932 {'max_features': 6, 'n_estimators': 30}
59122.260006 {'max_features': 8, 'n_estimators': 3}
52441.5896087 {'max_features': 8, 'n_estimators': 10}
50041.4899416 {'max_features': 8, 'n_estimators': 30}
62371.1221202 {'bootstrap': False, 'max_features': 2, 'n_estimators': 3}
54572.2557534 {'bootstrap': False, 'max_features': 2, 'n_estimators': 10}
59634.0533132 {'bootstrap': False, 'max_features': 3, 'n_estimators': 3}
52456.0883904 {'bootstrap': False, 'max_features': 3, 'n_estimators': 10}
58825.665239 {'bootstrap': False, 'max_features': 4, 'n_estimators': 3}
52012.9945396 {'bootstrap': False, 'max_features': 4, 'n_estimators': 10}

在這個例子中,我們通過設(shè)定超參數(shù)max_features為6,n_estimators為30,得到了最佳方案。對這個組合,RMSE的值是49959,這比之前使用默認(rèn)的超參數(shù)的值(52634)要稍微好一些。祝賀你,你成功地微調(diào)了最佳模型!

提示:不要忘記,你可以像超參數(shù)一樣處理數(shù)據(jù)準(zhǔn)備的步驟。例如,網(wǎng)格搜索可以自動判斷是否添加一個你不確定的特征(比如,使用轉(zhuǎn)換量CombinedAttributesAdder的超參數(shù)add_bedrooms_per_room)。它還能用相似的方法來自動找到處理異常值、缺失特征、特征選擇等任務(wù)的最佳方法。

隨機搜索

當(dāng)探索相對較少的組合時,就像前面的例子,網(wǎng)格搜索還可以。但是當(dāng)超參數(shù)的搜索空間很大時,最好使用RandomizedSearchCV。這個類的使用方法和類GridSearchCV很相似,但它不是嘗試所有可能的組合,而是通過選擇每個超參數(shù)的一個隨機值的特定數(shù)量的隨機組合。這個方法有兩個優(yōu)點:

  • 如果你讓隨機搜索運行,比如1000次,它會探索每個超參數(shù)的1000個不同的值(而不是像網(wǎng)格搜索那樣,只搜索每個超參數(shù)的幾個值)。

  • 你可以方便地通過設(shè)定搜索次數(shù),控制超參數(shù)搜索的計算量。

集成方法

另一種微調(diào)系統(tǒng)的方法是將表現(xiàn)最好的模型組合起來。組合(集成)之后的性能通常要比單獨的模型要好(就像隨機森林要比單獨的決策樹要好),特別是當(dāng)單獨模型的誤差類型不同時。我們會在第7章更深入地講解這點。

分析最佳模型和它們的誤差

通過分析最佳模型,常??梢垣@得對問題更深的了解。比如,RandomForestRegressor可以指出每個屬性對于做出準(zhǔn)確預(yù)測的相對重要性:

>>> feature_importances = grid_search.best_estimator_.feature_importances_
>>> feature_importances
array([  7.14156423e-02,   6.76139189e-02,   4.44260894e-02,
         1.66308583e-02,   1.66076861e-02,   1.82402545e-02,
         1.63458761e-02,   3.26497987e-01,   6.04365775e-02,
         1.13055290e-01,   7.79324766e-02,   1.12166442e-02,
         1.53344918e-01,   8.41308969e-05,   2.68483884e-03,
         3.46681181e-03])

將重要性分?jǐn)?shù)和屬性名放到一起:

>>> extra_attribs = ["rooms_per_hhold", "pop_per_hhold", "bedrooms_per_room"]
>>> cat_one_hot_attribs = list(encoder.classes_)
>>> attributes = num_attribs + extra_attribs + cat_one_hot_attribs
>>> sorted(zip(feature_importances,attributes), reverse=True)
[(0.32649798665134971, 'median_income'),
 (0.15334491760305854, 'INLAND'),
 (0.11305529021187399, 'pop_per_hhold'),
 (0.07793247662544775, 'bedrooms_per_room'),
 (0.071415642259275158, 'longitude'),
 (0.067613918945568688, 'latitude'),
 (0.060436577499703222, 'rooms_per_hhold'),
 (0.04442608939578685, 'housing_median_age'),
 (0.018240254462909437, 'population'),
 (0.01663085833886218, 'total_rooms'),
 (0.016607686091288865, 'total_bedrooms'),
 (0.016345876147580776, 'households'),
 (0.011216644219017424, '<1H OCEAN'),
 (0.0034668118081117387, 'NEAR OCEAN'),
 (0.0026848388432755429, 'NEAR BAY'),
 (8.4130896890070617e-05, 'ISLAND')]

有了這個信息,你就可以丟棄一些不那么重要的特征(比如,顯然只要一個分類ocean_proximity就夠了,所以可以丟棄掉其它的)。

你還應(yīng)該看一下系統(tǒng)犯的誤差,搞清為什么會有些誤差,以及如何改正問題(添加更多的特征,或相反,去掉沒有什么信息的特征,清洗異常值等等)。

用測試集評估系統(tǒng)

調(diào)節(jié)完系統(tǒng)之后,你終于有了一個性能足夠好的系統(tǒng)?,F(xiàn)在就可以用測試集評估最后的模型了。這個過程沒有什么特殊的:從測試集得到預(yù)測值和標(biāo)簽,運行full_pipeline轉(zhuǎn)換數(shù)據(jù)(調(diào)用transform(),而不是fit_transform()?。?,再用測試集評估最終模型:

final_model = grid_search.best_estimator_

X_test = strat_test_set.drop("median_house_value", axis=1)
y_test = strat_test_set["median_house_value"].copy()

X_test_prepared = full_pipeline.transform(X_test)

final_predictions = final_model.predict(X_test_prepared)

final_mse = mean_squared_error(y_test, final_predictions)
final_rmse = np.sqrt(final_mse)   # => evaluates to 48,209.6

評估結(jié)果通常要比交叉驗證的效果差一點,如果你之前做過很多超參數(shù)微調(diào)(因為你的系統(tǒng)在驗證集上微調(diào),得到了不錯的性能,通常不會在未知的數(shù)據(jù)集上有同樣好的效果)。這個例子不屬于這種情況,但是當(dāng)發(fā)生這種情況時,你一定要忍住不要調(diào)節(jié)超參數(shù),使測試集的效果變好;這樣的提升不能推廣到新數(shù)據(jù)上。

然后就是項目的預(yù)上線階段:你需要展示你的方案(重點說明學(xué)到了什么、做了什么、沒做什么、做過什么假設(shè)、系統(tǒng)的限制是什么,等等),記錄下所有事情,用漂亮的圖表和容易記住的表達(dá)(比如,“收入中位數(shù)是房價最重要的預(yù)測量”)做一次精彩的展示。

啟動、監(jiān)控、維護(hù)系統(tǒng)

很好,你被允許啟動系統(tǒng)了!你需要為實際生產(chǎn)做好準(zhǔn)備,特別是接入輸入數(shù)據(jù)源,并編寫測試。

你還需要編寫監(jiān)控代碼,以固定間隔檢測系統(tǒng)的實時表現(xiàn),當(dāng)發(fā)生下降時觸發(fā)報警。這對于捕獲突然的系統(tǒng)崩潰和性能下降十分重要。做監(jiān)控很常見,是因為模型會隨著數(shù)據(jù)的演化而性能下降,除非模型用新數(shù)據(jù)定期訓(xùn)練。

評估系統(tǒng)的表現(xiàn)需要對預(yù)測值采樣并進(jìn)行評估。這通常需要人來分析。分析者可能是領(lǐng)域?qū)<?,或者是眾包平臺(比如Amazon Mechanical Turk 或 CrowdFlower)的工人。不管采用哪種方法,你都需要將人工評估的pipeline植入系統(tǒng)。

你還要評估系統(tǒng)輸入數(shù)據(jù)的質(zhì)量。有時因為低質(zhì)量的信號(比如失靈的傳感器發(fā)送隨機值,或另一個團(tuán)隊的輸出停滯),系統(tǒng)的表現(xiàn)會逐漸變差,但可能需要一段時間,系統(tǒng)的表現(xiàn)才能下降到一定程度,觸發(fā)警報。如果監(jiān)測了系統(tǒng)的輸入,你就可能盡量早的發(fā)現(xiàn)問題。對于線上學(xué)習(xí)系統(tǒng),監(jiān)測輸入數(shù)據(jù)是非常重要的。

最后,你可能想定期用新數(shù)據(jù)訓(xùn)練模型。你應(yīng)該盡可能自動化這個過程。如果不這么做,非常有可能你需要每隔至少六個月更新模型,系統(tǒng)的表現(xiàn)就會產(chǎn)生嚴(yán)重波動。如果你的系統(tǒng)是一個線上學(xué)習(xí)系統(tǒng),你需要定期保存系統(tǒng)狀態(tài)快照,好能方便地回滾到之前的工作狀態(tài)。

實踐!

希望這一章能告訴你機器學(xué)習(xí)項目是什么樣的,你能用學(xué)到的工具訓(xùn)練一個好系統(tǒng)。你已經(jīng)看到,大部分的工作是數(shù)據(jù)準(zhǔn)備步驟、搭建監(jiān)測工具、建立人為評估pipeline和自動化定期模型訓(xùn)練,當(dāng)然,最好能了解整個過程、熟悉三或四種算法,而不是在探索高級算法上浪費全部時間,導(dǎo)致在全局上的時間不夠。

因此,如果你還沒這樣做,現(xiàn)在最好拿起臺電腦,選擇一個感興趣的數(shù)據(jù)集,將整個流程從頭到尾完成一遍。一個不錯的著手開始的地點是競賽網(wǎng)站,比如http://kaggle.com/:你會得到一個數(shù)據(jù)集,一個目標(biāo),以及分享經(jīng)驗的人。

練習(xí)

使用本章的房產(chǎn)數(shù)據(jù)集:

  1. 嘗試一個支持向量機回歸器(sklearn.svm.SVR),使用多個超參數(shù),比如kernel="linear"(多個超參數(shù)C值)?,F(xiàn)在不用擔(dān)心這些超參數(shù)是什么含義。最佳的SVR預(yù)測表現(xiàn)如何?
  2. 嘗試用RandomizedSearchCV替換GridSearchCV。
  3. 嘗試在準(zhǔn)備pipeline中添加一個只選擇最重要屬性的轉(zhuǎn)換器。
  4. 嘗試創(chuàng)建一個單獨的可以完成數(shù)據(jù)準(zhǔn)備和最終預(yù)測的pipeline。
  5. 使用GridSearchCV自動探索一些準(zhǔn)備過程中的候選項。
    練習(xí)題答案可以在線上的Jupyter notebooks(https://github.com/ageron/handson-ml)找到。

(第一部分 機器學(xué)習(xí)基礎(chǔ))
第01章 機器學(xué)習(xí)概覽
第02章 一個完整的機器學(xué)習(xí)項目(上)
第02章 一個完整的機器學(xué)習(xí)項目(下)
第03章 分類
第04章 訓(xùn)練模型
第05章 支持向量機
第06章 決策樹
第07章 集成學(xué)習(xí)和隨機森林
第08章 降維
(第二部分 神經(jīng)網(wǎng)絡(luò)和深度學(xué)習(xí))
第9章 啟動和運行TensorFlow
第10章 人工神經(jīng)網(wǎng)絡(luò)
第11章 訓(xùn)練深度神經(jīng)網(wǎng)絡(luò)(上)
第11章 訓(xùn)練深度神經(jīng)網(wǎng)絡(luò)(下)
第12章 設(shè)備和服務(wù)器上的分布式 TensorFlow
第13章 卷積神經(jīng)網(wǎng)絡(luò)
第14章 循環(huán)神經(jīng)網(wǎng)絡(luò)
第15章 自編碼器
第16章 強化學(xué)習(xí)(上)
第16章 強化學(xué)習(xí)(下)


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容