點擊上方「AI有道」,選擇「星標公眾號」
重磅乾貨,第一時間送達!
本系列為《Scikit-Learn 和 TensorFlow 機器學習指南》的第三講。前兩講在文章底部的推薦閱讀裡可以查看。至於我為什麼推薦這本書,不用過多解釋了,總之書籍質量很高。紅色石頭會堅持提煉該書的翻譯與精煉筆記。並將每部分單獨整理成獨立的一篇文章,篇幅適宜,便於大家在公眾號查看。想看完整項目的請查閱我的 GitHub:
https://github.com/RedstoneWill/Hands-On-Machine-Learning-with-Sklearn-TensorFlow
我們將開始完整地介紹一個端對端(End-to-End)機器學習項目。假如你是某個房地產公司剛僱傭的數據科學家,你所要做的事情主要分成以下幾個步驟:
1.整體規劃。
2.獲取數據。
3.發現、可視化數據,增加直觀印象。
4.為機器學習準備數據。
5.選擇模型並進行訓練。
6.調試模型。
7.給出解決方案。
8.部署、監控、維護系統
本文將介紹前三個部分,教你如何入手第一個機器學習項目!
1. 使用真實數據學習機器學習時,最好使用真實數據,而不是「人造」數據。幸運的是,有許多開源的數據集可以免費使用,涉及許多行業領域。下面列舉一些:
知名的開源數據倉庫:
— http://archive.ics.uci.edu/ml/
— https://www.kaggle.com/datasets
— http://aws.amazon.com/fr/datasets/
綜合門戶網站:
— http://dataportals.org/
— http://opendatamonitor.eu/
— http://quandl.com/
其它:
— https://goo.gl/SJHN2k
— http://goo.gl/zDR78y
— https://www.reddit.com/r/datasets
這一章我們將使用來自 StatLib 倉庫的 California 房屋價格數據集(如下圖所示)。這份數據集來自 1990 年的普查統計。這份數據集雖然年代有點久了,但不妨礙我們使用。我們已經對該數據集進行了一些處理,便於學習。
2. 整體規劃歡迎來到機器學習房地產公司!你的第一個任務就是根據 California 普查數據來建立一個房價預測模型。這份普查數據包含了 California 每個地區的人口、收入中位數、房價中位數等信息,每個地區人口大約 600 到 3,000 人。
你的模型應該對這些數據進行學習,然後根據提供的其它信息,預測任意地區的房價中位數。
2.1 劃定問題
首先第一個問題就是問你的老闆商業目標是什麼,構建一個模型可能不是最終的目標。公司期望如何使用這個模型並從中獲利?這很重要,因為它決定了你如何劃定問題,選擇什麼算法,使用什麼性能測量方式來評估模型,以及在調試模型上花費多大的力氣。
你的老闆回答說你的模型輸出(預測地區房價中位數)將連同許多其它信號傳輸到另外一個機器學習系統(如下圖所示)。這個下遊系統將決定是否對該地區投資房地產。得到正確的預測非常重要,因為它直接影響到收益。
管道(pipeline):
數據處理組件的序列叫做數據管道(pipeline)。管道在機器學習系統中很常見,因為有許多數據要處理和轉換。
管道的各個組件是異步進行的。每個組件都會輸入大量數據並處理,然後將結果傳輸給管道的下一個組件,下一個組件繼續處理並輸出結果,依次進行。每個組件相對獨立,組件之間的接口就是簡單的數據存儲。這讓系統更加簡單且容易掌控(藉助數據流程圖),不同的團隊可以專注於各自的組件。而且,即便是某個組件崩潰了,下遊組件仍然能使用之前上遊輸出的數據進行正常工作(至少在一段時間內)。這讓整個系統更加健壯。
然而從另一方面來說,如果不能及時發現崩潰的組件,下遊組件輸入數據得不到及時更新,整個系統的性能也會下降。
下一個問題就是詢問當前是如何預測房價的,作為你的模型的性能參考。你的老闆回答說當前房價是由專家們進行人工預測的,方法是收集各個地區大量最新信息(除了房價),然後使用複雜的規則進行估計。這種做法成本高、費時間,而且正確率也不高,錯誤率達到了 15%。
好了,設計系統需要的所有信息已經準備好了。首先,你需要劃定問題:這是監督式,非監督式,還是增強學習?這是分類任務,回歸任務,還是其它任務?應該使用批量學習還是在線學習技術?在真正開始之前請先回答這些問題。
回答出來了嗎?我們一起來看一下:這是一個典型的監督式學習任務,因為訓練樣本的標籤是已知的(每個實例都有它的期望輸出,例如各地區的房價中位數)。這也是典型的回歸問題,因為我們的目標是預測房價。這也是多元回歸問題,因為系統將使用多個特徵進行預測(例如地區人課、收入中位數等)。在第一章預測居民幸福指數時,只有一個特徵,人均 GDP,是一個單變量回歸問題。最後,因為沒有連續的數據流輸入到系統,數據更新不是很頻繁,而且數據量較小,所佔內存不大,因此採用批量學習即可。
如果數據量很大,可以把整個數據集劃分到不同的伺服器上進行訓練(使用 MapReduce 技術,後面將會講到),或者你也可以使用在線學習技術。
2.2 性能指標下一步就要選擇評估模型的性能指標。回歸問題典型的性能指標是均方根誤差(Root Mean Square Error, RMSE),即測量系統預測誤差的標準差。例如,RMSE = 50,000 意味著有大約 68% 的預測值與真實值誤差在 $50,000 之內,大約有 95% 的預測值與真實值誤差在 $100,000 之內。計算 RMSE 的公式如下:
符號:
這個公式引入了一些常見的機器學習符號:
除了 RMSE 之外,還有其它性能指標。例如出現某些離群點,這種情況下可以使用平均絕對誤差(Mean Absolute Error, MAE)作為性能指標。公式如下:
2.3 檢查假設最後,最好列出目前為止做得所有假設並驗證,這能幫助你儘早發現問題。例如你預測房價,然後傳輸到下遊機器學習系統。但是,下遊機器學習系統實際上把你預測得價格轉換成了不同類別(例如便宜、中等、昂貴),使用這些類別代替實際預測值。這種情況下,準確預測房價並不是特別重要了!你只需要對房價進行類別劃分即可。這樣的話,這就是一個分類問題而不是回歸問題。這是需要提前弄清楚的,你可不想建立回歸模型之後才發現事實。
幸運的是,在與下遊機器學習系統溝通之後,確認這確實是一個回歸問題。好了,接下來就開始真正地編寫程序了。
3. 獲取數據完整的代碼在 GitHub 上獲取,地址是:
https://github.com/ageron/handson-ml
代碼形式是 Jupyter Notebook。
3.1 創建工作環境首先你需要安裝 Python,獲取地址:
https://www.python.org/
接下來需要創建一個工作空間目錄,在終端輸入以下命令(在提示符 $ 之後):
$ export ML_PATH="$HOME/ml"
$ mkdir -p $ML_PATH
你還需要安裝一些 Python 模塊:Jupyter、Numpy、Pandas、Matplotlib 和 Scikit-Learn。如果你已經都安裝好了,請直接跳過本節內容。如果沒有,你可以使用多種方式來安裝這些模塊(包括它們的依賴)。你可以使用系統自帶的包管理系統(例如 Ubuntu 上的 apt-get,或 macOS 上的 MacPorts、HomeBrew);也可以安裝 Python 的科學計算環境 Anaconda,使用 Anaconda 的包管理系統;或者直接使用 Python 自帶的包管理系統 pip(自 Python 2.7.9 開始自帶的)。你可以在終端輸入以下命令來檢查 pip 是否安裝:
pip 9.0.1 from […]/lib/python3.5/site-packages (python 3.5)
你應該安裝 pip 的最新版本,至少是 1.4 版本以上的,以支持二進位模塊的安裝(也稱為 wheels)。更新 pip 到最新版本的命令是:
創建獨立環境:
如果你想創建一個獨立的工作環境(強烈推薦!這樣可以使不同項目之間不會出現庫的衝突),輸入以下 pip 命令來安裝 virtualenv:
現在你可以創建一個獨立的 Python 環境了:
$ cd $ML_PATH
$ virtualenv env
每次你想激活這個獨立環境,只需打開一個終端輸入以下命令:
$ cd $ML_PATH
$ source env/bin/activate
補充一下,如果代碼寫完,想關閉當前環境,輸入以下命令:
一旦環境激活之後,你使用 pip 安裝的所有包都僅限於該獨立環境中,Python 也只會訪問這些包(如果你想訪問系統其它包,可以在創建環境的時候使用 virtualenv 的 –system-site-packages 選項)。查看 virtualenv 的文檔獲取更多信息。
現在,你可以使用簡單的 pip 命令來安裝所有需要的模塊和它們的依賴了:
為了檢查是否安裝成功,可以使用以下命令導入所有模塊:
$ python3 -c "import jupyter, matplotlib, numpy, pandas, scipy, sklearn"
沒有錯誤的話,就可以輸入以下命令打開 Jupyter Notebook 啦!
然後,一個 Jupyter 伺服器就運行在你的終端了,監聽埠 8888。你可以在瀏覽器中輸入地址:http://localhost:8888/ 來訪問伺服器(通常在伺服器啟動時就自動打開了)。顯示的目錄即為你創建的當前環境。
現在可以創建 Python notebook 了。點擊右上角 「New」,選擇 「Python 3」 即可(如下圖所示)。
這個過程實際上做了三件事:1. 在當前工作空間裡創建一個新的 notebook 未命名文件:Untitled.ipynb;2. 啟動 Jupyter Python 核來運行這個 notebook;3. 在新欄中打開這個 notebook。你應該把這個 notebook 重命名為 Housing.ipynb。
Notebook 包含一個單元格列表。每個單元格可以放入可執行代碼或者格式化文檔。現在,notebook 只有一個空的代碼單元格,名為 「In [1]」。在該單元格中輸入:print(「Hello world!」),點擊運行按鈕(如下圖所示)或按鍵 Shift+Enter,就會把當前單元格內容發給 notebook 的 Python 內核中,運行並返回輸出結果。結果顯示在單元格下面,且會在底部建立一個新的單元格。可以點擊菜單欄 Help 中的 User Interface Tour,學習更多 jupyter 的基本知識。
3.2 下載數據本項目需要下載的數據集是壓縮文件 housing.tgz,解壓後是 housing.csv 文件,包含所有數據。
你可以在瀏覽器上載數據集,然後使用命令 tar xzf housing.tgz 解壓文件,提取出 housing.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()
直接運行函數:
將會在你的工作空間新建目錄 datasets/housing/。程序會自動下載 housing.tgz 文件並解壓出 housing.csv 文件到 datasets/housing/ 目錄下。
下面定義數據導入函數:
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 對象。
3.3 快速查看數據結構先來看一下數據集的結構,運行以下語句,查看前 5 行:
housing = load_housing_data()
housing.head()
顯示結果如下:
該數據集中每一行代表一個地區,每個地區包含 10 格特徵屬性,分別是:
ongitude
latitude
housing_median_age
total_rooms
total_bed
rooms
population
households
median_income
median_house_value
ocean_proximity
使用 info() 方法來查看數據的整體描述,尤其是包含的行數,每個屬性的類型和非空值的數量。
>>> housing.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 20640 entries, 0 to 20639
Data columns (total 10 columns):
longitude 20640 non-null float64
latitude 20640 non-null float64
housing_median_age 20640 non-null float64
total_rooms 20640 non-null float64
total_bedrooms 20433 non-null float64
population 20640 non-null float64
households 20640 non-null float64
median_income 20640 non-null float64
median_house_value 20640 non-null float64
ocean_proximity 20640 non-null object
dtypes: float64(9), object(1)
memory usage: 1.6+ MB
可以看出數據集中總共有 20640 個實例。對於機器學習來說,數據量不算大,但非常適合入門使用。注意屬性 total_bedrooms 只有 20433 個非空值。意味著有 207 個地區缺少這個特徵值,我們將稍後處理這種情況。
所有屬性都是數值類型,除了 ocean_proximity。ocean_proximity 的類型是一個對象,因此可能是任何類型的 Python 對象,但一旦你從 CSV 文件中導入這個數據,那麼它一定是一個文本屬性。之前查看前 5 行數據時,會發現該屬性都是一樣的,意味著 ocean_proximity 很可能是一個類別屬性。可以通過使用 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() 方法展示的是數值屬性的總結:
注意,以上的結果,空值是不計入統計的。其中,count 表示總數,mean 表示均值,std 表示標準差,min 表示最小值,max 表示最大值。
另外一種對數據集有個整體感知的方法就是對每個數值屬性作柱狀圖。柱狀圖展示的是給定數值範圍(橫坐標)內所包含的實例總數(縱坐標)。你可以一次只畫一個屬性的柱狀圖,也可以對整個數據集使用 hist() 方法,將會對每個數值屬性繪製柱狀圖。例如,從柱狀圖種可以看到有超過 800 個地區的房價中位數在 $500000 左右。
%matplotlib inline
import matplotlib.pyplot as plt
housing.hist(bins=50, figsize=(20,15))
plt.show()
hist() 方法依賴於 Matplotlib(),而 Matplotlib() 又依賴於用戶指定的圖形後端來作圖。因此,在作圖之前你需要指定 Matplotlib 使用的後端,最簡單的做法是使用 Jupyter 的魔術命令 %matplotlib inline。這行命令會使用 Jupyter 自帶的後端並作圖。注意在 Jupyter notebook 種調用 show() 不是必須的,因為單元執行時 Jupyter 會自動顯示圖形。
在這些柱狀圖種注意以下幾點:
1. 首先,收入中位數屬性看起來並不是用標準的美元值來表徵的。實際上收入中位數是經過了縮放和削頂處理的,削頂就是把大於 15 的都設為 15(實際上是 15.0001),把小於 0.5 的都設為 0.5(實際上是 0.4999)。在機器學習種,對特徵屬性進行預處理很常見。這不一定是個問題,但是你要試著明白數據是如何計算的。
2. 房屋年齡中位數和房屋價格中位數也被削頂了。房價削頂可能是一個嚴重的問題,因為它是目標屬性(標籤)。削頂可能會讓機器學習算法無法預測出界限之外的值。你應該好好檢查一下削頂到底有沒有影響,如果需要精準預測房價中位數,包括是界限之外的值,那麼你有兩種方法:
a. 對削頂的樣本進行重新採集,收集實際數值。
b. 直接在訓練集種丟棄這些削頂的樣本(同時也對測試集這麼做,因為如果房價中位數超過界限,預測結果可能就不好)。
3. 這些屬性的量度不同。稍後我們將詳細討論這一問題。
4. 最後,許多柱狀圖有很長的尾巴:它們向右的拖尾比向左長得多。這可能會讓一些機器學習算法檢測模式變得更加困難。我們稍後會對這些屬性進行轉換,讓它們更加接近於正態分布曲線。
3.4 創建測試集
在這個階段就擱置部分數據可能聽起來比較奇怪。畢竟我們只是對數據有個初步的認識,在決定使用哪種算法之前應該對數據有更多的了解才是。沒錯,但是我們的大腦是個非常神奇的模式檢測系統,它很容易就過擬合:如果查看了測試集,很容易就發現測試集中一些有趣的模式,致使我們傾向於選擇符合這些模式的機器學習模型。當測量測試集的泛化誤差時,結果往往會很好。但是,部署系統之後會發現模型在實際使用時表現得並不好。這種情況稱為數據窺視偏差(data snooping bias)。
創建測試集理論上很簡單:隨機選擇整個數據集大約 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)),這樣每次產生的測試集都是相同的。
但是這兩種方法在數據集更新的時候都會失效。一種常用的解決方法是使用每個實例的標誌符來決定是否作為測試集(假設標識符是唯一且不變的)。例如,可以計算每個實例標識符的哈希值,只保留哈希值最後一個字節,如果該字節值小於等於 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]
雖然,housing 數據集沒有標識符這一列,但是最簡單的辦法是使用行索引作為標識符 ID:
housing_with_id = housing.reset_index()
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")
如果使用行索引作為唯一標識符,需要確保新的數據必須放置在原來數據集的後面,不能刪除行。如果做不到的話,可以使用一個最穩定的特徵作為標識符。例如,一個地區的經度和維度一定是唯一且百萬年不變的,因此可以結合這兩個特徵來作為唯一標識符:
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 個人對整個人口具有代表性。例如,美國人口中,女性佔 51.3%,男性佔 48.7%。因此,一個比較好的調查方式就是讓抽樣樣本保持這樣的性別比例:513 名女性,487 名男性。這種做法稱為分層抽樣(stratified sampling):將總人口分成均勻的子分組,稱為分層,從每個分層採樣合適數量的實例,以保證測試集對總人口具有代表性。如果採樣隨機抽樣,有 12% 的可能造成採樣偏差:女性人數低於 49% 或高於 54%,調查結果可能就會出錯。
假如專家告訴你收入中位數是預測房價中位數非常重要的屬性之一。你希望確保測試集能夠涵蓋整個數據集中所有的收入類別。因為收入中位數是連續數值,你首先需要創建收入類別屬性。讓我們更仔細地看一下收入中位數柱狀圖(經過處理)。
顯然,大部分收入中位數都在 2-5(萬美元) 之間,某些在 6 以上。數據集中每個分層都必須有足夠多數量的實例,否則對某分層重要性的估計可能出現偏差。這就意味著不能有太多分層,每個分層應該有足夠多的實例。下面的代碼通過將收入中位數除以 1.5 來創建一個輸入類別屬性(除以 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.350533
2.0 0.318798
4.0 0.176357
5.0 0.114583
1.0 0.039729
Name: income_cat, dtype: float64
你可以使用類似的代碼計算測試集中各收入類別的比例。下圖比較了整個數據集、純隨機採樣測試集、分層採樣測試集三者之間收入類比的比例。可以看出,分層採樣測試集的收入類別比例與整個數據集近似相同,而純隨機採樣測試集與整個數據集相比產生了較大的偏差。
現在你可以把 income_cat 屬性刪除,讓數據回到它的初始狀態(income_cat 屬性是為了進行分層採樣的):
for set in (strat_train_set, strat_test_set):
set.drop(["income_cat"], axis=1, inplace=True)
我們之所以花很多時間在劃分測試集上,是因為在機器學習項目中這非常重要但卻容易被忽視。更重要的,這些概念在我們之後討論交叉驗證(cross-validation)時會很有用。
想要及時獲取後續章節乾貨,請置頂公眾號!
【推薦閱讀】
機器學習實用指南:這些基礎盲點請務必注意!
機器學習實用指南:機器學習面臨哪些挑戰?
乾貨 | 公眾號歷史文章精選(附資源)
我的深度學習入門路線
我的機器學習入門路線圖