前面對GBDT的演算法原理進行了描述,通過前文了解到GBDT是以迴歸樹為基分類器的整合學習模型,既可以做分類,也可以做迴歸,由於GBDT設計很多CART決策樹相關內容,就暫不對其演算法流程進行實現,本節就根據具體資料,直接利用Python自帶的Sklearn工具包對GBDT進行實現。
資料集採用之前決策樹中的紅酒資料集,之前的資料集我們做了類別的處理(將連續的資料刪除了,且小批量資料進行了合併),這裡做同樣的處理,將其看為一個多分類問題。
首先依舊是讀取資料,並對資料進行檢查和預處理,這裡就不再贅述,所得資料情況如下:
- wine_df = pd.read_csv('./winequality-red.csv', delimiter=';', encoding='utf-8')
- columns_name = list(wine_df.columns)
- for name in columns_name:
- q1, q2, q3 = wine_df[name].quantile([0.25, 0.5, 0.75])
- IQR = q3 - q1
- lower_cap = q1 - 1.5 * IQR
- upper_cap = q3 + 1.5 * IQR
- wine_df[name] = wine_df[name].apply(lambda x: upper_cap if x > upper_cap else (lower_cap if (x < lower_cap) else x))
- sns.countplot(wine_df['quality'])
- wine_df.describe()
接下來就是先匯入使用GBDT所需要用到的工具包:
- # 這裡採用的是迴歸,因此是GradientBoostingRegressor,如果是分類則使用GradientBoostingClassifier
- from sklearn.ensemble import GradientBoostingClassifier
- from sklearn.metrics import mean_squared_error
- from sklearn import metrics
- from sklearn.model_selection import train_test_split import matplotlib.pyplot as plt
然後依舊是對資料進行切分,將資料分為訓練集和測試集:
- trainX, testX, trainY, testY = train_test_split(wine_df.drop(['quality']), wine_df['quality'], test_size=0.3, random_state=22)
然後就是建立模型:
- model = GradientBoostingClssifier()
這裡模型就有很多可選引數,用於調整模型,下面進行具體介紹:
首先是Boosting的框架的引數,這裡在使用GradientBoostingRegressor和GradientBoostingClassifier是一樣的,具體包括:
- n_estimators:弱分類器的最大迭代次數,也就是多少個弱分類器組成,預設值為100。當該值越小時容易欠擬合,太大又會過擬合;
- learning_rate:學習率,這在前面原理部分有進行介紹,預設值為1,較小的learning_rate意味著步長較小,需要更多的分類器才能夠達到效果,通常需要與上面n_eatimators結合共同調參;
- subsample:這也是一種正則化的方法,在正則化中有提到過,取值為(0,1],預設值為1,即不進行子取樣;
- init:初始化的弱分類器,即f0(x),預設為採用訓練樣本初始化的分類迴歸預測,若賦予值需要根據一定的先驗知識或者預擬合;
- loss:即損失函式,在原理篇介紹過相關損失函式,對於分類和迴歸中損失函式是不相同的:
- 在分類模型中,有對數似然損失函式“deviance”和指數損失函式“exponential”,預設為對數似然損失函式,一般選擇預設,因為選擇指數損失函式“exponential”又退回到AdaBoost了;
- 在迴歸模型中,有方差損失“ls”、絕對損失“lad”、Huber損失“huber”和分位數損失“quantile”,預設為均方差損失“ls”,一般來說,資料的噪音不多,採用均方差損失即可,噪音點較多,則推薦使用抗噪能力較強的Huber損失,如果需要對訓練集進行分段預測時則採用分位數損失“quantile”;
- alpha:這個引數只存在於GradientBoostingRegressor中,當使用Huber損失和分位數損失時,需要指定分位數的值,預設為0.9,如果噪音資料較多,可以適當降低這個值。
然後就是弱分類器有關的引數值,弱分類器採用的CART迴歸樹,決策樹中的相關引數在決策樹實現部分已經進行介紹,這裡主要對其中一些重要的引數再進行解釋:
- max_features:劃分樹時所用到的最大特徵數,預設為None,即使用全部的特徵,當取值為“log2”時,劃分時使用log2N個特徵,如果是“sqrt”和“auto”則分別對應著最多考慮√N¯個特徵,如果是整數,則代表考慮特徵的絕對數,如果是浮點數,則代表考慮總特徵數的百分比。一般來說樣本總特徵數小於50,直接採用50即可,當樣本特徵數量較大時,再考慮其他特徵數;
- max_depth:每個弱分類器的最大深度,預設為不輸入,樹的深度為3,一般對於資料較少或者特徵較少,該值不需要輸入,當樣本數量和特徵數量過於龐大,推薦使用最大深度限制,一般選擇10~100;
- min_samples_split:內部節點再劃分所需最小的樣本數,它限制了子樹進一步劃分的條件,如果節點的樣本數小於min_samples_split則不再進行分裂。預設值為2,若樣本數量較大,則推薦增大該值;
- min_samples_leaf:葉子節點最小樣本數,該值限定了葉子節點的最小樣本數,預設值為1,如果葉子節點樣本數量小於該值,則會和兄弟節點一起被剪枝,如果樣本量巨大,則推薦增加該值;
- min_weight_fraction_leaf:葉子節點最小樣本權重和,該值限制了葉子節點所有樣本權重和的最小值,若小於該值,則會和兄弟節點被剪枝,預設值為0,即不考慮權重。當樣本分類問題中類別分佈偏差較大,則會引入樣本權重,需要調整該值;
- max_leaf_nodes:最大葉子節點數,通過限制最大葉子結點數防止過擬合,預設為None。如果加入了限制,則演算法會建立在最大葉子節點數內最優的決策樹,當樣本特徵數量過多的話,可以限制該值;
- min_impurity_split:節點劃分最小不純度,這個值限定了決策樹的生長,若節點的不純度(即基尼係數)小於這個值,則該節點不再生長,即為葉子結點,預設值為1e-7,一般不推薦修改。
上述即為模型的主要引數,這裡首先全部使用預設值,對樣本進行訓練:
- model.fit(trainX, trainY)
print("模型在訓練集上分數為%s"%model.score(trainX, trainY))
pred_prob = model.predict_proba(trainX)
print('AUC:', metrics.roc_auc_score(np.array(trainY), pred_prob, multi_class='ovo'))
模型在訓練集上分數為0.8817106460418562
AUC: 0.9757763363472337
可以看到在訓練集上AUC表現還不錯,模型的分數但並不高,嘗試調整訓練引數,首先對於迭代次數和學習率共同進行調整:
- param_test1 = {'n_estimators': range(10, 501, 10), 'learning_rate': np.linspace(0.1, 1, 10)}
- gsearch = GridSearchCV(estimator=GradientBoostingClassifier(learning_rate=1,
- min_samples_split=2,
- min_samples_leaf=1,
- max_depth=3,
- max_features=None,
- subsample=0.8,
- ), param_grid=param_test1, cv=5)
- gsearch.fit(trainX, trainY)
- means = gsearch.cv_results_['mean_test_score']
- params = gsearch.cv_results_['params']
- for i in range(len(means)):
- print(params[i], means[i])
- print(gsearch.best_params_)
- print(gsearch.best_score_)
- # {'learning_rate': 0.2, 'n_estimators': 100}
找出最好的n_estimators=100和learning_rate=0.2,將其定下來,帶回模型,再次驗證:
- model = GradientBoostingClassifier(n_estimators=100, learning_rate=0.2,subsample=0.8)
- model.fit(trainX, trainY)
- print("模型在訓練集上分數為%s"%model.score(trainX, trainY))
- pred_prob = model.predict_proba(trainX)
- print('AUC:', metrics.roc_auc_score(np.array(trainY), pred_prob, multi_class='ovo'))
模型在訓練集上分數為0.9663330300272975
AUC: 0.9977791940084874
可以看到擬合效果已經很好了,再次調整引數,接下來調整弱分類器中的引數,max_depth和min_samples_split:
- param_test1 = {'max_depth': range(1, 6, 1), 'min_samples_split': range(1, 101, 10)}
- gsearch2 = GridSearchCV(estimator=GradientBoostingClassifier(n_estimators=100,
- learning_rate=0.2,
- max_features=None,
- min_samples_leaf=1,
- subsample=0.8,
- ), param_grid=param_test1, cv=5)
- gsearch2.fit(trainX, trainY)
- means = gsearch2.cv_results_['mean_test_score']
- params = gsearch2.cv_results_['params']
- for i in range(len(means)):
- print(params[i], means[i])
- print(gsearch2.best_params_)
- print(gsearch2.best_score_)
找出了樹的最大深度為5,由於最小樣本劃分數量同葉子節點最小樣本數量有一定關係,暫時不能定下min_samples_split,將其同min_samples_leaf共同調整:
- param_test1 = {'min_samples_leaf': range(1, 101, 10), 'min_samples_split': range(1, 101, 10)}
- gsearch3 = GridSearchCV(estimator=GradientBoostingClassifier(n_estimators=100,
- learning_rate=0.2,
- max_features=None,
- max_depth=5,
- subsample=0.8,
- ), param_grid=param_test1, cv=5)
- gsearch3.fit(trainX, trainY)
- means = gsearch3.cv_results_['mean_test_score']
- params = gsearch3.cv_results_['params']
- for i in range(len(means)):
- print(params[i], means[i])
- print(gsearch3.best_params_)
- print(gsearch3.best_score_)
- # {'min_samples_leaf': 21, 'min_samples_split': 41}
可以找出最小樣本劃分數量21和葉子節點最小數量,我們將這些引數再帶回模型:
- model = GradientBoostingClassifier(n_estimators=100, learning_rate=0.2, max_depth=5, min_samples_leaf=21, min_samples_split=41, subsample=0.8)
- model.fit(trainX, trainY)
- print("模型在訓練集上分數為%s"%model.score(trainX, trainY))
- pred_prob = model.predict_proba(trainX)
- print('AUC:', metrics.roc_auc_score(np.array(trainY), pred_prob, multi_class='ovo'))
模型在訓練集上分數為1.0
AUC: 1.0
可以看到在訓練集上已經完美擬合了,但為了驗證模型,我們需要再分離出一部分用於驗證模型的資料集:
- validX, tX, validY, tY = train_test_split(testX, testY, test_size=0.2)
然後使用驗證集,驗證模型:
- print("模型在測試集上分數為%s"%metrics.accuracy_score(validY, model.predict(validX)))
- pred_prob = model.predict_proba(validX)
- print('AUC test:', metrics.roc_auc_score(np.array(validY), pred_prob, multi_class='ovo'))
模型在測試集上分數為0.726790450928382
AUC test: 0.8413890948027345
可以看到模型在驗證集上表現並不是很好,上面模型存在一定的過擬合問題,繼續調整引數,通過調整max_features來提高模型的泛華能力:
- param_test1 = {'max_features': range(3, 12, 1)}
- gsearch4 = GridSearchCV(estimator=GradientBoostingClassifier(n_estimators=100,
- learning_rate=0.2,
- min_samples_leaf=21,
- min_samples_split=41,
- max_depth=5,
- subsample=0.8,
- ), param_grid=param_test1, cv=5)
- gsearch4.fit(trainX, trainY)
- means = gsearch4.cv_results_['mean_test_score']
- params = gsearch4.cv_results_['params']
- for i in range(len(means)):
- print(params[i], means[i])
- print(gsearch4.best_params_)
- print(gsearch4.best_score_)
- # {'max_features': 5}
進一步調整subsamples:
- param_test1 = {'subsample': np.linspace(0.1, 1, 10)}
- gsearch5 = GridSearchCV(estimator=GradientBoostingClassifier(n_estimators=100,
- learning_rate=0.2,
- min_samples_leaf=21,
- min_samples_split=41,
- max_depth=5,
- max_features=5
- ), param_grid=param_test1, cv=5)
- gsearch5.fit(trainX, trainY)
- means = gsearch5.cv_results_['mean_test_score']
- params = gsearch5.cv_results_['params']
- for i in range(len(means)):
- print(params[i], means[i])
- print(gsearch5.best_params_)
- print(gsearch5.best_score_)
- # {'subsample': 0.7}
到這裡基本主要引數都進行了調整,帶回到模型中:
- model = GradientBoostingClassifier(n_estimators=100, learning_rate=0.2, max_depth=5, min_samples_leaf=21, min_samples_split=41, max_features=5, subsample=0.7)
- model.fit(trainX, trainY)
- print("模型在訓練集上分數為%s"%model.score(trainX, trainY))
- pred_prob = model.predict_proba(trainX)
- print('AUC:', metrics.roc_auc_score(np.array(trainY), pred_prob, multi_class='ovo'))
模型在訓練集上分數為0.9990900818926297
AUC: 0.9999992641648271
有略微下降,因為通過提高模型的泛華能力,會增大模型的偏差,然後利用驗證集驗證模型:
- print("模型在測試集上分數為%s"%metrics.accuracy_score(validY, model.predict(validX)))
- pred_prob = model.predict_proba(validX)
- print('AUC test:', metrics.roc_auc_score(np.array(validY), pred_prob, multi_class='ovo'))
- 模型在測試集上分數為0.7161803713527851
- AUC test: 0.8429467644071055
進一步將模型的迭代次數增加一倍,學習率減半:
- model = GradientBoostingClassifier(n_estimators=200, learning_rate=0.1, max_depth=5, min_samples_leaf=21, min_samples_split=41, max_features=5, subsample=0.7)
- model.fit(trainX, trainY)
- print("模型在訓練集上分數為%s"%model.score(trainX, trainY))
- pred_prob = model.predict_proba(trainX)
- print('AUC:', metrics.roc_auc_score(np.array(trainY), pred_prob, multi_class='ovo'))
- # validX, tX, validY, tY = train_test_split(testX, testY, test_size=0.2)
- print("模型在測試集上分數為%s"%metrics.accuracy_score(validY, model.predict(validX)))
- pred_prob = model.predict_proba(validX)
- print('AUC test:', metrics.roc_auc_score(np.array(validY), pred_prob, multi_class='ovo'))
模型在訓練集上分數為0.9990900818926297
AUC: 1.0
模型在測試集上分數為0.7427055702917772
AUC test: 0.851199242237048
可以看到模型泛化能力有略微增強,可以嘗試進一步上述步驟,當迭代次數增加到一定程度,學習率減小到一定程度,模型泛化能力下降,可能是由於步長過小導致擬合和泛化能力下降。
以上就是GBDT的一個例項和引數調整過程,這裡使用的是CvGrid網格遍歷搜尋調參的方法,從結果來看並不理想,可能是由於樣本分佈的問題,還有就是在進行資料處理的時候採用了replace直接更改了樣本的標籤,在測試集中這部分資料可能會預測錯誤。 後面會繼續查詢原因,調整資料集再次進行訓練。
本例僅作為一個調參的學習過程,主要對GBDT中的引數有一個初步的瞭解,也是剛開始學習調參的方法,引數設定的也比較粗糙,後續會找一些新的資料集進一步對調參進行學習。