28、Spark数据处理与机器学习实战

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 中高效地处理大规模数据,并进行机器学习建模和分析。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值