Spark数据处理与机器学习实战
1. 内存中分组与创建表
在处理数据时,有时需要对一组行应用函数,类似于 SQL 中的 GROUP BY 操作。可以使用以下两种类似的方法。例如,计算每个性别的平均余额:
In: (df.na.fill({'gender': "U", 'balance': 0.0})
.groupBy("gender").avg('balance').show())
Out:
+------+------------+
|gender|avg(balance)|
+------+------------+
| F| -0.25|
| M| 2.0|
| U| 7.5|
+------+------------+
实际上,使用 Spark 可以将 DataFrame 注册为 SQL 表,充分发挥 SQL 的强大功能。该表保存在内存中,并且以类似于 RDD 的方式进行分布式存储。注册表时,需要提供一个名称,后续的 SQL 命令将使用该名称。例如,将表命名为 users:
In: df.registerTempTable("users")
通过调用 Spark SQL 上下文提供的 sql 方法,可以运行任何符合 SQL 规范的查询:
In: sqlContext.sql("""
SELECT gender, AVG(balance)
FROM users
WHERE gender IS NOT NULL
GROUP BY gender""").show()
Out:
+------+-----+
|gender| _c1|
+------+-----+
| F|-0.25|
| M| 2.0|
+------+-----+
2. 数据类型与操作
命令输出的表(以及 users 表本身)属于 Spark DataFrame 类型:
In: type(sqlContext.table("users"))
Out: pyspark.sql.dataframe.DataFrame
DataFrame、表和 RDD 之间密切相关,可以在 DataFrame 上使用 RDD 方法。例如,收集整个表:
In: sqlContext.table("users").collect()
Out: [Row(balance=10.0, gender=None, user_id=0),
Row(balance=1.0, gender=u'M', user_id=1),
Row(balance=-0.5, gender=u'F', user_id=2),
Row(balance=0.0, gender=u'F', user_id=3),
Row(balance=5.0, gender=None, user_id=4),
Row(balance=3.0, gender=u'M', user_id=5)]
获取第一行:
In: a_row = sqlContext.sql("SELECT * FROM users").first()
a_row
Out: Row(balance=10.0, gender=None, user_id=0)
可以通过属性或字典键的方式访问 Row 对象的属性:
In: print a_row['balance']
print a_row.balance
Out: 10.0
10.0
还可以使用 Row 的 asDict 方法将其转换为 Python 字典:
In: a_row.asDict()
Out: {'balance': 10.0, 'gender': None, 'user_id': 0}
3. 数据写入磁盘
将预处理后的 DataFrame 或 RDD 写入磁盘,可以使用 write 方法。有多种格式可供选择,这里将其保存为本地机器上的 JSON 文件:
In: (df.na.drop().write
.save("file:///tmp/complete_users.json", format='json'))
检查本地文件系统上的输出,会发现该操作会创建多个文件(part-r-…)。每个文件包含一些序列化为 JSON 对象的行,将它们合并在一起将得到完整的输出。因为 Spark 是为处理大型分布式文件而设计的,所以写入操作会针对此进行优化,每个节点写入完整 RDD 的一部分:
In: !ls -als /tmp/complete_users.json
Out: total 28
4 drwxrwxr-x 2 vagrant vagrant 4096 Feb 25 22:54 .
4 drwxrwxrwt 9 root root 4096 Feb 25 22:54 ..
4 -rw-r--r-- 1 vagrant vagrant 83 Feb 25 22:54 part-r-00000-...
4 -rw-rw-r-- 1 vagrant vagrant 12 Feb 25 22:54 .part-r-00000-...
4 -rw-r--r-- 1 vagrant vagrant 82 Feb 25 22:54 part-r-00001-...
4 -rw-rw-r-- 1 vagrant vagrant 12 Feb 25 22:54 .part-r-00001-...
0 -rw-r--r-- 1 vagrant vagrant 0 Feb 25 22:54 _SUCCESS
4 -rw-rw-r-- 1 vagrant vagrant 8 Feb 25 22:54 ._SUCCESS.crc
读取时,不需要创建单独的文件,多个文件片段也可以正常读取。JSON 文件还可以在 SQL 查询的 FROM 子句中读取:
In: sqlContext.sql(
"SELECT * FROM json.`file:///tmp/complete_users.json`").show()
Out:
+-------+------+-------+
|balance|gender|user_id|
+-------+------+-------+
| 1.0| M| 1|
| -0.5| F| 2|
| 0.0| F| 3|
| 3.0| M| 5|
+-------+------+-------+
除了 JSON 格式,Parquet 格式在处理结构化大数据集时也非常流行。Parquet 是一种列式存储格式,可在 Hadoop 生态系统中使用,它可以压缩和编码数据,并能处理嵌套结构,因此非常高效。保存和加载 Parquet 文件的操作与 JSON 类似,同样会在磁盘上生成多个文件:
In: df.na.drop().write.save(
"file:///tmp/complete_users.parquet", format='parquet')
In: !ls -als /tmp/complete_users.parquet/
Out: total 44
4 drwxrwxr-x 2 vagrant vagrant 4096 Feb 25 22:54 .
4 drwxrwxrwt 10 root root 4096 Feb 25 22:54 ..
4 -rw-r--r-- 1 vagrant vagrant 376 Feb 25 22:54 _common_metadata
4 -rw-rw-r-- 1 vagrant vagrant 12 Feb 25 22:54 ._common_metadata..
4 -rw-r--r-- 1 vagrant vagrant 1082 Feb 25 22:54 _metadata
4 -rw-rw-r-- 1 vagrant vagrant 20 Feb 25 22:54 ._metadata.crc
4 -rw-r--r-- 1 vagrant vagrant 750 Feb 25 22:54 part-r-00000-...
4 -rw-rw-r-- 1 vagrant vagrant 16 Feb 25 22:54 .part-r-00000-...
4 -rw-r--r-- 1 vagrant vagrant 746 Feb 25 22:54 part-r-00001-...
4 -rw-rw-r-- 1 vagrant vagrant 16 Feb 25 22:54 .part-r-00001-...
0 -rw-r--r-- 1 vagrant vagrant 0 Feb 25 22:54 _SUCCESS
4 -rw-rw-r-- 1 vagrant vagrant 8 Feb 25 22:54 ._SUCCESS.crc
4. 处理 Spark DataFrames
前面介绍了如何从 JSON 和 Parquet 文件加载 DataFrame,但如何从现有的 RDD 创建 DataFrame 呢?只需为 RDD 中的每个记录创建一个 Row 对象,并调用 SQL 上下文的 createDataFrame 方法。最后,可以将其注册为临时表,充分利用 SQL 语法的强大功能:
In: from pyspark.sql import Row
rdd_gender = \
sc.parallelize([Row(short_gender="M", long_gender="Male"),
Row(short_gender="F", long_gender="Female")])
(sqlContext.createDataFrame(rdd_gender)
.registerTempTable("gender_maps"))
In: sqlContext.table("gender_maps").show()
Out:
+-----------+------------+
|long_gender|short_gender|
+-----------+------------+
| Male| M|
| Female| F|
+-----------+------------+
这也是处理 CSV 文件的首选方法。首先使用 sc.textFile 读取文件,然后使用 split 方法、Row 构造函数和 createDataFrame 方法创建最终的 DataFrame。
当内存中有多个 DataFrame 或可以从磁盘加载时,可以进行连接和使用经典 RDBMS 中可用的所有操作。例如,将从 RDD 创建的 DataFrame 与存储在 Parquet 文件中的 users 数据集进行连接:
In: sqlContext.sql("""
SELECT balance, long_gender, user_id
FROM parquet.`file:///tmp/complete_users.parquet`
JOIN gender_maps ON gender=short_gender""").show()
Out:
+-------+-----------+-------+
|balance|long_gender|user_id|
+-------+-----------+-------+
| 3.0| Male| 5|
| 1.0| Male| 1|
| 0.0| Female| 3|
| -0.5| Female| 2|
+-------+-----------+-------+
在 Web UI 中,每个 SQL 查询在 SQL 选项卡下映射为一个虚拟有向无环图(DAG),这有助于跟踪任务的进度并理解查询的复杂性。在执行上述连接查询时,可以清楚地看到两个分支进入同一个 BroadcastHashJoin 块:第一个来自 RDD,第二个来自 Parquet 文件,随后的块只是对所选列的投影。
5. 内存清理
由于表存储在内存中,最后需要清理释放用于保存它们的内存。通过调用 sqlContext 提供的 tableNames 方法,可以获取当前内存中所有表的列表。然后,使用 dropTempTable 方法释放这些表的内存,之后对这些表的任何进一步引用都将返回错误:
In: sqlContext.tableNames()
Out: [u'gender_maps', u'users']
In:
for table in sqlContext.tableNames():
sqlContext.dropTempTable(table)
6. Spark 机器学习
自 Spark 1.3 以来,DataFrame 成为数据科学操作中处理数据集的首选方式。接下来进入主要任务:创建一个模型来预测数据集中缺失的一个或多个属性。Spark 的 MLlib 机器学习库可以提供很大的帮助。MLlib 虽然是用 Scala 和 Java 构建的,但它的功能也可以在 Python 中使用。它包含分类、回归和推荐学习器,以及一些用于降维和特征选择的例程,还具有许多文本处理功能。所有这些功能都能够处理大型数据集,并利用集群中所有节点的能力来实现目标。
截至 2016 年,MLlib 主要由两个包组成:mllib 操作 RDD,ml 操作 DataFrame。由于后者性能良好且是数据科学中最流行的数据表示方式,开发者选择对 ml 分支进行贡献和改进,而 mllib 分支则保持现状,不再进行进一步开发。乍一看,MLlib 似乎是一个完整的库,但在使用 Spark 后会发现,默认包中既没有统计库也没有数值库,这时 SciPy 和 NumPy 就派上用场了,它们对于数据科学来说至关重要。
下面将探索新的 pyspark.ml 包的功能。目前,与最先进的 Scikit-learn 库相比,它仍处于早期阶段,但未来潜力巨大。需要注意的是,Spark 是一个高级、分布式且复杂的软件,应该仅用于处理大数据和多节点集群。实际上,如果数据集可以放入内存,使用 Scikit-learn 等专注于数据科学问题的库会更方便。在单节点上对小数据集运行 Spark 可能比 Scikit-learn 等效算法慢五倍。
7. KDD99 数据集实践
使用真实世界的 KDD99 数据集进行探索。该竞赛的目标是创建一个网络入侵检测系统,能够识别哪些网络流量是恶意的,哪些不是。数据集中包含许多不同的攻击,目标是使用数据集中数据包流的特征准确预测这些攻击。
该数据集在发布后的最初几年对开发优秀的入侵检测系统非常有用,但如今,数据集中包含的所有攻击都很容易检测,因此不再用于入侵检测系统(IDS)的开发。数据集中的特征包括协议(tcp、icmp 和 udp)、服务(http、smtp 等)、数据包大小、协议中激活的标志、尝试成为根用户的次数等。有关 KDD99 挑战和数据集的更多信息可在 http://kdd.ics.uci.edu/databases/kddcup99/kddcup99.html 上获取。
这是一个经典的多类分类问题,下面将详细介绍如何在 Spark 中执行此任务。为了保持代码的整洁,将使用一个新的 IPython Notebook。
8. 读取数据集
首先下载并解压数据集。由于所有分析都在一个小的虚拟机上运行,因此保守地只使用原始训练数据集(未压缩 75MB)的 10%。如果想尝试使用完整的训练数据集(未压缩 750MB),可以取消以下代码片段中的注释行。使用 bash 命令下载训练数据集、测试数据集(47MB)和特征名称:
In: !rm -rf ../datasets/kdd*
# !wget -q -O ../datasets/kddtrain.gz \
# http://kdd.ics.uci.edu/databases/kddcup99/kddcup.data.gz
!wget -q -O ../datasets/kddtrain.gz \
http://kdd.ics.uci.edu/databases/kddcup99/kddcup.data_10_percent.gz
!wget -q -O ../datasets/kddtest.gz \
http://kdd.ics.uci.edu/databases/kddcup99/corrected.gz
!wget -q -O ../datasets/kddnames \
http://kdd.ics.uci.edu/databases/kddcup99/kddcup.names
!gunzip ../datasets/kdd*gz
打印前几行以了解数据格式。可以看出这是一个没有标题的经典 CSV 文件,每行末尾包含一个点。还可以看到一些字段是数字,但有几个是文本,目标变量包含在最后一个字段中:
In: !head -3 ../datasets/kddtrain
Out:
0,tcp,http,SF,181,5450,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,8,8,0.00,0.00,
0.00,0.00,1.00,0.00,0.00,9,9,1.00,0.00,0.11,0.00,0.00,0.00,0.00,0.00,
normal.
0,tcp,http,SF,239,486,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,8,8,0.00,0.00,0
.00,0.00,1.00,0.00,0.00,19,19,1.00,0.00,0.05,0.00,0.00,0.00,0.00,0.00
,normal.
0,tcp,http,SF,235,1337,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,8,8,0.00,0.00,
0.00,0.00,1.00,0.00,0.00,29,29,1.00,0.00,0.03,0.00,0.00,0.00,0.00,0.0
0,normal.
为了创建具有命名字段的 DataFrame,首先需要读取 kddnames 文件中包含的标题。目标字段将简单命名为 target。读取并解析文件后,打印问题的特征数量(记住目标变量不是特征)及其前 10 个名称:
In:
with open('../datasets/kddnames', 'r') as fh:
header = [line.split(':')[0]
for line in fh.read().splitlines()][1:]
header.append('target')
print "Num features:", len(header)-1
print "First 10:", header[:10]
Out: Num features: 41
First 10: ['duration', 'protocol_type', 'service', 'flag', 'src_
bytes', 'dst_bytes', 'land', 'wrong_fragment', 'urgent', 'hot']
接下来创建两个单独的 RDD,一个用于训练数据,另一个用于测试数据:
In:
train_rdd = sc.textFile('file:///home/vagrant/datasets/kddtrain')
test_rdd = sc.textFile('file:///home/vagrant/datasets/kddtest')
需要解析每个文件的每一行以创建 DataFrame。首先将 CSV 文件的每一行拆分为单独的字段,然后将每个数值转换为浮点数,将每个文本值转换为字符串,最后删除每行末尾的点。最后,使用 sqlContext 提供的 createDataFrame 方法为训练和测试数据集创建两个具有命名列的 Spark DataFrame:
In:
def line_parser(line):
def piece_parser(piece):
if "." in piece or piece.isdigit():
return float(piece)
else:
return piece
return [piece_parser(piece) for piece in line[:-1].split(',')]
train_df = sqlContext.createDataFrame(
train_rdd.map(line_parser), header)
test_df = sqlContext.createDataFrame(
test_rdd.map(line_parser), header)
目前只编写了 RDD 转换操作,下面引入一个动作来查看数据集中有多少观测值,并同时检查之前代码的正确性:
In: print "Train observations:", train_df.count()
print "Test observations:", test_df.count()
Out: Train observations: 494021
Test observations: 311029
虽然只使用了完整 KDD99 数据集的十分之一,但仍然要处理五十万条观测值。乘以特征数量 41,可以明显看出将在包含超过两千万个值的观测矩阵上训练分类器。对于 Spark 来说,这并不是一个很大的数据集(完整的 KDD99 数据集也不大),世界各地的开发者已经在处理 PB 级和数十亿条记录的数据。所以不要被这些数字吓到,Spark 就是为处理这些数据而设计的。
最后,查看 DataFrame 的架构,确定哪些字段是数值型,哪些是字符串型(为简洁起见,结果已截断):
In: train_df.printSchema()
Out: root
|-- duration: double (nullable = true)
|-- protocol_type: string (nullable = true)
|-- service: string (nullable = true)
|-- flag: string (nullable = true)
|-- src_bytes: double (nullable = true)
|-- dst_bytes: double (nullable = true)
|-- land: double (nullable = true)
|-- wrong_fragment: double (nullable = true)
|-- urgent: double (nullable = true)
|-- hot: double (nullable = true)
...
...
...
|-- target: string (nullable = true)
9. 特征工程
通过直观分析,只有四个字段是字符串类型:protocol_type、service、flag 和 target(如预期的那样,target 是多类目标标签)。由于将使用基于树的分类器,因此需要将每个变量的文本级别编码为数字。在 Scikit-learn 中,可以使用 sklearn.preprocessing.LabelEncoder 对象完成此操作,在 Spark 中对应的是 pyspark.ml.feature 包中的 StringIndexer。
需要使用 Spark 对四个变量进行编码,因此需要将四个 StringIndexer 对象级联在一起:每个对象将对 DataFrame 的特定列进行操作,输出一个包含额外列的 DataFrame(类似于映射操作)。映射是自动的,按频率排序:Spark 对所选列中每个级别的计数进行排序,将最流行的级别映射为 0,下一个映射为 1,依此类推。需要注意的是,此操作会遍历数据集一次以计算每个级别的出现次数;如果已经知道映射,使用广播并进行映射操作会更有效。
同样,也可以使用独热编码器生成数值观测矩阵。在使用独热编码器的情况下,DataFrame 中将有多个输出列,每个分类特征的每个级别对应一个列。为此,Spark 提供了 pyspark.ml.feature.OneHotEncoder 类。
更一般地说,pyspark.ml.feature 包中包含的所有类都用于从 DataFrame 中提取、转换和选择特征。它们都读取一些列并在 DataFrame 中创建其他列。
截至 Spark 1.6,Python 中可用的特征操作包含在以下详尽列表中(所有这些都可以在 pyspark.ml.feature 包中找到)。除了少数几个需要在文本中内联或稍后解释的名称外,其他名称应该很直观:
-
文本输入(理想情况下)
:
- HashingTF 和 IDF
- Tokenizer 及其基于正则表达式的实现 RegexTokenizer
- Word2vec
- StopWordsRemover
- Ngram
-
分类特征
:
- StringIndexer 及其逆编码器 IndexToString
- OneHotEncoder
- VectorIndexer(开箱即用的分类到数值索引器)
-
其他输入
:
- Binarizer
- PCA
- PolynomialExpansion
- Normalizer、StandardScaler 和 MinMaxScaler
- Bucketizer(对特征值进行分桶)
- ElementwiseProduct(将列相乘)
-
通用
:
- SQLTransformer(实现由 SQL 语句定义的转换,将 DataFrame 视为名为
THIS
的表)
- RFormula(使用 R 风格的语法选择列)
- VectorAssembler(从多个列创建特征向量)
回到示例,现在要将每个分类变量的级别编码为离散数字。为此,将为每个变量使用一个 StringIndexer 对象,并使用 ML Pipeline 将它们设置为阶段。
In: from pyspark.ml import Pipeline
from pyspark.ml.feature import StringIndexer
cols_categorical = ["protocol_type", "service", "flag","target"]
preproc_stages = []
for col in cols_categorical:
out_col = col + "_cat"
preproc_stages.append(
StringIndexer(
inputCol=col, outputCol=out_col, handleInvalid="skip"))
pipeline = Pipeline(stages=preproc_stages)
indexer = pipeline.fit(train_df)
train_num_df = indexer.transform(train_df)
test_num_df = indexer.transform(test_df)
进一步研究管道,查看未拟合的管道和拟合后的管道中的阶段。需要注意的是,Spark 和 Scikit-learn 有很大的区别:在 Scikit-learn 中,fit 和 transform 方法在同一个对象上调用,而在 Spark 中,fit 方法会生成一个新对象(通常其名称会添加一个 Model 后缀,如 Pipeline 和 PipelineModel),在这个新对象上可以调用 transform 方法。这种差异源于闭包,拟合后的对象很容易在进程和集群之间分布:
In: print pipeline.getStages()
print
print pipeline
print indexer
Out:
[StringIndexer_432c8aca691aaee949b8, StringIndexer_4f10bbcde2452dd
1b771, StringIndexer_4aad99dc0a3ff831bea6, StringIndexer_4b369fea0787
3fc9c2a3]
Pipeline_48df9eed31c543ba5eba
PipelineModel_46b09251d9e4b117dc8d
查看第一行数据经过管道后的变化。注意这里使用了一个动作,因此管道和管道模型中的所有阶段都会被执行:
In: print "First observation, after the 4 StringIndexers:\n"
print train_num_df.first()
Out: First observation, after the 4 StringIndexers:
Row(duration=0.0, protocol_type=u'tcp', service=u'http', flag=u'SF',
src_bytes=181.0, dst_bytes=5450.0, land=0.0, wrong_fragment=0.0,
urgent=0.0, hot=0.0, num_failed_logins=0.0, logged_in=1.0, num_
compromised=0.0, root_shell=0.0, su_attempted=0.0, num_root=0.0,
num_file_creations=0.0, num_shells=0.0, num_access_files=0.0, num_
outbound_cmds=0.0, is_host_login=0.0, is_guest_login=0.0, count=8.0,
srv_count=8.0, serror_rate=0.0, srv_serror_rate=0.0, rerror_rate=0.0,
srv_rerror_rate=0.0, same_srv_rate=1.0, diff_srv_rate=0.0, srv_diff_
host_rate=0.0, dst_host_count=9.0, dst_host_srv_count=9.0, dst_host_
same_srv_rate=1.0, dst_host_diff_srv_rate=0.0, dst_host_same_src_port_
rate=0.11, dst_host_srv_diff_host_rate=0.0, dst_host_serror_rate=0.0,
dst_host_srv_serror_rate=0.0, dst_host_rerror_rate=0.0, dst_host_srv_
rerror_rate=0.0, target=u'normal', protocol_type_cat=1.0, service_
cat=2.0, flag_cat=0.0, target_cat=2.0)
得到的 DataFrame 看起来非常完整且易于理解,所有变量都有名称和值。可以立即注意到分类特征仍然存在,例如,既有 protocol_type(分类)又有 protocol_type_cat(从分类变量映射而来的数值版本)。
从 DataFrame 中提取某些列就像在 SQL 查询中使用 SELECT 一样简单。现在构建一个包含所有数值特征名称的列表:从 header 中找到的名称开始,移除分类特征,并用数值派生的特征替换它们。最后,由于只需要特征,移除目标变量及其数值派生的等效项:
In: features_header = set(header) \
- set(cols_categorical) \
| set([c + "_cat" for c in cols_categorical]) \
- set(["target", "target_cat"])
features_header = list(features_header)
print features_header
print "Total numerical features:", len(features_header)
Out: ['num_access_files', 'src_bytes', 'srv_count', 'num_outbound_
cmds', 'rerror_rate', 'urgent', 'protocol_type_cat', 'dst_host_same_
srv_rate', 'duration', 'dst_host_diff_srv_rate', 'srv_serror_rate',
'is_host_login', 'wrong_fragment', 'serror_rate', 'num_compromised',
'is_guest_login', 'dst_host_rerror_rate', 'dst_host_srv_serror_rate',
'hot', 'dst_host_srv_count', 'logged_in', 'srv_rerror_rate', 'dst_
host_srv_diff_host_rate', 'srv_diff_host_rate', 'dst_host_same_src_
port_rate', 'root_shell', 'service_cat', 'su_attempted', 'dst_host_
count', 'num_file_creations', 'flag_cat', 'count', 'land', 'same_srv_
rate', 'dst_bytes', 'num_shells', 'dst_host_srv_rerror_rate', 'num_
root', 'diff_srv_rate', 'num_failed_logins', 'dst_host_serror_rate']
Total numerical features: 41
综上所述,通过以上步骤,我们完成了从数据处理、特征工程到模型准备的一系列操作,为后续在 KDD99 数据集上进行机器学习建模奠定了坚实的基础。利用 Spark 的强大功能,能够高效地处理大规模数据,并进行复杂的特征转换和分析。在实际应用中,可以根据具体需求进一步调整和优化这些操作,以达到更好的模型性能。
Spark数据处理与机器学习实战(续)
10. 模型训练与评估
在完成特征工程后,接下来可以使用处理好的数据集进行模型训练和评估。这里以决策树分类器为例,在 Spark 中使用 pyspark.ml.classification 包中的 DecisionTreeClassifier 进行多类分类任务。
from pyspark.ml.classification import DecisionTreeClassifier
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
# 定义特征列和标签列
features_col = features_header
label_col = "target_cat"
# 创建决策树分类器
dt = DecisionTreeClassifier(labelCol=label_col, featuresCol="features")
# 使用 VectorAssembler 将特征列组合成一个特征向量列
from pyspark.ml.feature import VectorAssembler
assembler = VectorAssembler(inputCols=features_col, outputCol="features")
# 创建新的 Pipeline 来组合特征向量组装和决策树分类器
pipeline_dt = Pipeline(stages=[assembler, dt])
# 拟合训练数据
model_dt = pipeline_dt.fit(train_num_df)
# 对测试数据进行预测
predictions = model_dt.transform(test_num_df)
# 评估模型
evaluator = MulticlassClassificationEvaluator(
labelCol=label_col, predictionCol="prediction", metricName="accuracy")
accuracy = evaluator.evaluate(predictions)
print("Decision Tree Accuracy = %g " % (accuracy))
上述代码的执行流程如下:
1. 导入所需的分类器和评估器。
2. 定义特征列和标签列。
3. 创建决策树分类器,并指定标签列和特征列。
4. 使用 VectorAssembler 将多个特征列组合成一个特征向量列。
5. 创建新的 Pipeline,将特征向量组装和决策树分类器作为阶段。
6. 使用训练数据拟合 Pipeline 得到模型。
7. 对测试数据进行预测。
8. 使用评估器计算模型的准确率。
11. 模型调优
为了提高模型的性能,可以进行模型调优。在 Spark 中,可以使用 pyspark.ml.tuning 包中的 CrossValidator 进行交叉验证和参数调优。
from pyspark.ml.tuning import ParamGridBuilder, CrossValidator
# 定义参数网格
paramGrid = (ParamGridBuilder()
.addGrid(dt.maxDepth, [2, 5, 10, 20, 30])
.addGrid(dt.maxBins, [10, 20, 40, 80, 100])
.build())
# 创建交叉验证器
cv = CrossValidator(estimator=pipeline_dt,
estimatorParamMaps=paramGrid,
evaluator=evaluator,
numFolds=5)
# 拟合训练数据进行交叉验证
cvModel = cv.fit(train_num_df)
# 获取最佳模型
bestModel = cvModel.bestModel
# 对测试数据进行预测
predictions_cv = bestModel.transform(test_num_df)
# 评估最佳模型
accuracy_cv = evaluator.evaluate(predictions_cv)
print("Decision Tree Accuracy after Cross Validation = %g " % (accuracy_cv))
上述代码的执行流程如下:
1. 导入参数网格构建器和交叉验证器。
2. 定义决策树分类器的参数网格,包括最大深度和最大分箱数。
3. 创建交叉验证器,指定估计器、参数网格、评估器和折数。
4. 使用训练数据进行交叉验证,得到最佳模型。
5. 对测试数据进行预测。
6. 评估最佳模型的准确率。
12. 结果分析
通过对比调优前后的模型准确率,可以评估模型调优的效果。同时,可以查看决策树模型的结构,了解特征的重要性。
# 查看决策树模型的结构
print(model_dt.stages[1].toDebugString)
# 查看特征重要性
import pandas as pd
feature_importances = model_dt.stages[1].featureImportances
feature_names = features_col
importances = pd.DataFrame({'feature': feature_names, 'importance': feature_importances.toArray()})
importances = importances.sort_values('importance', ascending=False)
print(importances)
13. 总结与展望
本文详细介绍了在 Spark 中进行数据处理、特征工程、模型训练和调优的完整流程。通过使用 KDD99 数据集,展示了如何将原始数据转换为适合机器学习模型处理的格式,并使用决策树分类器进行多类分类任务。同时,介绍了如何使用交叉验证进行模型调优,提高模型的性能。
未来的工作可以考虑以下几个方面:
-
尝试其他模型
:除了决策树分类器,还可以尝试其他机器学习模型,如随机森林、支持向量机等,比较不同模型的性能。
-
进一步的特征工程
:可以尝试更多的特征提取和转换方法,如特征选择、特征缩放等,以提高模型的性能。
-
分布式训练
:如果数据集非常大,可以考虑使用分布式训练方法,充分利用 Spark 的分布式计算能力。
以下是整个流程的 mermaid 流程图:
graph LR
A[读取数据集] --> B[数据预处理]
B --> C[特征工程]
C --> D[模型训练]
D --> E[模型评估]
E --> F{是否需要调优}
F -- 是 --> G[模型调优]
G --> D
F -- 否 --> H[结果分析]
通过以上步骤和方法,可以在 Spark 中高效地处理大规模数据,并进行机器学习建模和分析。
超级会员免费看

被折叠的 条评论
为什么被折叠?



