🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
文章目录
RightOuterJoin and LeftOuterJoin
Spark SQL、Dataset 和 DataFrames API
Spark是一个统一的大数据处理框架,用于处理和分析大型数据集。Spark 在Scala, Python, Java, 和 R 中提供了高级 API,其中包含功能强大的库,包括用于机器学习的 MLlib、用于 SQL 支持的 Spark SQL、用于实时流的 Spark 流式处理和用于图形处理的 GraphX。第二Spark由Matei Zaharia在加州大学伯克利分校的AMPLab创立,后来捐赠给Apache软件基金会,于2014年2月24日成为顶级项目。第三第一个版本于2017年5月30日发布。
概述
《Spark》的开发是为了解决哈多普的原始数据处理框架MapReduce的局限性。Matei Zaharia看到了MapReduce在加州大学伯克利分校和Facebook(他在那里实习)的局限性,并试图创建一个更快,更通用,多用途的数据处理框架,可以处理迭代和交互式应用程序。在它提供了一个统一的平台(图 2-1),支持多种类型的工作负载,如流式处理、交互式、图形处理、机器学习和批处理。我们Spark 作业的运行速度比等效的 MapReduce 作业快许多倍,因为它具有快速的内存中功能和高级 DAG(有向无环图)执行引擎。Spark是用斯卡拉语编写的,因此它是Spark事实上的编程接口。我们将在本书中通篇使用 Scala。我们将在第7章中使用PySpark,即用于火花的Python API,用于分布式深度学习。
图 2-1Apache Spark ecosystem
Cluster Managers(集群管理器)
集群管理器管理和分配集群资源。Spark 支持随附于 Spark(独立调度程序)、YARN、Mesos 和 Kubernetes 附带的独立集群管理器。
Architecture(建筑)
在较高级别,Spark 将 Spark 应用程序任务的执行分布在群集节点上(图 2-2)。每个 Spark 应用程序在其驱动程序中都有一个 SparkContext 对象。SparkContext 表示与集群管理器的连接,集群管理器为 Spark 应用程序提供计算资源。连接到群集后,Spark 会获取工作线程节点上的执行程序。然后,Spark 将应用程序代码发送给执行程序。应用程序通常会运行一个或多个作业以响应 Spark 操作。然后,每个作业由 Spark 划分为阶段或任务的较小有向无环图 (DAG)。然后,每个任务将分发并发送到工作线程节点上的执行程序进行执行。
图 2-2Apache Spark体系结构
每个 Spark 应用程序都有自己的一组执行程序。由于来自不同应用程序的任务在不同的 JVM 中运行,因此 Spark 应用程序不会干扰另一个 Spark 应用程序。这也意味着 Spark 应用程序很难在不使用外部数据源(如 HDFS 或 S3)的情况下共享数据。使用堆外内存存储(如Tachyon(又名Alluxio)可以使数据共享更快,更轻松。我将在本章后面更详细地讨论阿卢克西奥。
执行Spark 应用程序
您可以使用交互式外壳程序(火花外壳程序或 pyspark)或提交应用程序(火花提交)来执行 Spark 应用程序。有些人更喜欢使用基于Web的交互式笔记本,如阿帕奇齐柏林飞艇和朱皮特与Spark进行交互。数据砖和云端等商业供应商也提供自己的交互式笔记本环境。我将在整个章节中使用火花壳。有两种部署模式可用于在具有集群管理器(如 YARN)的环境中启动 Spark 应用程序。
集群模式
在群集模式下,驱动程序在由 YARN 管理的应用程序主服务器内运行。客户端可以在不影响应用程序执行的情况下退出。要在集群模式下启动应用程序或火花外壳:
spark-shell --master yarn --deploy-mode cluster
spark-submit --class mypath.myClass --master yarn --deploy-mode cluster
客户端模式
在客户端模式下,驱动程序在客户端中运行。应用程序主服务器仅用于从 YARN 请求资源。要在客户端模式下启动应用程序或火花外壳,
spark-shell --master yarn --deploy-mode client
spark-submit --class mypath.myClass --master yarn --deploy-mode client
spark-shell简介
通常使用交互式 shell 进行即席数据分析或浏览。它也是学习火花 API 的好工具。火花的交互式外壳在 Spark或Python中可用。在以下示例中,我们将创建城市的 RDD,并将它们全部转换为大写。当您启动火花外壳时,将自动创建一个名为“spark”的 SparkSession,如清单 2-1 所示。
spark-shell
Spark context Web UI available at http://10.0.2.15:4041
Spark context available as 'sc' (master = local[*], app id = local-1574144576837).
Spark session available as 'spark'.
Welcome to
____ __
/ __/__ ___ _____/ /__
_\ \/ _ \/ _ `/ __/ '_/
/___/ .__/\_,_/_/ /_/\_\ version 2.4.4
/_/
Using Scala version 2.11.12 (OpenJDK 64-Bit Server VM, Java 1.8.0_212)
Type in expressions to have them evaluated.
Type :help for more information.
scala>val myCities = sc.parallelize(List(
"tokyo",
"new york",
"sydney",
"san francisco"))
scala>val uCities = myCities.map {x =>x.toUpperCase}
scala>uCities.collect.foreach(println)
TOKYO
NEW YORK
SYDNEY
SAN FRANCISCO
Listing 2-1Introduction to spark-shell
SparkSession
如图 2-2 所示,SparkContext 支持访问所有 Spark 特性和功能。该驱动程序使用 SparkContext 访问其他上下文,如流式处理上下文、SQL 上下文和配置项上下文。从 Spark 2.0 开始,火花会话提供了一个与 Spark 交互的单一入口点。通过 Spark1.x 中的 Spark 上下文、SQL 上下文、配置上下文和流式处理上下文提供的所有功能现在都可以通过 SparkSession 进行访问。七您可能仍然会遇到在 Spark 1.x 中编写的代码。在 Spark 1.x 中,你会写出这样的东西。
val sparkConf = new SparkConf().setAppName("MyApp").setMaster("local")
val sc = new SparkContext(sparkConf).set("spark.executor.cores", "4")
val sqlContext = new org.apache.spark.sql.SQLContext(sc)
在 Spark 2.x 中,您不必显式创建火花组件、SparkContext 或 SQLContext,因为它们的所有功能都已包含在 SparkSession 中。
val spark = SparkSession.
builder().
appName("MyApp").
config("spark.executor.cores", "4").
getOrCreate()
弹性分布式数据集 (RDD)
RDD 是跨群集中的一个或多个节点分区的对象的弹性不可变分布式集合。RDD 可以通过两种类型的操作并行处理和操作:转换和操作。
注意RDD 是 Spark 1.x 中 Spark 的主要编程接口,数据集已取代 RDD 成为从 Spark 2.0 开始的主 API。建议用户从RDD切换到数据集/数据帧,因为编程接口更丰富,性能更好。我将在本章的后面部分讨论数据集和数据帧。
Creating an RDD
创建 RDD 非常简单。您可以从现有的 Scala 集合或通过读取存储在 HDFS 或 S3 中的外部文件来创建 RDD。
并行化
并行化从Scala集合创建 RDD。
val data = (1 to 5).toList
val rdd = sc.parallelize(data)
val cities = sc.parallelize(List("tokyo","new york","sydney","san francisco"))
textFile
文本文件从存储在 HDFS 或 S3 中的文本文件创建 RDD。
val rdd = sc.textFile("hdfs://master01:9000/files/mydirectory")
val rdd = sc.textFile("s3a://mybucket/files/mydata.csv")
请注意,RDD 是不可变的。数据转换会生成另一个 RDD,而不是修改当前的 RDD。RDD操作可分为两类:转换和操作。
Transformations(转换)
转换是创建新的 RDD 的操作。我描述了一些最常见的转换。有关完整列表,请参阅联机 Spark 文档。
Map
映射对 RDD 中的每个元素执行一个函数。它将创建并返回结果的新 RDD。地图的返回类型不一定必须与原始 RDD 的类型相同。
val cities = sc.parallelize(List("tokyo","new york","paris","san francisco"))
val upperCaseCities = myCities.map {x =>x.toUpperCase}
upperCaseCities.collect.foreach(println)
TOKYO
NEW YORK
PARIS
SAN FRANCISCO
让我们展示另一个地图示例。
val lines = sc.parallelize(List("Michael Jordan", "iPhone"))
val words = lines.map(line =>line.split(" "))
words.collect
res2: Array[Array[String]] = Array(Array(Michael, Jordan), Array(iPhone))
FlatMap
平面映射对 RDD 中的每个元素执行一个函数,然后拼合结果。
val lines = sc.parallelize(List("Michael Jordan", "iPhone"))
val words = lines.flatMap(line =>line.split(" "))
words.collect
res3: Array[String] = Array(Michael, Jordan, iPhone)
Filter(滤波器)
筛选器返回仅包含与指定条件匹配的元素的 RDD。
val lines = sc.parallelize(List("Michael Jordan", "iPhone","Michael Corleone"))
val words = lines.map(line =>line.split(" "))
val results = words.filter(w =>w.contains("Michael"))
results.collect
res9: Array[Array[String]] = Array(Array(Michael, Jordan), Array(Michael, Corleone))
Distinct
非重复值仅返回非重复值。
val cities1 = sc.parallelize(List("tokyo","tokyo","paris","sydney"))
val cities2 = sc.parallelize(List("perth","tokyo","canberra","sydney"))
val cities3 = cities1.union(cities2)
cities3.distinct.collect.foreach(println)
sydney
perth
canberra
tokyo
paris
ReduceByKey
使用指定的化简函数将值与同一键组合在一起。
val pairRDD = sc.parallelize(List(("a", 1), ("b",2), ("c",3), ("a", 30), ("b",25), ("a",20)))
val sumRDD = pairRDD.reduceByKey((x,y) =>x+y)
sumRDD.collect
res15: Array[(String, Int)] = Array((b,27), (a,51), (c,3))
Keys
密钥返回仅包含密钥的 RDD。
val rdd = sc.parallelize(List(("a", "Larry"), ("b", "Curly"), ("c", "Moe")))
val keys = rdd.keys
keys.collect.foreach(println)
a
b
c
Values
值返回仅包含值的 RDD。
val rdd = sc.parallelize(List(("a", "Larry"), ("b", "Curly"), ("c", "Moe")))
val value = rdd.values
value.collect.foreach(println)
Larry
Curly
Moe
Inner Join
内部连接根据连接谓词返回来自两个 RDD 的所有元素的 RDD。
val data = Array((100,"Jim Hernandez"), (101,"Shane King"))
val employees = sc.parallelize(data)
val data2 = Array((100,"Glendale"), (101,"Burbank"))
val cities = sc.parallelize(data2)
val data3 = Array((100,"CA"), (101,"CA"), (102,"NY"))
val states = sc.parallelize(data3)
val record = employees.join(cities).join(states)
record.collect.foreach(println)
(100,((Jim Hernandez,Glendale),CA))
(101,((Shane King,Burbank),CA))
RightOuterJoin and LeftOuterJoin
右外联从右侧 RDD 返回元素的 RDD,即使左侧 RDD 上没有匹配的行也是如此。左外连接等效于右外联,列的顺序不同。
val record = employees.join(cities).rightOuterJoin(states)
record.collect.foreach(println)
(100,(Some((Jim Hernandez,Glendale)),CA))
(102,(None,NY))
(101,(Some((Shane King,Burbank)),CA))
Union
Union返回一个 RDD,其中包含两个或多个 RDD 的组合。
val data = Array((103,"Mark Choi","Torrance","CA"), (104,"Janet Reyes","RollingHills","CA"))
val employees = sc.parallelize(data)
val data = Array((105,"Lester Cruz","VanNuys","CA"), (106,"John White","Inglewood","CA"))
val employees2 = sc.parallelize(data)
val rdd = sc.union([employees, employees2])
rdd.collect.foreach(println)
(103,MarkChoi,Torrance,CA)
(104,JanetReyes,RollingHills,CA)
(105,LesterCruz,VanNuys,CA)
(106,JohnWhite,Inglewood,CA)
Subtract(减去)
减去返回一个 RDD,该 RDD 仅包含第一个 RDD 中的元素。
val data = Array((103,"Mark Choi","Torrance","CA"), (104,"Janet Reyes","Rolling Hills","CA"),(105,"Lester Cruz","Van Nuys","CA"))
val rdd = sc.parallelize(data)
val data2 = Array((103,"Mark Choi","Torrance","CA"))
val rdd2 = sc.parallelize(data2)
val employees = rdd.subtract(rdd2)
employees.collect.foreach(println)
(105,LesterCruz,Van Nuys,CA)
(104,JanetReyes,Rolling Hills,CA)
Coalesce(合并)
合并可减少 RDD 中的分区数。您可能希望在对大型 RDD 执行筛选后使用合并。虽然过滤减少了新RDD消耗的数据量,但它继承了原始RDD的分区数。如果新的RDD明显小于原始RDD,则它可能具有数百或数千个小分区,这可能会导致性能问题。
当您希望在写入HDFS时减少Spark生成的文件数量时,合并也很有用,可以防止可怕的“小文件”问题。每个分区都作为单独的文件写入 HDFS。请注意,使用合并时可能会遇到性能问题,因为您在写入 HDFS 时有效地降低了并行度。如果发生这种情况,请尝试增加分区数。在以下示例中,我们只将一个 Parquet 文件写入 HDFS。
df.coalesce(1).write.mode("append").parquet("/user/hive/warehouse/Mytable")
Repartition(重新分区)
重新分区可以减少和增加RDD中的分区数。在减少分区时,通常会使用合并,因为它比重新分区更有效。增加分区数对于在写入 HDFS 时提高并行度非常有用。在以下示例中,我们将六个 Parquet 文件写入 HDFS。
df.repartition(6).write.mode("append").parquet("/user/hive/warehouse/Mytable")
注意:合并通常比重新分区快。重新分区将执行完全随机播放,创建新分区并在工作线程节点之间平均分布数据。合并可最大限度地减少数据移动,并通过使用现有分区避免完全随机播放。
Actions(行动)
操作是向驱动程序返回值的 RDD 操作。我列出了一些最常见的操作。有关操作的完整列表,请参阅联机 Spark 文档。
Collect(收集)
Collect 将整个数据集作为数组返回到驱动程序。
val myCities = sc.parallelize(List("tokyo","new york","paris","san francisco"))
myCities.collect
res2: Array[String] = Array(tokyo, new york, paris, san francisco)
Count(计数)
Count 返回数据集中元素数的计数。
val myCities = sc.parallelize(List("tokyo","new york","paris","san francisco"))
myCities.count
res3: Long = 4
Take(拿)
Take 以数组的形式返回数据集的前 n 个元素。
val myCities = sc.parallelize(List("tokyo","new york","paris","san francisco"))
myCities.take(2)
res4: Array[String] = Array(tokyo, new york)
Foreach(前期)
Foreach 对数据集的每个元素执行一个函数。
val myCities = sc.parallelize(List("tokyo","new york","paris","san francisco"))
myCities.collect.foreach(println)
tokyo
newyork
paris
sanFrancisco
Lazy Evaluation
Spark 支持惰性求值,这对于大数据处理至关重要。Spark 中的所有转换都进行了懒惰的评估。Spark 不会立即执行转换。您可以继续定义更多转换。当您最终想要最终结果时,您将执行一个操作,这将导致执行转换。
Caching(缓存)
默认情况下,每次运行操作时都会重新执行每个转换。您可以使用缓存或持久化方法将 RDD 缓存在内存中,以避免多次重新执行转换。
Accumulator(蓄电池)
累加器是仅“添加”到的变量。它们通常用于实现计数器。在此示例中,我使用累加器将数组的元素相加:
val accum = sc.longAccumulator("Accumulator 01")
sc.parallelize(Array(10, 20, 30, 40)).foreach(x =>accum.add(x))
accum.value
res2: Long = 100
Broadcast Variable(广播变量)
广播变量是存储在每个节点内存中的只读变量。Spark 使用高速广播算法来减少复制广播变量的网络延迟。使用广播变量不是将数据存储在慢速存储引擎(如 HDFS 或 S3)中,而是在每个节点上存储数据集副本的更快方法。
val broadcastVar = sc.broadcast(Array(10, 20, 30))
broadcastVar.value
res0: Array[Int] = Array(10, 20, 30)
Spark SQL、Dataset 和 DataFrames API
开发 Spark SQL 是为了简化处理和分析结构化数据的过程。数据集类似于RDD,因为它支持强类型,但在引擎盖下,数据集有一个更高效的引擎。从 Spark 2.0 开始,数据集 API 现在是主要的编程接口。数据帧只是一个具有命名列的数据集,类似于关系表。Spark SQL 和数据帧共同为处理和分析结构化数据提供了强大的编程接口。下面是有关如何使用数据帧 API 的快速示例。
val jsonDF = spark.read.json("/jsondata/customers.json")
jsonDF.show
+---+------+--------------+-----+------+-----+
|age| city| name|state|userid| zip|
+---+------+--------------+-----+------+-----+
| 35|Frisco| Jonathan West| TX| 200|75034|
| 28|Dallas|Andrea Foreman| TX| 201|75001|
| 69| Plano| Kirsten Jung| TX| 202|75025|
| 52| Allen|Jessica Nguyen| TX| 203|75002|
+---+------+--------------+-----+------+-----+
jsonDF.select ("age","city").show
+---+------+
|age| city|
+---+------+
| 35|Frisco|
| 28|Dallas|
| 69| Plano|
| 52| Allen|
+---+------+
jsonDF.filter($"userid" < 202).show()
+---+------+--------------+-----+------+-----+
|age| city| name|state|userid| zip|
+---+------+--------------+-----+------+-----+
| 35|Frisco| Jonathan West| TX| 200|75034|
| 28|Dallas|Andrea Foreman| TX| 201|75001|
+---+------+--------------+-----+------+-----+
jsonDF.createOrReplaceTempView("jsonDF")
val df = spark.sql("SELECT userid, zip FROM jsonDF")
df.show
+------+-----+
|userid| zip|
+------+-----+
| 200|75034|
| 201|75001|
| 202|75025|
| 203|75002|
+------+-----+
说明数据帧和数据集 API 已在 Spark 2.0 中统一。数据帧现在只是行数据集的类型别名,其中行是通用的非类型化对象。相反,数据集是强类型对象的集合 数据集[T].斯卡拉支持强类型和无类型化API,而在Java中,数据集[T]是主要的抽象。数据帧是 R 和 Python 的主要编程接口,因为它缺乏对编译时类型安全性的支持。
Spark 数据源
读取和写入不同的文件格式和数据源是最常见的数据处理任务之一。我们将在示例中同时使用 RDD 和数据帧 API。
.CSV
Spark 为您提供了从 CSV 文件中读取数据的不同方法。您可以先将数据读入RDD,然后将其转换为数据帧。
val dataRDD = sc.textFile("/sparkdata/customerdata.csv")
val parsedRDD = dataRDD.map{_.split(",")}
case class CustomerData(customerid: Int, name: String, city: String, state: String, zip: String)
val dataDF = parsedRDD.map{ a =>CustomerData (a(0).toInt, a(1).toString, a(2).toString,a(3).toString,a(4).toString) }.toDF
Starting in Spark 2.0, the CSV connector is already built-in.
val dataDF = spark.read.format("csv")
.option("header", "true")
.load("/sparkdata/customerdata.csv")
.XML
数据砖有一个火花XML包,可以很容易地读取XML数据。
cat users.xml
<userid>100</userid><name>Wendell Ryan</name><city>San Diego</city><state>CA</state><zip>92102</zip>
<userid>101</userid><name>Alicia Thompson</name><city>Berkeley</city><state>CA</state><zip>94705</zip>
<userid>102</userid><name>Felipe Drummond</name><city>Palo Alto</city><state>CA</state><zip>94301</zip>
<userid>103</userid><name>Teresa Levine</name><city>Walnut Creek</city><state>CA</state><zip>94507</zip>
hadoop fs -mkdir /xmldata
hadoop fs -put users.xml /xmldata
spark-shell --packages com.databricks:spark-xml_2.10:0.4.1
Create a DataFrame using Spark XML. In this example, we specify the row tag and the path in HDFS where the XML file is located.
import com.databricks.spark.xml._
val xmlDF = spark.read
.option("rowTag", "user")
.xml("/xmldata/users.xml");
xmlDF: org.apache.spark.sql.DataFrame = [city: string, name: string, state: string, userid: bigint, zip: bigint]
Let’s also take a look at the data.
xmlDF.show
+------------+---------------+-----+------+-----+
| city| name|state|userid| zip|
+------------+---------------+-----+------+-----+
| San Diego| Wendell Ryan| CA| 100|92102|
| Berkeley|Alicia Thompson| CA| 101|94705|
| Palo Alto|Felipe Drummond| CA| 102|94301|
|Walnut Creek| Teresa Levine| CA| 103|94507|
+------------+---------------+-----+------+-----+
JSON
我们将创建一个 JSON 文件作为此示例的示例数据。确保该文件位于 HDFS 中名为 /json 数据的文件夹中。
cat users.json
{"userid": 200, "name": "Jonathan West", "city":"Frisco", "state":"TX", "zip": "75034", "age":35}
{"userid": 201, "name": "Andrea Foreman", "city":"Dallas", "state":"TX", "zip": "75001", "age":28}
{"userid": 202, "name": "Kirsten Jung", "city":"Plano", "state":"TX", "zip": "75025", "age":69}
{"userid": 203, "name": "Jessica Nguyen", "city":"Allen", "state":"TX", "zip": "75002", "age":52}
Create a DataFrame from the JSON file.
val jsonDF = spark.read.json("/jsondata/users.json")
jsonDF: org.apache.spark.sql.DataFrame = [age: bigint, city: string, name: string, state: string, userid: bigint, zip: string]
Check the data.
jsonDF.show
+---+------+--------------+-----+------+-----+
|age| city| name|state|userid| zip|
+---+------+--------------+-----+------+-----+
| 35|Frisco| Jonathan West| TX| 200|75034|
| 28|Dallas|Andrea Foreman| TX| 201|75001|
| 69| Plano| Kirsten Jung| TX| 202|75025|
| 52| Allen|Jessica Nguyen| TX| 203|75002|
+---+------+--------------+-----+------+-----+
关系数据库和 MPP 数据库
在此示例中,我们使用 MySQL,但也支持其他关系数据库和 MPP 引擎,如甲骨文、雪花、红移、黑斑羚、普雷斯托和 Azure DW。通常,只要关系数据库具有 JDBC 驱动程序,就应该可以从 Spark 访问它。性能取决于 JDBC 驱动程序对批处理操作的支持。有关更多详细信息,请查看 JDBC 驱动程序的文档。
mysql -u root -pmypassword
create databases salesdb;
use salesdb;
create table customers (
customerid INT,
name VARCHAR(100),
city VARCHAR(100),
state CHAR(3),
zip CHAR(5));
spark-shell --driver-class-path mysql-connector-java-5.1.40-bin.jar
启动 spark-shell.
将 CSV 文件读入 RDD 并将其转换为数据帧。
val dataRDD = sc.textFile("/home/hadoop/test.csv")
val parsedRDD = dataRDD.map{_.split(",")}
case class CustomerData(customerid: Int, name: String, city: String, state: String, zip: String)
val dataDF = parsedRDD.map{ a =>CustomerData (a(0).toInt, a(1).toString, a(2).toString,a(3).toString,a(4).toString) }.toDF
将数据框注册为临时表,以便我们可以对其运行 SQL 查询。
dataDF.createOrReplaceTempView("dataDF")
让我们设置连接属性。
val jdbcUsername = "myuser"
val jdbcPassword = "mypass"
val jdbcHostname = "10.0.1.112"
val jdbcPort = 3306
val jdbcDatabase ="salesdb"
val jdbcrewriteBatchedStatements = "true"
val jdbcUrl = s"jdbc:mysql://${jdbcHostname}:${jdbcPort}/${jdbcDatabase}?user=${jdbcUsername}&password=${jdbcPassword}&rewriteBatchedStatements=${jdbcrewriteBatchedStatements}"
val connectionProperties = new java.util.Properties()
这将使我们能够指定正确的保存模式 - 追加,覆盖等。
import org.apache.spark.sql.SaveMode
将 SELECT 语句返回的数据插入到存储在 MySQL salesdb 数据库中的客户表中。
spark.sql("select * from dataDF")
.write
.mode(SaveMode.Append)
.jdbc(jdbcUrl, "customers", connectionProperties)
让我们使用 JDBC 读取一个表。让我们确保用一些测试数据填充MySQL中的用户表。
mysql -u root -pmypassword
use salesdb;
describe users;
+--------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+--------------+------+-----+---------+-------+
| userid | bigint(20) | YES | | NULL | |
| name | varchar(100) | YES | | NULL | |
| city | varchar(100) | YES | | NULL | |
| state | char(3) | YES | | NULL | |
| zip | char(5) | YES | | NULL | |
| age | tinyint(4) | YES | | NULL | |
+--------+--------------+------+-----+---------+-------+
select * from users;
Empty set (0.00 sec)
insert into users values (300,'Fred Stevens','Torrance','CA',90503,23);
insert into users values (301,'Nancy Gibbs','Valencia','CA',91354,49);
insert into users values (302,'Randy Park','Manhattan Beach','CA',90267,21);
insert into users values (303,'Victoria Loma','Rolling Hills','CA',90274,75);
select * from users;
+--------+---------------+-----------------+-------+-------+------+
| userid | name | city | state | zip | age |
+--------+---------------+-----------------+-------+-------+------+
| 300 | Fred Stevens | Torrance | CA | 90503 | 23 |
| 301 | Nancy Gibbs | Valencia | CA | 91354 | 49 |
| 302 | Randy Park | Manhattan Beach | CA | 90267 | 21 |
| 303 | Victoria Loma | Rolling Hills | CA | 90274 | 75 |
+--------+---------------+-----------------+-------+-------+------+
spark-shell --driver-class-path mysql-connector-java-5.1.40-bin.jar --jars mysql-connector-java-5.1.40-bin.jar
让我们设置 jdbcurl 和连接属性。
val jdbcURL = s"jdbc:mysql://10.0.1.101:3306/salesdb?user=myuser&password=mypass"
val connectionProperties = new java.util.Properties()
我们可以从整个表创建数据帧。
val df = spark.read.jdbc(jdbcURL, "users", connectionProperties)
df.show
+------+-------------+---------------+-----+-----+---+
|userid| name| city|state| zip|age|
+------+-------------+---------------+-----+-----+---+
| 300| Fred Stevens| Torrance| CA|90503| 23|
| 301| Nancy Gibbs| Valencia| CA|91354| 49|
| 302| Randy Park|Manhattan Beach| CA|90267| 21|
| 303|Victoria Loma| Rolling Hills| CA|90274| 75|
+------+-------------+---------------+-----+-----+---+
Parquet
阅读和写入Parquet很简单。
val df = spark.read.load("/sparkdata/employees.parquet")
df.select("id","firstname","lastname","salary")
.write
.format("parquet")
.save("/sparkdata/myData.parquet")
You can run SELECT statements on Parquet files directly.
val df = spark.sql("SELECT * FROM parquet.`/sparkdata/myData.parquet`")
HBase
有多种方法可以从星火访问 HBase。例如,可以使用“存储”数据集将数据写入 HBase。启动 HBase 外壳。
创建一个 HBase 表并用测试数据填充它。
hbase shell
create 'users', 'cf1'
启动spark-shell.
spark-shell
val hconf = HBaseConfiguration.create()
val jobConf = new JobConf(hconf, this.getClass)
jobConf.setOutputFormat(classOf[TableOutputFormat])
jobConf.set(TableOutputFormat.OUTPUT_TABLE,"users")
val num = sc.parallelize(List(1,2,3,4,5,6))
val theRDD = num.filter.map(x=>{
val rowkey = "row" + x
val put = new Put(Bytes.toBytes(rowkey))
put.add(Bytes.toBytes("cf1"), Bytes.toBytes("fname"), Bytes.toBytes("my fname" + x))
(newImmutableBytesWritable, put)
})
theRDD.saveAsHadoopDataset(jobConf)
您还可以使用来自 Spark 的 HBase 客户端 API 将数据读取和写入 HBase。如前所述,斯卡拉可以访问所有 Java 库。
启动 HBase 外壳。创建另一个 HBase 表,并用测试数据填充它。
hbase shell
create 'employees', 'cf1'
put 'employees','400','cf1:name', 'Patrick Montalban'
put 'employees','400','cf1:city', 'Los Angeles'
put 'employees','400','cf1:state', 'CA'
put 'employees','400','cf1:zip', '90010'
put 'employees','400','cf1:age', '71'
put 'employees','401','cf1:name', 'Jillian Collins'
put 'employees','401','cf1:city', 'Santa Monica'
put 'employees','401','cf1:state', 'CA'
put 'employees','401','cf1:zip', '90402'
put 'employees','401','cf1:age', '45'
put 'employees','402','cf1:name', 'Robert Sarkisian'
put 'employees','402','cf1:city', 'Glendale'
put 'employees','402','cf1:state', 'CA'
put 'employees','402','cf1:zip', '91204'
put 'employees','402','cf1:age', '29'
put 'employees','403','cf1:name', 'Warren Porcaro'
put 'employees','403','cf1:city', 'Burbank'
put 'employees','403','cf1:state', 'CA'
put 'employees','403','cf1:zip', '91523'
put 'employees','403','cf1:age', '62'
让我们验证数据是否已成功插入到 HBase 表中。
scan 'employees'
ROW COLUMN+CELL
400 column=cf1:age, timestamp=1493105325812, value=71
400 column=cf1:city, timestamp=1493105325691, value=Los Angeles
400 column=cf1:name, timestamp=1493105325644, value=Patrick Montalban
400 column=cf1:state, timestamp=1493105325738, value=CA
400 column=cf1:zip, timestamp=1493105325789, value=90010
401 column=cf1:age, timestamp=1493105334417, value=45
401 column=cf1:city, timestamp=1493105333126, value=Santa Monica
401 column=cf1:name, timestamp=1493105333050, value=Jillian Collins
401 column=cf1:state, timestamp=1493105333145, value=CA
401 column=cf1:zip, timestamp=1493105333165, value=90402
402 column=cf1:age, timestamp=1493105346254, value=29
402 column=cf1:city, timestamp=1493105345053, value=Glendale
402 column=cf1:name, timestamp=1493105344979, value=Robert Sarkisian
402 column=cf1:state, timestamp=1493105345074, value=CA
402 column=cf1:zip, timestamp=1493105345093, value=91204
403 column=cf1:age, timestamp=1493105353650, value=62
403 column=cf1:city, timestamp=1493105352467, value=Burbank
403 column=cf1:name, timestamp=1493105352445, value=Warren Porcaro
403 column=cf1:state, timestamp=1493105352513, value=CA
403 column=cf1:zip, timestamp=1493105352549, value=91523
启动spark-shell.
spark-shell
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.{HBaseConfiguration, HTableDescriptor}
import org.apache.hadoop.hbase.client.HBaseAdmin
import org.apache.hadoop.hbase.mapreduce.TableInputFormat
import org.apache.hadoop.hbase.HColumnDescriptor
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;
val configuration = HBaseConfiguration.create()
指定 HBase 表和行键。
val table = new HTable(configuration, "employees");
val g = new Get(Bytes.toBytes("401"))
val result = table.get(g);
从表中提取值。
val val2 = result.getValue(Bytes.toBytes("cf1"),Bytes.toBytes("name"));
val val3 = result.getValue(Bytes.toBytes("cf1"),Bytes.toBytes("city"));
val val4 = result.getValue(Bytes.toBytes("cf1"),Bytes.toBytes("state"));
val val5 = result.getValue(Bytes.toBytes("cf1"),Bytes.toBytes("zip"));
val val6 = result.getValue(Bytes.toBytes("cf1"),Bytes.toBytes("age"));
将值转换为适当的数据类型。
val id = Bytes.toString(result.getRow())
val name = Bytes.toString(val2);
val city = Bytes.toString(val3);
val state = Bytes.toString(val4);
val zip = Bytes.toString(val5);
val age = Bytes.toShort(val6);
打印值。
println(" employee id: " + id + " name: " + name + " city: " + city + " state: " + state + " zip: " + zip + " age: " + age);
employee id: 401 name: Jillian Collins city: Santa Monica state: CA zip: 90402 age: 13365
让我们使用 HBase API 写入 HBase。
val configuration = HBaseConfiguration.create()
val table = new HTable(configuration, "employees");
指定新的行键。
val p = new Put(new String("404").getBytes());
用新值填充单元格。
p.add("cf1".getBytes(), "name".getBytes(), new String("Denise Shulman").getBytes());
p.add("cf1".getBytes(), "city".getBytes(), new String("La Jolla").getBytes());
p.add("cf1".getBytes(), "state".getBytes(), new String("CA").getBytes());
p.add("cf1".getBytes(), "zip".getBytes(), new String("92093").getBytes());
p.add("cf1".getBytes(), "age".getBytes(), new String("56").getBytes());
写入 H 库表。
table.put(p);
table.close();
确认这些值已成功插入到 HBase 表中。
hbase shell
scan 'employees'
ROW COLUMN+CELL
400 column=cf1:age, timestamp=1493105325812, value=71
400 column=cf1:city, timestamp=1493105325691, value=Los Angeles
400 column=cf1:name, timestamp=1493105325644, value=Patrick Montalban
400 column=cf1:state, timestamp=1493105325738, value=CA
400 column=cf1:zip, timestamp=1493105325789, value=90010
401 column=cf1:age, timestamp=1493105334417, value=45
401 column=cf1:city, timestamp=1493105333126, value=Santa Monica
401 column=cf1:name, timestamp=1493105333050, value=Jillian Collins
401 column=cf1:state, timestamp=1493105333145, value=CA
401 column=cf1:zip, timestamp=1493105333165, value=90402
402 column=cf1:age, timestamp=1493105346254, value=29
402 column=cf1:city, timestamp=1493105345053, value=Glendale
402 column=cf1:name, timestamp=1493105344979, value=Robert Sarkisian
402 column=cf1:state, timestamp=1493105345074, value=CA
402 column=cf1:zip, timestamp=1493105345093, value=91204
403 column=cf1:age, timestamp=1493105353650, value=62
403 column=cf1:city, timestamp=1493105352467, value=Burbank
403 column=cf1:name, timestamp=1493105352445, value=Warren Porcaro
403 column=cf1:state, timestamp=1493105352513, value=CA
403 column=cf1:zip, timestamp=1493105352549, value=91523
404 column=cf1:age, timestamp=1493123890714, value=56
404 column=cf1:city, timestamp=1493123890714, value=La Jolla
404 column=cf1:name, timestamp=1493123890714, value=Denise Shulman
404 column=cf1:state, timestamp=1493123890714, value=CA
404 column=cf1:zip, timestamp=1493123890714, value=92093
虽然通常较慢,但您也可以通过SQL查询引擎(如黑斑羚或普雷斯托)访问HBase。
Amazon S3
Amazon S3 是一种常用的对象存储,经常用作瞬态集群的数据存储。它也是用于备份和冷数据的经济高效的存储。从 S3 读取数据就像从 HDFS 或任何其他文件系统读取数据一样。
从亚马逊 S3 读取 CSV 文件。确保您已配置 S3 凭证。
val myCSV = sc.textFile("s3a://mydata/customers.csv")
将 CSV 数据映射到 RDD。
import org.apache.spark.sql.Row
val myRDD = myCSV.map(_.split(',')).map(e ⇒ Row(r(0).trim.toInt, r(1), r(2).trim.toInt, r(3)))
创建架构。
import org.apache.spark.sql.types.{StructType, StructField, StringType, IntegerType};
val mySchema = StructType(Array(
StructField("customerid",IntegerType,false),
StructField("customername",StringType,false),
StructField("age",IntegerType,false),
StructField("city",StringType,false)))
val myDF = spark.createDataFrame(myRDD, mySchema)
Solr
您可以使用SolrJ从火花与索尔进行交互。
import java.net.MalformedURLException;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrServer;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocumentList;
val solr = new HttpSolrServer("http://master02:8983/solr/mycollection");
val query = new SolrQuery();
query.setQuery("*:*");
query.addFilterQuery("userid:3");
query.setFields("userid","name","age","city");
query.setStart(0);
query.set("defType", "edismax");
val response = solr.query(query);
val results = response.getResults();
println(results);
从 Spark 访问 Solr 集合的一种更简单的方法是通过火花 solr 包。清醒工场启动了火花索拉项目,以提供火花索尔集成。九与SolrJ相比,使用火花索拉要容易得多,功能也强大得多,允许您从Solr集合创建数据帧。
首先从 spark-shell导入 JAR 文件。
spark-shell --jars spark-solr-3.0.1-shaded.jar
指定集合和连接信息。
val options = Map( "collection" -> "mycollection","zkhost" -> "{ master02:8983/solr}")
创建数据帧。
val solrDF = spark.read.format("solr")
.options(options)
.load
Microsoft Excel
虽然从Spark访问Excel电子表格是我通常不推荐的,但某些用例需要该功能。一家名为 Crealytics 的公司开发了一个用于与 Excel 交互的 Spark 插件。该库需要火花 2.x。可以使用 --包命令行选项添加包。
spark-shell --packages com.crealytics:spark-excel_2.11:0.9.12
从 Excel 工作表创建数据帧。
val ExcelDF = spark.read
.format("com.crealytics.spark.excel")
.option("sheetName", "sheet1")
.option("useHeader", "true")
.option("inferSchema", "true")
.option("treatEmptyValuesAsNulls", "true")
.load("budget.xlsx")
Write a DataFrame to an Excel worksheet.
ExcelDF2.write
.format("com.crealytics.spark.excel")
.option("sheetName", "sheet1")
.option("useHeader", "true")
.mode("overwrite")
.save("budget2.xlsx")
You can find more details from their GitHub page: github.com/crealytics.
Secure FTP(安全 FTP)
从 SFTP 服务器下载文件并将其写入数据帧也是一个常见的请求。弹簧ML提供了一个火花SFTP连接器库。该库需要 Spark 2.x,并利用 jsch,这是 SSH2 的 Java 实现。读取和写入 SFTP 服务器将作为单个进程执行。
spark-shell --packages com.springml:spark-sftp_2.11:1.1.
从 SFTP 服务器中的文件创建数据帧。
val sftpDF = spark.read.
format("com.springml.spark.sftp").
option("host", "sftpserver.com").
option("username", "myusername").
option("password", "mypassword").
option("inferSchema", "true").
option("fileType", "csv").
option("delimiter", ",").
load("/myftp/myfile.csv")
将数据帧作为 CSV 文件写入 FTP 服务器。
sftpDF2.write.
format("com.springml.spark.sftp").
option("host", "sftpserver.com").
option("username", "myusername").
option("password", "mypassword").
option("fileType", "csv").
option("delimiter", ",").
save("/myftp/myfile.csv")
您可以从他们的GitHub页面找到更多详细信息:github.com/springml/spark-sftp。
简介Spark MLlib
机器学习是 Spark 的主要应用之一。Spark MLlib 包括用于回归、分类、聚类、协同过滤和频繁模式挖掘的常用机器学习算法。它还提供了一组广泛的功能,用于构建管道、模型选择和调整以及功能选择、提取和转换。
Spark MLlib算法
Spark MLlib包含大量用于各种任务的机器学习算法。我们将在后续章节中介绍其中的大多数内容。
分类
- 逻辑回归(二项式和多项式)
- 决策树
- 随机森林
- 梯度提升树
- 多层感知器
- 线性支持向量机
- 朴素贝叶斯
- One-vs.-Rest
回归
- 线性回归
- 决策树
- 随机森林
- 梯度提升树
- Survival 回归
- Isotonic 回归
聚类
- K-Means
- Bisecting K-Means(对 K 均值一分为二)
- 高斯混合模型(GMM)
- Latent Dirichlet Allocation (LDA)
协同过滤
- Alternating Least Square (ALS)交替最小二乘法
Frequent Pattern Mining(频繁的模式挖掘)
- FP-Growth
- PrefixSpan
ML Pipelines
Spark MLlib 的早期版本仅包含基于 RDD 的 API。基于数据帧的 API 现在是 Spark 的主要 API。一旦基于数据帧的 API 达到功能奇偶校验,基于 RDD 的 API 将在 Spark 2.3 中被弃用。x基于 RDD 的 API 将在 Spark 3.0 中删除。基于 DataFrames 的 API 通过提供用于表示类似于关系数据库表的表格数据的更高级抽象,使转换功能变得容易,使其成为实现管道的自然选择。
Spark MLlib API 引入了几个用于创建机器学习管道的概念。图 2-3 显示了用于处理文本数据的简单 Spark MLlib 管道。分词器将文本分解为一个单词袋,将单词附加到输出数据帧。术语频率-反向文档频率 (TF–IDF) 将数据帧作为输入,将单词包转换为特征向量,并将其添加到第三个数据帧中。
图 2-3一个简单的火花 MLlib 流水线
Pipeline
管道是用于创建机器学习工作流的一系列连接阶段。级可以是变压器或估计器。
Transformer
转换器将数据帧作为输入,并输出一个新的数据帧,并将附加列追加到新数据帧。新的数据帧包括输入数据帧中的列和其他列。
Estimator
估计器是一种机器学习算法,用于根据训练数据拟合模型。估计器接受训练数据并生成机器学习模型。
ParamGridBuilder
参数网格生成器用于构建参数网格。交叉验证器执行网格搜索,并使用参数网格中用户指定的超参数组合来训练模型。
交叉验证器
交叉验证器交叉评估拟合的机器学习模型,并通过尝试使用用户指定的超参数组合拟合基础估计器来输出最佳模型。使用交叉验证器或训练验证拆分估计器执行模型选择。
Evaluator
评估器计算机器学习模型的性能。它输出精度和召回率等指标,以衡量拟合模型的性能。赋值器的示例包括分别用于二元分类和多类分类任务的二元分类计算器和多分类计算器,以及用于回归任务的回归计算器。
特征提取、转换和选择
大多数情况下,在使用原始数据拟合模型之前,需要进行额外的预处理。例如,基于距离的算法要求对要素进行标准化。当分类数据是单热编码时,某些算法的性能更好。文本数据通常需要标记化和特征矢量化。对于非常大的数据集,可能需要降维。Spark MLlib 包括用于这些类型任务的大量变压器和估计器。我将讨论 Spark MLlib 中一些最常用的变压器和估计器。
字符串索引器
大多数机器学习算法不能直接处理字符串,并要求数据采用数字格式。字符串索引器是将标签的字符串列转换为索引的估计器。它支持四种不同的方法来生成索引:字母数据库、字母表索引、频率精确度和频率辅助码。默认值设置为频率Desc,最频繁的标签设置为 0,结果按标签频率的降序排序。
import org.apache.spark.ml.feature.StringIndexer
val df = spark.createDataFrame(
Seq((0, "car"), (1, "car"), (2, "truck"), (3, "van"), (4, "van"), (5, "van"))
).toDF("id", "class")
df.show
+---+-----+
| id|class|
+---+-----+
| 0| car|
| 1| car|
| 2|truck|
| 3| van|
| 4| van|
| 5| van|
+---+-----+
val model = new StringIndexer()
.setInputCol("class")
.setOutputCol("classIndex")
val indexer = model.fit(df)
val indexed = indexer.transform(df)
indexed.show()
+---+-----+----------+
| id|class|classIndex|
+---+-----+----------+
| 0| car| 1.0|
| 1| car| 1.0|
| 2|truck| 2.0|
| 3| van| 0.0|
| 4| van| 0.0|
| 5| van| 0.0|
+---+-----+----------+
Tokenizer(分词器)
在分析文本数据时,通常必须将句子拆分为单独的术语或单词。分词器正是这样做的。您可以使用正则表达式执行更高级的标记化,使用正则表达式。标记化通常是机器学习 NLP 管道中的第一步。我将在第 4 章中更详细地讨论自然语言处理 (NLP)。
import org.apache.spark.ml.feature.Tokenizer
val df = spark.createDataFrame(Seq(
(0, "Mark gave a speech last night in Laguna Beach"),
(1, "Oranges are full of nutrients and low in calories"),
(2, "Eddie Van Halen is amazing")
)).toDF("id", "sentence")
df.show(false)
+---+-------------------------------------------------+
|id |sentence |
+---+-------------------------------------------------+
|0 |Mark gave a speech last night in Laguna Beach |
|1 |Oranges are full of nutrients and low in calories|
|2 |Eddie Van Halen is amazing |
+---+-------------------------------------------------+
val tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words")
val tokenized = tokenizer.transform(df)
tokenized.show(false)
+---+-------------------------------------------------+
|id |sentence |
+---+-------------------------------------------------+
|0 |Mark gave a speech last night in Laguna Beach |
|1 |Oranges are full of nutrients and low in calories|
|2 |Eddie Van Halen is amazing |
+---+-------------------------------------------------+
+-----------------------------------------------------------+
|words |
+-----------------------------------------------------------+
|[mark, gave, a, speech, last, night, in, laguna, beach] |
|[oranges, are, full, of, nutrients, and, low, in, calories]|
|[eddie, van, halen, is, amazing] |
+-----------------------------------------------------------+
VectorAssembler(矢量装配器)
Spark MLlib 算法要求将要素存储在单个向量列中。通常,训练数据将以表格格式提供,其中数据存储在单独的列中。矢量组件是将一组列合并为单个矢量列的转换器。
import org.apache.spark.ml.feature.VectorAssembler
val df = spark.createDataFrame(
Seq((0, 50000, 7, 1))
).toDF("id", "income", "employment_length", "marital_status")
val assembler = new VectorAssembler()
.setInputCols(Array("income", "employment_length", "marital_status"))
.setOutputCol("features")
val df2 = assembler.transform(df)
df2.show(false)
+---+------+-----------------+--------------+-----------------+
|id |income|employment_length|marital_status|features |
+---+------+-----------------+--------------+-----------------+
|0 |50000 |7 |1 |[50000.0,7.0,1.0]|
+---+------+-----------------+--------------+-----------------+
StandardScaler(标准标定器)
如第1章所述,一些机器学习算法需要对特征进行规范化才能正常工作。标准标度器是一种估计器,用于将要素归一化为具有单位标准差和/或零均值。它接受两个参数:使用Std和使用Mean.使用Std将特征缩放为单位标准差。默认情况下,此参数设置为 true。在缩放之前,将 Mean 设置为以平均值为真中心。默认情况下,此参数设置为 false。
import org.apache.spark.ml.feature.StandardScaler
import org.apache.spark.ml.feature.VectorAssembler
val df = spark.createDataFrame(
Seq((0, 186, 200, 56),(1, 170, 198, 42))
).toDF("id", "height", "weight", "age")
val assembler = new VectorAssembler()
.setInputCols(Array("height", "weight", "age"))
.setOutputCol("features")
val df2 = assembler.transform(df)
df2.show(false)
+---+------+------+---+------------------+
|id |height|weight|age|features |
+---+------+------+---+------------------+
|0 |186 |200 |56 |[186.0,200.0,56.0]|
|1 |170 |198 |42 |[170.0,198.0,42.0]|
+---+------+------+---+------------------+
val scaler = new StandardScaler()
.setInputCol("features")
.setOutputCol("scaledFeatures")
.setWithStd(true)
.setWithMean(false)
val model = scaler.fit(df2)
val scaledData = model.transform(df2)
scaledData.select("features","scaledFeatures").show(false)
+------------------+------------------------------------------------------+
|features |scaledFeatures |
+------------------+------------------------------------------------------+
|[186.0,200.0,56.0]|[16.440232662587228,141.42135623730948,5.656854249492]|
|[170.0,198.0,42.0]|[15.026019100214134,140.0071426749364,4.2426406871192]|
+------------------+------------------------------------------------------+
用于重新缩放数据的附加变压器包括归一化器、最小值标定器和最大Abs标定器。有关更多详细信息,请查看 Apache Spark 在线文档。
StopWordsRemover(停止字切换)
通常在文本分析中使用,停止字切换从字符串序列中删除非索引字。停用词(如 I、the 和 a)对文档的含义没有多大贡献。
import org.apache.spark.ml.feature.StopWordsRemover
val remover = new StopWordsRemover().setInputCol("data").setOutputCol("output")
val dataSet = spark.createDataFrame(Seq(
(0, Seq("She", "is", "a", "cute", "baby")),
(1, Seq("Bob", "never", "went", "to", "Seattle"))
)).toDF("id", "data")
val df = remover.transform(dataSet)
df.show(false)
+---+-------------------------------+---------------------------+
|id |data |output |
+---+-------------------------------+---------------------------+
|0 |[She, is, a, cute, baby] |[cute, baby] |
|1 |[Bob, never, went, to, Seattle]|[Bob, never, went, Seattle]|
+---+-------------------------------+---------------------------+
n-gram
When performing text analysis, it is sometimes advantageous to combine terms into n-grams, a combination of terms in a document. Creating n-grams helps extract more meaningful information from a document. For example, the words “San” and “Diego” individually don’t mean much, but combining them into a bigram “San Diego” provides more context. We use n-gram later in Chapter 4.
import org.apache.spark.ml.feature.NGram
val df = spark.createDataFrame(Seq(
(0, Array("Los", "Angeles", "Lobos", "San", "Francisco")),
(1, Array("Stand", "Book", "Case", "Phone", "Mobile", "Magazine")),
(2, Array("Deep", "Learning", "Machine", "Algorithm", "Pizza"))
)).toDF("id", "words")
val ngram = new NGram().setN(2).setInputCol("words").setOutputCol("ngrams")
val df2 = ngram.transform(df)
df2.select("ngrams").show(false)
+---------------------------------------------------------------------+
|ngrams |
+---------------------------------------------------------------------+
|[Los Angeles, Angeles Lobos, Lobos San, San Francisco] |
|[Stand Book, Book Case, Case Phone, Phone Mobile, Mobile Magazine] |
|[Deep Learning, Learning Machine, Machine Algorithm, Algorithm Pizza]|
+---------------------------------------------------------------------+
OneHotEncoderEstimator
One-hot 编码将分类特征转换为二进制向量,其中最多只有一个值,表示所有特征集中存在特定特征值。西单热编码分类变量是许多机器学习算法(如逻辑回归和支持向量机)的要求。OneHot编码器测试器可以隐藏多个列,为每个输入列生成一个热编码的向量列。
import org.apache.spark.ml.feature.StringIndexer
val df = spark.createDataFrame(
Seq((0, "Male"), (1, "Male"), (2, "Female"), (3, "Female"), (4, "Female"), (5, "Male"))
).toDF("id", "gender")
df.show()
+---+------+
| id|gender|
+---+------+
| 0| Male|
| 1| Male|
| 2|Female|
| 3|Female|
| 4|Female|
| 5| Male|
+---+------+
val indexer = new StringIndexer()
.setInputCol("gender")
.setOutputCol("genderIndex")
val indexed = indexer.fit(df).transform(df)
indexed.show()
+---+------+-----------+
| id|gender|genderIndex|
+---+------+-----------+
| 0| Male| 1.0|
| 1| Male| 1.0|
| 2|Female| 0.0|
| 3|Female| 0.0|
| 4|Female| 0.0|
| 5| Male| 1.0|
+---+------+-----------+
import org.apache.spark.ml.feature.OneHotEncoderEstimator
val encoder = new OneHotEncoderEstimator()
.setInputCols(Array("genderIndex"))
.setOutputCols(Array("genderEnc"))
val encoded = encoder.fit(indexed).transform(indexed)
encoded.show()
+---+------+-----------+-------------+
| id|gender|genderIndex| genderEnc|
+---+------+-----------+-------------+
| 0| Male| 1.0| (1,[],[])|
| 1| Male| 1.0| (1,[],[])|
| 2|Female| 0.0|(1,[0],[1.0])|
| 3|Female| 0.0|(1,[0],[1.0])|
| 4|Female| 0.0|(1,[0],[1.0])|
| 5| Male| 1.0| (1,[],[])|
+---+------+-----------+-------------+
SQLTransformer
SQL转换器允许您使用SQL执行数据转换。虚拟表“__THIS__”对应于输入数据集。
import org.apache.spark.ml.feature.SQLTransformer
val df = spark.createDataFrame(
Seq((0, 5.2, 6.7), (2, 25.5, 8.9))).toDF("id", "col1", "col2")
val transformer = new SQLTransformer().setStatement("SELECT ABS(col1 - col2) as c1, MOD(col1, col2) as c2 FROM __THIS__")
val df2 = transformer.transform(df)
df2.show()
+----+-----------------+
| c1| c2|
+----+-----------------+
| 1.5| 5.2|
|16.6|7.699999999999999|
+----+-----------------+
Term Frequency–Inverse Document Frequency (TF–IDF)
TF–IDF or term frequency–inverse document frequency is a feature vectorization method commonly used in text analysis. It is frequently used to indicate the importance of a term or word to a document in the corpus. A transformer, HashingTF, uses feature hashing to convert terms into feature vectors. An estimator, IDF, scales the vectors generated by the HashingTF (or CountVectorizer). I discuss TF–IDF in greater detail in Chapter 4.
import org.apache.spark.ml.feature.{HashingTF, IDF, Tokenizer}
val df = spark.createDataFrame(Seq(
(0, "Kawhi Leonard is the league MVP"),
(1, "Caravaggio pioneered the Baroque technique"),
(2, "Using Apache Spark is cool")
)).toDF("label", "sentence")
df.show(false)
+-----+------------------------------------------+
|label|sentence |
+-----+------------------------------------------+
|0 |Kawhi Leonard is the league MVP |
|1 |Caravaggio pioneered the Baroque technique|
|2 |Using Apache Spark is cool |
+-----+------------------------------------------+
val tokenizer = new Tokenizer()
.setInputCol("sentence")
.setOutputCol("words")
val df2 = tokenizer.transform(df)
df2.select("label","words").show(false)
+-----+------------------------------------------------+
|label|words |
+-----+------------------------------------------------+
|0 |[kawhi, leonard, is, the, league, mvp] |
|1 |[caravaggio, pioneered, the, baroque, technique]|
|2 |[using, apache, spark, is, cool] |
+-----+------------------------------------------------+
val hashingTF = new HashingTF()
.setInputCol("words")
.setOutputCol("features")
.setNumFeatures(20)
val df3 = hashingTF.transform(df2)
df3.select("label","features").show(false)
+-----+-----------------------------------------------+
|label|features |
+-----+-----------------------------------------------+
|0 |(20,[1,4,6,10,11,18],[1.0,1.0,1.0,1.0,1.0,1.0])|
|1 |(20,[1,5,10,12],[1.0,1.0,2.0,1.0]) |
|2 |(20,[1,4,5,15],[1.0,1.0,1.0,2.0]) |
+-----+-----------------------------------------------+
val idf = new IDF()
.setInputCol("features")
.setOutputCol("scaledFeatures")
val idfModel = idf.fit(df3)
val df4 = idfModel.transform(df3)
df4.select("label", "scaledFeatures").show(3,50)
+-----+--------------------------------------------------+
|label| scaledFeatures|
+-----+--------------------------------------------------+
| 0|(20,[1,4,6,10,11,18],[0.0,0.28768207245178085,0...|
| 1|(20,[1,5,10,12],[0.0,0.28768207245178085,0.5753...|
| 2|(20,[1,4,5,15],[0.0,0.28768207245178085,0.28768...|
+-----+--------------------------------------------------+
主成分分析(PCA)
主成分分析 (PCA) 是一种降维技术,它将相关特征组合成一组较小的线性不相关特征(称为主成分)。PCA在图像识别和异常检测等多个领域都有应用。我将在第 4 章中更详细地讨论 PCA。
import org.apache.spark.ml.feature.PCA
import org.apache.spark.ml.linalg.Vectors
val data = Array(
Vectors.dense(4.2, 5.4, 8.9, 6.7, 9.1),
Vectors.dense(3.3, 8.2, 7.0, 9.0, 7.2),
Vectors.dense(6.1, 1.4, 2.2, 4.3, 2.9)
)
val df = spark.createDataFrame(data.map(Tuple1.apply)).toDF("features")
val pca = new PCA()
.setInputCol("features")
.setOutputCol("pcaFeatures")
.setK(2)
.fit(df)
val result = pca.transform(df).select("pcaFeatures")
result.show(false)
+---------------------------------------+
|pcaFeatures |
+---------------------------------------+
|[13.62324332562565,3.1399510055159445] |
|[14.130156836243236,-1.432033103462711]|
|[3.4900743524527704,0.6866090886347056]|
+---------------------------------------+
ChiSqSelector(奇思克选机)
ChiSq选择性器使用卡方独立性测试进行特征选择。卡方检验是一种检验两个类别变量关系的方法。数字顶部特征是默认的选择方法。它返回基于卡方检验的一组特征,或具有最大预测影响的特征。其他选择方法包括百分位数、fpr、fdr 和少数几种。
import org.apache.spark.ml.feature.ChiSqSelector
import org.apache.spark.ml.linalg.Vectors
val data = Seq(
(0, Vectors.dense(5.1, 2.9, 5.6, 4.8), 0.0),
(1, Vectors.dense(7.3, 8.1, 45.2, 7.6), 1.0),
(2, Vectors.dense(8.2, 12.6, 19.5, 9.21), 1.0)
)
val df = spark.createDataset(data).toDF("id", "features", "class")
val selector = new ChiSqSelector()
.setNumTopFeatures(1)
.setFeaturesCol("features")
.setLabelCol("class")
.setOutputCol("selectedFeatures")
val df2 = selector.fit(df).transform(df)
df2.show()
+---+--------------------+-----+----------------+
| id| features|class|selectedFeatures|
+---+--------------------+-----+----------------+
| 0| [5.1,2.9,5.6,4.8]| 0.0| [5.1]|
| 1| [7.3,8.1,45.2,7.6]| 1.0| [7.3]|
| 2|[8.2,12.6,19.5,9.21]| 1.0| [8.2]|
+---+--------------------+-----+----------------+
Correlation(相关)
相关性评估两个变量之间线性关系的强度。对于线性问题,您可以使用相关性来选择相关要素(要素类相关性)并识别冗余要素(要素内相关性)。斯帕克·姆利布支持皮尔逊和斯皮尔曼的相关性。在以下示例中,相关性计算输入向量的相关矩阵。
import org.apache.spark.ml.linalg.{Matrix, Vectors}
import org.apache.spark.ml.stat.Correlation
import org.apache.spark.sql.Row
val data = Seq(
Vectors.dense(5.1, 7.0, 9.0, 6.0),
Vectors.dense(3.2, 1.1, 6.0, 9.0),
Vectors.dense(3.5, 4.2, 9.1, 3.0),
Vectors.dense(9.1, 2.6, 7.2, 1.8)
)
val df = data.map(Tuple1.apply).toDF("features")
+-----------------+
| features|
+-----------------+
|[5.1,7.0,9.0,6.0]|
|[3.2,1.1,6.0,9.0]|
|[3.5,4.2,9.1,3.0]|
|[9.1,2.6,7.2,1.8]|
+-----------------+
val Row(c1: Matrix) = Correlation.corr(df, "features").head
c1: org.apache.spark.ml.linalg.Matrix =
1.0 -0.01325851107237613 -0.08794286922175912 -0.6536434849076798
-0.01325851107237613 1.0 0.8773748081826724 -0.1872850762579899
-0.08794286922175912 0.8773748081826724 1.0 -0.46050932066780714
-0.6536434849076798 -0.1872850762579899 -0.46050932066780714 1.0
val Row(c2: Matrix) = Correlation.corr(df, "features", "spearman").head
c2: org.apache.spark.ml.linalg.Matrix =
1.0 0.399999999999999 0.19999999999999898 -0.8000000000000014
0.399999999999999 1.0 0.8000000000000035 -0.19999999999999743
0.19999999999999898 0.8000000000000035 1.0 -0.39999999999999486
-0.8000000000000014 -0.19999999999999743 -0.39999999999999486 1.0
您还可以计算存储在数据帧列中的值的相关性,如下所示。
dataDF.show
+------------+-----------+------------+-----------+-----------+-----+
|sepal_length|sepal_width|petal_length|petal_width| class|label|
+------------+-----------+------------+-----------+-----------+-----+
| 5.1| 3.5| 1.4| 0.2|Iris-setosa| 0.0|
| 4.9| 3.0| 1.4| 0.2|Iris-setosa| 0.0|
| 4.7| 3.2| 1.3| 0.2|Iris-setosa| 0.0|
| 4.6| 3.1| 1.5| 0.2|Iris-setosa| 0.0|
| 5.0| 3.6| 1.4| 0.2|Iris-setosa| 0.0|
| 5.4| 3.9| 1.7| 0.4|Iris-setosa| 0.0|
| 4.6| 3.4| 1.4| 0.3|Iris-setosa| 0.0|
| 5.0| 3.4| 1.5| 0.2|Iris-setosa| 0.0|
| 4.4| 2.9| 1.4| 0.2|Iris-setosa| 0.0|
| 4.9| 3.1| 1.5| 0.1|Iris-setosa| 0.0|
| 5.4| 3.7| 1.5| 0.2|Iris-setosa| 0.0|
| 4.8| 3.4| 1.6| 0.2|Iris-setosa| 0.0|
| 4.8| 3.0| 1.4| 0.1|Iris-setosa| 0.0|
| 4.3| 3.0| 1.1| 0.1|Iris-setosa| 0.0|
| 5.8| 4.0| 1.2| 0.2|Iris-setosa| 0.0|
| 5.7| 4.4| 1.5| 0.4|Iris-setosa| 0.0|
| 5.4| 3.9| 1.3| 0.4|Iris-setosa| 0.0|
| 5.1| 3.5| 1.4| 0.3|Iris-setosa| 0.0|
| 5.7| 3.8| 1.7| 0.3|Iris-setosa| 0.0|
| 5.1| 3.8| 1.5| 0.3|Iris-setosa| 0.0|
+------------+-----------+------------+-----------+-----------+-----+
dataDF.stat.corr("petal_length","label")
res48: Double = 0.9490425448523336
dataDF.stat.corr("petal_width","label")
res49: Double = 0.9564638238016178
dataDF.stat.corr("sepal_length","label")
res50: Double = 0.7825612318100821
dataDF.stat.corr("sepal_width","label")
res51: Double = -0.41944620026002677
评估指标
如第 1 章所述,精度、召回率和准确性是评估模型性能的重要评估指标。但是,它们可能并不总是某些问题的最佳指标。
Area Under the Receiver Operating Characteristic (AUROC)
接收器工作特性 (AUROC) 下方的区域是用于评估二元分类器的常见性能指标。接收器工作特性 (ROC) 是一个图形,用于绘制真阳性率与假阳性率。曲线下的面积 (AUC) 是 ROC 曲线下方的面积。AUC 可以解释为模型将随机正示例排名高于随机负示例的概率。十二曲线下方的面积越大(AUROC 越接近 1.0),模型的性能越好。AUROC 为 0.5 的模型是无用的,因为它的预测准确性与随机猜测一样好。
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
val evaluator = new BinaryClassificationEvaluator()
.setMetricName("areaUnderROC")
.setRawPredictionCol("rawPrediction")
.setLabelCol("label")
F1 度量值
F1 度量值或 F1 分数是精度和召回率的谐波平均值或加权平均值。它是用于评估多类分类器的常见性能指标。当类分布不均匀时,这也是一个很好的衡量标准。最好的F1分数是1,而最差的分数是0。良好的 F1 度量值意味着您的漏报率低,误报率低。F1 度量值的公式为:F1-度量值 = 2 ∗(精度∗召回率)/(精度 + 召回率)。
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
val evaluator = new MulticlassClassificationEvaluator()
.setMetricName("f1")
.setLabelCol("label")
.setPredictionCol("prediction")
均方根误差(RMSE)
均方根误差 (RMSE) 是回归任务的最常见指标。RMSE 只是均方误差 (MSE) 的平方根。MSE 指示回归线与一组数据点的接近程度,方法是获取从点到回归线的距离或“误差”,并将它们平方。十三MSE 越小,适合度越高。但是,MSE 与原始数据的单位不匹配,因为该值是平方的。RMSE 具有与输出相同的单位。
import org.apache.spark.ml.evaluation.RegressionEvaluator
val evaluator = new RegressionEvaluator()
.setLabelCol("label")
.setPredictionCol("prediction")
.setMetricName("rmse")
我在后续章节中介绍了其他评估指标,例如平方误差集合和内 (WSSSE) 和轮廓系数。有关 Spark MLlib 支持的所有评估指标的完整列表,请参阅 Spark 的在线文档。
Model Persistence(模型持久性)
Spark MLlib 允许您保存模型并在以后加载它们。如果要将模型与第三方应用程序集成或与团队的其他成员共享,这将特别有用。
保存单个随机森林模型
rf = RandomForestClassifier(numBin=10,numTrees=30)
model = rf.fit(training)
model.save("modelpath")
加载单个随机森林模型
val model2 = RandomForestClassificationModel.load(“modelpath”)
保存完整管道
val pipeline = new Pipeline().setStages(Array(labelIndexer,vectorAssembler, rf))
val cv = new CrossValidator().setEstimator(pipeline)
val model = cv.fit(training)
model.save("modelpath")
加载完整管道
val model2 = CrossValidatorModel.load("modelpath")
Spark MLlib示例
让我们来看一个示例。我们将使用心脏病数据集十四从UCI机器学习存储库中预测心脏病的存在。这些数据是由罗伯特·德特拉诺,医学博士,博士及其在VA医疗中心,长滩和克利夫兰诊所基金会的团队收集的。从历史上看,克利夫兰数据集一直是许多研究的主题,因此我们将使用该数据集。原始数据集有76个属性,但其中只有14个用于ML研究(表2-1)。我们将进行二项式分类并确定患者是否患有心脏病(清单2-2)。
Table 2-1克利夫兰心脏病数据集属性信息
属性 | 描述: __________ |
age | 年龄 |
sex | 性别 |
cp | 胸痛类型 |
trestbps | 静息血压 |
chol | 血清胆固醇(毫克/分升) |
fbs | 空腹血糖>120毫克/分升 |
restecg | 静息心电图结果 |
thalach | 达到的最大心率 |
exang | 运动性心绞痛 |
oldpeak | 相对于休息的运动引起的 ST 段性抑郁 |
slope | 峰值运动ST段的斜率 |
ca | 用氟橡胶着色的主要容器(0-3)的数量 |
thal | 铊应力测试结果 |
num | 预测属性 – 心脏病的诊断 |
让我们开始吧。下载文件并将其复制到 HDFS。
wget http://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/cleveland.data
head -n 10 processed.cleveland.data
63.0,1.0,1.0,145.0,233.0,1.0,2.0,150.0,0.0,2.3,3.0,0.0,6.0,0
67.0,1.0,4.0,160.0,286.0,0.0,2.0,108.0,1.0,1.5,2.0,3.0,3.0,2
67.0,1.0,4.0,120.0,229.0,0.0,2.0,129.0,1.0,2.6,2.0,2.0,7.0,1
37.0,1.0,3.0,130.0,250.0,0.0,0.0,187.0,0.0,3.5,3.0,0.0,3.0,0
41.0,0.0,2.0,130.0,204.0,0.0,2.0,172.0,0.0,1.4,1.0,0.0,3.0,0
56.0,1.0,2.0,120.0,236.0,0.0,0.0,178.0,0.0,0.8,1.0,0.0,3.0,0
62.0,0.0,4.0,140.0,268.0,0.0,2.0,160.0,0.0,3.6,3.0,2.0,3.0,3
57.0,0.0,4.0,120.0,354.0,0.0,0.0,163.0,1.0,0.6,1.0,0.0,3.0,0
63.0,1.0,4.0,130.0,254.0,0.0,2.0,147.0,0.0,1.4,2.0,1.0,7.0,2
53.0,1.0,4.0,140.0,203.0,1.0,2.0,155.0,1.0,3.1,3.0,0.0,7.0,1
hadoop fs -put processed.cleveland.data /tmp/data
我们使用spark-shell来交互式训练我们的模型。
spark-shell
val dataDF = spark.read.format("csv")
.option("header", "true")
.option("inferSchema", "true")
.load(d("/tmp/data/processed.cleveland.data")
.toDF("id","age","sex","cp","trestbps","chol","fbs","restecg",
"thalach","exang","oldpeak","slope","ca","thal","num")
dataDF.printSchema
root
|-- id: string (nullable = false)
|-- age: float (nullable = true)
|-- sex: float (nullable = true)
|-- cp: float (nullable = true)
|-- trestbps: float (nullable = true)
|-- chol: float (nullable = true)
|-- fbs: float (nullable = true)
|-- restecg: float (nullable = true)
|-- thalach: float (nullable = true)
|-- exang: float (nullable = true)
|-- oldpeak: float (nullable = true)
|-- slope: float (nullable = true)
|-- ca: float (nullable = true)
|-- thal: float (nullable = true)
|-- num: float (nullable = true)
val myFeatures = Array("age", "sex", "cp", "trestbps", "chol", "fbs",
"restecg", "thalach", "exang", "oldpeak", "slope",
"ca", "thal", "num")
import org.apache.spark.ml.feature.VectorAssembler
val assembler = new VectorAssembler()
.setInputCols(myFeatures)
.setOutputCol("features")
val dataDF2 = assembler.transform(dataDF)
import org.apache.spark.ml.feature.StringIndexer
val labelIndexer = new StringIndexer()
.setInputCol("num")
.setOutputCol("label")
val dataDF3 = labelIndexer.fit(dataDF2).transform(dataDF2)
val dataDF4 = dataDF3.where(dataDF3("ca").isNotNull)
.where(dataDF3("thal").isNotNull)
.where(dataDF3("num").isNotNull)
val Array(trainingData, testData) = dataDF4.randomSplit(Array(0.8, 0.2), 101)
import org.apache.spark.ml.classification.RandomForestClassifier
val rf = new RandomForestClassifier()
.setFeatureSubsetStrategy("auto")
.setSeed(101)
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
val evaluator = new BinaryClassificationEvaluator().setLabelCol("label")
import org.apache.spark.ml.tuning.ParamGridBuilder
val pgrid = new ParamGridBuilder()
.addGrid(rf.maxBins, Array(10, 20, 30))
.addGrid(rf.maxDepth, Array(5, 10, 15))
.addGrid(rf.numTrees, Array(20, 30, 40))
.addGrid(rf.impurity, Array("gini", "entropy"))
.build()
import org.apache.spark.ml.Pipeline
val pipeline = new Pipeline().setStages(Array(rf))
import org.apache.spark.ml.tuning.CrossValidator
val cv = new CrossValidator()
.setEstimator(pipeline)
.setEvaluator(evaluator)
.setEstimatorParamMaps(pgrid)
.setNumFolds(3)
清单 2-2使用随机森林执行二元分类
现在,我们可以拟合模型了。
val model = cv.fit(trainingData)
对测试数据执行预测。
val prediction = model.transform(testData)
让我们评估模型。
import org.apache.spark.ml.param.ParamMap
val pm = ParamMap(evaluator.metricName -> "areaUnderROC")
val aucTestData = evaluator.evaluate(prediction, pm)
图形处理
Spark包括一个名为 GraphX 的图形处理框架。有一个单独的包称为图形帧,它基于数据帧。图形帧目前不是核心阿帕奇火花的一部分。在撰写本文时,GraphX和图形框架仍在积极开发中。十五我在第6章中介绍了GraphX。
超越Spark MLlib:第三方机器学习集成
Spark可以访问丰富的第三方框架和库生态系统,这要归功于无数的开源贡献者以及微软和谷歌等公司。虽然我介绍了核心的火花MLlib算法,但本书的重点是更强大的下一代算法和框架,如XGBoost,LightGBM,隔离森林,火花NLP和分布式深度学习。我将在以后的章节中介绍它们。
使用Alluxio优化Spark和SparkMLlib
Alluxio,前身为Tachyon,是加州大学伯克利分校AMPLab的一个开源项目。Alluxio是一个以内存为中心的分布式存储系统,最初由李浩元于2012年开发,当时他是一名博士生,也是AMPLab的Apache Spark提交者的创始者。十六该项目是伯克利数据分析堆栈(BDAS)的存储层。2015年,Alluxio, Inc.由李创立,旨在将Alluxio商业化,从安德森·霍洛维茨获得了750万美元的现金注入。如今,Alluxio拥有来自全球50个组织的200多名贡献者,如英特尔,IBM,雅虎和红帽。目前,一些知名公司正在生产中使用Alluxio,如百度,阿里巴巴,Rackspace和巴克莱银行。十七
Alluxio可用于优化Spark机器学习和深度学习工作负载,为超大数据集提供超快的大数据存储。Alluxio进行的深度学习基准测试显示,从Alluxio而不是S3读取数据时,性能显着提高。十八
Architecture(建筑)
Alluxio是一个以内存为中心的分布式存储系统,旨在成为大数据事实上的存储统一层。它提供了一个虚拟化层,统一访问不同的存储引擎(如本地FS,HDFS,S3和NFS)和计算框架(如火花,地图还原,蜂巢和普雷斯托)。图 2-4 概述了 Alluxio 的架构。
图2-4Alluxio 架构概述
Alluxio是协调数据共享和指导数据访问的中间层,同时为计算框架和大数据应用程序提供高性能的低延迟内存速度。阿卢克西奥与火花和Hadoop无缝集成,只需要很小的配置更改。通过利用Alluxio的统一命名空间功能,应用程序只需连接到Alluxio即可访问存储在任何受支持的存储引擎中的数据。阿卢克西奥有自己的原生API以及一个与Hadoop兼容的文件系统接口。方便类使用户能够执行最初为 Hadoop 编写的代码,而无需更改任何代码。REST API 提供对其他语言的访问。我们将在本章的后面部分探讨这些 API。
Alluxio的统一命名空间功能不支持关系数据库和MPP引擎(如红移或雪花)或文档数据库(如MongoDB)。当然,支持与Alluxio和提到的存储引擎之间的写入。开发人员可以使用 Spark 等计算框架从 Redshift 表创建数据帧,并将其以 Parquet 或 CSV 格式存储在 Alluxio 文件系统中,反之亦然(图 2-5)。
图2-5Alluxio 技术架构
为什么使用Alluxio?
显著提高大数据处理性能和可扩展性
多年来,内存变得越来越便宜,而其性能也越来越快。与此同时,硬盘驱动器的性能仅略有好转。毫无疑问,内存中的数据处理比处理磁盘上的数据快一个数量级。在几乎所有的编程范例中,我们都建议在内存中缓存数据以提高性能。与MapReduce相比,阿帕奇火花的主要优势之一是它能够缓存数据。Alluxio将其提升到一个新的水平,提供的大数据应用程序不仅仅是一个缓存层,而是一个成熟的分布式高性能以内存为中心的存储系统。
百度运营着世界上最大的Alluxio集群之一,拥有1000个工作节点,处理超过2PB的数据。借助Alluxio,百度在查询和处理时间方面平均提高了10倍和30倍的性能,从而显着提高了百度做出重要业务决策的能力。十九巴克莱发表了一篇文章,描述了他们在Alluxio的经历。巴克莱数据科学家吉安马里奥·斯帕卡尼亚和高级分析主管哈里·鲍威尔能够使用Alluxio将他们的Spark工作从几小时调整到几秒钟。断续器Qunar.com 是中国最大的旅游搜索引擎之一,使用Alluxio将性能提高了15倍至300倍。
多个框架和应用程序可以以内存速度共享数据
典型的大数据集群具有多个会话,这些会话运行不同的计算框架,例如Spark和MapReduce。在Spark的情况下,每个应用程序都有自己的执行器进程,执行器中的每个任务都在自己的JVM上运行,从而将Spark应用程序彼此隔离。这意味着 Spark(和 MapReduce)应用程序除了写入 HDFS 或 S3 等存储系统外,无法共享数据。如图 2-6 所示,Spark 作业和 MapReduce 作业使用的是 HDFS 或 S3 中存储的相同数据。在图 2-7 中,多个 Spark 作业使用相同的数据,每个作业在自己的堆空间中存储自己的数据版本。不仅数据重复,而且通过 HDFS 或 S3 共享数据可能会很慢,尤其是在共享大量数据的情况下。
图2-6通过HDFS或S3共享数据的不同框架
图2-7通过HDFS或S3共享数据的不同作业
通过将 Alluxio 用作堆外存储(图 2-8),多个框架和作业可以以内存速度共享数据,从而减少数据重复、提高吞吐量并减少延迟。
图 2-8以内存速度共享数据的不同作业和框架
在应用程序终止或发生故障时提供高可用性和持久性
在 Spark 中,执行器进程和执行程序内存驻留在同一 JVM 中,所有缓存数据存储在 JVM 堆空间中(图 2-9)。
图 2-9具有自己的堆内存的 Spark 作业
当作业完成或由于某种原因 JVM 由于运行时异常而崩溃时,缓存在堆空间中的所有数据都将丢失,如图 2-10 和 2-11 所示。
图 2-10Spark 作业崩溃或完成
图 2-11Spark 作业崩溃或完成。堆空间丢失
解决方案是将 Alluxio 用作堆外存储(图 2-12)。
图 2-12使用 Alluxio 作为堆外存储的 Spark
在这种情况下,即使 Spark JVM 崩溃,数据在 Alluxio 中仍然可用(图 2-13 和图 2-14)。
图 2-13Spark 作业崩溃或完成
图 2-14Spark 作业崩溃或完成。堆空间丢失。堆外内存仍然可用
优化整体内存使用情况并最大限度地减少垃圾回收
通过使用 Alluxio,内存使用效率大大提高,因为数据在作业和框架之间共享,并且由于数据存储在堆外,垃圾回收也最小化,从而进一步提高了作业和应用程序的性能(图 2-15)。
图 2-15多个 Spark 和 MapReduce 作业可以访问存储在 Alluxio 中的相同数据
降低硬件要求
使用Alluxio进行大数据处理比使用HDFS和S3要快得多。IBM的测试显示,在写入IO方面,Alluxio的表现比HDFS高出110倍。二十三有了这种性能,对额外硬件的需求就会减少,从而节省了基础设施和许可成本。
Apache Spark 和 Alluxio
您可以在Alluxio中访问数据,类似于从Spark访问存储在HDFS和S3中的数据。
val dataRDD = sc.textFile("alluxio://localhost:19998/test01.csv")
val parsedRDD = dataRDD.map{_.split(",")}
case class CustomerData(userid: Long, city: String, state: String, age: Short)
val dataDF = parsedRDD.map{ a =>CustomerData(a(0).toLong, a(1).toString, a(2).toString, a(3).toShort) }.toDF
dataDF.show()
+------+---------------+-----+---+
|userid| city|state|age|
+------+---------------+-----+---+
| 300| Torrance| CA| 23|
| 302|Manhattan Beach| CA| 21|
+------+---------------+-----+---+
总结
本章简要介绍了 Spark 和 Spark MLlib,足以为您提供执行常见数据处理和机器学习任务所需的技能。我的目标是让你尽快上手。