Spark 秘籍(二)
原文:
zh.annas-archive.org/md5/BF1FAE88E839F4D0A5A0FD250CEC5835译者:飞龙
第五章:Spark Streaming
Spark Streaming 为 Apache Spark 增加了大数据处理的圣杯——即实时分析。它使 Spark 能够摄取实时数据流,并以极低的延迟(几秒钟)提供实时智能。
在本章中,我们将涵盖以下配方:
-
使用流式处理的单词计数
-
流式处理 Twitter 数据
-
使用 Kafka 进行流式处理
介绍
流式处理是将持续流动的输入数据分成离散单元的过程,以便可以轻松处理。现实生活中熟悉的例子是流式视频和音频内容(尽管用户可以在观看之前下载完整的电影,但更快的解决方案是以小块流式传输数据,这些数据开始播放给用户,而其余数据则在后台下载)。
除了多媒体之外,流式处理的实际例子包括市场数据源、天气数据、电子股票交易数据等的处理。所有这些应用程序产生大量数据,速度非常快,并且需要对数据进行特殊处理,以便实时从数据中获取洞察。
流式处理有一些基本概念,在我们专注于 Spark Streaming 之前最好先了解。流式应用程序接收数据的速率称为数据速率,以每秒千字节(kbps)或每秒兆字节(mbps)的形式表示。
流式处理的一个重要用例是复杂事件处理(CEP)。在 CEP 中,控制正在处理的数据范围很重要。这个范围称为窗口,可以基于时间或大小。基于时间的窗口的一个例子是分析过去一分钟内的数据。基于大小的窗口的一个例子可以是给定股票的最近 100 笔交易的平均要价。
Spark Streaming 是 Spark 的库,提供支持处理实时数据。这个流可以来自任何来源,比如 Twitter、Kafka 或 Flume。
在深入研究配方之前,Spark Streaming 有一些基本构建块,我们需要充分理解。
Spark Streaming 有一个称为StreamingContext的上下文包装器,它包装在SparkContext周围,并且是 Spark Streaming 功能的入口点。流式数据根据定义是连续的,需要进行时间切片处理。这段时间被称为批处理间隔,在创建StreamingContext时指定。RDD 和批处理之间是一对一的映射,也就是说,每个批处理都会产生一个 RDD。正如您在下图中所看到的,Spark Streaming 接收连续数据,将其分成批次并馈送给 Spark。
批处理间隔对于优化流式应用程序非常重要。理想情况下,您希望至少以数据获取的速度进行处理;否则,您的应用程序将产生积压。Spark Streaming 在一个批处理间隔的持续时间内收集数据,比如 2 秒。一旦这个 2 秒的间隔结束,该间隔内收集的数据将被交给 Spark 进行处理,而流式处理将专注于收集下一个批处理间隔的数据。现在,这个 2 秒的批处理间隔是 Spark 处理数据的全部时间,因为它应该空闲以接收下一个批处理的数据。如果 Spark 能够更快地处理数据,您可以将批处理间隔减少到 1 秒。如果 Spark 无法跟上这个速度,您必须增加批处理间隔。
Spark Streaming 中的 RDD 的连续流需要以一种抽象的形式表示,通过这种抽象可以对其进行处理。这种抽象称为离散流(DStream)。对 DStream 应用的任何操作都会导致对底层 RDD 的操作。
每个输入 DStream 都与一个接收器相关联(除了文件流)。接收器从输入源接收数据并将其存储在 Spark 的内存中。有两种类型的流式源:
-
基本来源,如文件和套接字连接
-
高级来源,如 Kafka 和 Flume
Spark Streaming 还提供了窗口计算,您可以在其中对数据的滑动窗口应用转换。滑动窗口操作基于两个参数:
-
窗口长度:这是窗口的持续时间。例如,如果您想要获取最后 1 分钟的数据分析,窗口长度将是 1 分钟。
-
滑动间隔:这表示您希望多频繁执行操作。比如您希望每 10 秒执行一次操作;这意味着每 10 秒,窗口的 1 分钟将有 50 秒的数据与上一个窗口相同,以及 10 秒的新数据。
这两个参数都作用于底层的 RDD,显然不能被分开;因此,这两个参数都应该是批处理间隔的倍数。窗口长度也必须是滑动间隔的倍数。
DStream 还具有输出操作,允许将数据推送到外部系统。它们类似于 RDD 上的操作(在 DStream 上发生的抽象级别更高)。
除了打印 DStream 的内容之外,还支持标准 RDD 操作,例如saveAsTextFile,saveAsObjectFile和saveAsHadoopFile,分别由类似的对应物saveAsTextFiles,saveAsObjectFiles和saveAsHadoopFiles。
一个非常有用的输出操作是foreachRDD(func),它将任意函数应用于所有 RDD。
使用流媒体进行单词计数
让我们从一个简单的流媒体示例开始,在其中一个终端中,我们将输入一些文本,流媒体应用程序将在另一个窗口中捕获它。
如何做...
- 启动 Spark shell 并为其提供一些额外的内存:
$ spark-shell --driver-memory 1G
- 流特定的导入:
scala> import org.apache.spark.SparkConf
scala> import org.apache.spark.streaming.{Seconds, StreamingContext}
scala> import org.apache.spark.storage.StorageLevel
scala> import StorageLevel._
- 隐式转换的导入:
scala> import org.apache.spark._
scala> import org.apache.spark.streaming._
scala> import org.apache.spark.streaming.StreamingContext._
- 使用 2 秒批处理间隔创建
StreamingContext:
scala> val ssc = new StreamingContext(sc, Seconds(2))
- 在本地主机上使用端口
8585创建一个SocketTextStreamDstream,并使用MEMORY_ONLY缓存:
scala> val lines = ssc.socketTextStream("localhost",8585,MEMORY_ONLY)
- 将行分成多个单词:
scala> val wordsFlatMap = lines.flatMap(_.split(" "))
- 将单词转换为(单词,1),即将
1作为单词的每次出现的值输出为键:
scala> val wordsMap = wordsFlatMap.map( w => (w,1))
- 使用
reduceByKey方法为每个单词的出现次数添加一个数字作为键(该函数一次处理两个连续的值,由a和b表示):
scala> val wordCount = wordsMap.reduceByKey( (a,b) => (a+b))
- 打印
wordCount:
scala> wordCount.print
- 启动
StreamingContext;记住,直到启动StreamingContext之前什么都不会发生:
scala> ssc.start
- 现在,在一个单独的窗口中,启动 netcat 服务器:
$ nc -lk 8585
- 输入不同的行,例如
to be or not to be:
to be or not to be
- 检查 Spark shell,您将看到类似以下截图的单词计数结果:
流媒体 Twitter 数据
Twitter 是一个著名的微博平台。它每天产生大量数据,大约有 5 亿条推文。Twitter 允许通过 API 访问其数据,这使其成为测试任何大数据流应用程序的典范。
在这个示例中,我们将看到如何使用 Twitter 流媒体库在 Spark 中实时流式传输数据。Twitter 只是提供流数据给 Spark 的一个来源,并没有特殊的地位。因此,Twitter 没有内置的库。尽管如此,Spark 确实提供了一些 API 来促进与 Twitter 库的集成。
使用实时 Twitter 数据源的一个示例用途是查找过去 5 分钟内的热门推文。
如何做...
-
如果您还没有 Twitter 帐户,请创建一个 Twitter 帐户。
-
点击创建新应用。
-
输入名称,描述,网站和回调 URL,然后点击创建您的 Twitter 应用程序。
-
您将到达应用程序管理屏幕。
-
导航到密钥和访问令牌 | 创建我的访问令牌。
-
记下屏幕上的四个值,我们将在第 14 步中使用:
消费者密钥(API 密钥)
消费者密钥(API 密钥)
访问令牌
访问令牌密钥
- 我们将需要在一段时间内在这个屏幕上提供这些值,但是,现在,让我们从 Maven 中央库下载所需的第三方库:
$ wget http://central.maven.org/maven2/org/apache/spark/spark-streaming-twitter_2.10/1.2.0/spark-streaming-twitter_2.10-1.2.0.jar
$ wget http://central.maven.org/maven2/org/twitter4j/twitter4j-stream/4.0.2/twitter4j-stream-4.0.2.jar
$ wget http://central.maven.org/maven2/org/twitter4j/twitter4j-core/4.0.2/twitter4j-core-4.0.2.jar
- 打开 Spark shell,提供前面三个 JAR 作为依赖项:
$ spark-shell --jars spark-streaming-twitter_2.10-1.2.0.jar, twitter4j-stream-4.0.2.jar,twitter4j-core-4.0.2.jar
- 执行特定于 Twitter 的导入:
scala> import org.apache.spark.streaming.twitter._
scala> import twitter4j.auth._
scala> import twitter4j.conf._
- 流特定的导入:
scala> import org.apache.spark.streaming.{Seconds, StreamingContext}
- 导入隐式转换:
scala> import org.apache.spark._
scala> import org.apache.spark.streaming._
scala> import org.apache.spark.streaming.StreamingContext._
- 使用 10 秒批处理间隔创建
StreamingContext:
scala> val ssc = new StreamingContext(sc, Seconds(10))
- 使用 2 秒批处理间隔创建
StreamingContext:
scala> val cb = new ConfigurationBuilder
scala> cb.setDebugEnabled(true)
.setOAuthConsumerKey("FKNryYEKeCrKzGV7zuZW4EKeN")
.setOAuthConsumerSecret("x6Y0zcTBOwVxpvekSCnGzbi3NYNrM5b8ZMZRIPI1XRC3pDyOs1")
.setOAuthAccessToken("31548859-DHbESdk6YoghCLcfhMF88QEFDvEjxbM6Q90eoZTGl")
.setOAuthAccessTokenSecret("wjcWPvtejZSbp9cgLejUdd6W1MJqFzm5lByUFZl1NYgrV")
val auth = new OAuthAuthorization(cb.build)
注意
这些是示例值,您应该放入自己的值。
- 创建 Twitter DStream:
scala> val tweets = TwitterUtils.createStream(ssc,auth)
- 过滤掉英文推文:
scala> val englishTweets = tweets.filter(_.getLang()=="en")
- 从推文中获取文本:
scala> val status = englishTweets.map(status => status.getText)
- 设置检查点目录:
scala> ssc.checkpoint("hdfs://localhost:9000/user/hduser/checkpoint")
- 启动
StreamingContext:
scala> ssc.start
scala> ssc.awaitTermination
- 您可以使用
:paste将所有这些命令放在一起:
scala> :paste
import org.apache.spark.streaming.twitter._
import twitter4j.auth._
import twitter4j.conf._
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark._
import org.apache.spark.streaming._
import org.apache.spark.streaming.StreamingContext._
val ssc = new StreamingContext(sc, Seconds(10))
val cb = new ConfigurationBuilder
cb.setDebugEnabled(true).setOAuthConsumerKey("FKNryYEKeCrKzGV7zuZW4EKeN")
.setOAuthConsumerSecret("x6Y0zcTBOwVxpvekSCnGzbi3NYNrM5b8ZMZRIPI1XRC3pDyOs1")
.setOAuthAccessToken("31548859-DHbESdk6YoghCLcfhMF88QEFDvEjxbM6Q90eoZTGl")
.setOAuthAccessTokenSecret("wjcWPvtejZSbp9cgLejUdd6W1MJqFzm5lByUFZl1NYgrV")
val auth = new OAuthAuthorization(cb.build)
val tweets = TwitterUtils.createStream(ssc,Some(auth))
val englishTweets = tweets.filter(_.getLang()=="en")
val status = englishTweets.map(status => status.getText)
status.print
ssc.checkpoint("hdfs://localhost:9000/checkpoint")
ssc.start
ssc.awaitTermination
使用 Kafka 进行流处理
Kafka 是一个分布式、分区和复制的提交日志服务。简单地说,它是一个分布式消息服务器。Kafka 将消息源维护在称为主题的类别中。主题的一个示例可以是您想要获取有关的公司的新闻的股票代码,例如 Cisco 的 CSCO。
生成消息的进程称为生产者,消费消息的进程称为消费者。在传统的消息传递中,消息服务有一个中央消息服务器,也称为代理。由于 Kafka 是一个分布式消息传递服务,它有一个代理集群,功能上充当一个 Kafka 代理,如下所示:
对于每个主题,Kafka 维护分区日志。这个分区日志由分布在集群中的一个或多个分区组成,如下图所示:
Kafka 从 Hadoop 和其他大数据框架借鉴了许多概念。分区的概念与 Hadoop 中的InputSplit概念非常相似。在最简单的形式中,使用TextInputFormat时,InputSplit与块相同。块以TextInputFormat中的键值对形式读取,其中键是行的字节偏移量,值是行的内容本身。类似地,在 Kafka 分区中,记录以键值对的形式存储和检索,其中键是称为偏移量的顺序 ID 号,值是实际消息。
在 Kafka 中,消息的保留不取决于消费者的消费。消息将保留一段可配置的时间。每个消费者可以以任何他们喜欢的顺序读取消息。它需要保留的只是一个偏移量。另一个类比可以是阅读一本书,其中页码类似于偏移量,而页内容类似于消息。只要他们记住书签(当前偏移量),读者可以以任何方式阅读。
为了提供类似于传统消息系统中的发布/订阅和 PTP(队列)的功能,Kafka 有消费者组的概念。消费者组是一组消费者,Kafka 集群将其视为一个单元。在消费者组中,只需要一个消费者接收消息。如果消费者 C1 在下图中接收主题 T1 的第一条消息,则该主题上的所有后续消息也将传递给该消费者。使用这种策略,Kafka 保证了给定主题的消息传递顺序。
在极端情况下,当所有消费者都在一个消费者组中时,Kafka 集群的行为类似于 PTP/队列。在另一个极端情况下,如果每个消费者都属于不同的组,它的行为类似于发布/订阅。在实践中,每个消费者组有一定数量的消费者。
这个示例将展示如何使用来自 Kafka 的数据执行单词计数。
准备好
这个示例假设 Kafka 已经安装。Kafka 自带 ZooKeeper。我们假设 Kafka 的主目录在/opt/infoobjects/kafka中:
- 启动 ZooKeeper:
$ /opt/infoobjects/kafka/bin/zookeeper-server-start.sh /opt/infoobjects/kafka/config/zookeeper.properties
- 启动 Kafka 服务器:
$ /opt/infoobjects/kafka/bin/kafka-server-start.sh /opt/infoobjects/kafka/config/server.properties
- 创建一个
test主题:
$ /opt/infoobjects/kafka/bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
如何做...:
- 下载
spark-streaming-kafka库及其依赖项:
$ wget http://central.maven.org/maven2/org/apache/spark/spark-streaming-kafka_2.10/1.2.0/spark-streaming-kafka_2.10-1.2.0.jar
$ wget http://central.maven.org/maven2/org/apache/kafka/kafka_2.10/0.8.1/kafka_2.10-0.8.1.jar
$ wget http://central.maven.org/maven2/com/yammer/metrics/metrics-core/2.2.0/metrics-core-2.2.0.jar
$ wget http://central.maven.org/maven2/com/101tec/zkclient/0.4/zkclient-0.4.jar
- 启动 Spark shell 并提供
spark-streaming-kafka库:
$ spark-shell --jars spark-streaming-kafka_2.10-1.2.0.jar, kafka_2.10-0.8.1.jar,metrics-core-2.2.0.jar,zkclient-0.4.jar
- 流特定导入:
scala> import org.apache.spark.streaming.{Seconds, StreamingContext}
- 隐式转换导入:
scala> import org.apache.spark._
scala> import org.apache.spark.streaming._
scala> import org.apache.spark.streaming.StreamingContext._
scala> import org.apache.spark.streaming.kafka.KafkaUtils
- 创建具有 2 秒批处理间隔的
StreamingContext:
scala> val ssc = new StreamingContext(sc, Seconds(2))
- 设置 Kafka 特定变量:
scala> val zkQuorum = "localhost:2181"
scala> val group = "test-group"
scala> val topics = "test"
scala> val numThreads = 1
- 创建
topicMap:
scala> val topicMap = topics.split(",").map((_,numThreads.toInt)).toMap
- 创建 Kafka DStream:
scala> val lineMap = KafkaUtils.createStream(ssc, zkQuorum, group, topicMap)
- 从 lineMap 中取出值:
scala> val lines = lineMap.map(_._2)
- 创建值的
flatMap:
scala> val words = lines.flatMap(_.split(" "))
- 创建(单词,出现次数)的键值对:
scala> val pair = words.map( x => (x,1))
- 对滑动窗口进行单词计数:
scala> val wordCounts = pair.reduceByKeyAndWindow(_ + _, _ - _, Minutes(10), Seconds(2), 2)
scala> wordCounts.print
- 设置
checkpoint目录:
scala> ssc.checkpoint("hdfs://localhost:9000/user/hduser/checkpoint")
- 启动
StreamingContext:
scala> ssc.start
scala> ssc.awaitTermination
- 在另一个窗口的 Kafka 中的
test主题上发布一条消息:
$ /opt/infoobjects/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
-
现在,通过在第 15 步按Enter并在每条消息后按Enter来在 Kafka 上发布消息。
-
现在,当您在 Kafka 上发布消息时,您将在 Spark shell 中看到它们:
还有更多...
假设您想要维护每个单词出现次数的运行计数。Spark Streaming 具有名为updateStateByKey操作的功能。updateStateByKey操作允许您在更新时维护任意状态并使用新提供的信息进行更新。
这种任意状态可以是聚合值,也可以是状态的改变(比如 Twitter 用户的心情)。执行以下步骤:
- 让我们在对 RDD 对调用
updateStateByKey:
scala> val runningCounts = wordCounts.updateStateByKey( (values: Seq[Int], state: Option[Int]) => Some(state.sum + values.sum))
注意
updateStateByKey操作返回一个新的“状态”DStream,其中每个键的状态都通过在键的先前状态和键的新值上应用给定函数来更新。这可以用于维护每个键的任意状态数据。
使此操作生效涉及两个步骤:
-
定义状态
-
定义状态
update函数
对于每个键,都会调用一次updateStateByKey操作,值表示与该键关联的值序列,非常类似于 MapReduce,状态可以是任意状态,我们选择使其为Option[Int]。在第 18 步的每次调用中,通过将当前值的总和添加到先前状态来更新先前状态。
- 打印结果:
scala> runningCounts.print
- 以下是使用
updateStateByKey操作来维护任意状态的所有步骤的组合:
Scala> :paste
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark._
import org.apache.spark.streaming._
import org.apache.spark.streaming.kafka._
import org.apache.spark.streaming.StreamingContext._
val ssc = new StreamingContext(sc, Seconds(2))
val zkQuorum = "localhost:2181"
val group = "test-group"
val topics = "test"
val numThreads = 1
val topicMap = topics.split(",").map((_,numThreads.toInt)).toMap
val lineMap = KafkaUtils.createStream(ssc, zkQuorum, group, topicMap)
val lines = lineMap.map(_._2)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(x => (x,1))
val runningCounts = pairs.updateStateByKey( (values: Seq[Int], state: Option[Int]) => Some(state.sum + values.sum))
runningCounts.print
ssc.checkpoint("hdfs://localhost:9000/user/hduser/checkpoint")
ssc.start
ssc.awaitTermination
- 按下Ctrl + D运行它(使用
:paste粘贴的代码)。
第六章:使用 MLlib 开始机器学习
本章分为以下配方:
-
创建向量
-
创建标记点
-
创建矩阵
-
计算摘要统计信息
-
计算相关性
-
进行假设检验
-
使用 ML 创建机器学习管道
介绍
以下是维基百科对机器学习的定义:
"机器学习是一门探索从数据中学习的算法的构建和研究的科学学科。"
基本上,机器学习是利用过去的数据来预测未来。机器学习在很大程度上依赖于统计分析和方法。
在统计学中,有四种测量标度:
| 规模类型 | 描述 |
|---|---|
| 名义标度 | =,≠识别类别不能是数字示例:男性,女性 |
| 序数标度 | =,≠,<,>名义标度+从最不重要到最重要的排名示例:公司等级制度 |
| 间隔标度 | =,≠,<,>,+,-序数标度+观察之间的距离分配的数字指示顺序任何连续值之间的差异与其他值相同 60°温度不是 30°的两倍 |
| 比例标度 | =,≠,<,>,+,×,÷间隔标度+观察的比率10 的两倍 |
数据之间可以进行的另一个区分是连续数据和离散数据。连续数据可以取任何值。大多数属于间隔和比例标度的数据是连续的。
离散变量只能取特定的值,值之间有明确的界限。例如,一所房子可以有两间或三间房间,但不能有 2.75 间。属于名义和序数标度的数据始终是离散的。
MLlib 是 Spark 的机器学习库。在本章中,我们将专注于机器学习的基础知识。
创建向量
在了解向量之前,让我们专注于点是什么。一个点只是一组数字。这组数字或坐标定义了点在空间中的位置。坐标的数量确定了空间的维度。
我们可以用最多三个维度来可视化空间。具有三个以上维度的空间称为超空间。让我们利用这个空间的隐喻。
让我们从一个人开始。一个人具有以下维度:
-
重量
-
身高
-
年龄
我们在三维空间中工作。因此,点(160,69,24)的解释将是 160 磅的体重,69 英寸的身高和 24 岁的年龄。
注意
点和向量是同一回事。向量中的维度称为特征。换句话说,我们可以将特征定义为被观察现象的个体可测属性。
Spark 有本地向量和矩阵,还有分布式矩阵。分布式矩阵由一个或多个 RDD 支持。本地向量具有数字索引和双值,并存储在单台机器上。
MLlib 中有两种本地向量:密集和稀疏。密集向量由其值的数组支持,而稀疏向量由两个并行数组支持,一个用于索引,另一个用于值。
因此,人的数据(160,69,24)将使用密集向量表示为[160.0,69.0,24.0],使用稀疏向量格式表示为(3,[0,1,2],[160.0,69.0,24.0])。
是将向量稀疏还是密集取决于它有多少空值或 0。让我们以一个包含 10,000 个值的向量为例,其中有 9,000 个值为 0。如果我们使用密集向量格式,它将是一个简单的结构,但会浪费 90%的空间。稀疏向量格式在这里会更好,因为它只保留非零的索引。
稀疏数据非常常见,Spark 支持libsvm格式,该格式每行存储一个特征向量。
如何做…
- 启动 Spark shell:
$ spark-shell
- 显式导入 MLlib 向量(不要与其他向量类混淆):
Scala> import org.apache.spark.mllib.linalg.{Vectors,Vector}
- 创建密集向量:
scala> val dvPerson = Vectors.dense(160.0,69.0,24.0)
- 创建稀疏向量:
scala> val svPerson = Vectors.sparse(3,Array(0,1,2),Array(160.0,69.0,24.0))
它是如何工作的...
以下是vectors.dense的方法签名:
def dense(values: Array[Double]): Vector
这里,值表示向量中元素的双精度数组。
以下是Vectors.sparse的方法签名:
def sparse(size: Int, indices: Array[Int], values: Array[Double]): Vector
这里,size表示向量的大小,indices是索引数组,values是双精度值数组。确保您指定double作为数据类型,或者至少在一个值中使用十进制;否则,对于只有整数的数据集,它将抛出异常。
创建一个带标签的点
带标签的点是一个带有相关标签的本地向量(稀疏/密集),在监督学习中用于帮助训练算法。您将在下一章中了解更多相关信息。
标签以双精度值存储在LabeledPoint中。这意味着当您有分类标签时,它们需要被映射为双精度值。您分配给类别的值是无关紧要的,只是一种便利。
| 类型 | 标签值 |
|---|---|
| 二元分类 | 0 或 1 |
| 多类分类 | 0, 1, 2… |
| 回归 | 十进制值 |
如何做…
- 启动 Spark shell:
$spark-shell
- 显式导入 MLlib 向量:
scala> import org.apache.spark.mllib.linalg.{Vectors,Vector}
- 导入
LabeledPoint:
scala> import org.apache.spark.mllib.regression.LabeledPoint
- 使用正标签和密集向量创建一个带标签的点:
scala> val willBuySUV = LabeledPoint(1.0,Vectors.dense(300.0,80,40))
- 使用负标签和密集向量创建一个带标签的点:
scala> val willNotBuySUV = LabeledPoint(0.0,Vectors.dense(150.0,60,25))
- 使用正标签和稀疏向量创建一个带标签的点:
scala> val willBuySUV = LabeledPoint(1.0,Vectors.sparse(3,Array(0,1,2),Array(300.0,80,40)))
- 使用负标签和稀疏向量创建一个带标签的点:
scala> val willNotBuySUV = LabeledPoint(0.0,Vectors.sparse(3,Array(0,1,2),Array(150.0,60,25)))
- 创建一个包含相同数据的
libsvm文件:
$vi person_libsvm.txt (libsvm indices start with 1)
0 1:150 2:60 3:25
1 1:300 2:80 3:40
- 将
person_libsvm.txt上传到hdfs:
$ hdfs dfs -put person_libsvm.txt person_libsvm.txt
- 做更多的导入:
scala> import org.apache.spark.mllib.util.MLUtils
scala> import org.apache.spark.rdd.RDD
- 从
libsvm文件加载数据:
scala> val persons = MLUtils.loadLibSVMFile(sc,"person_libsvm.txt")
创建矩阵
矩阵只是一个表示多个特征向量的表。可以存储在一台机器上的矩阵称为本地矩阵,可以分布在集群中的矩阵称为分布式矩阵。
本地矩阵具有基于整数的索引,而分布式矩阵具有基于长整数的索引。两者的值都是双精度。
有三种类型的分布式矩阵:
-
RowMatrix:每行都是一个特征向量。 -
IndexedRowMatrix:这也有行索引。 -
CoordinateMatrix:这只是一个MatrixEntry的矩阵。MatrixEntry表示矩阵中的一个条目,由其行和列索引表示。
如何做…
- 启动 Spark shell:
$spark-shell
- 导入与矩阵相关的类:
scala> import org.apache.spark.mllib.linalg.{Vectors,Matrix, Matrices}
- 创建一个密集的本地矩阵:
scala> val people = Matrices.dense(3,2,Array(150d,60d,25d, 300d,80d,40d))
- 创建一个
personRDD作为向量的 RDD:
scala> val personRDD = sc.parallelize(List(Vectors.dense(150,60,25), Vectors.dense(300,80,40)))
- 导入
RowMatrix和相关类:
scala> import org.apache.spark.mllib.linalg.distributed.{IndexedRow, IndexedRowMatrix,RowMatrix, CoordinateMatrix, MatrixEntry}
- 创建一个
personRDD的行矩阵:
scala> val personMat = new RowMatrix(personRDD)
- 打印行数:
scala> print(personMat.numRows)
- 打印列数:
scala> print(personMat.numCols)
- 创建一个索引行的 RDD:
scala> val personRDD = sc.parallelize(List(IndexedRow(0L, Vectors.dense(150,60,25)), IndexedRow(1L, Vectors.dense(300,80,40))))
- 创建一个索引行矩阵:
scala> val pirmat = new IndexedRowMatrix(personRDD)
- 打印行数:
scala> print(pirmat.numRows)
- 打印列数:
scala> print(pirmat.numCols)
- 将索引行矩阵转换回行矩阵:
scala> val personMat = pirmat.toRowMatrix
- 创建一个矩阵条目的 RDD:
scala> val meRDD = sc.parallelize(List(
MatrixEntry(0,0,150),
MatrixEntry(1,0,60),
MatrixEntry(2,0,25),
MatrixEntry(0,1,300),
MatrixEntry(1,1,80),
MatrixEntry(2,1,40)
))
- 创建一个坐标矩阵:
scala> val pcmat = new CoordinateMatrix(meRDD)
- 打印行数:
scala> print(pcmat.numRows)
- 打印列数:
scala> print(pcmat.numCols)
计算摘要统计
汇总统计用于总结观察结果,以获得对数据的整体感觉。摘要包括以下内容:
-
数据的中心趋势-均值、众数、中位数
-
数据的分布-方差、标准差
-
边界条件-最小值、最大值
这个示例介绍了如何生成摘要统计信息。
如何做…
- 启动 Spark shell:
$ spark-shell
- 导入与矩阵相关的类:
scala> import org.apache.spark.mllib.linalg.{Vectors,Vector}
scala> import org.apache.spark.mllib.stat.Statistics
- 创建一个
personRDD作为向量的 RDD:
scala> val personRDD = sc.parallelize(List(Vectors.dense(150,60,25), Vectors.dense(300,80,40)))
- 计算列的摘要统计:
scala> val summary = Statistics.colStats(personRDD)
- 打印这个摘要的均值:
scala> print(summary.mean)
- 打印方差:
scala> print(summary.variance)
- 打印每列中非零值的数量:
scala> print(summary.numNonzeros)
- 打印样本大小:
scala> print(summary.count)
- 打印每列的最大值:
scala> print(summary.max)
计算相关性
相关性是两个变量之间的统计关系,当一个变量改变时,会导致另一个变量的改变。相关性分析衡量了这两个变量相关的程度。
如果一个变量的增加导致另一个变量的增加,这被称为正相关。如果一个变量的增加导致另一个变量的减少,这是负相关。
Spark 支持两种相关算法:Pearson 和 Spearman。Pearson 算法适用于两个连续变量,例如一个人的身高和体重或房屋大小和房价。Spearman 处理一个连续和一个分类变量,例如邮政编码和房价。
准备就绪
让我们使用一些真实数据,这样我们可以更有意义地计算相关性。以下是 2014 年初加利福尼亚州萨拉托加市房屋的大小和价格:
| 房屋面积(平方英尺) | 价格 |
|---|---|
| 2100 | $1,620,000 |
| 2300 | $1,690,000 |
| 2046 | $1,400,000 |
| 4314 | $2,000,000 |
| 1244 | $1,060,000 |
| 4608 | $3,830,000 |
| 2173 | $1,230,000 |
| 2750 | $2,400,000 |
| 4010 | $3,380,000 |
| 1959 | $1,480,000 |
如何做…
- 启动 Spark shell:
$ spark-shell
- 导入统计和相关类:
scala> import org.apache.spark.mllib.linalg._
scala> import org.apache.spark.mllib.stat.Statistics
- 创建一个房屋面积的 RDD:
scala> val sizes = sc.parallelize(List(2100, 2300, 2046, 4314, 1244, 4608, 2173, 2750, 4010, 1959.0))
- 创建一个房价的 RDD:
scala> val prices = sc.parallelize(List(1620000 , 1690000, 1400000, 2000000, 1060000, 3830000, 1230000, 2400000, 3380000, 1480000.00))
- 计算相关性:
scala> val correlation = Statistics.corr(sizes,prices)
correlation: Double = 0.8577177736252577
0.85 表示非常强的正相关性。
由于这里没有特定的算法,所以默认是 Pearson。corr方法被重载以将算法名称作为第三个参数。
- 用 Pearson 计算相关性:
scala> val correlation = Statistics.corr(sizes,prices)
- 用 Spearman 计算相关性:
scala> val correlation = Statistics.corr(sizes,prices,"spearman")
在前面的例子中,两个变量都是连续的,所以 Spearman 假设大小是离散的。Spearman 使用的更好的例子是邮政编码与价格。
进行假设检验
假设检验是确定给定假设为真的概率的一种方法。假设一个样本数据表明女性更倾向于投票给民主党。这可能对更大的人口来说是真的,也可能不是。如果这个模式只是样本数据中的偶然现象呢?
观察假设检验目标的另一种方式是回答这个问题:如果一个样本中有一个模式,那么这个模式存在的机会是多少?
我们怎么做?有一句话说,证明某事最好的方法是试图证伪它。
要证伪的假设被称为零假设。假设检验适用于分类数据。让我们看一个党派倾向的民意调查的例子。
| 党派 | 男性 | 女性 |
|---|---|---|
| 民主党 | 32 | 41 |
| 共和党 | 28 | 25 |
| 独立 | 34 | 26 |
如何做…
- 启动 Spark shell:
$ spark-shell
- 导入相关的类:
scala> import org.apache.spark.mllib.stat.Statistics
scala> import org.apache.spark.mllib.linalg.{Vector,Vectors}
scala> import org.apache.spark.mllib.linalg.{Matrix, Matrices}
- 为民主党创建一个向量:
scala> val dems = Vectors.dense(32.0,41.0)
- 为共和党创建一个向量:
scala> val reps= Vectors.dense(28.0,25.0)
- 为独立党创建一个向量:
scala> val indies = Vectors.dense(34.0,26.0)
- 对观察数据进行卡方拟合度检验:
scala> val dfit = Statistics.chiSqTest(dems)
scala> val rfit = Statistics.chiSqTest(reps)
scala> val ifit = Statistics.chiSqTest(indies)
- 打印拟合度检验结果:
scala> print(dfit)
scala> print(rfit)
scala> print(ifit)
- 创建输入矩阵:
scala> val mat = Matrices.dense(2,3,Array(32.0,41.0, 28.0,25.0, 34.0,26.0))
- 进行卡方独立性检验:
scala> val in = Statistics.chiSqTest(mat)
- 打印独立性检验结果:
scala> print(in)
使用 ML 创建机器学习管道
Spark ML 是 Spark 中构建机器学习管道的新库。这个库正在与 MLlib 一起开发。它有助于将多个机器学习算法组合成一个单一的管道,并使用 DataFrame 作为数据集。
准备就绪
让我们首先了解一些 Spark ML 中的基本概念。它使用转换器将一个 DataFrame 转换为另一个 DataFrame。简单转换的一个例子可以是追加列。你可以把它看作是关系世界中的"alter table"的等价物。
另一方面,估计器代表一个机器学习算法,它从数据中学习。估计器的输入是一个 DataFrame,输出是一个转换器。每个估计器都有一个fit()方法,它的工作是训练算法。
机器学习管道被定义为一系列阶段;每个阶段可以是估计器或者转换器。
我们在这个示例中要使用的例子是某人是否是篮球运动员。为此,我们将有一个估计器和一个转换器的管道。
估计器获取训练数据来训练算法,然后转换器进行预测。
暂时假设LogisticRegression是我们正在使用的机器学习算法。我们将在随后的章节中解释LogisticRegression的细节以及其他算法。
如何做…
- 启动 Spark shell:
$ spark-shell
- 进行导入:
scala> import org.apache.spark.mllib.linalg.{Vector,Vectors}
scala> import org.apache.spark.mllib.regression.LabeledPoint
scala> import org.apache.spark.ml.classification.LogisticRegression
- 为篮球运动员 Lebron 创建一个标记点,身高 80 英寸,体重 250 磅:
scala> val lebron = LabeledPoint(1.0,Vectors.dense(80.0,250.0))
- 为不是篮球运动员的 Tim 创建一个标记点,身高 70 英寸,体重 150 磅:
scala> val tim = LabeledPoint(0.0,Vectors.dense(70.0,150.0))
- 为篮球运动员 Brittany 创建一个标记点,身高 80 英寸,体重 207 磅:
scala> val brittany = LabeledPoint(1.0,Vectors.dense(80.0,207.0))
- 为不是篮球运动员的 Stacey 创建一个标记点,身高 65 英寸,体重 120 磅:
scala> val stacey = LabeledPoint(0.0,Vectors.dense(65.0,120.0))
- 创建一个训练 RDD:
scala> val trainingRDD = sc.parallelize(List(lebron,tim,brittany,stacey))
- 创建一个训练 DataFrame:
scala> val trainingDF = trainingRDD.toDF
- 创建一个
LogisticRegression估计器:
scala> val estimator = new LogisticRegression
- 通过拟合训练 DataFrame 来创建一个转换器:
scala> val transformer = estimator.fit(trainingDF)
- 现在,让我们创建一个测试数据—John 身高 90 英寸,体重 270 磅,是篮球运动员:
scala> val john = Vectors.dense(90.0,270.0)
- 创建另一个测试数据—Tom 身高 62 英寸,体重 150 磅,不是篮球运动员:
scala> val tom = Vectors.dense(62.0,120.0)
- 创建一个训练 RDD:
scala> val testRDD = sc.parallelize(List(john,tom))
- 创建一个
Featurescase 类:
scala> case class Feature(v:Vector)
- 将
testRDD映射到Features的 RDD:
scala> val featuresRDD = testRDD.map( v => Feature(v))
- 将
featuresRDD转换为具有列名"features"的 DataFrame:
scala> val featuresDF = featuresRDD.toDF("features")
- 通过向其添加
predictions列来转换featuresDF:
scala> val predictionsDF = transformer.transform(featuresDF)
- 打印
predictionsDF:
scala> predictionsDF.foreach(println)
PredictionDF,如您所见,除了保留特征之外,还创建了三列—rawPrediction、probability和prediction。让我们只选择features和prediction:
scala> val shorterPredictionsDF = predictionsDF.select("features","prediction")
- 将预测重命名为
isBasketBallPlayer:
scala> val playerDF = shorterPredictionsDF.toDF("features","isBasketBallPlayer")
- 打印
playerDF的模式:
scala> playerDF.printSchema
第七章:使用 MLlib 进行监督学习 - 回归
本章分为以下几个部分:
-
使用线性回归
-
理解成本函数
-
使用套索进行线性回归
-
进行岭回归
介绍
以下是维基百科对监督学习的定义:
“监督学习是从标记的训练数据中推断函数的机器学习任务。”
监督学习有两个步骤:
-
使用训练数据集训练算法;这就像是先提出问题和它们的答案
-
使用测试数据集向训练好的算法提出另一组问题。
有两种监督学习算法:
-
回归:这预测连续值输出,比如房价。
-
分类:这预测离散值输出(0 或 1)称为标签,比如一封电子邮件是否是垃圾邮件。分类不仅限于两个值;它可以有多个值,比如标记一封电子邮件为重要、不重要、紧急等等(0, 1, 2…)。
注意
本章将介绍回归,下一章将介绍分类。
作为回归的示例数据集,我们将使用加利福尼亚州萨拉托加市最近售出的房屋数据作为训练集来训练算法。一旦算法训练好了,我们将要求它根据房屋的尺寸来预测房价。下图说明了工作流程:
这里的假设,对于它的作用来说,可能听起来像一个误称,你可能会认为预测函数可能是一个更好的名字,但是假设这个词是出于历史原因而使用的。
如果我们只使用一个特征来预测结果,就称为双变量分析。当我们有多个特征时,就称为多变量分析。事实上,我们可以有任意多个特征。其中一种算法,支持向量机(SVM),我们将在下一章中介绍,实际上允许你拥有无限数量的特征。
本章将介绍如何使用 MLlib,Spark 的机器学习库进行监督学习。
注意
数学解释已尽可能简单地提供,但你可以随意跳过数学,直接转到*如何做……*部分。
使用线性回归
线性回归是一种基于一个或多个预测变量或特征x来建模响应变量y值的方法。
准备工作
让我们使用一些房屋数据来预测房屋的价格,基于它的大小。以下是 2014 年初加利福尼亚州萨拉托加市房屋的大小和价格:
| 房屋大小(平方英尺) | 价格 |
|---|---|
| 2100 | $ 1,620,000 |
| 2300 | $ 1,690,000 |
| 2046 | $ 1,400,000 |
| 4314 | $ 2,000,000 |
| 1244 | $ 1,060,000 |
| 4608 | $ 3,830,000 |
| 2173 | $ 1,230,000 |
| 2750 | $ 2,400,000 |
| 4010 | $ 3,380,000 |
| 1959 | $ 1,480,000 |
这里有一个相同的图形表示:
如何做…
- 启动 Spark shell:
$ spark-shell
- 导入统计和相关类:
scala> import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.regression.LabeledPoint
scala> import org.apache.spark.mllib.regression.LinearRegressionWithSGD
- 创建
LabeledPoint数组,房价作为标签:
scala> val points = Array(
LabeledPoint(1620000,Vectors.dense(2100)),
LabeledPoint(1690000,Vectors.dense(2300)),
LabeledPoint(1400000,Vectors.dense(2046)),
LabeledPoint(2000000,Vectors.dense(4314)),
LabeledPoint(1060000,Vectors.dense(1244)),
LabeledPoint(3830000,Vectors.dense(4608)),
LabeledPoint(1230000,Vectors.dense(2173)),
LabeledPoint(2400000,Vectors.dense(2750)),
LabeledPoint(3380000,Vectors.dense(4010)),
LabeledPoint(1480000,Vectors.dense(1959))
)
- 创建上述数据的 RDD:
scala> val pricesRDD = sc.parallelize(points)
- 使用这些数据训练模型,进行 100 次迭代。这里,步长被保持得很小,以适应响应变量的非常大的值,也就是房价。第四个参数是每次迭代使用的数据集的一部分,最后一个参数是要使用的初始权重集(不同特征的权重):
scala> val model = LinearRegressionWithSGD.train(pricesRDD,100,0.0000006,1.0,Vectors.zeros(1))
- 预测一个 2500 平方英尺的房屋的价格:
scala> val prediction = model.predict(Vectors.dense(2500))
房屋大小只是一个预测变量。房价取决于其他变量,比如地块大小,房屋年龄等等。你拥有的变量越多,你的预测就会越准确。
理解成本函数
成本函数或损失函数在机器学习算法中非常重要。大多数算法都有某种形式的成本函数,目标是最小化它。影响成本函数的参数,比如上一个步骤中的stepSize,需要手动设置。因此,理解成本函数的整个概念非常重要。
在这个步骤中,我们将分析线性回归的成本函数。线性回归是一个简单的算法,可以帮助读者理解成本函数对于复杂算法的作用。
让我们回到线性回归。目标是找到最佳拟合线,使得误差的均方最小。这里,我们将误差定义为最佳拟合线的值与训练数据集中响应变量的实际值之间的差异。
对于单个自变量的简单情况,最佳拟合线可以写成:
这个函数也被称为假设函数,可以写成:
线性回归的目标是找到最佳拟合线。在这条线上,θ[0]代表y轴上的截距,θ[1]代表线的斜率,如下方程所示:
我们必须选择θ[0]和θ[1],使得h(x)对于训练数据集中的y最接近。因此,对于第i个数据点,线与数据点之间的距离的平方为:
换句话说,这是预测房价与房屋实际售价之间的差的平方。现在,让我们计算训练数据集中这个值的平均值:
上述方程被称为线性回归的成本函数J。目标是最小化这个成本函数。
这个成本函数也被称为平方误差函数。如果它们分别针对J绘制,θ[0]和θ[1]都会遵循凸曲线。
让我们举一个非常简单的数据集的例子,包括三个值,(1,1), (2,2), 和 (3,3),以便计算更容易:
假设θ[1]为 0,也就是说,最佳拟合线与x轴平行。在第一种情况下,假设最佳拟合线是x轴,也就是y=0。那么,成本函数的值将如下:
现在,让我们把这条线稍微移动到y=1。那么,成本函数的值将如下:
现在,让我们把这条线进一步移动到y=2。那么,成本函数的值将如下:
现在,当我们把这条线进一步移动到y=3,成本函数的值将如下:
现在,让我们把这条线进一步移动到y=4。那么,成本函数的值将如下:
所以,你看到成本函数的值先减少,然后再次增加,就像这样:
现在,让我们通过将θ[0]设为 0 并使用不同的θ[1]值来重复这个练习。
在第一种情况下,假设最佳拟合线是x轴,也就是y=0。那么,成本函数的值将如下:
现在,让我们使用斜率为 0.5。那么,成本函数的值将如下:
现在,让我们使用斜率为 1。那么,成本函数的值将如下:
现在,当我们使用斜率为 1.5 时,以下将是成本函数的值:
现在,让我们使用斜率为 2.0。以下将是成本函数的值:
如您在两个图中所见,当斜率或曲线的梯度为 0 时,J的最小值是。
当θ[0]和θ[1]都映射到 3D 空间时,它就像一个碗的形状,成本函数的最小值在其底部。
到达最小值的这种方法称为梯度下降。在 Spark 中,实现是随机梯度下降。
使用套索进行线性回归
套索是线性回归的收缩和选择方法。它最小化了通常的平方误差和系数绝对值之和的边界。它基于原始套索论文,可在statweb.stanford.edu/~tibs/lasso/lasso.pdf找到。
我们在上一个示例中使用的最小二乘法也称为普通最小二乘法(OLS)。OLS 有两个挑战:
-
预测准确性:使用 OLS 进行的预测通常具有较低的预测偏差和较高的方差。通过缩小一些系数(甚至使它们为零),可以提高预测准确性。偏差会有所增加,但整体预测准确性会提高。
-
解释:对于预测变量的数量较多,希望找到其中表现最强的子集(相关性)。
注意
偏差与方差
预测误差背后有两个主要原因:偏差和方差。理解偏差和方差的最佳方法是看一个情况,我们在同一数据集上多次进行预测。
偏差是预测结果与实际值之间的估计差距,方差是不同预测值之间的差异的估计。
通常,添加更多的特征有助于减少偏差,这是很容易理解的。如果在构建预测模型时,我们遗漏了一些具有显著相关性的特征,这将导致显著的误差。
如果您的模型方差很高,可以删除特征以减少它。更大的数据集也有助于减少方差。
在这里,我们将使用一个简单的数据集,这是一个不适当的数据集。不适当的数据集是指样本数据量小于预测变量的数量。
| y | x0 | x1 | x2 | x3 | x4 | x5 | x6 | x7 | x8 |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 5 | 3 | 1 | 2 | 1 | 3 | 2 | 2 | 1 |
| 2 | 9 | 8 | 8 | 9 | 7 | 9 | 8 | 7 | 9 |
您可以很容易地猜到,在这里,九个预测变量中,只有两个与y有强相关性,即x0和x1。我们将使用这个数据集和套索算法来验证其有效性。
如何做…
- 启动 Spark shell:
$ spark-shell
- 导入统计和相关类:
scala> import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.regression.LabeledPoint
scala> import org.apache.spark.mllib.regression.LassoWithSGD
- 创建带有房价作为标签的
LabeledPoint数组:
scala> val points = Array(
LabeledPoint(1,Vectors.dense(5,3,1,2,1,3,2,2,1)),
LabeledPoint(2,Vectors.dense(9,8,8,9,7,9,8,7,9))
)
- 创建一个 RDD 的前述数据:
scala> val rdd = sc.parallelize(points)
- 使用这些数据训练一个模型,使用 100 次迭代。在这里,步长和正则化参数已经手动设置:
scala> val model = LassoWithSGD.train(rdd,100,0.02,2.0)
- 检查有多少预测变量的系数被设置为零:
scala> model.weights
org.apache.spark.mllib.linalg.Vector = [0.13455106581619633,0.02240732644670294,0.0,0.0,0.0,0.01360995990267153,0.0,0.0,0.0]
如您所见,九个预测变量中有六个的系数被设置为零。这是套索的主要特征:它认为不实用的任何预测变量,通过将它们的系数设置为零,从方程中移除它们。
进行岭回归
改进预测质量的套索的另一种方法是岭回归。在套索中,许多特征的系数被设置为零,因此从方程中消除,在岭回归中,预测变量或特征受到惩罚,但永远不会被设置为零。
如何做…
- 启动 Spark shell:
$ spark-shell
- 导入统计和相关类:
scala> import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.regression.LabeledPoint
scala> import org.apache.spark.mllib.regression.RidgeRegressionWithSGD
- 创建带有房价作为标签的
LabeledPoint数组:
scala> val points = Array(
LabeledPoint(1,Vectors.dense(5,3,1,2,1,3,2,2,1)),
LabeledPoint(2,Vectors.dense(9,8,8,9,7,9,8,7,9))
)
- 创建一个包含上述数据的 RDD:
scala> val rdd = sc.parallelize(points)
- 使用这些数据训练一个模型,进行 100 次迭代。在这里,步长和正则化参数已经手动设置:
scala> val model = RidgeRegressionWithSGD.train(rdd,100,0.02,2.0)
- 检查有多少预测变量的系数被设为零:
scala> model.weights
org.apache.spark.mllib.linalg.Vector = [0.049805969577244584,0.029883581746346748,0.009961193915448916,0.019922387830897833,0.009961193915448916,0.029883581746346748,0.019922387830897833,0.019922387830897833,0.009961193915448916]
如您所见,与套索不同,岭回归不会将任何预测变量的系数设为零,但它确实使一些系数非常接近于零。
第八章:监督学习与 MLlib – 分类
本章分为以下几个部分:
-
使用逻辑回归进行分类
-
使用支持向量机进行二元分类
-
使用决策树进行分类
-
使用随机森林进行分类
-
使用梯度提升树进行分类
-
使用朴素贝叶斯进行分类
介绍
分类问题类似于上一章讨论的回归问题,只是结果变量 y 只取少数离散值。在二元分类中,y 只取两个值:0 或 1。你也可以将分类中响应变量可以取的值看作代表类别。
使用逻辑回归进行分类
在分类中,响应变量 y 具有离散值,而不是连续值。一些例子包括电子邮件(垃圾邮件/非垃圾邮件)、交易(安全/欺诈)等。
下面方程中的 y 变量可以取两个值,0 或 1:
这里,0 被称为负类,1 表示正类。虽然我们称它们为正类或负类,但这只是为了方便起见。算法对这种分配持中立态度。
线性回归,虽然对于回归任务效果很好,但对于分类任务存在一些限制。这些包括:
-
拟合过程对异常值非常敏感
-
不能保证假设函数 h(x) 将适合于 0(负类)到 1(正类)的范围内
逻辑回归保证 h(x) 将适合于 0 到 1 之间。尽管逻辑回归中有回归一词,但这更像是一个误称,它实际上是一个分类算法:
在线性回归中,假设函数如下:
在逻辑回归中,我们稍微修改假设方程如下:
g 函数被称为Sigmoid 函数或逻辑函数,对于实数 t 定义如下:
这是 Sigmoid 函数的图形:
正如你所看到的,当 t 接近负无穷时,g(t) 接近 0,当 t 接近无穷时,g(t) 接近 1。因此,这保证了假设函数的输出永远不会超出 0 到 1 的范围。
现在假设函数可以重写为:
h(x) 是给定预测变量 x 的 y = 1 的估计概率,因此 h(x) 也可以重写为:
换句话说,假设函数显示了在给定特征矩阵 x 的情况下 y 为 1 的概率,由 参数化。这个概率可以是 0 到 1 之间的任意实数,但我们的分类目标不允许我们有连续值;我们只能有两个值 0 或 1,表示负类或正类。
假设我们预测 y = 1 如果
并且 y = 0 否则。如果我们再次看一下 S 形函数图,我们会意识到,当 S 形函数是
,也就是说,对于 t 的正值,它将预测为正类:
自从使用逻辑回归进行分类,这意味着对于使用逻辑回归进行分类的情况下,将会预测正类。为了更好地说明这一点,让我们将其扩展到双变量情况的非矩阵形式:
由方程表示的平面将决定给定向量属于正类还是负类。这条线被称为决策边界。
这个边界不一定是线性的,取决于训练集。如果训练数据不能在线性边界上分离,可以添加更高级别的多项式特征来促进它。一个例子是通过平方 x1 和 x2 来添加两个新特征,如下所示:
请注意,对于学习算法来说,这种增强与以下方程式完全相同:
学习算法将把多项式的引入视为另一个特征。这给了你在拟合过程中很大的权力。这意味着通过正确选择多项式和参数,可以创建任何复杂的决策边界。
让我们花一些时间来理解如何选择参数的正确值,就像我们在线性回归的情况下所做的那样。线性回归的成本函数J是:
正如你所知,我们在这个成本函数中对成本进行了平均。让我们用成本项来表示这一点:
换句话说,成本项是算法在预测h(x)的真实响应变量值y时必须支付的成本:
这个成本对于线性回归来说效果很好,但是对于逻辑回归来说,这个成本函数是非凸的(也就是说,它会导致多个局部最小值),我们需要找到一个更好的凸方式来估计成本。
逻辑回归中效果很好的成本函数如下:
让我们通过结合这两个成本函数将它们合并成一个:
让我们将这个成本函数重新放回到J中:
目标是最小化成本,也就是最小化的值。这是通过梯度下降算法来实现的。Spark 有两个支持逻辑回归的类:
-
LogisticRegressionWithSGD -
LogisticRegressionWithLBFGS
LogisticRegressionWithLBFGS类更受欢迎,因为它消除了优化步长的步骤。
准备工作
2006 年,铃木、鹤崎和光岡在日本不同海滩上对一种濒临灭绝的穴居蜘蛛的分布进行了一些研究。
让我们看一些关于颗粒大小和蜘蛛存在的数据:
| 颗粒大小(mm) | 蜘蛛存在 |
|---|---|
| 0.245 | 不存在 |
| 0.247 | 不存在 |
| 0.285 | 存在 |
| 0.299 | 存在 |
| 0.327 | 存在 |
| 0.347 | 存在 |
| 0.356 | 不存在 |
| 0.36 | 存在 |
| 0.363 | 不存在 |
| 0.364 | 存在 |
| 0.398 | 不存在 |
| 0.4 | 存在 |
| 0.409 | 不存在 |
| 0.421 | 存在 |
| 0.432 | 不存在 |
| 0.473 | 存在 |
| 0.509 | 存在 |
| 0.529 | 存在 |
| 0.561 | 不存在 |
| 0.569 | 不存在 |
| 0.594 | 存在 |
| 0.638 | 存在 |
| 0.656 | 存在 |
| 0.816 | 存在 |
| 0.853 | 存在 |
| 0.938 | 存在 |
| 1.036 | 存在 |
| 1.045 | 存在 |
我们将使用这些数据来训练算法。缺席将表示为 0,存在将表示为 1。
如何做…
- 启动 Spark shell:
$ spark-shell
- 导入统计和相关类:
scala> import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.regression.LabeledPoint
scala> import org.apache.spark.mllib.classification.LogisticRegressionWithLBFGS
- 创建一个带有蜘蛛存在或不存在的
LabeledPoint数组作为标签:
scala> val points = Array(
LabeledPoint(0.0,Vectors.dense(0.245)),
LabeledPoint(0.0,Vectors.dense(0.247)),
LabeledPoint(1.0,Vectors.dense(0.285)),
LabeledPoint(1.0,Vectors.dense(0.299)),
LabeledPoint(1.0,Vectors.dense(0.327)),
LabeledPoint(1.0,Vectors.dense(0.347)),
LabeledPoint(0.0,Vectors.dense(0.356)),
LabeledPoint(1.0,Vectors.dense(0.36)),
LabeledPoint(0.0,Vectors.dense(0.363)),
LabeledPoint(1.0,Vectors.dense(0.364)),
LabeledPoint(0.0,Vectors.dense(0.398)),
LabeledPoint(1.0,Vectors.dense(0.4)),
LabeledPoint(0.0,Vectors.dense(0.409)),
LabeledPoint(1.0,Vectors.dense(0.421)),
LabeledPoint(0.0,Vectors.dense(0.432)),
LabeledPoint(1.0,Vectors.dense(0.473)),
LabeledPoint(1.0,Vectors.dense(0.509)),
LabeledPoint(1.0,Vectors.dense(0.529)),
LabeledPoint(0.0,Vectors.dense(0.561)),
LabeledPoint(0.0,Vectors.dense(0.569)),
LabeledPoint(1.0,Vectors.dense(0.594)),
LabeledPoint(1.0,Vectors.dense(0.638)),
LabeledPoint(1.0,Vectors.dense(0.656)),
LabeledPoint(1.0,Vectors.dense(0.816)),
LabeledPoint(1.0,Vectors.dense(0.853)),
LabeledPoint(1.0,Vectors.dense(0.938)),
LabeledPoint(1.0,Vectors.dense(1.036)),
LabeledPoint(1.0,Vectors.dense(1.045)))
- 创建前述数据的 RDD:
scala> val spiderRDD = sc.parallelize(points)
- 使用这些数据训练模型(当所有预测因子为零时,截距是该值):
scala> val lr = new LogisticRegressionWithLBFGS().setIntercept(true)
scala> val model = lr.run(spiderRDD)
- 预测粒度为
0.938的蜘蛛的存在:
scala> val predict = model.predict(Vectors.dense(0.938))
使用 SVM 进行二元分类
分类是一种根据其效用将数据分为不同类别的技术。例如,电子商务公司可以对潜在访客应用两个标签“会购买”或“不会购买”。
这种分类是通过向机器学习算法提供一些已经标记的数据来完成的,称为训练数据。挑战在于如何标记两个类之间的边界。让我们以下图所示的简单示例为例:
在前面的案例中,我们将灰色和黑色指定为“不会购买”和“会购买”标签。在这里,画一条线将两个类别分开就像下面这样简单:
这是我们能做到的最好吗?实际上并不是,让我们试着做得更好。黑色分类器与“会购买”和“不会购买”车辆并不是真正等距的。让我们尝试做得更好,就像下面这样:
现在看起来不错。实际上,这正是 SVM 算法所做的。您可以在前面的图中看到,实际上只有三辆车决定了线的斜率:线上方的两辆黑色车和线下方的一辆灰色车。这些车被称为支持向量,而其余的车,即向量,是无关紧要的。
有时候画一条线并不容易,可能需要一条曲线来分开两个类别,就像下面这样:
有时甚至这还不够。在这种情况下,我们需要超过两个维度来解决问题。我们需要的不是分类线,而是一个超平面。实际上,每当数据过于混乱时,增加额外的维度有助于找到一个分离类别的超平面。下图说明了这一点:
这并不意味着增加额外的维度总是一个好主意。大多数情况下,我们的目标是减少维度,只保留相关的维度/特征。有一整套算法专门用于降维;我们将在后面的章节中介绍这些算法。
如何做…
- Spark 库中加载了示例
libsvm数据。我们将使用这些数据并将其加载到 HDFS 中:
$ hdfs dfs -put /opt/infoobjects/spark/data/mllib/sample_libsvm_data.txt /user/hduser/sample_libsvm_data.txt
- 启动 Spark shell:
$ spark-shell
- 执行所需的导入:
scala> import org.apache.spark.mllib.classification.SVMWithSGD
scala> import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics
scala> import org.apache.spark.mllib.regression.LabeledPoint
scala> import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.util.MLUtils
- 将数据加载为 RDD:
scala> val svmData = MLUtils.loadLibSVMFile(sc,"sample_libsvm_data.txt")
- 记录的数量:
scala> svmData.count
- 现在让我们将数据集分成一半训练数据和一半测试数据:
scala> val trainingAndTest = svmData.randomSplit(Array(0.5,0.5))
- 分配
training和test数据:
scala> val trainingData = trainingAndTest(0)
scala> val testData = trainingAndTest(1)
- 训练算法并构建模型进行 100 次迭代(您可以尝试不同的迭代次数,但您会发现,在某个时候,结果开始收敛,这是一个不错的选择):
scala> val model = SVMWithSGD.train(trainingData,100)
- 现在我们可以使用这个模型来预测任何数据集的标签。让我们预测测试数据中第一个点的标签:
scala> val label = model.predict(testData.first.features)
- 让我们创建一个元组,第一个值是测试数据的预测值,第二个值是实际标签,这将帮助我们计算算法的准确性:
scala> val predictionsAndLabels = testData.map( r => (model.predict(r.features),r.label))
- 您可以计算有多少记录预测和实际标签不匹配:
scala> predictionsAndLabels.filter(p => p._1 != p._2).count
使用决策树进行分类
决策树是机器学习算法中最直观的。我们经常在日常生活中使用决策树。
决策树算法有很多有用的特性:
-
易于理解和解释
-
处理分类和连续特征
-
处理缺失的特征
-
不需要特征缩放
决策树算法以倒序方式工作,其中包含特征的表达式在每个级别进行评估,并将数据集分成两个类别。我们将通过一个简单的哑剧的例子来帮助您理解这一点,大多数人在大学时都玩过。我猜了一个动物,然后让我的同事问我问题来猜出我的选择。她的提问是这样的:
Q1:这是一只大动物吗?
A:是的
Q2:这种动物是否活了 40 年以上?
A:是的
Q3:这种动物是大象吗?
A:是的
这显然是一个过于简化的情况,她知道我假设了一只大象(在大数据世界中你还能猜到什么?)。让我们扩展这个例子,包括一些更多的动物,如下图所示(灰色框是类):
前面的例子是多类分类的一个案例。在这个配方中,我们将专注于二元分类。
准备就绪
每当我们的儿子早上要上网球课时,前一天晚上教练会查看天气预报,并决定第二天早上是否适合打网球。这个配方将使用这个例子来构建一个决策树。
让我们决定影响早上是否打网球的天气特征:
-
雨
-
风速
-
温度
让我们建立一个不同组合的表:
| 雨 | 有风 | 温度 | 打网球? |
|---|---|---|---|
| 是 | 是 | 炎热 | 否 |
| 是 | 是 | 正常 | 否 |
| 是 | 是 | 凉爽 | 否 |
| 否 | 是 | 炎热 | 否 |
| 否 | 是 | 凉爽 | 否 |
| 否 | 否 | 炎热 | 是 |
| 否 | 否 | 正常 | 是 |
| 否 | 否 | 凉爽 | 否 |
现在我们如何构建决策树呢?我们可以从雨、有风或温度中的一个开始。规则是从一个特征开始,以便最大化信息增益。
在雨天,正如你在表中看到的,其他特征并不重要,也不会打网球。对于风速很高的情况也是如此。
决策树,像大多数其他算法一样,只接受特征值作为双精度值。所以,让我们进行映射:
正类是 1.0,负类是 0.0。让我们使用 CSV 格式加载数据,使用第一个值作为标签:
$vi tennis.csv
0.0,1.0,1.0,2.0
0.0,1.0,1.0,1.0
0.0,1.0,1.0,0.0
0.0,0.0,1.0,2.0
0.0,0.0,1.0,0.0
1.0,0.0,0.0,2.0
1.0,0.0,0.0,1.0
0.0,0.0,0.0,0.0
如何做...
- 启动 Spark shell:
$ spark-shell
- 执行所需的导入:
scala> import org.apache.spark.mllib.tree.DecisionTree
scala> import org.apache.spark.mllib.regression.LabeledPoint
scala> import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.tree.configuration.Algo._
scala> import org.apache.spark.mllib.tree.impurity.Entropy
- 加载文件:
scala> val data = sc.textFile("tennis.csv")
- 解析数据并将其加载到
LabeledPoint中:
scala> val parsedData = data.map {
line => val parts = line.split(',').map(_.toDouble)
LabeledPoint(parts(0), Vectors.dense(parts.tail)) }
- 用这些数据训练算法:
scala> val model = DecisionTree.train(parsedData, Classification, Entropy, 3)
- 为无雨、大风和凉爽的温度创建一个向量:
scala> val v=Vectors.dense(0.0,1.0,0.0)
- 预测是否应该打网球:
scala> model.predict(v)
工作原理...
让我们为这个配方中创建的网球决策树绘制决策树:
这个模型有三个级别的深度。选择哪个属性取决于我们如何最大化信息增益。它的衡量方式是通过衡量分裂的纯度。纯度意味着,无论确定性是否增加,那么给定的数据集将被视为正面或负面。在这个例子中,这相当于是否打网球的机会在增加,还是不打网球的机会在增加。
纯度是用熵来衡量的。熵是系统中混乱程度的度量。在这种情况下,更容易理解它是一种不确定性的度量:
纯度的最高级别是 0,最低级别是 1。让我们尝试使用公式来确定纯度。
当雨是是的时候,打网球的概率是p+为 0/3 = 0。不打网球的概率p_为 3/3 = 1:
这是一个纯净的集合。
当雨不下时,打网球的概率p+为 2/5 = 0.4。不打网球的概率p_为 3/5 = 0.6:
这几乎是一个不纯的集合。最不纯的情况是概率为 0.5 的情况。
Spark 使用三种方法来确定不纯度:
-
基尼不纯度(分类)
-
熵(分类)
-
方差(回归)
信息增益是父节点杂质与两个子节点杂质的加权和之差。让我们看一下第一个分裂,将大小为 8 的数据分成大小为 3(左)和 5(右)的两个数据集。让我们称第一个分裂为s1,父节点为rain,左子节点为no rain,右子节点为wind。所以信息增益将是:
由于我们已经为no rain和wind计算了熵的杂质,现在让我们计算rain的熵:
现在让我们计算信息增益:
所以在第一个分裂中,信息增益为 0.2。这是我们能达到的最好效果吗?让我们看看我们的算法得出了什么。首先,让我们找出树的深度:
scala> model.depth
Int = 2
在这里,深度是2,而我们直观地构建的是3,所以这个模型似乎更优化。让我们看看树的结构:
scala> model.toDebugString
String = "DecisionTreeModel classifier of depth 2 with 5 nodes
If (feature 1 <= 0.0)
If (feature 2 <= 0.0)
Predict: 0.0
Else (feature 2 > 0.0)
Predict: 1.0
Else (feature 1 > 0.0)
Predict: 0.0
让我们以可视化的方式构建它,以便更好地理解:
我们不会在这里详细介绍,因为我们已经在之前的模型中做过了。我们将直接计算信息增益:0.44
正如你在这种情况下所看到的,信息增益为 0.44,是第一个模型的两倍多。
如果你看第二级节点,杂质为零。在这种情况下,这是很好的,因为我们在深度为 2 的情况下得到了它。想象一种情况,深度为 50。在那种情况下,决策树对训练数据效果很好,但对测试数据效果很差。这种情况被称为过拟合。
避免过拟合的一个解决方案是修剪。你将训练数据分成两组:训练集和验证集。你使用训练集训练模型。现在你用模型对验证集进行测试,逐渐移除左节点。如果移除叶节点(通常是单例节点,即只包含一个数据点)改善了模型的性能,那么这个叶节点就从模型中被修剪掉。
使用随机森林进行分类
有时一个决策树是不够的,所以会使用一组决策树来产生更强大的模型。这些被称为集成学习算法。集成学习算法不仅限于使用决策树作为基本模型。
集成学习算法中最受欢迎的是随机森林。在随机森林中,不是生长单一树,而是生长K棵树。每棵树都被赋予训练数据的一个随机子集S。更有趣的是,每棵树只使用特征的一个子集。在进行预测时,对树进行多数投票,这就成为了预测。
让我们用一个例子来解释这一点。目标是对一个给定的人做出预测,判断他/她的信用是好还是坏。
为了做到这一点,我们将提供带有标签的训练数据,也就是说,在这种情况下,一个带有特征和标签的人。现在我们不想创建特征偏差,所以我们将提供一个随机选择的特征集。提供一个随机选择的特征子集的另一个原因是,大多数真实世界的数据具有数百甚至数千个特征。例如,文本分类算法通常具有 50k-100k 个特征。
在这种情况下,为了给故事增添趣味,我们不会提供特征,而是会问不同的人为什么他们认为一个人信用好或坏。现在根据定义,不同的人暴露于一个人的不同特征(有时是重叠的),这给了我们与随机选择特征相同的功能。
我们的第一个例子是 Jack,他被贴上了“坏信用”的标签。我们将从 Jack 最喜欢的酒吧——大象酒吧的 Joey 开始。一个人能够推断为什么给定一个标签的唯一方法是通过问是/否的问题。让我们看看 Joey 说了什么:
Q1: Jack 是否慷慨地给小费?(特征:慷慨)
A: 不
Q2:杰克每次至少花 60 美元吗?(特征:挥霍)
A:是的
Q3:他是否倾向于在最小的挑衅下卷入酒吧斗殴?(特征:易怒)
A:是的
这就解释了为什么杰克信用不好。
现在我们问杰克的女朋友斯泰西:
Q1:我们一起出去玩时,杰克是否总是买单?(特征:慷慨)
A:不
Q2:杰克是否还我 500 美元?(特征:责任)
A:不
Q3:他是否有时为了炫耀而过度花钱?(特征:挥霍)
A:是的
这就解释了为什么杰克信用不好。
现在我们问杰克的好朋友乔治:
Q1:当杰克和我在我的公寓里玩时,他会自己清理吗?(特征:有组织)
A:不
Q2:杰克在我超级碗聚餐时是空手而来吗?(特征:关心)
A:是的
Q3:他是否曾经用“我忘了在家里带钱包”这个借口让我付他在餐馆的账单?(特征:责任)
A:是的
这就解释了为什么杰克信用不好。
现在我们谈谈信用良好的杰西卡。让我们问杰西卡的姐姐斯泰西:
Q1:每当我钱不够时,杰西卡是否会主动帮忙?(特征:慷慨)
A:是的
Q2:杰西卡是否按时支付账单?(特征:责任)
A:是的
Q3:杰西卡是否愿意帮我照顾孩子?(特征:关心)
A:是的
这就解释了为什么杰西卡信用良好。
现在我们问乔治,他碰巧是她的丈夫:
Q1:杰西卡是否保持房子整洁?(特征:有组织)
A:是的
Q2:她是否期望昂贵的礼物?(特征:挥霍)
A:不
Q3:当你忘记割草时,她会生气吗?(特征:易怒)
A:不
这就解释了为什么杰西卡信用良好。
现在让我们问大象酒吧的调酒师乔伊:
Q1:每当她和朋友一起来酒吧时,她是否大多是指定司机?(特征:负责)
A:是的
Q2:她是否总是带剩菜回家?(特征:挥霍)
A:是的
Q3:她是否慷慨地给小费?(特征:慷慨)
A:是的
随机森林的工作方式是在两个级别上进行随机选择:
-
数据的一个子集
-
一些特征的子集来分割数据
这两个子集可能会重叠。
在我们的例子中,我们有六个特征,我们将为每棵树分配三个特征。这样,我们有很大的机会会有重叠。
让我们将另外八个人添加到我们的训练数据集中:
| 名字 | 标签 | 慷慨 | 责任 | 关心 | 组织 | 挥霍 | 易怒 |
|---|---|---|---|---|---|---|---|
| 杰克 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
| 杰西卡 | 1 | 1 | 1 | 1 | 1 | 0 | 0 |
| 珍妮 | 0 | 0 | 0 | 1 | 0 | 1 | 1 |
| 瑞克 | 1 | 1 | 1 | 0 | 1 | 0 | 0 |
| 帕特 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
| 杰布:1 | 1 | 1 | 1 | 0 | 0 | 0 | |
| 杰伊 | 1 | 0 | 1 | 1 | 1 | 0 | 0 |
| 纳特 | 0 | 1 | 0 | 0 | 0 | 1 | 1 |
| 罗恩 | 1 | 0 | 1 | 1 | 1 | 0 | 0 |
| 马特 | 0 | 1 | 0 | 0 | 0 | 1 | 1 |
准备好了
让我们将创建的数据放入以下文件的libsvm格式中:
rf_libsvm_data.txt
0 5:1 6:1
1 1:1 2:1 3:1 4:1
0 3:1 5:1 6:1
1 1:1 2:1 4:1
0 5:1 6:1
1 1:1 2:1 3:1 4:1
0 1:1 5:1 6:1
1 2:1 3:1 4:1
0 1:1 5:1 6:1
现在将其上传到 HDFS:
$ hdfs dfs -put rf_libsvm_data.txt
如何做…
- 启动 Spark shell:
$ spark-shell
- 执行所需的导入:
scala> import org.apache.spark.mllib.tree.RandomForest
scala> import org.apache.spark.mllib.tree.configuration.Strategy
scala> import org.apache.spark.mllib.util.MLUtils
- 加载和解析数据:
scala> val data =
MLUtils.loadLibSVMFile(sc, "rf_libsvm_data.txt")
- 将数据分割成“训练”和“测试”数据集:
scala> val splits = data.randomSplit(Array(0.7, 0.3))
scala> val (trainingData, testData) = (splits(0), splits(1))
- 创建分类作为树策略(随机森林也支持回归):
scala> val treeStrategy = Strategy.defaultStrategy("Classification")
- 训练模型:
scala> val model = RandomForest.trainClassifier(trainingData,
treeStrategy, numTrees=3, featureSubsetStrategy="auto", seed = 12345)
- 在测试实例上评估模型并计算测试错误:
scala> val testErr = testData.map { point =>
val prediction = model.predict(point.features)
if (point.label == prediction) 1.0 else 0.0
}.mean()
scala> println("Test Error = " + testErr)
- 检查模型:
scala> println("Learned Random Forest:n" + model.toDebugString)
Learned Random Forest:nTreeEnsembleModel classifier with 3 trees
Tree 0:
If (feature 5 <= 0.0)
Predict: 1.0
Else (feature 5 > 0.0)
Predict: 0.0
Tree 1:
If (feature 3 <= 0.0)
Predict: 0.0
Else (feature 3 > 0.0)
Predict: 1.0
Tree 2:
If (feature 0 <= 0.0)
Predict: 0.0
Else (feature 0 > 0.0)
Predict: 1.0
它是如何工作的…
正如您在这个小例子中所看到的,三棵树使用了不同的特征。在具有数千个特征和训练数据的实际用例中,这种情况不会发生,但大多数树在如何看待特征和多数票的情况下会有所不同。请记住,在回归的情况下,树的平均值会得到最终值。
使用梯度提升树进行分类
另一个集成学习算法是梯度提升树(GBTs)。GBTs 一次训练一棵树,每棵新树都改进了先前训练树的缺点。
由于 GBTs 一次训练一棵树,所以它们可能比随机森林需要更长的时间。
准备好了
我们将使用前一个配方中使用的相同数据。
如何做…
- 启动 Spark shell:
$ spark-shell
- 执行所需的导入操作:
scala> import org.apache.spark.mllib.tree.GradientBoostedTrees
scala> import org.apache.spark.mllib.tree.configuration.BoostingStrategy
scala> import org.apache.spark.mllib.util.MLUtils
- 加载并解析数据:
scala> val data =
MLUtils.loadLibSVMFile(sc, "rf_libsvm_data.txt")
- 将数据分成“训练”和“测试”数据集:
scala> val splits = data.randomSplit(Array(0.7, 0.3))
scala> val (trainingData, testData) = (splits(0), splits(1))
- 创建一个分类作为增强策略,并将迭代次数设置为
3:
scala> val boostingStrategy =
BoostingStrategy.defaultParams("Classification")
scala> boostingStrategy.numIterations = 3
- 训练模型:
scala> val model = GradientBoostedTrees.train(trainingData, boostingStrategy)
- 在测试实例上评估模型并计算测试误差:
scala> val testErr = testData.map { point =>
val prediction = model.predict(point.features)
if (point.label == prediction) 1.0 else 0.0
}.mean()
scala> println("Test Error = " + testErr)
- 检查模型:
scala> println("Learned Random Forest:n" + model.toDebugString)
在这种情况下,模型的准确率为 0.9,低于我们在随机森林情况下得到的准确率。
使用朴素贝叶斯进行分类
让我们考虑使用机器学习构建电子邮件垃圾邮件过滤器。在这里,我们对两类感兴趣:垃圾邮件表示未经请求的消息,非垃圾邮件表示常规电子邮件:
第一个挑战是,当给定一封电子邮件时,我们如何将其表示为特征向量x。一封电子邮件只是一堆文本或一组单词(因此,这个问题领域属于更广泛的文本分类类别)。让我们用一个长度等于字典大小的特征向量来表示一封电子邮件。如果字典中的给定单词出现在电子邮件中,则值为 1;否则为 0。让我们构建一个表示内容为在线药店销售的电子邮件的向量:
该特征向量中的单词字典称为词汇表,向量的维度与词汇表的大小相同。如果词汇表大小为 10,000,则该特征向量中的可能值将为 210,000。
我们的目标是对y给定x的概率进行建模。为了对P(x|y)进行建模,我们将做出一个强烈的假设,即x是有条件独立的。这个假设被称为朴素贝叶斯假设,基于这个假设的算法被称为朴素贝叶斯分类器。
例如,对于y=1,表示垃圾邮件,出现“在线”和“药店”这两个词的概率是独立的。这是一个与现实无关的强烈假设,但在获得良好预测时效果非常好。
准备就绪
Spark 自带一个用于朴素贝叶斯的示例数据集。让我们将这个数据集加载到 HDFS 中:
$ hdfs dfs -put /opt/infoobjects/spark/data/mllib/sample_naive_bayes_data.txt
sample_naive_bayes_data.txt
如何做…
- 启动 Spark shell:
$ spark-shell
- 执行所需的导入操作:
scala> import org.apache.spark.mllib.classification.NaiveBayes
scala> import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.mllib.regression.LabeledPoint
- 将数据加载到 RDD 中:
scala> val data = sc.textFile("sample_naive_bayes_data.txt")
- 将数据解析为
LabeledPoint:
scala> val parsedData = data.map { line =>
val parts = line.split(',')
LabeledPoint(parts(0).toDouble, Vectors.dense(parts(1).split(' ').map(_.toDouble)))
}
- 将数据一分为二,分别放入“训练”和“测试”数据集中:
scala> val splits = parsedData.randomSplit(Array(0.5, 0.5), seed = 11L)
scala> val training = splits(0)
scala> val test = splits(1)
- 使用“训练”数据集训练模型:
val model = NaiveBayes.train(training, lambda = 1.0)
- 预测“测试”数据集的标签:
val predictionAndLabel = test.map(p => (model.predict(p.features), p.label))