[机器学习] XGBoost 自定义损失函数-FocalLoss
一.XGBoost損失函數
- 損失函數:損失函數描述了預測值和真實標簽的差異,通過對損失函數的優化來獲得對學習任務的一個近似求解方法
- boosting類算法的損失函數的作用: Boosting的框架, 無論是GBDT還是Adaboost, 其在每一輪迭代中, 根本沒有理會損失函數具體是什么, 僅僅用到了損失函數的一階導數通過隨機梯度下降來參數更新
- 二階導數:GBDT在模型訓練時只使用了代價函數的一階導數信息,XGBoost對代價函數進行二階泰勒展開,可以同時使用一階和二階導數
- 牛頓法梯度更新:XGBoost是用了牛頓法進行的梯度更新。通過對損失進行分解得到一階導數和二階導數并通過牛頓法來迭代更新梯度。
XGBOOST是GBDT的一種高效實現方式,XGBOOST在每一次迭代時,對損失函數進行了二階泰勒展開,在下一次迭代時同時考慮了一階導和二階導。這一點與GBDT僅使用一階導有所區別。
XGBOOST是一個非常靈活的模型,允許使用者根據實際使用場景調整損失函數,對于常見的二分類問題一般使用的binary:logistic損失函數,其形式為:
這個損失函數對于正類錯分和負類錯分給予的懲罰時相同的,但是對于不平衡數據集,或者某些特殊情況(兩類錯分代價不一樣)的時候這樣的損失函數就不再合理了。
XGBOOST很靈活,允許使用者自定義損失函數的形式,只需要輸入損失函數的一階導和二階導(grad,hess)即可。
基于XGBoost的損失函數的分解求導,可以知道XGBoost的除正則項以外的核心影響因子是損失函數的1階導和2階導,所以對于任意的學習任務的損失函數,可以對其求一階導數和二階導數帶入到XGBoost的自定義損失函數范式里面進行處理。
def custom_obj(pred, dtrain):# pred 和 dtrain 的順序不能弄反# STEP1 獲得labellabel = dtrain.get_label()# STEP2 如果是二分類任務,需要讓預測值通過sigmoid函數獲得0~1之間的預測值# 如果是回歸任務則下述任務不需要通過sigmoid# 分類任務sigmoid化def sigmoid(x):return 1/(1+np.exp(-x))sigmoid_pred = sigmoid(原始預測值)#回歸任務pred = 原始預測值# STEP3 一階導和二階導grad = 一階導hess = 二階導return grad, hess二.常見的損失函數
分類問題
非平衡分類學習任務,例如首筆首期30+的風險建模任務,首期30+的逾期率比例相對ever30+的逾期率為1/3左右,通過修正占比少的正樣本權重來對影響正樣本對損失函數的貢獻度,可以進一步提升模型的效果
def weighted_binary_cross_entropy(pred, dtrain,imbalance_alpha=10):# retrieve data from dtrain matrixlabel = dtrain.get_label()# compute the prediction with sigmoidsigmoid_pred = 1.0 / (1.0 + np.exp(-pred))# gradientgrad = -(imbalance_alpha ** label) * (label - sigmoid_pred)hess = (imbalance_alpha ** label) * sigmoid_pred * (1.0 - sigmoid_pred)return grad, hessFocal Loss
Focal Loss for Dense Object Detection 是ICCV2017的Best student paper,文章思路很簡單但非常具有開拓性意義,效果也非常令人稱贊。
大家還可以看知乎的討論:如何評價 Kaiming 的 Focal Loss for Dense Object Detection?
Focal Loss的引入主要是為了解決難易樣本數量不平衡(注意,有區別于正負樣本數量不平衡)的問題,實際可以使用的范圍非常廣泛,為了方便解釋,拿目標檢測的應用場景來說明
Focal Loss的主要思想就是改變損失函數.Focal loss是在交叉熵損失函數基礎上進行的修改
單階段的目標檢測器通常會產生高達100k的候選目標,只有極少數是正樣本,正負樣本數量非常不平衡。我們在計算分類的時候常用的損失——交叉熵的公式如下:
是經過激活函數的輸出,所以在0-1之間。可見普通的交叉熵對于正樣本而言,輸出概率越大損失越小。對于負樣本而言,輸出概率越小則損失越小。此時的損失函數在大量簡單樣本的迭代過程中比較緩慢且可能無法優化至最優。
為了解決正負樣本不平衡的問題,我們通常會在交叉熵損失的前面加上一個參數平衡因子alpha,用來平衡正負樣本本身的比例不均. 文中alpha取0.25,即正樣本要比負樣本占比小,這是因為負例易分。
只添加alpha雖然可以平衡正負樣本的重要性,,?對難易樣本的不平衡沒有任何幫助
根據正、負、難、易,樣本一共可以分為以下四類:
但這并不能解決全部問題, 實際上目標檢測中大量的候選目標都是像下圖一樣的易分樣本。
這些樣本的損失很低,但是由于數量極不平衡,易分樣本的數量相對來講太多,最終主導了總的損失。而本文的作者認為,易分樣本(即置信度高的樣本)對模型的提升效果非常小,模型應該主要關注與那些難分樣本
那么Focal loss是怎么改進的呢?
一個簡單的思想:把高置信度(p)樣本的損失再降低一些不就好了嗎!
首先在原有的基礎上加了一個因子,其中gamma>0使得減少易分類樣本的損失。使得更關注于困難的、錯分的樣本。
舉個例,gamma為2,對于正類樣本而言,預測結果為0.95肯定是簡單樣本,所以(1-0.95)的gamma次方就會很小,這時損失函數值就變得更小。而預測概率為0.3的樣本其損失相對很大。
對于負類樣本而言同樣,預測0.1的結果應當遠比預測0.7的樣本損失值要小得多。對于預測概率為0.5時,損失只減少了0.25倍,所以更加關注于這種難以區分的樣本。這樣減少了簡單樣本的影響,大量預測概率很小的樣本疊加起來后的效應才可能比較有效。
Focal Loss的最終形式結合了上面的公式解決了難易樣本的不平衡,解決了正負樣本的不平衡,結合使用,同時解決正負難易2個問題!
#focal 損失 # LGB+Focal Loss 其中alpha:為不能讓容易分類類別的損失函數太小, 默認值0.25; #gamma:更加關注困難樣本 即關注y=1的樣本 默認值2 from scipy.misc import derivative def focal_loss_lgb_sk( y_pred,dtrain, alpha=0.25, gamma=2):label = dtrain.get_label()a,g = alpha, gammadef fl(x,t):p = 1/(1+np.exp(-x))return -( a*t + (1-a)*(1-t) ) * (( 1 - ( t*p + (1-t)*(1-p)) )**g) * ( t*np.log(p)+(1-t)*np.log(1-p) )partial_fl = lambda x: fl(x, label)grad = derivative(partial_fl, y_pred, n=1, dx=1e-6)hess = derivative(partial_fl, y_pred, n=2, dx=1e-6)return grad, hess最終的Focal Loss形式如下:
gamma調節簡單樣本權重降低的速率,當gamma為0時即為交叉熵損失函數,當gamma增加時,調整因子的影響也在增加。實驗發現gamma為2是最優。
這樣以來,訓練過程關注對象的排序為正難>負難>正易>負易。
1. alpha ?(0,1)??二分類場景下,類似于正例的sample_weight概念,可以按照樣本占比,適度加權, 假如有5條正例、95條負例,則建議?alpha=0.95, 取0.5 相關于關掉此功能
2.?gamma???????? [0, +)??對于難度的區分程度,?取?gamma = 0?相當于關掉該功能,?gamma越大則越重視難度,即專注于比較困難的樣本。建議在 (0.5, 10.0)?范圍嘗試
自定義損失函數 XGBoost+Focal Loss
xgboost如果想應用focal loss可以使用自定義損失函數來實現。
首先介紹一個比較好用的求導的python包? sympy。用這個包來求導,可以比較輕松求出一階導和二階導。
下面的alpha 與上一節中 平衡因子alpha 不太一樣, 相當于?? 上一節中?? 只在y=1處加入了alpha,? 然后y=0不加入。?? # alpha指的是用來處理非平衡的參數, label為1的樣本的損失是label為0的多少倍(1-N)
from sympy import diff, symbols, log# y指的是true label # p指的是預測出來的概率值(是經過sigmoid轉換之后的) # gamma指的是local foss的參數 # alpha指的是用來處理非平衡的參數 # 同時考慮了focal loss參數和處理非平衡參數的損失函數 y, p, gamma, alpha = symbols('y p gamma alpha') loss = alpha * (-y * log(p) * (1 - p) ** gamma) - (1 - alpha) * (1 - y) * log(1 - p) * p ** gamma grad = diff(loss, p) * p * (1 - p) # 求一階導 print('grad:') print(grad) hess = diff(grad, p) * p * (1 - p) # 求二階導 print('hess:') print(hess)''' grad: p*(1 - p)*(alpha*gamma*y*(1 - p)**gamma*log(p)/(1 - p) - alpha*y*(1 - p)**gamma/p - gamma*p**gamma*(1 - alpha)*(1 - y)*log(1 - p)/p + p**gamma*(1 - alpha)*(1 - y)/(1 - p)) hess: p*(1 - p)*(p*(1 - p)*(-alpha*gamma**2*y*(1 - p)**gamma*log(p)/(1 - p)**2 + alpha*gamma*y*(1 - p)**gamma*log(p)/(1 - p)**2 + 2*alpha*gamma*y*(1 - p)**gamma/(p*(1 - p)) + alpha*y*(1 - p)**gamma/p**2 - gamma**2*p**gamma*(1 - alpha)*(1 - y)*log(1 - p)/p**2 + 2*gamma*p**gamma*(1 - alpha)*(1 - y)/(p*(1 - p)) + gamma*p**gamma*(1 - alpha)*(1 - y)*log(1 - p)/p**2 + p**gamma*(1 - alpha)*(1 - y)/(1 - p)**2) - p*(alpha*gamma*y*(1 - p)**gamma*log(p)/(1 - p) - alpha*y*(1 - p)**gamma/p - gamma*p**gamma*(1 - alpha)*(1 - y)*log(1 - p)/p + p**gamma*(1 - alpha)*(1 - y)/(1 - p)) + (1 - p)*(alpha*gamma*y*(1 - p)**gamma*log(p)/(1 - p) - alpha*y*(1 - p)**gamma/p - gamma*p**gamma*(1 - alpha)*(1 - y)*log(1 - p)/p + p**gamma*(1 - alpha)*(1 - y)/(1 - p))) '''注意: xgboost版本為 0.90
在xgboost里邊設置自定義損失函數,將上邊生成的一階導和二階導貼過來。
當設置alpha=0.5, gamma=0時,就是原始的損失函數, 相當于logloss*0.5,對比輸出的結果可以判斷求導是否正確。
import numpy as np import xgboost as xgbgamma = 0 alpha = 0.5# y指的是true label # p指的是預測出來的概率值(是經過sigmoid轉換之后 p = 1.0 / (1.0 + np.exp(-p)) # gamma指的是local foss的參數 # alpha指的是用來處理非平衡的參數, label為1的樣本的損失是label為0的多少倍 def logistic_obj(p, dtrain):y = dtrain.get_label()p = 1.0 / (1.0 + np.exp(-p))grad = p * (1 - p) * (alpha * gamma * y * (1 - p) ** gamma * np.log(p) / (1 - p) - alpha * y * (1 - p) ** gamma / p - gamma * p ** gamma * (1 - alpha) * (1 - y) * np.log(1 - p) / p + p ** gamma * (1 - alpha) * (1 - y) / (1 - p))hess = p * (1 - p) * (p * (1 - p) * (-alpha * gamma ** 2 * y * (1 - p) ** gamma * np.log(p) / (1 - p) ** 2 + alpha * gamma * y * (1 - p) ** gamma * np.log(p) / (1 - p) ** 2 + 2 * alpha * gamma * y * (1 - p) ** gamma / (p * (1 - p)) + alpha * y * (1 - p) ** gamma / p ** 2 - gamma ** 2 * p ** gamma * (1 - alpha) * (1 - y) * np.log(1 - p) / p ** 2 + 2 * gamma * p ** gamma * (1 - alpha) * (1 - y) / (p * (1 - p)) + gamma * p ** gamma * (1 - alpha) * (1 - y) * np.log(1 - p) / p ** 2 + p ** gamma * (1 - alpha) * (1 - y) / (1 - p) ** 2) - p * (alpha * gamma * y * (1 - p) ** gamma * np.log(p) / (1 - p) - alpha * y * (1 - p) ** gamma / p - gamma * p ** gamma * (1 - alpha) * (1 - y) * np.log(1 - p) / p + p ** gamma * (1 - alpha) * (1 - y) / (1 - p)) + (1 - p) * (alpha * gamma * y * (1 - p) ** gamma * np.log(p) / (1 - p) - alpha * y * (1 - p) ** gamma / p - gamma * p ** gamma * (1 - alpha) * (1 - y) * np.log(1 - p) / p + p ** gamma * (1 - alpha) * (1 - y) / (1 - p)))return grad, hessdtrain = xgb.DMatrix('../demo_data/agaricus.txt.train') dtest = xgb.DMatrix('../demo_data/agaricus.txt.test')print("---------no focal loss-----------")watchlist = [(dtrain, 'train'), (dtest, 'eval')]params = {'max_depth': 2, 'eta': 0.01, 'silent': 1, 'eval_metric': 'auc', "objective": "binary:logistic"} xgb.train(params=params, dtrain=dtrain, num_boost_round=3, early_stopping_rounds=50,evals=[(dtrain, 'train'), (dtest, 'test')], verbose_eval=1)print("---------focal loss-----------") params = {'max_depth': 2, 'eta': 0.01, 'silent': 1, 'eval_metric': 'auc', "objective": "binary:logitraw"} xgb.train(params=params, dtrain=dtrain, num_boost_round=3, early_stopping_rounds=50,evals=[(dtrain, 'train'), (dtest, 'test')], verbose_eval=1, obj=logistic_obj)回歸問題
美團的ETA預估
深度學習在美團配送ETA預估中的探索與實踐 - 美團技術團隊
通過對損失函數進行修正大幅度提高了業務預測效果(使得結果傾向于提前到達)
1.帶懲罰的最小二乘損失函數
def custom_normal_train( y_pred,dtrain):label = dtrain.get_label()residual = (label - y_pred).astype("float")grad = np.where(residual<0, -2*(residual)/(label+1), -10*2*(residual)/(label+1))#對預估里程低于實際里程的情況加大懲罰hess = np.where(residual<0, 2/(label+1), 10*2/(label+1))#對預估里程低于實際里程的情況加大懲罰return grad, hesshttps://www.kaggle.com/c/allstate-claims-severity/discussion/24520#140255
kaggle 的保險定價競賽中評價函數為MAE(存在不可導區域),通過使用下述的損失函數來獲得MAE的近似損失函數來求解該問題
1.fair loss
def fair_obj( preds,dtrain):labels = dtrain.get_label() con = 2residual = preds-labelsgrad = con*residual / (abs(residual)+con)hess = con**2 / (abs(residual)+con)**2return grad,hesshttps://www.kaggle.com/c/allstate-claims-severity/discussion/24520#140255?提供了非常高質量的近似MAE損失函數優化的討論
2.mae的近似損失函數- Pseudo Huber loss
def huber_approx_obj( preds,dtrain): d = preds -dtrain.get_label() h = 1 #h is delta in the formula scale = 1 + (d / h) ** 2 scale_sqrt = np.sqrt(scale) grad = d / scale_sqrt hess = 1 / scale / scale_sqrt return grad, hess3.mae的近似損失函數 ln(cosh) loss
def logcoshobj( preds,dtrain): label = dtrain.get_label()d = preds - labelgrad = np.tanh(d)/label hess = (1.0 - grad*grad)/label return grad, hess總結
以上是生活随笔為你收集整理的[机器学习] XGBoost 自定义损失函数-FocalLoss的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 便宜虚拟主机租用要注意哪些问题
- 下一篇: Mrtg的配置以及具体安装方法