PySpark 机器学习教程(二)
六、随机森林
这一章主要讲述用 PySpark 构建随机森林(RF)进行分类。我们将了解它们的各个方面以及预测是如何发生的;但在了解更多关于随机森林的知识之前,我们必须了解 RF 的构建模块,即决策树(DT)。决策树也用于分类/回归。但是就准确性而言,由于各种原因,随机森林胜过 DT 分类器,我们将在本章后面讨论这些原因。让我们了解更多关于决策树的知识。
决策图表
决策树属于机器学习的监督类别,使用频率表进行预测。决策树的一个优点是它可以处理分类变量和数值变量。顾名思义,它以一种树状结构运行,并根据各种分裂形成这些规则,最终做出预测。决策树中使用的算法是 J. R. Quinlan 开发的 ID3。
我们可以将决策树分解成不同的组件,如图 6-1 所示。
图 6-1
决策图表
树从中分支的最顶端的分裂节点被称为根节点;在上面的例子中,年龄是根节点。圆圈中表示的值称为叶节点或预测。让我们用一个样本数据集来理解决策树实际上是如何工作的。
表 6-1 中显示的数据包含了一些不同年龄组和属性的人的样本数据。基于这些属性要做出的最终决定是保险费是否应该偏高。这是一个典型的分类案例,我们将使用决策树对其进行分类。我们有四个输入栏(年龄组、吸烟者、医疗状况、工资水平)。
表 6-1
示例数据集
|年龄层
|
吸烟者
|
医疗条件
|
薪资水平
|
保险费
| | --- | --- | --- | --- | --- | | 老的 | 是 | 是 | 高的 | 高的 | | 十几岁的青少年 | 是 | 是 | 中等 | 高的 | | 年纪轻的 | 是 | 是 | 中等 | 低的 | | 老的 | 不 | 是 | 高的 | 高的 | | 年纪轻的 | 是 | 是 | 高的 | 低的 | | 十几岁的青少年 | 不 | 是 | 低的 | 高的 | | 十几岁的青少年 | 不 | 不 | 低的 | 低的 | | 老的 | 不 | 不 | 低的 | 高的 | | 十几岁的青少年 | 不 | 是 | 中等 | 高的 | | 年纪轻的 | 不 | 是 | 低的 | 高的 | | 年纪轻的 | 是 | 不 | 高的 | 低的 | | 十几岁的青少年 | 是 | 不 | 中等 | 低的 | | 年纪轻的 | 不 | 不 | 中等 | 高的 | | 老的 | 是 | 不 | 中等 | 高的 |
熵
决策树生成这些数据的子集,每个子集包含相同的类值(同质);为了计算同质性,我们使用熵。这也可以使用基尼指数和分类误差等其他指标来计算,但我们将使用熵来理解决策树是如何工作的。计算熵的公式是
-p log2p-q log2q
图 6-2 表明如果子集完全纯,熵等于零;这意味着它只属于一个类,如果子集被平均分成两个类,则它等于 1
图 6-2
熵
如果我们想计算我们的目标变量(保险费)的熵,我们首先要计算每一类的概率,然后用上面的公式计算熵。
|保险费
| | --- | | 高(9) | 低(5) |
高类别的概率等于 9/14 =0.64
低类别的概率等于 5/14 =0.36
熵=p(高)log2(p(高)p(低)log2(p(低))
=——0.64′??【日志】(0.64))——0.36′??【日志】(0.36))
= 0.94
为了构建决策树,我们需要计算两种熵:
-
目标熵(保险费)
-
具有属性的目标的熵(例如。保险费-吸烟者)
我们已经看到了目标的熵,所以让我们计算具有输入特征的目标的第二个熵。例如,让我们考虑吸烟者的特征。
熵计算
目标-保险费
特征-吸烟者
|
保险费(目标)
| | --- | --- | |
高(9)
|
低(5)
| | --- | --- | | 吸烟者(特写) | 是(7) | three | four | | 第七项 | six | one |
P是 == 0.5
Pno== 0.5
= 0.99
= 0.59
=0.79
类似地,我们计算所有其他属性的熵:
熵 ( 目标,年龄组 ) = 0.69
熵 ( 目标,医疗状况 ) = 0.89
熵 ( 目标,薪资水平 ) = 0.91
信息增益
信息增益(IG)用于在决策树中进行分裂。提供最大信息增益的属性用于分割子集。信息增益告诉我们,就预测而言,哪一个特征是最重要的。从熵的角度来说,IG 是目标在分割前和分割后的熵的变化。
= 0.94 − 0.79
= 0.15
= 0.94 – 0.69
=0.25
= 0.94 – 0.89
=0.05
= 0.94 – 0.91
=0.03
我们可以观察到,年龄组属性给出了最大的信息增益;因此,决策树的根节点应该是年龄组,第一次拆分发生在该属性上,如图 6-3 所示。
图 6-3
决策树分裂
寻找提供最大信息增益的下一个属性的过程递归地继续,并且在决策树中进行进一步的分裂。最后,决策树可能看起来如图 6-4 所示。
图 6-4
决策树拆分
决策树提供的优点是,它可以通过跟踪根节点到任何叶节点而容易地转换成一组规则,因此可以容易地用于分类。存在与决策树相关联的超参数集,这些超参数集提供了以不同方式构建树的更多选项。其中之一是最大深度,它允许我们决定决策树的深度;树越深,树的裂缝就越多,有可能过度拟合。
随机森林
现在我们知道了决策树是如何工作的,我们可以继续学习随机森林。顾名思义,随机森林是由很多树组成的:很多决策树。它们非常受欢迎,有时是监督机器学习的首选方法。随机森林也可以用于分类和回归。他们将来自许多单独决策树的投票组合起来,然后用多数票预测该类,或者在回归的情况下取平均值。这真的很有效,因为弱学习者最终会聚集在一起做出强预测。重要性在于这些决策树的形成方式。“随机”这个名称在 RF 中是有原因的,因为这些树是由一组随机的特征和一组随机的训练样本组成的。现在,每个决策树都使用一组略有不同的数据点进行训练,试图了解输入和输出之间的关系,最终与使用其他数据集进行训练的其他决策树的预测相结合,从而形成随机森林。如果我们举一个与上面相似的例子,创建一个有五棵决策树的随机森林,它可能看起来如图 6-5 所示。
图 6-5
个体决策树
现在,这些决策树中的每一个都使用了数据子集和特征子集来进行训练。这也称为“打包”技术,即引导聚合。每棵树都对预测进行投票排序,投票数最多的类是随机森林分类器的最终预测,如图 6-6 所示。
图 6-6
随机森林
随机森林提供的一些优势如下:
-
特征重要性:随机森林可以根据预测能力给出用于训练的每个特征的重要性。这提供了一个很好的机会来选择相关的特性,去掉较弱的特性。所有特征重要性的总和总是等于 1。
-
更高的准确性:由于它从单个决策树收集投票,随机森林的预测能力比单个决策树相对更高。
-
较少过拟合:单个分类器的结果被平均或最大化投票,因此减少了过拟合的机会。
随机森林的一个缺点是,与决策树相比,它很难可视化,并且在计算方面涉及更多,因为它构建了多个单独的分类器,
密码
本章的这一节重点介绍如何使用 PySpark 和 Jupyter Notebook 从头构建一个随机森林分类器。
注意
这本书的 GitHub repo 上提供了完整的数据集和代码,在 Spark 2.0 和更高版本上执行得最好。
让我们使用 Spark 的 MLlib 库构建一个随机森林模型,并使用输入特性预测目标变量。
数据信息
我们将在这个例子中使用的数据集是一个有几千行六列的开源数据集。我们必须使用五个输入变量来预测使用随机森林模型的目标变量。
步骤 1:创建 SparkSession 对象
我们启动 Jupyter 笔记本并导入 SparkSession,然后创建一个新的 SparkSession 对象来使用 Spark。
[In]: from pyspark.sql import SparkSession
[In]: spark=SparkSession.builder.appName('random_forest').getOrCreate()
步骤 2:读取数据集
然后,我们使用 Dataframe 在 Spark 中加载和读取数据集。我们必须确保我们已经从数据集可用的同一个目录文件夹中打开了 PySpark,否则我们必须提到数据文件夹的目录路径。
[In]: df=spark.read.csv('affairs.csv',inferSchema=True,header=True)
步骤 3:探索性数据分析
在本节中,我们将通过查看数据集并验证数据集的形状和变量的各种统计度量来更深入地研究数据集。我们从检查数据集的形状开始。
[In]: print((df.count(), len(df.columns)))
[Out]: (6366, 6)
因此,上面的输出确认了数据集的大小,然后我们可以验证输入值的数据类型,以检查我们是否需要更改/转换任何列的数据类型。
[In]: df.printSchema()
[Out]: root
|-- rate_marriage: integer (nullable = true)
|-- age: double (nullable = true)
|-- yrs_married: double (nullable = true)
|-- children: double (nullable = true)
|-- religious: integer (nullable = true)
|-- affairs: integer (nullable = true)
正如我们所看到的,没有需要转换成数字形式的分类列。让我们用 Spark 中的 show 函数来看看数据集:
[In]: df.show(5)
[Out]:
我们现在可以使用 describe 函数来检查数据集的统计度量。
[In]: df.describe().select('summary','rate_marriage','age','yrs_married','children','religious').show()
[Out]:
我们可以观察到,人的平均年龄接近 29 岁,结婚 9 年。
让我们研究各个列,以便更深入地了解数据的细节。与 counts 一起使用的 groupBy 函数返回数据中每个类别的出现频率。
[In]: df.groupBy('affairs').count().show()
[Out]:
所以,在总人口中,有超过 33%的人有婚外情。
[In]: df.groupBy('rate_marriage').count().show()
[Out]:
大多数人对他们的婚姻评价很高(4 或 5),其余的人评价较低。让我们再深入一点,了解婚姻评级是否与婚外情变量有关。
[In]: df.groupBy('rate_marriage','affairs').count().orderBy('rate_marriage','affairs','count',ascending=True).show()
[Out]:
显然,这些数据表明,对婚姻评价低的人有外遇的比例很高。这可能被证明是预测的一个有用特征。我们将以类似的方式探索其他变量。
[In]: df.groupBy('religious','affairs').count().orderBy('religious','affairs','count',ascending=True).show()
[Out]:
从宗教角度的评分以及对宗教特征评分较低和对婚外情评分较高的人数来看,我们也有类似的情况。
[In]: df.groupBy('children','affairs').count().orderBy('children','affairs','count',ascending=True).show()
[Out]:
上表没有清楚地表明子女数量和卷入婚外情机会之间关系的任何趋势。让我们使用 groupBy 函数和 mean 来了解数据集的更多信息。
[In]: df.groupBy('affairs').mean().show()
[Out]:
所以,从年龄的角度来看,有婚外情的人对他们的婚姻评价很低,甚至有点偏高。他们结婚的时间也更长,也更不信教。
步骤 4:特征工程
这是我们使用 Spark 的 VectorAssembler 创建一个组合所有输入特征的单一向量的部分。
[In]: from pyspark.ml.feature import VectorAssembler
我们需要将所有的输入列组合成一个向量,作为模型的输入特征。因此,我们选择需要用来创建单个特征向量的输入列,并将输出向量命名为 features。
[In]: df_assembler = VectorAssembler(inputCols=['rate_marriage', 'age', 'yrs_married', 'children', 'religious'], outputCol="features")
[In}:df = df_assembler.transform(df)
[In]: df.printSchema()
[Out]:
root
|-- rate_marriage: integer (nullable = true)
|-- age: double (nullable = true)
|-- yrs_married: double (nullable = true)
|-- children: double (nullable = true)
|-- religious: integer (nullable = true)
|-- affairs: integer (nullable = true)
|-- features: vector (nullable = true)
正如我们所看到的,现在我们有了一个名为 features 的额外列,它是所有输入要素的组合,表示为一个密集矢量。
[In]: df.select(['features','affairs']).show(10,False)
[Out]:
让我们只选择 features 列作为输入,事务列作为输出来训练随机森林模型。
[In]: model_df=df.select(['features','affairs'])
步骤 5:拆分数据集
为了训练和评估随机森林模型的性能,我们必须将数据集分为训练和测试数据集。我们将其分成 75/25 的比例,并在数据集的 75%上训练我们的模型。我们可以打印火车的形状和测试数据来验证大小。
[In]: train_df,test_df=model_df.randomSplit([0.75,0.25])
[In]: print(train_df.count())
[Out]: 4775
[In]: train_df.groupBy('affairs').count().show()
[Out]:
+-------+-----+
|affairs|count|
+-------+-----+
| 1| 1560|
| 0| 3215|
+-------+-----+
这确保我们将目标类(“事务”)的集合值平衡到训练集和测试集中。
[In]: test_df.groupBy('affairs').count().show()
[Out]:
+-------+-----+
|affairs|count|
+-------+-----+
| 1| 493|
| 0| 1098|
+-------+-----+
步骤 6:建立和训练随机森林模型
在这一部分中,我们使用输入和状态等特征作为输出列来构建和训练随机森林模型。
[In]: from pyspark.ml.classification import RandomForestClassifier
[In]: rf_classifier=RandomForestClassifier(labelCol='affairs',numTrees=50).fit(train_df)
有许多超参数可以设置来调整模型的性能,但我们在这里选择的是默认参数,除了我们想要构建的决策树的数量。
步骤 7:测试数据的评估
一旦我们在训练数据集上训练了我们的模型,我们就可以在测试集上评估它的性能。
[In]: rf_predictions=rf_classifier.transform(test_df)
[In]: rf_predictions.show()
[Out]:
预测表中的第一列是测试数据的输入要素。第二列是测试数据的实际标签或输出。第三列(rawPrediction)表示两种可能输出的置信度。第四列是每个类别标签的条件概率,最后一列是随机森林分类器的预测。我们可以对预测列应用一个 groupBy 函数,以找出对正类和负类的预测数量。
[In]: rf_predictions.groupBy('prediction').count().show()
[Out]:
+----------+-----+
|prediction|count|
+----------+-----+
| 0.0| 1257|
| 1.0| 334|
+----------+-----+
为了评估这些预测,我们将导入分类评估器。
[In]: from pyspark.ml.evaluation import MulticlassClassificationEvaluator
[In]: from pyspark.ml.evaluation import BinaryClassificationEvaluator
准确
[In]: rf_accuracy=MulticlassClassificationEvaluator(labelCol='affairs',metricName='accuracy').evaluate(rf_predictions)
[In]: print('The accuracy of RF on test data is {0:.0%}'.format(rf_accuracy))
[Out]: The accuracy of RF on test data is 73%
精确
[In]: rf_precision=MulticlassClassificationEvaluator(labelCol='affairs',metricName='weightedPrecision').evaluate(rf_predictions)
[In]: print('The precision rate on test data is {0:.0%}'.format(rf_precision))
[Out]: The precision rate on test data is 71%
罗马纪元
[In]: rf_auc=BinaryClassificationEvaluator(labelCol='affairs').evaluate(rf_predictions)
[In]: print( rf_auc)
[Out]: 0.738
如前所述,RF 给出了每个特征在预测能力方面的重要性,这对于找出对预测贡献最大的关键变量非常有用。
[In]: rf_classifier.featureImportances
[Out]: (5,[0,1,2,3,4],[0.563965247822,0.0367408623003,0.243756511958,0.0657893200779,0.0897480578415])
我们使用了五个特征,并且可以使用特征重要性函数来找出重要性。要了解哪个输入要素映射到哪个索引值,我们可以使用元数据信息。
[In]: df.schema["features"].metadata["ml_attr"]["attrs"]
[Out]:
{'idx': 0, 'name': 'rate_marriage'},
{'idx': 1, 'name': 'age'},
{'idx': 2, 'name': 'yrs_married'},
{'idx': 3, 'name': 'children'},
{'idx': 4, 'name': 'religious'}}
因此,从预测的角度来看,结婚率是最重要的特征,其次是结婚年龄。最不重要的变量似乎是年龄。
步骤 8:保存模型
有时,在训练完模型后,我们只需要调用模型进行预测,因此保留模型对象并重用它进行预测是非常有意义的。这包括两个部分。
-
保存 ML 模型
-
加载 ML 模型
[In]: from pyspark.ml.classification import RandomForestClassificationModel
[In]: rf_classifier.save("/home/jovyan/work/RF_model")
This way we saved the model as object locally.The next step is to load the model again for predictions
[In]: rf=RandomForestClassificationModel.load("/home/jovyan/work/RF_model")
[In]: new_preditions=rf.transform(new_df)
新的预测表将包含具有模型预测的列
结论
在本章中,我们回顾了理解随机森林的构建块并在 PySpark 中创建 ML 模型以进行分类以及评估指标(如准确度、精密度和 auc)的过程。我们还讲述了如何在本地保存 ML 模型对象并在预测中重用它。
七、推荐系统
在实体店中可以观察到的一个常见趋势是,我们在购物时有销售人员指导和推荐我们相关的产品。另一方面,在在线零售平台上,有无数不同的产品可供选择,我们必须自己导航才能找到合适的产品。现在的情况是,用户有太多的选项和选择,但是他们不喜欢花太多的时间浏览整个目录。因此,推荐系统(RS)的作用对于推荐相关项目和推动客户转化变得至关重要。
传统实体店使用货架图来排列商品,这样可以增加高销量商品的可见性并增加收入,而在线零售店需要根据每个客户的偏好来保持动态,而不是对每个人都保持相同。
推荐系统主要用于以个性化的方式向正确的用户自动推荐正确的内容或产品,以增强整体体验。推荐系统在使用大量数据和学习理解特定用户的偏好方面非常强大。推荐可以帮助用户轻松浏览数以百万计的产品或大量内容(文章/视频/电影),并向他们展示他们可能喜欢或购买的正确商品/信息。因此,简单地说,RS 代表用户帮助发现信息。现在,这取决于用户来决定 rs 是否在推荐方面做得很好,他们可以选择选择产品/内容或放弃并继续前进。用户的每一个决定(积极的或消极的)都有助于根据最新数据重新训练 rs,以便能够给出更好的建议。在这一章中,我们将回顾 RS 是如何工作的,以及提出这些建议时所使用的不同类型的技术。我们还将使用 PySpark 构建一个推荐系统。
推荐
在向用户推荐各种事物的意义上,推荐系统可以用于多种目的。例如,其中一些可能属于以下类别:
-
零售产品
-
乔布斯
-
关系/朋友
-
电影/音乐/视频/书籍/文章
-
广告(ad 的复数)
“推荐什么”部分完全取决于使用 RS 的环境,可以通过提供用户最有可能购买的商品或通过在适当的时间展示相关内容来增加参与度,从而帮助企业增加收入。RS 关注的关键方面是,被推荐的产品或内容应该是用户可能喜欢但自己不会发现的东西。除此之外,RS 还需要各种各样的推荐元素来保持足够的趣味性。一些当今企业大量使用 RS 的例子,如亚马逊产品、脸书的朋友建议、LinkedIn 的“你可能认识的人”、网飞的电影、YouTube 的视频、Spotify 的音乐和 Coursera 的课程。
从商业角度来看,这些建议的影响被证明是巨大的,因此花费了更多的时间来使这些规则更加有效和相关。RS 在零售环境中提供的一些直接优势包括:
-
收入增加
-
用户的积极评价和评级
-
参与度提高
对于其他垂直行业,如广告推荐和其他内容推荐,RS 非常有助于帮助他们找到适合用户的东西,从而增加采用率和订阅量。如果没有 RS,以个性化方式向数百万用户推荐在线内容或向每个用户提供通用内容可能会令人难以置信地偏离目标,并对用户产生负面影响。
现在我们知道了 RS 的用法和特性,我们可以看看不同类型的 RS。可以构建的 RS 主要有五种类型:
-
基于流行度的 RS
-
基于内容的遥感
-
基于协同过滤的粗糙集
-
混合遥感
-
基于粗糙集的关联规则挖掘
除了最后一项,即基于 RS 的关联规则挖掘,我们将简要介绍每一项,因为它超出了本书的范围。
基于流行度的 RS
这是可以用来向用户推荐产品或内容的最基本和最简单的 RS。它根据大多数用户的购买/观看/喜欢/下载来推荐项目/内容。虽然它实现起来简单易行,但它不会产生相关的结果,因为对每个用户的推荐都是一样的,但它有时会优于一些更复杂的 RS。RS 的实现方式是简单地根据各种参数对项目进行排名,并推荐列表中排名最高的项目。如前所述,项目或内容可以按以下方式排序:
-
下载次数
-
购买次数
-
浏览次数
-
最高评级
-
共享次数
-
喜欢的次数
这种 RS 直接向客户推荐最畅销或最受关注/购买的商品,从而增加客户转化的机会。这种 RS 的局限性在于它不是超个性化的。
基于内容的遥感
这种类型的 RS 向用户推荐用户过去喜欢过的类似项目。因此,整个想法是计算任意两个项目之间的相似性得分,并基于用户的兴趣简档推荐给用户。我们首先为每个项目创建项目配置文件。现在,可以通过多种方式创建这些项目配置文件,但最常用的方法是包含关于项目的详细信息或属性的信息。例如,电影的项目简档可以具有各种属性的值,例如恐怖、艺术、喜剧、动作、戏剧和商业,如下所示。
|电影 ID
|
恐怖
|
艺术
|
喜剧
|
行动
|
戏剧
|
商业
| | --- | --- | --- | --- | --- | --- | --- | | Two thousand three hundred and ten | Zero point zero one | Zero point three | Zero point eight | Zero | Zero point five | Zero point nine |
上面是一个项目配置文件的例子,每个项目都有一个相似的向量来表示它的属性。现在,让我们假设用户已经观看了 10 部这样的电影,并且非常喜欢它们。因此,对于该特定用户,我们最终得到了表 7-1 中所示的项目矩阵。
表 7-1。
电影数据
|电影 ID
|
恐怖
|
艺术
|
喜剧
|
行动
|
戏剧
|
商业
| | --- | --- | --- | --- | --- | --- | --- | | Two thousand three hundred and ten | Zero point zero one | Zero point three | Zero point eight | Zero | Zero point five | Zero point nine | | Two thousand six hundred and thirty-one | Zero | Zero point four five | Zero point eight | Zero | Zero point five | Zero point six five | | Two thousand four hundred and forty-four | Zero point two | Zero | Zero point eight | Zero | Zero point five | Zero point seven | | Two thousand nine hundred and seventy-four | Zero point six | Zero point three | Zero | Zero point six | Zero point five | Zero point three | | Two thousand one hundred and fifty-one | Zero point nine | Zero point two | Zero | Zero point seven | Zero point five | Zero point nine | | Two thousand eight hundred and seventy-six | Zero | Zero point three | Zero point eight | Zero | Zero point five | Zero point nine | | Two thousand three hundred and forty-five | Zero | Zero point three | Zero point eight | Zero | Zero point five | Zero point nine | | Two thousand three hundred and nine | Zero point seven | Zero | Zero | Zero point eight | Zero point four | Zero point five | | Two thousand three hundred and sixty-six | Zero point one | Zero point one five | Zero point eight | Zero | Zero point five | Zero point six | | Two thousand three hundred and eighty-eight | Zero | Zero point three | Zero point eight five | Zero | Zero point eight | Zero point nine |
用户概要
基于内容的 RC 中的另一个组件是用户简档,它是使用用户喜欢或评价的项目简档创建的。假设用户喜欢表 7-1 中的电影,用户简档可能看起来像单个向量,它只是项目向量的平均值。用户配置文件可能如下所示。
|用户标识
|
恐怖
|
艺术
|
喜剧
|
行动
|
戏剧
|
商业
| | --- | --- | --- | --- | --- | --- | --- | | 1A92 | Zero point two five one | Zero point two three | Zero point five six five | Zero point two one | Zero point five two | Zero point seven two five |
这种创建用户简档的方法是最基本的方法之一,还有其他复杂的方法来创建更丰富的用户简档,如标准化值、加权值等。下一步是根据之前的偏好推荐用户可能喜欢的项目(电影)。因此,用户简档和项目简档之间的相似性得分被计算并相应地排序。相似度得分越多,用户喜欢该电影的几率越高。有几种方法可以计算相似性得分。
欧几里得距离
用户简档和项目简档都是高维向量,因此为了计算两者之间的相似性,我们需要计算两个向量之间的距离。使用下面的公式可以很容易地计算出 n 维向量的欧几里德距离:
距离值越高,两个向量越不相似。因此,计算用户简档和所有其他项目之间的距离,并按降序排列。以这种方式向用户推荐前几个项目。
余弦相似性
计算用户和项目简档之间的相似性得分的另一种方式是余弦相似性。它测量的不是距离,而是两个向量(用户简档向量和项目简档向量)之间的角度。两个向量之间的角度越小,它们彼此越相似。余弦相似度可以使用下面的公式计算出来:
是(x,y)=cos(θ)= xy / |x||y|
让我们来看看基于内容的 RS 的优缺点。
优点:
-
基于内容的 RC 独立于其他用户的数据工作,因此可以应用于个人的历史数据。
-
RC 背后的基本原理很容易理解,因为推荐是基于用户简档和项目简档之间的相似性得分。
-
也可以仅仅基于用户的历史兴趣和偏好向用户推荐新的和未知的项目。
缺点:
-
项目配置文件可能有偏差,可能无法反映准确的属性值,并可能导致不正确的建议。
-
推荐完全取决于用户的历史,并且只能推荐与历史观看/喜欢的项目相似的项目,而不考虑访问者的新兴趣或喜欢。
基于协同过滤的粗糙集
基于 CF 的 RS 不需要项目属性或描述来进行推荐;相反,它作用于用户项目交互。这些互动可以通过各种方式来衡量,如评级、购买的商品、花费的时间、在另一个平台上的分享等。在深入探讨 CF 之前,让我们后退一步,思考一下我们是如何在日常生活中做出某些决定的,例如以下这些决定:
-
看哪部电影
-
读哪本书
-
去哪家餐馆
-
去哪个地方旅游
我们问我们的朋友,对!我们向在某些方面与我们相似、品味和爱好与我们相同的人寻求推荐。我们的利益在某些领域是一致的,所以我们相信他们的建议。这些人可能是我们的家庭成员、朋友、同事、亲戚或社区成员。在现实生活中,很容易知道这个圈子里的人都是些什么人,但当涉及到在线推荐时,协同过滤中的关键任务是找到与你最相似的用户。每个用户可以由包含用户项目交互的反馈值的向量来表示。让我们先了解用户项目矩阵,以理解 CF 方法。
用户项目矩阵
用户项目矩阵顾名思义。在行中,我们有所有的唯一用户;沿着列,我们有所有独特的项目。这些值填充有反馈或交互分数,以突出用户对该产品的喜欢或不喜欢。一个简单的用户条目矩阵可能类似于表 7-2 所示。
表 7-2。
用户项目矩阵
|用户标识
|
项目 1
|
项目 2
|
项目 3
|
项目 4
|
项目 5
|
项目 n
| | --- | --- | --- | --- | --- | --- | --- | | 14SD | one | four | | | five | | | 26BB | | three | three | | | one | | 24DG | one | four | one | | five | Two | | 59YU | | Two | | | five | | | 21HT | three | Two | one | Two | five | | | 公元前 68 年 | | one | | | | five | | 26DF | one | four | | three | three | | | 25TR | one | four | | | five | | | 33XF | five | five | five | one | five | five | | 73QS | one | | three | | | one |
正如您所观察到的,用户项目矩阵通常非常稀疏,因为有数百万个项目,并且每个用户不会与每个项目进行交互;所以这个矩阵包含了很多空值。矩阵中的值通常是基于用户与该特定项目的交互而推导出的反馈值。在 UI 矩阵中可以考虑两种类型的反馈。
明确的反馈
这种类型的反馈通常是当用户在交互之后给项目评级并且已经体验了项目特征时。评级可以有多种类型。
-
用 1-5 级评分
-
向他人推荐的简单评分项目(是或否或从不)
-
喜欢该项目(是或否)
显式反馈数据包含非常有限的数据点,因为即使在购买或使用商品后,也只有很小比例的用户会花时间给出评级。一个完美的例子可以是一部电影,因为很少有用户在观看后给出评级。因此,仅基于显式反馈数据构建 RS 会将我们置于一个棘手的境地,尽管数据本身噪声较小,但有时不足以构建 RS。
隐性反馈
这种反馈不是直接的,主要是从用户在在线平台上的活动中推断出来的,并且基于与项目的交互。例如,如果用户已经购买了该商品,将其添加到购物车中,查看了该商品,并且花了大量时间查看关于该商品的信息,这表明用户对该商品有更高的兴趣。隐式反馈值很容易收集,并且当每个用户通过在线平台导航时,他们可以获得大量的数据点。隐式反馈面临的挑战是,它包含大量嘈杂的数据,因此不会在推荐中增加太多价值。
现在我们已经了解了 UI 矩阵和进入该矩阵的值的类型,我们可以看到不同类型的协同过滤(CF)。主要有两种 CF:
-
基于最近邻的 CF
-
基于潜在因素的 CF
基于最近邻的 CF
这种 CF 的工作方式是通过找到与活跃用户(对于我们试图推荐的用户)喜欢或不喜欢相同项目的最相似用户来找出用户的 k 个最近邻居。最近邻的协同过滤包括两个步骤。第一步是找到 k 个最近的邻居,第二步是预测活跃用户喜欢特定项目的评级或可能性。可以使用我们在本章中讨论过的一些早期技术来找出 k-最近邻。余弦相似性或欧几里德距离等指标可以帮助我们根据两组用户喜欢或不喜欢的共同项目,从用户总数中找到与活跃用户最相似的用户。也可以使用的另一个度量是 Jaccard 相似性。让我们看一个例子来理解这个指标——回到之前的用户项目矩阵,只取五个用户的数据,如表 7-3 所示。
表 7-3。
用户项目矩阵
|用户标识
|
项目 1
|
项目 2
|
项目 3
|
项目 4
|
项目 5
|
项目 n
| | --- | --- | --- | --- | --- | --- | --- | | 14SD | one | four | | | five | | | 26BB | | three | three | | | one | | 24DG | one | four | one | | five | Two | | 59YU | | Two | | | five | | | 26DF | one | four | | three | three | |
假设我们总共有五个用户,我们想找到离活动用户最近的两个邻居(14SD)。Jaccard 的相似性可以通过使用
是(x,y)= | rx’ry |/| rx’ry |
因此,这是任意两个用户共同评价的项目数除以两个用户评价的项目总数:
sim(用户 1,用户 2) = 1 / 5 = 0.2,因为他们只对项目 2 进行了共同评价)。
其余四个用户与活跃用户的相似性得分将类似于表 7-4 所示。
表 7-4。
用户相似性得分
|用户标识
|
相似性得分
| | --- | --- | | 14SD | one | | 26BB | Zero point two | | 24DG | Zero point six | | 59YU | Zero point six seven seven | | 26DF | Zero point seven five |
因此,根据 Jaccard 相似性,前两个最近的邻居是第四和第五用户。不过,这种方法有一个主要问题,因为 Jaccard 相似性在计算相似性得分时不考虑反馈值,而只考虑已评级的常见项目。因此,可能存在这样一种可能性,即用户可能对许多项目进行了共同评级,但一个人可能对它们进行了高评级,而另一个人可能对它们进行了低评级。Jaccard 相似性得分仍然可能以两个用户的高分结束,这是违反直觉的。在上面的例子中,很明显,活动用户与第三用户(24DG)最相似,因为他们对三个常见项目具有完全相同的评级,而第三用户甚至没有出现在前两个最近的邻居中。因此,我们可以选择其他度量来计算 k-最近邻。
缺少值
用户项目矩阵将包含许多缺失值,原因很简单,因为有许多项目,并且不是每个用户都与每个项目交互。有几种方法可以处理 UI 矩阵中缺失的值。
-
用 0 替换丢失的值。
-
用用户的平均评分替换缺失值。
对共同项目的评级越相似,邻居离活跃用户越近。同样,有两类基于最近邻的 CF
-
基于用户的 CF
-
基于项目的 CF
这两个 RS 之间的唯一区别是,在基于用户的 CF 中,我们找到 k 个最近的用户,而在基于项目的 CF 中,我们找到 k 个最近的项目推荐给用户。我们将看到推荐在基于用户的 RS 中是如何工作的。
顾名思义,在基于用户的 CF 中,整个思路就是找到与活跃用户最相似的用户,将相似用户已经购买/评价很高的商品推荐给活跃用户,而这些商品是他还没有看过/买过/试过的。这种 RS 的假设是,如果两个或更多的用户对一堆项目有相同的意见,那么他们很可能对其他项目也有相同的意见。让我们看一个例子来理解基于用户的协同过滤:有三个用户,我们想向其中的活跃用户推荐一个新项目。其余两个用户是与活动用户在项目的好恶方面的前两个最近邻居,如图 7-1 所示。
图 7-1
活跃用户和最近邻居
这三个用户都对某个特定的相机品牌给予了很高的评价,根据图 7-2 所示的相似性得分,前两个用户是与活跃用户最相似的用户。
图 7-2
所有用户都喜欢一个项目
现在,前两个用户对另一个项目(Xbox 360)的评价也非常高,第三个用户尚未与之互动,也没有看到如图 7-3 所示。使用该信息,我们试图预测活动用户将给予新项目(XBOX 360)的评级,这也是该特定项目(Xbox 360)的最近邻居的评级的加权平均值。
图 7-3
最近的邻居也喜欢另一个项目
然后,基于用户的 CF 向活跃用户推荐另一个项目(XBOX 360 ),因为他最有可能对该项目评价较高,因为最近的邻居也对该项目评价较高,如图 7-4 所示。
图 7-4
主动用户推荐
基于潜在因素的 CF
这种协作过滤也使用用户项目矩阵,但是不是寻找最近的邻居和预测评级,而是试图将 UI 矩阵分解成两个潜在的因素矩阵。潜在因素是从原始值导出的值。它们与观察到的变量有着内在的联系。这些新矩阵的秩要低得多,并且包含潜在的因素。这也称为矩阵分解。我们举个例子来理解矩阵因式分解的过程。我们可以将秩为 r 的 m×n 大小的矩阵“A”分解成两个更小的秩矩阵 X,Y,使得 X 和 Y 的点积产生原始的 A 矩阵。如果我们有表 7-5 所示的矩阵 A,
表 7-5。
潜在因素计算
|one
|
Two
|
three
|
five
| | --- | --- | --- | --- | | Two | four | eight | Twelve | | three | six | seven | Thirteen |
然后我们可以把所有的列值写成第一列和第三列(A1 和 A3)的线性组合。
A1 = 1 * A1 + 0 * A3
A2 = 2 * A1 + 0 * A3
A3 = 0 * A1 + 1 * A3
A4 = 2 * A1 + 1 * A3
现在,我们可以创建两个小秩矩阵,使得这两个矩阵的乘积返回原始矩阵 a。
X =
|one
|
three
| | --- | --- | | Two | eight | | three | seven |
Y =
|one
|
Two
|
Zero
|
Two
| | --- | --- | --- | --- | | Zero | Zero | one | one |
x 包含 A1 和 A3 的列值,Y 包含线性组合的系数。
X 和 Y 之间的点积返回到矩阵‘A’(原始矩阵)
考虑到表 7-2 中所示的相同用户项目矩阵,我们将其因式分解或分解成两个更小的秩矩阵。
-
用户潜在因素矩阵
-
项目潜在因素矩阵
用户潜在因素矩阵包含映射到这些潜在因素的所有用户,类似地,项目潜在因素矩阵包含映射到每个潜在因素的列中的所有项目。寻找这些潜在因素的过程是使用机器学习优化技术来完成的,例如交替最小二乘法。用户项目矩阵被分解成潜在因素矩阵,使得用户对任何项目的评级是用户潜在因素值和项目潜在因素值之间的乘积。主要目标是最小化整个用户项目矩阵评级和预测项目评级的误差平方和。例如,第二用户(26BB)对项目 2 的预测评级将是
评级(用户 2,项目 2)= 1
在每一个预测的收视率上都会有一定量的误差,因此成本函数变成了预测收视率和实际收视率之间的误差平方和。训练推荐模型包括以这样一种方式学习这些潜在因素,即它最小化总体评级的 SSE。我们可以用 ALS 方法找到最低的 SSE。ALS 的工作方式是首先固定用户潜在因素值,并尝试改变项目潜在因素值,以使总体 SSE 降低。在下一步中,项目潜在因素值保持固定,并且用户潜在因素值被更新以进一步降低 SSE。这在用户矩阵和项目矩阵之间保持交替,直到 SSE 不再减少。
优点:
-
不需要项目的内容信息,并且可以基于有价值的用户项目交互来进行推荐。
-
基于其他用户的个性化体验。
限制:
-
冷启动问题:如果用户没有物品交互的历史数据。则 RC 不能预测新用户的 k 个最近邻居,并且不能做出推荐。
-
缺失值:由于项目数量庞大,很少有用户与所有项目进行交互,因此有些项目从未被用户评级,也无法推荐。
-
无法推荐新的或未分级的项目:如果项目是新的且尚未被用户看到,则在其他用户与之交互之前,无法向现有用户推荐该项目。
-
准确性差:它不能很好地执行,因为许多组件都在不断变化,如用户的兴趣,有限的商品保质期,以及很少的商品评级。
混合推荐系统
顾名思义,混合推荐系统包括来自多个推荐系统的输入,这使得它在向用户提供有意义的推荐方面更加强大和相关。正如我们所看到的,使用个人简历有一些限制,但结合起来,他们克服了这些限制,因此能够推荐用户认为更有用和个性化的项目或信息。混合 RS 可以通过特定的方式构建,以满足业务需求。其中一种方法是构建单个 RS,并将多个 RS 输出的建议组合起来,然后推荐给用户,如图 7-5 所示。
图 7-5
综合建议
另一种方法是利用基于内容的推荐器的优势,并将它们用作基于协同过滤的推荐的输入,以向用户提供更好的推荐。这种方法也可以反过来,协同过滤可以用作基于内容的推荐的输入,如图 7-6 所示。
图 7-6
混合建议
混合推荐还包括使用其他类型的推荐,例如基于人口统计的和基于知识的,以增强其推荐的性能。混合 RS 已经成为各种业务不可或缺的一部分,帮助他们的用户消费正确的内容,因此获得了很多价值。
密码
本章的这一节重点介绍使用 PySpark 和 Jupyter Notebook 中的 ALS 方法从头构建 RS。
注意
完整的数据集和代码可以在本书的 GitHub repo 上参考,在 Spark 2.0 和更高版本上执行得最好。
让我们使用 Spark 的 MLlib 库构建一个推荐器模型,并预测任何给定用户对某个项目的评分。
数据信息
我们将在本章使用的数据集是一个著名的开源电影镜头数据集的子集,包含总共 10 万条记录,有三列(User_Id、title、rating)。我们将使用 75%的数据来训练我们的推荐模型,并在剩余的 25%用户评级上测试它。
步骤 1:创建 SparkSession 对象
我们启动 Jupyter 笔记本并导入 SparkSession,然后创建一个新的 SparkSession 对象来使用 Spark:
[In]: from pyspark.sql import SparkSession
[In]: spark=SparkSession.builder.appName('lin_reg').getOrCreate()
步骤 2:读取数据集
然后,我们使用 dataframe 在 Spark 中加载和读取数据集。我们必须确保我们已经从数据集可用的同一个目录文件夹中打开了 PySpark,否则我们必须提到数据文件夹的目录路径。
[In]:
df=spark.read.csv('movie_ratings_df.csv',inferSchema=True,header=True)
步骤 3:探索性数据分析
在本节中,我们将通过查看数据集、验证数据集的形状,以及获得已评级的电影数量和每个用户已评级的电影数量来研究数据集。
[In]: print((df.count(), len(df.columns)))
[Out]: (100000,3)
因此,上面的输出确认了数据集的大小,然后我们可以验证输入值的数据类型,以检查我们是否需要更改/转换任何列的数据类型。
[In]: df.printSchema()
[Out]: root
|-- userId: integer (nullable = true)
|-- title: string (nullable = true)
|-- rating: integer (nullable = true)
总共有三列,其中两列是数字,标题是分类。使用 PySpark 构建 RS 的关键是我们需要数字形式的 user_id 和 item_id。因此,我们稍后会将电影标题转换成数值。我们现在使用 rand 函数来查看数据帧的几行,以随机顺序打乱记录。
[In]: df.orderBy(rand()).show(10,False)
[Out]:
[In]: df.groupBy('userId').count().orderBy('count',ascending=False).show(10,False)
[Out]:
[In]: df.groupBy('userId').count().orderBy('count',ascending=True).show(10,False)
[Out]:
记录数最高的用户评价了 737 部电影,每个用户至少评价了 20 部电影。
[In]: df.groupBy('title').count().orderBy('count',ascending=False).show(10,False)
[Out]:
收视率最高的电影是星球大战 (1977),被评为 583 次,每部电影至少有 1 名用户评为。
步骤 4:特征工程
我们现在使用 StringIndexer 将电影标题列从分类值转换为数值。我们从 PySpark 库中导入 stringIndexer 和 Indextostring。
[In]: from pyspark.sql.functions import *
[In]: from pyspark.ml.feature import StringIndexer,IndexToString
接下来,我们通过提到输入列和输出列来创建 stringindexer 对象。然后,我们将对象放在数据帧上,并将其应用于电影标题列,以创建带有数值的新数据帧。
[In]: stringIndexer = StringIndexer(inputCol="title", outputCol="title_new")
[In]: model = stringIndexer.fit(df)
[In]: indexed = model.transform(df)
让我们通过查看新数据帧的几行来验证标题列的数值。
[In]: indexed.show(10)
[Out]:
正如我们所看到的,我们现在有了一个额外的列(title_new ),用数值表示电影标题。如果 user_id 也是分类类型,我们必须重复相同的过程。为了验证电影计数,我们在一个新的数据帧上重新运行 groupBy。
[In]: indexed.groupBy('title_new').count().orderBy('count',ascending=False).show(10,False)
[Out]:
步骤 5:拆分数据集
既然我们已经为构建推荐器模型准备了数据,我们可以将数据集分成训练集和测试集。我们将其分成 75 比 25 的比例来训练模型并测试其准确性。
[In]: train,test=indexed.randomSplit([0.75,0.25])
[In]: train.count()
[Out]: 75104
[In]: test.count()
[Out]: 24876
步骤 6:建立和训练推荐模型
我们从 PySpark ml 库中导入 ALS 函数,并在训练数据集上构建模型。可以调整多个超参数来提高模型的性能。其中两个重要的是 non negative =“True”不会在推荐中产生负面评级,cold start strategy =“drop”会阻止任何 NaN 评级预测。
[In]: from pyspark.ml.recommendation import ALS
[In]: rec=ALS(maxIter=10,regParam=0.01,userCol='userId',itemCol='title_new',ratingCol='rating',nonnegative=True,coldStartStrategy="drop")
[In]: rec_model=rec.fit(train)
步骤 7:对测试数据的预测和评估
整个练习的最后一部分是检查模型在未知或测试数据上的性能。我们使用 transform 函数对测试数据进行预测,并使用 RegressionEvaluate 来检查模型对测试数据的 RMSE 值。
[In]: predicted_ratings=rec_model.transform(test)
[In]: predicted_ratings.printSchema()
root
|-- userId: integer (nullable = true)
|-- title: string (nullable = true)
|-- rating: integer (nullable = true)
|-- title_new: double (nullable = false)
|-- prediction: float (nullable = false)
[In]: predicted_ratings.orderBy(rand()).show(10)
[Out]:
[xIn]: from pyspark.ml.evaluation import RegressionEvaluator
[In]: evaluator=RegressionEvaluator(metricName='rmse',predictionCol='prediction',labelCol='rating')
[In]: rmse=evaluator.evaluate(predictions)
[In] : print(rmse)
[Out]: 1.0293574739493354
RMSE 不是很高;我们在实际评分和预测评分上犯了一个点的错误。这可以通过调整模型参数和使用混合方法来进一步改善。
第八步:推荐活跃用户可能喜欢的热门电影
在检查了模型的性能并调整了超参数之后,我们可以继续向用户推荐他们没有看过但可能喜欢的顶级电影。第一步是在数据帧中创建独特电影的列表。
[In]: unique_movies=indexed.select('title_new').distinct()
[In]: unique_movies.count()
[Out]: 1664
所以,我们总共有 1664 部不同的电影。
[In]: a = unique_movies.alias('a')
我们可以在数据集中选择任何需要推荐其他电影的用户。在我们的例子中,我们使用 userId = 85。
[In]: user_id=85
我们将过滤该活跃用户已经评级或看过的电影。
[In]: watched_movies=indexed.filter(indexed['userId'] == user_id).select('title_new').distinct()
[In]: watched_movies.count()
[Out]: 287
[In]: b=watched_movies.alias('b')
因此,在该活跃用户已经评级的 1,664 部电影中,总共有 287 部独特的电影。因此,我们想从剩余的 1377 个项目中推荐电影。我们现在合并这两个表,通过从连接的表中过滤空值来查找我们可以推荐的电影。
[In]: total_movies = a.join(b, a.title_new == b.title_new,how='left')
[In]: total_movies.show(10,False)
[Out]:
[In]: remaining_movies=total_movies.where(col("b.title_new").isNull()).select(a.title_new).distinct()
[In]: remaining_movies.count()
[Out]: 1377
[In]: remaining_movies=remaining_movies.withColumn("userId",lit(int(user_id)))
[In]: remaining_movies.show(10,False)
[Out]:
最后,我们现在可以使用我们之前构建的推荐者模型,对活跃用户的剩余电影数据集进行预测。我们只过滤几个预测评级最高的推荐。
[In]: recommendations=rec_model.transform(remaining_movies).orderBy('prediction',ascending=False)
[In]: recommendations.show(5,False)
[Out]:
因此,电影标题 1433 和 1322 对于该活动用户具有最高的预测评级(85)。我们可以通过将电影标题重新添加到推荐中来使其更加直观。我们使用 Indextostring 函数创建一个额外的列来返回电影标题。
[In]:
movie_title = IndexToString(inputCol="title_new", outputCol="title",labels=model.labels)
[In]: final_recommendations=movie_title.transform(recommendations)
[In]: final_recommendations.show(10,False)
[Out]:
所以,对 userId (85)的推荐是 Boys,Les (1997)和 F aust (1994)。这可以很好地封装在一个函数中,该函数依次执行上述步骤,并为活跃用户生成推荐。GitHub repo 上有完整的代码,内置了这个函数。
结论
在本章中,我们讨论了各种类型的推荐模型,以及每种模型的优点和局限性。然后,我们在 PySpark 中创建了一个基于协同过滤的推荐系统,使用 ALS 方法向用户推荐电影。
八、聚类
到目前为止,在前面的章节中,我们已经看到了有监督的机器学习,其中目标变量或标签是我们已知的,我们试图根据输入特征来预测输出。无监督学习在某种意义上是不同的,因为没有标记的数据,我们不试图预测任何输出;相反,我们试图找到有趣的模式,并在数据中找出组。相似的值被分组在一起。
当我们加入一所新的学校或大学,我们会遇到许多新面孔,每个人看起来都不一样。我们几乎不认识研究所里的任何人,最初也没有成立小组。慢慢地,我们开始花时间和其他人在一起,团体开始发展。我们与许多不同的人打交道,并弄清楚他们与我们有多相似和不相似。几个月后,我们几乎在自己的朋友群中安顿下来。群体中的朋友/成员具有相似的属性/爱好/品味,因此会呆在一起。聚类有点类似于这种基于定义组的属性集来形成组的方法。
从集群开始
我们可以对任何类型的数据进行聚类,形成相似的观察结果组,并使用它来做出更好的决策。在早期,客户细分通常是通过基于规则的方法来完成的,这需要大量的人工工作,并且只能使用有限数量的变量。例如,如果企业想要进行客户细分,他们会考虑多达 10 个变量,如年龄、性别、工资、地点等。,并创建仍能提供合理性能的基于规则的细分市场;但在今天的情况下,这将变得非常无效。一个原因是数据可用性丰富,另一个原因是动态的客户行为。有成千上万的其他变量可以被认为是产生这些机器学习驱动的片段,这些片段更加丰富和有意义。
当我们开始聚类时,每个观察都是不同的,不属于任何组,而是基于每个观察的属性有多相似。我们以这样的方式对它们进行分组,即每组包含最相似的记录,并且任意两组之间存在尽可能多的差异。那么,我们如何衡量两个观察值是相似还是不同呢?
有多种方法可以计算任意两个观测值之间的距离。首先,我们认为任何观测值都是一种向量形式,包含如下所示的观测值(A)。
|年龄
|
工资(万美元)
|
重量(千克)
|
高度(英尺。)
| | --- | --- | --- | --- | | Thirty-two | eight | Sixty-five | six |
现在,假设我们想要计算这个观察/记录与任何其他观察(B)的距离,其他观察(B)也包含类似的属性,如下所示。
|年龄
|
工资(万美元)
|
重量(千克)
|
高度(英尺。)
| | --- | --- | --- | --- | | Forty | Fifteen | Ninety | five |
我们可以用欧几里得方法来测量距离,这很简单。
它也被称为笛卡尔距离。我们试图计算任意两点间直线的距离;如果这些点之间的距离很小,它们更可能是相似的,而如果距离很大,它们彼此不相似,如图 8-1 所示。
图 8-1
基于欧氏距离的相似性
任意两点之间的欧几里德距离可以使用下面的公式计算:
Dist ( A , B ) = 27.18
因此,观察值 A 和 B 之间的欧几里德距离是 27.18。计算观测值之间距离的其他技术如下:
-
曼哈顿距离
-
马哈拉诺比斯距离
-
闵可夫斯基距离
-
切比雪夫距离
-
余弦距离
聚类的目标是具有最小的簇内距离和最大的簇间差异。基于我们用来进行聚类的距离方法,我们可能会得到不同的组,因此,确保选择与业务问题相一致的正确距离度量是至关重要的。在研究不同的集群技术之前,让我们快速回顾一下集群的一些应用。
应用程序
如今,聚类被用于从客户细分到异常检测的各种用例中。企业广泛使用机器学习驱动的聚类来分析客户和细分,以围绕这些结果创建市场战略。聚类通过在一个聚类中找到相似的对象和彼此远离的不相似的对象来驱动大量搜索引擎的结果。它基于搜索查询推荐最接近的相似结果
基于数据类型和业务需求,可以通过多种方式进行集群。最常用的是 K-means 和层次聚类。
k 均值
k’代表我们想要在给定数据集中形成的聚类或组的数量。这种类型的聚类包括预先决定聚类的数量。在研究 K-means 聚类如何工作之前,让我们先熟悉几个术语。
-
图心
-
变化
质心是指在一个簇或组的中心的中心数据点。它也是聚类中最具代表性的点,因为它是与聚类中其他点距离最远的数据点。图 8-2 显示了三个随机集群的质心(用十字表示)。
图 8-2
星团的质心
每个聚类或组包含不同数量的最接近聚类质心的数据点。一旦单个数据点改变聚类,聚类的质心值也会改变。改变组内的中心位置,产生新的质心,如图 8-3 所示。
图 8-3
新群的新质心
聚类的整体思想是最小化类内距离,即数据点与类的质心的内部距离,并最大化类间距离,即两个不同类的质心之间的距离。
方差是该聚类内质心和数据点之间的聚类内距离的总和,如图 8-4 所示。方差随着聚类数的增加而不断减小。聚类越多,每个聚类中的数据点数量就越少,因此可变性就越小。
图 8-4
在调整距离中
K-means 聚类总共由四个步骤组成,以形成数据集中的内部组。我们将考虑一个样本数据集来理解 K 均值聚类算法是如何工作的。数据集包含一些用户,他们的年龄和体重值如表 8-1 所示。现在我们将使用 K-means 聚类来得出有意义的聚类并理解算法。
表 8-1
K 均值的样本数据集
|用户标识
|
年龄
|
重量
| | --- | --- | --- | | one | Eighteen | Eighty | | Two | Forty | Sixty | | three | Thirty-five | One hundred | | four | Twenty | Forty-five | | five | Forty-five | One hundred and twenty | | six | Thirty-two | Sixty-five | | seven | Seventeen | Fifty | | eight | Fifty-five | Fifty-five | | nine | Sixty | Ninety | | Ten | Ninety | Fifty |
如果我们在二维空间中绘制这些用户,我们可以看到最初没有点属于任何组,我们的目的是在这组用户中找到聚类(我们可以尝试两个或三个),使得每个组包含相似的用户。每个用户由年龄和体重表示,如图 8-5 所示。
图 8-5
聚类前的用户
第一步:决定 K
它从决定簇的数量(K)开始。大多数情况下,我们在开始时不能确定正确的组数,但是我们可以使用一种基于可变性的称为肘方法的方法来找到最佳的组数。对于这个例子,为了简单起见,让我们从 K=2 开始。因此,我们在这个样本数据中寻找两个集群。
步骤 2:随机初始化质心
下一步是随机将任意两个点视为新聚类的质心。这些可以随机选择,所以我们选择用户号 5 和用户号 10 作为新群集中的两个质心,如表 8-2 所示。
表 8-2
K 均值的样本数据集
|用户标识
|
年龄
|
重量
| | --- | --- | --- | | one | Eighteen | Eighty | | Two | Forty | Sixty | | three | Thirty-five | One hundred | | four | Twenty | Forty-five | | 5(质心 1) | Forty-five | One hundred and twenty | | six | Thirty-two | Sixty-five | | seven | Seventeen | Fifty | | eight | Fifty-five | Fifty-five | | nine | Sixty | Ninety | | 10(质心 2) | Ninety | Fifty |
质心可以用重量和年龄值来表示,如图 8-6 所示。
图 8-6
两个簇的随机质心
步骤 3:为每个值分配分类号
在这一步,我们计算每个点到质心的距离。在这个例子中,我们计算每个用户到两个质心点的欧几里德平方距离。根据距离值,我们继续决定用户属于哪个特定的集群(1 或 2)。无论用户靠近哪个质心(距离更小),都将成为该聚类的一部分。为每个用户计算欧几里得平方距离,如表 8-3 所示。用户 5 和用户 10 到各自质心的距离为零,因为它们是与质心相同的点。
表 8-3
基于距质心距离的聚类分配
|用户标识
|
年龄
|
重量
|
质心 1 的 ED*
|
质心 2 的 ED*
|
串
| | --- | --- | --- | --- | --- | --- | | one | Eighteen | Eighty | Forty-eight | seventy-eight | one | | Two | Forty | Sixty | Sixty | Fifty-one | Two | | three | Thirty-five | One hundred | Twenty-two | Seventy-four | one | | four | Twenty | Forty-five | Seventy-nine | Seventy | Two | | five | Forty-five | One hundred and twenty | Zero | Eighty-three | one | | six | Thirty-two | Sixty-five | Fifty-seven | Sixty | one | | seven | Seventeen | Fifty | Seventy-five | Seventy-three | Two | | eight | Fifty-five | Fifty-five | Sixty-six | Thirty-five | Two | | nine | Sixty | Ninety | Thirty-four | Fifty | one | | Ten | Ninety | Fifty | Eighty-three | Zero | Two | | (*欧几里德距离) |
因此,根据距质心的距离,我们将每个用户分配到簇 1 或簇 2。集群 1 包含五个用户,集群 2 也包含五个用户。初始集群如图 8-7 所示。
图 8-7
初始聚类和质心
如前所述,在聚类中包含或排除新的数据点之后,聚类的质心必然会改变。由于早期的质心(C1,C2)不再位于簇的中心,我们在下一步计算新的质心。
步骤 4:计算新的质心和重新分配集群
K-means 聚类的最后一步是计算聚类的新质心,并根据与新质心的距离将聚类重新分配给每个值。让我们计算群集 1 和群集 2 的新质心。为了计算聚类 1 的质心,我们只需取那些属于聚类 1 的值的年龄和体重的平均值,如表 8-4 所示。
表 8-4
聚类 1 的新质心计算
|用户标识
|
年龄
|
重量
| | --- | --- | --- | | one | Eighteen | Eighty | | three | Thirty-five | One hundred | | five | Forty-five | One hundred and twenty | | six | Thirty-two | Sixty-five | | nine | Sixty | Ninety | | 平均值 | 38 | 91 |
群集 2 的质心计算也以类似的方式完成,如表 8-5 所示。
表 8-5
群集 2 的新质心计算
|用户标识
|
年龄
|
重量
| | --- | --- | --- | | Two | Forty | Sixty | | four | Twenty | Forty-five | | seven | Seventeen | Fifty | | eight | Fifty-five | Fifty-five | | Ten | Ninety | Fifty | | 平均值 | 44.4 | 52 |
现在我们有了每个聚类的新质心值,用十字表示,如图 8-8 所示。箭头表示质心在群集内的移动。
图 8-8
两个星团的新质心
对于每个簇的质心,我们重复步骤 3,计算每个用户与新质心的欧几里德平方距离,并找出最近的质心。然后,我们根据离质心的距离将用户重新分配到集群 1 或集群 2。在这种情况下,只有一个值(用户 6)将其集群从 1 更改为 2,如表 8-6 所示。
表 8-6
集群的重新分配
|用户标识
|
年龄
|
重量
|
质心 1 的 ED*
|
质心 2 的 ED*
|
串
| | --- | --- | --- | --- | --- | --- | | one | Eighteen | Eighty | Twenty-three | Thirty-eight | one | | Two | Forty | Sixty | Thirty-one | nine | Two | | three | Thirty-five | One hundred | nine | forty-nine | one | | four | Twenty | Forty-five | forty-nine | Twenty-five | Two | | five | Forty-five | One hundred and twenty | Thirty | sixty-eight | one | | six | Thirty-two | Sixty-five | Twenty-seven | Eighteen | Two | | seven | Seventeen | Fifty | Forty-six | Twenty-seven | Two | | eight | Fifty-five | Fifty-five | Forty | Eleven | Two | | nine | Sixty | Ninety | Twenty-two | Forty-one | one | | Ten | Ninety | Fifty | Sixty-six | Forty-six | Two |
现在,根据到每个集群质心的距离,集群 1 只剩下四个用户,集群 2 包含六个用户,如图 8-9 所示。
图 8-9
集群的重新分配
我们不断重复上述步骤,直到集群分配不再有变化。新星团的质心如表 8-7 所示。
表 8-7
质心的计算
|用户标识
|
年龄
|
重量
| | --- | --- | --- | | one | Eighteen | Eighty | | three | Thirty-five | One hundred | | five | Forty-five | One hundred and twenty | | nine | Sixty | Ninety | | 平均值 | Thirty-nine point five | Ninety-seven point five |
|用户标识
|
年龄
|
重量
| | --- | --- | --- | | Two | Forty | Sixty | | four | Twenty | Forty-five | | six | Thirty-two | Sixty-five | | seven | Seventeen | Fifty | | eight | Fifty-five | Fifty-five | | Ten | Ninety | Fifty | | 平均值 | Forty-two point three three | Fifty-four point one seven |
当我们执行这些步骤时,质心的移动变得越来越小,并且这些值几乎成为特定聚类的一部分,如图 8-10 所示。
图 8-10
集群的重新分配
正如我们所观察到的,即使在质心发生变化之后,点也不再发生变化,这就完成了 K-means 聚类。结果可能会有所不同,因为它是基于第一组随机质心。为了重现结果,我们也可以自己设置起点。具有值的最终聚类如图 8-11 所示。
图 8-11
最终聚类
聚类 1 包含在身高属性上处于平均水平但在体重变量上似乎非常高的用户,而聚类 2 似乎将那些高于平均水平但非常在意自己体重的用户分组在一起,如图 8-12 所示。
图 8-12
最终聚类的属性
决定聚类数(K)
大多数时候,选择最佳数量的集群是相当棘手的,因为我们需要对数据集和业务问题的背景有深刻的理解。此外,当谈到无监督学习时,没有正确或错误的答案。与另一种方法相比,一种方法可能会产生不同数量的簇。我们必须试着找出哪种方法效果最好,以及创建的集群是否与决策足够相关。每个聚类可以用几个重要的属性来表示,这些属性表示或给出关于该特定聚类的信息。但是,有一种方法可以选择数据集的最佳聚类数。这种方法被称为肘法。
肘方法有助于我们测量大量聚类数据的总方差。聚类数量越多,方差就越小。如果我们的聚类数与数据集中的记录数相等,那么可变性将为零,因为每个点与其自身的距离为零。可变性或 SSE(误差平方和)以及“K”值如图 8-13 所示。
图 8-13
肘法
正如我们所观察到的,在 K 值为 3 和 4 之间有一种肘形结构。总方差(组内差异)会突然减少,之后方差会缓慢下降。事实上,它在 K=9 值之后变平。因此,如果我们使用肘方法,K =3 的值是最有意义的,因为它可以用较少的聚类数量捕获最多的可变性。
分层聚类
这是另一种类型的无监督机器学习技术,它不同于 K-means,因为我们不需要事先知道聚类的数量。有两种类型的分层聚类。
-
聚集聚类(自下而上的方法)
-
分裂聚类(自上而下的方法)
我们将讨论凝聚聚类,因为它是主要类型。首先假设每个数据点都是一个独立的聚类,然后逐渐将最近的值组合到相同的聚类中,直到所有的值都成为一个聚类的一部分。这是一种自底向上的方法,它计算每个聚类之间的距离,并将两个最接近的聚类合并为一个。让我们借助可视化来理解凝聚聚类。假设我们最初有七个数据点(A1-A7),需要使用聚集聚类将它们分组到包含相似值的簇中,如图 8-14 所示。
图 8-14
每个值作为一个单独的集群
在初始阶段(步骤 1),每个点被视为一个单独的聚类。在下一步中,计算每个点之间的距离,并将最近的点组合成单个聚类。在本例中,A1 和 A2、A5 和 A6 彼此距离最近,因此形成如图 8-15 所示的单个集群。
图 8-15
最近的聚类合并在一起
在使用层次聚类时,可以通过多种方式来确定最佳聚类数。一种方法是使用 elbow 方法本身,另一种方法是使用一种叫做树状图的东西。它用于可视化聚类之间的可变性(欧几里德距离)。在树状图中,垂直线的高度代表点或簇与底部列出的数据点之间的距离。每个点绘制在 X 轴上,距离表示在 Y 轴上(长度)。它是数据点的分层表示。在本例中,第 2 步的树状图如图 8-16 所示。
图 8-16
系统树图
在步骤 3 中,重复计算聚类之间的距离的练习,并将最近的聚类组合成单个聚类。这次 A3 与(A1,A2)合并,A4 与(A5,A6)合并,如图 8-17 所示。
图 8-17
最近的聚类合并在一起
第三步后的树状图如图 8-18 所示。
图 8-18
步骤 3 后的树状图
在步骤 4 中,计算唯一剩余的点 A7 之间的距离,并发现其更靠近群集(A4,A5,A6)。它与图 8-19 所示的同一个集群合并。
图 8-19
簇状构造
在最后一个阶段(步骤 5),所有点被组合成一个单独的簇(A1,A2,A3,A4,A5,A6,A7),如图 8-20 所示。
图 8-20
凝聚聚类
有时很难通过树状图确定正确的聚类数,因为它可能变得非常复杂,并且很难根据用于聚类的数据集进行解释。与 K-means 相比,层次聚类在大型数据集上效果不佳。聚类对数据点的规模也非常敏感,因此总是建议在聚类之前进行数据缩放。还有其他类型的聚类可用于将相似的数据点分组在一起,如下所示:
-
高斯混合模型聚类
-
模糊 C 均值聚类
但以上方法不在本书讨论范围之内。我们现在开始使用 PySpark 中的 K-means 数据集来构建集群。
密码
本章的这一节将介绍使用 PySpark 和 Jupyter Notebook 进行 K-Means 聚类。
注意
完整的数据集和代码可以在本书的 GitHub repo 上参考,在 Spark 2.0 和更高版本上执行得最好。
在本练习中,我们考虑最标准化的开源数据集 IRIS 数据集,用于捕获聚类数并比较监督和非监督性能。
数据信息
我们将在本章使用的数据集是著名的开源 IRIS 数据集,包含总共 150 条记录,共 5 列(萼片长度、萼片宽度、花瓣长度、花瓣宽度、物种)。每种类型有 50 个记录。我们将尝试在不使用物种标签信息的情况下,将这些物种分组。
步骤 1:创建 SparkSession 对象
我们启动 Jupyter Notebook 并导入 SparkSession,然后创建一个新的 SparkSession 对象来使用 Spark:
[In]: from pyspark.sql import SparkSession
[In]: spark=SparkSession.builder.appName('K_means').getOrCreate()
步骤 2:读取数据集
然后,我们使用 dataframe 在 Spark 中加载和读取数据集。我们必须确保我们已经从数据集可用的同一个目录文件夹中打开了 PySpark,否则我们必须提到数据文件夹的目录路径。
[In]:
df=spark.read.csv('iris_dataset.csv',inferSchema=True,header=True)
步骤 3:探索性数据分析
在本节中,我们通过查看数据集并验证其形状来探索数据集。
[In]:print((df.count(), len(df.columns)))
[Out]: (150,3)
因此,上面的输出确认了数据集的大小,然后我们可以验证输入值的数据类型,以检查我们是否需要更改/转换任何列的数据类型。
[In]: df.printSchema()
[Out]: root
|-- sepal_length: double (nullable = true)
|-- sepal_width: double (nullable = true)
|-- petal_length: double (nullable = true)
|-- petal_width: double (nullable = true)
|-- species: string (nullable = true)
总共有五列,其中四列是数字列,标签列是分类列。
[In]: from pyspark.sql.functions import rand
[In]: df.orderBy(rand()).show(10,False)
[Out]:
+------------+-----------+------------+-----------+----------+
|sepal_length|sepal_width|petal_length|petal_width|species |
+------------+-----------+------------+-----------+----------+
|5.5 |2.6 |4.4 |1.2 |versicolor|
|4.5 |2.3 |1.3 |0.3 |setosa |
|5.1 |3.7 |1.5 |0.4 |setosa |
|7.7 |3.0 |6.1 |2.3 |virginica |
|5.5 |2.5 |4.0 |1.3 |versicolor|
|6.3 |2.3 |4.4 |1.3 |versicolor|
|6.2 |2.9 |4.3 |1.3 |versicolor|
|6.3 |2.5 |4.9 |1.5 |versicolor|
|4.7 |3.2 |1.3 |0.2 |setosa |
|6.1 |2.8 |4.0 |1.3 |versicolor|
+------------+-----------+------------+-----------+----------+
[In]: df.groupBy('species').count().orderBy('count').show(10,False)
[Out]:
+----------+-----+
|species |count|
+----------+-----+
|virginica |50 |
|setosa |50 |
|versicolor|50 |
+----------+-----+
因此,它确认了数据集中每个物种都有相同数量的记录
步骤 4:特征工程
这是我们使用 Spark 的 VectorAssembler 创建一个组合所有输入特征的单一向量的部分。它仅创建一个要素来捕获该特定行的输入值。因此,不是四个输入列(我们不考虑标签列,因为它是一种无监督的机器学习技术),它本质上是以列表的形式将它转换为具有四个输入值的单个列。
[In]: from pyspark.ml.linalg import Vector
[In]: from pyspark.ml.feature import VectorAssembler
[In]: input_cols=['sepal_length', 'sepal_width', 'petal_length', 'petal_width']
[In]: vec_assembler = VectorAssembler(inputCols = input_cols, outputCol="features")
[In]: final_data = vec_assembler.transform(df)
步骤 5:构建 K-Means 聚类模型
最终数据包含可用于运行 K 均值聚类的输入向量。由于我们需要在使用 K-means 之前预先声明' K '的值,所以我们可以使用 elbow 方法来计算出' K '的正确值。为了使用肘方法,我们对不同的“K”值运行 K 均值聚类。首先,我们从 PySpark 库中导入 K-means,并创建一个空列表来捕获 K 的每个值的可变性或 SSE(在聚类距离内)。
[In]:from pyspark.ml.clustering import KMeans
[In]:errors=[]
[In]:
for k in range(2,10):
kmeans = KMeans(featuresCol='features',k=k)
model = kmeans.fit(final_data)
intra_distance = model.computeCost(final_data)
errors.append(intra_distance)
注意
“K”的最小值应该为 2,以便能够构建聚类。
现在,我们可以使用 numpy 和 matplotlib 绘制星团内距离与星团数量的关系。
[In]: import pandas as pd
[In]: import numpy as np
[In]: import matplotlib.pyplot as plt
[In]: cluster_number = range(2,10)
[In]: plt.xlabel('Number of Clusters (K)')
[In]: plt.ylabel('SSE')
[In]: plt.scatter(cluster_number,errors)
[In]: plt.show()
[Out]:
在这种情况下,k=3 似乎是最佳的聚类数,因为我们可以看到 3 到 4 个值之间的肘形结构。我们使用 k=3 构建最终的集群。
[In]: kmeans = KMeans(featuresCol='features',k=3)
[In]: model = kmeans.fit(final_data)
[In]: model.transform(final_data).groupBy('prediction').count().show()
[Out]:
+----------+-----+
|prediction|count|
+----------+-----+
| 1| 50|
| 2| 38|
| 0| 62|
+----------+-----+
基于 IRIS 数据集,K-Means 聚类为我们提供了三个不同的聚类。我们肯定是做了一些错误的分配,因为只有一个类别有 50 条记录,其余的类别都混在一起了。我们可以使用 transform 函数将分类编号分配给原始数据集,并使用 groupBy 函数来验证分组。
[In]: predictions=model.transform(final_data)
[In]: predictions.groupBy('species','prediction').count().show()
[Out]:
+----------+----------+-----+
| species|prediction|count|
+----------+----------+-----+
| virginica| 2| 14|
| setosa| 0| 50|
| virginica| 1| 36|
|versicolor| 1| 3|
|versicolor| 2| 47|
+----------+----------+-----+
正如可以观察到的,setosa 物种与 versicolor 完美地分组在一起,几乎被捕获在同一个簇中,但 verginica 似乎属于两个不同的组。K-means 每次都会产生不同的结果,因为它每次都随机选择起始点(质心)。因此,在 K-means 聚类中得到的结果可能与这些结果完全不同,除非我们使用种子来重现这些结果。种子确保分割和初始质心值在整个分析过程中保持一致。
步骤 6:集群的可视化
在最后一步,我们可以借助 Python 的 matplotlib 库来可视化新的集群。为此,我们首先将 Spark 数据帧转换成 Pandas 数据帧。
[In]: pandas_df = predictions.toPandas()
[In]: pandas_df.head()
我们导入所需的库来绘制第三个可视化并观察集群。
[In]: from mpl_toolkits.mplot3d import Axes3D
[In]: cluster_vis = plt.figure(figsize=(12,10)).gca(projection='3d')
[In]: cluster_vis.scatter(pandas_df.sepal_length, pandas_df.sepal_width, pandas_df.petal_length, c=pandas_df.prediction,depthshade=False)
[In]: plt.show()
结论
在这一章中,我们讨论了不同类型的无监督机器学习技术,并使用 PySpark 中的 K-means 算法构建了聚类。K-Means 使用随机质心初始化对数据点进行分组,而分层聚类侧重于将整个数据点合并到单个聚类中。我们还介绍了各种确定最佳聚类数的技术,如肘方法和树状图,它们在对数据点进行分组时使用方差优化。