Sklearn 與 TensorFlow 機器學習實戰—一個完整的機器學習項目

本章中,你會僞裝做爲被一家地產公司剛剛僱傭的數據科學家,完整地學習一個案例項目。下面是主要步驟:node

  1. 項目概述。
  2. 獲取數據。
  3. 發現並可視化數據,發現規律。
  4. 爲機器學習算法準備數據。
  5. 選擇模型,進行訓練。
  6. 微調模型。
  7. 給出解決方案。
  8. 部署、監控、維護系統。

使用真實數據

學習機器學習時,最好使用真實數據,而不是人工數據集。幸運的是,有上千個開源數據集能夠進行選擇,涵蓋多個領域。如下是一些能夠查找的數據的地方:python

本章,咱們選擇的是 StatLib 的加州房產價格數據集(見圖 2-1)。這個數據集是基於 1990 年加州普查的數據。數據已經有點老(1990 年還能買一個灣區不錯的房子),可是它有許多優勢,利於學習,因此假設這個數據爲最近的。爲了便於教學,咱們添加了一個類別屬性,併除去了一些。git

圖 2-1 加州房產價格github

項目概覽

歡迎來到機器學習房地產公司!你的第一個任務是利用加州普查數據,創建一個加州房價模型。這個數據包含每一個街區組的人口、收入中位數、房價中位數等指標。算法

街區組是美國調查局發佈樣本數據的最小地理單位(一個街區一般有 600 到 3000 人)。咱們將其簡稱爲「街區」。數據庫

你的模型要利用這個數據進行學習,而後根據其它指標,預測任何街區的的房價中位數。bootstrap

提示:你是一個有條理的數據科學家,你要作的第一件事是拿出你的機器學習項目清單。你可使用附錄 B 中的清單;這個清單適用於大多數的機器學習項目,可是你仍是要確認它是否知足需求。在本章中,咱們會檢查許多清單上的項目,可是也會跳過一些簡單的,有些會在後面的章節再討論。後端

劃定問題

問老闆的第一個問題應該是商業目標是什麼?創建模型可能不是最終目標。公司要如何使用、並從模型受益?這很是重要,由於它決定了如何劃定問題,要選擇什麼算法,評估模型性能的指標是什麼,要花多少精力進行微調。數組

老闆告訴你你的模型的輸出(一個區的房價中位數)會傳給另外一個機器學習系統(見圖 2-2),也有其它信號會傳入後面的系統。這一整套系統能夠肯定某個區進行投資值不值。肯定值不值得投資很是重要,它直接影響利潤。瀏覽器

圖 2-2 房地產投資的機器學習流水線

流水線

一系列的數據處理組件被稱爲數據流水線。流水線在機器學習系統中很常見,由於有許多數據要處理和轉換。

組件一般是異步運行的。每一個組件吸納進大量數據,進行處理,而後將數據傳輸到另外一個數據容器中,然後流水線中的另外一個組件收入這個數據,而後輸出,這個過程依次進行下去。每一個組件都是獨立的:組件間的接口只是數據容器。這樣可讓系統更便於理解(記住數據流的圖),不一樣的項目組能夠關注於不一樣的組件。進而,若是一個組件失效了,下游的組件使用失效組件最後生產的數據,一般能夠正常運行(一段時間)。這樣就使整個架構至關健壯。

另外一方面,若是沒有監控,失效的組件會在不被注意的狀況下運行一段時間。數據會受到污染,整個系統的性能就會降低。

下一個要問的問題是,如今的解決方案效果如何。老闆一般會給一個參考性能,以及如何解決問題。老闆說,如今街區的房價是靠專家手工估計的,專家隊伍收集最新的關於一個區的信息(不包括房價中位數),他們使用複雜的規則進行估計。這種方法費錢費時間,並且估計結果不理想,偏差率大概有 15%。

OK,有了這些信息,你就能夠開始設計系統了。首先,你須要劃定問題:監督或非監督,仍是強化學習?這是個分類任務、迴歸任務,仍是其它的?要使用批量學習仍是線上學習?繼續閱讀以前,請暫停一下,嘗試本身回答下這些問題。

你能回答出來嗎?一塊兒看下答案:很明顯,這是一個典型的監督學習任務,由於你要使用的是有標籤的訓練樣本(每一個實例都有預約的產出,即街區的房價中位數)。而且,這是一個典型的迴歸任務,由於你要預測一個值。講的更細些,這是一個多變量回歸問題,由於系統要使用多個變量進行預測(要使用街區的人口,收入中位數等等)。在第一章中,你只是根據人均 GDP 來預測生活滿意度,所以這是一個單變量回歸問題。最後,沒有連續的數據流進入系統,沒有特別需求須要對數據變更做出快速適應。數據量不大能夠放到內存中,所以批量學習就夠了。

提示:若是數據量很大,你能夠要麼在多個服務器上對批量學習作拆分(使用 MapReduce 技術,後面會看到),或是使用線上學習。

選擇性能指標

下一步是選擇性能指標。迴歸問題的典型指標是均方根偏差(RMSE)。均方根偏差測量的是系統預測偏差的標準差。例如,RMSE 等於 50000,意味着,68% 的系統預測值位於實際值的 50000 美圓之內,95% 的預測值位於實際值的 100000 美圓之內(一個特徵一般都符合高斯分佈,即知足 「68-95-99.7」規則:大約68%的值落在內,95% 的值落在內,99.7%的值落在內,這裏的σ等於50000)。公式 2-1 展現了計算 RMSE 的方法。

公式 2-1 均方根偏差(RMSE)

符號的含義

這個方程引入了一些常見的貫穿本書的機器學習符號:

  • m是測量 RMSE 的數據集中的實例數量。
    例如,若是用一個含有 2000 個街區的驗證集求 RMSE,則m = 2000

  • x^{(i)} 是數據集第i個實例的全部特徵值(不包含標籤)的向量,y^{(i)} 是它的標籤(這個實例的輸出值)。 

    例如,若是數據集中的第一個街區位於經度 –118.29°,緯度 33.91°,有 1416 名居民,收入中位數是 38372 美圓,房價中位數是 156400 美圓(忽略掉其它的特徵),則有:

    和,

  • X是包含數據集中全部實例的全部特徵值(不包含標籤)的矩陣。每一行是一個實例,第i行是 x^{(i)} 的轉置,記爲 x^{(i)T}

    例如,仍然是前面提到的第一區,矩陣X就是:

  • h是系統的預測函數,也稱爲假設(hypothesis)。當系統收到一個實例的特徵向量 x^{(i)},就會輸出這個實例的一個預測值 \hat y{(i)} = h(x{(i)})\hat y 讀做y-hat)。

    例如,若是系統預測第一區的房價中位數是 158400 美圓,則 \hat y{(1)} = h(x{(1)}) = 158400。預測偏差是 \hat y{(1)} – y{(1)} = 2000

  • RMSE(X,h)是使用假設h在樣本集上測量的損失函數。

咱們使用小寫斜體表示標量值(例如 \it m 或 \it{y^{(i)}})和函數名(例如 \it h),小寫粗體表示向量(例如 \bb{x^{(i)}}),大寫粗體表示矩陣(例如 \bb{X})。

雖然大多數時候 RMSE 是迴歸任務可靠的性能指標,在有些狀況下,你可能須要另外的函數。例如,假設存在許多異常的街區。此時,你可能須要使用平均絕對偏差(Mean Absolute Error,也稱做平均絕對誤差),見公式 2-2:

公式2-2 平均絕對偏差

RMSE 和 MAE 都是測量預測值和目標值兩個向量距離的方法。有多種測量距離的方法,或範數:

  • 計算對應歐幾里得範數的平方和的根(RMSE):這個距離介紹過。它也稱做ℓ2範數,標記爲 \| \cdot \|_2(或只是 \| \cdot \|)。

  • 計算對應於ℓ1(標記爲 \| \cdot \|_1)範數的絕對值和(MAE)。有時,也稱其爲曼哈頓範數,由於它測量了城市中的兩點,沿着矩形的邊行走的距離。

  • 更通常的,包含n個元素的向量vℓk範數(K 階閔氏範數),定義成

    ℓ0(漢明範數)只顯示了這個向量的基數(即,非零元素的個數),ℓ∞(切比雪夫範數)是向量中最大的絕對值。

  • 範數的指數越高,就越關注大的值而忽略小的值。這就是爲何 RMSE 比 MAE 對異常值更敏感。可是當異常值是指數分佈的(相似正態曲線),RMSE 就會表現很好。

覈實假設

最後,最好列出並覈對迄今(你或其餘人)做出的假設,這樣能夠儘早發現嚴重的問題。例如,你的系統輸出的街區房價,會傳入到下游的機器學習系統,咱們假設這些價格確實會被當作街區房價使用。可是若是下游系統實際上將價格轉化成了分類(例如,便宜、中等、昂貴),而後使用這些分類,而不是使用價格。這樣的話,得到準確的價格就不那麼重要了,你只須要獲得合適的分類。問題相應地就變成了一個分類問題,而不是迴歸任務。你可不想在一個迴歸系統上工做了數月,最後才發現真相。

幸運的是,在與下游系統主管探討以後,你很確信他們須要的就是實際的價格,而不是分類。很好!整裝待發,能夠開始寫代碼了。

獲取數據

開始動手。最後用 Jupyter notebook 完整地敲一遍示例代碼。完整的代碼位於 https://github.com/ageron/handson-ml。

建立工做空間

首先,你須要安裝 Python。可能已經安裝過了,沒有的話,能夠從官網下載 https://www.python.org/。

接下來,須要爲你的機器學習代碼和數據集建立工做空間目錄。打開一個終端,輸入如下命令(在提示符$以後):

$ export ML_PATH="$HOME/ml"      # 能夠更改路徑
$ mkdir -p $ML_PATH

還須要一些 Python 模塊:Jupyter、NumPy、Pandas、Matplotlib 和 Scikit-Learn。若是全部這些模塊都已經在 Jupyter 中運行了,你能夠直接跳到下一節「下載數據」。若是還沒安裝,有多種方法能夠進行安裝(包括它們的依賴)。你可使用系統的包管理系統(好比 Ubuntu 上的apt-get,或 macOS 上的 MacPorts 或 HomeBrew),安裝一個 Python 科學計算環境好比 Anaconda,使用 Anaconda 的包管理系統,或者使用 Python 本身的包管理器pip,它是 Python 安裝包(自從 2.7.9 版本)自帶的。能夠用下面的命令檢測是否安裝pip

$ pip3 --version
pip 9.0.1 from [...]/lib/python3.5/site-packages (python 3.5)

你須要保證pip是近期的版本,至少高於 1.4,以保障二進制模塊文件的安裝(也稱爲 wheel)。要升級pip,可使用下面的命令:

$ pip3 install --upgrade pip
Collecting pip
[...]
Successfully installed pip-9.0.1

建立獨立環境

若是你但願在一個獨立環境中工做(強烈推薦這麼作,不一樣項目的庫的版本不會衝突),用下面的pip命令安裝virtualenv

$ pip3 install --user --upgrade virtualenv
Collecting virtualenv
[...]
Successfully installed virtualenv

如今能夠經過下面命令建立一個獨立的 Python 環境:

$ cd $ML_PATH
$ virtualenv env
Using base prefix '[...]'
New python executable in [...]/ml/env/bin/python3.5
Also creating executable in [...]/ml/env/bin/python
Installing setuptools, pip, wheel...done.

之後每次想要激活這個環境,只需打開一個終端而後輸入:

$ cd $ML_PATH
$ source env/bin/activate

啓動該環境時,使用pip安裝的任何包都只安裝於這個獨立環境中,Python 指揮訪問這些包(若是你但願 Python 能訪問系統的包,建立環境時要使用包選項--system-site)。更多信息,請查看virtualenv文檔。

如今,你可使用pip命令安裝全部必需的模塊和它們的依賴:

$ pip3 install --upgrade jupyter matplotlib numpy pandas scipy scikit-learn
Collecting jupyter
  Downloading jupyter-1.0.0-py2.py3-none-any.whl
Collecting matplotlib
  [...]

要檢查安裝,能夠用下面的命令引入每一個模塊:

$ python3 -c "import jupyter, matplotlib, numpy, pandas, scipy, sklearn"

這個命令不該該有任何輸出和錯誤。如今你能夠用下面的命令打開 Jupyter:

$ jupyter notebook
[I 15:24 NotebookApp] Serving notebooks from local directory: [...]/ml
[I 15:24 NotebookApp] 0 active kernels
[I 15:24 NotebookApp] The Jupyter Notebook is running at: http://localhost:8888/
[I 15:24 NotebookApp] Use Control-C to stop this server and shut down all
kernels (twice to skip confirmation).

Jupyter 服務器如今運行在終端上,監聽 888 8端口。你能夠用瀏覽器打開http://localhost:8888/,以訪問這個服務器(服務器啓動時,一般就自動打開了)。你能夠看到一個空的工做空間目錄(若是按照先前的virtualenv步驟,只包含env目錄)。

如今點擊按鈕 New 建立一個新的 Python 注本,選擇合適的 Python 版本(見圖 2-3)。

圖 2-3 Jupyter 的工做空間

這一步作了三件事:首先,在工做空間中建立了一個新的 notebook 文件Untitled.ipynb;第二,它啓動了一個 Jupyter 的 Python 內核來運行這個 notebook;第三,在一個新欄中打開這個 notebook。接下來,點擊 Untitled,將這個 notebook 重命名爲Housing(這會將ipynb文件自動命名爲Housing.ipynb)。

notebook 包含一組代碼框。每一個代碼框能夠放入可執行代碼或格式化文本。如今,notebook 只有一個空的代碼框,標籤是In [1]:。在框中輸入print("Hello world!"),點擊運行按鈕(見圖 2-4)或按Shift+Enter。這會將當前的代碼框發送到 Python 內核,運行以後會返回輸出。結果顯示在代碼框下方。因爲抵達了 notebook 的底部,一個新的代碼框會被自動建立出來。從 Jupyter 的 Help 菜單中的 User Interface Tour,能夠學習 Jupyter 的基本操做。

圖 2-4 在 notebook 中打印Hello world!

下載數據

通常狀況下,數據是存儲於關係型數據庫(或其它常見數據庫)中的多個表、文檔、文件。要訪問數據,你首先要有密碼和登陸權限,並要瞭解數據模式。可是在這個項目中,這一切要簡單些:只要下載一個壓縮文件,housing.tgz,它包含一個 CSV 文件housing.csv,含有全部數據。

你可使用瀏覽器下載,運行tar xzf housing.tgz解壓出csv文件,可是更好的辦法是寫一個小函數來作這件事。若是數據變更頻繁,這麼作是很是好的,由於可讓你寫一個小腳本隨時獲取最新的數據(或者建立一個定時任務來作)。若是你想在多臺機器上安裝數據集,獲取數據自動化也是很是好的。

下面是獲取數據的函數:

import os
import tarfile
from six.moves import urllib

DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml/master/"
HOUSING_PATH = "datasets/housing"
HOUSING_URL = DOWNLOAD_ROOT + HOUSING_PATH + "/housing.tgz"

def fetch_housing_data(housing_url=HOUSING_URL, housing_path=HOUSING_PATH):
    if not os.path.isdir(housing_path):
        os.makedirs(housing_path)
    tgz_path = os.path.join(housing_path, "housing.tgz")
    urllib.request.urlretrieve(housing_url, tgz_path)
    housing_tgz = tarfile.open(tgz_path)
    housing_tgz.extractall(path=housing_path)
    housing_tgz.close()

如今,當你調用fetch_housing_data(),就會在工做空間建立一個datasets/housing目錄,下載housing.tgz文件,解壓出housing.csv

而後使用Pandas加載數據。仍是用一個小函數來加載數據:

import pandas as pd

def load_housing_data(housing_path=HOUSING_PATH):
    csv_path = os.path.join(housing_path, "housing.csv")
    return pd.read_csv(csv_path)

這個函數會返回一個包含全部數據的 Pandas DataFrame 對象。

快速查看數據結構

使用DataFramehead()方法查看該數據集的前5行(見圖 2-5)。

圖 2-5 數據集的前五行

每一行都表示一個街區。共有 10 個屬性(截圖中能夠看到 6 個):經度、維度、房屋年齡中位數、總房間數、總臥室數、人口數、家庭數、收入中位數、房屋價值中位數、離大海距離。

info()方法能夠快速查看數據的描述,特別是總行數、每一個屬性的類型和非空值的數量(見圖 2-6)。

圖 2-6 房屋信息

數據集中共有 20640 個實例,按照機器學習的標準這個數據量很小,可是很是適合入門。咱們注意到總房間數只有 20433 個非空值,這意味着有 207 個街區缺乏這個值。咱們將在後面對它進行處理。

全部的屬性都是數值的,除了離大海距離這項。它的類型是對象,所以能夠包含任意 Python 對象,可是由於該項是從 CSV 文件加載的,因此必然是文本類型。在剛纔查看數據前五項時,你可能注意到那一列的值是重複的,意味着它多是一項表示類別的屬性。可使用value_counts()方法查看該項中都有哪些類別,每一個類別中都包含有多少個街區:

>>> housing["ocean_proximity"].value_counts()
<1H OCEAN     9136
INLAND        6551
NEAR OCEAN    2658
NEAR BAY      2290
ISLAND           5
Name: ocean_proximity, dtype: int64

再來看其它字段。describe()方法展現了數值屬性的歸納(見圖 2-7)。

圖 2-7 每一個數值屬性的歸納

countmeanminmax幾行的意思很明顯了。注意,空值被忽略了(因此,臥室總數是 20433 而不是 20640)。std是標準差(揭示數值的分散度)。25%、50%、75% 展現了對應的分位數:每一個分位數指明小於這個值,且指定分組的百分比。例如,25% 的街區的房屋年齡中位數小於 18,而 50% 的小於 29,75% 的小於 37。這些值一般稱爲第 25 個百分位數(或第一個四分位數),中位數,第 75 個百分位數(第三個四分位數)。

另外一種快速瞭解數據類型的方法是畫出每一個數值屬性的柱狀圖。柱狀圖(的縱軸)展現了特定範圍的實例的個數。你還能夠一次給一個屬性畫圖,或對完整數據集調用hist()方法,後者會畫出每一個數值屬性的柱狀圖(見圖 2-8)。例如,你能夠看到略微超過 800 個街區的median_house_value值差很少等於 500000 美圓。

%matplotlib inline   # only in a Jupyter notebook
import matplotlib.pyplot as plt
housing.hist(bins=50, figsize=(20,15))
plt.show()

圖 2-8 每一個數值屬性的柱狀圖

注:hist()方法依賴於 Matplotlib,後者依賴於用戶指定的圖形後端以打印到屏幕上。所以在畫圖以前,你要指定 Matplotlib 要使用的後端。最簡單的方法是使用 Jupyter 的魔術命令%matplotlib inline。它會告訴 Jupyter 設定好 Matplotlib,以使用 Jupyter 本身的後端。繪圖就會在 notebook 中渲染了。注意在 Jupyter 中調用show()不是必要的,由於代碼框執行後 Jupyter 會自動展現圖像。

注意柱狀圖中的一些點:

  1. 首先,收入中位數貌似不是美圓(USD)。與數據採集團隊交流以後,你被告知數據是通過縮放調整的,太高收入中位數的會變爲 15(實際爲 15.0001),太低的會變爲 5(實際爲 0.4999)。在機器學習中對數據進行預處理很正常,這不必定是個問題,但你要明白數據是如何計算出來的。

  2. 房屋年齡中位數和房屋價值中位數也被設了上限。後者多是個嚴重的問題,由於它是你的目標屬性(你的標籤)。你的機器學習算法可能學習到價格不會超出這個界限。你須要與下游團隊覈實,這是否會成爲問題。若是他們告訴你他們須要明確的預測值,即便超過 500000 美圓,你則有兩個選項:

    1. 對於設了上限的標籤,從新收集合適的標籤;
    2. 將這些街區從訓練集移除(也從測試集移除,由於若房價超出 500000 美圓,你的系統就會被差評)。
  3. 這些屬性值有不一樣的量度。咱們會在本章後面討論特徵縮放。

  4. 最後,許多柱狀圖的尾巴很長:相較於左邊,它們在中位數的右邊延伸過遠。對於某些機器學習算法,這會使檢測規律變得更難些。咱們會在後面嘗試變換處理這些屬性,使其變爲正態分佈。

但願你如今對要處理的數據有必定了解了。

警告:稍等!在你進一步查看數據以前,你須要建立一個測試集,將它放在一旁,千萬不要再看它。

建立測試集

在這個階段就分割數據,聽起來很奇怪。畢竟,你只是簡單快速地查看了數據而已,你須要再仔細調查下數據以決定使用什麼算法。這麼想是對的,可是人類的大腦是一個神奇的發現規律的系統,這意味着大腦很是容易發生過擬合:若是你查看了測試集,就會不經意地按照測試集中的規律來選擇某個特定的機器學習模型。再當你使用測試集來評估偏差率時,就會致使評估過於樂觀,而實際部署的系統表現就會差。這稱爲數據透視誤差。

理論上,建立測試集很簡單:只要隨機挑選一些實例,通常是數據集的 20%,放到一邊:

import numpy as np
def split_train_test(data, test_ratio):
    shuffled_indices = np.random.permutation(len(data))
    test_set_size = int(len(data) * test_ratio)
    test_indices = shuffled_indices[:test_set_size]
    train_indices = shuffled_indices[test_set_size:]
    return data.iloc[train_indices], data.iloc[test_indices]

而後能夠像下面這樣使用這個函數:

>>> train_set, test_set = split_train_test(housing, 0.2)
>>> print(len(train_set), "train +", len(test_set), "test")
16512 train + 4128 test

這個方法可行,可是並不完美:若是再次運行程序,就會產生一個不一樣的測試集!屢次運行以後,你(或你的機器學習算法)就會獲得整個數據集,這是須要避免的。

解決的辦法之一是保存第一次運行獲得的測試集,並在隨後的過程加載。另外一種方法是在調用np.random.permutation()以前,設置隨機數生成器的種子(好比np.random.seed(42)),以產生老是相同的洗牌指數(shuffled indices)。

可是若是數據集更新,這兩個方法都會失效。一個一般的解決辦法是使用每一個實例的ID來斷定這個實例是否應該放入測試集(假設每一個實例都有惟一而且不變的ID)。例如,你能夠計算出每一個實例ID的哈希值,只保留其最後一個字節,若是該值小於等於 51(約爲 256 的 20%),就將其放入測試集。這樣能夠保證在屢次運行中,測試集保持不變,即便更新了數據集。新的測試集會包含新實例中的 20%,但不會有以前位於訓練集的實例。下面是一種可用的方法:

import hashlib
def test_set_check(identifier, test_ratio, hash):
    return hash(np.int64(identifier)).digest()[-1] < 256 * test_ratio

def split_train_test_by_id(data, test_ratio, id_column, hash=hashlib.md5):
    ids = data[id_column]
    in_test_set = ids.apply(lambda id_: test_set_check(id_, test_ratio, hash))
    return data.loc[~in_test_set], data.loc[in_test_set]

不過,房產數據集沒有ID這一列。最簡單的方法是使用行索引做爲 ID:

housing_with_id = housing.reset_index()   # adds an `index` column
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")

若是使用行索引做爲惟一識別碼,你須要保證新數據都放到現有數據的尾部,且沒有行被刪除。若是作不到,則能夠用最穩定的特徵來建立惟一識別碼。例如,一個區的維度和經度在幾百萬年以內是不變的,因此能夠將二者結合成一個 ID:

housing_with_id["id"] = housing["longitude"] * 1000 + housing["latitude"]
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "id")

Scikit-Learn 提供了一些函數,能夠用多種方式將數據集分割成多個子集。最簡單的函數是train_test_split,它的做用和以前的函數split_train_test很像,並帶有其它一些功能。首先,它有一個random_state參數,能夠設定前面講過的隨機生成器種子;第二,你能夠將種子傳遞給多個行數相同的數據集,能夠在相同的索引上分割數據集(這個功能很是有用,好比你的標籤值是放在另外一個DataFrame裏的):

from sklearn.model_selection import train_test_split

train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)

目前爲止,咱們採用的都是純隨機的取樣方法。當你的數據集很大時(尤爲是和屬性數相比),這一般可行;但若是數據集不大,就會有采樣誤差的風險。當一個調查公司想要對 1000 我的進行調查,它們不是在電話亭裏隨機選 1000 我的出來。調查公司要保證這 1000 我的對人羣總體有表明性。例如,美國人口的 51.3% 是女性,48.7% 是男性。因此在美國,嚴謹的調查須要保證樣本也是這個比例:513 名女性,487 名男性。這稱做分層採樣(stratified sampling):將人羣分紅均勻的子分組,稱爲分層,從每一個分層去取合適數量的實例,以保證測試集對總人數有表明性。若是調查公司採用純隨機採樣,會有 12% 的機率致使採樣誤差:女性人數少於 49%,或多於 54%。無論發生那種狀況,調查結果都會嚴重誤差。

假設專家告訴你,收入中位數是預測房價中位數很是重要的屬性。你可能想要保證測試集能夠表明總體數據集中的多種收入分類。由於收入中位數是一個連續的數值屬性,你首先須要建立一個收入類別屬性。再仔細地看一下收入中位數的柱狀圖(圖 2-9)(譯註:該圖是對收入中位數處理事後的圖):

圖 2-9 收入分類的柱狀圖

大多數的收入中位數的值彙集在 2-5(萬美圓),可是一些收入中位數會超過 6。數據集中的每一個分層都要有足夠的實例位於你的數據中,這點很重要。不然,對分層重要性的評估就會有誤差。這意味着,你不能有過多的分層,且每一個分層都要足夠大。後面的代碼經過將收入中位數除以 1.5(以限制收入分類的數量),建立了一個收入類別屬性,用ceil對值舍入(以產生離散的分類),而後將全部大於 5的分類納入到分類 5:

housing["income_cat"] = np.ceil(housing["median_income"] / 1.5)
housing["income_cat"].where(housing["income_cat"] < 5, 5.0, inplace=True)

如今,就能夠根據收入分類,進行分層採樣。你可使用 Scikit-Learn 的StratifiedShuffleSplit類:

from sklearn.model_selection import StratifiedShuffleSplit
split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)

for train_index, test_index in split.split(housing, housing["income_cat"]):
    strat_train_set = housing.loc[train_index]
    strat_test_set = housing.loc[test_index]

檢查下結果是否符合預期。你能夠在完整的房產數據集中查看收入分類比例:

>>> housing["income_cat"].value_counts() / len(housing)
3.0    0.350581
2.0    0.318847
4.0    0.176308
5.0    0.114438
1.0    0.039826
Name: income_cat, dtype: float64

使用類似的代碼,還能夠測量測試集中收入分類的比例。圖 2-10 對比了總數據集、分層採樣的測試集、純隨機採樣測試集的收入分類比例。能夠看到,分層採樣測試集的收入分類比例與總數據集幾乎相同,而隨機採樣數據集誤差嚴重。

圖 2-10 分層採樣和純隨機採樣的樣本誤差比較

如今,你須要刪除income_cat屬性,使數據回到初始狀態:

for set in (strat_train_set, strat_test_set):
    set.drop(["income_cat"], axis=1, inplace=True)

咱們用了大量時間來生成測試集的緣由是:測試集一般被忽略,但實際是機器學習很是重要的一部分。還有,生成測試集過程當中的許多思路對於後面的交叉驗證討論是很是有幫助的。接下來進入下一階段:數據探索。

數據探索和可視化、發現規律

目前爲止,你只是快速查看了數據,對要處理的數據有了總體瞭解。如今的目標是更深的探索數據。

首先,保證你將測試集放在了一旁,只是研究訓練集。另外,若是訓練集很是大,你可能須要再採樣一個探索集,保證操做方便快速。在咱們的案例中,數據集很小,因此能夠在全集上直接工做。建立一個副本,以避免損傷訓練集:

housing = strat_train_set.copy()

地理數據可視化

由於存在地理信息(緯度和經度),建立一個全部街區的散點圖來數據可視化是一個不錯的主意(圖 2-11):

housing.plot(kind="scatter", x="longitude", y="latitude")

圖 2-11 數據的地理信息散點圖

這張圖看起來很像加州,可是看不出什麼特別的規律。將alpha設爲 0.1,能夠更容易看出數據點的密度(圖 2-12):

housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)

圖 2-12 顯示高密度區域的散點圖

如今看起來好多了:能夠很是清楚地看到高密度區域,灣區、洛杉磯和聖迭戈,以及中央谷,特別是從薩克拉門託和弗雷斯諾。

一般來說,人類的大腦很是善於發現圖片中的規律,可是須要調整可視化參數使規律顯現出來。

如今來看房價(圖 2-13)。每一個圈的半徑表示街區的人口(選項s),顏色表明價格(選項c)。咱們用預先定義的名爲jet的顏色圖(選項cmap),它的範圍是從藍色(低價)到紅色(高價):

housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.4,
    s=housing["population"]/100, label="population",
    c="median_house_value", cmap=plt.get_cmap("jet"), colorbar=True,
)
plt.legend()

圖 2-13 加州房價

這張圖說明房價和位置(好比,靠海)和人口密度聯繫密切,這點你可能早就知道。可使用聚類算法來檢測主要的彙集,用一個新的特徵值測量彙集中心的距離。儘管北加州海岸區域的房價不是很是高,但離大海距離屬性也可能頗有用,因此這不是用一個簡單的規則就能夠定義的問題。

查找關聯

由於數據集並非很是大,你能夠很容易地使用corr()方法計算出每對屬性間的標準相關係數(standard correlation coefficient,也稱做皮爾遜相關係數):

corr_matrix = housing.corr()

如今來看下每一個屬性和房價中位數的關聯度:

>>> corr_matrix["median_house_value"].sort_values(ascending=False)
median_house_value    1.000000
median_income         0.687170
total_rooms           0.135231
housing_median_age    0.114220
households            0.064702
total_bedrooms        0.047865
population           -0.026699
longitude            -0.047279
latitude             -0.142826
Name: median_house_value, dtype: float64

相關係數的範圍是 -1 到 1。當接近 1 時,意味強正相關;例如,當收入中位數增長時,房價中位數也會增長。當相關係數接近 -1 時,意味強負相關;你能夠看到,緯度和房價中位數有輕微的負相關性(即,越往北,房價越可能下降)。最後,相關係數接近 0,意味沒有線性相關性。圖 2-14 展現了相關係數在橫軸和縱軸之間的不一樣圖形。

圖 2-14 不一樣數據集的標準相關係數(來源:Wikipedia;公共領域圖片)

警告:相關係數只測量線性關係(若是x上升,y則上升或降低)。相關係數可能會徹底忽略非線性關係(例如,若是x接近 0,則y值會變高)。在上面圖片的最後一行中,他們的相關係數都接近於 0,儘管它們的軸並不獨立:這些就是非線性關係的例子。另外,第二行的相關係數等於 1 或 -1;這和斜率沒有任何關係。例如,你的身高(單位是英寸)與身高(單位是英尺或納米)的相關係數就是 1。

另外一種檢測屬性間相關係數的方法是使用 Pandas 的scatter_matrix函數,它能畫出每一個數值屬性對每一個其它數值屬性的圖。由於如今共有 11 個數值屬性,你能夠獲得11 ** 2 = 121張圖,在一頁上畫不下,因此只關注幾個和房價中位數最有可能相關的屬性(圖 2-15):

from pandas.tools.plotting import scatter_matrix

attributes = ["median_house_value", "median_income", "total_rooms",
              "housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12, 8))

圖 2-15 散點矩陣

若是 pandas 將每一個變量對本身做圖,主對角線(左上到右下)都會是直線圖。因此 Pandas 展現的是每一個屬性的柱狀圖(也能夠是其它的,請參考 Pandas 文檔)。

最有但願用來預測房價中位數的屬性是收入中位數,所以將這張圖放大(圖 2-16):

housing.plot(kind="scatter", x="median_income",y="median_house_value",
             alpha=0.1)

圖 2-16 收入中位數 vs 房價中位數

這張圖說明了幾點。首先,相關性很是高;能夠清晰地看到向上的趨勢,而且數據點不是很是分散。第二,咱們以前看到的最高價,清晰地呈現爲一條位於 500000 美圓的水平線。這張圖也呈現了一些不是那麼明顯的直線:一條位於 450000 美圓的直線,一條位於 350000 美圓的直線,一條在 280000 美圓的線,和一些更靠下的線。你可能但願去除對應的街區,以防止算法重複這些巧合。

屬性組合試驗

但願前面的一節能教給你一些探索數據、發現規律的方法。你發現了一些數據的巧合,須要在給算法提供數據以前,將其去除。你還發現了一些屬性間有趣的關聯,特別是目標屬性。你還注意到一些屬性具備長尾分佈,所以你可能要將其進行轉換(例如,計算其log對數)。固然,不一樣項目的處理方法各不相同,但大致思路是類似的。

給算法準備數據以前,你須要作的最後一件事是嘗試多種屬性組合。例如,若是你不知道某個街區有多少戶,該街區的總房間數就沒什麼用。你真正須要的是每戶有幾個房間。類似的,總臥室數也不重要:你可能須要將其與房間數進行比較。每戶的人口數也是一個有趣的屬性組合。讓咱們來建立這些新的屬性:

housing["rooms_per_household"] = housing["total_rooms"]/housing["households"]
housing["bedrooms_per_room"] = housing["total_bedrooms"]/housing["total_rooms"]
housing["population_per_household"]=housing["population"]/housing["households"]

如今,再來看相關矩陣:

>>> corr_matrix = housing.corr()
>>> corr_matrix["median_house_value"].sort_values(ascending=False)
median_house_value          1.000000
median_income               0.687170
rooms_per_household         0.199343
total_rooms                 0.135231
housing_median_age          0.114220
households                  0.064702
total_bedrooms              0.047865
population_per_household   -0.021984
population                 -0.026699
longitude                  -0.047279
latitude                   -0.142826
bedrooms_per_room          -0.260070
Name: median_house_value, dtype: float64

看起來不錯!與總房間數或臥室數相比,新的bedrooms_per_room屬性與房價中位數的關聯更強。顯然,臥室數/總房間數的比例越低,房價就越高。每戶的房間數也比街區的總房間數的更有信息,很明顯,房屋越大,房價就越高。

這一步的數據探索沒必要很是完備,此處的目的是有一個正確的開始,快速發現規律,以獲得一個合理的原型。可是這是一個交互過程:一旦你獲得了一個原型,並運行起來,你就能夠分析它的輸出,進而發現更多的規律,而後再回到數據探索這步。

爲機器學習算法準備數據

如今來爲機器學習算法準備數據。不要手工來作,你須要寫一些函數,理由以下:

  • 函數可讓你在任何數據集上(好比,你下一次獲取的是一個新的數據集)方便地進行重複數據轉換。

  • 你能慢慢創建一個轉換函數庫,能夠在將來的項目中複用。

  • 在將數據傳給算法以前,你能夠在實時系統中使用這些函數。

  • 這可讓你方便地嘗試多種數據轉換,查看哪些轉換方法結合起來效果最好。

可是,仍是先回到乾淨的訓練集(經過再次複製strat_train_set),將預測量和標籤分開,由於咱們不想對預測量和目標值應用相同的轉換(注意drop()建立了一份數據的備份,而不影響strat_train_set):

housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()

數據清洗

大多機器學習算法不能處理缺失的特徵,所以先建立一些函數來處理特徵缺失的問題。前面,你應該注意到了屬性total_bedrooms有一些缺失值。有三個解決選項:

  • 去掉對應的街區;

  • 去掉整個屬性;

  • 進行賦值(0、平均值、中位數等等)。

DataFramedropna()drop(),和fillna()方法,能夠方便地實現:

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,你須要計算訓練集的中位數,用中位數填充訓練集的缺失值,不要忘記保存該中位數。後面用測試集評估系統時,須要替換測試集中的缺失值,也能夠用來實時替換新數據中的缺失值。

Scikit-Learn 提供了一個方便的類來處理缺失值:Imputer。下面是其使用方法:首先,須要建立一個Imputer實例,指定用某屬性的中位數來替換該屬性全部的缺失值:

from sklearn.preprocessing import Imputer

imputer = Imputer(strategy="median")

由於只有數值屬性才能算出中位數,咱們須要建立一份不包括文本屬性ocean_proximity的數據副本:

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

如今,就能夠用fit()方法將imputer實例擬合到訓練數據:

imputer.fit(housing_num)

imputer計算出了每一個屬性的中位數,並將結果保存在了實例變量statistics_中。雖然此時只有屬性total_bedrooms存在缺失值,但咱們不能肯定在之後的新的數據中會不會有其餘屬性也存在缺失值,因此安全的作法是將imputer應用到每一個數值:

>>> 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])

如今,你就可使用這個「訓練過的」imputer來對訓練集進行轉換,將缺失值替換爲中位數:

X = imputer.transform(housing_num)

結果是一個包含轉換後特徵的普通的 Numpy 數組。若是你想將其放回到 PandasDataFrame中,也很簡單:

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

Scikit-Learn 設計

Scikit-Learn 設計的 API 設計的很是好。它的主要設計原則是:

  • 一致性:全部對象的接口一致且簡單:

    • 估計器(estimator)。任何能夠基於數據集對一些參數進行估計的對象都被稱爲估計器(好比,imputer就是個估計器)。估計自己是經過fit()方法,只須要一個數據集做爲參數(對於監督學習算法,須要兩個數據集;第二個數據集包含標籤)。任何其它用來指導估計過程的參數都被當作超參數(好比imputerstrategy),而且超參數要被設置成實例變量(一般經過構造器參數設置)。
    • 轉換器(transformer)。一些估計器(好比imputer)也能夠轉換數據集,這些估計器被稱爲轉換器。API也是至關簡單:轉換是經過transform()方法,被轉換的數據集做爲參數。返回的是通過轉換的數據集。轉換過程依賴學習到的參數,好比imputer的例子。全部的轉換都有一個便捷的方法fit_transform(),等同於調用fit()transform()(但有時fit_transform()通過優化,運行的更快)。
    • 預測器(predictor)。最後,一些估計器能夠根據給出的數據集作預測,這些估計器稱爲預測器。例如,上一章的LinearRegression模型就是一個預測器:它根據一個國家的人均 GDP 預測生活滿意度。預測器有一個predict()方法,能夠用新實例的數據集作出相應的預測。預測器還有一個score()方法,能夠根據測試集(和相應的標籤,若是是監督學習算法的話)對預測進行衡器。
  • 可檢驗。全部估計器的超參數均可以經過實例的public變量直接訪問(好比,imputer.strategy),而且全部估計器學習到的參數也能夠經過在實例變量名後加下劃線來訪問(好比,imputer.statistics_)。

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

  • 可組合。儘量使用現存的模塊。例如,用任意的轉換器序列加上一個估計器,就能夠作成一個流水線,後面會看到例子。

  • 合理的默認值。Scikit-Learn 給大多數參數提供了合理的默認值,很容易就能建立一個系統。

處理文本和類別屬性

前面,咱們丟棄了類別屬性ocean_proximity,由於它是一個文本屬性,不能計算出中位數。大多數機器學習算法跟喜歡和數字打交道,因此讓咱們把這些文本標籤轉換爲數字。

Scikit-Learn 爲這個任務提供了一個轉換器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])

譯註:

在原書中使用LabelEncoder轉換器來轉換文本特徵列的方式是錯誤的,該轉換器只能用來轉換標籤(正如其名)。在這裏使用LabelEncoder沒有出錯的緣由是該數據只有一列文本特徵值,在有多個文本特徵列的時候就會出錯。應使用factorize()方法來進行操做:

housing_cat_encoded, housing_categories = housing_cat.factorize()
housing_cat_encoded[:10]

好了一些,如今就能夠在任何 ML 算法裏用這個數值數據了。你能夠查看映射表,編碼器是經過屬性classes_來學習的(<1H OCEAN被映射爲 0,INLAND被映射爲 1,等等):

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

這種作法的問題是,ML 算法會認爲兩個臨近的值比兩個疏遠的值要更類似。顯然這樣不對(好比,分類 0 和 4 比 0 和 1 更類似)。要解決這個問題,一個常見的方法是給每一個分類建立一個二元屬性:當分類是<1H OCEAN,該屬性爲 1(不然爲 0),當分類是INLAND,另外一個屬性等於 1(不然爲 0),以此類推。這稱做獨熱編碼(One-Hot Encoding),由於只有一個屬性會等於 1(熱),其他會是 0(冷)。

Scikit-Learn 提供了一個編碼器OneHotEncoder,用於將整數分類值轉變爲獨熱向量。注意fit_transform()用於 2D 數組,而housing_cat_encoded是一個 1D 數組,因此須要將其變形:

>>> 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>

注意輸出結果是一個 SciPy 稀疏矩陣,而不是 NumPy 數組。當類別屬性有數千個分類時,這樣很是有用。通過獨熱編碼,咱們獲得了一個有數千列的矩陣,這個矩陣每行只有一個 1,其他都是 0。使用大量內存來存儲這些 0 很是浪費,因此稀疏矩陣只存儲非零元素的位置。你能夠像一個 2D 數據那樣進行使用,可是若是你真的想將其轉變成一個(密集的)NumPy 數組,只需調用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,咱們能夠用一步執行這兩個轉換(從文本分類到整數分類,再從整數分類到獨熱向量):

>>> 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]])

注意默認返回的結果是一個密集 NumPy 數組。向構造器LabelBinarizer傳遞sparse_output=True,就能夠獲得一個稀疏矩陣。

譯註:

在原書中使用LabelBinarizer的方式也是錯誤的,該類也應用於標籤列的轉換。正確作法是使用sklearn即將提供的CategoricalEncoder類。若是在你閱讀此文時sklearn中還沒有提供此類,用以下方式代替:(來自Pull Request #9151)

# Definition of the CategoricalEncoder class, copied from PR #9151.
# Just run this cell, or copy it to your code, do not try to understand it (yet).

from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.utils import check_array
from sklearn.preprocessing import LabelEncoder
from scipy import sparse

class CategoricalEncoder(BaseEstimator, TransformerMixin):
    """Encode categorical features as a numeric array.
    The input to this transformer should be a matrix of integers or strings,
    denoting the values taken on by categorical (discrete) features.
    The features can be encoded using a one-hot aka one-of-K scheme
    (``encoding='onehot'``, the default) or converted to ordinal integers
    (``encoding='ordinal'``).
    This encoding is needed for feeding categorical data to many scikit-learn
    estimators, notably linear models and SVMs with the standard kernels.
    Read more in the :ref:`User Guide <preprocessing_categorical_features>`.
    Parameters
    ----------
    encoding : str, 'onehot', 'onehot-dense' or 'ordinal'
        The type of encoding to use (default is 'onehot'):
        - 'onehot': encode the features using a one-hot aka one-of-K scheme
          (or also called 'dummy' encoding). This creates a binary column for
          each category and returns a sparse matrix.
        - 'onehot-dense': the same as 'onehot' but returns a dense array
          instead of a sparse matrix.
        - 'ordinal': encode the features as ordinal integers. This results in
          a single column of integers (0 to n_categories - 1) per feature.
    categories : 'auto' or a list of lists/arrays of values.
        Categories (unique values) per feature:
        - 'auto' : Determine categories automatically from the training data.
        - list : ``categories[i]`` holds the categories expected in the ith
          column. The passed categories are sorted before encoding the data
          (used categories can be found in the ``categories_`` attribute).
    dtype : number type, default np.float64
        Desired dtype of output.
    handle_unknown : 'error' (default) or 'ignore'
        Whether to raise an error or ignore if a unknown categorical feature is
        present during transform (default is to raise). When this is parameter
        is set to 'ignore' and an unknown category is encountered during
        transform, the resulting one-hot encoded columns for this feature
        will be all zeros.
        Ignoring unknown categories is not supported for
        ``encoding='ordinal'``.
    Attributes
    ----------
    categories_ : list of arrays
        The categories of each feature determined during fitting. When
        categories were specified manually, this holds the sorted categories
        (in order corresponding with output of `transform`).
    Examples
    --------
    Given a dataset with three features and two samples, we let the encoder
    find the maximum value per feature and transform the data to a binary
    one-hot encoding.
    >>> from sklearn.preprocessing import CategoricalEncoder
    >>> enc = CategoricalEncoder(handle_unknown='ignore')
    >>> enc.fit([[0, 0, 3], [1, 1, 0], [0, 2, 1], [1, 0, 2]])
    ... # doctest: +ELLIPSIS
    CategoricalEncoder(categories='auto', dtype=<... 'numpy.float64'>,
              encoding='onehot', handle_unknown='ignore')
    >>> enc.transform([[0, 1, 1], [1, 0, 4]]).toarray()
    array([[ 1.,  0.,  0.,  1.,  0.,  0.,  1.,  0.,  0.],
           [ 0.,  1.,  1.,  0.,  0.,  0.,  0.,  0.,  0.]])
    See also
    --------
    sklearn.preprocessing.OneHotEncoder : performs a one-hot encoding of
      integer ordinal features. The ``OneHotEncoder assumes`` that input
      features take on values in the range ``[0, max(feature)]`` instead of
      using the unique values.
    sklearn.feature_extraction.DictVectorizer : performs a one-hot encoding of
      dictionary items (also handles string-valued features).
    sklearn.feature_extraction.FeatureHasher : performs an approximate one-hot
      encoding of dictionary items or strings.
    """

    def __init__(self, encoding='onehot', categories='auto', dtype=np.float64,
                 handle_unknown='error'):
        self.encoding = encoding
        self.categories = categories
        self.dtype = dtype
        self.handle_unknown = handle_unknown

    def fit(self, X, y=None):
        """Fit the CategoricalEncoder to X.
        Parameters
        ----------
        X : array-like, shape [n_samples, n_feature]
            The data to determine the categories of each feature.
        Returns
        -------
        self
        """

        if self.encoding not in ['onehot', 'onehot-dense', 'ordinal']:
            template = ("encoding should be either 'onehot', 'onehot-dense' "
                        "or 'ordinal', got %s")
            raise ValueError(template % self.handle_unknown)

        if self.handle_unknown not in ['error', 'ignore']:
            template = ("handle_unknown should be either 'error' or "
                        "'ignore', got %s")
            raise ValueError(template % self.handle_unknown)

        if self.encoding == 'ordinal' and self.handle_unknown == 'ignore':
            raise ValueError("handle_unknown='ignore' is not supported for"
                             " encoding='ordinal'")

        X = check_array(X, dtype=np.object, accept_sparse='csc', copy=True)
        n_samples, n_features = X.shape

        self._label_encoders_ = [LabelEncoder() for _ in range(n_features)]

        for i in range(n_features):
            le = self._label_encoders_[i]
            Xi = X[:, i]
            if self.categories == 'auto':
                le.fit(Xi)
            else:
                valid_mask = np.in1d(Xi, self.categories[i])
                if not np.all(valid_mask):
                    if self.handle_unknown == 'error':
                        diff = np.unique(Xi[~valid_mask])
                        msg = ("Found unknown categories {0} in column {1}"
                               " during fit".format(diff, i))
                        raise ValueError(msg)
                le.classes_ = np.array(np.sort(self.categories[i]))

        self.categories_ = [le.classes_ for le in self._label_encoders_]

        return self

    def transform(self, X):
        """Transform X using one-hot encoding.
        Parameters
        ----------
        X : array-like, shape [n_samples, n_features]
            The data to encode.
        Returns
        -------
        X_out : sparse matrix or a 2-d array
            Transformed input.
        """
        X = check_array(X, accept_sparse='csc', dtype=np.object, copy=True)
        n_samples, n_features = X.shape
        X_int = np.zeros_like(X, dtype=np.int)
        X_mask = np.ones_like(X, dtype=np.bool)

        for i in range(n_features):
            valid_mask = np.in1d(X[:, i], self.categories_[i])

            if not np.all(valid_mask):
                if self.handle_unknown == 'error':
                    diff = np.unique(X[~valid_mask, i])
                    msg = ("Found unknown categories {0} in column {1}"
                           " during transform".format(diff, i))
                    raise ValueError(msg)
                else:
                    # Set the problematic rows to an acceptable value and
                    # continue `The rows are marked `X_mask` and will be
                    # removed later.
                    X_mask[:, i] = valid_mask
                    X[:, i][~valid_mask] = self.categories_[i][0]
            X_int[:, i] = self._label_encoders_[i].transform(X[:, i])

        if self.encoding == 'ordinal':
            return X_int.astype(self.dtype, copy=False)

        mask = X_mask.ravel()
        n_values = [cats.shape[0] for cats in self.categories_]
        n_values = np.array([0] + n_values)
        indices = np.cumsum(n_values)

        column_indices = (X_int + indices[:-1]).ravel()[mask]
        row_indices = np.repeat(np.arange(n_samples, dtype=np.int32),
                                n_features)[mask]
        data = np.ones(n_samples * n_features)[mask]

        out = sparse.csc_matrix((data, (row_indices, column_indices)),
                                shape=(n_samples, indices[-1]),
                                dtype=self.dtype).tocsr()
        if self.encoding == 'onehot-dense':
            return out.toarray()
        else:
            return out

轉換方法:

#from sklearn.preprocessing import CategoricalEncoder # in future versions of Scikit-Learn

cat_encoder = CategoricalEncoder()
housing_cat_reshaped = housing_cat.values.reshape(-1, 1)
housing_cat_1hot = cat_encoder.fit_transform(housing_cat_reshaped)
housing_cat_1hot

自定義轉換器

儘管 Scikit-Learn 提供了許多有用的轉換器,你仍是須要本身動手寫轉換器執行任務,好比自定義的清理操做,或屬性組合。你須要讓自制的轉換器與 Scikit-Learn 組件(好比流水線)無縫銜接工做,由於 Scikit-Learn 是依賴鴨子類型的(而不是繼承),你所須要作的是建立一個類並執行三個方法:fit()(返回self),transform(),和fit_transform()。經過添加TransformerMixin做爲基類,能夠很容易地獲得最後一個。另外,若是你添加BaseEstimator做爲基類(且構造器中避免使用*args**kargs),你就能獲得兩個額外的方法(get_params()set_params()),兩者能夠方便地進行超參數自動微調。例如,一個小轉換器類添加了上面討論的屬性:

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)

在這個例子中,轉換器有一個超參數add_bedrooms_per_room,默認設爲True(提供一個合理的默認值頗有幫助)。這個超參數可讓你方便地發現添加了這個屬性是否對機器學習算法有幫助。更通常地,你能夠爲每一個不能徹底確保的數據準備步驟添加一個超參數。數據準備步驟越自動化,能夠自動化的操做組合就越多,越容易發現更好用的組合(並能節省大量時間)。

特徵縮放

數據要作的最重要的轉換之一是特徵縮放。除了個別狀況,當輸入的數值屬性量度不一樣時,機器學習算法的性能都不會好。這個規律也適用於房產數據:總房間數分佈範圍是 6 到 39320,而收入中位數只分布在 0 到 15。注意一般狀況下咱們不須要對目標值進行縮放。

有兩種常見的方法可讓全部的屬性有相同的量度:線性函數歸一化(Min-Max scaling)和標準化(standardization)。

線性函數歸一化(許多人稱其爲歸一化(normalization))很簡單:值被轉變、從新縮放,直到範圍變成 0 到 1。咱們經過減去最小值,而後再除以最大值與最小值的差值,來進行歸一化。Scikit-Learn 提供了一個轉換器MinMaxScaler來實現這個功能。它有一個超參數feature_range,可讓你改變範圍,若是不但願範圍是 0 到 1。

標準化就很不一樣:首先減去平均值(因此標準化值的平均值老是 0),而後除以方差,使獲得的分佈具備單位方差。與歸一化不一樣,標準化不會限定值到某個特定的範圍,這對某些算法可能構成問題(好比,神經網絡常須要輸入值得範圍是 0 到 1)。可是,標準化受到異常值的影響很小。例如,假設一個街區的收入中位數因爲某種錯誤變成了100,歸一化會將其它範圍是 0 到 15 的值變爲 0-0.15,可是標準化不會受什麼影響。Scikit-Learn 提供了一個轉換器StandardScaler來進行標準化。

警告:與全部的轉換同樣,縮放器只能向訓練集擬合,而不是向完整的數據集(包括測試集)。只有這樣,你才能用縮放器轉換訓練集和測試集(和新數據)。

轉換流水線

你已經看到,存在許多數據轉換步驟,須要按必定的順序執行。幸運的是,Scikit-Learn 提供了類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構造器須要一個定義步驟順序的名字/估計器對的列表。除了最後一個估計器,其他都要是轉換器(即,它們都要有fit_transform()方法)。名字能夠隨意起。

當你調用流水線的fit()方法,就會對全部轉換器順序調用fit_transform()方法,將每次調用的輸出做爲參數傳遞給下一個調用,一直到最後一個估計器,它只執行fit()方法。

流水線暴露相同的方法做爲最終的估計器。在這個例子中,最後的估計器是一個StandardScaler,它是一個轉換器,所以這個流水線有一個transform()方法,能夠順序對數據作全部轉換(它還有一個fit_transform方法可使用,就沒必要先調用fit()再進行transform())。

你如今就有了一個對數值的流水線,你還須要對分類值應用LabelBinarizer:如何將這些轉換寫成一個流水線呢?Scikit-Learn 提供了一個類FeatureUnion實現這個功能。你給它一列轉換器(能夠是全部的轉換器),當調用它的transform()方法,每一個轉換器的transform()會被並行執行,等待輸出,而後將輸出合併起來,並返回結果(固然,調用它的fit()方法就會調用每一個轉換器的fit())。一個完整的處理數值和類別屬性的流水線以下所示:

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),
    ])

譯註:

若是你在上面代碼中的cat_pipeline流水線使用LabelBinarizer轉換器會致使執行錯誤,解決方案是用上文提到的CategoricalEncoder轉換器來代替:

cat_pipeline = Pipeline([
        ('selector', DataFrameSelector(cat_attribs)),
        ('cat_encoder', CategoricalEncoder(encoding="onehot-dense")),
    ])

你能夠很簡單地運行整個流水線:

>>> 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)

每一個子流水線都以一個選擇轉換器開始:經過選擇對應的屬性(數值或分類)、丟棄其它的,來轉換數據,並將輸出DataFrame轉變成一個 NumPy 數組。Scikit-Learn 沒有工具來處理 PandasDataFrame,所以咱們須要寫一個簡單的自定義轉換器來作這項工做:

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

選擇並訓練模型

可到這一步了!你在前面限定了問題、得到了數據、探索了數據、採樣了一個測試集、寫了自動化的轉換流水線來清理和爲算法準備數據。如今,你已經準備好選擇並訓練一個機器學習模型了。

在訓練集上訓練和評估

好消息是基於前面的工做,接下來要作的比你想的要簡單許多。像前一章那樣,咱們先來訓練一個線性迴歸模型:

from sklearn.linear_model import LinearRegression

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

完畢!你如今就有了一個可用的線性迴歸模型。用一些訓練集中的實例作下驗證:

>>> 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]

行的通,儘管預測並不怎麼準確(好比,第二個預測偏離了 50%!)。讓咱們使用 Scikit-Learn 的mean_squared_error函數,用所有訓練集來計算下這個迴歸模型的 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

OK,有總比沒有強,但顯然結果並很差:大多數街區的median_housing_values位於 120000 到 265000 美圓之間,所以預測偏差 68628 美圓不能讓人滿意。這是一個模型欠擬合訓練數據的例子。當這種狀況發生時,意味着特徵沒有提供足夠多的信息來作出一個好的預測,或者模型並不強大。就像前一章看到的,修復欠擬合的主要方法是選擇一個更強大的模型,給訓練算法提供更好的特徵,或去掉模型上的限制。這個模型尚未正則化,因此排除了最後一個選項。你能夠嘗試添加更多特徵(好比,人口的對數值),可是首先讓咱們嘗試一個更爲複雜的模型,看看效果。

來訓練一個DecisionTreeRegressor。這是一個強大的模型,能夠發現數據中複雜的非線性關係(決策樹會在第 6 章詳細講解)。代碼看起來很熟悉:

from sklearn.tree import DecisionTreeRegressor

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

如今模型就訓練好了,用訓練集評估下:

>>> 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

等一下,發生了什麼?沒有偏差?這個模型多是絕對完美的嗎?固然,更大可能性是這個模型嚴重過擬合數據。如何肯定呢?如前所述,直到你準備運行一個具有足夠信心的模型,都不要碰測試集,所以你須要使用訓練集的部分數據來作訓練,用一部分來作模型驗證。

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

評估決策樹模型的一種方法是用函數train_test_split來分割訓練集,獲得一個更小的訓練集和一個驗證集,而後用更小的訓練集來訓練模型,用驗證集來評估。這須要必定工做量,並不難並且也可行。

另外一種更好的方法是使用 Scikit-Learn 的交叉驗證功能。下面的代碼採用了 K 折交叉驗證(K-fold cross-validation):它隨機地將訓練集分紅十個不一樣的子集,成爲「折」,而後訓練評估決策樹模型 10 次,每次選一個不用的折來作評估,用其它 9 個來作訓練。結果是一個包含 10 個評分的數組:

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 交叉驗證功能指望的是效用函數(越大越好)而不是損失函數(越低越好),所以得分函數實際上與 MSE 相反(即負值),這就是爲何前面的代碼在計算平方根以前先計算-scores

來看下結果:

>>> 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

如今決策樹就不像前面看起來那麼好了。實際上,它看起來比線性迴歸模型還糟!注意到交叉驗證不只可讓你獲得模型性能的評估,還能測量評估的準確性(即,它的標準差)。決策樹的評分大約是 71200,一般波動有 ±3200。若是隻有一個驗證集,就得不到這些信息。可是交叉驗證的代價是訓練了模型屢次,不可能老是這樣。

讓咱們計算下線性迴歸模型的的相同分數,以作確保:

>>> 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

判斷沒錯:決策樹模型過擬合很嚴重,它的性能比線性迴歸模型還差。

如今再嘗試最後一個模型:RandomForestRegressor。第7章咱們會看到,隨機森林是經過用特徵的隨機子集訓練許多決策樹。在其它多個模型之上創建模型稱爲集成學習(Ensemble Learning),它是推動 ML 算法的一種好方法。咱們會跳過大部分的代碼,由於代碼本質上和其它模型同樣:

>>> 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

如今好多了:隨機森林看起來頗有但願。可是,訓練集的評分仍然比驗證集的評分低不少。解決過擬合能夠經過簡化模型,給模型加限制(即,規整化),或用更多的訓練數據。在深刻隨機森林以前,你應該嘗試下機器學習算法的其它類型模型(不一樣核心的支持向量機,神經網絡,等等),不要在調節超參數上花費太多時間。目標是列出一個可能模型的列表(兩到五個)。

提示:你要保存每一個試驗過的模型,以便後續能夠再用。要確保有超參數和訓練參數,以及交叉驗證評分,和實際的預測值。這可讓你比較不一樣類型模型的評分,還能夠比較偏差種類。你能夠用 Python 的模塊pickle,很是方便地保存 Scikit-Learn 模型,或使用sklearn.externals.joblib,後者序列化大 NumPy 數組更有效率:

from sklearn.externals import joblib

joblib.dump(my_model, "my_model.pkl")
# 而後
my_model_loaded = joblib.load("my_model.pkl")

模型微調

假設你如今有了一個列表,列表裏有幾個有但願的模型。你如今須要對它們進行微調。讓咱們來看幾種微調的方法。

網格搜索

微調的一種方法是手工調整超參數,直到找到一個好的超參數組合。這麼作的話會很是冗長,你也可能沒有時間探索多種組合。

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

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)

當你不能肯定超參數該有什麼值,一個簡單的方法是嘗試連續的 10 的冪(若是想要一個粒度更小的搜尋,能夠用更小的數,就像在這個例子中對超參數n_estimators作的)。

param_grid告訴 Scikit-Learn 首先評估全部的列在第一個dict中的n_estimatorsmax_features3 × 4 = 12種組合(不用擔憂這些超參數的含義,會在第 7 章中解釋)。而後嘗試第二個dict中超參數的2 × 3 = 6種組合,此次會將超參數bootstrap設爲False而不是True(後者是該超參數的默認值)。

總之,網格搜索會探索12 + 6 = 18RandomForestRegressor的超參數組合,會訓練每一個模型五次(由於用的是五折交叉驗證)。換句話說,訓練總共有18 × 5 = 90輪!K 折將要花費大量時間,完成後,你就能得到參數的最佳組合,以下所示:

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

提示:由於 30 是n_estimators的最大值,你也應該估計更高的值,由於評估的分數可能會隨n_estimators的增大而持續提高。

你還能直接獲得最佳的估計器:

>>> 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是以(默認值)refit=True開始運行的,則一旦用交叉驗證找到了最佳的估計器,就會在整個訓練集上從新訓練。這是一個好方法,由於用更多數據訓練會提升性能。

固然,也能夠獲得評估得分:

>>> 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}

在這個例子中,咱們經過設定超參數max_features爲 6,n_estimators爲 30,獲得了最佳方案。對這個組合,RMSE 的值是 49959,這比以前使用默認的超參數的值(52634)要稍微好一些。祝賀你,你成功地微調了最佳模型!

提示:不要忘記,你能夠像超參數同樣處理數據準備的步驟。例如,網格搜索能夠自動判斷是否添加一個你不肯定的特徵(好比,使用轉換器CombinedAttributesAdder的超參數add_bedrooms_per_room)。它還能用類似的方法來自動找處處理異常值、缺失特徵、特徵選擇等任務的最佳方法。

隨機搜索

當探索相對較少的組合時,就像前面的例子,網格搜索還能夠。可是當超參數的搜索空間很大時,最好使用RandomizedSearchCV。這個類的使用方法和類GridSearchCV很類似,但它不是嘗試全部可能的組合,而是經過選擇每一個超參數的一個隨機值的特定數量的隨機組合。這個方法有兩個優勢:

  • 若是你讓隨機搜索運行,好比 1000 次,它會探索每一個超參數的 1000 個不一樣的值(而不是像網格搜索那樣,只搜索每一個超參數的幾個值)。

  • 你能夠方便地經過設定搜索次數,控制超參數搜索的計算量。

集成方法

另外一種微調系統的方法是將表現最好的模型組合起來。組合(集成)以後的性能一般要比單獨的模型要好(就像隨機森林要比單獨的決策樹要好),特別是當單獨模型的偏差類型不一樣時。咱們會在第7章更深刻地講解這點。

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

經過分析最佳模型,經常能夠得到對問題更深的瞭解。好比,RandomForestRegressor能夠指出每一個屬性對於作出準確預測的相對重要性:

>>> 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])

將重要性分數和屬性名放到一塊兒:

>>> 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的類型(INLAND)就夠了,因此能夠丟棄掉其它的)。

你還應該看一下系統犯的偏差,搞清爲何會有些偏差,以及如何改正問題(添加更多的特徵,或相反,去掉沒有什麼信息的特徵,清洗異常值等等)。

用測試集評估系統

調節完系統以後,你終於有了一個性能足夠好的系統。如今就能夠用測試集評估最後的模型了。這個過程沒有什麼特殊的:從測試集獲得預測值和標籤,運行full_pipeline轉換數據(調用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

評估結果一般要比交叉驗證的效果差一點,若是你以前作過不少超參數微調(由於你的系統在驗證集上微調,獲得了不錯的性能,一般不會在未知的數據集上有一樣好的效果)。這個例子不屬於這種狀況,可是當發生這種狀況時,你必定要忍住不要調節超參數,使測試集的效果變好;這樣的提高不能推廣到新數據上。

而後就是項目的預上線階段:你須要展現你的方案(重點說明學到了什麼、作了什麼、沒作什麼、作過什麼假設、系統的限制是什麼,等等),記錄下全部事情,用漂亮的圖表和容易記住的表達(好比,「收入中位數是房價最重要的預測量」)作一次精彩的展現。

啓動、監控、維護系統

很好,你被容許啓動系統了!你須要爲實際生產作好準備,特別是接入輸入數據源,並編寫測試。

你還須要編寫監控代碼,以固定間隔檢測系統的實時表現,當發生降低時觸發報警。這對於捕獲忽然的系統崩潰和性能降低十分重要。作監控很常見,是由於模型會隨着數據的演化而性能降低,除非模型用新數據按期訓練。

評估系統的表現須要對預測值採樣並進行評估。這一般須要人來分析。分析者多是領域專家,或者是衆包平臺(好比 Amazon Mechanical Turk 或 CrowdFlower)的工人。無論採用哪一種方法,你都須要將人工評估的流水線植入系統。

你還要評估系統輸入數據的質量。有時由於低質量的信號(好比失靈的傳感器發送隨機值,或另外一個團隊的輸出停滯),系統的表現會逐漸變差,但可能須要一段時間,系統的表現才能降低到必定程度,觸發警報。若是監測了系統的輸入,你就可能儘可能早的發現問題。對於線上學習系統,監測輸入數據是很是重要的。

最後,你可能想按期用新數據訓練模型。你應該儘量自動化這個過程。若是不這麼作,很是有可能你須要每隔至少六個月更新模型,系統的表現就會產生嚴重波動。若是你的系統是一個線上學習系統,你須要按期保存系統狀態快照,好能方便地回滾到以前的工做狀態。

實踐!

但願這一章能告訴你機器學習項目是什麼樣的,你能用學到的工具訓練一個好系統。你已經看到,大部分的工做是數據準備步驟、搭建監測工具、創建人爲評估的流水線和自動化按期模型訓練,固然,最好能瞭解整個過程、熟悉三或四種算法,而不是在探索高級算法上浪費所有時間,致使在全局上的時間不夠。

所以,若是你還沒這樣作,如今最好拿起臺電腦,選擇一個感興趣的數據集,將整個流程從頭至尾完成一遍。一個不錯的着手開始的地點是競賽網站,好比 http://kaggle.com/:你會獲得一個數據集,一個目標,以及分享經驗的人。

相關文章
相關標籤/搜索