27、基于Spark的分布式机器学习实践

基于Spark的分布式机器学习实践

1. 引言

在分布式环境中运行作业是现代数据科学的重要需求。为了满足这一需求,我们引入了一些基本概念和工具,如Hadoop和Spark框架。本文将重点介绍如何在Spark中进行数据科学实践,包括变量共享、数据预处理等关键内容。

2. 环境搭建

由于机器学习需要大量的计算资源,为了节省资源(尤其是内存),我们将使用不依赖YARN的Spark独立模式。在这种模式下,所有处理都将在驱动机器上进行,不会进行共享。但不用担心,本文中的代码在集群环境中同样适用。具体操作步骤如下:
1. 使用 vagrant up 命令启动虚拟机。
2. 当虚拟机准备好后,使用 vagrant ssh 命令访问虚拟机。
3. 在虚拟机内部使用 ./start_jupyter.sh 命令以IPython Notebook的方式启动Spark独立模式。
4. 打开浏览器,访问 http://localhost:8888

关闭时,使用 Ctrl + C 键退出IPython Notebook,然后使用 vagrant halt 命令关闭虚拟机。需要注意的是,即使在这种配置下,当至少有一个IPython Notebook正在运行时,你也可以通过 http://localhost:4040 访问Spark UI。

3. 集群节点间的变量共享

在分布式环境中工作时,有时需要在节点之间共享信息,以便所有节点都能使用一致的变量进行操作。Spark提供了两种类型的变量来处理这种情况:只读变量和只写变量。

3.1 广播只读变量

广播变量是由驱动节点(在我们的配置中,即运行IPython Notebook的节点)与集群中的所有节点共享的变量。它是只读变量,因为一旦一个节点广播了该变量,即使其他节点更改了它,也不会被读回。

以下是一个简单的示例,我们要对一个仅包含性别信息的数据集进行独热编码:

one_hot_encoding = {"M": (1, 0, 0),
                    "F": (0, 1, 0),
                    "U": (0, 0, 1)
                   }

最初尝试的简单解决方案(但不起作用)是并行化虚拟数据集(或从磁盘读取),然后在RDD上使用 map 方法和lambda函数将性别映射到其编码元组:

(sc.parallelize(["M", "F", "U", "F", "M", "U"])
   .map(lambda x: one_hot_encoding[x])
   .collect())

这个解决方案在本地可以工作,但在实际的分布式环境中无法运行,因为所有节点的工作空间中都没有 one_hot_encoding 变量。一个快速的解决方法是将Python字典包含在映射函数中:

def map_ohe(x):
    ohe = {"M": (1, 0, 0),
           "F": (0, 1, 0),
           "U": (0, 0, 1)
          }
    return ohe[x]
sc.parallelize(["M", "F", "U", "F", "M", "U"]).map(map_ohe).collect()

这种解决方案在本地和服务器上都能工作,但不太理想,因为我们将数据和处理混合在一起,使得映射函数不可重用。更好的方法是让映射函数引用广播变量:

bcast_map = sc.broadcast(one_hot_encoding)
def bcast_map_ohe(x, shared_ohe):
    return shared_ohe[x]
(sc.parallelize(["M", "F", "U", "F", "M", "U"])
 .map(lambda x: bcast_map_ohe(x, bcast_map.value))
 .collect())

可以将广播变量想象成写在HDFS中的文件。当一个通用节点想要访问它时,只需要HDFS路径(作为 map 方法的参数传递),这样可以确保所有节点都能读取相同的内容。广播变量存储在集群中所有节点的内存中,因此不要共享大量数据,以免填满内存并导致后续处理无法进行。要删除广播变量,可以使用 unpersist 方法:

bcast_map.unpersist()
3.2 累加器只写变量

累加器是Spark集群中可以共享的另一种变量。累加器是只写变量,可以进行累加操作,通常用于实现求和或计数功能。只有驱动节点(运行IPython Notebook的节点)可以读取其值,其他节点无法读取。

以下是一个示例,我们要处理一个文本文件,并统计其中空行的数量:

# 第一种低效的解决方案:使用两个独立的Spark作业
print "The number of empty lines is:"
(sc.textFile('file:///home/vagrant/datasets/hadoop_git_readme.txt')
   .filter(lambda line: len(line) == 0)
   .count())

# 第二种更有效的解决方案
accum = sc.accumulator(0)
def split_line(line):   
    if len(line) == 0:
        accum.add(1)
    return 1
tot_lines = (
    sc.textFile('file:///home/vagrant/datasets/hadoop_git_readme.txt')
      .map(split_line)
      .count())
empty_lines = accum.value
print "In the file there are %d lines" % tot_lines
print "And %d lines are empty" % empty_lines

原生情况下,Spark支持数值类型的累加器,默认操作是求和。通过更多的编码,我们可以将其转换为更复杂的操作。

3.3 广播和累加器的结合使用示例

虽然广播变量和累加器是简单且功能有限的变量(一个是只读的,另一个是只写的),但它们可以用于创建非常复杂的操作。例如,我们尝试在分布式环境中对鸢尾花数据集应用不同的机器学习算法。具体步骤如下:
1. 读取数据集并将其广播到所有节点(因为数据集足够小,可以放入内存)。
2. 每个节点将在数据集上使用不同的分类器,并返回分类器名称及其在整个数据集上的准确率得分。为了简化这个简单示例,我们不进行任何预处理、训练/测试拆分或超参数优化。
3. 如果分类器引发任何异常,应将错误的字符串表示形式与分类器名称一起存储在累加器中。
4. 最终输出应包含成功完成分类任务的分类器列表及其准确率得分。

# 第一步:加载鸢尾花数据集并广播到所有节点
from sklearn.datasets import load_iris
bcast_dataset = sc.broadcast(load_iris())

# 第二步:创建自定义累加器
from pyspark import AccumulatorParam
class ErrorAccumulator(AccumulatorParam):
    def zero(self, initialList):
        return initialList
    def addInPlace(self, v1, v2):
        if not isinstance(v1, list):
            v1 = [v1]
        if not isinstance(v2, list):
            v2 = [v2]
        return v1 + v2
errAccum = sc.accumulator([], ErrorAccumulator())

# 第三步:定义映射函数
def apply_classifier(clf, dataset):
    clf_name = clf.__class__.__name__
    X = dataset.value.data
    y = dataset.value.target
    try:
        from sklearn.metrics import accuracy_score
        clf.fit(X, y)
        y_pred = clf.predict(X)
        acc = accuracy_score(y, y_pred)
        return [(clf_name, acc)]
    except Exception as e:
        errAccum.add((clf_name, str(e)))
        return []

# 第四步:执行核心任务
from sklearn.linear_model import SGDClassifier
from sklearn.dummy import DummyClassifier
from sklearn.decomposition import PCA
from sklearn.manifold import MDS
classifiers = [DummyClassifier('most_frequent'), 
               SGDClassifier(), 
               PCA(), 
               MDS()]
(sc.parallelize(classifiers)
     .flatMap(lambda x: apply_classifier(x, bcast_dataset))
     .collect())

# 查看错误信息
print "The errors are:"
errAccum.value

# 清理广播数据集
bcast_dataset.unpersist()

需要注意的是,在这个示例中,我们使用了一个可以广播的小数据集。在实际的大数据问题中,你需要从HDFS加载数据集,并广播HDFS路径。

4. Spark中的数据预处理

到目前为止,我们已经了解了如何从本地文件系统和HDFS加载文本数据。文本文件可以包含非结构化数据(如文本文档)或结构化数据(如CSV文件)。对于半结构化数据,如包含JSON对象的文件,Spark有特殊的例程可以将文件转换为DataFrame,类似于R和Python pandas中的DataFrame。DataFrame非常类似于关系数据库管理系统(RDBMS)中的表,其中设置了模式。

4.1 JSON文件和Spark DataFrames

要导入符合JSON格式的文件,我们首先需要创建一个SQL上下文:

from pyspark.sql import SQLContext
sqlContext = SQLContext(sc)

以下是一个小JSON文件的内容示例:

!cat /home/vagrant/datasets/users.json

输出:

{"user_id":0, "balance": 10.0}
{"user_id":1, "gender":"M", "balance": 1.0}
{"user_id":2, "gender":"F", "balance": -0.5}
{"user_id":3, "gender":"F", "balance": 0.0}
{"user_id":4, "balance": 5.0}
{"user_id":5, "gender":"M", "balance": 3.0}

使用 sqlContext 提供的 read.json 方法,我们可以将文件转换为格式良好的DataFrame,并使用 show 方法显示:

df = sqlContext.read \
.json("file:///home/vagrant/datasets/users.json")
df.show()

输出:

+-------+------+-------+
|balance|gender|user_id|
+-------+------+-------+
|   10.0|  null|      0|
|    1.0|     M|      1|
|   -0.5|     F|      2|
|    0.0|     F|      3|
|    5.0|  null|      4|
|    3.0|     M|      5|
+-------+------+-------+

我们还可以使用 printSchema 方法查看DataFrame的模式:

df.printSchema()

输出:

root
 |-- balance: double (nullable = true)
 |-- gender: string (nullable = true)
 |-- user_id: long (nullable = true)

与RDBMS中的表一样,我们可以对DataFrame中的数据进行选择、过滤等操作。例如,我们要打印性别不为空且余额严格大于零的用户的余额、性别和用户ID:

# 使用filter和select方法
(df.filter(df['gender'] != 'null')
.filter(df['balance'] > 0)
   .select(['balance', 'gender', 'user_id'])
   .show())

# 使用SQL-like语法
(df.filter('gender is not null')
   .filter('balance > 0').select("*").show())

# 只使用一个filter方法
df.filter('gender is not null and balance > 0').show()
4.2 处理缺失数据

数据预处理中的一个常见问题是处理缺失数据。Spark DataFrame与pandas DataFrame类似,提供了广泛的操作。例如,最简单的方法是丢弃包含缺失信息的行:

df.na.drop().show()

如果这种操作删除了太多行,我们可以指定要考虑删除行的列:

df.na.drop(subset=["gender"]).show()

另外,我们也可以为每列设置默认值,而不是删除行数据:

df.na.fill({'gender': "U", 'balance': 0.0}).show()

通过以上步骤,我们可以在Spark中有效地进行数据科学实践,包括变量共享和数据预处理等关键操作。这些技术为在分布式环境中进行大规模数据处理和机器学习提供了强大的支持。

5. 使用SQL-like语法进行数据操作

在Spark DataFrame中,使用SQL-like语法可以更方便地对数据进行选择、过滤、连接、分组和聚合等操作,大大简化了数据预处理的过程。以下是一些常见操作的示例:

5.1 选择操作

选择特定列可以使用 select 方法,既可以传入列名列表,也可以使用SQL-like字符串:

# 使用列名列表
df.select(['balance', 'user_id']).show()

# 使用SQL-like字符串
df.select('balance', 'user_id').show()
5.2 过滤操作

过滤满足特定条件的数据可以使用 filter 方法,同样支持列名表达式和SQL-like字符串:

# 使用列名表达式
df.filter(df['balance'] > 0).show()

# 使用SQL-like字符串
df.filter('balance > 0').show()
5.3 连接操作

连接多个DataFrame可以使用 join 方法,示例如下:
假设我们有另一个包含用户额外信息的DataFrame df_extra

from pyspark.sql import Row

data_extra = [
    Row(user_id=0, age=25),
    Row(user_id=1, age=30),
    Row(user_id=2, age=35)
]
df_extra = sqlContext.createDataFrame(data_extra)

# 内连接
df.join(df_extra, on='user_id', how='inner').show()
5.4 分组和聚合操作

分组和聚合操作可以使用 groupBy agg 方法:

# 按性别分组,计算每组的平均余额
df.groupBy('gender').agg({'balance': 'avg'}).show()

# 也可以使用SQL-like字符串进行聚合
df.groupBy('gender').agg({'balance': 'avg'}).withColumnRenamed('avg(balance)', 'average_balance').show()

以下是一个操作类型与对应方法的表格总结:
| 操作类型 | 方法示例 |
| ---- | ---- |
| 选择 | select(['col1', 'col2']) select('col1', 'col2') |
| 过滤 | filter(df['col'] > value) filter('col > value') |
| 连接 | join(other_df, on='key_col', how='inner') |
| 分组和聚合 | groupBy('col').agg({'col_to_agg': 'agg_func'}) |

6. 特征工程算法

Spark提供了一些现成的算法用于特征工程,以下是一些常见算法及其使用示例。

6.1 独热编码

在前面的示例中我们已经使用过独热编码,这里再详细说明一下。独热编码可以将分类变量转换为二进制向量,方便机器学习算法处理。

from pyspark.ml.feature import OneHotEncoder, StringIndexer

# 对性别列进行独热编码
stringIndexer = StringIndexer(inputCol="gender", outputCol="genderIndex")
model = stringIndexer.fit(df)
indexed = model.transform(df)

encoder = OneHotEncoder(inputCol="genderIndex", outputCol="genderVec")
encoded = encoder.transform(indexed)
encoded.select('gender', 'genderIndex', 'genderVec').show()
6.2 标准化

标准化可以将特征缩放到均值为0,标准差为1的范围,有助于提高某些机器学习算法的性能。

from pyspark.ml.feature import StandardScaler
from pyspark.ml.linalg import Vectors

# 将数据转换为向量格式
df_vector = df.rdd.map(lambda x: (x[0], Vectors.dense(x[1:]))).toDF(["label", "features"])

scaler = StandardScaler(inputCol="features", outputCol="scaledFeatures",
                        withStd=True, withMean=False)

# 计算统计信息并生成模型
scalerModel = scaler.fit(df_vector)

# 对数据进行标准化
scaledData = scalerModel.transform(df_vector)
scaledData.select("features", "scaledFeatures").show()

以下是特征工程算法及其功能的表格:
| 算法名称 | 功能 |
| ---- | ---- |
| 独热编码 | 将分类变量转换为二进制向量 |
| 标准化 | 将特征缩放到均值为0,标准差为1的范围 |

7. 机器学习算法及性能评估

在分布式环境中,Spark提供了多种机器学习算法可供选择,并且可以方便地评估它们的性能。

7.1 可用的学习器

常见的学习器包括线性回归、逻辑回归、决策树等。以下是一个使用逻辑回归进行分类的示例:

from pyspark.ml.classification import LogisticRegression
from pyspark.ml.feature import VectorAssembler

# 假设我们要根据余额和性别预测用户是否活跃(这里简单假设余额大于0为活跃)
df = df.withColumn('is_active', (df['balance'] > 0).cast('double'))

# 特征组装
assembler = VectorAssembler(inputCols=['balance'], outputCol='features')
df = assembler.transform(df)

# 划分训练集和测试集
train_data, test_data = df.randomSplit([0.7, 0.3])

# 创建逻辑回归模型
lr = LogisticRegression(featuresCol='features', labelCol='is_active')

# 训练模型
model = lr.fit(train_data)

# 进行预测
predictions = model.transform(test_data)
predictions.select('is_active', 'prediction').show()
7.2 性能评估

评估模型性能可以使用各种指标,如准确率、召回率、F1值等。以下是计算准确率的示例:

from pyspark.ml.evaluation import MulticlassClassificationEvaluator

evaluator = MulticlassClassificationEvaluator(labelCol='is_active', predictionCol='prediction', metricName='accuracy')
accuracy = evaluator.evaluate(predictions)
print("Accuracy = %g" % accuracy)

以下是机器学习流程的mermaid流程图:

graph LR
    A[数据准备] --> B[特征工程]
    B --> C[划分训练集和测试集]
    C --> D[选择学习器]
    D --> E[训练模型]
    E --> F[进行预测]
    F --> G[性能评估]
8. 集群中的交叉验证和超参数优化

在集群环境中,可以使用交叉验证来进行超参数优化,以找到最佳的模型参数。

from pyspark.ml.tuning import ParamGridBuilder, CrossValidator

# 创建参数网格
paramGrid = ParamGridBuilder() \
    .addGrid(lr.regParam, [0.1, 0.01]) \
    .addGrid(lr.elasticNetParam, [0.0, 0.5, 1.0]) \
    .build()

# 创建交叉验证器
crossval = CrossValidator(estimator=lr,
                          estimatorParamMaps=paramGrid,
                          evaluator=evaluator,
                          numFolds=3)

# 运行交叉验证
cvModel = crossval.fit(train_data)

# 获取最佳模型
bestModel = cvModel.bestModel

# 使用最佳模型进行预测
best_predictions = bestModel.transform(test_data)
best_accuracy = evaluator.evaluate(best_predictions)
print("Best Accuracy = %g" % best_accuracy)

通过以上一系列操作,我们可以在Spark中完成从数据预处理、特征工程、模型训练到性能评估和超参数优化的完整机器学习流程,充分利用Spark在分布式环境中的优势,处理大规模数据并构建高效的机器学习模型。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值