機器學習項目流程
在這我們會從頭開始做一個機器學習項目,向大家展示一個機器學習項目的一個基本流程與方法。一個機器學習主要分為以下幾個步驟:
- 從整體上了解項目
- 獲取數據
- 發現並可視化數據,以深入了解數據
- 為機器學習算法准備數據
- 選擇模型並訓練
- 模型調優
- 展示解決方案
- 部署、監控、以及維護我們的系統
我們不會遍歷所有步驟,僅從一個例子展示一個常規的流程。
使用真實數據
在學習機器學習時,最好是使用真實數據,不要用人工代碼生成的模擬數據。一些可以獲取數據的地方如:
- 開放數據集
- UC Irvine Machine Learning Repository
- Kaggle 數據集
- Aamzon AWS 數據集
- 提供收集的數據集
- Data Portals
- OpenDataMonitor
- Quandl
- 其他列出開放數據集的網站
- Wikipedia 的機器學習數據集
- Quora.com
- Subreddit 數據集
在這章我我們會使用California Housing Prices 數據集,數據來源為StatLib。這個數據集基於的是1990年加州人口普查數據。數據地址如下:
https://github.com/ageron/handson-ml2/blob/master/datasets/housing/housing.tgz
查看數據結構
下載數據后,我們首先使用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) housing = load_housing_data()
housing.head()
可以看到這個數據集一共有10個屬性,以及各個屬性的數據類型。下面可以使用 info() 方法查看一下數據集的描述:
housing.info()
此方法打印出了數據的總行數為 20640,各個屬性的數值類型,以及非空的數值(non-null)數目。20640行意味着這個是一個很小的數據集,而其中尤其要注意的是:total_bedrooms 的行數僅有20433,小於20640。說明有207條數據缺失這個值。
在10條屬性中,有9條均為 float64 類型,僅有ocean_proximity 的屬性為 object 類型。在 python 中,object 類型可以代表任何對象。由於我們已知數據是從csv讀入的,所以此屬性的類型應為文本(text)類型。通過head() 方法查看前五條數據,可以看到這個屬性的取值均為“NEAR BAY”,所以可以大致推斷這個屬性的值類型為離散型的屬性。對於離散型屬性取值,我們可以通過value_counts()的方法查看離散值的統計信息,以及有多少條目屬於某個取值:
housing['ocean_proximity'].value_counts()
繼續查看其它屬性,describe() 方法可以打印出數值型屬性的統計數據:
housing.describe()
count指標表示的是條目數(空置已被忽略,所以total_bedrooms 的count數較小),mean是平均數,std是標准差(衡量的是數據的離散程度)。25% - 75% 分別是分位數。
另一個快速了解數據的方式是給數值型屬性畫一個直方圖,直方圖可以給出屬性在某個取值范圍內的個數。我們可以每次畫出一個屬性的直方圖,或是在數據集上調用hist() 方法,此方法會為每個數值型屬性畫出它的直方圖。
%matplotlib inline import matplotlib.pyplot as plt housing.hist(bins=50, figsize=(20, 15))
在這些直方圖中,我們需要注意以下幾點:
- 首先,median_income 的取值看起來並不像是美元的值,因為它的取值是從0.4999 到 15.000100,不符合我們對工資單位的認知。這是因為這個屬性的取值被縮放了,所以 3 對應的應為 $30,000。同時,這個屬性也被設定了上限(即15.0001)。所以在機器學習項目中,一定要了解每個屬性的取值是如何計算得來的,這樣我們會對數據有更清楚的認知。
- housing_median_age 以及median_house_value 都被設定了上限。對median_house_value 設置上限造成的影響可能會更大,因為這個是我們的目標屬性(也就是label)。我們的機器學習模型可能會學習到:房間永遠不會超過它的上限值(500001.000000)。在這種情況下,我們需要跟需求方進行溝通,需要了解這個上限對於他們來說是否是一個問題。如果他們回復說模型需要做到非常精准的預測,即使是超過$500,000 的值也是需要的,則我們接下來有以下兩種方法:
- 收集原始信息,也就是這些label 在被設定上限前的數據
- 從訓練數據中移除掉這些數據(同時在測試數據中也不能使用這些數據)
- 這些數值型屬性的取值都處於不同的范圍,需要將它們進行規范化
- 最后,很多直方圖都是重尾分布:中值的右邊拖的很長,而左邊較短。這種分布會增加一些機器學習算法在進行模式識別時的難度。我們需要之后轉化這些屬性,盡量量它們的分布轉為鍾形分布
到現在為止,希望讀者對我們要處理的數據已經有了更進一步的了解。
創建測試集
測試集在機器學習方法中用於判斷模型的准確度,一般我們會從原始數據集中隨機選擇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)
這個方法是隨機從數據集中取20%的數據作為測試數據。但是這個其實是有問題的,因為在多輪次訓練中,若是每次均通過隨機取數據條目,則最終所有數據條目都有機會被放入到訓練數據中,而這也正是違背了我們最開始的初衷 —— 測試數據僅用於驗證模型精准度,而不能用於模型訓練。
對此,其中一個辦法是:在調用 np.random.permutation() 前,為隨機數加一個種子參數,如 np.random.seed(42)。這樣可以保證每次獲取的shufle_indices 都是一樣的。另一個更簡單的辦法是在第一次執行時就將測試數據保存,之后在使用時再加載。
然而,以上兩種方式仍有缺陷。假設我們原有數據集做了更新,增加了新的數據條目,則以上兩種方法均未顧及到這點。所以一個更常規的解決方法是:使用每條數據的標識符來決定是否將此條目放入測試集(假設每條數據都有一個唯一、不可變的標識符)。例如,我們可以計算每條數據的標識符的哈希值,並將哈希值小於或等於(最大哈希值×20%)的條目放入測試集。這樣可以確保在多次運行后,測試集仍保持一致。即使在之后添加了新的數據條目,測試集中的數據仍為整體數據集的20%,且不會包含任何曾經屬於過訓練集的數據條目。下面是一個實現:
from zlib import crc32 def test_set_check(identifier, test_ratio): return crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2**32 def split_train_test_by_id(data, test_ratio, id_column): ids = data[id_column] in_test_set = ids.apply(lambda id_: test_set_check(id_, test_ratio)) return data.loc[~in_test_set], data.loc[in_test_set]
第一個方法是判斷是否屬於最大哈希值的 20%,第二個方法是根據哈希值的大小取出訓練集與測試集。
接下來的問題是:housing 數據及沒有一個標識符的列。所以一個簡單的辦法是:直接使用條目的index 作為 ID:
housing_with_id = housing.reset_index() # add an 'index' column train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, 'index') len(train_set) 16512 len(test_set) 4128
但是這個方法依然有它的局限性,因為我們必須確保新加入的數據集的 index id 是以追加的方式加入到此數據集的,並且在原數據集中不能有任何條目被刪除,否則就對 index id 的順序造成了干擾。如果以上兩者均無法在實際應用中達成,則我們可以嘗試用其他更穩定的屬性去構造一個標識符。例如,一個地區的經度與緯度理論上來說是一個穩定值,所以我們可以使用這兩個屬性longitude 以及 latitude 去構造一個 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") print(len(train_set), len(test_set)) 16322 4318
SciKit-Learn 庫提供了幾個方法,可以使用不同的方式將數據集划分為多個子集。最簡單的方法為train_test_split(),這個方法與此文中定義的split_train_test() 方法基本一致,但是會提供更多的功能。首先可以提供一個 random_state 參數,用於指定隨機種子,然后我們可以傳入多個數據集(它們的行數相同),並將它們分割為相同的索引列表。這個功能是非常有用的,比如我們有一個DataFrame 是訓練集,但是它的label卻在另一個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個人能代表整個國家的意見,那么就必須考慮到構成整個國家的人口的差異。例如,假設這個國家里男性占 55%,女性占45%。則在執行采樣時,這1000 個人中的男女比例也應為 11:9,也就是分別為550 人與 450 人。這種方式稱為“分層采樣”:人口的數據被分為多個同樣類別組成的子集,稱為“層”。所以在采樣時,需要按比例從各層中采樣,才能代表總體的數據。
假設這里我們有個專家告訴我們,median_income 是一個非常重要的指標,它與預估房價的關系非常緊密。那這里我們可能就需要確保:測試集里的數據能代表整個數據集中的各個不同收入范圍樣本。由於 median_income 是一個連續性的數值型屬性,所以我們首先要創建一個income 的類別屬性。我們再回顧一下 median_income 的直方圖:
可以看到大部分median_income 的值集中在1.5 到6(也就是 $15,000 - $60,000)之間,但是直到 6 之后很遠的地方(如15),仍有數據點。在采樣中很重要的一點是:在每個“分層“中,都要采集足夠的樣本,否則每個層的重要性可能就存在偏差。也就是說,我們不能有太多的層,並且每層也應該足夠大。下面的代碼我們使用了 pd.cut() 方法,用於創建一個新的income 類別屬性(包含5個類別,label 從1到5):類別1為0到1.5(也就是小於$15,000),類別2從1.5到3,依次類推:
housing["income_cat"] = pd.cut(housing["median_income"], bins=[0., 1.5, 3.0, 4.5, 6., np.inf], labels=[1, 2, 3, 4, 5])
可以看到每條數據的 income_cat 屬性均填寫了歸於哪一類。
housing["income_cat"].hist()
現在我們可以根據income的類別,開始做分層采樣。對此,可以使用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]
檢查一下結果:
strat_test_set['income_cat'].value_counts() / len(strat_test_set)
3 0.350533
2 0.318798
4 0.176357
5 0.114583
1 0.039729
Name: income_cat, dtype: float64
也可以看看直方圖:
start_test_set['income_cat'].hist()
可以看到采樣出來的 test 集合中,income_cat的分布與原始集合基本是一致的。
現在我們有了一個 test 的采樣集合后,就可以去掉income_cat 的屬性了,讓數據回歸原始狀態:
for set_ in (strat_test_set, strat_train_set):
set_.drop("income_cat", axis=1, inplace=True)
至此,我們總結一下當前的工作:
- 獲取數據集
- 查看數據結構,進一步了解數據集的屬性
- 根據分層采樣分割出測試集
下一步我們會繼續探索、可視化數據集。