如何使用Pyspark构建机器学习管道

315 阅读6分钟

Pyspark简介

Spark是一个用于大数据处理的开源框架。它最初是用scala编写的,后来由于对使用大数据进行机器学习的需求不断增加,因此发布了相同的python API。因此,Pyspark是一个用于Spark的Python API。它整合了Spark的力量和Python的简单性,用于数据分析。Pyspark可以有效地与spark组件一起工作,如spark SQL、Mllib和流,让我们利用大数据和机器学习的真正潜力。

在这篇文章中,我们将为企鹅数据建立一个分类管道。 我们将讨论如何处理缺失的数据,以及在Pyspark的管道模块的帮助下对数据进行扩展和转换。

启动一个Spark会话

Spark会话是每个底层spark功能的入口。它可以让我们创建和使用RDDs、Dataframes和Datasets。因此,要使用Spark,必须启动一个Spark会话。在Python中,我们可以通过使用下面提到的构建器模式来做到这一点。

from pyspark.sql import SparkSession
spark = SparkSession 
     .builder 
     .appName('classification with pyspark') 
     .config("spark.some.config.option", "some-value") 
     .getOrCreate()

读取数据

现在已经创建了一个spark会话,我们现在可以通过下面的代码片断来读取我们的数据。

dt = spark.read.csv('D:Data Setspenguins_size.csv',  header=True)
dt.show(5)
+-------+---------+----------------+---------------+-----------------+-----------+------+
|species|   island|culmen_length_mm|culmen_depth_mm|flipper_length_mm|body_mass_g|   sex|
+-------+---------+----------------+---------------+-----------------+-----------+------+
| Adelie|Torgersen|            39.1|           18.7|              181|       3750|  MALE|
| Adelie|Torgersen|            39.5|           17.4|              186|       3800|FEMALE|
| Adelie|Torgersen|            40.3|             18|              195|       3250|FEMALE|
| Adelie|Torgersen|              NA|             NA|               NA|         NA|    NA|
| Adelie|Torgersen|            36.7|           19.3|              193|       3450|FEMALE|
+-------+---------+----------------+---------------+-----------------+-----------+------+
only showing top 5 rows

打印模式以了解我们的数据集中的数据类型。

output: root
 |-- species: string (nullable = true)
 |-- island: string (nullable = true)
 |-- culmen_length_mm: string (nullable = true)
 |-- culmen_depth_mm: string (nullable = true)
 |-- flipper_length_mm: string (nullable = true)
 |-- body_mass_g: string (nullable = true)
 |-- sex: string (nullable = true)

正如你所看到的,所有的列都是字符串类型的,我们不能对字符串数据进行操作。因此,我们将把culmen长度、深度和flipper长度转换为浮点数,把身体质量转换为整数。

from pyspark.sql.types import IntegerType, FloatType
df = dt.withColumn("culmen_depth_mm",dt.culmen_depth_mm.cast(FloatType()))
                   .withColumn("culmen_length_mm",dt.culmen_length_mm.cast(FloatType()))
                    .withColumn("flipper_length_mm",dt.flipper_length_mm.cast('float'))
                     .withColumn("body_mass_g",dt.body_mass_g.cast('int'))

你可以用上面提到的两种方法进行转换。

让我们来看看我们的数据是否得到了转换。

output:root
 |-- species: string (nullable = true)
 |-- island: string (nullable = true)
 |-- culmen_length_mm: float (nullable = true)
 |-- culmen_depth_mm: float (nullable = true)
 |-- flipper_length_mm: float (nullable = true)
 |-- body_mass_g: integer (nullable = true)
 |-- sex: string (nullable = true)

处理缺失值

在我们的数据集中有缺失值,让我们看看哪一列有多少缺失值。

from pyspark.sql.functions import col,isnan, when, count
df.select([count(when(isnan(c) | col(c).isNull() | col(c).contains('NA'), c)).alias(c) for c in df.columns]).show()

性别列有缺失值,但它们是字符串格式的,所以我们使用contains()和IsNull()。

找出缺失值的行

df.where(col('sex').contains('NA')).show()

现在,我们将创建一个没有任何缺失值的数据集。

df_new = df.where(df.sex != 'NA')
df_new.show(10)
+-------+---------+----------------+---------------+-----------------+-----------+------+
|species|   island|culmen_length_mm|culmen_depth_mm|flipper_length_mm|body_mass_g|   sex|
+-------+---------+----------------+---------------+-----------------+-----------+------+
| Adelie|Torgersen|            39.1|           18.7|            181.0|       3750|  MALE|
| Adelie|Torgersen|            39.5|           17.4|            186.0|       3800|FEMALE|
| Adelie|Torgersen|            40.3|           18.0|            195.0|       3250|FEMALE|
| Adelie|Torgersen|            36.7|           19.3|            193.0|       3450|FEMALE|
| Adelie|Torgersen|            39.3|           20.6|            190.0|       3650|  MALE|
| Adelie|Torgersen|            38.9|           17.8|            181.0|       3625|FEMALE|
| Adelie|Torgersen|            39.2|           19.6|            195.0|       4675|  MALE|
| Adelie|Torgersen|            41.1|           17.6|            182.0|       3200|FEMALE|
| Adelie|Torgersen|            38.6|           21.2|            191.0|       3800|  MALE|
| Adelie|Torgersen|            34.6|           21.1|            198.0|       4400|  MALE|
+-------+---------+----------------+---------------+-----------------+-----------+------+
only showing top 10 rows

对分类变量进行编码

机器学习算法无法处理非数字数据,因此在将数据输入算法之前,需要将其转化为数字数据。

因此,首先,我们将根据数据类型将列名分开,这使我们更容易处理它们。

from collections import defaultdict
data_types = defaultdict(list)
for entry in df.schema.fields:
  data_types[str(entry.dataType)].append(entry.name)
print(data_types)
Output: defaultdict(list,
            {'StringType': ['species', 'island', 'sex'],
             'FloatType': ['culmen_length_mm',
              'culmen_depth_mm',
              'flipper_length_mm'],
             'IntegerType': ['body_mass_g']})
cat_cols = [var for var in data_types["StringType"]]

接下来,我们将导入Stringindexer,它是相当于Pyspark和OneHotEncoder的Scikit Learn Labelencoder。我们将使用管道方法来方便地将数据从分类类型转化为数字类型。OneHot编码将为每一行创建一个稀疏的向量。有关不同编码方法的详细知识,请访问这里

from pyspark.ml.feature import StringIndexer, OneHotEncoder
stage_string_index = [StringIndexer(inputCol=col, outputCol=col+' string_indexed') for col in cat_cols]
stage_onehot_enc =   [OneHotEncoder(inputCol=col+' string_indexed', outputCol=col+' onehot_enc') for col in cat_cols]
from pyspark.ml import Pipeline
ppl = Pipeline(stages= stage_string_index + stage_onehot_enc)
df_trans = ppl.fit(df_new).transform(df_new)
df_trans.show(10)
+-------+---------+----------------+---------------+-----------------+-----------+------+----------------------+---------------------+------------------+------------------+-----------------+--------------+
|species|   island|culmen_length_mm|culmen_depth_mm|flipper_length_mm|body_mass_g|   sex|species string_indexed|island string_indexed|sex string_indexed|species onehot_enc|island onehot_enc|sex onehot_enc|
+-------+---------+----------------+---------------+-----------------+-----------+------+----------------------+---------------------+------------------+------------------+-----------------+--------------+
| Adelie|Torgersen|            39.1|           18.7|            181.0|       3750|  MALE|                   0.0|                  2.0|               0.0|     (2,[0],[1.0])|        (2,[],[])| (2,[0],[1.0])|
| Adelie|Torgersen|            39.5|           17.4|            186.0|       3800|FEMALE|                   0.0|                  2.0|               1.0|     (2,[0],[1.0])|        (2,[],[])| (2,[1],[1.0])|
| Adelie|Torgersen|            40.3|           18.0|            195.0|       3250|FEMALE|                   0.0|                  2.0|               1.0|     (2,[0],[1.0])|        (2,[],[])| (2,[1],[1.0])|
| Adelie|Torgersen|            36.7|           19.3|            193.0|       3450|FEMALE|                   0.0|                  2.0|               1.0|     (2,[0],[1.0])|        (2,[],[])| (2,[1],[1.0])|
| Adelie|Torgersen|            39.3|           20.6|            190.0|       3650|  MALE|                   0.0|                  2.0|               0.0|     (2,[0],[1.0])|        (2,[],[])| (2,[0],[1.0])|
| Adelie|Torgersen|            38.9|           17.8|            181.0|       3625|FEMALE|                   0.0|                  2.0|               1.0|     (2,[0],[1.0])|        (2,[],[])| (2,[1],[1.0])|
| Adelie|Torgersen|            39.2|           19.6|            195.0|       4675|  MALE|                   0.0|                  2.0|               0.0|     (2,[0],[1.0])|        (2,[],[])| (2,[0],[1.0])|
| Adelie|Torgersen|            41.1|           17.6|            182.0|       3200|FEMALE|                   0.0|                  2.0|               1.0|     (2,[0],[1.0])|        (2,[],[])| (2,[1],[1.0])|
| Adelie|Torgersen|            38.6|           21.2|            191.0|       3800|  MALE|                   0.0|                  2.0|               0.0|     (2,[0],[1.0])|        (2,[],[])| (2,[0],[1.0])|
| Adelie|Torgersen|            34.6|           21.1|            198.0|       4400|  MALE|                   0.0|                  2.0|               0.0|     (2,[0],[1.0])|        (2,[],[])| (2,[0],[1.0])|
+-------+---------+----------------+---------------+-----------------+-----------+------+----------------------+---------------------+------------------+------------------+-----------------+--------------+
only showing top 10 rows

在上面的代码片段中,我们定义了一个流水线,它将stage_string_indexer和stage_onehot_enc相继进行。第一阶段的输出列被用来作为第二阶段的输入。

缩放参数

如果你观察数据,与其他参数相比,体质量特征太大。而有些算法容易出现未缩放的参数,所以对数据进行缩放是一个好的做法。我们将再次使用Pipeline来缩放参数。为此,我们将需要VectorAssembler和StandardScaler方法。

*VectorAssembler从给定的列列表中创建一个单一的特征向量。

from pyspark.ml.feature import StandardScaler, VectorAssembler
assembler = [VectorAssembler(inputCols=[col], outputCol=col+'_vec') for col in ['culmen_length_mm','culmen_depth_mm','flipper_length_mm','body_mass_g']]
scale = [StandardScaler(inputCol=col+'_vec', outputCol=col+'_scaled') for col in ['culmen_length_mm','culmen_depth_mm','flipper_length_mm','body_mass_g']]
pipe = Pipeline(stages = assembler + scale)
df_scale = pipe.fit(df_trans).transform(df_trans)
df_scale.toPandas().iloc[:,-4:]

分类建模

在本节中,我们将使用pyspark定义一个管道,来处理我们打算进行的分类建模。在这篇文章中,我们将使用一个随机森林分类器。所以,让我们进入编码部分。

train_set, test_set =df_scale.randomSplit([0.75,0.25])
 pyspark.ml.classification import RandomForestClassifier
features = VectorAssembler(inputCols=[ 'island onehot_enc', 'sex onehot_enc',
                                        'culmen_length_mm_scaled','culmen_depth_mm_scaled','flipper_length_mm_scaled',
                                         'body_mass_g_scaled'], outputCol='features')
model_rf = RandomForestClassifier(featuresCol='features', labelCol='species string_indexed')
pipe_lr = Pipeline(stages = [features, model_rf]) 

在上面的代码片段中,我们定义了一个向量集合器,并将选定的列作为我们数据集的输入。接下来,我们定义了我们的随机森林分类器,其标签列种string_indexed来自我们已经缩放的数据集。

接下来,我们将为我们的管道定义参数网格。这对于超参数的调整至关重要。

from pyspark.ml.tuning import CrossValidator, ParamGridBuilder
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
evaluator = MulticlassClassificationEvaluator(predictionCol='prediction', labelCol='species string_indexed')
parameters = ParamGridBuilder()
            .addGrid(model_rf.bootstrap, [True,False])
            .addGrid(model_rf.maxDepth, [5,10,20,30])
            .build()
cv = CrossValidator(estimator=pipe_lr,
                    estimatorParamMaps=parameters,
                    evaluator= evaluator)

在上面的代码中,我们首先定义了我们的评估器,它是一个多类评估器,我们又像以前一样指定了标签列。然后我们用不同的模型参数定义了我们的参数网格。最后用默认的交叉验证数3、评估器、参数网格和评估器建立交叉验证器。

cvModel = cv.fit(train_set)

让我们找出最佳模型

cvModel.bestModel.stages[-1]
output: RandomForestClassificationModel: uid=RandomForestClassifier_17983cbf4844, numTrees=20, numClasses=3, numFeatures=8

训练精度

predict = cvModel.transform(test_set)

f1 = evaluator.evaluate(predict, {evaluator.metricName:'f1'})

accuracy = evaluator.evaluate(predict, {evaluator.metricName:'accuracy'})

print(f'F1 score:{f1}')

print(f'Accuracy score:{accuracy}')
output: F1 score:0.978494623655914
Accuracy score:0.978494623655914

为了得到预测值,我们只需要对我们的cvModel调用transform()。

为了使代码更加简洁实用,你可以从一开始就描述整个过程,也就是在一个管道中对分类变量进行编码到分类。你所需要注意的是你的输入和输出列。

总结

当涉及到机器学习的规模时,Pyspark是一个无价的资产。而能够写出整洁且容易调试的代码,总是令人向往的。在这篇文章中,我们使用Pyspark库设计了一个分类管道。以下是文章的一些主要收获。

  • 我们学会了用Pyspark加载和读取数据集
  • 用StingIndexer和OneHotEncoder对分类变量进行编码
  • 我们使用VectorAssembler和StandardScaler对数据进行缩放
  • 最后建立一个分类管道和参数网格,用于超参数调整。

所以,这就是关于用Pyspark建立一个机器学习管道的全部内容。

我希望,你喜欢这篇文章。