1 引言¶
預處理操作是機器學習整個周期中必不可少的一個過程,也是最能快速改善模型性能的一個過程,往往稍微轉換一下特征屬性的形態,就能得到性能的極大提升。當然,數據預處理絕對也是耗時最長的一個過程,這一過程不僅要求洞悉整個數據集結構分布,還要探查每一個特征屬性細節情況,並作出應對處理,使數據以最適合的狀態傳輸給模型。
針對預處理操作,sklearn中提供了許多模塊工具,靈活使用工具可以讓數據預處理輕松很多。
本文簡要介紹數據預處理中的一些主要方法,並結合sklearn中提供的模塊進行實踐。
2 無量綱化¶
對於大部分機器學習任務而言,對原始數據進行無量綱化是是建模前的必不可少的一個環節。通過無量綱化,可以消除量綱不一致對模型造成的不良影響。標准化和歸一化是最為常見的兩種無量綱化方法,下面分別展開介紹這兩種方法。
2.1 標准化¶
標准化對數據的分布的進行轉換,使其符合某種分布(一般指正態分布)的一種特征變換。一般而言,標准化都是指通過z-score的方法將數據轉換為服從均值為0,標准差為1的標准正態分布數據,通過如下公式進行轉換: $$x' = \frac{{x - \mu }}{\sigma }$$ 式中,$\mu$和$\sigma$是指$x$所在特征屬性集的均值和標准差。
(1)sklearn.preprocessing.scale方法實現標准化
from sklearn import preprocessing
import numpy as np
X_train = np.array([[ 1., -1., 2.],
[ 2., 0., 0.],
[ 0., 1., -1.]])
X_scaled = preprocessing.scale(X_train)
再次查看X_train各列,我們會發現,均值和方差都已經標准化:
X_train.mean(axis=0)
array([1. , 0. , 0.33333333])
X_scaled.std(axis=0)
array([1., 1., 1.])
(2)sklearn.preprocessing.StandardScaler類實現歸一化
除了scale方法外,在sklearn.preprocessing模塊中還提供有一個專門的類用於實現標准化:StandardScaler,StandardScaler類會自動計算實例化類時傳入的訓練集的均值、標准差,並將這些信息保留,這也就意味着,對訓練集的標准化方式可以復用,例如對測試集和預測樣本進行同樣的標准化。所以,一般來說,更加建議使用StandardScaler類來實現標准化。
# 傳入一個訓練集,實例化StandarScaler類
scaler = preprocessing.StandardScaler()
scaler.fit(X_train) # 收集標准化信息,均值,標准差
StandardScaler(copy=True, with_mean=True, with_std=True)
scaler.mean_ # 查看均值
array([1. , 0. , 0.33333333])
scaler.scale_ # 查看標准差
array([0.81649658, 0.81649658, 1.24721913])
創建StandarScaler類實例后,需要通過類中的transform方法對X-train進行標准化:
scaler.transform(X_train)
array([[ 0. , -1.22474487, 1.33630621], [ 1.22474487, 0. , -0.26726124], [-1.22474487, 1.22474487, -1.06904497]])
StandardScaler類中還提供有一個fit_transform方法,這個方法合並了fit和transform兩個方法的功能,同時根據傳入的數據集收集標准化信息,並將標准化方案應用於傳入的訓練集:
scaler = preprocessing.StandardScaler()
x_train = scaler.fit_transform(X_train)
x_train
array([[ 0. , -1.22474487, 1.33630621], [ 1.22474487, 0. , -0.26726124], [-1.22474487, 1.22474487, -1.06904497]])
假設現在有一個測試樣本,那么,也可以通過transform方法將標准化方案應用於測試樣本上:
X_test = [[-1., 1., 0.]]
scaler.transform(X_test)
array([[-2.44948974, 1.22474487, -0.26726124]])
2.2 歸一化¶
歸一化是指對數據的數值范圍進行特定縮放,但不改變其數據分布的一種線性特征變換。大多數場景下,歸一化都是將數據縮放到[0,1]區間范圍內,計算公式如下: $$x' = \frac{{x - \min }}{{\max - \min }}$$ 式中,$min$和$max$是$x$所屬特征集合的最小值和最大值。可見,這種歸一化方式的最終結果只受極值的影響。
(1)sklearn.preprocessing.minmax_scale方法實現歸一化。
X_train = np.array([[ 1., -1., 2.],
[ 2., 0., 0.],
[ 0., 1., -1.]])
X_train = preprocessing.minmax_scale(X_train)
X_train
array([[0.5 , 0. , 1. ], [1. , 0.5 , 0.33333333], [0. , 1. , 0. ]])
(2)sklearn.preprocessing.MinMaxScaler類實現歸一化。
X_train = np.array([[ 1., -1., 2.],
[ 2., 0., 0.],
[ 0., 1., -1.]])
min_max_scaler = preprocessing.MinMaxScaler()
X_train_minmax = min_max_scaler.fit_transform(X_train)
X_train_minmax
array([[0.5 , 0. , 1. ], [1. , 0.5 , 0.33333333], [0. , 1. , 0. ]])
使用訓練好的min_max_scaler對新的測試樣本進行歸一化:
X_test = np.array([[-3., -1., 4.]])
X_test_minmax = min_max_scaler.transform(X_test)
X_test_minmax
array([[-1.5 , 0. , 1.66666667]])
我們知道,歸一化是將特征屬性值縮放到[0,1]范圍,但在某些特殊的場景下,我們需要將特征屬性縮放到其他范圍,MinMaxScaler類通過feature_range參數也提供了這一功能,feature_range參數接受一個元組作為參數,默認值為(0,1)。
X_train = np.array([[ 1., -1., 2.],
[ 2., 0., 0.],
[ 0., 1., -1.]])
min_max_scaler = preprocessing.MinMaxScaler(feature_range=(10,20))
X_train_minmax = min_max_scaler.fit_transform(X_train)
X_train_minmax
array([[15. , 10. , 20. ], [20. , 15. , 13.33333333], [10. , 20. , 10. ]])
(3)sklearn.preprocessing.MaxAbsScaler類實現歸一化
MaxAbsScaler是專門為稀疏數據做歸一化設計的,通過特征值除以整個特征集合最大絕對值實現,最終將數據投影到[-1, 1]范圍內,對原來取值為0的數據並不會做出變換,所以不會影響數據的稀疏性。
最后來總結一下標准化和歸一化。
標准化是依照特征矩陣的列處理數據,其通過求z-score的方法,轉換為標准正態分布,和整體樣本分布相關,每個樣本點都能對標准化產生影響,而歸一化是將樣本的特征值轉換到同一量綱下把數據映射到指定區間內,僅由變量的極值決定,所以對異常值較為敏感。標准化和歸一化都是一種線性變換,都是對向量x按照比例壓縮再進行平移。無論是標准化還是歸一化,都可以將數據無量綱化,消除不同量綱對結果的影響,同時都可以加過模型的收斂速度。 標准化與歸一化之間如何選擇呢?
大多數機器學習算法中,會選擇StandardScaler來進行特征縮放,因為MinMaxScaler對異常值非常敏感。在PCA,聚類,邏輯回歸,支持向量機,神經網絡這些算法中,StandardScaler往往是最好的選擇。
MinMaxScaler在不涉及距離度量、梯度、協方差計算以及數據需要被壓縮到特定區間時使用廣泛,比如數字圖像處理中量化像素強度時,都會使用MinMaxScaler將數據壓縮於[0,1]區間之中。若是歸一化時需要保留數據的稀疏性,則可以使用MaxAbscaler歸一化。 在大多數情況下,建議先試試看StandardScaler,效果不好換MinMaxScaler。
另外,這里再提一下正則化(Normalization),很多資料把正則化與歸一化、標准化放到一起討論,雖然正則化也是數據預處理方法的一種,但我並不認為正則化是無量綱化方法。正則化通過某個特征值除以整個樣本所有特征值的范數計算,使得整個樣本范數為1,通常在文本分類和聚類中使用較多。sklearn中提供preprocessing.normalize方法和preprocessing.Normalizer類實現:
X_train = np.array([[ 1., -2., 2.],
[ 2., 0., 0.],
[ 0., 1., -1.]])
max_abs_scaler = preprocessing.MaxAbsScaler()
X_train_max_abs = max_abs_scaler.fit_transform(X_train)
X_train_max_abs
array([[ 0.5, -1. , 1. ], [ 1. , 0. , 0. ], [ 0. , 0.5, -0.5]])
X_train = np.array([[ 1., -1., 2.],
[ 2., 0., 0.],
[ 0., 1., -1.]])
nor = preprocessing.Normalizer()
X_train_nor = nor.fit_transform(X_train)
X_train_nor
array([[ 0.40824829, -0.40824829, 0.81649658], [ 1. , 0. , 0. ], [ 0. , 0.70710678, -0.70710678]])
3 缺失值處理¶
由於各種各樣的原因,我們所面對的數據經常是有所缺失的,然而sklearn中實現的各個算法都假設數據沒有缺失為前提,如果直接用缺失數據跑算法影響最終結果不說,也容易產生各種異常,所以在數據預處理階段,對缺失值進行處理是很有必要的。對於缺失值處理,直接刪除包含缺失值的特征屬性或者樣本是最簡單的方法,但是這種方法卻也將其他部分信息拋棄,在很多情況下,特別是數據樣本不多、數據價值大時,未免得不償失。在sklearn中,提供了諸多其他處理缺失值的方案,例如以均值、中位數、眾數亦或者是指定值填充缺失值等,這些方案都在sklearn.impute模塊中提供的SimpleImputer類中實現,SimpleImputer類參數如下:
import numpy as np
from sklearn.impute import SimpleImputer
imp = SimpleImputer(missing_values=np.nan, strategy='mean') # 指定缺失值為nan,以均值填充
imp.fit([[1, 2], [np.nan, 3], [7, 6]])
X = [[np.nan, 2], [6, np.nan], [7, 6]]
imp.transform(X)
array([[4. , 2. ], [6. , 3.66666667], [7. , 6. ]])
imp = SimpleImputer(missing_values=0, strategy='constant', fill_value=1) # 指定缺失值為0,指定以常數1填充
imp.fit([[5, 2], [4, 0], [7, 6]])
X = [[0, 2], [6, 0], [7, 6]]
imp.transform(X)
array([[1, 2], [6, 1], [7, 6]])
4 離散型特征屬性處理¶
很多時候,我們所要處理的特征屬性未必是連續型的,也可能是離散型,以衣服為例,款式(男款、女款),大小(X、XL、XXL),顏色(綠色、紅色、白色),都是離散型特征屬性。對於這類離散型特征屬性,需要編碼之后才能用來建模。離散型特征屬性值可以分為兩種:
(1)數字編碼
整數編碼是指對離散型屬性以整數來標識,例如色澤這一特征中,以整數“0”標識“男款”,整數“1”標識“女款”。sklearn中提供了LabelEncoder和OrdinalEncoder兩個類用以實現對數據的不同取值以數字標識。LabelEncoder和OrdinalEncoder會自動根據提供的訓練數據進行統計,分別對每個特征屬性從0開始編碼,不同的是,LabelEncoder類一次只能對一個一維數組(一個特征屬性)編碼,而OrdinalEncoder能同時對各個特征屬性編碼:
enc = preprocessing.LabelEncoder() # 只能接受一個一維數組
X = ['紅色', '白色', '綠色']
enc.fit(X)
X_ = enc.transform(X)
X_
array([1, 0, 2])
enc = preprocessing.OrdinalEncoder() # 可以同時多通過特征屬性編碼
X = [['女款', 'X', '綠色'], ['女款', 'XL', '紅色'], ['男款', 'XXL', '白色']]
enc.fit(X)
X_ = enc.transform(X)
X_
array([[0., 0., 2.], [0., 1., 1.], [1., 2., 0.]])
enc.inverse_transform(X_) # 可以使用inverse_transform逆轉
array([['女款', 'X', '綠色'], ['女款', 'XL', '紅色'], ['男款', 'XXL', '白色']], dtype=object)
但在很多模型中,使用整數編碼並不合理,特別是在聚類這類需要計算空間距離的算法模型。仔細觀察上面編碼,顏色這一屬性有三種取值(綠色、紅色、白色),分別以(2,1,0)表示,顏色之間是沒有大小意義的,但以三個數字表示后,就賦予了三種屬性值大小上的意義,且在算法計算距離時,綠色(2)到白色(0)的距離比紅色(1)到白色(0)大,這是不合理的。
對於這類取值沒有大小意義的離散型特征屬性,有一種更加合適的編碼方式:獨熱編碼。
(2)獨熱編碼
獨熱編碼即 One-Hot 編碼,其方法是使用N位狀態寄存器來對N個狀態進行編碼,每個狀態都由他獨立的寄存器位,並且在任意時候,其中只有一位有效。sklearn中提供了OneHotEncoder類用以實現對數據的獨熱編碼:
enc = preprocessing.OneHotEncoder()
X = [['女款', 'X', '綠色'], ['女款', 'XL', '紅色'], ['男款', 'XXL', '白色']]
enc.fit(X)
enc.transform([['男款', 'XL', '綠色']]).toarray()
array([[0., 1., 0., 1., 0., 0., 0., 1.]])
在上述輸出結果中,特征屬性有多少種取值經過獨熱編碼后就擴展為多少個維度,以款式為例,經過獨熱編碼后,擴展為兩個維度,第一維中1表示是女款,0表示非女款。
在實例化OneHotEncoder類時,可以通過categories參數指定各特征屬性的所有類別,這樣即使存在訓練數據中沒有出現的類別,在后續出現時也能正確編碼:
style = ['女款', '男款']
size = [ 'X','XL','XXL']
color = ['綠色','紅色','白色']
enc = preprocessing.OneHotEncoder(categories=[style, size, color])
X = [['女款', 'X', '綠色'], ['女款', 'XL', '紅色']]
enc.fit(X)
enc.transform(X).toarray()
array([[1., 0., 1., 0., 0., 1., 0., 0.], [1., 0., 0., 1., 0., 0., 1., 0.]])
enc.transform([['男款', 'XXL', '白色']]).toarray() # 男款,XXL,白色三個屬性值均為在X中出現,但是可以正確編碼
array([[0., 1., 0., 0., 1., 0., 0., 1.]])
創建好OneHotEncoder類實例並通過訓練數據后,就可以對后續的數據進行獨熱編碼,但是,有時候卻不可避免地出現categories和訓練數據集中都未出現過的取值,這時候繼續編碼就會拋出異常。為了防止這一情況發生,我們可以在創建OneHotEncoder實例時,傳入參數handle_unknown='ignore',這樣的話,如果出現某一特征屬性值未在categories和訓練數據集中出現過,通過熱獨編碼時,該特征屬性多對應的維度都會以0來填充。
style = ['女款', '男款']
size = [ 'X','XL']
color = ['綠色','紅色']
enc = preprocessing.OneHotEncoder(categories=[style, size, color],handle_unknown='ignore')
X = [['女款', 'X', '綠色'], ['女款', 'XL', '紅色']]
enc.fit(X)
enc.transform([['男款', 'XXL', '白色']]).toarray() # XXL, 白色在categories和X中都為出現過
array([[0., 1., 0., 0., 0., 0.]])
獨熱編碼解決了離散型屬性難以有效刻畫的問,在一定程度上也起到了擴充特征的作用,它的值只有0和1,不同的類型存儲在垂直的空間。當類別的數量很多時,特征空間會變得非常大。在這種情況下,一般可以用PCA來減少維度。而且one hot encoding+PCA這種組合在實際中也非常有用。
5 連續型特征屬性離散化¶
有時候,將連續型特征屬性離散化能夠顯著提高模型的表現力。連續型特征屬性離散化包括二值化和分段等方法。
(1)二值化
二值化是指通過一個閾值對屬性值進行划分,當小於這個閾值時,將值映射為0,大於閾值時映射為1。二值化是對文本計數數據的常見操作,分析人員可以決定僅考慮某種現象的存在與否。它還可以用作考慮布爾隨機變量的估計器的預處理步驟(例如,使用貝葉斯設置中的伯努利分布建模)。
sklearn中提供了Binarizer實現二值化,默認閾值為0,也就是將非正數映射為0,將正數映射為1。也可以在實例化時通過參數threshold,設置其他閾值。
X = [[ 1., -1., 2.],
[ 2., -4., 0.],
[ 3., 2., -1.]]
binarizer = preprocessing.Binarizer().fit(X)
binarizer
Binarizer(copy=True, threshold=0.0)
binarizer.transform(X)
array([[1., 0., 1.], [1., 0., 0.], [1., 1., 0.]])
binarizer = preprocessing.Binarizer(threshold=1.5).fit(X)
binarizer.transform(X)
array([[0., 0., 1.], [1., 0., 0.], [1., 1., 0.]])
(2)分段
二值化只能將數據映射為兩個值,分段可以對數據進行排序后分為多個部分然后進行編碼。在sklearn中,分段操作通過KBinsDiscretizer類進行。KBinsDiscretizer類有三個重要參數,必須了解一下:
X = [[-2, 1, -4, -1],
[-1, 2, -3, -0.5],
[ 0, 3, -2, 0.5],
[ 1, 4, -1, 2]]
est = preprocessing.KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='uniform').fit(X)
est.transform(X)
array([[0., 0., 0., 0.], [1., 1., 1., 0.], [2., 2., 2., 1.], [2., 2., 2., 2.]])