標籤:

介紹一位可能過時的咖狗神器XGBoost

介紹一位可能過時的咖狗神器XGBoost

Part I:特徵工程

Part II:模型部分


前言:

最近做了一個實驗室內部的數據挖掘比賽,訓練集11w,測試集約2w,屬於數據不均衡的二分類問題,正負例之比大約1:350。

本文介紹data mining類比賽的特徵工程,以及xgboost的使用與調參,並附上部分代碼。(xgboost應該很多人用過了,本文是給還不會用的同學看的)

Part I:特徵工程

在很多數據挖掘比賽中,特徵工程是非常重要的一部分。(因為用不上深度學習方法啊!不能自動學習特徵)因此我們需要人工處理一些特徵,而機器學習類演算法又比較依賴於好的特徵,所以特徵工程是肯定要做的。

這裡我用pandas來讀取csv文件和處理字元特徵和缺失值,用sklearn來做數據歸一化,注意:pandas可以做的事情sklearn一般也可以做,根據個人喜好選一個即可。

1. 字元特徵處理

數據挖掘類的特徵一般有兩種形式,數值型和字元型,我們一般需要把字元型特徵也轉換成數值型特徵。(也可以直接丟棄一些對任務不重要的字元特徵)

常見的字元型特徵又可以分成以下三種形式:

1)值域範圍比較小的字元特徵:比如性別,顏色,這種特徵相當於類別,而且類別種類一般比較小。如果我們直接把它們轉化成數字類別,模型可能會覺得這也是有順序的數值型屬性,比如,性別男是1,女是2,其中1<2,模型可能會認為屬性男<女。

因此,我們可以使用one hot形式,避免這種數值比較所帶來的影響。性別有兩個屬性男和女,那麼男可以表示為[1, 0],女可以表示為[0, 1]。

Pandas中提供了get_dummies函數把字元特徵轉換成one hot形式:

dummies = pd.get_dummies(dframe[feat]) #獲取one hot特徵dummies = dummies.rename(columns=lambda x: feat+str(x)) # 改特徵名dframe = pd.concat([dframe,dummies],axis=1) #把新特徵加入原始數據集

2)字元+數值組合型字元串特徵:這種特徵比較常見,比如AT18,而且通常值域範圍相對比較大,如果也轉換成one hot特徵,會加大特徵維度,而xgboost對高維特徵處理得不夠好。(或者可以先one hot,再用pca進行降維?)

一種很粗暴的方法是,我們可以把整個組合型特徵看成是一個字元特徵,直接映射成一個數值,pandas提供了factorize()函數:

dframe[feat+_toNum] = pd.factorize(dframe[feat])[0]

或者,先用正則表達式把字元和數字分別提取出來,再單獨對字元進行映射(數字無需再映射),將原始的一個字元串特徵轉化成兩個數值型特徵。

dframe[feat+_str] = dframe[feat].map( lambda x: re.compile(([a-zA-Z]+)).search(str(x)).group())dframe[feat+_str] = pd.factorize(dframe[feat+_str])[0]dframe[feat+_num] = dframe[feat].map(lambda x: match_number(x)).astype(int)+1 def match_number(value): match = re.compile("([0-9]+)").search(str(value)) if match: return match.group() else: return 0

關於正則表達式:re.compile用於編譯正則表達式,生成一個pattern;re.search 掃描整個字元串,從字元串中匹配compile所定義的pattern,最後返回第一個成功的匹配;re.group()返回符合這個pattern的字元串。

3)時間型字元特徵,這種特徵一般包括年月日時分秒,值域非常大。

一種最粗暴的方法是,分別把年月日時分秒提取出來:(我只提取了年月信息,個人覺得其餘特徵不是很重要,得看具體任務)

dframe[year] = dframe.apply(lambda x: int(x[feat][:4]), axis=1)dframe[month] = dframe.apply(lambda x: int(x[feat][5:7]), axis=1)#dframe[day] = dframe.apply(lambda x: int(x[feat][8:10]), axis=1)#dframe[hour] = dframe.apply(lambda x: int(x[feat][11:13]), axis=1)#dframe[minute] = dframe.apply(lambda x: int(x[feat][14:16]), axis=1)#dframe[second] = dframe.apply(lambda x: int(x[feat][17:19]), axis=1)

或者劃分成幾個時間區域,比如2017年前和2017年後,或者上午下午和晚上。

2. 缺失值處理

缺失值幾乎是一定會存在的,常見的解決方法用:

1)直接以0.9的缺失概率丟棄該列。

假如原數據集中缺失值非常多(佔到0.9以上),那麼這一列特徵其實意義不大,我們可以直接丟棄。

for feat in feat_cols: if dtrain[feat].isnull().sum() > 100000: #這是我自行估算的數值 drop_list.append(feat)

我沒有直接修改原始數據集,定義了drop_list表示我不用的特徵列,str_list表示需要進行處理的字元型特徵列(最後也會加入drop_list),最後得到use_list作為我使用的特徵列:

drop_list.extend(str_list)use_list = [x for x in dtrain.columns if x not in drop_list]train_x, train_y = dtrain[use_list], dtrain[label]

2)使用中位數,或者眾數填補。

注意到我們有訓練集和測試集,我們也要用訓練集中的均值去填補測試集中的缺失值,而不是用測試集的均值進行填補。這裡我用的是sklearn中preprocessing模型的函數:

# 將pd形式的數據轉化成np形式train_x, train_y = dtrain[att_list], dtrain[label]test_x = dtest[att_list]train_x, test_x = np.asarray(train_x), np.asarray(test_x)train_imp = Imputer(missing_values=NaN,strategy=mean,axis=0)train_imp.fit(train_x)train_x = train_imp.transform(train_x)test_x = train_imp.transform(test_x)

3)使用隨機森林預測缺失值

假設,我們需要預測某一列的缺失值,那麼先把該列數據分成有值的部分y_train,和缺失值部分,然後在有值的部分將除該列外的特徵定義為x_train,缺失值部分的所對應的新特徵就是x_test。我們使用x_train進行訓練,作為預測器輸入的新特徵,使用y_train作為預測的特徵值。訓練完之後,再使用x_test對缺失值進行預測填補。

y_train = dframe[feat][dframe[feat].notnull()].valuesx_train = dframe[dframe[feat].notnull()].drop([feat],axis=1).valuesx_test = dframe[dframe[feat].isnull()].drop([feat],axis=1).valuesrfc = RandomForestClassifier().fit(x_train, y_train)dframe[feat][dframe[feat].isnull()] = rfc.predict(x_test)

或者也可以用別的機器學習方法進行預測,預測缺失值一般比直接填補缺失值效果要好一點,但是需要注意過擬合問題。

3. 數據歸一化

至此,特徵已經全部處理成非空數值型,然後我們來看一下數據特徵,觀察一下數據的最值,均值,和缺失值:

print(dframe.describe())

可以看到不同特徵有很大差別,不同特徵往往具有不同的量綱和量綱單位,所以我們要對列特徵進行歸一化,避免某些值域範圍大的特徵佔到比較大的影響。

1)Z-Score標準化

先減去均值再除以方差,將每一列數據歸一化到0均值,方差為1(即標準正態分布),可以衡量分值偏離均值的程度,具體公式為:

X_{scale} = frac{x-mu}{sigma}

sklearn中提供了Normalizer()函數進行歸一化:

normalizer = preprocessing.Normalizer().fit(train_x)train_x = normalizer.transform(train_x)test_x = normalizer.transform(test_x)# 轉化回pd形式,因為後面會用到DataFrame形式train_x, test_x = pd.DataFrame(train_x), pd.DataFrame(test_x)

我們需要對訓練集和測試集都進行歸一化,這裡先用訓練集的數據進行歸一化,再根據訓練集的數據特徵對測試集進行歸一化。

2)Min-Max歸一化

對原始數據進行線性變換,將屬性縮放到一個[0,1]之間,公式為:

X_{scale} = frac{x-min}{max-min}

可以看到,這裡需要計算數據集中的max和min值,那麼當有新數據加入時,可能導致max和min的變化,需要重新定義,這也是這種方法的一個缺陷。

sklearn中提供了MinMaxScaler()函數進行歸一化:

min_max_scaler = preprocessing.MinMaxScaler()minmax_train = min_max_scaler.fit_transform(train_x)minmax_test = min_max_scaler.transform(test_x)train_x, test_x = pd.DataFrame(minmax_train), pd.DataFrame(minmax_test)

4. 重採樣

對於不均衡數據,理論上有以下兩種方法可以緩解:

1)欠採樣,丟棄訓練集的一些負例,使得正負例比重接近。

df_pos = dtrain[dtrain[label]==1] df_neg = dtrain[dtrain[label]==1] df_neg_sample = df_neg.sample(frac=0.4) #對負樣本進行採樣df = pd.concat([df_pos, df_neg_sample], ignore_index=True)

這裡我只是選擇了40%的負樣本,如果嚴格按照正負例比例相等的話,要丟棄更多負樣本,可能會損失很多重要信息。

2)過採樣,增加訓練集中的正例。

可以對正例特徵增加一些干擾,作為新的正例。

Part II:模型部分

在眾多分類演算法中,xgboost就是kaggle大殺器級別,效果好,速度快(並行處理,注意這裡的並行處理不是以樹為單位並行,是以特徵為單位並行),內置交叉驗證,正則化提升,很適合不均衡數據,可以自定義優化目標和評價指標,工具包還能自己處理缺失值和字元型特徵,也可以比較好得處理相關特徵,省心又省事。

我在這個task上,隨便跑個xgboost就能比svm和神經網路效果好。但是可能會過擬合,雖然在pubilc board上排名高,有可能在private board上撲街。

另外說明:xgboost優點不需要做太多特徵工程。

關於歸一化:因為樹會對特徵值進行排序,自動選擇特徵,那麼我們做不做歸一化,特徵的順序都是不變的,所以可以不用做歸一化。

關於缺失值:xgb可以自己學習缺失值的分裂方向,對於某個缺失的特徵,xgb會分別計算其屬於左子樹和右子樹的損失減少量得分,然後選擇比較好的分裂方向。

關於onehot:onehot比較適合處理無序的類別特徵,但是會增加特徵維度,增加樹的深度,因此onehot用在xgb 我只用於處理值域比較小的類別特徵。

1. 模型定義

這裡我用的是xgboost.sklearn中的XGBClassifier分類器,和xgb包是不同的。

題外話:我安裝xgboost看教程裝了好久都裝不上,最後我萬念俱灰一句 pip install xgboost 竟然直接就裝好了。

首先,我們先來定義一個基礎分類器:

from xgboost.sklearn import XGBClassifierxgb = XGBClassifier(learning_rate =0.1, n_estimators=5000, max_depth=5, min_child_weight=1, gamma=0, subsample=0.9, colsample_bytree=0.8, reg_alpha=1e-5, objective= binary:logistic, nthread=4, scale_pos_weight=1, seed=27)

參數說明如下:

Booster本身的參數:max_depth、min_child_weight和gamma控制了模型複雜度,alpha用於正則化,subsample和colsample_bytree進行隨機化。

max_depth是數的最大深度,默認值是6,常用值是3-10,通過把值調小來控制過擬合,因為比較深的樹可能會學習到特定樣本的一些特定特徵。

min_child_weight是子節點中最小的樣本權重和,作為是否停止拆分過程的閾值。通過把值調大來控制過擬合,但是如果這個閾值設置過大,也看導致欠擬合。

subsample是用於訓練模型的子樣本占整個樣本集合的比例,也用於控制過擬合。通過把值調小來控制過擬合,使得模型更加conservative,太小也有欠擬合風險。

colsample_bytree是在建樹時對特徵採樣的比例。

alpha是L1正則的懲罰係數,這裡不對偏置項進行正則。

Task所需要的參數:

learning_rate是學習率,seed是隨機種子,

objective是目標函數,二分類問題可以使用binary:logistic,多分類使用multi:softmax且設置類別,線性回歸使用reg:linear,邏輯回歸使用reg:logistic。

2. 模型訓練

首先,我們需要把原始訓練集劃分多份訓練集和驗證集,進行交叉驗證,避免模型過擬合。useTrainCV用於設置交叉驗證,cv_folds設置分成幾份數據集,一般進行五折交叉驗證。

xgb_param = alg.get_xgb_params()xgtrain = xgb.DMatrix(x.values, label=y.values)cvresult = xgb.cv(xgb_param, xgtrain, num_boost_round=alg.get_params()[n_estimators], nfold=cv_folds, metrics=auc, early_stopping_rounds=60)alg.set_params(n_estimators=cvresult.shape[0])

並且設置early_stopping_rounds,也是對付過擬合的一種方法。

在xgboost中,直接使用model.fit(x, y, eval_metric),就可以根據評估指標,對x和y進行擬合來訓練模型了。

alg.fit(x, y, eval_metric=auc)

根據具體任務,評估指標可以設置為auc,acc等等。

3. 模型預測

根據訓練數據fit好模型後,我們需要在驗證集或者測試集上進行預測。

使用alg.predict可以得到預測的類別,使用predict_proba可以得到預測的類別概率,我們使用auc評估指標,所以提交的是預測的類別概率。

dtrain_predictions = alg.predict(x)dtrain_predprob = alg.predict_proba(x)[:,1]

如果是在驗證集上,根據label值可以查看對應的衡量指標:

print ("Accuracy : %.4g" % metrics.accuracy_score(y.values, dtrain_predictions))print ("AUC Score (Train): %f" % metrics.roc_auc_score(y, dtrain_predprob))

4. 模型調優

同一個模型,不同參數可能會有不同的結果,我們想盡量找到比較優秀的參數,就需要進行調參工作,根據模型結果不斷嘗試新參數,提高模型結果。

假設我們現在要對max_depth進行調優,那麼我們先設置max_depth的調參範圍:

param_test = { max_depth : [i for i in range(3,10)]}

然後固定其餘參數,使用網格法選擇最佳的一組參數:

gsearch = GridSearchCV( estimator = XGBClassifier(learning_rate =0.1, gamma=0.1, subsample=0.9, n_estimators=140, max_depth=9, min_child_weight=1, colsample_bytree=0.9, reg_alpha=1e-5, objective= binary:logistic, nthread=4, scale_pos_weight=1, seed=27), param_grid = param_test, scoring=roc_auc,n_jobs=4,iid=False, cv=5)gsearch.fit(x, y)print(gsearch.grid_scores_,
, gsearch.best_params_,
, gsearch.best_score_)

另外附上我的一些調參範圍設置:

param_test = { max_depth: [i for i in range(3,10)], #min_child_weight: [i for i in range(1,6)] #subsample:[i/10.0 for i in range(6,10)], #subsample:[i/100.0 for i in range(85,100,5)], #colsample_bytree:[i/10.0 for i in range(6,10)] #colsample_bytree:[i/100.0 for i in range(85,100,5)] #gamma:[i/10.0 for i in range(0,5)] #learning_rate:[1e-4, 1e-3, 1e-5, 1e-2, 0.1] #reg_alpha:[1e-4, 1e-6, 1e-3, 1e-5, 1e-2, 0.1, 1, 10] #n_estimators: [140, 100, 80, 200]}

可以先把範圍設置比較大一些,找到一個比較好的區域後,再進一步細化區域。

在設置參數的時候,可以先觀察模型在訓練集和驗證集上的誤差和方差,先判斷模型是過擬合還是欠擬合,再進行調參。

如果方差比較大,說明可能過擬合,如果偏差比較大,說明可能欠擬合。

如果過擬合:比如把max_depth調小,min_child_weight調大,subsample調小等等。

如果欠擬合:與上面相反。

調整好參數後,可以再觀察一下誤差和方差,看有沒有改進。


xgboost代碼篇到此,後面如果有時間的話,可能會寫一個xgboost原理篇。

參考鏈接:Complete Guide to Parameter Tuning in XGBoost


推薦閱讀:

機器學習評估指標
《Python數據挖掘》筆記(二)關聯規則挖掘
大話機器學習之窮人如何玩轉數據挖掘
Python協同過濾演算法入門(1)相似度計算篇

TAG:數據挖掘 |