機器學習項目流程(一)初探數據集


機器學習項目流程

在這我們會從頭開始做一個機器學習項目,向大家展示一個機器學習項目的一個基本流程與方法。一個機器學習主要分為以下幾個步驟:

  1. 從整體上了解項目
  2. 獲取數據
  3. 發現並可視化數據,以深入了解數據
  4. 為機器學習算法准備數據
  5. 選擇模型並訓練
  6. 模型調優
  7. 展示解決方案
  8. 部署、監控、以及維護我們的系統

我們不會遍歷所有步驟,僅從一個例子展示一個常規的流程。

 

使用真實數據

在學習機器學習時,最好是使用真實數據,不要用人工代碼生成的模擬數據。一些可以獲取數據的地方如:

  1. 開放數據集
    1. UC Irvine Machine Learning Repository
    2. Kaggle 數據集
    3. Aamzon AWS 數據集
    4. 提供收集的數據集
      1. Data Portals
      2. OpenDataMonitor
      3. Quandl
      4. 其他列出開放數據集的網站
        1. Wikipedia 的機器學習數據集
        2. Quora.com
        3. 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))

 

 

 

在這些直方圖中,我們需要注意以下幾點:

  1. 首先,median_income 的取值看起來並不像是美元的值,因為它的取值是從0.4999 到 15.000100,不符合我們對工資單位的認知。這是因為這個屬性的取值被縮放了,所以 3 對應的應為 $30,000。同時,這個屬性也被設定了上限(即15.0001)。所以在機器學習項目中,一定要了解每個屬性的取值是如何計算得來的,這樣我們會對數據有更清楚的認知。
  2. housing_median_age 以及median_house_value 都被設定了上限。對median_house_value 設置上限造成的影響可能會更大,因為這個是我們的目標屬性(也就是label)。我們的機器學習模型可能會學習到:房間永遠不會超過它的上限值(500001.000000)。在這種情況下,我們需要跟需求方進行溝通,需要了解這個上限對於他們來說是否是一個問題。如果他們回復說模型需要做到非常精准的預測,即使是超過$500,000 的值也是需要的,則我們接下來有以下兩種方法:
    1. 收集原始信息,也就是這些label 在被設定上限前的數據
    2. 從訓練數據中移除掉這些數據(同時在測試數據中也不能使用這些數據)
    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)

 

這個方法是隨機從數據集中取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)

 

至此,我們總結一下當前的工作:

  1. 獲取數據集
  2. 查看數據結構,進一步了解數據集的屬性
  3. 根據分層采樣分割出測試集

 

下一步我們會繼續探索、可視化數據集。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM