3.1 分类概述


类(Classification)是在给定一组输入特征的情况下预测标签、类别、类或离散变量的任务**。与其他机器学习任务例如回归相比,关键区别在于分类任务的输出标签是一组可能值的有限集合(例如三个类别)。

在现实世界中还有几个可以应用分类的应用场景:

  • 预测信贷风险:融资公司在向公司或个人提供贷款之前,可能会查看很多信息。最后决定是否提供贷款,是否提供贷款是一个二进制分类问题。
  • 新闻分类:分类算法还可以被训练来预测新闻文章的主题 (体育、政治、商业等) 。
  • 对用户行为分类:根据从传感器(如手机的重力感应器或智能手表)采集的数据,可以预测用户的行为活动,而输出将是一组有限类别的集合(如行走、睡眠、站立或运动)。

在继续介绍分类计数之前,先回顾一下几种不同的分类类型:

  • 最简单的分类类型是二元分类(Binary Classification),只会预测出两个标签。例如欺诈分析,某一交易归类为欺诈行为或是非欺诈行为;或者垃圾邮件分类,一个给定的电子邮件归类为垃圾邮件或是非垃圾邮件。
  • 输出多于两个标签的分类任务是多分类(Multiclass Classification),预测结果是从多于两个的候选标签集合中选出来的一个标签。例如,Facebook 从一张照片中预测是哪个人,气象学家预测天气(雨天,晴天,多云等)。注意始终有一组有限的候选类别来预测,而不是无限制的类别候选,有时候多分类也称为多项式分类(multinomial classification)。
  • 最后一种分类任务是多标签分类(Multilabel Classification),一个给定的输入可对应多个标签。例如根据书的内容预测书的主题,虽然这可以是一个多分类问题,但更适合于多标签分类,因为一本书可以涉及多个主题,划分到多个分类中。多标签分类的另一个例子是识别图像中的多个物体,预测输出物体的个数不一定是固定的,不同图像可以检测出不同数量的物体。

3.2 Spark 中的分类模型

Spark 有一些可以直接用的二分类和多分类模型:

  • Logistic 回归(Logistic regression)
  • 决策树(Decision trees)
  • 随机森林(Random forests)
  • 梯度提升决策树(Gradient-boosted trees)

Spark 本身不支持多标签分类,为了训练出多标签分类模型,用户必须训练每一个标签模型然后人工地将它们结合起来。当人工创建好模型后,可以使用评估多种模型效果的内置工具。选择模型时,模型的可扩展性是一个重要的考虑因素,总的来说 Spark 支持训练大规模机器学习模型(如果数据量不算巨大不需要分布式集群,基于单节点的训练还有其他很多优秀的工具)。下图简单列出了各个模型的可扩展性能,据此可以找到适合特定任务的最佳模型(如果可扩展性是核心考虑因素的话):
image.png
image.png

可以看到,几乎所有这些模型都可以适应规模庞大的输入数据集,无限制训练实例的原因是因为,这些训练采用了随机梯度下降和 L-BFGS 等方法进行训练,这些方法专门针对大数据集进行了优化,并避免了可能存在的限制训练实例数量的若干问题。下面开始先加载数据再来详细看看这些分类模型:

  1. bInput = spark.read.format("parquet").load("/data/binary-classification")\
  2. .selectExpr("features", "cast(label as double) as label")

3.3 逻辑回归


逻辑回归(Logistic Regression)是最常见的分类方法之一。它是一种线性模型,为输入的每个特征赋以权重之后将它们组合在一起,从而获得该输入属于特定类的概率。这些权重很有用,因为它们很好地表示了特征重要性。如果某特征的权重很大,则说明该特征的变化对结果有显著影响**(假定执行了标准化),而较小的权重意味着该特征不是那么重要。

模型的超参数决定了模型本身的基本结构配置。逻辑回归包含以下超参数

  • family:可以设置为 “multinomial”(两个或更多个不同的标签,对应多分类)或 “binary”(仅两个不同的标签,对应二分类)。
  • elasticNetParam:从 0 到 1 的浮点值,该参数依照弹性网络正则化(elastic net regularization)的方法将 L1 正则化和 L2 正则化混合(即两者的线性组合)。对 L1 或 L2 的选择取决于特定用例,其指导原则如下:L1 正则化(值 1)将在模型中产生稀疏性,因为(对输出的影响不大的)某些特征权重将变为零,因此它可以作为一个简单的特征选择方法。而 L2 正则化(值 0)不会造成稀疏,因为特定特征的相应权重只会趋于零而不会等于零。弹性网络给出了两者之间最好的结合,即可以选择一个介于 0 和 1 之间的值,以指定 L1 和 L2 正则化的混合。在大多数情况下,应该通过多次测试来调整这个值。
  • fitIntercept:可以是 true 或 false,此超参数决定是否适应截距。通常情况下,如果没有对训练数据执行标准化,则需要添加截距。
  • regParam:大于等于 0 的值,确定在目标函数中正则化项的权重,它的选择和数据集的噪音情况和数据维度有关,最好尝试多个值(如 0、0.01、0.1、1)。
  • standardization:可以是 true 或 false,设置它决定在将输入数据传递到模型之前是否要对其进行标准化

训练参数用于指定我们如何执行训练,下面是逻辑回归的训练参数

  • maxIter:迭代次数,更改此参数可能不会对结果造成很大的影响,所以它不应该是要调整的首个参数。默认值是 100。
  • tol:此值指定一个用于停止迭代的阈值,达到该阈值说明模型已经优化的足够好了。指定该参数后,算法可能在达到 maxIter 指定的次数之前停止迭代,默认值为 1.0E-6。这也不应该是要调整的第一个参数
  • weightCol:权重列的名称,用于赋予某些行更大的权重。如果有衡量每个训练样本重要性的方法,并对这些样本赋予了不同的训练权重值,这就是个很有用的工具。例如在 10000 个样例中知道哪些样本的标签比其他样本的标签更精确,就可以赋予那些更有用的样本以更大的权值。

下面这些参数指定模型如何实际进行预测而又不影响训练

  • threshold:一个 0~1 的 Double 值。此参数是预测时的概率阈值,可以根据需要调整此参数以平衡误报(false positive)和漏报(false negative)。例如,如果误报的成本高昂,可能希望使该预测阈值非常高。
  • thresholds:该参数允许在进行多分类的时候指定每个类的阈值数组,它和之前的 threshold 类似。

下面是一个使用 LogisticRegression(逻辑回归)模型的简单示例,没有指定任何参数而是使用默认值,并且数据符合正确的列命名。在实际当中,一般也不需要设置许多参数:

  1. from pyspark.ml.classification import LogisticRegression
  2. lr = LogisticRegression()
  3. print lr.explainParams() #查看所有的参数
  4. lrModel = lr.fit(bInput)

一旦模型被训练好了,就可以观察系数和截距项来获取有关模型的信息。系数对应于各特征的权重(每个特征权重乘以各特征来计算预测值),而截距项是斜线截距的值(如果在指定模型时选择了适当的截距参数 fitIntercept)。查看系数有助于检查构建的模型,也有助于理解各特征如何影响预测:

  1. # in Python
  2. print lrModel.coefficients
  3. print lrModel.intercept

对于多项式分类模型(上面是针对二分类模型的),lrModel.coefficientMatrixlrModel.interceptVector 可以用来得到系数和截距值,它们会返回 Matrix 类型和 Vector 类型的值。

逻辑回归提供了一个模型摘要(Model Summary),给出了最终训练模型的相关信息,它类似于在许多 R 语言机器学习包中看到的摘要。利用二分类摘要,可以得到关于模型本身的各种信息,包括 ROC 曲线下的面积、f 值、准确率、召回率、ROC 曲线。要注意,对于曲线下面积不考虑实例权重,因此如果想了解考虑权重的情况,则必须手动处理。可以使用以下 API 查看摘要:

  1. # in Python
  2. summary = lrModel.summary
  3. print summary.areaUnderROC
  4. summary.roc.show()
  5. summary.pr.show()

模型到达最终结果状态的速度会显示在目标历史(objective history)中,可以查看模型摘要的目标历史记录来获得:

  1. summary.objectiveHistory

它是一个 Double 类型的数组,包含了每次训练迭代时模型到底表现如何,此信息有助于帮助用户了解是否已经进行足够次数的迭代训练或是否需要调整其他参数。

3.4 决策树

决策树(Decision Tree)是一种更友好和更易于理解的分类方法,因为它类似于人类经常使用的简单决策模型。例如预测某人是否会买冰激凌,一个很好的特征可能是这个人是否喜欢冰淇淋,在伪代码中如果 person.likes(“ice_cream”) 返回 true,他们会买冰淇淋,否则不会。决策树模型是基于所有输入构建一棵树形结构,在预测时通过判断各种可能的分支来给出预测结果,这使它被常常做为一个最先被试用的模型,因为它易于推理,易于检查,并且对数据的结构只需要很少的假设。简而言之,它并不是试图训练系数来拟合函数,而是简单地创造了一棵树来进行预测。该模型还支持多分类,并输出各种预测结果和它们的概率。

虽然决策树模型简单,但它有一些缺点,它可能会非常快地就会出现过拟合的情况,意思是在无约束的条件下决策树会基于每个训练样例,从根节点开始创建一条判断路径,这意味着它会对模型训练集中的所有信息进行编码,这很糟糕,因为这样模型就不能泛化到新的数据(可能会得到很差的预测准确度)。当然也有一些方法通过限制分支结构(例如限制高度)来避免过拟合的问题。

有许多不同的方法来配置和训练决策树,下面是 Spark 实现支持的超参数

  • maxDepth:因为正在训练生成一棵树结构,所以指定最大深度以避免过拟合数据集是很有帮助的。默认值为 5。
  • maxBins:在决策树中,连续特征(continuous features)被转换为类别特征(categorical features),maxBins 确定应基于连续特征创建多少个槽(bin,相当于类别特征个数),更多的槽提供更细的粒度级别。该值必须大于等于 2,并且也需要大于等于数据集中任何类别特征中的类别数。默认值为 32。
  • impurity:建立一个 “树” 时,在模型应该分支时需要进行配置。不纯度(impurity)表示是否应该在某叶子结点拆分的度量(信息增益)。该参数可以设置为 “entropy” 或 “gini” (默认值),这是两个常用的不纯度度量。
  • minInfoGain:此参数确定可用于分割的最小信息增益。较大的值可以防止过拟合,这通常需要通过测试决策树模型不同变体来确定。默认值是 0。
  • minInstancePerNode:此参数确定需要在一个节点结束训练的实例最小数目。可以把这看成是控制最大深度的另一种方式,可以通过限制深度来防止过拟合,或者可以指定在一个叶子节点上的最少训练样本来防止过拟合。如果它不满足会修剪(prune)树,直到满足要求。较大的值可以防止过拟合。默认值为 1,但可以是大于 1 的任何数值。

还有一些针对如何执行训练的训练参数可以配置,checkpointInterval 为设置检查点(checkpoint)的间隔,checkpoint 是一种在训练过程中保存模型的方法,此方法可以保证当集群节点因某种原因崩溃时,不影响整个训练过程,将该值设置为 10 表示模型每 10 次迭代都会保存 checkpoint,将此设置为 - 1 以关闭 checkpoint,需要将此参数与 checkpointDir(检查点目录)和 useNodeIdCache = true 一起设置。

决策树只有一个预测参数:thresholds,它的定义和逻辑回归的同名参数相同。下面是一个使用决策树分类器的示例:

  1. from pyspark.ml.classification import DecisionTreeClassifier
  2. dt = DecisionTreeClassifier()
  3. print dt.explainParams()
  4. dtModel = dt.fit(bInput)

3.5 随机森林和梯度提升树

随机森林和梯度提升树是决策树的扩展,其实不必在所有数据上只训练一棵树,而是在不同的数据子集上训练多个树。这样做的思路是,多棵决策树成为某一特定领域的专家,而其他决策树则可以成为其他特定领域的专家。通过结合这些不同的专家,就可以达到超过任何单独方法的 “群众智慧”。此外,结合这些方法使用有助于防止过度拟合。

随机森林(Random Forest)梯度提升树(Gradient-Boosted Trees)是组合决策树的两种截然不同的方法。在随机森林中只训练大量的树,然后平均它们的结果做出预测;而梯度提升树对每棵树进行加权预测(因为一些树对某些类别的预测能力比其他树更强)。它们的参数大致相同,但是梯度提升树目前仅支持二分类任务。

随机森林和梯度提升树具有与决策树模型相同的模型超参数。此外它们各自还添加了几个自己的超参数:

  • 仅适合随机森林的参数有 numTrees,即用于训练的树的总数;featureSubsetStrategy 确定拆分时应考虑多少特征,它可以是 “auto”、”all”、”sqrt”、”log2” 或数字 “n”。当输入为 “n” 时,模型将在训练过程中使用 n 个特征数,当 n 在范围 1~ 特征数量之间时,模型将在训练期间使用 n 个特征。这里没有一个适合所有情况的解决方案,所以在 pipeline 任务中需要尝试不同的值。
  • 仅适合梯度提升树(GBT)的参数有 lossType,即梯度提升树在训练过程中优化的损失函数,目前只支持 logistic loss 损失;maxIter 即迭代次数,改变这个值可能不会改变结果太多,所以它不应该是首要调整的参数,默认值是 100;以及 stepSize,代表算法的学习速度。较大的步长(step size)意味着在两次迭代训练之间较大的变化,该参数可以帮助优化过程,是在训练中应该多次测试的参数,默认值为 0.1,可以是从 0 到 1 的任何数值。

这两者的模型只有一个训练参数为 checkpointInterval,与决策树的定义相同。两者的预测参数 thresholds 也与决策树相同。下面是使用这些分类方法的代码示例:

  1. from pyspark.ml.classification import RandomForestClassifier
  2. rfClassifier = RandomForestClassifier()
  3. print rfClassifier.explainParams()
  4. trainedModel = rfClassifier.fit(bInput)
  5. from pyspark.ml.classification import GBTClassifier
  6. gbtClassifier = GBTClassifier()
  7. print gbtClassifier.explainParams()
  8. trainedModel = gbtClassifier.fit(bInput)

3.6 朴素贝叶斯

朴素贝叶斯(Naive Bayes)分类是基于贝叶斯定理的分类方法。该模型背后的核心假设是,数据的所有特征都是相互独立的。当然严格的独立性是有点不现实,但即使不符合这条假设仍然可以生成有用的模型。朴素贝叶斯分类通常用于文本或文档分类,尽管它也可以用作更通用的分类任务。有两种不同的模型类型:多元贝努利模型(multivariate Bernoulli model),其中指示器变量代表文档中的一个单词是否存在;多项式模型(multinomial model),其中使用所有单词计数。

使用朴素贝叶斯一个重要的注意事项是,所有的输入特征值必须为非负数。下面的模型超参数是为确定模型的基本结构而指定的:

  • modelType:可设置为 “bernoulli” 或“multinomial”。
  • weightCol:允许对不同的数据点赋值不同的权值。

smoothing 这个训练参数指定使用加法平滑(additive smoothing)时的正则化量,该设置有助于平滑分类数据,并通过改变某些类的预期概率来避免过拟合问题,默认值是 1。像所有其他模型一样,朴素贝叶斯也使用相同的预测参数 thresholds。下面是一个使用朴素贝叶斯分类的示例:

  1. from pyspark.ml.classification import NaiveBayes
  2. nb = NaiveBayes()
  3. print nb.explainParams()
  4. trainedModel = nb.fit(bInput.where("label != 0"))

在该示例的样例数据集中具有负值的特征,具有负特征的行对应于带有标签 “0” 的行,因此只要通过标签过滤它们,而不是进一步处理它们。

3.7 Evaluator

Evaluator 允许指定模型的终止标准当只有一个评估器时,它的作用并不太大。但是当在 pipeline 中使用它时,可以自动尝试模型和转换器的各种参数,尝试所有参数的组合,以查看哪些性能最好。评估器在这条 pipeline 和参数网格(Parameter Grid)上下文中最有用。

对于分类,有两个评估器对应两列:一个模型预测的标签和一个真正的标签,对于二分类使用 BinaryClassificationEvaluator,它支持优化两个不同的指标 “areaUnderROC” 和“areaUnderPR”。对于多分类,需要使用 MulticlassClassificationEvaluator,它支持优化 “f1”、”weightedPrecision”、”weightedRecall” 和 “accuracy”。

MLlib 还包含一些工具,能同时评估多个分类指标。但这些 metric 类暂时还没有从底层 RDD 框架中移植到基于 DataFrame 的机器学习包中,因此在 Spark2.2 中仍然需要创建一个 RDD 来使用它们,将来此功能很可能会移植到 DataFrame 上,可以使用三种不同的分类指标:二分类指标、多分类指标和多标签分类指标。

所有这些评测标准都遵循相同的近似样式,将生成的输出值与真实值进行比较,模型将计算所有相关的 metric 标准,然后可以查询每个指标的值:

  1. rom pyspark.mllib.evaluation import BinaryClassificationMetrics
  2. out = model.transform(bInput)\
  3. .select("prediction", "label")\
  4. .rdd.map(lambda x: (float(x[0]), float(x[1])))
  5. metrics = BinaryClassificationMetrics(out)

这样做之后,可以使用与逻辑回归类似的 API,在此 metric 指标对象上查看典型的 metric 指标:

  1. # in Python
  2. print metrics.areaUnderPR
  3. print metrics.areaUnderROC
  4. print "Receiver Operating Characteristic"
  5. metrics.roc.toDF().show()

有一些 MLlib 模型不支持多分类,在这些情况下,用户可以利用一个 one-vs-rest 分类器**来执行多分类,它是在给定一个二分类器的情况下,执行多分类任务。这背后的思想是对于每一个希望预测的类,one-vs-rest 分类器将把该分类转化为一个二分类问题,将目标类做为一类,把其余的其它类别做为另外一类**。这样,分类预测就变成了一个二分类问题(是此类或不是此类)。

One-vs-rest 是作为估计器实现的。对于基分类器,它创建 k 个分类器实例,为 k 个类别中的每个分类创建一个对应的二分类问题,训练类 i 的分类器预测标签是否为 i,将类 i 与所有其他类区分开来。预测是通过对每个二分类器进行评估来完成的,将最有可能的分类器的索引输出为标签。多层感知器(Multilayer Perceptron)是基于(可配置层数和层宽度的)神经网络的分类器,后面的部分会详细讨论。