sklearn(4)—— 數據預處理與特徵工程

1 概述

1.1 數據預處理與特徵工程

想象一下未來美好的一天,成爲一個精通各種算法和調參調庫的數據挖掘工程師了。某一天你從你的同事,一位藥物研究人員那裏,得到了一份病人臨牀表現的數據。藥物研究人員用前四列數據預測一下最後一數據,還說他要出差幾天,可能沒辦法和你一起研究數據了,希望出差回來以後,可以有個初步分析結果。於是你就看了看數據,看着很普通,預測連續型變量,好說,導隨機森林迴歸器調出來,調參調呀調,MSE很小,跑了個還不錯的結果。

幾天後,你同事出差回來了,準備要一起開會了,會上你碰見了和你同事在同一個項目裏工作的統計學家。他問起你的分析結果,你說你已經小有成效了,統計學家很喫驚,他說:“不錯呀,這組數據問題太多,我都分析不出什麼來。”

你心裏可能咯噔一下,忐忑地回答說:“我沒聽說數據有什麼問題呀。”

於是統計學家說:“誒?沒人告訴你說,最後一列數據如果取個對數,結果會更好嗎?”

你內心毫無波動:“沒。”

統計學家:“誒那你肯定聽說了第四列數據有點問題吧,這個特徵的取值範圍是1~10,0是表示缺失值的。而且他們輸入數據的時候出錯,很多10都被錄入成0了,現在分不出來了。”

你:”…“

統計學家:”還有第二列和第三列數據基本是一樣的,相關性太強了。“

你:”這個我發現了,不過這兩個特徵在預測中的重要性都不高,無論其他特徵怎樣出錯,我這邊結果裏顯示第一列的特徵是最重要的,所以也無所謂啦。“

統計學家:“啥?第一列不就是編號嗎?”

你:“不是吧。”

統計學家:“哦我想起來了!第一列就是編號,不過那個編號是我們根據第五列排序之後編上去的!這個第一列和第五列是由很強的聯繫,但是毫無意義啊!”

老血噴了一屏幕,數據挖掘工程師卒。

這個悲慘又可愛的故事來自《數據挖掘導論》,雖然這是故事裏的狀況十分極端,但我還是想把這段對話作爲今天這章的開頭,博大家一笑(雖然可能聽完就淚流滿面了)。在過去兩週,我們已經講了兩個算法:決策樹和隨機森林,我們通過決策樹帶大家認識了sklearn,通過隨機森林講解了機器學習中調參的基本思想,現在可以說,只要上過前面兩堂課的,人人都會調隨機森林和決策樹的分類器了,而我呢,也只需要跟着各大機器學習書籍的步伐,給大家一週一個算法帶着講解就是了。如果這樣的話,結果可能就是,大家去工作了,遇到了一個不那麼靠譜的同事,給了你一組有坑的數據,最後你就一屏幕老血吐過去,犧牲在數據行業的前線了。

數據不給力,再高級的算法都沒有用。

我們的課堂/教材中的數據,都是經過層層篩選,適用於課堂教學的——運行時間短,預測效果好,沒有嚴重缺失等等問題。尤其是sklearn中的數據,堪稱完美。各大機器學習教材也是如此,都給大家提供處理好的數據,這就導致,很多人在學了很多算法之後,到了現實應用之中,發現模型經常就調不動了,因爲現實中的數據,離平時上課使用的完美數據集,相差十萬八千里。所以我決定,少講一兩個簡單的算法,爲大家專門拿一堂課來講解建模之前的流程,數據預處理和特徵工程。這樣大家即可以學到數據挖掘過程中很重要但是卻經常被忽視的一些步驟,也可以不受課堂的限制,如果自己有時間,可以嘗試在真實數據上建模。

數據挖掘的五大流程:

  1. 獲取數據
  2. 數據預處理
    數據預處理是從數據中檢測,糾正或刪除損壞,不準確或不適用於模型的記錄的過程
    可能面對的問題有:數據類型不同,比如有的是文字,有的是數字,有的含時間序列,有的連續,有的間斷。
    也可能,數據的質量不行,有噪聲,有異常,有缺失,數據出錯,量綱不一,有重複,數據是偏態,數據量太
    大或太小
    數據預處理的目的:讓數據適應模型,匹配模型的需求
  3. 特徵工程:
    特徵工程是將原始數據轉換爲更能代表預測模型的潛在問題的特徵的過程,可以通過挑選最相關的特徵,提取
    特徵以及創造特徵來實現。其中創造特徵又經常以降維算法的方式實現。
    可能面對的問題有:特徵之間有相關性,特徵和標籤無關,特徵太多或太小,或者乾脆就無法表現出應有的數
    據現象或無法展示數據的真實面貌
    特徵工程的目的:1) 降低計算成本,2) 提升模型上限
  4. 建模,測試模型並預測出結果
  5. 上線,驗證模型效果

1.2 sklearn中的數據預處理和特徵工程

sklearn中包含衆多數據預處理和特徵工程相關的模塊,雖然剛接觸sklearn時,大家都會爲其中包含的各種算法的廣度深度所震驚,但其實sklearn六大板塊中有兩塊都是關於數據預處理和特徵工程的,兩個板塊互相交互,爲建模之前的全部工程打下基礎。

在這裏插入圖片描述

  • 模塊preprocessing:幾乎包含數據預處理的所有內容
  • 模塊Impute:填補缺失值專用
  • 模塊feature_selection:包含特徵選擇的各種方法的實踐
  • 模塊decomposition:包含降維算法

2 數據預處理 Preprocessing & Impute

2.1 數據無量綱化

在機器學習算法實踐中,我們往往有着將不同規格的數據轉換到同一規格,或不同分佈的數據轉換到某個特定分佈的需求,這種需求統稱爲將數據“無量綱化”。譬如梯度和矩陣爲核心的算法中,譬如邏輯迴歸,支持向量機,神經網絡,無量綱化可以加快求解速度;而在距離類模型,譬如K近鄰,K-Means聚類中,無量綱化可以幫我們提升模型精度,避免某一個取值範圍特別大的特徵對距離計算造成影響。(一個特例是決策樹和樹的集成算法們,對決策樹我們不需要無量綱化,決策樹可以把任意數據都處理得很好。)

數據的無量綱化可以是線性的,也可以是非線性的。線性的無量綱化包括中心化處理(Zero-centered或者Meansubtraction)和縮放處理(Scale)。中心化的本質是讓所有記錄減去一個固定值,即讓數據樣本數據平移到某個位置。縮放的本質是通過除以一個固定值,將數據固定在某個範圍之中,取對數也算是一種縮放處理。

  • preprocessing.MinMaxScaler

當數據(x)按照最小值中心化後,再按極差(最大值 - 最小值)縮放,數據移動了最小值個單位,並且會被收斂到[0,1]之間,而這個過程,就叫做數據歸一化(Normalization,又稱Min-Max Scaling)。注意,Normalization是歸一化,不是正則化真正的正則化是regularization,不是數據預處理的一種手段。歸一化之後的數據服從正態分佈,公式如下:
在這裏插入圖片描述
在sklearn當中,我們使用preprocessing.MinMaxScaler來實現這個功能。MinMaxScaler有一個重要參數,feature_range,控制我們希望把數據壓縮到的範圍,默認是[0,1]。

from sklearn.preprocessing import MinMaxScaler
data = [[-1, 2], [-0.5, 6], [0, 10], [1, 18]]
#不太熟悉numpy的小夥伴,能夠判斷data的結構嗎?
#如果換成表是什麼樣子?
import pandas as pd
pd.DataFrame(data)
'''
輸出
	 0 	    1
0 	-1.0 	2
1 	-0.5 	6
2 	0.0 	10
3 	1.0 	18
'''

#實現歸一化
scaler = MinMaxScaler() #實例化
scaler = scaler.fit(data) #fit,在這裏本質是生成min(x)和max(x)
result = scaler.transform(data) #通過接口導出結果
result
'''
array([[0.  , 0.  ],
       [0.25, 0.25],
       [0.5 , 0.5 ],
       [1.  , 1.  ]])
'''
result_ = scaler.fit_transform(data) #訓練和導出結果一步達成
result
'''
array([[0.  , 0.  ],
       [0.25, 0.25],
       [0.5 , 0.5 ],
       [1.  , 1.  ]])
'''
scaler.inverse_transform(result) #將歸一化後的結果逆轉
'''
array([[-1. ,  2. ],
       [-0.5,  6. ],
       [ 0. , 10. ],
       [ 1. , 18. ]])
'''
#使用MinMaxScaler的參數feature_range實現將數據歸一化到[0,1]以外的範圍中
data = [[-1, 2], [-0.5, 6], [0, 10], [1, 18]]
scaler = MinMaxScaler(feature_range=[5,10]) #依然實例化
result = scaler.fit_transform(data) #fit_transform一步導出結果
result
'''
array([[ 5.  ,  5.  ],
       [ 6.25,  6.25],
       [ 7.5 ,  7.5 ],
       [10.  , 10.  ]])
'''
#當X中的特徵數量非常多的時候,fit會報錯並表示,數據量太大了我計算不了
#此時使用partial_fit作爲訓練接口
#scaler = scaler.partial_fit(data)

BONUS: 使用numpy來實現歸一化

import numpy as np
X = np.array([[-1, 2], [-0.5, 6], [0, 10], [1, 18]])
#歸一化
X_nor = (X - X.min(axis=0)) / (X.max(axis=0) - X.min(axis=0))
X_nor
'''
array([[0.  , 0.  ],
       [0.25, 0.25],
       [0.5 , 0.5 ],
       [1.  , 1.  ]])
'''
#逆轉歸一化
X_returned = X_nor * (X.max(axis=0) - X.min(axis=0)) + X.min(axis=0)
X_returned
'''
array([[-1. ,  2. ],
       [-0.5,  6. ],
       [ 0. , 10. ],
       [ 1. , 18. ]])
'''

代碼解析:

X.min() # 返回所有數值中的最小值
'''
-1.0
'''
X.min(axis=0) # 返回列的最小值(跨列計算)
'''
array([-1.,  2.])
'''
X.min(axis=1) # 返回行的最小值(跨行計算)
'''
array([-1. , -0.5,  0. ,  1. ])
'''
  • preprocessing.StandardScaler

當數據(x)按均值(μ)中心化後,再按標準差(σ)縮放,數據就會服從爲均值爲0,方差爲1的正態分佈(即標準正態分佈),而這個過程,就叫做數據標準化(Standardization,又稱Z-score normalization),公式如下:
在這裏插入圖片描述

from sklearn.preprocessing import StandardScaler
data = [[-1, 2], [-0.5, 6], [0, 10], [1, 18]]
scaler = StandardScaler() #實例化
scaler.fit(data) #fit,本質是生成均值和方差
'''
StandardScaler(copy=True, with_mean=True, with_std=True)
'''
scaler.mean_ #查看均值的屬性mean_
'''
array([-0.125,  9.   ])
'''
scaler.var_ #查看方差的屬性var_
'''
array([ 0.546875, 35.      ])
'''
x_std = scaler.transform(data) #通過接口導出結果
x_std
'''
array([[-1.18321596, -1.18321596],
       [-0.50709255, -0.50709255],
       [ 0.16903085,  0.16903085],
       [ 1.52127766,  1.52127766]])     
'''
x_std.mean() #導出的結果是一個數組,用mean()查看均值
'''
0.0
'''
x_std.std() #用std()查看方差
'''
1.0
'''
scaler.fit_transform(data) #使用fit_transform(data)一步達成結果
'''
array([[-1.18321596, -1.18321596],
       [-0.50709255, -0.50709255],
       [ 0.16903085,  0.16903085],
       [ 1.52127766,  1.52127766]])
'''
scaler.inverse_transform(x_std) #使用inverse_transform逆轉標準化
'''
array([[-1. ,  2. ],
       [-0.5,  6. ],
       [ 0. , 10. ],
       [ 1. , 18. ]])
'''

對於StandardScaler和MinMaxScaler來說,空值NaN會被當做是缺失值,在fit的時候忽略,在transform的時候保持缺失NaN的狀態顯示。並且,儘管去量綱化過程不是具體的算法,但在fit接口中,依然只允許導入至少二維數組,一維數組導入會報錯。通常來說,我們輸入的X會是我們的特徵矩陣,現實案例中特徵矩陣不太可能是一維所以不會存在這個問題。

  • StandardScaler和MinMaxScaler選哪個?

看情況。大多數機器學習算法中,會選擇StandardScaler來進行特徵縮放,因爲MinMaxScaler對異常值非常敏感。在PCA,聚類,邏輯迴歸,支持向量機,神經網絡這些算法中,StandardScaler往往是最好的選擇。

MinMaxScaler在不涉及距離度量、梯度、協方差計算以及數據需要被壓縮到特定區間時使用廣泛,比如數字圖像處理中量化像素強度時,都會使用MinMaxScaler將數據壓縮於[0,1]區間之中。
建議先試試看StandardScaler,效果不好換MinMaxScaler。

除了StandardScaler和MinMaxScaler之外,sklearn中也提供了各種其他縮放處理(中心化只需要一個pandas廣播一下減去某個數就好了,因此sklearn不提供任何中心化功能)。比如,在希望壓縮數據,卻不影響數據的稀疏性時(不影響矩陣中取值爲0的個數時),我們會使用MaxAbsScaler;在異常值多,噪聲非常大時,我們可能會選用分位數來無量綱化,此時使用RobustScaler。更多詳情請參考以下列表。

無量綱化 功能 中心化 縮放 詳解
.StandardScaler 標準化 均值 方差 通過減掉均值並將數據縮放到單位方差來標準化特徵標準化完畢後的特徵服從標準正態分佈,即方差爲1,均值爲0
.MinMaxScaler 歸一化 最小值 極差 通過最大值最小值將每個特徵縮放到給定範圍,默認[0,1]
.MaxAbsScaler 縮放 N/A 最大值 通過讓每一個特徵裏的數據,除以該特徵中絕對值最大的數值的絕對值,將數據壓縮到[-1,1]之間,這種做法並沒有中心化數據,因此不會破壞數據的稀疏性。數據的稀疏性是指,數據中包含0的比例,0越多,數據越稀疏。
.Robustscaler 無量綱化 中位數 四分位數範圍 使用可以處理異常值,對異常值不敏感的統計量來縮放數據。
這個縮放器刪除中位數並根據百分位數範圍( RQ:Interquartile Range)縮放數據。IQR是第一分位數(25%)和第三數範圍分維數(75%)之間的範圍。數據集的標準化是通過去除均值,縮放到單位方差來完成,但是異常值通常會對樣本的均值和方差造成負面影響,當異常值很多噪音很大時,用中位數和四分位數範圍通常會產生更好的效果。
.Normalizer 無量綱化 N/A sklearn中末明確,依範數原理應當是:
I1:樣本向量的長度/樣本中每個元素絕對值的和
I2:樣本向量的長度/樣本中每個元素的歐氏距離
將樣本獨立縮放到到單位範數。每個至少帶個非0值的樣本都回被獨立縮放,使得整個樣本(整個向量)的的長度都爲I1範數或I2範數。這個類可以處理密集數組( numpy arrays或 scipy中的稀疏矩陣( scipysparse),如果你希望避免複製/轉換過程中的負擔,請使用CSR格式的矩陣。
將輸入的數據縮放到單位範數是文本分類或聚類中的常見操作。例如,兩個正則化後的TF-IDF向量的點積是向量的餘弦相似度,並且是信息檢索社區常用的向量空間模型的基本桕似性度量。
使用參數norm來確定要正則化的範數方向,可以選擇"I1"I2以及"max"三種選項,默認I2範數這個評估器的fit接口什麼也不做,但在管道中使用依然是很有用的。
.Power
Transformer
非線性
無量綱化
N/A N/A 應用特徵功率變換使數據更接近正態分佈。
功率變換是一系列參數單調變換,用於使數據更像高斯。這對於建模與異方差性(非常數方差)或其他需要正態性的情況相關的問題非常有用。要求輸入的數據嚴格爲正, power_transform0通過最大似然估計來穩定方差和並確定最小化偏度的最佳參數。
默認情況下,標準化應用於轉換後的數據
.Quantile
Transformer
非線性
無量綱化
N/A N/A 使用百分位數轉換特徵,通過縮小邊緣異常值和非異常值之間的距離來提供特徵的非線性變換。可以使用參數output_distribution=" normal"來將數據映射到標準正態分佈。
.KernelCenterer 中心化 均值 N/A 將核矩陣中心化。設K(x,2)是由ph(x)^Tphi(定義的核,其中phi是將x映射到希爾伯特空間的函數。
KernelCentere在不明確計算phi(x)的情況下讓數據中心化爲0均值。它相當於使用sklearn.preprocessing. StandardScaler(with std= False)來將phi(x)中心化

2.2 缺失值

機器學習和數據挖掘中所使用的數據,永遠不可能是完美的。很多特徵,對於分析和建模來說意義非凡,但對於實際收集數據的人卻不是如此,因此數據挖掘之中,常常會有重要的字段缺失值很多,但又不能捨棄字段的情況。因此,數據預處理中非常重要的一項就是處理缺失值。

import pandas as pd
data = pd.read_csv(r"C:\Users\ASUS\sklearn\Narrativedata.csv"
                   ,index_col=0
                  )
data.head()

在這裏插入圖片描述

在這裏,我們使用從泰坦尼克號提取出來的數據,這個數據有三個特徵,一個數值型,兩個字符型,標籤也是字符型。從這裏開始,我們就使用這個數據給大家作爲例子,讓大家慢慢熟悉sklearn中數據預處理的各種方式。

  • impute.SimpleImputer
    class sklearn.impute.SimpleImputer (missing_values=nan, strategy=’mean’, fill_value=None, verbose=0,copy=True)

在講解隨機森林的案例時,我們用這個類和隨機森林迴歸填補了缺失值,對比了不同的缺失值填補方式對數據的影響。這個類是專門用來填補缺失值的。它包括四個重要參數:

參數 含義&輸入
missing_values 告訴 Simplelmputer,數據中的缺失值長什麼樣,默認空值 np.nan
strategy 我們填補缺失值的策略,默認均值。
輸入"mean"使用均值填補(僅對數值型特徵可用)
輸入" median"用中值填補(僅對數值型特徵可用)
輸入" most_frequent"用衆數填補(對數值型和字符型特徵都可用)
輸入“ constant"表示請參考參數“ fill_value"中的值(對數值型和字符型特徵都可用)
fill_value 當參數 startegy爲" constant"的時候可用,可輸入字符串或數字表示要填充的值,常用0
copy 默認爲True,將創建特徵矩陣的副本,反之則會將缺失值填補到原本的特徵矩陣中去。
data.info()
'''
<class 'pandas.core.frame.DataFrame'>
Int64Index: 891 entries, 0 to 890
Data columns (total 4 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   Age       714 non-null    float64
 1   Sex       891 non-null    object 
 2   Embarked  889 non-null    object 
 3   Survived  891 non-null    object 
dtypes: float64(1), object(3)
memory usage: 34.8+ KB
'''
#填補年齡
Age = data.loc[:,"Age"].values.reshape(-1,1) #sklearn當中特徵矩陣必須是二維
Age[:20]
'''
array([[22.],
       [38.],
       [26.],
       [35.],
       [35.],
       [nan],
       [54.],
       [ 2.],
       [27.],
       [14.],
       [ 4.],
       [58.],
       [20.],
       [39.],
       [14.],
       [55.],
       [ 2.],
       [nan],
       [31.],
       [nan]])
'''
from sklearn.impute import SimpleImputer
imp_mean = SimpleImputer() #實例化,默認均值填補
imp_median = SimpleImputer(strategy="median") #用中位數填補
imp_0 = SimpleImputer(strategy="constant",fill_value=0) #用0填補

imp_mean = imp_mean.fit_transform(Age) #fit_transform一步完成調取結果
imp_median = imp_median.fit_transform(Age)
imp_0 = imp_0.fit_transform(Age)

imp_mean[:20]
'''
array([[22.        ],
       [38.        ],
       [26.        ],
       [35.        ],
       [35.        ],
       [29.69911765],
       [54.        ],
       [ 2.        ],
       [27.        ],
       [14.        ],
       [ 4.        ],
       [58.        ],
       [20.        ],
       [39.        ],
       [14.        ],
       [55.        ],
       [ 2.        ],
       [29.69911765],
       [31.        ],
       [29.69911765]])
'''
imp_median[:20]
'''
array([[22.],
       [38.],
       [26.],
       [35.],
       [35.],
       [28.],
       [54.],
       [ 2.],
       [27.],
       [14.],
       [ 4.],
       [58.],
       [20.],
       [39.],
       [14.],
       [55.],
       [ 2.],
       [28.],
       [31.],
       [28.]])
'''
imp_0[:20]
'''
array([[22.],
       [38.],
       [26.],
       [35.],
       [35.],
       [ 0.],
       [54.],
       [ 2.],
       [27.],
       [14.],
       [ 4.],
       [58.],
       [20.],
       [39.],
       [14.],
       [55.],
       [ 2.],
       [ 0.],
       [31.],
       [ 0.]])
'''
#在這裏我們使用中位數填補Age
data.loc[:,"Age"] = imp_median
data.info()
'''
<class 'pandas.core.frame.DataFrame'>
Int64Index: 891 entries, 0 to 890
Data columns (total 4 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   Age       891 non-null    float64
 1   Sex       891 non-null    object 
 2   Embarked  889 non-null    object 
 3   Survived  891 non-null    object 
dtypes: float64(1), object(3)
memory usage: 34.8+ KB
'''
#使用衆數填補Embarked
Embarked = data.loc[:,"Embarked"].values.reshape(-1,1)
imp_mode = SimpleImputer(strategy = "most_frequent") # 使用衆數填寫
data.loc[:,"Embarked"] = imp_mode.fit_transform(Embarked)
data.info()
'''
<class 'pandas.core.frame.DataFrame'>
Int64Index: 891 entries, 0 to 890
Data columns (total 4 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   Age       891 non-null    float64
 1   Sex       891 non-null    object 
 2   Embarked  891 non-null    object 
 3   Survived  891 non-null    object 
dtypes: float64(1), object(3)
memory usage: 34.8+ KB
'''

data.loc[:,"Age"]
'''
0      22.0
1      38.0
2      26.0
3      35.0
4      35.0
       ... 
886    27.0
887    19.0
888     NaN
889    26.0
890    32.0
Name: Age, Length: 891, dtype: float64
'''
data.loc[:,"Age"].values # 一維數組
'''
array([22.  , 38.  , 26.  , 35.  , 35.  ,   nan, 54.  ,  2.  , 27.  ,
       14.  ,  4.  , 58.  , 20.  , 39.  , 14.  , 55.  ,  2.  ,   nan,
       15.  ,   nan, 35.  , 34.  , 15.  , 28.  ,  8.  , 38.  ,   nan,
       16.  ,   nan,   nan, 40.  ,   nan,   nan, 66.  , 28.  , 42.  ,
       17. ......])
'''
data.loc[:,"Age"].values.reshape(-1,1) # 升維 二維數組
'''
array([[22.  ],
       [38.  ],
       [26.  ],
       [35.  ],
       [35.  ],
       [  nan],
       [54.  ],
       [ 2.  ],
       [27.  ],
       [14.  ],
       ....])
'''

BONUS:用Pandas和Numpy進行填補其實更加簡單

import pandas as pd
data = pd.read_csv(r"C:\Users\ASUS\sklearn\Narrativedata.csv",index_col=0)
data.head()
'''
 	Age 	Sex 	Embarked 	Survived
0 	22.0 	male 		S 		No
1 	38.0 	female 		C 		Yes
2 	26.0 	female 		S 		Yes
3 	35.0 	female 		S 		Yes
4 	35.0 	male 		S 		No
'''
data.loc[:,"Age"] = data.loc[:,"Age"].fillna(data.loc[:,"Age"].median()) # 中位數填補
#.fillna 在DataFrame裏面直接進行填補
data.dropna(axis=0,inplace=True)
data.info()
'''
<class 'pandas.core.frame.DataFrame'>
Int64Index: 889 entries, 0 to 890
Data columns (total 4 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   Age       889 non-null    float64
 1   Sex       889 non-null    object 
 2   Embarked  889 non-null    object 
 3   Survived  889 non-null    object 
dtypes: float64(1), object(3)
memory usage: 34.7+ KB
'''
#.dropna(axis=0)刪除所有有缺失值的行,.dropna(axis=1)刪除所有有缺失值的列
#參數inplace,爲True表示在原數據集上進行修改,爲False表示生成一個複製對象,不修改原數據,默認False

2.3 處理分類型特徵:編碼與啞變量

在機器學習中,大多數算法,譬如邏輯迴歸,支持向量機SVM,k近鄰算法等都只能夠處理數值型數據,不能處理文字,在sklearn當中,除了專用來處理文字的算法,其他算法在fit的時候全部要求輸入數組或矩陣,也不能夠導入文字型數據(其實手寫決策樹和普斯貝葉斯可以處理文字,但是sklearn中規定必須導入數值型)。然而在現實中,許多標籤和特徵在數據收集完畢的時候,都不是以數字來表現的。比如說,學歷的取值可以是[“小學”,“初中”,“高中”,“大學”],付費方式可能包含[“支付寶”,“現金”,“微信”]等等。在這種情況下,爲了讓數據適應算法和庫,我們必須將數據進行編碼,即是說,將文字型數據轉換爲數值型。

  • preprocessing.LabelEncoder:標籤專用,能夠將分類轉換爲分類數值
from sklearn.preprocessing import LabelEncoder
y = data.iloc[:,-1] #要輸入的是標籤,不是特徵矩陣,所以允許一維
le = LabelEncoder() #實例化
le = le.fit(y) #導入數據
label = le.transform(y) #transform接口調取結果
label #查看獲取的結果label
'''
array([0, 2, 2, 2, 0, 0, 0, 0, 2, 2, 1, 2, 0, 0, 0, 1, 0, 2, 0, 2, 1, 2,
       2, 2, 0, 1, 0, 0, 2, 0, 0, 2, 2, 0, 0, 0, 2, 0, 0, 2, 0, 0, 0, 1,
       2, 0, 0, 2, 0, 0, 0, 0, 2, 2, 0, 2, 2, 0, 2, 0, 0, 0, 0, 0, 2, 2,
       0, 2, 0, 0, 0, 0, 0, 2, 1, 0, 1, 2, 2, 0, 2, 2, 0, 2, 2, 0, 0, 2,
       0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 2, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 2, 2, 0, 1, 0, 0,
       2, 0, 0, 2, 0, 0, 0, 1, 1, 2, 0, 0, 0, 2, 0, 0, 1, 0, 1, 1, 0, 0,
       0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 2, 2, 0, 0, 0, 1, 0, 2, 0, 0, 0, 1,
       ....])
'''
le.classes_ #屬性.classes_查看標籤中究竟有多少類別
'''
array(['No', 'Unknown', 'Yes'], dtype=object)
'''
le.fit_transform(y) #也可以直接fit_transform一步到位
'''
array([0, 2, 2, 2, 0, 0, 0, 0, 2, 2, 1, 2, 0, 0, 0, 1, 0, 2, 0, 2, 1, 2,
       2, 2, 0, 1, 0, 0, 2, 0, 0, 2, 2, 0, 0, 0, 2, 0, 0, 2, 0, 0, 0, 1,
       2, 0, 0, 2, 0, 0, 0, 0, 2, 2, 0, 2, 2, 0, 2, 0, 0, 0, 0, 0, 2, 2,
       0, 2, 0, 0, 0, 0, 0, 2, 1, 0, 1, 2, 2, 0, 2, 2, 0, 2, 2, 0, 0, 2,
       0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 2, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 2, 2, 0, 1, 0, 0,
       2, 0, 0, 2, 0, 0, 0, 1, 1, 2, 0, 0, 0, 2, 0, 0, 1, 0, 1, 1, 0, 0,
       0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 2, 2, 0, 0,....])
'''
le.inverse_transform(label) #使用inverse_transform可以逆轉
'''
array(['No', 'Yes', 'Yes', 'Yes', 'No', 'No', 'No', 'No', 'Yes', 'Yes',
       'Unknown', 'Yes', 'No', 'No', 'No', 'Unknown', 'No', 'Yes', 'No',
       'Yes', 'Unknown', 'Yes', 'Yes', 'Yes', 'No', 'Unknown', 'No', 'No',
       'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'No',
       'No', 'Yes', 'No', 'No', 'No', 'Unknown', 'Yes', 'No', 'No', 'Yes',
       'No', 'No', 'No', 'No', 'Yes', 'Yes', 'No', 'Yes', 'Yes', 'No',
       'Yes', 'No', 'No', 'No', 'No', 'No', 'Yes', 'Yes', 'No', 'Yes',
       'No', 'No', 'No', 'No', 'No', 'Yes', 'Unknown', 'No',...])
'''
data.iloc[:,-1] = label #讓標籤等於我們運行出來的結果
data.head()
'''

	Age 	Sex 	Embarked 	Survived
0 	22.0 	male 		S 		0
1 	38.0 	female 		C 		2
2 	26.0 	female 		S 		2
3 	35.0 	female 		S 		2
4 	35.0 	male 		S 		0
'''
#如果不需要教學展示的話我會這麼寫:
from sklearn.preprocessing import LabelEncoder
data.iloc[:,-1] = LabelEncoder().fit_transform(data.iloc[:,-1])
data.head()
'''

	Age 	Sex 	Embarked 	Survived
0 	22.0 	male 		S 		0
1 	38.0 	female 		C 		2
2 	26.0 	female 		S 		2
3 	35.0 	female 		S 		2
4 	35.0 	male 		S 		0
'''
  • preprocessing.OrdinalEncoder:特徵專用,能夠將分類特徵轉換爲分類數值
from sklearn.preprocessing import OrdinalEncoder
#接口categories_對應LabelEncoder的接口classes_,一模一樣的功能
data_ = data.copy()
data_.head()
'''

	Age 	Sex 	Embarked 	Survived
0 	22.0 	male 		S 		0
1 	38.0 	female 		C 		2
2 	26.0 	female 		S 		2
3 	35.0 	female 		S 		2
4 	35.0 	male 		S 		0
'''
OrdinalEncoder().fit(data_.iloc[:,1:-1]).categories_ # [:,1:-1] -1 表示最後一列的前一列
'''
[array(['female', 'male'], dtype=object), array(['C', 'Q', 'S'], dtype=object)]
'''
data_.iloc[:,1:-1] = OrdinalEncoder().fit_transform(data_.iloc[:,1:-1])
data_.head()
'''
	Age 	Sex 	Embarked 	Survived
0 	22.0 	1.0 	2.0 		0
1 	38.0 	0.0 	0.0 		2
2 	26.0 	0.0 	2.0 		2
3 	35.0 	0.0 	2.0 		2
4 	35.0 	1.0 	2.0 		0
''''
  • preprocessing.OneHotEncoder:獨熱編碼,創建啞變量

我們剛纔已經用OrdinalEncoder把分類變量Sex和Embarked都轉換成數字對應的類別了。在艙門Embarked這一列中,我們使用[0,1,2]代表了三個不同的艙門,然而這種轉換是正確的嗎?
我們來思考三種不同性質的分類數據:

1) 艙門(S,C,Q)
三種取值S,C,Q是相互獨立的,彼此之間完全沒有聯繫,表達的是S≠C≠Q的概念。這是名義變量。

2) 學歷(小學,初中,高中)
三種取值不是完全獨立的,我們可以明顯看出,在性質上可以有高中>初中>小學這樣的聯繫,學歷有高低,但是學歷取值之間卻不是可以計算的,我們不能說小學 + 某個取值 = 初中。這是有序變量。

3) 體重(>45kg,>90kg,>135kg)
各個取值之間有聯繫,且是可以互相計算的,比如120kg - 45kg = 90kg,分類之間可以通過數學計算互相轉換。這是有距變量。

然而在對特徵進行編碼的時候,這三種分類數據都會被我們轉換爲[0,1,2],這三個數字在算法看來,是連續且可以計算的,這三個數字相互不等,有大小,並且有着可以相加相乘的聯繫。所以算法會把艙門,學歷這樣的分類特徵,都誤會成是體重這樣的分類特徵。這是說,我們把分類轉換成數字的時候,忽略了數字中自帶的數學性質,所以給算法傳達了一些不準確的信息,而這會影響我們的建模。

類別OrdinalEncoder可以用來處理有序變量,但對於名義變量,我們只有使用啞變量的方式來處理,才能夠儘量向算法傳達最準確的信息:
在這裏插入圖片描述

這樣的變化,讓算法能夠徹底領悟,原來三個取值是沒有可計算性質的,是“有你就沒有我”的不等概念。在我們的數據中,性別和艙門,都是這樣的名義變量。因此我們需要使用獨熱編碼,將兩個特徵都轉換爲啞變量。


from sklearn.preprocessing import OneHotEncoder
X = data.iloc[:,1:-1]

enc = OneHotEncoder(categories='auto').fit(X)
result = enc.transform(X).toarray()
result
'''
array([[0., 1., 0., 0., 1.],
       [1., 0., 1., 0., 0.],
       [1., 0., 0., 0., 1.],
       ...,
       [1., 0., 0., 0., 1.],
       [0., 1., 1., 0., 0.],
       [0., 1., 0., 1., 0.]])
'''
#依然可以直接一步到位,但爲了給大家展示模型屬性,所以還是寫成了三步
OneHotEncoder(categories='auto').fit_transform(X).toarray()
'''
array([[0., 1., 0., 0., 1.],
       [1., 0., 1., 0., 0.],
       [1., 0., 0., 0., 1.],
       ...,
       [1., 0., 0., 0., 1.],
       [0., 1., 1., 0., 0.],
       [0., 1., 0., 1., 0.]])
'''
#依然可以還原
pd.DataFrame(enc.inverse_transform(result))
'''
		0 		1
0 		male 	S
1 		female 	C
2 		female 	S
3 		female 	S
4 		male 	S
... 	... 	...
884 	male 	S
885 	female 	S
886 	female 	S
887 	male 	C
888 	male 	Q

889 rows × 2 columns
'''
enc.get_feature_names()
result
'''
array([[0., 1., 0., 0., 1.],
       [1., 0., 1., 0., 0.],
       [1., 0., 0., 0., 1.],
       ...,
       [1., 0., 0., 0., 1.],
       [0., 1., 1., 0., 0.],
       [0., 1., 0., 1., 0.]])
'''
result.shape # (889, 5)
#axis=1,表示跨行進行合併,也就是將量表左右相連,如果是axis=0,就是將量表上下相連
newdata = pd.concat([data,pd.DataFrame(result)],axis=1)
newdata.head()
'''
 	Age 	Sex 	Embarked 	Survived 	0 		1 		2 		3 		4
0 	22.0 	male 	S 			0.0 		0.0 	1.0 	0.0 	0.0 	1.0
1 	38.0 	female 	C 			2.0 		1.0 	0.0 	1.0 	0.0 	0.0
2 	26.0 	female 	S 			2.0 		1.0 	0.0 	0.0 	0.0 	1.0
3 	35.0 	female 	S 			2.0 		1.0 	0.0 	0.0 	0.0 	1.0
4 	35.0 	male 	S 			0.0 		0.0 	1.0 	0.0 	0.0 	1.0
'''
newdata.drop(["Sex","Embarked"],axis=1,inplace=True)
newdata.columns =
["Age","Survived","Female","Male","Embarked_C","Embarked_Q","Embarked_S"]
newdata.head()
'''
 	Age 	Survived 	Female 	Male 	Embarked_C 	Embarked_Q 	Embarked_S
0 	22.0 	0.0 		0.0 	1.0 	0.0 		0.0 		1.0
1 	38.0 	2.0 		1.0 	0.0 	1.0 		0.0 		0.0
2 	26.0 	2.0 		1.0 	0.0 	0.0 		0.0 		1.0
3 	35.0 	2.0 		1.0 	0.0 	0.0 		0.0 		1.0
4 	35.0 	0.0 		0.0 	1.0 	0.0 		0.0 		1.0
'''

特徵可以做啞變量,標籤也可以嗎?可以,使用類sklearn.preprocessing.LabelBinarizer可以對做啞變量,許多算法都可以處理多標籤問題(比如說決策樹),但是這樣的做法在現實中不常見,因此我們在這裏就不贅述了。

編碼與啞變量 功能 重要參數 重要屬性 重要接口
.LabelEncoder 分類標籤編碼 N/A .classes_:查看標籤中究竟有多少類別 fit,
transform,
fit_transform,
inverse_transform
.OrdinalEncoder 分類標籤編碼 N/A .categories_:查看特徵中究竟有多少類別 fit,
transform,
fit_transform,
inverse_transform
.OnehotEncoder 獨熱編碼,爲名義變量創建啞變量 categories:每個特徵都有哪些類別,默認auto"表示讓算法自己判斷,或者可以輸入列表,每個元素都是個列表,表示每個特徵中的不同類別
handle unknown:當輸入了 categories,且算法遇見了 categories中沒有有寫明的特徵或類別時,是否報錯。默認"error"表示請報錯,也可以選擇" ignore’表示請無視。如果選擇" gnore",則末再 categories中註明的特徵或類別的啞變量會全部顯示爲0.在逆轉 inverse transform)中,未知特徵或類別會被返回爲None。
.categories_:查看特徵中究竟 多少類別,如果是自己輸入的類別,那就不需要查看了 fit,
transform,
fit_transform,
inverse_transform,
get_feature_ names:查看生成的啞變量的每一列都是什麼特徵的什麼取值

2.4 處理連續型特徵:二值化與分段

  • sklearn.preprocessing.Binarizer

根據閾值將數據二值化(將特徵值設置爲0或1),用於處理連續型變量。大於閾值的值映射爲1,而小於或等於閾值的值映射爲0。默認閾值爲0時,特徵中所有的正值都映射到1。二值化是對文本計數數據的常見操作,分析人員可以決定僅考慮某種現象的存在與否。它還可以用作考慮布爾隨機變量的估計器的預處理步驟(例如,使用貝葉斯設置中的伯努利分佈建模)。

#將年齡二值化
data_2 = data.copy()
from sklearn.preprocessing import Binarizer
X = data_2.iloc[:,0].values.reshape(-1,1) #類爲特徵專用,所以不能使用一維數組
transformer = Binarizer(threshold=30).fit_transform(X)
transformer
'''
array([[0.],
       [1.],
       [0.],
       [1.],
       [1.],
       [0.],
       [1.],
       [0.],
       [0.],
       [0.],
       [0.],
       [1.],
       [0.],
       [1.],
       [0.],
       [1.],
       [0.],
       [0.],
       [1.],
       ....])
'''
  • preprocessing.KBinsDiscretizer

將連續型變量劃分爲分類變量的類,能夠將連續型變量排序後按順序分箱後編碼。總共包含三個重要參數:

參數 含義&輸入
n_bins 每個特徵中分箱的個數,默認5,一次會被運用到所有導入的特徵
encode 編碼的方式,默認"onehot "
“onehot”:做啞變量,之後返回一個稀疏矩陣,每一列是一個特徵中的一個類別,含有該類別的樣本表示爲1,不含的表示爲0
"ordinal ”:每個特徵的每個箱都被編碼爲—個整數,返回每一列是一個特徵,每個特徵下含有不同整數編碼的箱的矩陣
“onehot-dense”:做啞變量,之後返回一個密集數組。
strategy 用來定義箱寬的方式,默認" quantile"
" unifor":表示等寬分箱,即每個特徵中的每個箱的最大值之間的差爲(特徵.max(0-特徵min0)/( n_bins)
“pantile”:表示等位分箱,即每個特徵中的每個箱內的樣本數量都相同
" kmeans":表示按聚類分箱,每個箱中的值到最近的維k均值聚類的簇心得距離都相同
from sklearn.preprocessing import KBinsDiscretizer
X = data.iloc[:,0].values.reshape(-1,1)
est = KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='uniform')
est.fit_transform(X)
'''
array([[0.],
       [1.],
       [0.],
       [1.],
       [1.],
       [1.],
       [2.],
       [0.],
       [1.],
       [0.],
       [0.],
       [2.],
       [0.],
       [1.],
       [0.],
       [2.],
       ...])
'''
#查看轉換後分的箱:變成了一列中的三箱
set(est.fit_transform(X).ravel())
'''
{0.0, 1.0, 2.0}
'''
est = KBinsDiscretizer(n_bins=3, encode='onehot', strategy='uniform')
#查看轉換後分的箱:變成了啞變量
est.fit_transform(X).toarray()
'''
array([[1., 0., 0.],
       [0., 1., 0.],
       [1., 0., 0.],
       ...,
       [0., 1., 0.],
       [1., 0., 0.],
       [0., 1., 0.]])
'''

3. 特徵選擇feature selection

當數據預處理完成後,我們就要開始進行特徵工程了,

特徵提取
(feature extraction)
特徵創造
(feature creation)
特徵選擇
(feature select)
從文字,圖像,聲音等其他非結構化數據中提取新信息作爲特徵。比如說,從淘寶寶貝的名稱中提取出產品類別,產品顏色,是否是網紅產品等等。 把現有特徵進行組合,或互相計算,得到新的特徵。比如說,我們有一列特徵是速度,一列特徵是距離,我們就可以通過讓兩列相處,創造新的特徵:通過距離所花的時間。 從所有的特徵中選擇出有意義,對模型有幫助的特徵,以避免必須將所有特徵都導入模型去訓練的情況。

在做特徵選擇之前,有非常重要的事:跟數據提供者開會!

一定要抓住給你提供數據的人,尤其是理解業務和數據含義的人,跟他們聊一段時間。技術能夠讓模型起飛,前提是你和業務人員一樣理解數據。所以特徵選擇的第一步,其實是根據我們的目標,用業務常識來選擇特徵。來看完整版泰坦尼克號數據中的這些特徵:
在這裏插入圖片描述
其中是否存活是我們的標籤。很明顯,以判斷是否存活爲目的,票號,登船的艙門,乘客編號明顯是無關特徵,可以直接刪除。姓名,艙位等級,船艙編號,也基本可以判斷是相關性比較低的特徵。性別,年齡,船上的親人數量,這些應該是相關性比較高的特徵。

所以,特徵工程的第一步是:理解業務。

當然了,在真正的數據應用領域,比如金融,醫療,電商,我們的數據不可能像泰坦尼克號數據的特徵這樣少,這樣明顯,那如果遇見極端情況,我們無法依賴對業務的理解來選擇特徵,該怎麼辦呢?我們有四種方法可以用來選擇特徵:過濾法,嵌入法,包裝法,和降維算法。

#導入數據,讓我們使用 digit recognize0r數據來一展身手
import pandas as pd 
data = pd.read_csv(r"C:\Users\ASUS\sklearn\digit_recognizor.csv")
X = data.iloc[:,1:]
y = data.iloc[:,0]
X.shape # 42000, 784)

'''
這個數據量相對誇張,如果使用支持向量機和神經網絡,很可能會直接跑不出來。
使用KNN跑一次大概需要半個小時,用這個數據舉例,能更夠體現特徵工程的重要性。
'''

3.1 Filter過濾法

過濾方法通常用作預處理步驟,特徵選擇完全獨立於任何機器學習算法,它是根據各種統計檢驗中的分數及相關性的各項指標來選擇特徵

全部特徵→最佳特徵子集→算法→模型評估

3.1.1方差過濾

3.1.1.1 VarianceThreshold

這是通過特徵本身的方差來篩選特徵的類。比如一個特徵本身的方差很小,就表示樣本在這個特徵上基本沒有差異,可能特徵中的大多數值都一樣,甚至整個特徵的取值都相同,那這個特徵對於樣本區分沒有什麼作用。所以無論接下來的特徵工程要做什麼,都要優先消除方差爲0的特徵。 Variance threshold?有重要參數 threshold,表示方差的閾值,表示捨棄所有方差小於 threshold的特徵,不填默認爲0,即刪除所有的記錄都相同的特徵。

from sklearn.feature_selection import VarianceThreshold 
selector = VarianceThreshold()  #實例化,不填參數默認方差爲0
X_var0 = selector.fit_transform(X)  #獲取刪除不合格特徵之後的新特徵矩陣
#也可以直接寫成X= vairanceThresho().fit_transform(x)
X_var0.shape  # (42000, 708)

可以看見,我們已經刪除了方差爲0的特徵,但是依然剩下了708多個特徵,明顯還需要進一步的特徵選擇。然而,如果我們知道我們需要多少個特徵,方差也可以幫助我們將特徵選擇一步到位。比如說,我們希望留下一半的特徵,那可以設定一個讓特徵總數減半的方差閾值,只要找到特徵方差的中位數,再將這個中位數作爲參數threshold的值輸入就好了.

import numpy as np 
X_fsvar= VarianceThreshold(np.median(X.var().values)).fit_transform(X)
X.var().values # 提取每一列的方差
np.median(X.var(). values)  #1352.286783180131 選取這些數中的中位數
X_fsvar = VarianceThreshold(np.median(X.var().values)).fit_transform(X) # 根據中位數進行特徵選擇(可選取一半的數據,前提是知道需要一半的特徵)
X_fsvar.shape # (42000, 392)

當特徵是二分類時,特徵的取值就是伯努利隨機變量,這些變量的方差可以計算爲

Var[X] = p(1-p)

其中X是特徵矩陣,p是二分類特徵中的類在這個特徵中所佔的概率。

#若特徵是伯努利隨機變量,假設p=0.8,即二分類特徵中某種分類佔到80‰以上的時候刪除特徵
X_bvar = VarianceThreshold(.8*(1-.8)).fit_transform(X)
X_bvar.shape  # (42000, 685)
3.1.1.2 方差過濾對模型的影響

**我們這樣做了以後,對模型效果會有怎樣的影響呢?**在這裏,我爲大家準備了KNN和隨機森林分別在方差過濾前和方差過濾後運行的效果和運行時間的對比。KNN是K近鄰算法中的分類算法,其原理非常簡單,是利用毎個樣本到其他樣本點的距離來判斷每個樣本點的相似度,然後對樣本進行分類。KNN必須遍歷每個特徵和毎個樣本,因而特徵越多,KNN的計算也就會越緩慢。由於這一段代碼對比運行時間過長,所以我爲大家貼出了代碼和結果。

1.導入模塊並準備數據

# KNN vs 隨機森林 在不同方差過濾效果下的對比
from sklearn.ensemble import RandomForestClassifier as RFC 
from sklearn.neighbors import KNeighborsClassifier as KNN 
from sklearn.model_selection import cross_val_score 
import numpy as np 

x = data.iloc[:,1:]
y= data.iloc[:,0]

x_fsvar = varianceThreshold(np.median(x.var().values)).fit_transform(x)

我們從模塊 neighbors導入 KNeighborsClassfier縮寫爲KNN,導入隨機森林縮寫爲RFC,然後導入交叉驗證模塊和numpy.其中未過濾的數據是x和y,使用中位數過濾後的數據是x_fsvar,都是我們之前已經運行過的代碼。

2.KNN方差過濾前

#=====[TIME WARNING:35mins +]=====#
cross_val_score(KNN(),x,y,cv=5).mean () # 0.9658569700264943

# python中的魔法命令,可以直接使用%% timeit來計算運行這個ce11中的代碼所需的時間
#爲了計算所需的時間,需要將這個ce11中的代碼運行很多次(通常是7次)後求平均值,因此運行%%timeit的時間會遠遠超過ce11中的代碼單獨運行的時間
#=====[TIME WARNING:4 hours]=====#% timeit 
cross_val_score(KNN(),x,y,cv=5).mean () # 33min 58s ± 43.9 s per loop(mean ± std.dev.of 7 runs,I loop each)

3.KNN方差過濾後

#=====[TIME WARNING:20mins +]=====#
cross_val_score(KNN(),x_fsvar,y,cv=5).mean () # 0.9659997478150573
#=====[TIME WARNING:2 hours]=====#% timeit 
cross_val_score(KNN(),x_fsvar,y,cv=5).mean () # 20min  ± 4 min 55s per loop(mean ± std.dev.of 7 runs,I loop each)

可以看出,對於KNN,過濾後的效果十分明顯:準確率稍有提升,但平均運行時間減少了10分鐘,特徵選擇過後算法的效率上升了1/3.那隨機森林又如何呢?

4.隨機森林方差過濾前

# 隨機森林-方差過濾前
cross_val_score(RFC(n_estimators=10,random_state=0),x,y,cv=5).mean () # 0.9380003861799541% timeit 
# 查看一下模型運行的時間
cross_val_score(RFC(n_estimators=10,random_state=0),x,y,cv=5).mean () # 11.5s ± 305ms per loop(mean ± std.dev.of 7 runs,I loop each)

5.隨機森林方差過濾後

# 隨機森林-方差過濾後
cross_val_score(RFC(n_estimators=10,random_state=0),x_fsvar,y,cv=5).mean () # 0.9388098166696807% timeit 
# 查看一下模型運行的時間
cross_val_score(RFC(n_estimators=10,random_state=0),x_fsvar,y,cv=5).mean () # 11.1s ± 72ms per loop(mean ± std.dev.of 7 runs,I loop each)

首先可以觀察到的是,隨機森林的準確率略遜於KNN,但運行時間卻連KNN的1%都不到,只需要十幾秒鐘。其次,方差過濾後,隨機森林的準確率也微弱上升,但運行時間卻幾乎是沒什麼變化,依然是11秒鐘。

爲什麼隨機森林運行如此之快? 爲什麼方差過濾對隨機森林沒很大的有影響?這是由於兩種算法的原理中涉及到的計算量不同。最近鄰算法KNN,單棵決策樹,支持向量機SVM,神經網絡,迴歸算法,都需要遍歷特徵或升維來進行運算,所以他們本身的運算量就很大,需要的時間就很長,因此方差過濾這樣的特徵選擇對他們來說就尤爲重要。但對於不需要歷特徵的算法,比如隨機森林,它隨初選取特徵進行分枝,本身運箅就非常快速,因此特徵選擇對它來說效果平平。這其實很容易理解,無論過濾法如何降低特徵的數量,隨機森林也只會選取固定數量的特徵來建模;而最近鄰算法就不同了,特徵越少,距離計算的維度就越少,模型明顯會隨着特徵的減少變得輕量。因此,過濾法的主要對象是:需要遍歷特徵或升維的算法們,而過濾法的主要目的是:在維持算法表現的前提下,幫助算法們降低計算成本

思考:過濾法對隨機森林無效,卻對樹模型有效?
從算法原理上來說,傳統決策樹需要遍歷所有特徵,計算不純度後進行分枝,而隨機森林卻是隨機選擇特徵進行計算和分枝,因此隨機森林的運算更快,過濾法對隨機森林無用,對決策樹卻有用。
在 sklearn中,決策樹和隨機森林都是隨機選擇特徵進行分枝(可以參考sklearn(2)—— 決策樹,參數 random_state),但決策樹在建模過程中隨機抽取的特徵數目卻迒遠超過隨機森林當中毎棵樹隨機抽取的特徵數目(比如說對於這個780維的數據,隨機森林每棵樹只會抽取10-20個特徵,而決策樹可能會抽取300-400個特徵)),因此,過濾法對隨機森林無用,卻對決策樹有用。
也因此,在 sklearn中,隨機森林中的毎棵樹都比單獨的一棵決策樹簡單得多,高維數據下的隨機森林的計算比決策樹快很多。

對受影響的算法來說,我們可以將方差過濾的影響總結如下:

閾值很小
被過濾掉得特徵比較少
閾值比較大
被過濾掉的特徵有很多
模型表現 不會有太大影響 可能變更好,代表被濾掉的特徵大部分是噪音
也可能變糟糕,代表被濾掉的特徵中很多都是有效特徵
運行時間 可能降低模型的運行時間
基於方差很小的特徵有多少
當方差很小的特徵不多時
對模型沒有太大影響
一定能夠降低模型的運行時間
算法在遍歷特徵時的計算越複雜,運行時間下降得越多

我們的對比當中,我們使用的方差閾值是特徵方差的中位數,因此屬於閾值比較大,過濾掉的特徵比較多的情況。我們可以觀察到,無論是KNN還是隨機森林,在過濾掉一半特徵之後,模型的精確度都上升了。這說明被我們過濾掉的特徵在當前隨機模式( random_state=0)下大部分是噪音。那我們就可以保留這個去掉了一半特徵的數據,來爲之後的特徵選擇做準備。當然,如果過濾之後模型的效果反而變差了,我們就可以認爲,被我們過濾掉的特徵中有很多都有有效特徵,那我們就放棄過濾,使用其他手段來進行特徵選擇。

思考:雖然隨機森林算得快,但KNN的效果比隨機森林更好?
調整一下 n_estimators試試看吧O(∩_∩)O,隨機森林是個非常強大的模型哦
3.1.1.3選取超參數 threshold

我們怎樣知道,方差過濾掉的到底時噪音還是有效特徵呢?過濾後模型到底會變好還是會變壞呢? 答案是:每個數據集不一樣,只能自己去嘗試。這裏的方差閾值,其實相當於是一個超參數,要選定最優的超參數,我們可以畫學習曲線,找模型效果最好的點。但現實中,我們往往不會這樣去做,因爲這樣會耗費大量的時間。我們只會使用閾值爲0或者閾值很小的方差過濾,來爲我們優先消除一些明顯用不到的特徵,然後我們會選擇更優的特徵選擇方法繼續削減特徵數量.

3.1.2 相關性過濾

方差挑選完畢之後,我們就要考慮下一個問題:相關性了。我們希望選出與標籤相關且有意義的特徵,因爲這樣的特徵能夠爲我們提供大量信息。如果特徵與模型天關,那隻會白白浪費我們的計算內存,可能還會給模型帶來噪音。在 sklearn當中,我們有三種常用的方法來評判特徵與標籤之間的相關性:卡方,F檢驗,互信息。

3.1.2.1 卡方過濾

卡方過濾是專門針對離散型標籤(即分類問題)的相關性過濾。卡方檢驗類 feature_selection.chi2計算毎個非負特徵和標籤之間的卡方統計量,並依照卡方統討量由高到低爲特徵排名。再結合 feature_selection.SelectKBest這個可以輸入"評分標準“來選出前K個分數最高的特徵的類,我們可以藉此除去最可能獨立於標籤,與我們分類目的無關的特徵。

另外,如果卡方檢驗檢測到某個特徵中所有的值都相同,會提示我們使用方差先進行方差過濾。並且,剛纔我們已經驗證過,當我們使用方差過濾篩選掉一半的特徵後,模型的表現時提升的。因此在這裏,我們使用 threshold=中位數時完成的方差過濾的數據來做卡方檢驗(如果方差過濾後模型的表現反而降低了,那我們就不會使用方差過濾後的數據,而是使用原數據):

from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.model_selection import cross_val_score 
from sklearn.feature_selection import SelectKBest 
from sklearn.feature_selection import chi2

#假設在這裏我知道我需要300個特徵
x_fschi = SelectKBest(chi2,k=300).fit_transform(X_fsvar,y)
x_fschi.shape # (42000, 300)

驗證一下模型的效果如何:

cross_val_score(RFC(n_estimators=10,random_state=0),x_fschi,y,cv=5).mean() # 0.9333098667649198

可以看岀,模型的效果降低了,這說明我們在設定k=300的時候刪除了與模型相關且有效的特徵,我們的K值設置得太小,要麼我們需要調整K值,要麼我們必須放棄相關性過濾。當然,如果模型的表現提升,則說明我們的相關性過濾是有效的,是過濾掉了模型的噪音的,這時候我們就保留相關性過濾的結果。

3.1.2.2 選取超參數K

那如何設置一個最佳的K值呢?在現實數據中,數據量很大,模型很複雜的時候,我們也許不能先去跑一遍模型看看效果,而是希望最開始就能夠選擇一個最優的超參數k。那第一個方法,就是我們之前提過的學習曲線:

#=====[TIME WARNING:5mins]=====#
%matplotlib inline 
import matplotlib.pyplot as plt 
score= []
for i in range(390,200,-10):
    x_fschi = SelectKBest(chi2,k=i).fit_transform(X_fsvar,y)
    once = cross_val_score(RFC(n_estimators=10,random_state=0),x_fschi,y,cv=5).mean()
    score.append(once)

plt.plot(range(390,200,-10),score)
plt.show() # 如下圖

在這裏插入圖片描述
通過這條曲線,我們可以觀察到,隨着K值的不斷增加,模型的表現不斷上升,這說明,K越大越好,數據中所有的特徵都是與特徵相關的。但是運行這條曲線的時間同樣也是非常地長,接下來我們就來介紹一種更好的選擇k的方法:看p值選擇k。

卡方檢驗的本質是推測兩組數據之間的差異,其檢驗的原假設是"兩組數據是相互獨立的",卡方檢驗返回卡方值和p值兩個統計量,萁中卡方值很難界定有效的範圍,而p值我們一般使用0.01或0.05作爲顯著性水平。即p值判的邊界,具體我們可以這樣來看。

P值 <=0.05或001 >0.05或0.01
數據差異 差異不是自然形成的 這些差異是很自然的樣本誤差
相關性 兩組數據是相關的 兩組數據是相互獨立的
原假設 拒絕原假設,接受備擇假設 接受原假設

從特徵工程的角度,我們希望選取卡方值很大,p值小於0.05的特徵,即和標籤是相關聯的特徵。而調用SelectKBest之前,我們可以直接從chi2實例化後的模型中獲得各個特徵所對應的卡方值和P值。

chivalue, pvalues_chi= chi2(X_fsvar,y)
chivalue # 卡方值
'''
array([ 945664.84392643, 1244766.05139164, 1554872.30384525,
       1834161.78305343, 1903618.94085294, 1845226.62427198,
       1602117.23307537,  708535.17489837,  974050.20513718,
       1188092.19961931, 1319151.43467036, 1397847.8836796 ,
       1433554.26798015, 1429043.15373433, 1332663.17213405,
       1101423.25372261,  809989.56940485,  519266.71772284,
        285681.88297156,  191589.23696468,  902883.1255264 ,
        ....])
'''
pvalues_chi # P值
'''
所有的值都爲0,代表所有的P值都<0.05 (即兩組數據都於標籤相關)
array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       ....])
'''
#k取多少?我們想要消除所有p值大於設定值,比如0.05或0.01的特徵
k= chivalue.shape[0] -(pvalues_chi >0.05).sum()
k # 392
# x_fschi= SelectKBest(chi2,k=填寫具體的k).fit_transform(x_fsvar,y)
#cross_val_score(RFC(n_estimators=10,random_state=0),x_fschi,y,cv=5). mean()

chivalue.shape # (392,)
chivalue.shape[0] # 392
pvalues_chi >0.05 # True =1 ,False = 0
'''
array([False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       ...])
'''
# 獲得P值大於0.05 個數的和,即True的個數
(pvalues_chi >0.05).sum() # 0

可以觀察到,所有特徵的p值都是0,這說明對於 digit_recognizor這個數據集來說,方差驗證已經把所有和標籤無關的特徵都剔除了,或者這個數據集本身就不含與標籤無關的特徵。在這種情況下,捨棄任何一個特徵,都會捨棄對模型有用的信息,而使模型表現下降,因此在我們對計算速度感到滿意時,我們不需要使用相關性過濾來過濾我們的數據。如果我們認爲運算速度太緩慢,那我們可以酌情刪除一些特徵,但前提是,我們必須犧牲模型的表現。接下來,我們試試看用其他的相關性過濾方法驗證一下我們在這個數據集上的結論。

3.1.2.3 F檢驗

F檢驗,又稱ANOA,方差齊性檢驗,是用來捕捉毎個特徵與標籤之間的線性關係的過濾方法。F檢驗既可以做迴歸也可以做分類,因此包含 feature_selection. f_classif(F檢驗分類)和 feature_selection. f_regression(F檢驗迴歸)兩個類。其中F檢驗分類用於標籤是離散型變量的數據,而F檢驗迴歸用於標籤是連續型變量的數據。

和卡方檢驗一樣,這兩個類需要和類 SelectKBest連用,並且我們也可以直接通過輸出的統計量來判斷我們到底要設置一個什麼樣的K。需要注意的是,F檢驗在數據服從正態分佈時效果會非常穩定,因此如果使F檢驗過濾,我們會先將數據轉換成服從正態分佈的方式。

F檢驗的本質是尋找兩組數據之間的線性關係,其原假設是"數據不存在顯著的線性關係″。它返回F值和ρ值兩個統計量。和卡方過濾一樣,我們希望選取p值小於0.05或0.01的特徵,這些特徵與標籤時顯著線性相關的, 而p值大於0.05或0.01的特徵則被我們認爲是和標籤沒有顯著線性關係的特徵,應該被刪除。以F檢驗的分類爲例,我們繼續在數字數據集上來進行特徵選擇:

from sklearn.feature_selection import f_classif 
F, pvalues_f  = f_classif(X_fsvar,y)
F # F 值
'''
array([ 618.65383492,  846.18897012, 1115.40617051, 1362.3677305 ,
       1452.03355369, 1381.09095571, 1138.26505266,  464.29616121,
        660.00977785,  849.66393412, 1004.7450309 , 1124.76177588,
       1200.99190762, 1209.29489877, 1110.4944286 ,  854.66183292,
       ...])
'''
pvalues_f # P值(即是存在不是0的值,也是非常小的)
'''
array([0.00000000e+000, 0.00000000e+000, 0.00000000e+000, 0.00000000e+000,
       0.00000000e+000, 0.00000000e+000, 0.00000000e+000, 0.00000000e+000,
       0.00000000e+000, 0.00000000e+000, 0.00000000e+000, 0.00000000e+000,
       0.00000000e+000, 0.00000000e+000, 0.00000000e+000, 0.00000000e+000,
       ....])   
'''
k= F.shape[0] -(pvalues_f >0.05).sum()
k # 392
# x_fsF= SelectKBest(f_classif,k=填寫具體的k).fit_transform(x_fsvar,y)
#cross_val_score(RFC(n_estimators=10,random_state=0),x_fsF,cv=5). mean()

得到的結論和我們用卡方過濾得到的結論一模一樣:沒有任何特徵的p值大於0.01,所有的特徵都是和標籤相關的,因此我們不需要相關性過濾。

3.1.2.4 互信息法

互信息法是用來捕捉每個特徵與標籤之間的任意關係(包括線性和非線性關係)的過濾方法。和F檢驗相似,它既可以做迴歸也可以做分類,並且包含兩個類 feature_selection.mutual_info_classi(互信息分類)和feature_selection.mutual_info_regression(互信息迴歸)。這兩個類的用法和參數都和F檢驗一模一樣,不過互信息法比F檢驗更加強大,F檢驗只能夠找出線性關係,而互信息法可以找出任意關係。

互信息法不返回p值或F值類似的統計量。它返回"每個特徵與目標之間的互信息量的估計",這個估計量在[0,1]之間取值,爲0則表示兩個變量獨立,爲1則表示兩個變量完全相關。以互信息分類爲例的代碼如下:

from sklearn. feature_selection import mutual_info_classif as MIC 
result = MIC(X_fsvar,y)
k = result.shape [0]-sum(result<= 0)
# x_fsmic= SelectKBest(MIC,k=填寫具體的k).fit_transform( X_fsvar,y)
#cross_val_score(RFC(n_estimators=10,random_state=0),x_fsmic, y, cv=5).mean()

result>0
'''
array([ True,  True,  True,  True,  True,  True,  True,  True,  True,
        True,  True,  True,  True,  True,  True,  True,  True,  True,
        True,  True,  True,  True,  True,  True,  True,  True,  True,
        ...])
'''
(result>0).sum() # 392
(result==0).sum() # 0

所有特徵的互信息量估計都大於0,因此所有特徵都與標籤相關

當然了,無論是F檢驗還是互信息法,大家也都可以使用學習曲線,只是使用統計量的方法會更加髙效。當統計量判斷已經沒有特徵可以刪除時,無論用學習曲線如何跑,刪除特徵都只會降低模型的表現。當然了,如果數據量太龐大,模型太複雜,我們還是可以犧牲模型表現來提升模型速度,一切都看大家的具體需求.

3.1.3 過濾法總結

到這裏我們學習了常用的基於過濾法的特徵選擇,包括方差過濾,基於卡方,F檢驗和互信息的相關性過濾,講解了各個過濾的原理和麪臨的問題,以及怎樣調這些過濾類的超參數。通常來說,我會建議,先使用方差過濾,然後使用互信息法來捕捉相關性,不過了解各種各樣的過濾方式也是必要的。所有信息被總結在下表,大家自取:

說明 超參數的選擇
VarianceThreshold 方差過濾,可輸入方差閾值,返回方差大於閾值的新特徵矩陣 看具體數據究竟是含有更多噪聲還是更多有效特徵一般就使用0或1來篩選也可以畫學習曲線或取中位數跑模型來幫助確認
SelectKBest 用來選取K個統計量結果最佳的特徵,生成符合統計量要求的新特徵矩陣 看配合使用的統計量
chi2 卡方檢驗,專用於分類算法,捕捉相關性 追求p小於顯著性水平的特徵
f_classif F檢驗分類,只能捕捉線性相關性;要求數據服從正態分佈 追求p小於顯著性水平的特徵
f_regression F檢驗迴歸,只能捕捉線性相關性;要求數據服從正態分佈 追求p小於顯著性水平的特徵
mutual_info_classif 互信息分類,可以捕捉任何相關性;不能用於稀疏矩陣 追求互信息估計大於0的特徵
mutual_info_regression 互信息迴歸,可以捕捉任何相關性;不能用於稀疏矩陣 追求互信息估計大於0的特徵

3.2 Embedded嵌入法

嵌入法是一種讓算法自己決定使用哪些特徵的方法,即特徵選擇和算法訓練同時進行,類似於Filter,只不過係數是通過訓練得來的。嵌入法是一種讓算法自己決定使用哪些特徵的方法,即特徵選擇和算法訓練同時進行。在使用嵌入法時,我們先使用某些機器學習的算法和模型進行訓練,得到各個特徵的權值係數,根據權值係數從大到小選擇特徵。這些權值係數往往代表了特徵對於模型的某種貢獻或某種重要性,比如決策樹和樹的集成模型中的 feature_importances_屬性,可以列出各個特徵對樹的建立的貢獻,我們就可以基於這種貢獻的評估,找出對模型建立最有用的特徵。因此相比於過濾法,嵌入法的結果會更加精確到模型的效用本身,對於提高模型效力有更好的效果。並且,由於考慮特徵對模型的貢獻,因此無關的特徵(需要相關性過濾的特徵)和無區分度的特徵(需要方差過濾的特徵)都會因爲缺乏對模型的貢獻而被刪除掉,可謂是過濾法的進化版.
在這裏插入圖片描述
然而,嵌入法也不是沒有缺點。

過濾法中使用的統計量可以使用統計知識和常識來查找範圍(如p值應當低於顯著性水平0.05),而嵌入法中使用的權值係數卻沒有這樣的範圍可找——我們可以說,權值係數爲0的特徵對模型絲毫沒有作用,但當大量特徵都對模型有貢獻且貢獻不一時,我們就很難去界定一個有效的臨界值。這種情況下,模型權值係數就是我們的超參數,我們或許需要學習曲線,或者根據模型本身的某些性質去判斷這個超參數的最佳值究竟應該是多少。在我們之後的學習當中,每次講解新的算法,我都會爲大家提到這個算法中的特徵工程是如何處理,包括具體到每個算法的嵌入法如何使用。在這堂課中,我們會爲大家講解隨機森林和決策樹模型的嵌入法。

另外,嵌入法引入了算法來挑選特徵,因此其計算速度也會和應用的算法有很大的關係。如果採用計算量很大,計算緩慢的算法,嵌入法本身也會非常耗時耗力。並且,在選擇完畢之後,我們還是需要自己來評估模型.

  • feature_selection.SelectFromModel

class sklearn.feature_selection.SelectFromModel (estimator, threshold=None, prefit=False, norm_order=1,max_features=None)

SelectFromModel是一個元變換器,可以與任何在擬合後具有coef_, feature_importances_屬性或參數中可選懲罰項的評估器一起使用(比如隨機森林和樹模型就具有屬性 feature_importances_,邏輯迴歸就帶有l和2懲罰項,線性支持向量機也支持12懲罰項)。

對於有 feature_importances的模型來說,若重更性低於提供的閾值參數,則認爲這些特徵不重要並被移除。feature_ importances_的取值範圍是[0,1],如果設置閾值很小,比如0.001,就可以刪除那些對標籤預測完全沒貢獻的特徵。如果設置得很接近1,可能只有一兩個特徵能夠被留下。

選讀:使用懲罰項的橫型的嵌入法
而對於使用懲罰項的模型來說,正則化懲罰項越大,特徵在模型中對應的係數就會越小。當正則化懲罰項大到一定的程度的時候,部分特徵係數會變成0,當正則化懲罰項繼續增大到一定程度時,所有的特徵係數都會趨於0.但是我們會發現一部分特徵係數會更容易先變成0,這部分系數就是可以篩掉的。也就是說,我們選擇特徵係數較大的特徵。另外,支持向量機和邏輯迴歸使用參數C來控制返回的特徵矩陣的稀疏性,參數C越小,返回的特徵越少。 Lasso迴歸,用 alpha參數來控制返回的特徵矩陣,apha的值越大,返回的特徵越少,
參數 說明
estimator 使用的模型評估器,只要是帶 feature_ importances或者coef屬性,或帶有和2懲罰項的模型都可以使用
threshold 特徵重要性的閾值,重要性低於這個閾值的特徵都將被刪除
prefit 默認Fase,判斷是否將實例化後的模型直接傳遞給構造函數。如果爲Tnue,則必須直接調用ft和 transfom,不能使用 fit transform.並且 Select Model不能與cross val score, GridsearchCV和克隆估計器的類似實用程序一起使用。
norm_order k可輸入非零整數,正無窮,負無窮,默認值爲1
在評估器的coef_屬性高於一維的情況下,用於過濾低於閾值的係數的向量的範數的階數
max_features 在閾值設定下,要選擇的最大特徵數。要禁用閾值並僅根據 max_features選擇,請設置threshold =-np.inf

我們重點要考是前兩個參數。在這裏,我們使用隨機森林爲例,則需要學習曲線來幫助我們尋找最佳特徵值。

from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import RandomForestClassifier as RFC

RFC_ = RFC(n_estimators =10,random_state=0) # 隨機森林實例化(實例化後纔可以運行)
# threshold=0.005 重要性的特徵小於0.005都會被砍掉
# X_embedded = SelectFromModel(RFC_,threshold=0.005) 表示SelectFromModel實例化
X_embedded = SelectFromModel(RFC_,threshold=0.005).fit_transform(X,y)
#在這裏我只想取出來有限的特徵。0.005這個閾值對於有780個特徵的數據來說,是非常高的閾值,因爲平均每個特徵只能夠分到大約0.001的feature_importances_
X_embedded.shape # (42000, 47)

#模型的維度明顯被降低了
#同樣的,我們也可以畫學習曲線來找最佳閾值
#======【TIME WARNING:10 mins】======#
import numpy as np
import matplotlib.pyplot as plt

RFC_.fit(X,y).feature_importances_ # 查看RFC_特徵的重要性
# 在0-(RFC_.fit(X,y).feature_importances_).max()【最大的特徵重要性】,取出20個
threshold = np.linspace(0,(RFC_.fit(X,y).feature_importances_).max(),20)
score = []
for i in threshold:
    X_embedded = SelectFromModel(RFC_,threshold=i).fit_transform(X,y)
    once = cross_val_score(RFC_,X_embedded,y,cv=5).mean()
    score.append(once)
plt.plot(threshold,score)
plt.show() #如下圖

(RFC_.fit(X,y).feature_importances_).max() # 0.01276360214820271
threshold = np.linspace(0,(RFC_.fit(X,y).feature_importances_).max(),20)
threshold # 0到最大值(0.01276360214820271)之間取20個
'''
array([0.        , 0.00067177, 0.00134354, 0.00201531, 0.00268707,
       0.00335884, 0.00403061, 0.00470238, 0.00537415, 0.00604592,
       0.00671769, 0.00738945, 0.00806122, 0.00873299, 0.00940476,
       0.01007653, 0.0107483 , 0.01142007, 0.01209183, 0.0127636 ])
'''

在這裏插入圖片描述

圖像上來看,隨着閾值越來越髙,模型的效果逐漸變差,被刪除的特徵越來越多,信息損失也逐漸變大。但是在0.00134之前,模型的效果都可以維持在0.93以上,因此我們可以從中挑選一個數值來驗證一下模型的效果。

X_embedded = SelectFromModel(RFC_,threshold=0.00067).fit_transform(X,y)
X_embedded.shape # (42000, 324)
cross_val_score(RFC_,X_embedded,y,cv=5).mean() # 0.939905083368037

可以看出,特徵個數瞬間縮小到324多,這比我們在方差過濾的時候選擇中位數過濾出來的結果392列要小,並且交叉驗證分數0.9399高於方差過濾後的結果0.9388,這是由於嵌入法比方差過濾更具體到模型的表現的緣故,換一個算法,使用同樣的閾值,效果可能就沒有這麼好了。

和其他調參一樣,我們可以在第一條學習曲線後選定一個範圍,使用細化的學習曲線來找到最佳值:

#======【TIME WARNING:10 mins】======#
score2 = []
for i in np.linspace(0,0.00134,20):
    X_embedded = SelectFromModel(RFC_,threshold=i).fit_transform(X,y)
    once = cross_val_score(RFC_,X_embedded,y,cv=5).mean()
    score2.append(once)
plt.figure(figsize=[20,5])
plt.plot(np.linspace(0,0.00134,20),score2)
plt.xticks(np.linspace(0,0.00134,20)) #設置X軸方法(刻度、標籤)
plt.show() #如下圖

在這裏插入圖片描述

figure語法說明

figure(num=None, figsize=None, dpi=None, facecolor=None, 
       edgecolor=None, frameon=True)
    num:圖像編號或名稱,數字爲編號 ,字符串爲名稱
    figsize:指定figure的寬和高,單位爲英寸;
    dpi:參數指定繪圖對象的分辨率,即每英寸多少個像素,缺省值爲80 ;     
        1英寸等於2.5cm,A4紙是 21*30cm的紙張 ;
    facecolor:背景顏色;
    edgecolor:邊框顏色;
    frameon:是否顯示邊框;

查看結果,果然0.00067並不是最高點,真正的最高點0.000564已經將模型效果提升到了94%以上。我們使用0.000564來跑一跑我們的SelectFromModel

X_embedded = SelectFromModel(RFC_,threshold=0.000564).fit_transform(X,y)
X_embedded.shape # (42000, 340)
cross_val_score(RFC_,X_embedded,y,cv=5).mean() # 0.9408335415056387
#=====【TIME WARNING:2 min】=====#
#我們可能已經找到了現有模型下的最佳結果,如果我們調整一下隨機森林的參數呢?
cross_val_score(RFC(n_estimators=100,random_state=0),X_embedded,y,cv=5).mean() # 0.9639525817795566

得出的特徵數目依然小於方差篩選,並且模型的表現也比沒有篩選之前更高:已經完全可以和計算一次半小時的KNN相匹敵(KNN的準確率是96.58%),接下來再對隨機森林進行調參,準確率應該還可以再升高不少。可見在嵌入法下,我們很容易就能夠實現特徵選擇的目標:減少計算量,提升模型表現。因此,比起要思考很多統計量的過濾法來說,钁入法可能是更有效的種方法。然而,過濾法的計算遠遠比嵌入法要快,所以大型數據中,我們還是會優先考慮過濾法,或者下面這種結合了過濾和嵌入法的方法;包裝法 Wrapper。

3.3 Wrapper包裝法

包裝法也是一個特徵選擇和算法訓練同時進行的方法,與嵌入法十分相似,它也是依賴於算法自身的選擇,比如coef_屬性或feature_importances_屬性來完成特徵選擇。但不同的是,我們往往使用一個目標函數作爲黑盒來幫助我們選取特徵,而不是自己輸入某個評估指標或統計量的閾值。包裝法在初始特徵集上訓練評估器,並且通過coef_屬性或通過feature_importances_屬性獲得每個特徵的重要性。然後,從當前的一組特徵中修剪最不重要的特徵。在修剪的集合上遞歸地重複該過程,直到最終到達所需數量的要選擇的特徵。區別於過濾法和嵌入法的一次訓練解決所有問題(嵌入法的每次都使用全部特徵來進行訓練和建模),包裝法要使用特徵子集進行多次訓練,因此它所需要的計算成本是最高的。(包裝法需要的計算成本位於嵌入法和過濾法中間)
在這裏插入圖片描述
注意,在這個圖中的算法",指的不是我們最終用來導入數據的分類或迴歸算法(即不是隨機森林),而是專業的數據挖掘算法,即我們的目標函數。這些數據挖掘算法的核心功能就是選取最佳特徵子集。

最典型的目標函數是遞歸特徵消除法( Recursive feature elimination,簡寫爲RFE)。它是一種貪婪的優化算法旨在找到性能最佳的特徵子集。它反覆創建模型,並在毎次迭代時保留最佳特徵或剔除最差特徵,下一次迭代時它會使用上一次建模中沒有被選中的特徵來構建下一個模型,直到所有特徵都耗盡爲止。然後,它根據自己保留或剔除特徵的順序來對特徵進行排名,最終選岀—個最佳子集。包裝法的效果是所有特徵選擇方法中最利於提升模型表現的,它可以使用很少的特徵達到很優秀的效果。除此之外,在特徵數目相同時,包裝法和嵌入法的效果能夠匹敵,不過它比嵌入法算得更快,雖然它的計算量也十分龐大,不適用於太大型的數據。相比之下,包裝法是最髙效的特徵選擇方法。

  • feature_selection.RFE

class sklearn.feature_selection.RFE (estimator, n_features_to_select=None, step=1, verbose=0)

參數estimator是需要填寫的實例化後的評估器,n_features_to_select是想要選擇的特徵個數,step表示每次迭代中希望移除的特徵個數。除此之外,RFE類有兩個很重要的屬性,.support_:返回所有的特徵的是否最後被選中的布爾矩陣,以及 .ranking_返回特徵的按數次迭代中綜合重要性的排名。類feature_selection.RFECV會在交叉驗證循環中執行RFE以找到最佳數量的特徵,增加參數cv,其他用法都和RFE一模一樣。

from sklearn.feature_selection import RFE
RFC_ = RFC(n_estimators =10,random_state=0) # 實例化模型
# 沒迭代一次刪掉50個特徵
selector = RFE(RFC_, n_features_to_select=340, step=50).fit(X, y) # 訓練模型
# support_ 返回所有的特徵的是否最後被選中的布爾矩陣 
# support_.sum() 返回特徵總和
selector.support_.sum() # 340
# 返回特徵的按數次迭代中綜合重要性的排名
# 重要性相同排名相同,越重要排名越前(數字1 代表排名第一)
selector.ranking_ 
'''
array([10,  9,  8,  7,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,
        6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  7,  7,  6,  6,
        5,  6,  5,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  7,  6,  7,  7,
        7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  6,  6,  5,  4,
        4,  5,  3,  4,  4,  4,  5,  4,  5,  7,  6,  7,  7,  7,  8,  8,  8,
        ...])
'''
X_wrapper = selector.transform(X)
cross_val_score(RFC_,X_wrapper,y,cv=5).mean() # 0.9389522459432109

我們也可以對包裝法畫學習曲線:

#======【TIME WARNING: 15 mins】======#
score = []
for i in range(1,751,50):
    X_wrapper = RFE(RFC_,n_features_to_select=i, step=50).fit_transform(X,y)
    once = cross_val_score(RFC_,X_wrapper,y,cv=5).mean()
    score.append(once)
plt.figure(figsize=[20,5])
plt.plot(range(1,751,50),score)
plt.xticks(range(1,751,50))
plt.show() # 如下圖

在這裏插入圖片描述
明顯能夠看出,在包裝法下面,應用50個特徵時,模型的表現就已經達到了90%以上,比嵌入法和過濾法都高效很多。我們可以放大圖像,尋找模型變得非常穩定的點來畫進一步的學習曲線(就像我們在嵌入法中做的那樣)。如果我們此時追求的是最大化降低模型的運行時間,我們甚至可以直接選擇50作爲特徵的數目,這是一個在縮減了94%的特徵的基礎上,還能保證模型表現在90%以上的特徵組合,不可謂不高效。

同時,我們提到過,在特徵數目相同時,包裝法能夠在效果上匹敵嵌入法。試試看如果我們也使用340作爲特徵數目,運行一下,可以感受一下包裝法和嵌入法哪一個的速度更加快。由於包裝法效果和嵌入法相差不多,在更小的範圍內使用學習曲線,我們也可以將包裝法的效果調得很好,大家可以去試試看。

X_wrapper = RFE(RFC_,n_features_to_select=340, step=50).fit_transform(X,y)
once = cross_val_score(RFC_,X_wrapper,y,cv=5).mean()
#======【TIME WARNING: 20 mins+】======#
score = []
for i in range(1,340,20):
    X_wrapper = RFE(RFC_,n_features_to_select=i, step=20).fit_transform(X,y)
    once = cross_val_score(RFC_,X_wrapper,y,cv=5).mean()
    score.append(once)
plt.figure(figsize=[20,5])
plt.plot(range(1,340,20),score)
plt.xticks(range(1,340,20))
plt.show()#如下圖

在這裏插入圖片描述
根據再次測試的結果,可以再次測試如下代碼:

#======【TIME WARNING: 20 mins+】======#
score = []
for i in range(1,340,20):
    X_wrapper = RFE(RFC_,n_features_to_select=i, step=20).fit_transform(X,y)
    once = cross_val_score(RFC_,X_wrapper,y,cv=5).mean()
    score.append(once)
plt.figure(figsize=[20,5])
plt.plot(range(1,340,20),score)
plt.xticks(range(1,340,20))
plt.show()#如下圖

在這裏插入圖片描述

3.4 特徵選擇總結

至此,我們講完了降維之外的所有特徵選擇的方法。這些方法的代碼都不難,但是每種方法的原理都不同,並且都涉及到不同調整方法的超參數。經驗來說,過濾法更快速,但更粗糙。包裝法和嵌入法更精確,比較適合具體到算法去調整,但計算量比較大,運行時間長 。當數據量很大的時候,優先使用方差過濾和互信息法調整,再上其他特徵選擇方法。使用邏輯迴歸時,優先使用嵌入法。使用支持向量機時,優先使用包裝法。迷茫的時候,從過濾法走起,看具體數據具體分析。

其實特徵選擇只是特徵工程中的第一步。真正的高手,往往使用特徵創造或特徵提取來尋找高級特徵。在Kaggle之類的算法競賽中,很多高分團隊都是在高級特徵上做文章,而這是比調參和特徵選擇更難的,提升算法表現的高深方法。特徵工程非常深奧,雖然我們日常可能用到不多,但其實它非常美妙。若大家感興趣,也可以自己去網上搜一搜,多讀多看多試多想,技術逐漸會成爲你的囊中之物。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章