嵌入式特徵選擇方法,號稱結合了過濾式和包裹式的優點,將特徵選擇嵌入到模型構建的過程中:

這是特徵選擇的一整個流程的總結,所謂嵌入式特徵選擇,就是通過一些特殊的模型擬合數據然後根據模型自身的某些對於特徵的評價的屬性來作為評價指標,最後再使用包裹式的特徵選擇方法來選擇,當然,很多時候我們還是僅停留在計算出評價指標的階段,因為包裹式特徵選擇的最大問題就是計算量和時間是三者之中最大的。

最常用的進行嵌入式特徵選擇的模型:樹模型和帶正則項的模型(線性回歸、邏輯回歸、svm、svr、神經網路等)。

鑒於最近在寫回歸類模型的面經,就先從這類模型的特徵選擇方法開始說起好了。首先最常用的就是廣義線性回歸裏的L1正則化。

基於L1正則項的嵌入式特徵選擇

下面以lasso為例

import pandas as pd
from sklearn.datasets import load_boston
from sklearn.linear_model import Lasso
from sklearn.preprocessing import StandardScaler
X=pd.DataFrame(load_boston().data)
y=load_boston().target
sd=StandardScaler()
X=sd.fit_transform(X)
coefs=[]

for alpha in [0.01,0.05,0.1,0.5,1]:
lr=Lasso(alpha=alpha)
lr.fit(X,y)
coefs.append(lr.coef_)

coefs=pd.DataFrame(coefs)

這裡就存在一個很嚴重的問題了,我們取了5組不同的的正則化係數的情況下,得到的特徵重要性(也就是線性模型的權重值)的變化情況很嚴重,比如從上往下數第4、5個特徵的變動幅度太大了,雖然越大的正則化係數越容易得到小的權重係數,但是問題是這裡的第4、5個特徵的相對其它特徵的權重係數的取值的排序也發生了非常大的變化,尤其是第5個特徵,在正則化係數取值為0.01的時候其權重係數為-14.3945,可以說負貢獻很大,但是當正則化係數為其他值是,權重係數大幅下降甚至變成了0。

這裡的發生這種現象的原因有很多,比如存在多重共線性問題會導致權重的方差變化增大,通過查看原始數據的相關係數表可以看出,這列特徵和其它特徵存在較大的相關關係:

圖中列名為4的列為權重變化很大的特徵,可以看出它和其它特徵之間存在著較強的相關性

當然,不僅僅是共線性的問題,不同的數據集本身就會產生不同的特徵重要性判斷,這個在後面的樹模型的feature importance中也會有這樣的問題,而且數據量越小越容易出幺蛾子,關於更加詳細系統的lasso的這種不穩定的原因,後續會寫到回歸類面經的專欄裏,以及多重共線性的相關的問題,這裡不贅述了,因為要寫起來太特麼多了。

那麼這裡我們應該怎麼解決這個問題?我們到底應該依賴與哪一個正則化係數給出的判定的結果。

1、思路一

lassocv的思路給出了一個比較暴力的解決方案,嘗試大量的正則化係數,然後選擇損失函數最小的模型對應的正則化係數。簡單說,也就是對於上述的不穩定性問題採取無視的態度。就是取泛化性能最好的模型的特徵重要性判斷結果。

lr=LassoCV(alphas=np.linspace(0,1,100),cv=3)
lr.fit(X,y)
print(lr.alpha_) #這裡的alpha為最佳的alpha值
print(lr.mse_path_) #輸出每一個alpha對應的交叉驗證的mse的值

類似的邏輯回歸也有類似的LogisticRegressionCV的方法,原理基本是類似的,這裡就不贅述了,自己去看官網的api就行。

不僅僅是線性回歸和邏輯回歸,任何廣義線性模型如FM/FFM,神經網路,都可以使用L1正則項對損失函數施加懲罰項。

思路二

類似於集成的思想,使用stable selection,穩定性特徵選擇。

stability_selection.randomized_lasso - stability-selection 0.1.0 documentation?

thuijskens.github.io

穩定性特徵選擇,以lasso為例,實際上就是每次採樣部分數據,並且在給定範圍內隨機選擇一個正則化係數值來擬合輸入數據與標籤然後得到一串的權重係數。最後將不同樣本子集,不同正則化係數得到的模型的所有權重係數的結果進行簡單平均。懶的寫代碼了,源代碼很簡單,看看上面的官網就ok了。使用的庫是scikit -contrib下的stable-selection,不過思路很簡單,自己手寫也行。

另外還有一個randomizedlasso的思路,使用不同的正則化係數生成不同的模型得到不同的特徵重要性評價然後取平均但是實際情況中發現這種方法的效果有限計算量還挺大,不如思路一來的簡單粗暴所以不贅述,感興趣的自己去看stable_selection的源代碼就可以了,很easy的。

廣義線性回歸這塊的差不多就寫這麼多,神經網路、svm之類的,思路基本類似,思路一相對來說更常見吧,反正也是調調包的問題,再展開寫也沒什麼意義,網上教程一大堆,懶得寫了,《那就這樣吧》。


基於樹模型的嵌入式特徵選擇及其變種

之所以說存在變種是因為還有boruta、null importance、eli5、shap、xgbfi等各種解決方案,從一般到特徵的介紹吧。

最簡單最原始的,xgboost、lightgbm、catboost,randomforest。。。。等,取其中一種集成樹的python庫定義一個模型,然後去擬合輸入輸出得到訓練完畢的模型,輸出特徵的重要性即可。這張方法的好處在於,我們不需要額外的去做特徵選擇,因為模型訓練的過程中自身已經完成了特徵選擇,得到了不同特徵的評價得分(比較常用的是信息增益和分類使用次數,xgb貌似還有不少別的指標),然後根據這些得分的大小就可得到不同特徵針對對應的模型的特徵的貢獻度了。

import lightgbm as lgb
from sklearn.datasets import load_iris

X=load_iris().data
y=load_iris().target
clf=lgb.LGBMClassifier()
clf.fit(X,y)
print(clf.feature_importances_)

X的四個特徵的分裂使用總次數,可以看出特徵3的重要性最強。

需要注意的地方:用於評估特徵重要性時候行列採樣最好都設置為1

這個方法的問題在於,不穩定,穩定性的計算結果和訓練集數據息息相關,這也就意味著特徵重要性也會存在類似過擬合的現象,在A數據集中計算出一個結果,在B數據集中計算出另一個結果。

import numpy as np
import lightgbm as lgb
from sklearn.datasets import load_iris
from sklearn.model_selection import StratifiedKFold
sfk=StratifiedKFold(5)
X=load_iris().data
y=load_iris().target
clfs=[]
for train_index,test_index in sfk.split(X,y):
X_train,X_test=X[train_index],X[test_index]
Y_train,Y_test=y[train_index],y[test_index]
clf=lgb.LGBMClassifier()
clf.fit(X_train,Y_train)
clfs.append(clf)

for clf in clfs:
print(clf.feature_importances_)

feature_importances=np.zeros(X.shape[0])
for clf in clfs:
feature_importances+=clf.feature_importances_/5.0

可以看到,四個特徵的特徵重要性得分的先後順序在不同的子模型中都存在一些差異,這裡作為一種彌補的方法,我們可以對這5個子模型的特徵重要性進行求和平均,當然如果用10折也可以,結果會更加穩定一些。

然而,這個時候又有另外一個問題了,我們怎麼設定閾值來作為衡量特徵是否有效無效的問題,比如說我們根據前面的方法得到了一組特徵的重要性為【100,50,25,0,15,139,75】,那麼這種簡單的「clf.faeture_importances」的方法並不會告訴我們怎麼選擇閾值,所以,後來又誕生了boruta、null importance、eli5等方式來解決這個問題。

null importance沒有嚴格的理論證明,它是由一個kaggle上的大佬選手發明的方法,旨在通過引入原始特徵的shuffle來作為判斷特徵重要性的標準。

需要強調的是,我們在進行feature importance特徵重要性判定的時候,

再此之前還有另外一種思路,向原始數據中添加雜訊項,然後以雜訊項的特徵重要性為衡量的標準:

import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.datasets import load_iris
from sklearn.model_selection import StratifiedKFold
sfk=StratifiedKFold(5)
X=load_iris().data
y=load_iris().target
clfs=[]

X=pd.DataFrame(X,columns=load_iris().feature_names)
X[noise_1]=np.random.normal(size=len(X))
for train_index,test_index in sfk.split(X,y):
X_train,X_test=X.iloc[train_index],X.iloc[test_index]
Y_train,Y_test=y[train_index],y[test_index]
clf=lgb.LGBMClassifier()
clf.fit(X_train,Y_train)
clfs.append(clf)

feature_importances=np.zeros(X.shape[1])
for clf in clfs:
feature_importances+=clf.feature_importances_
feature_importances=pd.DataFrame(feature_importances)
feature_importances.index=X.columns

哈哈哈哈哈哈,加入的雜訊項居然比原始數據中的length和兩個width特徵還要好哈哈哈

為了避免某次正好生成了很好的雜訊特徵影響結果,我又進行了10次測試並且生成了50個模型計算均值來觀察結果。

import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.datasets import load_iris
from sklearn.model_selection import StratifiedKFold
sfk=StratifiedKFold(5)
X=load_iris().data
y=load_iris().target
clfs=[]

for _ in range(10):
X=pd.DataFrame(X,columns=load_iris().feature_names)
X[noise_1]=np.random.normal(size=len(X))
for train_index,test_index in sfk.split(X,y):
X_train,X_test=X.iloc[train_index],X.iloc[test_index]
Y_train,Y_test=y[train_index],y[test_index]
clf=lgb.LGBMClassifier()
clf.fit(X_train,Y_train)
clfs.append(clf)

feature_importances=np.zeros(X.shape[1])
for clf in clfs:
feature_importances+=clf.feature_importances_
feature_importances=pd.DataFrame(feature_importances)
feature_importances.index=X.columns

打擾了打擾了

(補充:一般情況下,特徵選擇勢必要刪除部分特徵,導致的結果就是偏差必然是不減少的,也就是模型在特徵選擇完畢之後的訓練集上的評價指標不會大於特徵選擇前的訓練集上的評價指標,特徵選擇的作用是能夠降低特徵的維度,降低方差,增大偏差,減少過擬合的風險,但是不代表一定能夠降低測試集的表現—即泛化誤差,比如上面的例子就很典型了,你把通過這種特徵選擇的方法刪一刪試試就知道了精度下降死你)

接下來我們用null importance來試試。null importance的思路不是添加新的雜訊項,直接把原始標籤shuffle一下。。。就可以了,這樣就達到了把所有特徵相對於表現都shuffle的效果了,真聰明呢。然後直觀的思路就是比較shuffle之後和shuffle之前的feature importance的大小,shuffle之後的特徵統一都叫做原始特徵的null feature。和前面的雜訊方法一樣,我們這裡選擇shuffle 50輪!(側面也暴露了這種方法的問題就是為了盡量消除隨機性的影響要進行多次測試,但是對於數據量很大的問題來說,要耗費不少時間)

這裡我們就能得到shuffle之後的null feauture的特徵重要性值了,如果shuffle 50次,則每個feature都會有50個shuffle之後的feature importance,我們可以將原始特徵的特徵重要性和這50個null importance的均值比較,或者分位數比較,不過創始人oliver用的是這個方法:

用的是四分之三位數,代碼如下

import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.datasets import load_iris
from sklearn.model_selection import StratifiedKFold
sfk=StratifiedKFold(5)
X=load_iris().data
y=load_iris().target
Y=load_iris().target

clfs=[]
for train_index,test_index in sfk.split(X,y):
X_train,X_test=X[train_index],X[test_index]
Y_train,Y_test=y[train_index],y[test_index]
clf=lgb.LGBMClassifier(subsample=0.6,colsample_bytree=0.6)
clf.fit(X_train,Y_train)
clfs.append(clf)

feature_importances=np.zeros(X.shape[1])
for clf in clfs:
feature_importances+=clf.feature_importances_/len(clfs)

null_importances=[]
for _ in range(50):
np.random.shuffle(Y)
null_importance=np.zeros(X.shape[1])
for train_index,test_index in sfk.split(X,Y):
X_train,X_test=X[train_index],X[test_index]
Y_train,Y_test=Y[train_index],Y[test_index]
clf=lgb.LGBMClassifier(subsample=0.6,colsample_bytree=0.6)
clf.fit(X_train,Y_train)
null_importance+=clf.feature_importances_/5.0 #5折交叉
null_importances.append(null_importance)

null_importances=pd.DataFrame(null_importances)

根據大佬給的公式我們計算一下:

score=[]
for i in range(4):
score.append(np.log((1+feature_importances[i])/np.percentile(null_importances[i],75)))

打擾了打擾了。。。

為什麼會失敗?只能說數據量太小了,什麼鬼情況都可能出現。。。。換做大點的數據一般不會出現這麼打臉的結果。。。。。

告辭!

不拋棄不放棄,我又嘗試了將雜訊和null importance結合的思路看看,根據上述的評價發現這兩種特徵重要性的評價方式確實都存在著不穩定因素,換一個結合的思路看看,先加入雜訊然後計算null importance最後根據公式計算score,以雜訊的score為基準進行篩選,為了提高穩定性我們這裡加入5個雜訊,最後score取平均值:

import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.datasets import load_iris
from sklearn.model_selection import StratifiedKFold
sfk=StratifiedKFold(5)
X=load_iris().data
y=load_iris().target
Y=load_iris().target

X=pd.DataFrame(X)
X[noise_1]=np.random.normal(size=len(X))
X[noise_2]=np.random.normal(size=len(X))
X[noise_3]=np.random.normal(size=len(X))
X[noise_4]=np.random.normal(size=len(X))
X[noise_5]=np.random.normal(size=len(X))

clfs=[]
for train_index,test_index in sfk.split(X,y):
X_train,X_test=X.iloc[train_index],X.iloc[test_index]
Y_train,Y_test=y[train_index],y[test_index]
clf=lgb.LGBMClassifier()
clf.fit(X_train,Y_train)
clfs.append(clf)

feature_importances=np.zeros(X.shape[1])
for clf in clfs:
feature_importances+=clf.feature_importances_/len(clfs)

#################### null importance 特徵選擇方法 ###################

null_importances=[]
for _ in range(50):
np.random.shuffle(Y)
null_importance=np.zeros(X.shape[1])
for train_index,test_index in sfk.split(X,Y):
X_train,X_test=X.iloc[train_index],X.iloc[test_index]
Y_train,Y_test=Y[train_index],Y[test_index]
clf=lgb.LGBMClassifier()
clf.fit(X_train,Y_train)
null_importance+=clf.feature_importances_/5.0 #5折交叉
null_importances.append(null_importance)

null_importances=pd.DataFrame(null_importances)

score=[]
for i in range(6):
score.append(np.log((1+feature_importances[i])/np.percentile(null_importances[i],75)))

最後5個都是加入的雜訊noise的score,我們對其進行平均整理之後得到:

perfect,可以看到雜訊的平均得分是最低的,其它特徵的得分都要比它高,這樣就相當於相除了null importance進行shuffle的時候的影響而使各個特徵收到的隨機的影響都一樣將比較放在一個相對的層面。看來以後還是得這麼來使用。


shap、eli5、boruta、xgbfi、grouplasso等。。。。每一個都要花很多時間來寫,尤其是shap,不但可以幫助解釋gbdt也可以幫助解釋深度學習,牛逼了,慢慢寫吧,另外開一章來寫,

推薦閱讀:

相關文章