一 样本不平衡

何谓样本不平衡——简单来说就是数据集中负样本的数量远远大于正样本的数量。在这个情况下,模型就会倾向于把样本预测为负样本,因为这是最便捷的降低损失、提高模型准确率的方法。例如:有一个正样本数量为1,负样本数量为99的数据集,模型就算无脑地把全部样本预测为负样本也能达到99%的准确度,试想有这么一个分类器,每次我们把数据喂‘给它时,在不调整阈值的情况下,它都倾向于把测试集的样本预测为负样本,你觉得这样的分类器还会是一个好的分类器吗?

在对不平衡数据进行训练时,通常会考虑一下怎么处理不平衡数据能使训练出来的结果较好。

在用xgboost训练二分类模型时,除了直接使用过采样和下采样,xgboost接口还提供一些处理不平衡数据的方法,有scale_pos_weight参数的设置,还有给样本赋予一定的权重。接下来让我们仔细看一下吧~

参数scale_pos_weight:

XGBoost中存在着调节样本不平衡的参数scale_pos_weight,通常我们在参数中输入的是负样本量与正样本量之比

底层原理:通过scale_pos_weight改变了正负样本的权重,进而改变了正负样本损失函数的大小,最终改变了样本的概率预测值 wi。

For common cases such as ads clickthrough log, the dataset is extremely imbalanced. This can affect the training of xgboost model, 
and there are two ways to improve it.
  If you care only about the ranking order (AUC) of your prediction
      Balance the positive and negative weights, via scale_pos_weight
      Use AUC for evaluation
  If you care about predicting the right probability
      In such a case, you cannot re-balance the dataset
      In such a case, set parameter max_delta_step to a finite number (say 1) will help convergence

官方的解释是这样的,scale_pos_weight 可以设置为数据中负样本数量/正样本数量

if (info.labels[i] == 1.0f) w *= param_.scale_pos_weight

scale_pos_weight 是用来调节正负样本不均衡问题的,用助于样本不平衡时训练的收敛。

scale_pos_weight = 负样本总数/正样本总数 。若训练负样本总数是500 ,正样本总数100,那么设置 scale_pos_weigh为 5 。

 

常见的情况比如广告点击日志,数据集是极其不平衡的,导致训练的xgboost模型受到影响,有两种方法可以改善:

1)  如果仅仅关注预测问题的AUC指标,那么你可以调节 scale_pos_weight参数来帮助训练数据不平衡带来的收敛问题 。

2)如果关注预测概率的准确性问题,那么你就不能调节scale_pos_weight参数来改变样本权重的方法帮助收敛,可通过设置参数 max_delta_step 为一个有限的值来帮助收敛 。


如何你仅仅关注预测问题的排序或者AUC指标,那么你尽管调节此参数。如果你希望得到真正的预测概率则不能够通过此参数来平衡样本。什么意思呢,让我们来举个例子:加入我们现在需要通过体重来预测男女,有三个人体重分别为50kg、60kg、70kg。假设他们是男生的真正概率是:0.4、0.6、0.8。那么好,我现在模型预测出的概率为:0.7、0.8、0.9。如果讲预测概率的话,显然模型效果很差,但是我们预测的男生概率的排序以及 ROU 曲线(包括对应 AUC 值)都不会改变。

 关于scale_pos_weight ,在xgboost里边的源码是

          bst_float w = _is_null_weight ? 1.0f : _weights[_idx];
          bst_float label = _labels[_idx];
          if (label == 1.0f) {
            w *= _scale_pos_weight;
          }
          if (!Loss::CheckLabel(label)) {
            // If there is an incorrect label, the host code will know.
            _additional_input[0] = 0;
          }
          # 改变了损失函数,即改变了损失函数的一阶导和二阶导大小,落到每个叶结点的概率预测值也随之改变。
          _out_gpair[_idx] = GradientPair(Loss::FirstOrderGradient(p, label) * w,
                                          Loss::SecondOrderGradient(p, label) * w);
        },


# 见源码 src/objective/regression_obj.cu

从源代码也可以看出,scale_pos_weight其实就是改变样本weight,也就是和在DMatrix里边设置每个样本的weight是一样的。

在DMatrix里边设置每个样本的weight 是怎样改变训练过程的呢,其实是改变训练的损失函数,源代码里的代码如下,可以看到对不同的样本赋予不同的权重实际上是影响了该样本在训练过程中贡献的损失,进而改变了一阶导和二阶导。

其中​是损失函数关于上一棵树的预测值的的一阶导,​是损失函数关于上一棵树的预测值的二阶导,当正样本的损失函数发生改变,相应的叶子节点的预测值也会发生改变。

本质上说,scale_pos_weight参数也是通过调节样本的预测概率值来改变预测结果,当我们需要提高模型的评估标准 ——如 auc、召回率recall的大小等,可以通过调整scale_pos_weight参数达到我们的目的;但是,假如需要确切了解每个样本为正样本的可能性大小时,不宜使用scale_pos_weight参数。

使用官方数据例子:agaricus.txt.train 与 agaricus.txt.test

系统Default,设置scale_pos_weight = 1。观察训练过程

print("\n--------- scale_pos_weight = 1 -----------")
dtrain = xgb.DMatrix('../demo_data/agaricus.txt.train')
dtest = xgb.DMatrix('../demo_data/agaricus.txt.test')
watchlist = [(dtrain, 'train'), (dtest, 'eval')]
params = {'max_depth': 2, 'eta': 1, 'silent': 1, 'eval_metric': 'auc', "objective": "binary:logistic",
          "scale_pos_weight": 1}
xgb.train(params=params, dtrain=dtrain, num_boost_round=5, evals=watchlist, verbose_eval=1)

不平衡对比

使用weight给样本赋予权重。

label=1,权重设置为3

label=0,  权重设置为1

print("\n--------- custom set weight=3, scale_pos_weight=1-----------")
dtrain = xgb.DMatrix('../demo_data/agaricus.txt.train', silent=True)
dtest = xgb.DMatrix('../demo_data/agaricus.txt.test', silent=True)
train_weight = [1 if i == 0 else 3 for i in dtrain.get_label().tolist()]
test_weight = [1 if i == 0 else 3 for i in dtest.get_label().tolist()]
dtrain2 = xgb.DMatrix('../demo_data/agaricus.txt.train', weight=train_weight, silent=True)
dtest2 = xgb.DMatrix('../demo_data/agaricus.txt.test', weight=test_weight, silent=True)
watchlist = [(dtrain2, 'train'), (dtest2, 'eval')]
params = {'max_depth': 2, 'eta': 1, 'silent': 1, 'eval_metric': 'auc', "objective": "binary:logistic",
          "scale_pos_weight": 1}
xgb.train(params=params, dtrain=dtrain2, num_boost_round=5, evals=watchlist, verbose_eval=1)

 将正样本复制成原来的3倍,和负样本拼起来组成训练集训练

print("\n---------read agaricus.txt.xxx_weight3 scale_pos_weight = 1 -----------")
dtrain = xgb.DMatrix('../demo_data/agaricus.txt.train_weight3')
dtest = xgb.DMatrix('../demo_data/agaricus.txt.test_weight3')
watchlist = [(dtrain, 'train'), (dtest, 'eval')]
params = {'max_depth': 2, 'eta': 1, 'silent': 1, 'eval_metric': 'auc', "objective": "binary:logistic",
          "scale_pos_weight": 1}
xgb.train(params=params, dtrain=dtrain, num_boost_round=5, evals=watchlist, verbose_eval=1)

scale_pos_weight = 3

print("\n--------- scale_pos_weight = 3-----------")
dtrain = xgb.DMatrix('../demo_data/agaricus.txt.train', silent=True)
dtest = xgb.DMatrix('../demo_data/agaricus.txt.test', silent=True)
watchlist = [(dtrain, 'train'), (dtest, 'eval')]
params = {'max_depth': 2, 'eta': 1, 'silent': 1, 'eval_metric': 'auc', "objective": "binary:logistic",
          "scale_pos_weight": 3}
xgb.train(params=params, dtrain=dtrain, num_boost_round=5, evals=watchlist, verbose_eval=1)

自定义损失函数,

将正样本的logloss设置为负样本的3倍,然后求一阶导和二阶导.

print("\n---------custom set weight=3, scale_pos_weight=1, use custom obj-----------")

from sklearn.metrics import roc_auc_score

ratio = 3

def logistic_obj(p, dtrain):
    y = dtrain.get_label()
    grad = p * (ratio * y + 1 - y) - ratio * y
    hess = p * (1 - p) * (beta * y + 1 - y)
    return grad, hess


def logistic_obj_feval(p, dtrain):
    y = dtrain.get_label()
    score = roc_auc_score(y, p)
    return 'feval-auc', score


dtrain = xgb.DMatrix('../demo_data/agaricus.txt.train', silent=True)
dtest = xgb.DMatrix('../demo_data/agaricus.txt.test', silent=True)
train_weight = [1 if i == 0 else 3 for i in dtrain.get_label().tolist()]
test_weight = [1 if i == 0 else 3 for i in dtest.get_label().tolist()]
dtrain2 = xgb.DMatrix('../demo_data/agaricus.txt.train', weight=train_weight, silent=True)
dtest2 = xgb.DMatrix('../demo_data/agaricus.txt.test', weight=test_weight, silent=True)
watchlist = [(dtrain2, 'train'), (dtest2, 'eval')]
params = {'max_depth': 2, 'eta': 1, 'silent': 1, 'eval_metric': 'auc', "objective": "binary:logistic",
          "scale_pos_weight": 1}
xgb.train(params=params, dtrain=dtrain2, num_boost_round=5,
          evals=watchlist, verbose_eval=1, obj=logistic_obj, feval=logistic_obj_feval)

5. 设置 subsample

如果参数subsample < 1 ,此时将正样本复制成原来的3倍,训练结果还是会不一样,因为样本总数改变了,子采样就会不一样, 其他情况输出不会变。

print("\n--------- scale_pos_weight = 3, subsample=0.8-----------")
dtrain = xgb.DMatrix('../demo_data/agaricus.txt.train', silent=True)
dtest = xgb.DMatrix('../demo_data/agaricus.txt.test', silent=True)
watchlist = [(dtrain, 'train'), (dtest, 'eval')]
params = {'max_depth': 2, 'eta': 1, 'silent': 1, 'eval_metric': 'auc', "objective": "binary:logistic",
          "scale_pos_weight": 3, "subsample":0.8}
xgb.train(params=params, dtrain=dtrain, num_boost_round=5, evals=watchlist, verbose_eval=1)


print("\n---------read agaricus.txt.xxx_weight3 scale_pos_weight = 1, subsample=0.8 -----------")
dtrain = xgb.DMatrix('../demo_data/agaricus.txt.train_weight3')
dtest = xgb.DMatrix('../demo_data/agaricus.txt.test_weight3')
watchlist = [(dtrain, 'train'), (dtest, 'eval')]
params = {'max_depth': 2, 'eta': 1, 'silent': 1, 'eval_metric': 'auc', "objective": "binary:logistic",
          "scale_pos_weight": 1, "subsample":0.8}
xgb.train(params=params, dtrain=dtrain, num_boost_round=5, evals=watchlist, verbose_eval=1)

三 结论

sample_weight参数允许您为每个训练示例指定不同的权重。 scale_pos_weight参数允许您为整类示例(“正”类)提供权重。

这些对应于成本敏感学习的两种不同方法。如果您认为错误分类正面示例(错过癌症患者)的成本对于所有正面示例都是相同的(但比错误分类负面示例的成本更高,例如告诉某人他们实际上没有癌症),那么您可以指定一个通过 scale_pos_weight 获得所有正面示例的权重.

参考:

样本不平衡问题操作手册 - 知乎


不平衡处理:xgboost 中scale_pos_weight、给样本设置权重weight、 自定义损失函数 和 简单复制正样本的区别_大风车-CSDN博客

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐