多任务梯度提升树MT-GBM(含KL Loss求导)的使用小结
论文:https://arxiv.org/abs/2201.06239
代码:https://github.com/mtgbmcode/mtgbmcode-1
一、多任务学习
● NN:共享中间层的参数,对于不同的任务构建特殊的任务层
● Traditional MT-GBDT:每个任务一个GBDT,任务间学习相互独立
● MT-GBM:共享树结构,称为同构异值树,为不同的目标提供相同的分裂结构和不同的输出值。
二、MT-GBM
● 通过计算融合Gradients和Hessians,用来划分节点。
● 通过计算每个样本在多目标上,单独的Gradients和Hessians,更新叶子节点值。
它是基于lightgbm开源库实现的,库名为:lightgbmmt。在论文中的效果:
使用时,要自定义objective和metric函数。开源代码中,作者的回归实验是:对一段时间中外外汇量进行预测,我们将原本的单目标交易量级转化为交易量涨跌幅和交易量级两个强相关且拥有不同意义的目标进行学习,在mape获得提升效果。
num_labels = 2 # 双label
# 设置评估函数,RMSE
def self_metric(preds, train_data):
labels = train_data.get_label()
# 注意:这里作者只关注第二个目标(即交易量级)的分数,因为预测目标就是交易量级
labels2 = labels.reshape((num_labels,-1)).transpose()[:,1]
preds2 = preds.reshape((num_labels,-1)).transpose()[:,1]
score = np.mean((labels2-preds2) ** 2)**0.5
return 'rmse', score, False
# 设置目标函数,RMSE
def mymse2(preds, train_data, ep = ):
labels = train_data.get_label()
labels2 = labels.reshape((num_labels,-1)).transpose()
preds2 = preds.reshape((num_labels,-1)).transpose()
grad2 = (preds2 - labels2)
# 注意:这里作者是做加权融合,权重的设置应该配合数目标的量纲,尽可能将不同目标的gradient放在同一量纲上
grad = grad2 * np.array([1.5,0.001])
grad = np.sum(grad,axis = 1)
grad2 = grad2.transpose().reshape((-1))
hess = grad * 0. + 1
hess2 = grad2 * 0. + 1
return grad, hess, grad2, hess2
param = {
'num_leaves':48,
'max_depth':6,
'learning_rate':.03,
'max_bin':200,
'lambda_l1':0.1,
'lambda_l2':0.2,
'verbose': 5,
# multitask 多任务的参数设置
'objective':'custom',
'num_labels':num_labels,
'tree_learner': 'serial2',
'num_threads':4}
target = ['amount2','amount'] # 个label为较前一天增长百分比,第二个label为值
X_train = df_feature[df_feature['report_date']<startdate][features]
y_train = df_feature[df_feature['report_date']<startdate][target].values
X_valid = df_feature[df_feature['report_date']>=startdate][features]
y_valid = df_feature[df_feature['report_date']>=startdate][target].values
# 查目标之间的相关性
print(df_feature[target].corr())
print(X_train.shape,X_valid.shape)
print(y_train.shape,y_valid.shape)
evals_result_mt = {}
train_data=lgb.Dataset(X_train,label=y_train)
validation_data=lgb.Dataset(X_valid,label=y_valid)
clf=lgb.train(param,train_data,verbose_eval=10,
fobj = mymse2,feval = self_metric,
num_boost_round=200,valid_sets=[train_data,validation_data],
evals_result=evals_result_mt)
clf.set_num_labels(2)
那假设我要预测股票未来一个月的收益均值和方差,我时间可能会想到:mu和sigma作为多目标,使用MT-GBM学习。那我要自定义KL metic和KL loss:
# KL散度 评估函数
def KL_metric(preds, train_data):
num_labels = 2
labels = train_data.get_label()
labels = labels.reshape((num_labels,-1)).transpose()
preds = preds.reshape((num_labels,-1)).transpose()
# +1e-8是因为MTGBM用0是初始化pred, sigma为0会导致KL散度为inf
label_mu = torch.from_numpy(labels[:,0]).float() # 多个mu
label_sigma = torch.from_numpy(labels[:,1]).float()
pred_mu = torch.from_numpy(preds[:,0]).float() + 1e-8
pred_sigma = torch.from_numpy(preds[:,1]).float() + 1e-8
p = torch.distributions.Normal(pred_mu, pred_sigma)
q = torch.distributions.Normal(label_mu, label_sigma)
kls = torch.distributions.kl_divergence(p, q).numpy()
kls_score = np.mean(kls)
return 'kl score', kls_score, False
# KL loss 目标函数
def KL_obj(preds, train_data, ep=0):
num_labels = 2
labels = train_data.get_label()
labels = labels.reshape((num_labels,-1)).transpose()
preds = preds.reshape((num_labels,-1)).transpose()
label_mu = torch.from_numpy(labels[:,0]).float() + 1e-8 # 多个mu
label_sigma = torch.from_numpy(labels[:,1]).float() + 1e-8
label_mu.requires_grad = False
label_sigma.requires_grad = False
pred_mu = torch.from_numpy(preds[:,0]).float() + 1e-8
pred_sigma = torch.from_numpy(preds[:,1]).float() + 1e-8
pred_mu.requires_grad = True
pred_sigma.requires_grad = True
# KL Loss func: f(label_mu, label_sigma, pred_mu, pred_sigma)
# ref: https://blog.csdn.net/weixin_41396062/article/details/104623348
# 公式中:p为pred, q为label, 小标1为pred,小标q为2
a = torch.div(label_sigma, pred_sigma)
log_a = torch.log(a)
b_numerator1 = torch.pow(pred_sigma, 2)
b_numerator2 = torch.sub(pred_mu, label_mu)
b_numerator3 = torch.pow(b_numerator2, 2)
b_numerator = torch.add(b_numerator1, b_numerator3)
b_denominator = 2 * torch.pow(label_sigma, 2)
b = torch.div(b_numerator, b_denominator)
kl_loss = log_a + b - (1/2)
# creat_graph=True在保留原图的基础上再建立额外的求导计算图, retain_graph=True保留了计算图和中间变量梯度
# ref: https://zhuanlan.zhihu.com/p/279758736
# 求导时,x可以是标量或者向量,但y只能是标量,若y为向量,需要加入grad_outputs
grad_mu = torch.autograd.grad(kl_loss, pred_mu, grad_outputs=torch.ones(pred_mu.shape), create_graph=True, retain_graph=True)[0]
grad_sigma = torch.autograd.grad(kl_loss, pred_sigma, grad_outputs=torch.ones(pred_sigma.shape), create_graph=True, retain_graph=True)[0]
# 因为后面不需要求导了,不需要再保留求导计算图
hess_mu = torch.autograd.grad(grad_mu, pred_mu, grad_outputs=torch.ones(pred_mu.shape), create_graph=False)[0]
hess_sigma = torch.autograd.grad(grad_sigma, pred_sigma, grad_outputs=torch.ones(pred_sigma.shape), create_graph=False)[0]
# tensor转为numpy
grad_mu, grad_sigma, hess_mu, hess_sigma = grad_mu.detach().numpy(), grad_sigma.detach().numpy(), hess_mu.detach().numpy(), hess_sigma.detach().numpy()
# 节点分裂:求每个样本的一阶和二阶的融合梯度,shape=(num_of_sample,)
grad = grad_mu + grad_sigma
hess = hess_mu + hess_sigma
# 节点更新:求原始每个样本和label的一阶梯度和二阶梯度, shape=(num_of_sample*num_of_label,)
grad2 = np.array(list(grad_mu) + list(grad_sigma))
hess2 = np.array(list(hess_mu) + list(hess_sigma))
return grad, hess, grad2, hess2
三、实验发现
1. 使用跟作者开源回归代码中的RMSE作为目标和评估函数,比KL Loss更好些。
2. 在训练模型时,轮模型会用0初始化预测结果,在KL loss求导时,sigma一阶梯度(grad_sigma)会因此变很大,人工对mu和sigma梯度进行加权平衡,也没起到作用(此时也很难加权,因为若pred_sigma为0,sigma的一阶导数会为inf)。
3. 多目标任务学习看重两个点:
(1)任务之间的相关性,这也是为什么作者在自己回归实验前,有做多目标的相关性分析,并强调要是两个强相关且拥有不同意义的目标。(可是他交易量涨跌幅和交易量级的相关性也才0.09);
(2)不同任务对应loss的量纲,要尽量保证在一个范围内,不然loss范围大的loss自然就会更被模型青睐。在MTGBM中体现在融合梯度上(用于节点划分),而KL loss不适合用在这里,因为mu和sigma的求导过程会使它们的梯度不属于相同量纲下,因此使用RMSE作为目标函数,通过加权配平,反而更好些。
其它:使用torch.autograd.grad能减少人工推导公式,提高求导效率。
相关文章