在Spark MLlib中处理不平衡数据集

36

我正在处理一个高度不平衡的二分类问题,想知道是否有人尝试过使用特定技术来处理不平衡数据集(例如 SMOTE),并应用于Spark的MLlib分类问题。

我正在使用MLlib的随机森林实现,并已经尝试了最简单的方法——对较大类别进行随机欠采样,但效果不如我预期。

如果您有类似问题的经验,请提供任何反馈。

谢谢。


SMOTEBoost算法建议使用弱学习算法训练数据集。为什么不实现像这样的东西:https://issues.apache.org/jira/browse/SPARK-1546 - kamaci
1
@eliasah,我的意思是我的数据集中正例非常少,相对于负例(大约每100个样本只有1个正例)。训练好的分类器偏向于多数类(负例),在这个类上具有更高的预测准确性,但在少数类上的预测准确性较差。"没有按预期工作"的意思是分类器的精度大约为60-70%(即60-70%的正例被正确分类),在进行10折交叉验证测试时。 - dbakr
这意味着您需要调整模型参数,或者随机森林可能不适合您的数据模型。您是否执行了网格搜索以查找参数配置? - eliasah
1
你的正类有多么相互关联和密集?特征是离散还是连续的?RF适用于具有局部连接的离散数据集上的离散数据。如果点是全局连接的(一个大块),那么你可以考虑SVM、谱聚类,甚至是k-means。 - Prune
3
@eliasah “二元分类不受不平衡数据的影响”。你有这个说法的任何参考资料吗?我不是说这不是真的,但至少对我来说不是很直观。 - kon psych
12
“二分类不受不平衡数据影响”——这绝对是不正确的。 - Serendipity
3个回答

56

使用Spark ML的类权重

目前,随机森林算法的类权重仍在开发中(请参见此处)。

但是,如果您愿意尝试其他分类器,这个功能已经被添加到了逻辑回归中(点此查看)。

考虑一个数据集中有80%的正例(标签==1),理论上我们想要“欠采样”正类。逻辑损失目标函数应该以更高的权重处理负类(标签==0)。

以下是在Scala中生成这个权重的示例,我们为数据集中的每条记录添加一个新列:

def balanceDataset(dataset: DataFrame): DataFrame = {

    // Re-balancing (weighting) of records to be used in the logistic loss objective function
    val numNegatives = dataset.filter(dataset("label") === 0).count
    val datasetSize = dataset.count
    val balancingRatio = (datasetSize - numNegatives).toDouble / datasetSize

    val calculateWeights = udf { d: Double =>
      if (d == 0.0) {
        1 * balancingRatio
      }
      else {
        (1 * (1.0 - balancingRatio))
      }
    }

    val weightedDataset = dataset.withColumn("classWeightCol", calculateWeights(dataset("label")))
    weightedDataset
  }

然后,我们按照以下方式创建分类器:

new LogisticRegression().setWeightCol("classWeightCol").setLabelCol("label").setFeaturesCol("features")

更多详情请查看:https://issues.apache.org/jira/browse/SPARK-9610

- 预测能力

你应该检查的一个不同问题-您的特征是否对您尝试预测的标签具有"预测能力"。在进行欠采样后仍然具有低精度的情况下,也许这与您的数据集本质上不平衡无关。


我会进行探索性数据分析 - 如果分类器不能比随机选择做得更好,则存在一种风险,即特征和类别之间根本没有联系。

  • 对每个特征与标签执行相关性分析
  • 为特征生成特定于类的直方图(即绘制每个类的数据的直方图,对于给定的特征在相同轴上),也可以是展示特征是否很好地区分两个类别的好方法。

过度拟合- 训练集上的误差较小,测试集上的误差较大可能表明您使用过于灵活的特征集过度拟合了。


偏差方差- 检查您的分类器是否存在高偏差或高方差问题。

  • 训练误差与验证误差-以训练示例的函数形式, 绘制验证误差和训练集误差的图表
    • 如果这些线似乎收敛到相同的值并且在最后非常接近,则您的分类器具有高偏差。在这种情况下,添加更多数据是没有帮助的。更改分类器为具有更高方差的分类器,或者只降低当前分类器的正则化参数。
    • 另一方面,如果这些线相当远,并且您的训练集误差很低,但验证误差很高,则您的分类器具有太高的方差。在这种情况下,增加更多数据非常有可能有所帮助。如果获取更多数据后方差仍然过高,则可以增加正则化参数。

感谢@Serendipity的指点。我不知道Spark ML中的逻辑回归支持类别权重。 - dbakr
@dbakr,你需要一个实现的例子吗?我刚试过了。 - Serendipity
1
谢谢@Serendipity!我注意到的一件事是,当分类器在加权数据集上进行训练时,输出概率(我需要实际概率而不是预测标签)不太准确。这意味着生成的概率与原始数据集分布不匹配,而是根据加权数据集进行调整。这反过来会导致验证集上的对数损失度量比手动欠采样原始训练集并手动校准分类器输出概率时更高。 - dbakr
2
这真是太有帮助了,谢谢。事实上,这个功能没有任何文档,也没有示例等等,你不得不参考 GH PR 和 JIRA 任务,这让我感到震惊。这样一个伟大的特性存在于 ml 库中,唯一发现它的方法就是深入挖掘 GH PR/Spark 源代码/JIRAS。Spark 的文档远远不如其他,这太糟糕了。 - David Arenburg
嗨@EmnaJaoua,您是指“WithColumn()”函数吗?您遇到了什么错误?从您的评论中很难理解代码。您能否请开一个新问题并提供错误信息、可复制问题的代码(如果可以,请上传数据样本)?然后您可以引用这个问题。 - Serendipity
显示剩余2条评论

4

我使用了@Serendipity提供的解决方案,但我们可以优化balanceDataset函数,避免使用udf。我还添加了更改标签列的功能。这是我最终得到的函数版本:

def balanceDataset(dataset: DataFrame, label: String = "label"): DataFrame = {
  // Re-balancing (weighting) of records to be used in the logistic loss objective function
  val (datasetSize, positives) = dataset.select(count("*"), sum(dataset(label))).as[(Long, Double)].collect.head
  val balancingRatio = positives / datasetSize

  val weightedDataset = {
    dataset.withColumn("classWeightCol", when(dataset(label) === 0.0, balancingRatio).otherwise(1.0 - balancingRatio))
  }
  weightedDataset
}

我们按照他所说的创建分类器,具体步骤为:
new LogisticRegression().setWeightCol("classWeightCol").setLabelCol("label").setFeaturesCol("features")

0

@dbakr 你在不平衡的数据集上进行偏见预测时得到了答案吗?

虽然我不确定这是否是你最初的计划,但请注意,如果你首先按比例r对数据集的多数类进行子采样,那么为了获得Spark逻辑回归的无偏预测,你可以选择: - 使用transform()函数提供的rawPrediction,并使用log(r)调整截距 - 或者你可以使用.setWeightCol("classWeightCol")训练带权重的回归(请参阅此处引用的文章以找出必须设置的权重值)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接