通过autograd或jax 快速实现自定义损失函数下的lightgbm
- 2021 年 1 月 28 日
- AI
autograd目前和xla一起集成到jax里了,google/jaxautograd目前和xla一起积极维护jax去了,不过jax的应用和autograd差不多,就是api有一些不同,不过jax目前在windows上没法使用,autograd倒是不限制系统,对自动求导感兴趣的各位大佬可以直接上手jax,性能高,lightgbm验证起来非常方便;
之前写自定义损失函数的时候总是需要自己去推导一下损失函数的一阶和二阶梯度的表达式,这一块儿后来找到了sympy,但是总觉得不太方便,后来找到了autograd,顺藤摸瓜找到了jax。
下面主要是autograd应用于lightgbm的demo,jax有时间我再好好研究一下,真是个好东西~~~~~
下面我们定义一个smape自定义损失函数:
from autograd import hessian
from autograd import grad
import autograd.numpy as np
def smape_eval(y_true, y_pred):
y_ture=np.array(y_true)
result= np.abs(y_pred - y_true) / (np.abs(y_pred) + np.abs(y_true))
return result
smape求导的一个问题是如果分母为0,也就是y_true=y_pred=0的时候,式子是没有意义的,在numpy下分母为0计算的结果为nan,使用autograd求导的结果也是一样的。
然后我们直接使用autograd的egrad功能,
grad_smape=egrad(smape_eval,1)
hessian_smape=egrad(egrad(smape_eval,1))
这里的函数smape_eval是之前写自定义评价指标用的,一开始实现smape一直有问题,所以用的mape,但是如果模型能够直接优化评价指标,可坑效果要更好,比如我们auc这种评价指标难以计算其一阶梯度和二阶梯度,如果lgb可以直接针对其进行优化,可能效果会更好。
这里的1用于指定我们要求导的变量,因为smape_eval里有两个变量,通过1指定我们使用第二个变量,0就是指定第一个变量了:
grad_smape=egrad(smape_eval,1)
hessian_smape=egrad(egrad(smape_eval,1))
print(grad_smape(np.array([0,0,0]),np.array([0,0,0,])))
print(hessian_smape(np.array([1,1,1]),np.array([2,2,2,])))

可以看一下这里的计算结果;
然后我们就可以开始愉快的定义自定义损失函数了:
def smape_loss(labels,preds):
# masked_arr = ~((preds==0)&(labels==0))
# preds, labels = preds[masked_arr], labels[masked_arr]
grad = grad_smape(labels,preds)
hess = hessian_smape(labels,preds)
grad[np.isnan(grad)]=0
hess[np.isnan(hess)]=0
return grad, hess
将smape_loss放到lgb的参数里:
lgb_params = {
'min_child_weight': 0.03454472573214212, #祖传参数
'feature_fraction': 0.3797454081646243,
'bagging_fraction': 0.4181193142567742,
'min_data_in_leaf': 106,
'objective': smape_loss,
'max_depth': -1,
'learning_rate': 0.006883242363721497,
"boosting_type": "gbdt",
"bagging_seed": 11,
"verbosity": -1,
'reg_alpha': 0.3899927210061127,
'reg_lambda': 0.6485237330340494,
'random_state': 47,
'n_estimators':10000,
'n_jobs':-1,
#'device': 'gpu',
#'gpu_platform_id' : 0,
#'gpu_device_id' : 0,
'metric':None
}
from sklearn.model_selection import KFold
kf=KFold(5)
cols=list(y.columns)
best_score=100
y_pred0 = np.zeros((y.shape[0], y.shape[1]))
y_all_pred0 = np.zeros((n_bag, y_all.shape[0], y_all.shape[1]))
for i in range(y.shape[1]):
for fold, (train_idx, test_idx) in enumerate(kf.split(X, y)):
model =lgb.LGBMRegressor(**lgb_params)
#model.fit(X,y[cols[i]])
X_train,y_train = X.iloc[train_idx], y.iloc[train_idx][cols[i]] # 每一次单独对一个时间步建立一个模型
X_test, y_test = X.iloc[test_idx], y.iloc[test_idx][cols[i]]
model.fit(X_train,y_train,eval_set=[(X_train,y_train),(X_test,y_test)],early_stopping_rounds=100, \
eval_metric=smape_eval,verbose=50)
print('done!')
y_pred = model.predict(X_test)
y_all_pred = model.predict(X_all)
y_pred0[test_idx,i:i+1] = y_pred.reshape(-1,1)
y_all_pred0[fold,:,i:i+1] = y_all_pred.reshape(-1,1)
y_pred += test2.AllVisits.values[test_idx]
y_pred = np.expm1(y_pred)
y_pred[y_pred < 0.5 * offset] = 0
res = smape(test2[y_cols[i]].values[test_idx], y_pred)
y_pred = offset*((y_pred / offset).round())
res_round = smape(test2[y_cols[i]].values[test_idx], y_pred)
y_all_pred += test_all2.AllVisits.values
y_all_pred = np.expm1(y_all_pred)
y_all_pred[y_all_pred < 0.5 * offset] = 0
res_all = smape(test_all2[y_cols[i]], y_all_pred)
y_all_pred = offset*((y_all_pred / offset).round())
res_all_round = smape(test_all2[y_cols[i]], y_all_pred)
print('smape train: %0.5f' % res, 'round: %0.5f' % res_round,
' smape LB: %0.5f' % res_all, 'round: %0.5f' % res_all_round)
del X_train,y_train,X_test,y_test;gc.collect()

没得问题了,有时间好好研究一下jax的用法~感动,不过简单应用的话用autograd就可以了。