特征選擇與特征工程
特征工程是機(jī)器學(xué)習(xí)的第一步,涉及清理現(xiàn)有數(shù)據(jù)集、提高信噪比和降低維數(shù)的所有技術(shù)。大多數(shù)算法對(duì)輸入數(shù)據(jù)有很強(qiáng)的假設(shè),當(dāng)使用原始數(shù)據(jù)集時(shí),它們的性能可能會(huì)受到負(fù)面影響。
另外有些特征之間高度相關(guān),在其中一個(gè)特征提供了足夠的信息之后,與之相關(guān)的其他特征往往無(wú)法提供額外的信息。這時(shí)我們就需要了解如何減少特征數(shù)量或者僅選擇最佳特征。
一、scikit-learn數(shù)據(jù)集
scikit-learn提供了一些用于測(cè)試的內(nèi)置數(shù)據(jù)集,這些數(shù)據(jù)集包含在sklearn.datasets中,每個(gè)數(shù)據(jù)集都包含了輸入集(特征集)X和標(biāo)簽(目標(biāo)值)y。比如波士頓房?jī)r(jià)的數(shù)據(jù)集(用于回歸問(wèn)題):
from sklearn.datasets import load_boston
boston = load_boston()
X = boston.data
y = boston.target
print('特征集的shape:', X.shape)
print('目標(biāo)集的shape:', y.shape)
特征集的shape: (506, 13)
目標(biāo)集的shape: (506,)
可以看到,這個(gè)數(shù)據(jù)集包含了506個(gè)樣本、13個(gè)特征,以及1個(gè)目標(biāo)值。
假如我們不想使用scikit-learn提供的數(shù)據(jù)集,那么我們還可以使用scikit-learn提供的工具來(lái)手動(dòng)創(chuàng)建特定的數(shù)據(jù)集。相關(guān)的方法有:
-
make_classification():用于創(chuàng)建適用于測(cè)試分類算法的數(shù)據(jù)集; -
make_regression():用于創(chuàng)建適用于測(cè)試回歸模型的數(shù)據(jù)集; -
make_blobs():用于創(chuàng)建適用于測(cè)試聚類算法的數(shù)據(jù)集。
二、創(chuàng)建訓(xùn)練集和測(cè)試集
一般來(lái)說(shuō),我們要在正式應(yīng)用我們訓(xùn)練的模型前對(duì)它進(jìn)行測(cè)試。因此我們需要將數(shù)據(jù)集分為訓(xùn)練集和測(cè)試集,顧名思義,前者用于訓(xùn)練模型參數(shù),后者用于測(cè)試模型性能。在某些情況下,我們甚至還會(huì)再分出一個(gè)數(shù)據(jù)集作為交叉驗(yàn)證集,這種處理方式適用于有多種模型可供選擇的情況。
數(shù)據(jù)集的分割有一些注意事項(xiàng):首先,兩個(gè)數(shù)據(jù)集必須要能反映原始數(shù)據(jù)的分布,否則在數(shù)據(jù)集失真的情況下得到的模型對(duì)于真實(shí)樣本的預(yù)測(cè)效力會(huì)比較差;其次,原始數(shù)據(jù)集必須在分割之前隨機(jī)混合,以避免連續(xù)元素之間的相關(guān)性。
在scikit-learn中,我們可以使用train_test_split()函數(shù)來(lái)快速實(shí)現(xiàn)數(shù)據(jù)集的分割。
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=1000)
這里前兩個(gè)位置參數(shù)分別是特征集和目標(biāo)集,test_size用于指定測(cè)試集大小占整個(gè)數(shù)據(jù)集的比例,random_state則是指定一個(gè)隨機(jī)種子,這樣可以確保我們?cè)谥貜?fù)試驗(yàn)時(shí)數(shù)據(jù)不會(huì)發(fā)生變化(數(shù)據(jù)集都變了,那模型效果的變化就不知道該歸因于模型的優(yōu)化還是歸因于數(shù)據(jù)集的變化了。)
三、管理分類數(shù)據(jù)
在許多分類問(wèn)題中,目標(biāo)數(shù)據(jù)集由各種類別標(biāo)簽組成。但是很多算法是不支持這種數(shù)據(jù)格式的,因此我們要對(duì)其進(jìn)行必要的編碼。
假設(shè)我們有一個(gè)由10個(gè)樣本組成的數(shù)據(jù)集,每個(gè)樣本有兩個(gè)特征。
import numpy as np
X = np.random.uniform(0.0, 1.0, size=(10, 2))
y = np.random.choice(('Male', 'Female'), size=(10))
print('X:', X)
print('y:', y)
X: [[0.48463048 0.21682675]
[0.27987595 0.28061459]
[0.13723177 0.45159025]
[0.42727284 0.99834867]
[0.61113219 0.31892401]
[0.14985227 0.71565914]
[0.048201 0.49254257]
[0.54466226 0.8419817 ]
[0.94426201 0.78924785]
[0.36877342 0.53250431]]
y: ['Female' 'Female' 'Male' 'Female' 'Female' 'Female' 'Male' 'Male'
'Female' 'Male']
1. 使用LabelEncoder類
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
yt = le.fit_transform(y)
print(y)
print(yt)
print(le.classes_)
['Female' 'Female' 'Male' 'Female' 'Female' 'Female' 'Male' 'Male'
'Female' 'Male']
[0 0 1 0 0 0 1 1 0 1]
['Female' 'Male']
獲得逆變換的方法很簡(jiǎn)單:
output = [1, 0, 1, 1, 0, 0]
decoded_output = [le.classes_[i] for i in output]
print(decoded_output)
['Male', 'Female', 'Male', 'Male', 'Female', 'Female']
這種方法很簡(jiǎn)單,但是有個(gè)缺點(diǎn):所有的標(biāo)簽都變成了數(shù)字,然后使用真實(shí)值的分類器會(huì)根據(jù)其距離考慮相似的數(shù)字,而忽略其代表的分類含義。因此我們通常優(yōu)先選擇獨(dú)熱編碼(one-hot encoding,又稱一次有效編碼),將數(shù)據(jù)二進(jìn)制化。
2. 使用LabelBinarizer類
from sklearn.preprocessing import LabelBinarizer
lb = LabelBinarizer()
yb = lb.fit_transform(y)
print(y)
print(yb)
print(lb.inverse_transform(yb))
['Female' 'Female' 'Male' 'Female' 'Female' 'Female' 'Male' 'Male'
'Female' 'Male']
[[0]
[0]
[1]
[0]
[0]
[0]
[1]
[1]
[0]
[1]]
['Female' 'Female' 'Male' 'Female' 'Female' 'Female' 'Male' 'Male'
'Female' 'Male']
可以看到,這里我們可以使用LabelBinarizer類的inverse_transform方法進(jìn)行逆轉(zhuǎn)化。
當(dāng)存在多個(gè)標(biāo)簽時(shí),這種方法會(huì)將其中一個(gè)標(biāo)簽變換為1,其余標(biāo)簽全部為0。這可能會(huì)導(dǎo)致的問(wèn)題顯而易見(jiàn),也就是我們將多分類問(wèn)題轉(zhuǎn)換成了二分類問(wèn)題。
四、管理缺失特征
我們可能會(huì)經(jīng)常碰見(jiàn)數(shù)據(jù)缺失的情況,有以下選項(xiàng)可以解決該問(wèn)題:
- 刪除整行:這個(gè)選項(xiàng)比較激進(jìn),一般只有當(dāng)數(shù)據(jù)集足夠大、缺少的特征值數(shù)量很多而且預(yù)測(cè)風(fēng)險(xiǎn)大時(shí)才會(huì)選擇;
- 創(chuàng)建子模型來(lái)預(yù)測(cè)這些特征值:第二個(gè)選項(xiàng)實(shí)現(xiàn)起來(lái)比較困難,因?yàn)樾枰_定一個(gè)監(jiān)督策略來(lái)訓(xùn)練每個(gè)特征的模型,最后預(yù)測(cè)它們的值;
- 使用自動(dòng)策略根據(jù)其他已知值插入這些缺失的特征值:考慮到以上的利弊,這可能是最好的選項(xiàng)了。
from sklearn.preprocessing import Imputer
data = np.array([[1, np.nan, 2],
[2, 3, np.nan],
[-1, 4, 2]])
# 插入均值
imp = Imputer(strategy='mean')
print('Mean:\n', imp.fit_transform(data))
# 插入中位數(shù)
imp = Imputer(strategy='median')
print('Median:\n', imp.fit_transform(data))
# 插入眾數(shù)
imp = Imputer(strategy='most_frequent')
print('Mode:\n', imp.fit_transform(data))
Mean:
[[ 1. 3.5 2. ]
[ 2. 3. 2. ]
[-1. 4. 2. ]]
Median:
[[ 1. 3.5 2. ]
[ 2. 3. 2. ]
[-1. 4. 2. ]]
Mode:
[[ 1. 3. 2.]
[ 2. 3. 2.]
[-1. 4. 2.]]
五、數(shù)據(jù)縮放和歸一化
一般的數(shù)據(jù)集是由不同的值組成的,可以從不同的分布得到且具有不同的尺度,有時(shí)還會(huì)有異常值。當(dāng)不同特征的取值范圍差異過(guò)大時(shí),很可能會(huì)對(duì)模型產(chǎn)生不良影響。因此我們往往需要先規(guī)范數(shù)據(jù)集。
我們來(lái)對(duì)比一下原始數(shù)據(jù)集和經(jīng)過(guò)縮放和中心化的數(shù)據(jù)集:
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import load_iris
import seaborn as sns
import matplotlib.pyplot as plt
sns.set()
# 導(dǎo)入數(shù)據(jù)
iris = load_iris()
data = iris.data
# 繪制原始數(shù)據(jù)散點(diǎn)圖
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
sns.scatterplot(x=data[:, 0], y=data[:, 1], ax=axes[0])
# 數(shù)據(jù)歸一化
scaler = StandardScaler()
scaled_data = scaler.fit_transform(data)
# 繪制規(guī)范化數(shù)據(jù)散點(diǎn)圖
sns.scatterplot(x=scaled_data[:, 0], y=scaled_data[:, 1], ax=axes[1])
plt.setp(axes, xlim=[-2, 8], ylim=[-3, 5]);

可以看到,我們的數(shù)據(jù)分布形態(tài)沒(méi)有變化,但是數(shù)據(jù)的分布范圍卻變了。我們將數(shù)據(jù)轉(zhuǎn)化成了均值為0(幾乎為0),標(biāo)準(zhǔn)差為1的歸一化數(shù)據(jù)。
print('轉(zhuǎn)化前均值:\n', np.mean(data, axis=0))
print('轉(zhuǎn)化后均值:\n', np.mean(scaled_data, axis=0))
print('轉(zhuǎn)化前方差:\n', np.std(data, axis=0))
print('轉(zhuǎn)化后方差:\n', np.std(scaled_data, axis=0))
轉(zhuǎn)化前均值:
[5.84333333 3.054 3.75866667 1.19866667]
轉(zhuǎn)化后均值:
[-1.69031455e-15 -1.63702385e-15 -1.48251781e-15 -1.62314606e-15]
轉(zhuǎn)化前方差:
[0.82530129 0.43214658 1.75852918 0.76061262]
轉(zhuǎn)化后方差:
[1. 1. 1. 1.]
在數(shù)據(jù)縮放時(shí),我們還可以使用類RobustScaler對(duì)異常值進(jìn)行控制和選擇分位數(shù)范圍。
from sklearn.preprocessing import RobustScaler
# 轉(zhuǎn)化數(shù)據(jù)1
rb1 = RobustScaler(quantile_range=(15, 85))
scaled_data1 = rb1.fit_transform(data)
# 轉(zhuǎn)化數(shù)據(jù)2
rb2 = RobustScaler(quantile_range=(25, 75))
scaled_data2 = rb2.fit_transform(data)
# 轉(zhuǎn)化數(shù)據(jù)3
rb3 = RobustScaler(quantile_range=(30, 60))
scaled_data3 = rb3.fit_transform(data)
# 繪制散點(diǎn)圖
fig, axes = plt.subplots(2, 2, figsize=(10, 10))
sns.scatterplot(x=data[:, 0], y=data[:, 1], ax=axes[0, 0])
sns.scatterplot(x=scaled_data1[:, 0], y=scaled_data1[:, 1], ax=axes[0, 1])
sns.scatterplot(x=scaled_data2[:, 0], y=scaled_data2[:, 1], ax=axes[1, 0])
sns.scatterplot(x=scaled_data3[:, 0], y=scaled_data3[:, 1], ax=axes[1, 1])
plt.setp(axes, ylim=[-4, 5], xlim=[-2, 8]);

可以看到,數(shù)據(jù)的大致分布形態(tài)仍然很接近,但是數(shù)據(jù)的分布范圍簡(jiǎn)直大變樣。另外,由于我們?cè)O(shè)置了不同的分位數(shù)范圍,因此數(shù)據(jù)的樣本量也不太一樣。
常用的還有MinMaxScaler和MaxAbsScaler,前者通過(guò)刪除不屬于給定范圍的元素,后者則通過(guò)考慮使用最大絕對(duì)值來(lái)縮放數(shù)據(jù)。
scikit-learn還為每個(gè)樣本規(guī)范化提供了一個(gè)類:Normalizer。它可以對(duì)數(shù)據(jù)集的每個(gè)元素應(yīng)用Max、L1和L2范數(shù)。
- Max:每個(gè)值都除以數(shù)據(jù)集中的最大值;
- L1:每個(gè)值都除以數(shù)據(jù)集中所有值的絕對(duì)值之和;
- L2:每個(gè)值都除以數(shù)據(jù)集中所有值的平方和的平方根
我們來(lái)看一個(gè)例子。
from sklearn.preprocessing import Normalizer
# 生成數(shù)據(jù)
data = np.array([1, 2]).reshape(1, 2)
print('原始數(shù)據(jù):', data)
# Max
n_max = Normalizer(norm='max')
print('Max:', n_max.fit_transform(data))
# L1范數(shù)
n_l1 = Normalizer(norm='l1')
print('L1范數(shù):', n_l1.fit_transform(data))
# L2范數(shù)
n_l2 = Normalizer(norm='l2')
print('L2范數(shù):', n_l2.fit_transform(data))
原始數(shù)據(jù): [[1 2]]
Max: [[0.5 1. ]]
L1范數(shù): [[0.33333333 0.66666667]]
L2范數(shù): [[0.4472136 0.89442719]]
六、特征選擇和過(guò)濾
不是所有的特征都能提供足夠的信息的,甚至有些特征會(huì)對(duì)我們的模型訓(xùn)練產(chǎn)生障礙,因此在模型訓(xùn)練開(kāi)始前我們要對(duì)特征做出一定的選擇。
接下來(lái)我們使用SelectKBest方法結(jié)合F檢驗(yàn)來(lái)篩選回歸模型的特征。
from sklearn.feature_selection import SelectKBest, f_regression
from sklearn.datasets import load_boston
boston = load_boston()
print('Boston data shape: ', boston.data.shape)
selector = SelectKBest(f_regression)
X_new = selector.fit_transform(boston.data, boston.target)
print('Filtered Boston data shape:', X_new.shape)
print('F-Scores:', selector.scores_)
Boston data shape: (506, 13)
Filtered Boston data shape: (506, 10)
F-Scores: [ 88.15124178 75.2576423 153.95488314 15.97151242 112.59148028
471.84673988 83.47745922 33.57957033 85.91427767 141.76135658
175.10554288 63.05422911 601.61787111]
然后我們使用SelectPercentile結(jié)合卡方檢驗(yàn)來(lái)篩選分類模型的特征。
from sklearn.feature_selection import SelectPercentile, chi2
from sklearn.datasets import load_iris
iris = load_iris()
print('Boston data shape: ', iris.data.shape)
selector = SelectPercentile(chi2, percentile=15)
X_new = selector.fit_transform(iris.data, iris.target)
print('Filtered Boston data shape:', X_new.shape)
print('F-Scores:', selector.scores_)
Boston data shape: (150, 4)
Filtered Boston data shape: (150, 1)
F-Scores: [ 10.81782088 3.59449902 116.16984746 67.24482759]
在數(shù)據(jù)預(yù)處理時(shí),我們還經(jīng)常會(huì)采用主成分分析等方法來(lái)實(shí)現(xiàn)數(shù)據(jù)降維等目的,不過(guò)這一部分我們完全可以單獨(dú)拆出一個(gè)章節(jié)來(lái)講解,感興趣的朋友可以關(guān)注下后續(xù)的更新。