天天看點

機器學習實戰指南:如何入手第一個機器學習項目?

機器學習實戰指南:如何入手第一個機器學習項目?

本系列為《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. 使用真實資料

學習機器學習時,最好使用真實資料,而不是“人造”資料。幸運的是,有許多開源的資料集可以免費使用,涉及許多行業領域。下面列舉一些:

這一章我們将使用來自 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 的公式如下:

機器學習實戰指南:如何入手第一個機器學習項目?
機器學習實戰指南:如何入手第一個機器學習項目?
機器學習實戰指南:如何入手第一個機器學習項目?
機器學習實戰指南:如何入手第一個機器學習項目?

2.3 檢查假設

最後,最好列出目前為止做得所有假設并驗證,這能幫助你盡早發現問題。例如你預測房價,然後傳輸到下遊機器學習系統。但是,下遊機器學習系統實際上把你預測得價格轉換成了不同類别(例如便宜、中等、昂貴),使用這些類别代替實際預測值。這種情況下,準确預測房價并不是特别重要了!你隻需要對房價進行類别劃分即可。這樣的話,這就是一個分類問題而不是回歸問題。這是需要提前弄清楚的,你可不想建立回歸模型之後才發現事實。

幸運的是,在與下遊機器學習系統溝通之後,确認這确實是一個回歸問題。好了,接下來就開始真正地編寫程式了。

3. 擷取資料

完整的代碼在 GitHub 上擷取,位址是:

https://github.com/ageron/handson-ml

代碼形式是 Jupyter Notebook。

3.1 建立工作環境

首先你需要安裝 Python,擷取位址:

https://www.python.org/

接下來需要建立一個工作空間目錄,在終端輸入以下指令(在提示符 $ 之後):

$ export ML_PATH="$HOME/ml"     # You can change the path if you prefer
$ 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 是否安裝:

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

你應該安裝 pip 的最新版本,至少是 1.4 版本以上的,以支援二進制子產品的安裝(也稱為 wheels)。更新 pip 到最新版本的指令是:

pip3 install --upgrade pip      

建立獨立環境:

如果你想建立一個獨立的工作環境(強烈推薦!這樣可以使不同項目之間不會出現庫的沖突),輸入以下 pip 指令來安裝 virtualenv:

pip3 install --user --upgrade virtualenv      

現在你可以建立一個獨立的 Python 環境了:

$ cd $ML_PATH
$ virtualenv env      

每次你想激活這個獨立環境,隻需打開一個終端輸入以下指令:

$ cd $ML_PATH
$ source env/bin/activate      

補充一下,如果代碼寫完,想關閉目前環境,輸入以下指令:

$ deactivate      

一旦環境激活之後,你使用 pip 安裝的所有包都僅限于該獨立環境中,Python 也隻會通路這些包(如果你想通路系統其它包,可以在建立環境的時候使用 virtualenv 的 –system-site-packages 選項)。檢視 virtualenv 的文檔擷取更多資訊。

現在,你可以使用簡單的 pip 指令來安裝所有需要的子產品和它們的依賴了:

$ pip3 install --upgrade jupyter matplotlib numpy pandas scipy scikit-learn      

為了檢查是否安裝成功,可以使用以下指令導入所有子產品:

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

沒有錯誤的話,就可以輸入以下指令打開 Jupyter Notebook 啦!

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

直接運作函數:

fetch_housing_data()      

将會在你的工作空間建立目錄 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() 方法展示的是數值屬性的總結:

housing.describe()      
機器學習實戰指南:如何入手第一個機器學習項目?

注意,以上的結果,空值是不計入統計的。其中,count 表示總數,mean 表示均值,std 表示标準差,min 表示最小值,max 表示最大值。 

另外一種對資料集有個整體感覺的方法就是對每個數值屬性作柱狀圖。柱狀圖展示的是給定數值範圍(橫坐标)内所包含的執行個體總數(縱坐标)。你可以一次隻畫一個屬性的柱狀圖,也可以對整個資料集使用 hist() 方法,将會對每個數值屬性繪制柱狀圖。例如,從柱狀圖種可以看到有超過 800 個地區的房價中位數在 $500000 左右。

%matplotlib inline # only in a Jupyter notebook
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() # adds an `index` column
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)時會很有用。