原文:
annas-archive.org/md5/39eecc62e023387ee8c22ca10d1a221a
译者:飞龙
协议:CC BY-NC-SA 4.0
第十三章:我的名字是贝叶斯,朴素贝叶斯
“预测是非常困难的,尤其是当它涉及未来时”
-尼尔斯·玻尔
机器学习(ML)与大数据的结合是一种革命性的组合,它在学术界和工业界的研究领域产生了巨大的影响。此外,许多研究领域也开始涉及大数据,因为数据集以空前的方式从不同的来源和技术中生成和产生,通常被称为数据洪流。这对机器学习、数据分析工具和算法提出了巨大的挑战,需要从大数据的体量、速度和多样性等标准中提取真正的价值。然而,从这些庞大的数据集中做出预测从未如此容易。
考虑到这个挑战,在本章中我们将深入探讨机器学习,并了解如何使用一种简单而强大的方法来构建可扩展的分类模型,甚至更多。简而言之,本章将涵盖以下主题:
-
多项分类
-
贝叶斯推断
-
朴素贝叶斯
-
决策树
-
朴素贝叶斯与决策树
多项分类
在机器学习中,多项式(也称为多类)分类是将数据对象或实例分类为两个以上的类别的任务,即拥有两个以上的标签或类别。将数据对象或实例分类为两个类别称为二元分类。从技术角度讲,在多项分类中,每个训练实例属于 N 个不同类别中的一个,其中N >= 2
。目标是构建一个模型,能够正确预测新实例属于哪些类别。在许多场景中,数据点可能属于多个类别。然而,如果一个给定的点属于多个类别,这个问题可以简化为一组不相关的二元问题,这些问题可以自然地使用二元分类算法解决。
读者应避免将多类分类与多标签分类混淆,在多标签分类中,每个实例需要预测多个标签。关于基于 Spark 的多标签分类实现,感兴趣的读者可以参考spark.apache.org/docs/latest/mllib-evaluation-metrics.html#multilabel-classification
。
多类别分类技术可以分为以下几类:
-
转换为二进制
-
从二进制扩展
-
层次分类
转换为二进制
使用二分类技术转换,多类分类问题可以转化为多个二分类问题的等效策略。换句话说,这种技术可以称为 问题转换技术。从理论和实践的角度进行详细讨论超出了本章的范围。因此,我们这里只讨论一种问题转换技术的例子,称为 一对其余(OVTR)算法,作为该类别的代表。
使用一对其余方法进行分类
在本小节中,我们将描述使用 OVTR 算法执行多类分类的例子,方法是将问题转化为多个等效的二分类问题。OVTR 策略将问题拆解并为每个类别训练一个二分类器。换句话说,OVTR 分类器策略包括为每个类别拟合一个二分类器。然后,它将当前类别的所有样本视为正样本,因此其他分类器的样本视为负样本。
这无疑是一种模块化的机器学习技术。然而,缺点是该策略需要来自多类家族的基础分类器。原因是分类器必须输出一个实值,也叫做 置信度分数,而不是预测实际标签。该策略的第二个缺点是,如果数据集(即训练集)包含离散的类标签,最终可能导致模糊的预测结果。在这种情况下,单个样本可能会被预测为多个类别。为了使前面的讨论更清晰,下面我们来看一个例子。
假设我们有一组 50 个观测值,分为三个类别。因此,我们将使用与之前相同的逻辑来选择负例。对于训练阶段,我们设定如下:
-
分类器 1 有 30 个正例和 20 个负例
-
分类器 2 有 36 个正例和 14 个负例
-
分类器 3 有 14 个正例和 24 个负例
另一方面,对于测试阶段,假设我有一个新实例,需要将其分类到之前的某一类别中。每个分类器当然都会产生一个关于估计的概率。这个估计是指该实例属于分类器中正例或负例的概率有多低?在这种情况下,我们应始终比较一对其余中的正类概率。现在,对于 N 个类别,我们将为每个测试样本获得 N 个正类的概率估计。比较它们,最大概率对应的类别即为该样本所属类别。Spark 提供了通过 OVTR 算法将多类问题转换为二分类问题,其中 逻辑回归 算法被用作基础分类器。
现在我们来看看另一个真实数据集的示例,演示 Spark 如何使用 OVTR 算法对所有特征进行分类。OVTR 分类器最终预测来自 光学字符识别 (OCR) 数据集的手写字符。然而,在深入演示之前,我们先来探索一下 OCR 数据集,以了解数据的探索性特征。需要注意的是,当 OCR 软件首次处理文档时,它会将纸张或任何物体划分为一个矩阵,使得网格中的每个单元格都包含一个字形(也称为不同的图形形状),这仅仅是对字母、符号、数字或任何来自纸张或物体的上下文信息的详细描述方式。
为了演示 OCR 流水线,假设文档仅包含与 26 个大写字母(即 A 到 Z)匹配的英文字母字符,我们将使用来自 UCI 机器学习数据库 的 OCR 字母数据集。该数据集由 W* Frey* 和 D. J. Slate 提供。在探索数据集时,您应该会看到 20,000 个示例,包含 26 个英文字母的大写字母。大写字母通过 20 种不同的、随机重塑和扭曲的黑白字体作为图形呈现,具有不同的形状。简而言之,从 26 个字母中预测所有字符将问题本身转化为一个具有 26 个类别的多类分类问题。因此,二分类器将无法达到我们的目的。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00362.gif图 1: 一些打印字形(来源:使用荷兰风格自适应分类器的字母识别,ML,第 6 卷,第 161-182 页,W. Frey 和 D.J. Slate [1991])
上图显示了我之前解释过的图像。数据集 提供了经过这种方式扭曲的打印字形的示例,因此这些字母对计算机来说具有挑战性,难以识别。然而,人类可以轻松识别这些字形。以下图展示了前 20 行的统计属性:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00020.jpeg图 2: 数据集的快照,显示为数据框
OCR 数据集的探索与准备
根据数据集描述,字形通过 OCR 阅读器扫描到计算机上,然后它们会被自动转换为像素。因此,所有 16 个统计属性(见图 2)也会记录到计算机中。黑色像素在框的各个区域中的浓度提供了一种方法,可以通过 OCR 或经过训练的机器学习算法来区分 26 个字母。
回想一下,支持向量机(SVM)、逻辑回归、朴素贝叶斯分类器或任何其他分类器算法(连同它们的学习器)都要求所有特征都是数字格式。LIBSVM 允许你使用稀疏训练数据集,以非传统格式存储数据。在将正常的训练数据集转换为 LIBSVM 格式时,只有数据集中的非零值才会以稀疏数组/矩阵的形式存储。索引指定实例数据的列(特征索引)。然而,任何缺失的数据也会被视为零值。索引用于区分不同的特征/参数。例如,对于三个特征,索引 1、2 和 3 分别对应于 x、y 和 z 坐标。不同数据实例中相同索引值之间的对应关系仅在构造超平面时才是数学上的;这些值作为坐标。如果跳过了中间的任何索引,它应被默认赋值为零。
在大多数实际情况下,我们可能需要对所有特征点进行数据归一化。简而言之,我们需要将当前的制表符分隔 OCR 数据转换为 LIBSVM 格式,以便简化训练步骤。因此,我假设你已经下载了数据并使用他们的脚本转换为 LIBSVM 格式。转换为 LIBSVM 格式后,数据集包含标签和特征,如下图所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00223.gif图 3: LIBSVM 格式的 OCR 数据集 20 行快照
有兴趣的读者可以参考以下研究文章以深入了解:Chih-Chung Chang 和 Chih-Jen Lin,LIBSVM:一个支持向量机库,ACM Intelligent Systems and Technology Transactions,2:27:1–27:27,2011。你还可以参考我在 GitHub 仓库提供的公共脚本,地址是 github.com/rezacsedu/RandomForestSpark/
,该脚本可以将 CSV 中的 OCR 数据直接转换为 LIBSVM 格式。我读取了所有字母的数据,并为每个字母分配了唯一的数字值。你只需要显示输入和输出文件路径,并运行该脚本。
现在让我们深入了解这个例子。我将演示的例子包含 11 个步骤,包括数据解析、Spark 会话创建、模型构建和模型评估。
步骤 1. 创建 Spark 会话 - 通过指定主节点 URL、Spark SQL 仓库和应用名称来创建一个 Spark 会话,如下所示:
val spark = SparkSession.builder.master("local[*]") //change acordingly.config("spark.sql.warehouse.dir", "/home/exp/").appName("OneVsRestExample") .getOrCreate()
步骤 2. 加载、解析和创建数据框 - 从 HDFS 或本地磁盘加载数据文件并创建数据框,最后显示数据框的结构,如下所示:
val inputData = spark.read.format("libsvm").load("data/Letterdata_libsvm.data")
inputData.show()
步骤 3. 生成训练集和测试集以训练模型 - 让我们通过将 70% 用于训练,30% 用于测试来生成训练集和测试集:
val Array(train, test) = inputData.randomSplit(Array(0.7, 0.3))
步骤 4. 实例化基础分类器 - 在这里,基础分类器充当多分类分类器。在本例中,它是逻辑回归算法,可以通过指定最大迭代次数、容忍度、回归参数和弹性网络参数等参数进行实例化。
请注意,当因变量是二项(即二元)时,逻辑回归是一种适合进行回归分析的方法。像所有回归分析一样,逻辑回归是一种预测分析。逻辑回归用于描述数据并解释一个二元因变量与一个或多个名义、顺序、区间或比率水平自变量之间的关系。
对于基于 Spark 的逻辑回归算法实现,有兴趣的读者可以参考spark.apache.org/docs/latest/mllib-linear-methods.html#logistic-regression
。
简而言之,训练逻辑回归分类器时使用了以下参数:
-
MaxIter
:这指定最大迭代次数。通常,次数越多越好。 -
Tol
:这是停止标准的容忍度。通常,值越小越好,这有助于模型进行更密集的训练。默认值为 1E-4。 -
FirIntercept
:表示你是否希望在生成概率解释时拦截决策函数。 -
Standardization
:这表示一个布尔值,决定是否希望标准化训练数据。 -
AggregationDepth
:越大越好。 -
RegParam
:这表示回归参数。大多数情况下,值越小越好。 -
ElasticNetParam
:这表示更高级的回归参数。大多数情况下,值越小越好。
然而,你可以根据问题类型和数据集特性指定拟合截距的Boolean
值为真或假:
val classifier = new LogisticRegression().setMaxIter(500) .setTol(1E-4) .setFitIntercept(true).setStandardization(true) .setAggregationDepth(50) .setRegParam(0.0001) .setElasticNetParam(0.01)
步骤 5. 实例化 OVTR 分类器 - 现在实例化一个 OVTR 分类器,将多分类问题转化为多个二分类问题,如下所示:
val ovr = new OneVsRest().setClassifier(classifier)
这里classifier
是逻辑回归估计器。现在是时候训练模型了。
步骤 6. 训练多分类模型 - 让我们使用训练集来训练模型,如下所示:
val ovrModel = ovr.fit(train)
步骤 7. 在测试集上评估模型 - 我们可以使用转换器(即ovrModel
)在测试数据上对模型进行评分,如下所示:
val predictions = ovrModel.transform(test)
步骤 8. 评估模型 - 在这一步,我们将预测第一列字符的标签。但在此之前,我们需要实例化一个evaluator
来计算分类性能指标,如准确率、精确度、召回率和f1
值,具体如下:
val evaluator = new MulticlassClassificationEvaluator().setLabelCol("label").setPredictionCol("prediction")
val evaluator1 = evaluator.setMetricName("accuracy")
val evaluator2 = evaluator.setMetricName("weightedPrecision")
val evaluator3 = evaluator.setMetricName("weightedRecall")
val evaluator4 = evaluator.setMetricName("f1")
步骤 9. 计算性能指标 - 计算测试数据集上的分类准确率、精确度、召回率、f1
值和错误率,如下所示:
val accuracy = evaluator1.evaluate(predictions)
val precision = evaluator2.evaluate(predictions)
val recall = evaluator3.evaluate(predictions)
val f1 = evaluator4.evaluate(predictions)
步骤 10. 打印性能指标:
println("Accuracy = " + accuracy)
println("Precision = " + precision)
println("Recall = " + recall)
println("F1 = " + f1)
println(s"Test Error = ${1 - accuracy}")
你应该观察如下值:
Accuracy = 0.5217246545696688
Precision = 0.488360500637862
Recall = 0.5217246545696688
F1 = 0.4695649096879411
Test Error = 0.47827534543033123
步骤 11. 停止 Spark 会话:
spark.stop() // Stop Spark session
通过这种方式,我们可以将一个多项式分类问题转换为多个二分类问题,而不会牺牲问题类型。然而,从第 10 步开始,我们可以观察到分类准确率并不理想。这可能是由多个原因造成的,例如我们用于训练模型的数据集的性质。此外,更重要的是,在训练逻辑回归模型时我们并没有调整超参数。而且,在进行转换时,OVTR 不得不牺牲一些准确性。
层次分类
在层次分类任务中,分类问题可以通过将输出空间划分为树的方式来解决。在这棵树中,父节点被划分为多个子节点。这个过程一直持续,直到每个子节点代表一个单一的类别。基于层次分类技术,已经提出了几种方法。计算机视觉就是一个典型的例子,其中识别图片或书写文本是使用层次处理的应用。关于这种分类器的详细讨论超出了本章的范围。
从二分类到多分类的扩展
这是一种将现有的二分类器扩展到多类分类问题的技术。为了解决多类分类问题,基于神经网络、决策树(DT)、随机森林、k 近邻、朴素贝叶斯和支持向量机(SVM)等算法已经提出并开发出来。在接下来的章节中,我们将讨论朴素贝叶斯和决策树算法,作为该类别的两个代表。
现在,在使用朴素贝叶斯算法解决多类分类问题之前,让我们在下一节中简要回顾一下贝叶斯推理。
贝叶斯推理
在这一节中,我们将简要讨论贝叶斯推理(BI)及其基础理论。读者将从理论和计算的角度了解这一概念。
贝叶斯推理概述
贝叶斯推理是一种基于贝叶斯定理的统计方法。它用于更新假设的概率(作为强有力的统计证据),以便统计模型能够不断更新,朝着更准确的学习方向发展。换句话说,所有类型的不确定性都以统计概率的形式在贝叶斯推理方法中揭示出来。这是理论和数学统计中的一个重要技术。我们将在后续章节中广泛讨论贝叶斯定理。
此外,贝叶斯更新在数据集序列的增量学习和动态分析中占据主导地位。例如,时间序列分析、生物医学数据分析中的基因组测序、科学、工程、哲学和法律等领域广泛应用贝叶斯推理。从哲学视角和决策理论看,贝叶斯推理与预测概率密切相关。然而,这一理论更正式的名称是贝叶斯概率。
什么是推理?
推理或模型评估是更新从模型得出的结局概率的过程。最终,所有的概率证据都将与当前的观察结果对比,以便在使用贝叶斯模型进行分类分析时可以更新观察结果。之后,这些信息通过对数据集中所有观察结果的一致性实例化被传回贝叶斯模型。传送到模型的规则被称为先验概率,这些概率是在引用某些相关观察结果之前评估的,特别是主观地或者假设所有可能的结果具有相同的概率。然后,当所有证据都已知时,计算出信念,这就是后验概率。这些后验概率反映了基于更新证据计算的假设水平。
贝叶斯定理用于计算后验概率,这些概率表示两个前提的结果。基于这些前提,从统计模型中推导出先验概率和似然函数,用于新数据的模型适应性。我们将在后续章节进一步讨论贝叶斯定理。
它是如何工作的?
在这里,我们讨论了一个统计推理问题的一般设置。首先,从数据中,我们估计所需的数量,可能也有一些我们希望估计的未知量。它可能只是一个响应变量或预测变量,一个类别,一个标签,或仅仅是一个数字。如果你熟悉频率派方法,你可能知道,在这种方法中,未知量,比如θ
,被假定为一个固定的(非随机)量,应该通过观察到的数据来估计。
然而,在贝叶斯框架中,未知量,比如θ
,被视为一个随机变量。更具体地说,假设我们对θ
的分布有一个初步的猜测,这通常被称为先验分布。现在,在观察到一些数据后,θ
的分布被更新。这个步骤通常是使用贝叶斯规则执行的(更多细节请参见下一节)。这就是为什么这种方法被称为贝叶斯方法的原因。简而言之,从先验分布中,我们可以计算出对未来观察结果的预测分布。
这个不起眼的过程可以通过大量论证证明是处理不确定推断的合适方法。然而,保持一致性的是这些论证的理性原则。尽管有强有力的数学证据,许多机器学习从业者对于使用贝叶斯方法感到不适,甚至有些不情愿。其背后的原因是,他们通常认为选择后验概率或先验概率是任意且主观的;然而,实际上,这种选择虽然主观,但并非任意的。
不恰当地,许多贝叶斯学派的人并没有真正用贝叶斯的思想来思考。因此,在文献中可以找到许多伪贝叶斯方法,其中使用的模型和先验并不能被严肃地视为先验信念的表达。贝叶斯方法也可能会遇到计算困难。许多这些问题可以通过马尔可夫链蒙特卡洛方法来解决,而这也是我的研究重点之一。随着你阅读本章,关于该方法的细节会更加清晰。
朴素贝叶斯
在机器学习中,朴素贝叶斯(NB)是基于著名的贝叶斯定理的概率分类器示例,其假设特征之间具有强独立性。在本节中,我们将详细讨论朴素贝叶斯。
贝叶斯定理概览
在概率论中,贝叶斯定理描述了基于与某一事件相关的先验知识来计算事件发生的概率。这是一个由托马斯·贝叶斯牧师最早提出的概率定理。换句话说,它可以被看作是理解概率理论如何被新的信息所影响的方式。例如,如果癌症与年龄相关,关于年龄的信息可以被用来更准确地评估某人可能患癌的概率*。*
贝叶斯定理的数学表达式如下:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00241.gif
在前面的方程中,A 和 B 是事件,且满足 P (B) ≠ 0,其他项可以描述如下:
-
P(A) 和 P(B) 是观察到 A 和 B 的概率,彼此之间不考虑相关性(即独立性)
-
P(A | B) 是在已知B为真时观察事件A的条件概率
-
P(B| A) 是在已知 A 为真时观察事件 B 的条件概率
正如你可能知道的那样,一项著名的哈佛研究表明,只有 10%的幸福人是富有的。然而,你可能认为这个统计数字非常有说服力,但你也可能会有点兴趣知道,富有的人中有多少人也是真正幸福的*。* 贝叶斯定理帮助你计算这个反向统计信息,方法是使用两个额外的线索:
-
总体上,幸福的人的百分比,即P(A)。
-
总体上,富有的人的百分比,即P(B)。
贝叶斯定理的关键思想是反转统计,考虑整体比例**。** 假设以下信息作为先验是已知的:
-
40% 的人是快乐的,=> P(A)。
-
5% 的人是富人 => P(B).
现在让我们假设哈佛的研究是正确的,即 P(B|A) = 10%。那么富人中快乐的比例,也就是 P(A | B), 可以按如下方式计算:
P(A|B) = {P(A) P(B| A)}/ P(B) = (40%10%)/5% = 80%
结果是,大多数人也是快乐的!很好。为了更清楚地说明,现在假设世界人口为 1000,为了简化计算。然后,根据我们的计算,存在以下两个事实:
-
事实 1:这告诉我们 400 人是快乐的,而哈佛的研究表明,这些快乐的人中有 40 个人也是富人。
-
事实 2:共有 50 个富人,因此其中快乐的比例是 40/50 = 80%。
这证明了贝叶斯定理及其有效性。然而,更全面的示例可以在 onlinecourses.science.psu.edu/stat414/node/43
找到。
我的名字是 Bayes,Naive Bayes。
我是 Bayes,Naive Bayes(NB)。我是一个成功的分类器,基于 最大后验(MAP)的原理。作为分类器,我具有高度的可扩展性,需要的参数数目与学习问题中的变量(特征/预测因子)数量成线性关系。我有几个特点,例如,我计算更快,如果你雇佣我来分类,我很容易实现,并且我可以很好地处理高维数据集。此外,我可以处理数据集中的缺失值。尽管如此,我是可适应的,因为模型可以在不重新构建的情况下用新的训练数据进行修改。
在贝叶斯统计中,MAP 估计是未知量的估计,它等于后验分布的众数。MAP 估计可以用于基于经验数据获取一个未观察量的点估计。
听起来像詹姆斯·邦德的电影情节吗?嗯,你/我们可以将分类器看作是 007 特工,对吧?开玩笑的。我相信我并不像 Naive Bayes 分类器的参数,因为它们像先验和条件概率一样,是通过一套确定的步骤来学习或决定的:这涉及到两个非常简单的操作,这在现代计算机上可以非常快速地执行,即计数和除法。没有迭代。没有周期。没有代价方程的优化(代价方程通常比较复杂,平均而言至少是三次方或二次方复杂度)。没有误差反向传播。没有涉及解矩阵方程的操作。这些使得 Naive Bayes 及其整体训练更加高效。
然而,在雇用此代理之前,您/我们可以发现它的优缺点,以便我们只利用它的优势像王牌一样使用它。好了,这里有一张表格总结了这个代理的优缺点:
代理 | 优点 | 缺点 | 更适合 |
---|---|---|---|
Naive Bayes (NB) | - 计算速度快 - 实现简单 - 对高维度数据效果好 - 可以处理缺失值 - 训练模型所需数据量小 - 可扩展 - 可适应性强,因为可以通过新增训练数据来修改模型,而无需重新构建模型 | - 依赖于独立性假设,因此如果假设不成立,表现会差 - 准确率较低 - 如果某个类标签和某个特征值一起没有出现,那么基于频率的概率估计会是零 | - 当数据中有大量缺失值时 - 当特征之间的依赖关系相似时 - 垃圾邮件过滤和分类 - 对新闻文章进行分类(如技术、政治、体育等) - 文本挖掘 |
表格 1: Naive Bayes 算法的优缺点
使用 NB 构建可扩展分类器
在本节中,我们将通过一步一步的示例来展示如何使用 Naive Bayes(NB)算法。正如前面所述,NB 具有很强的可扩展性,所需的参数数量与学习问题中变量(特征/预测因子)的数量成线性关系。这种可扩展性使得 Spark 社区能够使用该算法在大规模数据集上进行预测分析。Spark MLlib 中当前的 NB 实现支持多项式 NB 和 Bernoulli NB。
如果特征向量是二进制的,Bernoulli NB 是非常有用的。一个应用场景是使用词袋(BOW)方法进行文本分类。另一方面,多项式 NB 通常用于离散计数。例如,如果我们有一个文本分类问题,我们可以将 Bernoulli 试验的思想进一步拓展,在文档中使用频率计数,而不是词袋(BOW)。
在本节中,我们将展示如何通过结合 Spark 机器学习 API(包括 Spark MLlib、Spark ML 和 Spark SQL)来预测 基于笔迹的手写数字识别 数据集中的数字:
步骤 1. 数据收集、预处理和探索 - 手写数字的基于笔的识别数据集是从 UCI 机器学习库下载的,网址为 www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/pendigits.
。该数据集是在从 44 位书写者收集了大约 250 个数字样本后生成的,样本与笔的位置在每隔 100 毫秒的固定时间间隔内进行相关。每个数字都被写在一个 500 x 500 像素的框内。最后,这些图像被缩放到 0 到 100 之间的整数值,以在每个观测值之间创建一致的缩放。使用了一种著名的空间重采样技术,以获得沿弧轨迹上均匀间隔的 3 个和 8 个点。可以通过根据(x, y)坐标绘制 3 个或 8 个采样点来可视化一个示例图像以及从点到点的连线;它看起来像下表所示:
集 | ‘0’ | ‘1’ | ‘2’ | ‘3’ | ‘4’ | ‘5’ | ‘6’ | ‘7’ | ‘8’ | ‘9’ | 总计 |
---|---|---|---|---|---|---|---|---|---|---|---|
训练集 | 780 | 779 | 780 | 719 | 780 | 720 | 720 | 778 | 718 | 719 | 7493 |
测试 | 363 | 364 | 364 | 336 | 364 | 335 | 336 | 364 | 335 | 336 | 3497 |
表 2:用于训练集和测试集的数字数量
如前表所示,训练集包含 30 位书写者书写的样本,而测试集包含 14 位书写者书写的样本。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00130.jpeg
图 4:数字 3 和 8 的示例
关于该数据集的更多信息可以在 archive.ics.uci.edu/ml/machine-learning-databases/pendigits/pendigits-orig.names
上找到。数据集的一个样本快照的数字表示如下图所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00149.gif
图 5:手写数字数据集的 20 行快照
现在,为了使用独立变量(即特征)预测因变量(即标签),我们需要训练一个多类分类器,因为如前所述,数据集现在有九个类别,即九个手写数字。为了预测,我们将使用朴素贝叶斯分类器并评估模型的性能。
步骤 2. 加载所需的库和包:
import org.apache.spark.ml.classification.NaiveBayes
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
import org.apache.spark.sql.SparkSession
步骤 3. 创建一个活跃的 Spark 会话:
val spark = SparkSession.builder.master("local[*]").config("spark.sql.warehouse.dir", "/home/exp/").appName(s"NaiveBayes").getOrCreate()
请注意,这里已将主机 URL 设置为 local[*]
,这意味着您机器的所有核心将用于处理 Spark 任务。您应根据需求相应地设置 SQL 仓库以及其他配置参数。
步骤 4. 创建 DataFrame - 将存储在 LIBSVM 格式中的数据加载为 DataFrame:
val data = spark.read.format("libsvm").load("data/pendigits.data")
对于数字分类,输入特征向量通常是稀疏的,应当提供稀疏向量作为输入,以利用稀疏性。由于训练数据只使用一次,而且数据集的大小相对较小(即只有几 MB),如果多次使用 DataFrame,我们可以对其进行缓存。
步骤 5. 准备训练集和测试集 - 将数据拆分为训练集和测试集(25% 用于测试):
val Array(trainingData, testData) = data.randomSplit(Array(0.75, 0.25), seed = 12345L)
步骤 6. 训练朴素贝叶斯模型 - 使用训练集训练朴素贝叶斯模型,方法如下:
val nb = new NaiveBayes()
val model = nb.fit(trainingData)
步骤 7. 计算测试集上的预测结果 - 使用模型转换器计算预测结果,并最终显示每个标签的预测结果,方法如下:
val predictions = model.transform(testData)
predictions.show()
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00189.jpeg图 6: 每个标签(即每个数字)的预测结果
正如你在前面的图中看到的,一些标签的预测是准确的,而另一些则是错误的。我们需要了解加权准确率、精确率、召回率和 F1 值,而不是单纯地评估模型。
步骤 8. 评估模型 - 选择预测结果和真实标签,计算测试误差和分类性能指标,如准确率、精确率、召回率和 F1 值,方法如下:
val evaluator = new MulticlassClassificationEvaluator().setLabelCol("label").setPredictionCol("prediction")
val evaluator1 = evaluator.setMetricName("accuracy")
val evaluator2 = evaluator.setMetricName("weightedPrecision")
val evaluator3 = evaluator.setMetricName("weightedRecall")
val evaluator4 = evaluator.setMetricName("f1")
步骤 9. 计算性能指标 - 计算分类准确率、精确率、召回率、F1 值和测试数据上的误差,方法如下:
val accuracy = evaluator1.evaluate(predictions)
val precision = evaluator2.evaluate(predictions)
val recall = evaluator3.evaluate(predictions)
val f1 = evaluator4.evaluate(predictions)
步骤 10. 打印性能指标:
println("Accuracy = " + accuracy)
println("Precision = " + precision)
println("Recall = " + recall)
println("F1 = " + f1)
println(s"Test Error = ${1 - accuracy}")
你应该观察到如下值:
Accuracy = 0.8284365162644282
Precision = 0.8361211320692463
Recall = 0.828436516264428
F1 = 0.8271828540349192
Test Error = 0.17156348373557184
性能并不差。然而,你仍然可以通过进行超参数调优来提高分类准确率。通过交叉验证和训练集拆分选择合适的算法(即分类器或回归器),仍然有机会进一步提高预测准确率,这将在下一节中讨论。
调整我的参数!
你已经知道我的优缺点,我有一个缺点,那就是我的分类准确率相对较低。不过,如果你对我进行调优,我可以表现得更好。嗯,我们应该相信朴素贝叶斯吗?如果相信,难道我们不该看看如何提高这个模型的预测性能吗?我们以 WebSpam 数据集为例。首先,我们应该观察 NB 模型的性能,然后看看如何通过交叉验证技术提升性能。
从www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/binary/webspam_wc_normalized_trigram.svm.bz2
下载的 WebSpam 数据集包含特征和相应的标签,即垃圾邮件或正常邮件。因此,这是一个监督学习问题,任务是预测给定消息是否为垃圾邮件或正常邮件(即非垃圾邮件)。原始数据集大小为 23.5 GB,类别标签为+1 或-1(即二元分类问题)。后来,由于朴素贝叶斯不允许使用有符号整数,我们将-1 替换为 0.0,+1 替换为 1.0。修改后的数据集如下图所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00054.gif图 7: WebSpam 数据集的前 20 行快照
首先,我们需要按以下方式导入必要的包:
import org.apache.spark.ml.classification.NaiveBayes
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
import org.apache.spark.sql.SparkSession
import org.apache.spark.ml.Pipeline;
import org.apache.spark.ml.PipelineStage;
import org.apache.spark.ml.classification.LogisticRegression
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
import org.apache.spark.ml.feature.{HashingTF, Tokenizer}
import org.apache.spark.ml.linalg.Vector
import org.apache.spark.ml.tuning.{CrossValidator, ParamGridBuilder}
现在按以下方式创建 Spark 会话作为代码的入口点:
val spark = SparkSession.builder.master("local[*]").config("spark.sql.warehouse.dir", "/home/exp/").appName("Tuned NaiveBayes").getOrCreate()
让我们加载 WebSpam 数据集并准备训练集以训练朴素贝叶斯模型,如下所示:
// Load the data stored in LIBSVM format as a DataFrame.val data = spark.read.format("libsvm").load("hdfs://data/ webspam_wc_normalized_trigram.svm")// Split the data into training and test sets (30% held out for testing)val Array(trainingData, testData) = data.randomSplit(Array(0.75, 0.25), seed = 12345L)// Train a NaiveBayes model with using the training setval nb = new NaiveBayes().setSmoothing(0.00001)val model = nb.fit(trainingData)
在上述代码中,为了可复现性,设置种子是必需的。现在让我们对验证集进行预测,步骤如下:
val predictions = model.transform(testData)
predictions.show()
现在让我们获取evaluator
并计算分类性能指标,如准确度、精确度、召回率和f1
度量,如下所示:
val evaluator = new MulticlassClassificationEvaluator().setLabelCol("label").setPredictionCol("prediction")
val evaluator1 = evaluator.setMetricName("accuracy")
val evaluator2 = evaluator.setMetricName("weightedPrecision")
val evaluator3 = evaluator.setMetricName("weightedRecall")
val evaluator4 = evaluator.setMetricName("f1")
现在让我们计算并打印性能指标:
val accuracy = evaluator1.evaluate(predictions)
val precision = evaluator2.evaluate(predictions)
val recall = evaluator3.evaluate(predictions)
val f1 = evaluator4.evaluate(predictions)
// Print the performance metrics
println("Accuracy = " + accuracy)
println("Precision = " + precision)
println("Recall = " + recall)
println("F1 = " + f1)
println(s"Test Error = ${1 - accuracy}")
您应该收到以下输出:
Accuracy = 0.8839357429715676
Precision = 0.86393574297188752
Recall = 0.8739357429718876
F1 = 0.8739357429718876
Test Error = 0.11606425702843237
虽然准确度已经达到了令人满意的水平,但我们可以通过应用交叉验证技术进一步提高它。该技术的步骤如下:
-
通过链式连接一个 NB 估计器作为管道的唯一阶段来创建一个流水线
-
现在为调整准备参数网格
-
执行 10 折交叉验证
-
现在使用训练集拟合模型
-
计算验证集上的预测
诸如交叉验证之类的模型调整技术的第一步是创建管道。通过链式连接转换器、估计器和相关参数可以创建管道。
步骤 1. 管道创建 - 让我们创建一个朴素贝叶斯估计器(在以下情况下,nb
是一个估计器)并通过链式连接估计器创建一个管道:
val nb = new NaiveBayes().setSmoothing(00001)
val pipeline = new Pipeline().setStages(Array(nb))
一个管道可以被看作是用于训练和预测的数据工作流系统。ML 管道提供了一组统一的高级 API,构建在DataFrames之上,帮助用户创建和调优实用的机器学习管道。DataFrame、转换器、估计器、管道和参数是管道创建中最重要的五个组件。有兴趣的读者可以参考spark.apache.org/docs/latest/ml-pipeline.html
了解更多关于管道的信息。
在前面的情况下,我们的管道中唯一的阶段是一个用于在 DataFrame 上拟合以生成转换器以确保成功训练的算法估计器。
步骤 2. 创建网格参数 - 让我们使用 ParamGridBuilder
构建一个参数网格以进行搜索:
val paramGrid = new ParamGridBuilder().addGrid(nb.smoothing, Array(0.001, 0.0001)).build()
步骤 3. 执行 10 折交叉验证 - 现在我们将管道视为一个估计器,并将其包装在交叉验证器实例中。这将允许我们为所有管道阶段共同选择参数。CrossValidator
需要一个估计器、一组估计器 ParamMaps
和一个评估器。请注意,这里的评估器是 BinaryClassificationEvaluator
,其默认指标是 areaUnderROC
。但是,如果你使用 MultiClassClassificationEvaluator
作为评估器,你还可以使用其他性能指标:
val cv = new CrossValidator().setEstimator(pipeline).setEvaluator(new BinaryClassificationEvaluator).setEstimatorParamMaps(paramGrid).setNumFolds(10) // Use 3+ in practice
步骤 4. 使用训练集拟合交叉验证模型,如下所示:
val model = cv.fit(trainingData)
步骤 5. 如下计算性能:
val predictions = model.transform(validationData)
predictions.show()
步骤 6. 获取评估器,计算性能指标,并显示结果。现在让我们获取evaluator
并计算分类性能指标,如准确率、精度、召回率和 F1 值。这里将使用 MultiClassClassificationEvaluator
来计算准确率、精度、召回率和 F1 值:
val evaluator = new MulticlassClassificationEvaluator().setLabelCol("label").setPredictionCol("prediction")
val evaluator1 = evaluator.setMetricName("accuracy")
val evaluator2 = evaluator.setMetricName("weightedPrecision")
val evaluator3 = evaluator.setMetricName("weightedRecall")
val evaluator4 = evaluator.setMetricName("f1")
现在计算分类准确率、精度、召回率、F1 值和测试数据上的误差,如下所示:
val accuracy = evaluator1.evaluate(predictions)
val precision = evaluator2.evaluate(predictions)
val recall = evaluator3.evaluate(predictions)
val f1 = evaluator4.evaluate(predictions)
现在让我们打印性能指标:
println("Accuracy = " + accuracy)
println("Precision = " + precision)
println("Recall = " + recall)
println("F1 = " + f1)
println(s"Test Error = ${1 - accuracy}")
你现在应该会收到以下结果:
Accuracy = 0.9678714859437751
Precision = 0.9686742518830365
Recall = 0.9678714859437751
F1 = 0.9676697179934564
Test Error = 0.032128514056224855
现在与之前的结果相比,效果好多了,对吧?请注意,由于数据集的随机拆分和你使用的平台,你可能会得到稍有不同的结果。
决策树
在这一部分,我们将详细讨论决策树算法。还将讨论朴素贝叶斯和决策树的比较分析。决策树通常被认为是一种监督学习技术,用于解决分类和回归任务。决策树只是一个决策支持工具,使用类似树状的图(或决策模型)及其可能的结果,包括机会事件结果、资源成本和效用。从技术角度讲,决策树中的每一分支代表一个可能的决策、事件或反应,具体体现在统计概率上。
与朴素贝叶斯相比,决策树(DT)是一种更为强大的分类技术。其原因在于,决策树首先将特征划分为训练集和测试集。然后它会生成一个良好的泛化模型,以推断预测标签或类别。更有趣的是,决策树算法可以处理二分类和多分类问题。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00081.jpeg图 8: 使用 R 的 Rattle 包在入学测试数据集上生成的决策树示例
举例来说,在前面的示例图中,DT 通过录取数据学习,用一组if...else
决策规则来逼近正弦曲线。数据集包含每个申请入学的学生的记录,假设是申请美国大学的学生。每条记录包括研究生入学考试分数、CGPA 分数和列的排名。现在,我们需要根据这三个特征(变量)预测谁是合格的。DT 可以用于解决这种问题,在训练 DT 模型并修剪掉不需要的树枝后进行预测。通常,树越深,意味着决策规则越复杂,模型的拟合度越好。因此,树越深,决策规则越复杂,模型越拟合。
如果你想绘制前面的图形,只需运行我的 R 脚本,在 RStudio 中执行它,并提供录取数据集。脚本和数据集可以在我的 GitHub 仓库中找到:github.com/rezacsedu/AdmissionUsingDecisionTree
。
使用 DT 的优缺点
在雇佣我之前,你可以从表 3 中了解我的优缺点以及我最擅长的工作时间,以免你事后后悔!
代理 | 优点 | 缺点 | 更擅长于 |
---|---|---|---|
决策树(DTs) | - 实现、训练和解释简单 - 树形结构可视化 - 数据准备要求较少 - 较少的模型构建和预测时间 - 能处理数值型和类别型数据 - 可通过统计检验验证模型 - 对噪声和缺失值具有鲁棒性 - 高准确率 | - 大型和复杂的树难以解释 - 同一子树中可能出现重复 - 可能存在对角线决策边界问题 - DT 学习器可能创建过于复杂的树,无法很好地泛化数据 - 有时 DTs 可能因数据的小变动而不稳定 - 学习 DT 本身是一个 NP 完全问题(即非确定性多项式时间完全问题) - 如果某些类别占主导地位,DT 学习器会创建有偏的树 | - 目标是高度准确的分类 - 医学诊断和预后 - 信用风险分析 |
表 3: 决策树的优缺点
决策树与朴素贝叶斯比较
如前表所述,DT 由于其对训练数据集的灵活性,非常易于理解和调试。它们既适用于分类问题,也适用于回归问题。
如果你尝试预测分类值或连续值,决策树(DT)可以同时处理这两种问题。因此,如果你只有表格数据,将其输入到决策树中,它将构建模型来分类你的数据,无需任何额外的前期或手动干预。总之,决策树非常简单,易于实现、训练和解释。只需极少的数据准备,决策树就能在更短的预测时间内构建模型。正如前面所说,它们可以处理数字数据和分类数据,并且对噪声和缺失值非常鲁棒。使用统计测试验证模型也非常简单。更有趣的是,构建的树可以进行可视化。总体而言,决策树提供了非常高的准确性。
然而,决策树的缺点是,它们有时会导致训练数据的过拟合问题。这意味着你通常需要修剪树并找到一个最优的树模型,以提高分类或回归的准确性。此外,同一子树中可能会出现重复现象。有时它还会在决策边界上产生斜对角问题,从而导致过拟合和欠拟合的问题。此外,决策树学习器可能会生成过于复杂的树,无法很好地泛化数据,这使得整体解释变得困难。由于数据中的微小变动,决策树可能不稳定,因此学习决策树本身是一个 NP 完全问题。最后,如果某些类别在数据中占主导地位,决策树学习器可能会生成有偏的树。
读者可以参考表 1和表 3,获取朴素贝叶斯和决策树的对比总结。
另一方面,使用朴素贝叶斯时有一种说法:NB 要求你手动构建分类器。你无法直接将一堆表格数据输入它,它不会自动挑选最适合分类的特征。在这种情况下,选择正确的特征以及重要的特征由用户自己决定,也就是你自己。另一方面,决策树会从表格数据中选择最佳特征。鉴于这一点,你可能需要将朴素贝叶斯与其他统计技术结合,以帮助选择最佳特征并在之后进行分类。或者,使用决策树来提高准确性,特别是在精确度、召回率和 F1 度量方面。另一个关于朴素贝叶斯的优点是,它会作为一个连续的分类器进行输出。然而,缺点是它们更难调试和理解。朴素贝叶斯在训练数据中没有良好的特征且数据量较小时表现得相当不错。
总之,如果你试图从这两种方法中选择一个更好的分类器,通常最好测试每个模型来解决问题。我的建议是,使用你拥有的训练数据构建决策树和朴素贝叶斯分类器,然后使用可用的性能指标比较它们的表现,再根据数据集的特性决定哪一个最适合解决你的问题。
使用决策树算法构建可扩展的分类器
正如你已经看到的,使用 OVTR 分类器时,我们在 OCR 数据集上观察到了以下性能指标值:
Accuracy = 0.5217246545696688
Precision = 0.488360500637862
Recall = 0.5217246545696688
F1 = 0.4695649096879411
Test Error = 0.47827534543033123
这意味着模型在该数据集上的准确度非常低。在本节中,我们将看到如何通过使用 DT 分类器来提升性能。我们将展示一个使用 Spark 2.1.0 的例子,使用相同的 OCR 数据集。这个例子将包含多个步骤,包括数据加载、解析、模型训练,最后是模型评估。
由于我们将使用相同的数据集,为避免冗余,我们将跳过数据集探索步骤,直接进入示例:
第 1 步: 加载所需的库和包,如下所示:
import org.apache.spark.ml.Pipeline // for Pipeline creation
import org.apache.spark.ml.classification.DecisionTreeClassificationModel
import org.apache.spark.ml.classification.DecisionTreeClassifier
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
import org.apache.spark.ml.feature.{IndexToString, StringIndexer, VectorIndexer}
import org.apache.spark.sql.SparkSession //For a Spark session
第 2 步: 创建一个活动的 Spark 会话,如下所示:
val spark = SparkSession.builder.master("local[*]").config("spark.sql.warehouse.dir", "/home/exp/").appName("DecisionTreeClassifier").getOrCreate()
请注意,这里将主 URL 设置为 local[*]
,意味着你机器的所有核心将用于处理 Spark 作业。你应该根据需求设置 SQL 仓库和其他配置参数。
第 3 步:创建数据框 - 将存储在 LIBSVM 格式中的数据加载为数据框,如下所示:
val data = spark.read.format("libsvm").load("datab/Letterdata_libsvm.data")
对于数字分类,输入特征向量通常是稀疏的,应该将稀疏向量作为输入,以便利用稀疏性。由于训练数据只使用一次,而且数据集的大小相对较小(即几 MB),如果你多次使用 DataFrame,可以缓存它。
第 4 步:标签索引 - 索引标签,向标签列添加元数据。然后让我们在整个数据集上进行拟合,以便将所有标签包含在索引中:
val labelIndexer = new StringIndexer().setInputCol("label").setOutputCol("indexedLabel").fit(data)
第 5 步:识别分类特征 - 以下代码段自动识别分类特征并对其进行索引:
val featureIndexer = new VectorIndexer().setInputCol("features").setOutputCol("indexedFeatures").setMaxCategories(4).fit(data)
对于这种情况,如果特征的值大于四个不同的值,它们将被视为连续值。
第 6 步:准备训练集和测试集 - 将数据分成训练集和测试集(25% 用于测试):
val Array(trainingData, testData) = data.randomSplit(Array(0.75, 0.25), 12345L)
第 7 步: 按如下方式训练 DT 模型:
val dt = new DecisionTreeClassifier().setLabelCol("indexedLabel").setFeaturesCol("indexedFeatures")
第 8 步: 按如下方式将索引标签转换回原始标签:
val labelConverter = new IndexToString().setInputCol("prediction").setOutputCol("predictedLabel").setLabels(labelIndexer.labels)
第 9 步:创建 DT 管道 - 让我们通过组合索引器、标签转换器和树来创建一个 DT 管道:
val pipeline = new Pipeline().setStages(Array(labelIndexer,featureIndexer, dt, labelconverter))
第 10 步:运行索引器 - 使用变换器训练模型并运行索引器:
val model = pipeline.fit(trainingData)
第 11 步:计算测试集上的预测结果 - 使用模型变换器计算预测结果,并最终显示每个标签的预测结果,如下所示:
val predictions = model.transform(testData)
predictions.show()
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00344.jpeg图 9: 针对每个标签的预测(即每个字母)
如前图所示,部分标签预测准确,而部分标签预测错误。然而,我们知道加权准确率、精确率、召回率和 F1 值,但我们需要先评估模型。
第 12 步:评估模型 - 选择预测结果和真实标签来计算测试误差和分类性能指标,如准确率、精确率、召回率和 F1 值,如下所示:
val evaluator = new MulticlassClassificationEvaluator().setLabelCol("label").setPredictionCol("prediction")
val evaluator1 = evaluator.setMetricName("accuracy")
val evaluator2 = evaluator.setMetricName("weightedPrecision")
val evaluator3 = evaluator.setMetricName("weightedRecall")
val evaluator4 = evaluator.setMetricName("f1")
步骤 13. 计算性能指标 - 计算测试数据上的分类准确率、精确率、召回率、F1 值和错误率,如下所示:
val accuracy = evaluator1.evaluate(predictions)
val precision = evaluator2.evaluate(predictions)
val recall = evaluator3.evaluate(predictions)
val f1 = evaluator4.evaluate(predictions)
步骤 14. 打印性能指标:
println("Accuracy = " + accuracy)
println("Precision = " + precision)
println("Recall = " + recall)
println("F1 = " + f1)
println(s"Test Error = ${1 - accuracy}")
你应该观察到以下值:
Accuracy = 0.994277821625888
Precision = 0.9904583933020722
Recall = 0.994277821625888
F1 = 0.9919966504321712
Test Error = 0.005722178374112041
现在性能很优秀,对吧?然而,你仍然可以通过执行超参数调整来提高分类准确率。通过交叉验证和训练集划分,选择合适的算法(即分类器或回归器)还有进一步提高预测准确性的机会。
步骤 15. 打印决策树节点:
val treeModel = model.stages(2).asInstanceOf[DecisionTreeClassificationModel]
println("Learned classification tree model:\n" + treeModel.toDebugString)
最后,我们将打印决策树中的一些节点,如下图所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00199.gif图 10: 在模型构建过程中生成的一些决策树节点
总结
本章中,我们讨论了一些机器学习中的高级算法,并了解了如何使用简单而强大的贝叶斯推断方法来构建另一种分类模型——多项式分类算法。此外,我们还从理论和技术角度广泛讨论了朴素贝叶斯算法。在最后一步,我们讨论了决策树与朴素贝叶斯算法的对比分析,并提供了一些指导方针。
在下一章中,我们将深入研究机器学习,探索如何利用机器学习将无监督观察数据集中的记录进行聚类。
第十四章:是时候整理一下 - 使用 Spark MLlib 聚类你的数据
“如果你试图把一个星系做大,它就变成了一个星系团,而不是一个星系。如果你试图让它变得比这个还小,它似乎会自我分裂。”
- 杰里迈·P·奥斯特里克
在本章中,我们将深入探讨机器学习,并了解如何利用它将属于某个特定组或类别的记录聚类到无监督观察数据集中的方式。简而言之,本章将涵盖以下主题:
-
无监督学习
-
聚类技术
-
层次聚类(HC)
-
基于质心的聚类(CC)
-
基于分布的聚类(DC)
-
确定聚类数目
-
聚类算法的比较分析
-
提交计算集群上的任务
无监督学习
在本节中,我们将简要介绍无监督机器学习技术,并提供适当的示例。让我们从一个实际例子开始讨论。假设你在硬盘上的一个拥挤且庞大的文件夹里有大量未被盗版的、完全合法的 mp3 文件。现在,如果你能够构建一个预测模型,帮助自动将相似的歌曲分组,并将它们组织到你最喜欢的类别中,比如乡村、说唱、摇滚等等,那该多好。这个将项目分配到某一组的行为,就像是将 mp3 文件添加到相应的播放列表中,是一种无监督的方式。在前几章中,我们假设你得到了一个标注正确的训练数据集。然而,现实世界中,我们并不总是能够拥有这样的奢侈。例如,假设我们想要将大量音乐分成有趣的播放列表。如果我们无法直接访问它们的元数据,那我们如何可能将这些歌曲分组呢?一种可能的方法是将多种机器学习技术结合使用,但聚类通常是解决方案的核心。
简而言之,在无监督机器学习问题中,训练数据集的正确类别是不可用或未知的。因此,类别必须从结构化或非结构化数据集中推导出来,如图 1所示。这本质上意味着,这种类型的算法的目标是以某种结构化方式对数据进行预处理。换句话说,无监督学习算法的主要目标是探索输入数据中未标注的隐藏模式。然而,无监督学习还包括其他技术,以探索数据的关键特征,从而找到隐藏的模式。为了解决这一挑战,聚类技术被广泛应用于根据某些相似性度量以无监督的方式对未标注的数据点进行分组。
要深入了解无监督算法的理论知识,请参考以下三本书:Bousquet, O.; von Luxburg, U.; Raetsch, G., eds(2004)。高级机器学习讲座。Springer-Verlag。ISBN 978-3540231226,或 Duda, Richard O.;Hart, Peter E.;Stork, David G。(2001)。无监督学习与聚类。模式分类(第 2 版)。Wiley。ISBN 0-471-05669-3 和 Jordan, Michael I.;Bishop, Christopher M。(2004)神经网络。收录于 Allen B. Tucker 计算机科学手册,第 2 版(第七部分:智能系统)。佛罗里达州博卡拉顿:Chapman and Hall/CRC Press LLC。ISBN 1-58488-360-X。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00263.jpeg图 1: 使用 Spark 的无监督学习
无监督学习示例
在聚类任务中,算法通过分析输入示例之间的相似性,将相关特征分组成类别,其中相似的特征被聚集并用圆圈标出。聚类的应用包括但不限于以下几个方面:搜索结果分组,如客户分组,异常检测用于发现可疑模式,文本分类用于在文本中发现有用模式,社交网络分析用于发现一致的群体,数据中心计算机集群用于将相关计算机组合在一起,天文数据分析用于银河系形成,房地产数据分析用于根据相似特征识别邻里。我们将展示一个基于 Spark MLlib 的解决方案,适用于最后一个用例。
聚类技术
在本节中,我们将讨论聚类技术、挑战以及适用的示例。还将简要概述层次聚类、基于质心的聚类和基于分布的聚类。
无监督学习与聚类
聚类分析是将数据样本或数据点划分并放入相应的同质类或聚类中的过程。因此,聚类的一个简单定义可以被认为是将对象组织成在某种方式上相似的组。
因此,一个聚类是一个对象集合,这些对象在彼此之间是相似的,而与属于其他聚类的对象是不同的。如图 2所示,如果给定一组对象,聚类算法会根据相似性将这些对象分组。像 K-means 这样的聚类算法会定位数据点组的质心。然而,为了使聚类更加准确有效,算法需要评估每个点与聚类质心之间的距离。最终,聚类的目标是确定一组未标记数据中的内在分组。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00059.jpeg图 2: 聚类原始数据
Spark 支持许多聚类算法,如K-means、高斯混合、幂迭代聚类(PIC)、潜在狄利克雷分配(LDA)、二分 K-means和流式 K-means。LDA 常用于文档分类和聚类,广泛应用于文本挖掘。PIC 用于聚类图的顶点,该图的边属性由成对相似度表示。然而,为了让本章的目标更加清晰和集中,我们将仅讨论 K-means、二分 K-means 和高斯混合算法。
层次聚类
层次聚类技术基于这样一个基本思想:对象或特征与附近的对象或特征相比,更相关,而与远离的对象或特征的相关性较低。二分 K-means 是这样一种层次聚类算法的例子,它根据数据对象之间的对应距离将数据对象连接成簇。
在层次聚类技术中,聚类可以通过连接聚类各部分所需的最大距离来简单地描述。因此,不同的聚类会在不同的距离下形成。从图形上看,这些聚类可以使用树状图表示。有趣的是,层次聚类这一常见名称来源于树状图的概念。
基于中心的聚类
在基于中心的聚类技术中,聚类通过一个中心向量来表示。然而,这个向量本身不一定是数据点的成员。在这种类型的学习中,必须在训练模型之前提供一个预设的聚类数目。K-means 是这种学习类型的一个非常著名的例子,其中,如果你将聚类数目设置为一个固定的整数 K,K-means 算法就会将其定义为一个优化问题,这是一个独立的问题,用于找到 K 个聚类中心,并将数据对象分配到距离它们最近的聚类中心。简而言之,这是一个优化问题,目标是最小化聚类间的平方距离。
基于分布的聚类
基于分布的聚类算法是基于统计分布模型的,这些模型提供了更便捷的方式,将相关的数据对象聚类到相同的分布中。尽管这些算法的理论基础非常稳健,但它们大多存在过拟合的问题。然而,通过对模型复杂度施加约束,可以克服这一限制。
基于中心的聚类(CC)
在这一部分,我们将讨论基于中心的聚类技术及其计算挑战。将通过使用 Spark MLlib 的 K-means 示例,帮助更好地理解基于中心的聚类。
CC 算法中的挑战
如前所述,在像 K-means 这样的基于质心的聚类算法中,设定聚类数 K 的最优值是一个优化问题。这个问题可以被描述为 NP-hard(即非确定性多项式时间困难),具有较高的算法复杂性,因此常见的方法是尝试仅得到一个近似解。因此,解决这些优化问题会增加额外的负担,并因此带来不容忽视的缺点。此外,K-means 算法假设每个聚类的大小大致相同。换句话说,为了获得更好的聚类效果,每个聚类中的数据点必须是均匀的。
该算法的另一个主要缺点是,它试图优化聚类中心而不是聚类边界,这常常会导致错误地切割聚类之间的边界。然而,有时我们可以通过视觉检查来弥补这一点,但这通常不适用于超平面上的数据或多维数据。尽管如此,关于如何找到 K 的最优值的完整内容将在本章后面讨论。
K-means 算法是如何工作的?
假设我们有 n 个数据点 x[i],i=1…n,需要将它们划分为 k 个聚类。现在目标是为每个数据点分配一个聚类。K-means 算法的目标是通过求解以下方程来找到聚类的位置 μ[i],i=1…k,以最小化数据点到聚类的距离。数学上,K-means 算法通过解决以下优化问题来实现这一目标:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00320.jpeg
在上述方程中,c[i] 是分配给聚类 i 的数据点集合,d(x,μ[i]) =||x−μ[i]||²[2] 是要计算的欧几里得距离(我们稍后会解释为什么要使用这种距离度量)。因此,我们可以理解,使用 K-means 进行的整体聚类操作并非一个简单的问题,而是一个 NP-hard 优化问题。这也意味着 K-means 算法不仅仅是寻找全局最小值,还经常会陷入不同的局部解。
现在,让我们看看在将数据输入 K-means 模型之前,我们如何能制定算法。首先,我们需要预先决定聚类数 k。然后,通常你需要遵循以下步骤:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00367.jpeg
这里 |c| 表示 c 中元素的数量。
使用 K-means 算法进行聚类时,首先将所有坐标初始化为质心。随着算法的每次迭代,每个点会根据某种距离度量(通常是欧几里得距离)分配到离它最近的质心。
距离计算: 请注意,还有其他方法可以计算距离,例如:
切比雪夫距离 可以用来通过只考虑最显著的维度来度量距离。
哈明距离算法可以识别两个字符串之间的差异。另一方面,为了使距离度量具有尺度不变性,可以使用马氏距离来标准化协方差矩阵。曼哈顿距离用于通过仅考虑轴对齐的方向来衡量距离。闵可夫斯基距离算法用于统一欧几里得距离、曼哈顿距离和切比雪夫距离。哈弗辛距离用于测量球面上两点之间的大圆距离,也就是经度和纬度之间的距离。
考虑到这些距离测量算法,可以清楚地看出,欧几里得距离算法将是解决 K-means 算法中距离计算问题的最合适选择。接着,质心将被更新为该次迭代中分配给它的所有点的中心。这一过程将重复,直到质心变化最小。简而言之,K-means 算法是一个迭代算法,分为两个步骤:
-
聚类分配步骤:K-means 算法会遍历数据集中的每一个 m 个数据点,并将其分配到最接近的 k 个质心所代表的聚类中。对于每个点,计算它到每个质心的距离,并简单地选择距离最小的一个。
-
更新步骤:对于每个聚类,计算一个新的质心,该质心是该聚类中所有点的均值。从前一步骤中,我们得到了一个已分配到聚类中的点集。现在,对于每一个这样的点集,我们计算均值,并将其声明为新的聚类质心。
使用 Spark MLlib 的 K-means 聚类示例
为了进一步展示聚类的例子,我们将使用从Saratoga NY Homes 数据集下载的萨拉托加纽约住宅数据集,采用 Spark MLlib 进行无监督学习技术。该数据集包含了位于纽约市郊区的多栋住宅的若干特征。例如,价格、地块大小、临水、建筑年龄、土地价值、新建、中央空调、燃料类型、供暖类型、排污类型、居住面积、大学毕业率、卧室数量、壁炉数量、浴室数量以及房间数量。然而,以下表格中仅展示了部分特征:
价格 | 地块大小 | 临水 | 建筑年龄 | 土地价值 | 房间数 |
---|---|---|---|---|---|
132,500 | 0.09 | 0 | 42 | 5,000 | 5 |
181,115 | 0.92 | 0 | 0 | 22,300 | 6 |
109,000 | 0.19 | 0 | 133 | 7,300 | 8 |
155,000 | 0.41 | 0 | 13 | 18,700 | 5 |
86,060 | 0.11 | 0 | 0 | 15,000 | 3 |
120,000 | 0.68 | 0 | 31 | 14,000 | 8 |
153,000 | 0.4 | 0 | 33 | 23,300 | 8 |
170,000 | 1.21 | 0 | 23 | 146,000 | 9 |
90,000 | 0.83 | 0 | 36 | 222,000 | 8 |
122,900 | 1.94 | 0 | 4 | 212,000 | 6 |
325,000 | 2.29 | 0 | 123 | 126,000 | 12 |
表 1: 来自萨拉托加纽约住宅数据集的示例数据
该聚类技术的目标是基于每个房屋的特征,进行探索性分析,寻找可能的邻里区域,以便为位于同一地区的房屋找到潜在的邻居。在进行特征提取之前,我们需要加载并解析萨拉托加 NY 房屋数据集。此步骤还包括加载包和相关依赖项,读取数据集作为 RDD,模型训练、预测、收集本地解析数据以及聚类比较。
步骤 1. 导入相关包:
package com.chapter13.Clustering
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.mllib.clustering.{KMeans, KMeansModel}
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark._
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.functions._
import org.apache.spark.sql.types._
import org.apache.spark.sql._
import org.apache.spark.sql.SQLContext
步骤 2. 创建 Spark 会话 - 入口点 - 这里我们首先通过设置应用程序名称和主机 URL 来配置 Spark。为了简化起见,它是独立运行,并使用您机器上的所有核心:
val spark = SparkSession.builder.master("local[*]").config("spark.sql.warehouse.dir", "E:/Exp/").appName("KMeans").getOrCreate()
步骤 3. 加载和解析数据集 - 读取、解析并从数据集中创建 RDD,如下所示:
//Start parsing the dataset
val start = System.currentTimeMillis()
val dataPath = "data/Saratoga NY Homes.txt"
//val dataPath = args(0)
val landDF = parseRDD(spark.sparkContext.textFile(dataPath)).map(parseLand).toDF().cache()
landDF.show()
请注意,为了使前面的代码正常工作,您应该导入以下包:
import spark.sqlContext.implicits._
您将得到如下输出:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00339.jpeg**图 3:**萨拉托加 NY 房屋数据集快照
以下是parseLand
方法,用于从一个Double
数组创建一个Land
类,如下所示:
// function to create a Land class from an Array of Double
def parseLand(line: Array[Double]): Land = {Land(line(0), line(1), line(2), line(3), line(4), line(5),line(6), line(7), line(8), line(9), line(10),line(11), line(12), line(13), line(14), line(15))
}
读取所有特征为 double 类型的Land
类如下所示:
case class Land(Price: Double, LotSize: Double, Waterfront: Double, Age: Double,LandValue: Double, NewConstruct: Double, CentralAir: Double, FuelType: Double, HeatType: Double, SewerType: Double, LivingArea: Double, PctCollege: Double, Bedrooms: Double,Fireplaces: Double, Bathrooms: Double, rooms: Double
)
如您所知,训练 K-means 模型时,我们需要确保所有数据点和特征都是数值类型。因此,我们还需要将所有数据点转换为 double 类型,如下所示:
// method to transform an RDD of Strings into an RDD of Double
def parseRDD(rdd: RDD[String]): RDD[Array[Double]] = {rdd.map(_.split(",")).map(_.map(_.toDouble))
}
步骤 4. 准备训练集 - 首先,我们需要将数据框(即landDF
)转换为一个包含 double 类型数据的 RDD,并缓存数据,以创建一个新的数据框来链接集群编号,如下所示:
val rowsRDD = landDF.rdd.map(r => (r.getDouble(0), r.getDouble(1), r.getDouble(2),r.getDouble(3), r.getDouble(4), r.getDouble(5),r.getDouble(6), r.getDouble(7), r.getDouble(8),r.getDouble(9), r.getDouble(10), r.getDouble(11),r.getDouble(12), r.getDouble(13), r.getDouble(14),r.getDouble(15))
)
rowsRDD.cache()
现在我们需要将前面的 RDD(包含 double 类型数据)转换为一个包含稠密向量的 RDD,如下所示:
// Get the prediction from the model with the ID so we canlink them back to other information
val predictions = rowsRDD.map{r => (r._1, model.predict(Vectors.dense(r._2, r._3, r._4, r._5, r._6, r._7, r._8, r._9,r._10, r._11, r._12, r._13, r._14, r._15, r._16)
))}
步骤 5. 训练 K-means 模型 - 通过指定 10 个集群、20 次迭代和 10 次运行来训练模型,如下所示:
val numClusters = 5
val numIterations = 20
val run = 10
val model = KMeans.train(numericHome, numClusters,numIterations, run,KMeans.K_MEANS_PARALLEL)
基于 Spark 的 K-means 实现通过使用K-means++算法初始化一组集群中心开始工作, Bahmani 等人提出的K-means++,VLDB 2012。这是 K-means++的一种变体,试图通过从一个随机中心开始,然后进行多次选择,通过一个概率方法选择更多的中心,概率与它们到当前集群集合的平方距离成正比。它产生了一个可证明的接近最优聚类的结果。原始论文可以在theory.stanford.edu/~sergei/papers/vldb12-kmpar.pdf
找到。
步骤 6:评估模型误差率 - 标准的 K-means 算法旨在最小化每组数据点之间的距离平方和,即平方欧几里得距离,这也是 WSSSE 的目标。K-means 算法旨在最小化每组数据点(即聚类中心)之间的距离平方和。然而,如果你真的想最小化每组数据点之间的距离平方和,你最终会得到一个模型,其中每个聚类都是自己的聚类中心;在这种情况下,那个度量值将是 0。
因此,一旦你通过指定参数训练了模型,你可以使用集合内平方误差和(WSSE)来评估结果。从技术上讲,它就像是计算每个 K 个聚类中每个观察值的距离总和,计算公式如下:
// Evaluate clustering by computing Within Set Sum of Squared Errors
val WCSSS = model.computeCost(landRDD)
println("Within-Cluster Sum of Squares = " + WCSSS)
前面的模型训练集产生了 WCSSS 的值:
Within-Cluster Sum of Squares = 1.455560123603583E12
步骤 7:计算并打印聚类中心 - 首先,我们从模型中获取预测结果和 ID,以便可以将其与每个房子相关的其他信息进行关联。请注意,我们将使用在步骤 4 中准备的 RDD 行:
// Get the prediction from the model with the ID so we can link themback to other information
val predictions = rowsRDD.map{r => (r._1, model.predict(Vectors.dense(r._2, r._3, r._4, r._5, r._6, r._7, r._8, r._9, r._10,r._11, r._12, r._13, r._14, r._15, r._16)
))}
然而,在请求有关价格的预测时,应该提供该数据。可以按照如下方式操作:
val predictions = rowsRDD.map{r => (r._1, model.predict(Vectors.dense(r._1, r._2, r._3, r._4, r._5, r._6, r._7, r._8, r._9, r._10,r._11, r._12, r._13, r._14, r._15, r._16)
))}
为了更好的可视化和探索性分析,可以将 RDD 转换为 DataFrame,代码如下:
import spark.sqlContext.implicits._val predCluster = predictions.toDF("Price", "CLUSTER")
predCluster.show()
这将生成如下图所示的输出结果:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00044.gif图 4: 聚类预测的快照
由于数据集中没有可区分的 ID,我们使用Price
字段来进行关联。从前面的图中,你可以了解某个价格的房子属于哪个聚类,即属于哪个簇。为了更好的可视化效果,我们将预测的 DataFrame 与原始的 DataFrame 进行合并,以便知道每个房子对应的具体聚类编号:
val newDF = landDF.join(predCluster, "Price")
newDF.show()
你应该在下图中观察到输出结果:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00138.jpeg图 5: 每个房子预测的聚类快照
为了进行分析,我们将输出数据导入到 RStudio 中,并生成了如图 6所示的聚类。R 脚本可以在我的 GitHub 仓库中找到,网址是github.com/rezacsedu/ScalaAndSparkForBigDataAnalytics
。另外,你也可以编写自己的脚本并据此进行可视化。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00142.jpeg图 6: 社区的聚类
现在,为了进行更广泛的分析和可视化,我们可以观察每个聚类的相关统计数据。例如,下面我打印了与聚类 3 和 4 相关的统计数据,分别在图 8和图 9中展示:
newDF.filter("CLUSTER = 0").show()
newDF.filter("CLUSTER = 1").show()
newDF.filter("CLUSTER = 2").show()
newDF.filter("CLUSTER = 3").show()
newDF.filter("CLUSTER = 4").show()
现在获取每个聚类的描述性统计数据,见下:
newDF.filter("CLUSTER = 0").describe().show()
newDF.filter("CLUSTER = 1").describe().show()
newDF.filter("CLUSTER = 2").describe().show()
newDF.filter("CLUSTER = 3").describe().show()
newDF.filter("CLUSTER = 4").describe().show()
首先,让我们观察聚类 3 的相关统计数据,见下图:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00353.jpeg图 7: 聚类 3 的统计数据
现在让我们观察聚类 4 的相关统计数据,见下图:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00377.jpeg图 8: 聚类 4 的统计数据
请注意,由于原始截图太大,无法适应本页,因此原始图像已被修改,并且删除了包含其他房屋变量的列。
由于该算法的随机性,每次成功迭代时可能会得到不同的结果。然而,您可以通过以下方法锁定该算法的随机性:
val numClusters = 5
val numIterations = 20
val seed = 12345
val model = KMeans.train(landRDD, numClusters, numIterations, seed)
第 8 步:停止 Spark 会话 - 最后,使用 stop 方法停止 Spark 会话,如下所示:
spark.stop()
在前面的例子中,我们处理了一个非常小的特征集;常识和目视检查也会得出相同的结论。从上面的 K-means 算法示例中,我们可以理解该算法存在一些局限性。例如,很难预测 K 值,且全局簇表现不佳。此外,不同的初始分区可能会导致不同的最终簇,最后,它对不同大小和密度的簇表现不佳。
为了克服这些局限性,本书中介绍了一些更强大的算法,如 MCMC(马尔可夫链蒙特卡洛;参见en.wikipedia.org/wiki/Markov_chain_Monte_Carlo
)在书中呈现:Tribble, Seth D., 马尔可夫链蒙特卡洛算法使用完全均匀分布的驱动序列,斯坦福大学博士论文,2007 年。
层次聚类(HC)
在本节中,我们讨论层次聚类技术及其计算挑战。还将展示一个使用 Spark MLlib 的层次聚类的双分 K-means 算法示例,以更好地理解层次聚类。
层次聚类算法概述及挑战
层次聚类技术与基于质心的聚类在计算距离的方式上有所不同。这是最受欢迎和广泛使用的聚类分析技术之一,旨在构建一个簇的层次结构。由于一个簇通常包含多个对象,因此还会有其他候选项来计算距离。因此,除了通常选择的距离函数外,还需要决定使用的连接标准。简而言之,层次聚类中有两种策略:
-
自底向上方法:在这种方法中,每个观察从其自身簇开始。之后,簇的对会合并在一起,然后向上移动到层次结构中。
-
自顶向下方法:在这种方法中,所有观察从一个簇开始,分裂是递归进行的,然后向下移动到层次结构中。
这些自底向上或自顶向下的方法基于单链聚类(SLINK)技术,该技术考虑最小的对象距离;完全链聚类(CLINK),该方法考虑对象距离的最大值;以及无权重配对组法平均法(UPGMA)。后者也被称为平均链聚类。从技术上讲,这些方法不会从数据集中产生唯一的划分(即不同的簇)。
对这三种方法的比较分析可以在nlp.stanford.edu/IR-book/completelink.html.
找到。
然而,用户仍然需要从层次结构中选择合适的簇,以获得更好的聚类预测和分配。虽然这一类算法(如二分 K-means)在计算上比 K-means 算法更快,但这种类型的算法也有三个缺点:
-
首先,这些方法对于异常值或包含噪声或缺失值的数据集并不是非常稳健。这个缺点会导致附加的簇,甚至可能导致其他簇合并。这个问题通常被称为链式现象,尤其在单链聚类(single-linkage clustering)中比较常见。
-
其次,从算法分析来看,聚合型聚类和分裂型聚类的复杂度较高,这使得它们对于大数据集来说过于缓慢。
-
第三,SLINK 和 CLINK 曾经在数据挖掘任务中广泛使用,作为聚类分析的理论基础,但如今它们被认为是过时的。
使用 Spark MLlib 实现二分 K-means
二分 K-means 通常比常规 K-means 更快,但它通常会产生不同的聚类结果。二分 K-means 算法基于论文《A comparison of document clustering》中的方法,作者为 Steinbach、Karypis 和 Kumar,并经过修改以适应 Spark MLlib。
二分 K-means 是一种分裂型算法,它从一个包含所有数据点的单一簇开始。然后,它迭代地找到底层所有可分的簇,并使用 K-means 对每个簇进行二分,直到总共有 K 个叶子簇,或者没有可分的叶子簇为止。之后,同一层次的簇会被组合在一起,以增加并行性。换句话说,二分 K-means 在计算上比常规的 K-means 算法更快。需要注意的是,如果对底层所有可分簇进行二分后得到的叶子簇数量超过 K,则较大的簇会优先被选择。
请注意,如果对底层所有可分簇进行二分后得到的叶子簇数量超过 K,则较大的簇会优先被选择。以下是 Spark MLlib 实现中使用的参数:
-
K:这是期望的叶子聚类数量。然而,如果在计算过程中没有可分割的叶子聚类,实际数量可能会更少。默认值为 4。
-
MaxIterations:这是 K-means 算法中用于分割聚类的最大迭代次数。默认值为 20。
-
MinDivisibleClusterSize:这是最小的点数。默认值为 1。
-
Seed:这是一个随机种子,禁止随机聚类,并尽量在每次迭代中提供几乎相同的结果。然而,建议使用较长的种子值,如 12345 等。
使用 Spark MLlib 对邻里进行二分 K-means 聚类
在上一节中,我们看到如何将相似的房屋聚集在一起,以确定邻里。二分 K-means 算法与常规 K-means 算法类似,不同之处在于模型训练使用了不同的训练参数,如下所示:
// Cluster the data into two classes using KMeans
val bkm = new BisectingKMeans() .setK(5) // Number of clusters of the similar houses.setMaxIterations(20)// Number of max iteration.setSeed(12345) // Setting seed to disallow randomness
val model = bkm.run(landRDD)
你应该参考前面的示例并重新使用前面的步骤来获取训练数据。现在让我们通过计算 WSSSE 来评估聚类,方法如下:
val WCSSS = model.computeCost(landRDD)
println("Within-Cluster Sum of Squares = " + WCSSS) // Less is better
你应该观察到以下输出:Within-Cluster Sum of Squares = 2.096980212594632E11
。现在,若要进行进一步分析,请参阅上一节的第 5 步。
基于分布的聚类(DC)
在这一节中,我们将讨论基于分布的聚类技术及其计算挑战。为了更好地理解基于分布的聚类,将展示一个使用高斯混合模型(GMMs)与 Spark MLlib 的示例。
DC 算法中的挑战
像 GMM 这样的基于分布的聚类算法是一种期望最大化算法。为了避免过拟合问题,GMM 通常使用固定数量的高斯分布来建模数据集。这些分布是随机初始化的,并且相关参数也会进行迭代优化,以便更好地将模型拟合到训练数据集。这是 GMM 最强大的特点,有助于模型向局部最优解收敛。然而,算法的多次运行可能会产生不同的结果。
换句话说,与二分 K-means 算法和软聚类不同,GMM 是针对硬聚类进行优化的,为了获得这种类型,通常会将对象分配到高斯分布中。GMM 的另一个优势是,它通过捕捉数据点和属性之间所需的所有相关性和依赖关系,生成复杂的聚类模型。
不过,GMM 对数据的格式和形状有一些假设,这就给我们(即用户)增加了额外的负担。更具体地说,如果以下两个标准不满足,性能会急剧下降:
-
非高斯数据集:GMM 算法假设数据集具有潜在的高斯分布,这是生成性分布。然而,许多实际数据集不满足这一假设,可能导致较差的聚类性能。
-
如果聚类的大小不均,较小的聚类很可能会被较大的聚类所主导。
高斯混合模型是如何工作的?
使用 GMM 是一种流行的软聚类技术。GMM 试图将所有数据点建模为有限的高斯分布混合体;计算每个点属于每个聚类的概率,并与聚类相关的统计数据一起表示一个合成分布:所有点都来自 K 个具有自身概率的高斯子分布之一。简而言之,GMM 的功能可以用三步伪代码描述:
-
Objective function(目标函数):使用期望最大化(EM)作为框架,计算并最大化对数似然。
-
EM 算法:
-
E 步骤(E step):计算后验概率 - 即靠近的数据点。
-
M 步骤(M step):优化参数。
-
-
Assignment(分配):在 E 步骤中执行软分配。
从技术上讲,当给定一个统计模型时,该模型的参数(即应用于数据集时)是通过 最大似然估计 (MLE) 来估计的。另一方面,EM 算法是一个迭代过程,用于寻找最大似然。
由于 GMM 是一种无监督算法,GMM 模型依赖于推断的变量。然后,EM 迭代会转向执行期望(E)和最大化(M)步骤。
Spark MLlib 实现使用期望最大化算法从给定的数据点集中引导最大似然模型。当前的实现使用以下参数:
-
K 是所需聚类数,用于聚类你的数据点。
-
ConvergenceTol(收敛容忍度) 是我们认为收敛已达成时,最大对数似然的变化量。
-
MaxIterations(最大迭代次数) 是在没有达到收敛点的情况下执行的最大迭代次数。
-
InitialModel 是一个可选的起始点,用于启动 EM 算法。如果省略此参数,将从数据中构造一个随机起始点。
使用 Spark MLlib 进行 GMM 聚类的示例
在前面的章节中,我们看到了如何将相似的房屋聚集在一起以确定邻里。使用 GMM,也可以将房屋聚集在一起以寻找邻里,除了模型训练会采用不同的训练参数,如下所示:
val K = 5
val maxIteration = 20
val model = new GaussianMixture().setK(K)// Number of desired clusters.setMaxIterations(maxIteration)//Maximum iterations.setConvergenceTol(0.05) // Convergence tolerance. .setSeed(12345) // setting seed to disallow randomness.run(landRDD) // fit the model using the training set
你应该参考之前的示例,并重用获取训练数据的先前步骤。现在为了评估模型的性能,GMM 并没有提供像 WCSS 这样的性能指标作为代价函数。然而,GMM 提供了一些性能指标,比如 mu、sigma 和权重。这些参数表示不同聚类之间的最大似然(我们这里有五个聚类)。这可以如下演示:
// output parameters of max-likelihood model
for (i <- 0 until model.K) {println("Cluster " + i)println("Weight=%f\nMU=%s\nSigma=\n%s\n" format(model.weights(i), model.gaussians(i).mu, model.gaussians(i).sigma))
}
你应该观察到以下输出:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00154.jpeg图 9: 簇 1https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00017.jpeg图 10: 簇 2https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00240.jpeg图 11: 簇 3https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00139.jpeg图 12: 簇 4https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00002.jpeg图 13: 簇 5
簇 1 到簇 4 的权重表明这些簇是均质的,并且与簇 5 相比存在显著差异。
确定簇的数量
像 K-means 算法这样的聚类算法的优点在于,它可以对具有无限特征的数据进行聚类。当你有原始数据并希望了解数据中的模式时,这是一个非常好的工具。然而,在实验之前确定簇的数量可能并不成功,有时还可能导致过拟合或欠拟合问题。另一方面,K-means、二分 K-means 和高斯混合模型这三种算法的共同之处在于,簇的数量必须事先确定,并作为参数提供给算法。因此,非正式地说,确定簇的数量是一个独立的优化问题,需要解决。
在本节中,我们将使用基于肘部法则的启发式方法。我们从 K = 2 个簇开始,然后通过增加 K 并观察成本函数簇内平方和(Within-Cluster Sum of Squares)(WCSS)的值,运行 K-means 算法处理相同的数据集。在某些时刻,可以观察到成本函数有一个大的下降,但随着 K 值的增加,改进变得微乎其微。如聚类分析文献所建议的,我们可以选择 WCSS 最后一次大幅下降后的 K 值作为最优值。
通过分析以下参数,你可以找出 K-means 的性能:
-
中介性(Betweenness): 这是中介平方和,也称为簇内相似度(intracluster similarity)。
-
簇内平方和(Withiness): 这是簇内平方和,也叫做簇间相似度(intercluster similarity)。
-
总簇内平方和(Totwithinss): 这是所有簇内的平方和的总和,也叫做总簇内相似度(total intracluster similarity)。
值得注意的是,一个稳健且准确的聚类模型将具有较低的簇内平方和和较高的中介性值。然而,这些值取决于簇的数量,即 K 值,这个值需要在构建模型之前选择。
现在让我们讨论如何利用肘部法则来确定簇的数量。如下面所示,我们计算了 K-means 算法应用于家庭数据(基于所有特征)时,聚类数与成本函数 WCSS 的关系。可以观察到,当 K = 5 时,出现了一个大幅下降。因此,我们选择了 5 作为簇的数量,如图 10所示。基本上,这是最后一次大幅下降之后的值。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00303.jpeg图 14: 聚类数与 WCSS 的关系
聚类算法的比较分析
高斯混合模型主要用于期望最小化,这是优化算法的一个例子。与普通 K-means 算法相比,二分 K-means 更快,并且产生略微不同的聚类结果。下面我们尝试对比这三种算法。我们将展示每种算法在模型构建时间和计算成本方面的性能对比。如以下代码所示,我们可以通过 WCSS 计算成本。以下代码行可以用来计算 K-means 和二分算法的 WCSS:
val WCSSS = model.computeCost(landRDD) // land RDD is the training set
println("Within-Cluster Sum of Squares = " + WCSSS) // Less is better
对于本章使用的数据集,我们得到了以下 WCSS 的值:
Within-Cluster Sum of Squares of Bisecting K-means = 2.096980212594632E11
Within-Cluster Sum of Squares of K-means = 1.455560123603583E12
这意味着在计算成本方面,K-means 的表现稍微好一些。不幸的是,我们没有像 WCSS 这样的度量指标来评估 GMM 算法。现在让我们观察这三种算法的模型构建时间。我们可以在开始模型训练前启动系统时钟,并在训练结束后立即停止时钟,如下所示(对于 K-means):
val start = System.currentTimeMillis()
val numClusters = 5
val numIterations = 20
val seed = 12345
val runs = 50
val model = KMeans.train(landRDD, numClusters, numIterations, seed)
val end = System.currentTimeMillis()
println("Model building and prediction time: "+ {end - start} + "ms")
对于本章使用的训练集,我们得到了以下模型构建时间的值:
Model building and prediction time for Bisecting K-means: 2680ms
Model building and prediction time for Gaussian Mixture: 2193ms
Model building and prediction time for K-means: 3741ms
在不同的研究文章中发现,二分 K-means 算法在数据点的聚类分配上表现得更好。此外,与 K-means 相比,二分 K-means 也能更好地收敛到全局最小值。而 K-means 则容易陷入局部最小值。换句话说,使用二分 K-means 算法,我们可以避免 K-means 可能遭遇的局部最小值问题。
请注意,根据机器的硬件配置和数据集的随机性,您可能会观察到前述参数的不同值。
更详细的分析留给读者从理论角度进行。感兴趣的读者还应参考基于 Spark MLlib 的聚类技术,详情请见 spark.apache.org/docs/latest/mllib-clustering.html
以获得更多见解。
提交 Spark 作业进行聚类分析
本章展示的例子可以扩展到更大的数据集以服务于不同的目的。您可以将所有三种聚类算法与所需的依赖项一起打包,并将它们作为 Spark 作业提交到集群中。现在,使用以下代码行来提交您的 K-means 聚类 Spark 作业,例如(对其他类使用类似语法),以处理 Saratoga NY Homes 数据集:
# Run application as standalone mode on 8 cores
SPARK_HOME/bin/spark-submit \
--class org.apache.spark.examples.KMeansDemo \
--master local[8] \
KMeansDemo-0.1-SNAPSHOT-jar-with-dependencies.jar \
Saratoga_NY_Homes.txt# Run on a YARN cluster
export HADOOP_CONF_DIR=XXX
SPARK_HOME/bin/spark-submit \
--class org.apache.spark.examples.KMeansDemo \
--master yarn \
--deploy-mode cluster \ # can be client for client mode
--executor-memory 20G \
--num-executors 50 \
KMeansDemo-0.1-SNAPSHOT-jar-with-dependencies.jar \
Saratoga_NY_Homes.txt# Run on a Mesos cluster in cluster deploy mode with supervising
SPARK_HOME/bin/spark-submit \
--class org.apache.spark.examples.KMeansDemo \
--master mesos://207.184.161.138:7077 \ # Use your IP aadress
--deploy-mode cluster \
--supervise \
--executor-memory 20G \
--total-executor-cores 100 \
KMeansDemo-0.1-SNAPSHOT-jar-with-dependencies.jar \
Saratoga_NY_Homes.txt
总结
本章中,我们进一步深入探讨了机器学习,并了解了如何利用机器学习对无监督观测数据集中的记录进行聚类。因此,你学习了通过前几章的理解,如何快速而有力地将有监督和无监督技术应用于新问题。我们将展示的例子将从 Spark 的角度进行说明。对于 K-means、二分 K-means 和高斯混合算法,无法保证算法在多次运行时产生相同的聚类结果。例如,我们观察到,使用相同参数多次运行 K-means 算法时,每次运行产生的结果略有不同。
关于 K-means 和高斯混合模型的性能对比,请参见Jung 等人的聚类分析讲义。除了 K-means、二分 K-means 和高斯混合模型外,MLlib 还提供了另外三种聚类算法的实现,分别是 PIC、LDA 和流式 K-means。值得一提的是,为了精细调优聚类分析,我们通常需要去除一些被称为离群点或异常值的无效数据对象。但使用基于距离的聚类方法时,确实很难识别这些数据点。因此,除了欧氏距离外,还可以使用其他距离度量。无论如何,这些链接将是一个很好的起点资源:
-
mapr.com/ebooks/spark/08-unsupervised-anomaly-detection-apache-spark.html
-
github.com/keiraqz/anomaly-detection
-
www.dcc.fc.up.pt/~ltorgo/Papers/ODCM.pdf
在下一章中,我们将深入探讨如何调优 Spark 应用以提高性能。我们将看到一些优化 Spark 应用性能的最佳实践。
第十五章:使用 Spark ML 进行文本分析
“程序必须为人类阅读而编写,只有在偶然的情况下才是为了机器执行。”
- 哈罗德·阿贝尔森
在本章中,我们将讨论使用 Spark ML 进行文本分析这一美妙的领域。文本分析是机器学习中的一个广泛领域,并且在许多用例中都很有用,比如情感分析、聊天机器人、电子邮件垃圾邮件检测和自然语言处理。我们将学习如何使用 Spark 进行文本分析,重点介绍使用 Twitter 的 10,000 个样本数据集进行文本分类的用例。
简而言之,本章将涵盖以下主题:
-
理解文本分析
-
转换器和估计器
-
分词器
-
StopWordsRemover
-
NGrams
-
TF-IDF
-
Word2Vec
-
CountVectorizer
-
使用 LDA 进行主题建模
-
实现文本分类
理解文本分析
在过去几章中,我们已经探索了机器学习的世界以及 Apache Spark 对机器学习的支持。正如我们所讨论的,机器学习有一个工作流程,这些流程可以通过以下步骤来解释:
-
加载或获取数据。
-
清洗数据。
-
从数据中提取特征。
-
在数据上训练模型,根据特征生成期望的结果。
-
根据数据评估或预测某些结果。
一个典型流水线的简化视图如下所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00310.jpeg
因此,在训练模型并随后部署之前,数据的转换阶段有很多种可能性。此外,我们应该预期特征和模型属性的精细调整。我们甚至可以探索一种完全不同的算法,作为新工作流的一部分重复整个任务序列。
可以通过多个转换步骤创建一个流水线,因此我们使用领域特定语言(DSL)来定义节点(数据转换步骤),从而创建一个DAG(有向无环图)节点。因此,ML 流水线是一个由多个转换器和估计器组成的序列,用于将输入数据集拟合到流水线模型中。流水线中的每个阶段称为流水线阶段,如下所示:
-
估计器
-
模型
-
流水线
-
转换器
-
预测器
当你看一行文本时,我们看到句子、短语、单词、名词、动词、标点符号等,它们组合在一起时具有意义和目的。人类非常擅长理解句子、单词、俚语、注释或上下文。这来自于多年的练习和学习如何读写、正确的语法、标点符号、感叹词等。那么,我们如何编写计算机程序来尝试复制这种能力呢?
文本分析
文本分析是从一组文本中解锁意义的方法。通过使用各种技术和算法处理和分析文本数据,我们可以揭示数据中的模式和主题。所有这些的目标是理解非结构化的文本,以便得出上下文的意义和关系。
文本分析利用几种广泛的技术类别,接下来我们将讨论这些技术。
情感分析
分析 Facebook、Twitter 和其他社交媒体上人们的政治观点是情感分析的一个良好示例。同样,分析 Yelp 上餐厅的评论也是情感分析的另一个很好的示例。
自然语言处理(NLP)框架和库,如 OpenNLP 和斯坦福 NLP,通常用于实现情感分析。
主题建模
主题建模是检测文档语料库中主题或主题的一种有用技术。这是一种无监督算法,可以在一组文档中找到主题。例如,检测新闻文章中涉及的主题。另一个例子是检测专利申请中的思想。
潜在狄利克雷分配(LDA)是一个流行的无监督聚类模型,而潜在语义分析(LSA)则在共现数据上使用概率模型。
TF-IDF(词频-逆文档频率)
TF-IDF 衡量单词在文档中出现的频率以及在一组文档中的相对频率。此信息可用于构建分类器和预测模型。例如垃圾邮件分类、聊天对话等。
命名实体识别(NER)
命名实体识别通过检测句子中单词和名词的使用来提取关于人、组织、地点等的信息。这提供了关于文档实际内容的重要上下文信息,而不仅仅是将单词视为主要实体。
斯坦福 NLP 和 OpenNLP 都实现了 NER 算法。
事件抽取
事件抽取在 NER 基础上扩展,通过建立检测到的实体之间的关系。这可以用于推断两个实体之间的关系。因此,它增加了语义理解的层次,帮助理解文档内容。
变换器和估计器
Transformer 是一个函数对象,通过将变换逻辑(函数)应用于输入数据集,生成输出数据集,从而将一个数据集转换为另一个数据集。变换器有两种类型:标准变换器和估计器变换器。
标准变换器
标准变换器将输入数据集转换为输出数据集,明确地将变换函数应用于输入数据。除了读取输入列和生成输出列外,不依赖于输入数据。
这种变换器的调用方式如下所示:
*outputDF = transfomer.*transform*(inputDF)*
标准变换器的示例如下,并将在后续章节中详细解释:
-
Tokenizer
:此工具使用空格作为分隔符将句子拆分为单词。 -
RegexTokenizer
:此工具使用正则表达式将句子拆分为单词。 -
StopWordsRemover
:此工具从单词列表中去除常用的停用词。 -
Binarizer
:将字符串转换为二进制数字 0/1 -
NGram
:从句子中创建 N 个单词短语 -
HashingTF
:使用哈希表索引单词来创建词频计数 -
SQLTransformer
:实现由 SQL 语句定义的变换 -
VectorAssembler
:将给定的列列表组合成一个单一的向量列
标准 Transformer 的示意图如下,其中来自输入数据集的输入列被转换为输出列,从而生成输出数据集:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00247.jpeg
估算器变换器
估算器变换器通过首先根据输入数据集生成一个变换器来将输入数据集转换为输出数据集。然后,变换器处理输入数据,读取输入列并生成输出列,最终形成输出数据集。
这些变换器如下所示:
*transformer = estimator.*fit*(inputDF)* *outputDF = transformer.*transform*(inputDF)*
估算器变换器的示例如下:
-
IDF
-
LDA
-
Word2Vec
估算器变换器的示意图如下,其中来自输入数据集的输入列被转换为输出列,从而生成输出数据集:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00287.jpeg
在接下来的几个部分,我们将深入探讨文本分析,使用一个简单的示例数据集,数据集包含多行文本(句子),如以下截图所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00330.jpeg
以下代码用于将文本数据加载到输入数据集中。
使用一对 ID 和文本的序列来初始化一组句子,具体如下所示。
val lines = Seq(| (1, "Hello there, how do you like the book so far?"),| (2, "I am new to Machine Learning"),| (3, "Maybe i should get some coffee before starting"),| (4, "Coffee is best when you drink it hot"),| (5, "Book stores have coffee too so i should go to a book store")| )
lines: Seq[(Int, String)] = List((1,Hello there, how do you like the book so far?), (2,I am new to Machine Learning), (3,Maybe i should get some coffee before starting), (4,Coffee is best when you drink it hot), (5,Book stores have coffee too so i should go to a book store))
接下来,调用createDataFrame()
函数从我们之前看到的句子序列创建一个 DataFrame。
scala> val sentenceDF = spark.createDataFrame(lines).toDF("id", "sentence")
sentenceDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string]
现在你可以看到新创建的数据集,其中显示了包含两个列 ID 和句子的 Sentence DataFrame。
scala> sentenceDF.show(false)
|id|sentence |
|1 |Hello there, how do you like the book so far? |
|2 |I am new to Machine Learning |
|3 |Maybe i should get some coffee before starting |
|4 |Coffee is best when you drink it hot |
|5 |Book stores have coffee too so i should go to a book store|
分词
分词器将输入字符串转换为小写,并通过空格将字符串拆分为单独的标记。给定的句子通过默认的空格分隔符或使用自定义正则表达式的分词器拆分为单词。无论哪种方式,输入列都会转换为输出列。特别地,输入列通常是字符串,而输出列是一个单词序列。
分词器可以通过导入接下来的两个包来使用,分别是Tokenizer
和RegexTokenizer
:
import org.apache.spark.ml.feature.Tokenizer
import org.apache.spark.ml.feature.RegexTokenizer
首先,你需要初始化一个Tokenizer
,指定输入列和输出列:
scala> val tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words")
tokenizer: org.apache.spark.ml.feature.Tokenizer = tok_942c8332b9d8
接下来,在输入数据集上调用transform()
函数会生成一个输出数据集:
scala> val wordsDF = tokenizer.transform(sentenceDF)
wordsDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 1 more field]
以下是输出数据集,显示输入列的 ID、句子和输出列的单词,后者包含单词的序列:
scala> wordsDF.show(false)
|id|sentence |words |
|1 |Hello there, how do you like the book so far? |[hello, there,, how, do, you, like, the, book, so, far?] |
|2 |I am new to Machine Learning |[i, am, new, to, machine, learning] |
|3 |Maybe i should get some coffee before starting |[maybe, i, should, get, some, coffee, before, starting] |
|4 |Coffee is best when you drink it hot |[coffee, is, best, when, you, drink, it, hot] |
|5 |Book stores have coffee too so i should go to a book store|[book, stores, have, coffee, too, so, i, should, go, to, a, book, store]|
另一方面,如果你想设置基于正则表达式的Tokenizer
,你必须使用RegexTokenizer
而不是Tokenizer
。为此,你需要初始化一个RegexTokenizer
,指定输入列和输出列,并提供要使用的正则表达式模式:
scala> val regexTokenizer = new RegexTokenizer().setInputCol("sentence").setOutputCol("regexWords").setPattern("\\W")
regexTokenizer: org.apache.spark.ml.feature.RegexTokenizer = regexTok_15045df8ce41
接下来,在输入数据集上调用transform()
函数会产生一个输出数据集:
scala> val regexWordsDF = regexTokenizer.transform(sentenceDF)
regexWordsDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 1 more field]
以下是输出数据集,显示了输入列 ID、句子和输出列regexWordsDF
,其中包含了单词序列:
scala> regexWordsDF.show(false)
|id|sentence |regexWords |
|1 |Hello there, how do you like the book so far? |[hello, there, how, do, you, like, the, book, so, far] |
|2 |I am new to Machine Learning |[i, am, new, to, machine, learning] |
|3 |Maybe i should get some coffee before starting |[maybe, i, should, get, some, coffee, before, starting] |
|4 |Coffee is best when you drink it hot |[coffee, is, best, when, you, drink, it, hot] |
|5 |Book stores have coffee too so i should go to a book store|[book, stores, have, coffee, too, so, i, should, go, to, a, book, store]|
Tokenizer
的图示如下,其中输入文本中的句子通过空格分隔符拆分成单词:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00150.jpeg
StopWordsRemover
StopWordsRemover
是一个转换器,它接受一个包含单词的String
数组,并返回一个String
数组,其中去除了所有已定义的停用词。停用词的一些示例包括 I、you、my、and、or 等,这些在英语中是非常常见的单词。你可以覆盖或扩展停用词的集合,以适应用例的目的。如果没有这个清洗过程,后续的算法可能会因常见单词的影响而产生偏差。
为了调用StopWordsRemover
,你需要导入以下包:
import org.apache.spark.ml.feature.StopWordsRemover
首先,你需要初始化一个StopWordsRemover
,指定输入列和输出列。在这里,我们选择由Tokenizer
创建的单词列,并生成一个输出列,包含在删除停用词后过滤的单词:
scala> val remover = new StopWordsRemover().setInputCol("words").setOutputCol("filteredWords")
remover: org.apache.spark.ml.feature.StopWordsRemover = stopWords_48d2cecd3011
接下来,在输入数据集上调用transform()
函数会产生一个输出数据集:
scala> val noStopWordsDF = remover.transform(wordsDF)
noStopWordsDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 2 more fields]
以下是输出数据集,显示了输入列 ID、句子和输出列filteredWords
,其中包含了单词的序列:
scala> noStopWordsDF.show(false)
|id|sentence |words |filteredWords |
|1 |Hello there, how do you like the book so far? |[hello, there,, how, do, you, like, the, book, so, far?] |[hello, there,, like, book, far?] |
|2 |I am new to Machine Learning |[i, am, new, to, machine, learning] |[new, machine, learning] |
|3 |Maybe i should get some coffee before starting |[maybe, i, should, get, some, coffee, before, starting] |[maybe, get, coffee, starting] |
|4 |Coffee is best when you drink it hot |[coffee, is, best, when, you, drink, it, hot] |[coffee, best, drink, hot] |
|5 |Book stores have coffee too so i should go to a book store|[book, stores, have, coffee, too, so, i, should, go, to, a, book, store]|[book, stores, coffee, go, book, store]|
以下是输出数据集,显示了仅包含句子和filteredWords
的内容,filteredWords
包含了过滤后的单词序列:
scala> noStopWordsDF.select("sentence", "filteredWords").show(5,false)
|sentence |filteredWords |
|Hello there, how do you like the book so far? |[hello, there,, like, book, far?] |
|I am new to Machine Learning |[new, machine, learning] |
|Maybe i should get some coffee before starting |[maybe, get, coffee, starting] |
|Coffee is best when you drink it hot |[coffee, best, drink, hot] |
|Book stores have coffee too so i should go to a book store|[book, stores, coffee, go, book, store]|
StopWordsRemover
的图示如下,显示了过滤掉的停用词,如 I、should、some 和 before:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00021.jpeg
停用词是默认设置的,但可以很容易地覆盖或修改,正如下面的代码片段所示,我们将在过滤后的单词中删除“hello”,将其视为停用词:
scala> val noHello = Array("hello") ++ remover.getStopWords
noHello: Array[String] = Array(hello, i, me, my, myself, we, our, ours, ourselves, you, your, yours, yourself, yourselves, he, him, his, himself, she, her, hers, herself, it, its, itself, they, them, their, theirs, themselves, what, which, who, whom, this, that, these, those, am, is, are, was, were ...
scala>//create new transfomer using the amended Stop Words list
scala> val removerCustom = new StopWordsRemover().setInputCol("words").setOutputCol("filteredWords").setStopWords(noHello)
removerCustom: org.apache.spark.ml.feature.StopWordsRemover = stopWords_908b488ac87f//invoke transform function
scala> val noStopWordsDFCustom = removerCustom.transform(wordsDF)
noStopWordsDFCustom: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 2 more fields]//output dataset showing only sentence and filtered words - now will not show hello
scala> noStopWordsDFCustom.select("sentence", "filteredWords").show(5,false)
+----------------------------------------------------------+---------------------------------------+
|sentence |filteredWords |
+----------------------------------------------------------+---------------------------------------+
|Hello there, how do you like the book so far? |[there,, like, book, far?] |
|I am new to Machine Learning |[new, machine, learning] |
|Maybe i should get some coffee before starting |[maybe, get, coffee, starting] |
|Coffee is best when you drink it hot |[coffee, best, drink, hot] |
|Book stores have coffee too so i should go to a book store|[book, stores, coffee, go, book, store]|
+----------------------------------------------------------+---------------------------------------+
NGrams
NGrams 是由单词组合生成的单词序列。N 代表序列中单词的数量。例如,2-gram 是两个单词在一起,3-gram 是三个单词在一起。setN()
用于指定N
的值。
为了生成 NGrams,你需要导入该包:
import org.apache.spark.ml.feature.NGram
首先,你需要初始化一个NGram
生成器,指定输入列和输出列。在这里,我们选择由StopWordsRemover
创建的过滤单词列,并生成一个输出列,包含在删除停用词后过滤的单词:
scala> val ngram = new NGram().setN(2).setInputCol("filteredWords").setOutputCol("ngrams")
ngram: org.apache.spark.ml.feature.NGram = ngram_e7a3d3ab6115
接下来,在输入数据集上调用transform()
函数会产生一个输出数据集:
scala> val nGramDF = ngram.transform(noStopWordsDF)
nGramDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 3 more fields]
以下是输出数据集,显示了输入列 ID、句子和输出列ngram
,其中包含了 n-gram 序列:
scala> nGramDF.show(false)
|id|sentence |words |filteredWords |ngrams |
|1 |Hello there, how do you like the book so far? |[hello, there,, how, do, you, like, the, book, so, far?] |[hello, there,, like, book, far?] |[hello there,, there, like, like book, book far?] |
|2 |I am new to Machine Learning |[i, am, new, to, machine, learning] |[new, machine, learning] |[new machine, machine learning] |
|3 |Maybe i should get some coffee before starting |[maybe, i, should, get, some, coffee, before, starting] |[maybe, get, coffee, starting] |[maybe get, get coffee, coffee starting] |
|4 |Coffee is best when you drink it hot |[coffee, is, best, when, you, drink, it, hot] |[coffee, best, drink, hot] |[coffee best, best drink, drink hot] |
|5 |Book stores have coffee too so i should go to a book store|[book, stores, have, coffee, too, so, i, should, go, to, a, book, store]|[book, stores, coffee, go, book, store]|[book stores, stores coffee, coffee go, go book, book store]|
以下是输出数据集,显示了句子和 2-gram:
scala> nGramDF.select("sentence", "ngrams").show(5,false)
|sentence |ngrams |
|Hello there, how do you like the book so far? |[hello there,, there, like, like book, book far?] |
|I am new to Machine Learning |[new machine, machine learning] |
|Maybe i should get some coffee before starting |[maybe get, get coffee, coffee starting] |
|Coffee is best when you drink it hot |[coffee best, best drink, drink hot] |
|Book stores have coffee too so i should go to a book store|[book stores, stores coffee, coffee go, go book, book store]|
NGram 的图示如下,显示了在句子经过分词和去除停用词后生成的 2-gram:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00163.jpeg
TF-IDF
TF-IDF 代表词频-逆文档频率,它衡量一个词在文档集合中对某个文档的重要性。它在信息检索中被广泛使用,并反映了词在文档中的权重。TF-IDF 值随着词语出现次数的增加而增加,词语/术语的频率由两个关键元素组成:词频和逆文档频率。
TF 是词频,表示单词/术语在文档中的频率。
对于一个术语t,tf度量术语t在文档d中出现的次数。tf在 Spark 中通过哈希实现,将术语通过哈希函数映射到索引。
IDF 是逆文档频率,表示术语提供的关于该术语在文档中出现的趋势的信息。IDF 是包含该术语的文档数的对数缩放逆函数:
IDF = 总文档数/包含术语的文档数
一旦我们有了TF和IDF,我们就可以通过将TF和IDF相乘来计算TF-IDF值:
TF-IDF = TF * IDF
接下来,我们将看看如何使用 Spark ML 中的 HashingTF 转换器生成TF。
HashingTF
HashingTF是一个转换器,它接受一组术语并通过哈希每个术语来生成固定长度的向量,为每个术语生成索引。然后,使用哈希表的索引生成术语频率。
在 Spark 中,HashingTF 使用MurmurHash3算法来对术语进行哈希处理。
为了使用HashingTF
,您需要导入以下包:
import org.apache.spark.ml.feature.HashingTF
首先,您需要初始化一个HashingTF
,指定输入列和输出列。在这里,我们选择由StopWordsRemover
转换器创建的过滤词列,并生成输出列rawFeaturesDF
。我们还选择将特征数量设置为 100:
scala> val hashingTF = new HashingTF().setInputCol("filteredWords").setOutputCol("rawFeatures").setNumFeatures(100)
hashingTF: org.apache.spark.ml.feature.HashingTF = hashingTF_b05954cb9375
接下来,在输入数据集上调用transform()
函数会生成输出数据集:
scala> val rawFeaturesDF = hashingTF.transform(noStopWordsDF)
rawFeaturesDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 3 more fields]
以下是输出数据集,显示了输入列 ID、句子和输出列rawFeaturesDF
,其中包含由向量表示的特征:
scala> rawFeaturesDF.show(false)
|id |sentence |words |filteredWords |rawFeatures |
|1 |Hello there, how do you like the book so far? |[hello, there,, how, do, you, like, the, book, so, far?] |[hello, there,, like, book, far?] |(100,[30,48,70,93],[2.0,1.0,1.0,1.0]) |
|2 |I am new to Machine Learning |[i, am, new, to, machine, learning] |[new, machine, learning] |(100,[25,52,72],[1.0,1.0,1.0]) |
|3 |Maybe i should get some coffee before starting |[maybe, i, should, get, some, coffee, before, starting] |[maybe, get, coffee, starting] |(100,[16,51,59,99],[1.0,1.0,1.0,1.0]) |
|4 |Coffee is best when you drink it hot |[coffee, is, best, when, you, drink, it, hot] |[coffee, best, drink, hot] |(100,[31,51,63,72],[1.0,1.0,1.0,1.0]) |
|5 |Book stores have coffee too so i should go to a book store|[book, stores, have, coffee, too, so, i, should, go, to, a, book, store]|[book, stores, coffee, go, book, store]|(100,[43,48,51,77,93],[1.0,1.0,1.0,1.0,2.0])|
让我们看一下前面的输出,以便更好地理解。如果仅查看filteredWords
和rawFeatures
列,您会看到,
-
词汇数组
[hello, there, like, book, and far]
被转换为原始特征向量(100,[30,48,70,93],[2.0,1.0,1.0,1.0])
。 -
词汇数组
(book, stores, coffee, go, book, and store)
被转换为原始特征向量(100,[43,48,51,77,93],[1.0,1.0,1.0,1.0,2.0])
。
那么,这里的向量表示什么呢?其基本逻辑是,每个单词被哈希为一个整数,并计算在单词数组中出现的次数。
Spark 内部使用一个hashMap
(mutable.HashMap.empty[Int, Double]
),用于存储每个单词的哈希值,其中Integer
键表示哈希值,Double
值表示出现次数。使用 Double 类型是为了能够与 IDF 一起使用(我们将在下一节讨论)。使用这个映射,数组[book, stores, coffee, go, book, store]
可以看作[hashFunc(book), hashFunc(stores), hashFunc(coffee), hashFunc(go), hashFunc(book), hashFunc(store)]
, 其等于[43,48,51,77,93]
。 然后,如果你也统计出现次数的话,即:book-2, coffee-1, go-1, store-1, stores-1
。
结合前面的信息,我们可以生成一个向量(numFeatures, hashValues, Frequencies)
, 在这种情况下,它将是(100,[43,48,51,77,93],[1.0,1.0,1.0,1.0,2.0])
。
逆文档频率(IDF)
逆文档频率(IDF)是一种估算器,它应用于数据集并通过缩放输入特征生成特征。因此,IDF 作用于 HashingTF 转换器的输出。
为了调用 IDF,您需要导入该包:
import org.apache.spark.ml.feature.IDF
首先,您需要初始化一个IDF
,并指定输入列和输出列。这里,我们选择由 HashingTF 创建的单词列rawFeatures
,并生成一个输出列特征:
scala> val idf = new IDF().setInputCol("rawFeatures").setOutputCol("features")
idf: org.apache.spark.ml.feature.IDF = idf_d8f9ab7e398e
接下来,在输入数据集上调用fit()
函数会生成一个输出转换器(Transformer):
scala> val idfModel = idf.fit(rawFeaturesDF)
idfModel: org.apache.spark.ml.feature.IDFModel = idf_d8f9ab7e398e
此外,在输入数据集上调用transform()
函数会生成一个输出数据集:
scala> val featuresDF = idfModel.transform(rawFeaturesDF)
featuresDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 4 more fields]
以下是输出数据集,显示了输入列 ID 和输出列特征,其中包含前述转换中由 HashingTF 生成的缩放特征向量:
scala> featuresDF.select("id", "features").show(5, false)
|id|features |
|1 |(20,[8,10,13],[0.6931471805599453,3.295836866004329,0.6931471805599453]) |
|2 |(20,[5,12],[1.0986122886681098,1.3862943611198906]) |
|3 |(20,[11,16,19],[0.4054651081081644,1.0986122886681098,2.1972245773362196]) |
|4 |(20,[3,11,12],[0.6931471805599453,0.8109302162163288,0.6931471805599453]) |
|5 |(20,[3,8,11,13,17],[0.6931471805599453,0.6931471805599453,0.4054651081081644,1.3862943611198906,1.0986122886681098])|
以下是输出数据集,显示了输入列 ID、句子、rawFeatures
和输出列特征,其中包含前述转换中由 HashingTF 生成的缩放特征向量:
scala> featuresDF.show(false)
|id|sentence |words |filteredWords |rawFeatures |features |
|1 |Hello there, how do you like the book so far? |[hello, there,, how, do, you, like, the, book, so, far?] |[hello, there,, like, book, far?] |(20,[8,10,13],[1.0,3.0,1.0]) |(20,[8,10,13],[0.6931471805599453,3.295836866004329,0.6931471805599453]) |
|2 |I am new to Machine Learning |[i, am, new, to, machine, learning] |[new, machine, learning] |(20,[5,12],[1.0,2.0]) |(20,[5,12],[1.0986122886681098,1.3862943611198906]) |
|3 |Maybe i should get some coffee before starting |[maybe, i, should, get, some, coffee, before, starting] |[maybe, get, coffee, starting] |(20,[11,16,19],[1.0,1.0,2.0]) |(20,[11,16,19],[0.4054651081081644,1.0986122886681098,2.1972245773362196]) |
|4 |Coffee is best when you drink it hot |[coffee, is, best, when, you, drink, it, hot] |[coffee, best, drink, hot] |(20,[3,11,12],[1.0,2.0,1.0]) |(20,[3,11,12],[0.6931471805599453,0.8109302162163288,0.6931471805599453]) |
|5 |Book stores have coffee too so i should go to a book store|[book, stores, have, coffee, too, so, i, should, go, to, a, book, store]|[book, stores, coffee, go, book, store]|(20,[3,8,11,13,17],[1.0,1.0,1.0,2.0,1.0])|(20,[3,8,11,13,17],[0.6931471805599453,0.6931471805599453,0.4054651081081644,1.3862943611198906,1.0986122886681098])|
TF-IDF 的示意图如下,展示了TF-IDF 特征的生成过程:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00089.jpeg
Word2Vec
Word2Vec 是一个复杂的神经网络风格的自然语言处理工具,使用一种称为跳字模型(skip-grams)的方法,将一串单词转换为嵌入式向量表示。我们来看一个关于动物的句子集合,看看如何使用这种技术:
-
一只狗在叫
-
一些牛在吃草
-
狗通常会随便叫
-
那头牛喜欢吃草
使用带有隐藏层的神经网络(这种机器学习算法在许多无监督学习应用中被使用),我们可以学习到(通过足够的示例)dog和barking是相关的,cow和grass是相关的,因为它们经常出现在彼此附近,这种关系通过概率来衡量。Word2vec
的输出是一个Double
特征的向量。
为了调用Word2vec
,您需要导入该包:
import org.apache.spark.ml.feature.Word2Vec
首先,你需要初始化一个Word2vec
转换器,指定输入列和输出列。这里,我们选择由Tokenizer
创建的单词列,并生成一个大小为 3 的单词向量输出列:
scala> val word2Vec = new Word2Vec().setInputCol("words").setOutputCol("wordvector").setVectorSize(3).setMinCount(0)
word2Vec: org.apache.spark.ml.feature.Word2Vec = w2v_fe9d488fdb69
接下来,对输入数据集调用fit()
函数会生成一个输出转换器:
scala> val word2VecModel = word2Vec.fit(noStopWordsDF)
word2VecModel: org.apache.spark.ml.feature.Word2VecModel = w2v_fe9d488fdb69
此外,对输入数据集调用transform()
函数会生成一个输出数据集:
scala> val word2VecDF = word2VecModel.transform(noStopWordsDF)
word2VecDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 3 more fields]
以下是输出数据集,显示了输入列 ID、句子以及输出列wordvector
:
scala> word2VecDF.show(false)
|id|sentence |words |filteredWords |wordvector |
|1 |Hello there, how do you like the book so far? |[hello, there,, how, do, you, like, the, book, so, far?] |[hello, there,, like, book, far?] |[0.006875938177108765,-0.00819675214588642,0.0040686681866645815]|
|2 |I am new to Machine Learning |[i, am, new, to, machine, learning] |[new, machine, learning] |[0.026012470324834187,0.023195965060343344,-0.10863214979569116] |
|3 |Maybe i should get some coffee before starting |[maybe, i, should, get, some, coffee, before, starting] |[maybe, get, coffee, starting] |[-0.004304863978177309,-0.004591284319758415,0.02117823390290141]|
|4 |Coffee is best when you drink it hot |[coffee, is, best, when, you, drink, it, hot] |[coffee, best, drink, hot] |[0.054064739029854536,-0.003801364451646805,0.06522738828789443] |
|5 |Book stores have coffee too so i should go to a book store|[book, stores, have, coffee, too, so, i, should, go, to, a, book, store]|[book, stores, coffee, go, book, store]|[-0.05887459063281615,-0.07891856770341595,0.07510609552264214] |
Word2Vec 特征的示意图如下,展示了单词如何被转换为向量:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00347.jpeg
CountVectorizer
CountVectorizer
用于将一组文本文档转换为标记计数的向量,实质上为文档生成稀疏表示,覆盖词汇表。最终结果是一个特征向量,可以传递给其他算法。稍后,我们将看到如何在 LDA 算法中使用CountVectorizer
的输出进行主题检测。
为了调用CountVectorizer
,你需要导入相关的包:
import org.apache.spark.ml.feature.CountVectorizer
首先,你需要初始化一个CountVectorizer
转换器,指定输入列和输出列。这里,我们选择由StopWordRemover
创建的filteredWords
列,并生成输出列特征:
scala> val countVectorizer = new CountVectorizer().setInputCol("filteredWords").setOutputCol("features")
countVectorizer: org.apache.spark.ml.feature.CountVectorizer = cntVec_555716178088
接下来,对输入数据集调用fit()
函数会生成一个输出转换器:
scala> val countVectorizerModel = countVectorizer.fit(noStopWordsDF)
countVectorizerModel: org.apache.spark.ml.feature.CountVectorizerModel = cntVec_555716178088
此外,对输入数据集调用transform()
函数会生成一个输出数据集。
scala> val countVectorizerDF = countVectorizerModel.transform(noStopWordsDF)
countVectorizerDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 3 more fields]
以下是输出数据集,显示了输入列 ID、句子以及输出列特征:
scala> countVectorizerDF.show(false)
|id |sentence |words |filteredWords |features |
|1 |Hello there, how do you like the book so far? |[hello, there,, how, do, you, like, the, book, so, far?] |[hello, there,, like, book, far?] |(18,[1,4,5,13,15],[1.0,1.0,1.0,1.0,1.0])|
|2 |I am new to Machine Learning |[i, am, new, to, machine, learning] |[new, machine, learning] |(18,[6,7,16],[1.0,1.0,1.0]) |
|3 |Maybe i should get some coffee before starting |[maybe, i, should, get, some, coffee, before, starting] |[maybe, get, coffee, starting] |(18,[0,8,9,14],[1.0,1.0,1.0,1.0]) |
|4 |Coffee is best when you drink it hot |[coffee, is, best, when, you, drink, it, hot] |[coffee, best, drink, hot] |(18,[0,3,10,12],[1.0,1.0,1.0,1.0]) |
|5 |Book stores have coffee too so i should go to a book store|[book, stores, have, coffee, too, so, i, should, go, to, a, book, store]|[book, stores, coffee, go, book, store]|(18,[0,1,2,11,17],[1.0,2.0,1.0,1.0,1.0])|
CountVectorizer
的示意图如下,展示了从StopWordsRemover
转换中生成的特征:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00205.jpeg
使用 LDA 进行主题建模
LDA 是一种主题模型,它从一组文本文档中推断出主题。LDA 可以被看作是一种无监督聚类算法,如下所示:
-
主题对应聚类中心,文档对应数据集中的行
-
主题和文档都存在于特征空间中,特征向量是词计数向量
-
LDA 不是通过传统的距离估计聚类,而是使用基于文本文档生成统计模型的函数
为了调用 LDA,你需要导入相关的包:
import org.apache.spark.ml.clustering.LDA
步骤 1. 首先,你需要初始化一个 LDA 模型,设置 10 个主题和 10 次聚类迭代:
scala> val lda = new LDA().setK(10).setMaxIter(10)
lda: org.apache.spark.ml.clustering.LDA = lda_18f248b08480
步骤 2. 接下来,对输入数据集调用fit()
函数会生成一个输出转换器:
scala> val ldaModel = lda.fit(countVectorizerDF)
ldaModel: org.apache.spark.ml.clustering.LDAModel = lda_18f248b08480
步骤 3. 提取logLikelihood
,它计算在推断的主题下提供的文档的下界:
scala> val ll = ldaModel.logLikelihood(countVectorizerDF)
ll: Double = -275.3298948279124
步骤 4. 提取logPerplexity
,它计算在推断的主题下提供的文档的困惑度上界:
scala> val lp = ldaModel.logPerplexity(countVectorizerDF)
lp: Double = 12.512670220189033
步骤 5. 现在,我们可以使用describeTopics()
来获取 LDA 生成的主题:
scala> val topics = ldaModel.describeTopics(10)
topics: org.apache.spark.sql.DataFrame = [topic: int, termIndices: array<int> ... 1 more field]
第 6 步。 以下是输出数据集,展示了 LDA 模型计算出的 topic
、termIndices
和 termWeights
:
scala> topics.show(10, false)
|topic|termIndices |termWeights |
|0 |[2, 5, 7, 12, 17, 9, 13, 16, 4, 11] |[0.06403877783050851, 0.0638177222807826, 0.06296749987731722, 0.06129482302538905, 0.05906095287220612, 0.0583855194291998, 0.05794181263149175, 0.057342702589298085, 0.05638654243412251, 0.05601913313272188] |
|1 |[15, 5, 13, 8, 1, 6, 9, 16, 2, 14] |[0.06889315890755099, 0.06415969116685549, 0.058990446579892136, 0.05840283223031986, 0.05676844625413551, 0.0566842803396241, 0.05633554021408156, 0.05580861561950114, 0.055116582320533423, 0.05471754535803045] |
|2 |[17, 14, 1, 5, 12, 2, 4, 8, 11, 16] |[0.06230542516700517, 0.06207673834677118, 0.06089143673912089, 0.060721809302399316, 0.06020894045877178, 0.05953822260375286, 0.05897033457363252, 0.057504989644756616, 0.05586725037894327, 0.05562088924566989] |
|3 |[15, 2, 11, 16, 1, 7, 17, 8, 10, 3] |[0.06995373276880751, 0.06249041124300946, 0.061960612781077645, 0.05879695651399876, 0.05816564815895558, 0.05798721645705949, 0.05724374708387087, 0.056034215734402475, 0.05474217418082123, 0.05443850583761207] |
|4 |[16, 9, 5, 7, 1, 12, 14, 10, 13, 4] |[0.06739359010780331, 0.06716438619386095, 0.06391509491709904, 0.062049068666162915, 0.06050715515506004, 0.05925113958472128, 0.057946856127790804, 0.05594837087703049, 0.055000929117413805, 0.053537418286233956]|
|5 |[5, 15, 6, 17, 7, 8, 16, 11, 10, 2] |[0.061611492476326836, 0.06131944264846151, 0.06092975441932787, 0.059812552365763404, 0.05959889552537741, 0.05929123338151455, 0.05899808901872648, 0.05892061664356089, 0.05706951425713708, 0.05636134431063274] |
|6 |[15, 0, 4, 14, 2, 10, 13, 7, 6, 8] |[0.06669864676186414, 0.0613859230159798, 0.05902091745149218, 0.058507882633921676, 0.058373998449322555, 0.05740944364508325, 0.057039150886628136, 0.057021822698594314, 0.05677330199892444, 0.056741558062814376]|
|7 |[12, 9, 8, 15, 16, 4, 7, 13, 17, 10]|[0.06770789917351365, 0.06320078344027158, 0.06225712567900613, 0.058773135159638154, 0.05832535181576588, 0.057727684814461444, 0.056683575112703555, 0.05651178333610803, 0.056202395617563274, 0.05538103218174723]|
|8 |[14, 11, 10, 7, 12, 9, 13, 16, 5, 1]|[0.06757347958335463, 0.06362319365053591, 0.063359294927315, 0.06319462709331332, 0.05969320243218982, 0.058380063437908046, 0.057412693576813126, 0.056710451222381435, 0.056254581639201336, 0.054737785085167814] |
|9 |[3, 16, 5, 7, 0, 2, 10, 15, 1, 13] |[0.06603941595604573, 0.06312775362528278, 0.06248795574460503, 0.06240547032037694, 0.0613859713404773, 0.06017781222489122, 0.05945655694365531, 0.05910351349013983, 0.05751269894725456, 0.05605239791764803] |
LDA 的图示如下,展示了从 TF-IDF 特征中创建的主题:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00175.jpeg
实现文本分类
文本分类是机器学习领域中最广泛使用的范式之一,广泛应用于垃圾邮件检测、电子邮件分类等用例。就像任何其他机器学习算法一样,工作流由变换器和算法组成。在文本处理领域,预处理步骤如去除停用词、词干提取、分词、n-gram 提取、TF-IDF 特征加权等会发挥作用。一旦所需的处理完成,模型将被训练以将文档分类为两类或更多类。
二分类是将输入分类为两个输出类,如垃圾邮件/非垃圾邮件,或者某个信用卡交易是否为欺诈行为。多类分类可以生成多个输出类,如热、冷、冰冻、雨天等。还有一种称为多标签分类的技术,它可以根据汽车特征的描述生成多个标签,如速度、安全性和燃油效率。
为此,我们将使用一个包含 10k 条推文样本的数据集,并在该数据集上使用上述技术。然后,我们将对文本行进行分词,去除停用词,然后使用 CountVectorizer
构建单词(特征)向量。
接下来我们将数据划分为训练集(80%)和测试集(20%),并训练一个逻辑回归模型。最后,我们将在测试数据上评估并查看其表现如何。
工作流中的步骤如下图所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00177.jpeg
第 1 步。 加载包含 10k 条推文的输入文本数据,以及标签和 ID:
scala> val inputText = sc.textFile("Sentiment_Analysis_Dataset10k.csv")
inputText: org.apache.spark.rdd.RDD[String] = Sentiment_Analysis_Dataset10k.csv MapPartitionsRDD[1722] at textFile at <console>:77
第 2 步。 将输入行转换为数据框(DataFrame):
scala> val sentenceDF = inputText.map(x => (x.split(",")(0), x.split(",")(1), x.split(",")(2))).toDF("id", "label", "sentence")
sentenceDF: org.apache.spark.sql.DataFrame = [id: string, label: string ... 1 more field]
第 3 步。 使用带有空格分隔符的 Tokenizer
将数据转换为单词:
scala> import org.apache.spark.ml.feature.Tokenizer
import org.apache.spark.ml.feature.Tokenizerscala> val tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words")
tokenizer: org.apache.spark.ml.feature.Tokenizer = tok_ebd4c89f166escala> val wordsDF = tokenizer.transform(sentenceDF)
wordsDF: org.apache.spark.sql.DataFrame = [id: string, label: string ... 2 more fields]scala> wordsDF.show(5, true)
| id|label| sentence| words|
| 1| 0|is so sad for my ...|[is, so, sad, for...|
| 2| 0|I missed the New ...|[i, missed, the, ...|
| 3| 1| omg its already ...|[, omg, its, alre...|
| 4| 0| .. Omgaga. Im s...|[, , .., omgaga.,...|
| 5| 0|i think mi bf is ...|[i, think, mi, bf...|
第 4 步。 去除停用词并创建一个新数据框,包含过滤后的单词:
scala> import org.apache.spark.ml.feature.StopWordsRemover
import org.apache.spark.ml.feature.StopWordsRemoverscala> val remover = new StopWordsRemover().setInputCol("words").setOutputCol("filteredWords")
remover: org.apache.spark.ml.feature.StopWordsRemover = stopWords_d8dd48c9cdd0scala> val noStopWordsDF = remover.transform(wordsDF)
noStopWordsDF: org.apache.spark.sql.DataFrame = [id: string, label: string ... 3 more fields]scala> noStopWordsDF.show(5, true)
| id|label| sentence| words| filteredWords|
| 1| 0|is so sad for my ...|[is, so, sad, for...|[sad, apl, friend...|
| 2| 0|I missed the New ...|[i, missed, the, ...|[missed, new, moo...|
| 3| 1| omg its already ...|[, omg, its, alre...|[, omg, already, ...|
| 4| 0| .. Omgaga. Im s...|[, , .., omgaga.,...|[, , .., omgaga.,...|
| 5| 0|i think mi bf is ...|[i, think, mi, bf...|[think, mi, bf, c...|
第 5 步。 从过滤后的单词中创建特征向量:
scala> import org.apache.spark.ml.feature.CountVectorizer
import org.apache.spark.ml.feature.CountVectorizerscala> val countVectorizer = new CountVectorizer().setInputCol("filteredWords").setOutputCol("features")
countVectorizer: org.apache.spark.ml.feature.CountVectorizer = cntVec_fdf1512dfcbdscala> val countVectorizerModel = countVectorizer.fit(noStopWordsDF)
countVectorizerModel: org.apache.spark.ml.feature.CountVectorizerModel = cntVec_fdf1512dfcbdscala> val countVectorizerDF = countVectorizerModel.transform(noStopWordsDF)
countVectorizerDF: org.apache.spark.sql.DataFrame = [id: string, label: string ... 4 more fields]scala> countVectorizerDF.show(5,true)
| id|label| sentence| words| filteredWords| features|
| 1| 0|is so sad for my ...|[is, so, sad, for...|[sad, apl, friend...|(23481,[35,9315,2...|
| 2| 0|I missed the New ...|[i, missed, the, ...|[missed, new, moo...|(23481,[23,175,97...|
| 3| 1| omg its already ...|[, omg, its, alre...|[, omg, already, ...|(23481,[0,143,686...|
| 4| 0| .. Omgaga. Im s...|[, , .., omgaga.,...|[, , .., omgaga.,...|(23481,[0,4,13,27...|
| 5| 0|i think mi bf is ...|[i, think, mi, bf...|[think, mi, bf, c...|(23481,[0,33,731,...|
第 6 步。 创建包含标签和特征的 inputData
数据框:
scala> val inputData=countVectorizerDF.select("label", "features").withColumn("label", col("label").cast("double"))
inputData: org.apache.spark.sql.DataFrame = [label: double, features: vector]
第 7 步。 使用随机拆分将数据划分为 80% 的训练集和 20% 的测试集:
scala> val Array(trainingData, testData) = inputData.randomSplit(Array(0.8, 0.2))
trainingData: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [label: double, features: vector]
testData: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [label: double, features: vector]
第 8 步。 创建一个逻辑回归模型:
scala> import org.apache.spark.ml.classification.LogisticRegression
import org.apache.spark.ml.classification.LogisticRegressionscala> val lr = new LogisticRegression()
lr: org.apache.spark.ml.classification.LogisticRegression = logreg_a56accef5728
第 9 步。 通过拟合 trainingData
创建一个逻辑回归模型:
scala> var lrModel = lr.fit(trainingData)
lrModel: org.apache.spark.ml.classification.LogisticRegressionModel = logreg_a56accef5728scala> lrModel.coefficients
res160: org.apache.spark.ml.linalg.Vector = [7.499178040193577,8.794520490564185,4.837543313917086,-5.995818019393418,1.1754740390468577,3.2104594489397584,1.7840290776286476,-1.8391923375331787,1.3427471762591,6.963032309971087,-6.92725055841986,-10.781468845891563,3.9752.836891070557657,3.8758544006087523,-11.760894935576934,-6.252988307540...scala> lrModel.intercept
res161: Double = -5.397920610780994
第 10 步。 检查模型摘要,特别是 areaUnderROC
,一个好的模型其值应该是 > 0.90:
scala> import org.apache.spark.ml.classification.BinaryLogisticRegressionSummary
import org.apache.spark.ml.classification.BinaryLogisticRegressionSummaryscala> val summary = lrModel.summary
summary: org.apache.spark.ml.classification.LogisticRegressionTrainingSummary = org.apache.spark.ml.classification.BinaryLogisticRegressionTrainingSummary@1dce712cscala> val bSummary = summary.asInstanceOf[BinaryLogisticRegressionSummary]
bSummary: org.apache.spark.ml.classification.BinaryLogisticRegressionSummary = org.apache.spark.ml.classification.BinaryLogisticRegressionTrainingSummary@1dce712cscala> bSummary.areaUnderROC
res166: Double = 0.9999231930196596scala> bSummary.roc
res167: org.apache.spark.sql.DataFrame = [FPR: double, TPR: double]scala> bSummary.pr.show()
| recall|precision|
| 0.0| 1.0|
| 0.2306543172990738| 1.0|
| 0.2596354944726621| 1.0|
| 0.2832387212429041| 1.0|
|0.30504929787869733| 1.0|
| 0.3304451747833881| 1.0|
|0.35255452644158947| 1.0|
| 0.3740663280549746| 1.0|
| 0.3952793546459516| 1.0|
第 11 步。 使用训练好的模型转换训练集和测试集数据:
scala> val training = lrModel.transform(trainingData)
training: org.apache.spark.sql.DataFrame = [label: double, features: vector ... 3 more fields]scala> val test = lrModel.transform(testData)
test: org.apache.spark.sql.DataFrame = [label: double, features: vector ... 3 more fields]
第 12 步。 计算标签和预测列匹配的记录数。它们应该匹配,以便正确评估模型,否则会不匹配:
scala> training.filter("label == prediction").count
res162: Long = 8029scala> training.filter("label != prediction").count
res163: Long = 19scala> test.filter("label == prediction").count
res164: Long = 1334scala> test.filter("label != prediction").count
res165: Long = 617
结果可以放入如下所示的表格中:
数据集 | 总数 | 标签 == 预测 | 标签 != 预测 |
---|---|---|---|
训练 | 8048 | 8029 (99.76%) | 19 (0.24%) |
测试 | 1951 | 1334 (68.35%) | 617 (31.65%) |
虽然训练数据得到了很好的匹配,但测试数据的匹配率只有 68.35%。因此,仍有改进的空间,可以通过调整模型参数来实现。
逻辑回归是一种易于理解的方法,它通过输入的线性组合和以逻辑随机变量形式存在的随机噪声来预测二元结果。因此,逻辑回归模型可以通过多个参数进行调整。(本章不涉及逻辑回归模型的所有参数及其调优方法。)
可以用来调整模型的某些参数包括:
-
模型超参数包括以下参数:
-
elasticNetParam
:该参数指定您希望如何混合 L1 和 L2 正则化。 -
regParam
:该参数决定了在传入模型之前,输入应该如何进行正则化。
-
-
训练参数包括以下参数:
-
maxIter
:这是停止前的总交互次数。 -
weightCol
:这是权重列的名称,用于对某些行进行加权,使其比其他行更重要。
-
-
预测参数包括以下参数:
threshold
:这是二元预测的概率阈值。它决定了给定类别被预测的最低概率。
我们现在已经看到了如何构建一个简单的分类模型,因此可以根据训练集为任何新的推文打标签。逻辑回归只是可以使用的模型之一。
可以替代逻辑回归使用的其他模型如下:
-
决策树
-
随机森林
-
梯度提升树
-
多层感知机
总结
在本章中,我们介绍了使用 Spark ML 进行文本分析的世界,重点讲解了文本分类。我们了解了 Transformers 和 Estimators。我们看到了如何使用 Tokenizers 将句子分解为单词,如何去除停用词,以及生成 n-grams。我们还学习了如何实现HashingTF
和IDF
来生成基于 TF-IDF 的特征。我们还看到了如何使用Word2Vec
将单词序列转换为向量。
然后,我们还查看了 LDA,一种常用的技术,用于从文档中生成主题,而无需深入了解实际文本内容。最后,我们在来自 Twitter 数据集的 1 万个推文数据集上实施了文本分类,看看如何通过使用 Transformers、Estimators 和 Logistic Regression 模型进行二元分类,将这一切结合起来。
在下一章,我们将进一步深入探讨如何调整 Spark 应用程序以获得更好的性能。