特徵工程是機器學習的第一步,涉及清理現有數據集、提高信噪比和降低維數的所有技術。大多數演算法對輸入數據有很強的假設,當使用原始數據集時,它們的性能可能會受到負面影響。

另外有些特徵之間高度相關,在其中一個特徵提供了足夠的信息之後,與之相關的其他特徵往往無法提供額外的信息。這時我們就需要了解如何減少特徵數量或者僅選擇最佳特徵。

一、scikit-learn數據集

scikit-learn提供了一些用於測試的內置數據集,這些數據集包含在sklearn.datasets中,每個數據集都包含了輸入集(特徵集)X和標籤(目標值)y。比如波士頓房價的數據集(用於回歸問題):

from sklearn.datasets import load_boston

boston = load_boston()
X = boston.data
y = boston.target

print(特徵集的shape:, X.shape)
print(目標集的shape:, y.shape)
特徵集的shape (506, 13)
目標集的shape (506,)

可以看到,這個數據集包含了506個樣本、13個特徵,以及1個目標值。

假如我們不想使用scikit-learn提供的數據集,那麼我們還可以使用scikit-learn提供的工具來手動創建特定的數據集。相關的方法有:

  • make_classification():用於創建適用於測試分類演算法的數據集;
  • make_regression():用於創建適用於測試回歸模型的數據集;
  • make_blobs():用於創建適用於測試聚類演算法的數據集。

二、創建訓練集和測試集

一般來說,我們要在正式應用我們訓練的模型前對它進行測試。因此我們需要將數據集分為訓練集和測試集,顧名思義,前者用於訓練模型參數,後者用於測試模型性能。在某些情況下,我們甚至還會再分出一個數據集作為交叉驗證集,這種處理方式適用於有多種模型可供選擇的情況。

數據集的分割有一些注意事項:首先,兩個數據集必須要能反映原始數據的分佈,否則在數據集失真的情況下得到的模型對於真實樣本的預測效力會比較差;其次,原始數據集必須在分割之前隨機混合,以避免連續元素之間的相關性。

在scikit-learn中,我們可以使用train_test_split()函數來快速實現數據集的分割。

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)

這裡前兩個位置參數分別是特徵集和目標集,test_size用於指定測試集大小佔整個數據集的比例,random_state則是指定一個隨機種子,這樣可以確保我們在重複試驗時數據不會發生變化(數據集都變了,那模型效果的變化就不知道該歸因於模型的優化還是歸因於數據集的變化了。)

三、管理分類數據

在許多分類問題中,目標數據集由各種類別標籤組成。但是很多演算法是不支持這種數據格式的,因此我們要對其進行必要的編碼。

假設我們有一個由10個樣本組成的數據集,每個樣本有兩個特徵。

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]

獲得逆變換的方法很簡單:

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]

這種方法很簡單,但是有個缺點:所有的標籤都變成了數字,然後使用真實值的分類器會根據其距離考慮相似的數字,而忽略其代表的分類含義。因此我們通常優先選擇獨熱編碼(one-hot encoding,又稱一次有效編碼),將數據二進位化。

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方法進行逆轉化。

當存在多個標籤時,這種方法會將其中一個標籤變換為1,其餘標籤全部為0。這可能會導致的問題顯而易見,也就是我們將多分類問題轉換成了二分類問題。

四、管理缺失特徵

我們可能會經常碰見數據缺失的情況,有以下選項可以解決該問題:

  • 刪除整行:這個選項比較激進,一般只有當數據集足夠大、缺少的特徵值數量很多而且預測風險大時才會選擇;
  • 創建子模型來預測這些特徵值:第二個選項實現起來比較困難,因為需要確定一個監督策略來訓練每個特徵的模型,最後預測它們的值;
  • 使用自動策略根據其他已知值插入這些缺失的特徵值:考慮到以上的利弊,這可能是最好的選項了。

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:
, imp.fit_transform(data))

# 插入中位數
imp = Imputer(strategy=median)
print(Median:
, imp.fit_transform(data))

# 插入眾數
imp = Imputer(strategy=most_frequent)
print(Mode:
, 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.]]

五、數據縮放和歸一化

一般的數據集是由不同的值組成的,可以從不同的分佈得到且具有不同的尺度,有時還會有異常值。當不同特徵的取值範圍差異過大時,很可能會對模型產生不良影響。因此我們往往需要先規範數據集。

我們來對比一下原始數據集和經過縮放和中心化的數據集:

from sklearn.preprocessing import StandardScaler
from sklearn.datasets import load_iris
import seaborn as sns
import matplotlib.pyplot as plt
sns.set()

# 導入數據
iris = load_iris()
data = iris.data

# 繪製原始數據散點圖
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
sns.scatterplot(x=data[:, 0], y=data[:, 1], ax=axes[0])

# 數據歸一化
scaler = StandardScaler()
scaled_data = scaler.fit_transform(data)

# 繪製規範化數據散點圖
sns.scatterplot(x=scaled_data[:, 0], y=scaled_data[:, 1], ax=axes[1])
plt.setp(axes, xlim=[-2, 8], ylim=[-3, 5]);

可以看到,我們的數據分佈形態沒有變化,但是數據的分佈範圍卻變了。我們將數據轉化成了均值為0(幾乎為0),標準差為1的歸一化數據。

print(轉化前均值:
, np.mean(data, axis=0))
print(轉化後均值:
, np.mean(scaled_data, axis=0))
print(轉化前方差:
, np.std(data, axis=0))
print(轉化後方差:
, np.std(scaled_data, axis=0))
轉化前均值:
[5.84333333 3.054 3.75866667 1.19866667]
轉化後均值:
[-1.69031455e-15 -1.63702385e-15 -1.48251781e-15 -1.62314606e-15]
轉化前方差:
[0.82530129 0.43214658 1.75852918 0.76061262]
轉化後方差:
[1. 1. 1. 1.]

在數據縮放時,我們還可以使用類RobustScaler對異常值進行控制和選擇分位數範圍。

from sklearn.preprocessing import RobustScaler

# 轉化數據1
rb1 = RobustScaler(quantile_range=(15, 85))
scaled_data1 = rb1.fit_transform(data)

# 轉化數據2
rb2 = RobustScaler(quantile_range=(25, 75))
scaled_data2 = rb2.fit_transform(data)

# 轉化數據3
rb3 = RobustScaler(quantile_range=(30, 60))
scaled_data3 = rb3.fit_transform(data)

# 繪製散點圖
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]);

可以看到,數據的大致分佈形態仍然很接近,但是數據的分佈範圍簡直大變樣。另外,由於我們設置了不同的分位數範圍,因此數據的樣本量也不太一樣。

常用的還有MinMaxScalerMaxAbsScaler,前者通過刪除不屬於給定範圍的元素,後者則通過考慮使用最大絕對值來縮放數據。

scikit-learn還為每個樣本規範化提供了一個類:Normalizer。它可以對數據集的每個元素應用Max、L1和L2範數。

  • Max:每個值都除以數據集中的最大值;
  • L1:每個值都除以數據集中所有值的絕對值之和;
  • L2:每個值都除以數據集中所有值的平方和的平方根

我們來看一個例子。

from sklearn.preprocessing import Normalizer

# 生成數據
data = np.array([1, 2]).reshape(1, 2)
print(原始數據:, data)

# Max
n_max = Normalizer(norm=max)
print(Max:, n_max.fit_transform(data))

# L1範數
n_l1 = Normalizer(norm=l1)
print(L1範數:, n_l1.fit_transform(data))

# L2範數
n_l2 = Normalizer(norm=l2)
print(L2範數:, n_l2.fit_transform(data))
原始數據: [[1 2]]
Max [[0.5 1. ]]
L1範數 [[0.33333333 0.66666667]]
L2範數 [[0.4472136 0.89442719]]

六、特徵選擇和過濾

不是所有的特徵都能提供足夠的信息的,甚至有些特徵會對我們的模型訓練產生障礙,因此在模型訓練開始前我們要對特徵做出一定的選擇。

接下來我們使用SelectKBest方法結合F檢驗來篩選回歸模型的特徵。

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結合卡方檢驗來篩選分類模型的特徵。

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]

在數據預處理時,我們還經常會採用主成分分析等方法來實現數據降維等目的,不過這一部分我們完全可以單獨拆出一個章節來講解,感興趣的朋友可以關注下後續的更新。


推薦閱讀:
相關文章