Pyspark机器学习

2020-02-24  本文已影响0人  ChanZeeBm

项目概述&目的:这是一个虚拟的音乐服务数据集,拥有过千万用户,用户可以随时升级、降级、取消他们的套餐。用户的动态、意向可以直接影响到服务的盈利;而每次用户的操作都会被记录(即具体动作例如收藏、升级、降级、播放歌曲、添加歌单等),这些数据对于服务商而言有着重要价值,可从该数据中发现某些用户的某些操作的共通点,来判断该用户接下来会进行什么样的操作, 本次任务的目标是寻找潜在客户,而潜在客户也分为潜在意向客户和流失客户,本次我们要利用机器学习找到那些流失(即将流失)的客户,寻找他们的共同特征,利用优惠、试用等手段控制损失。

环境

安装
pip install pyspark
技术特点

pyspark采用了懒加载模式(需要真正运行的命令才会执行相关的指令),这种方式的优点是减少资源的开销,加快程序的开发。

流程
最终评估标准:

Accuracy:准确率,评价一个模型最直观的值,在验证集上准确率越高,模型越完美。

具体步骤

1. 导入所需库
# import libraries
from pyspark.sql import SparkSession
from pyspark.sql.functions import udf, concat, col, desc, year, month, asc, count, avg, countDistinct
from pyspark.sql.types import IntegerType
import pyspark.sql.functions as func

from pyspark.ml import Pipeline
from pyspark.ml.evaluation import  MulticlassClassificationEvaluator
from pyspark.ml.feature import VectorAssembler,Normalizer,StandardScaler,IDF,StringIndexer
from pyspark.ml.regression import LinearRegression
from pyspark.ml.classification import LogisticRegression, RandomForestClassifier
from pyspark.ml.evaluation import BinaryClassificationEvaluator
from pyspark.ml.tuning import ParamGridBuilder, CrossValidator
# from matplotlib import pyplot as plt
from datetime import date
from functools import reduce
import numpy as np
2. 创建spark对象
# create a Spark session
spark = SparkSession.builder.appName("sparkify").getOrCreate()
3. 加载数据
data = spark.read.json("s3n://udacity-dsnd/sparkify/sparkify_event_data.json")
4. 检索数据
# check detail for data
print(data.count())
print(data.describe())
print(data.printSchema())
print(data.show())
结果

图上的1、2、3、4分别由上而下对应代码,我们的数据一共有....好多行,2为数据类型,3为数据类型的总览,4为具体数据。

5. 清理数据,把userId和sessionId的为空(也就是无意义)的数据清理掉:
data = data.dropna(how = "any",subset=["userId","sessionId"])
data = data.filter(data["userId"] != "")
6. 定义客户流失

观察page,会发现有以下几个值:


image.png

可以看到该日志记录了用户的一些操作,其中Cancellation Confirmation和Downgrade这两个动作意味着用户(即将)流失,所以有这两个动作其一的将标志为Churn = 1(意味着需要进行一系列挽留操作的用户),其他则为0:

churn_func = udf(lambda x: 1 if x == "Cancellation Confirmation" or x == "Downgrade" else 0, IntegerType())
data = data.withColumn("Churn", churn_func(data.page))
7. 可视化

PS: 由于大数据集上无法加载pandas,所以利用小数据集(从大数据集提取的部分数据,两个数据之间运行的换境不一样,一个是在AWS,一个是在本地换境)作为展示:

# length visual
data_pd = data.select("length").toPandas()
data_pd.plot(kind = "hist", bins = 500)
plt.xlim(0, 550)
image.png
gender_pd = data.orderBy(desc("Churn")).dropDuplicates(subset = ["userId"]).where(col("Churn") == 1).groupBy("gender").agg(count("gender").alias("count")).toPandas()
gender_pd.plot(kind = "bar", x = "gender", y = "count")
Gender Ratio.png
8. 特征
  1. Numerical Features
# count songs
df_songs = data.groupBy("userId").agg(countDistinct("song").alias("countSong")).orderBy("userId")

# calc avg listen time
df_avg_length = data.groupBy("userId").agg(avg("length").alias("avgLength")).orderBy("userId")

# count all artist for each user
df_singers = data.dropDuplicates(["userId", "artist"]).groupBy("userId").agg(count("artist").alias("countArtist")).orderBy("userId")
  1. Categories Features
# select category features
df_catgory = data.select(["userId", "gender", "level", "location", "method"])
df_catgory = df_catgory.dropDuplicates(["userId"]).orderBy(desc("userId"))
合并&清理特征
#  user may have 2 Churn values, we keep value 1 only
df_calced = data.select("Churn","userId").orderBy(desc("Churn")).dropDuplicates(["userId"])
# join all features
for feature in [df_songs, df_avg_length, df_singers, df_catgory]:
    df_calced = df_calced.join(feature, ["userId"], how="left")
# drop all na value
df_calced = df_calced.na.drop()
df_calced = df_calced.dropna(how = "any")
把类型字段转换为数字
# convert category features to numberic
for index in ["gender", "level", "location", "method"]:
    indexer = StringIndexer(inputCol=index, outputCol=f"{index}Indexer")
    _fit = indexer.fit(df_calced)
    df_calced = _fit.transform(df_calced)
把所有特征转换为Pyspark可以处理的向量
# convert all features to vector
assembler = VectorAssembler(inputCols=["countSong", "avgLength", "countArtist", "genderIndexer", "levelIndexer", "locationIndexer", "methodIndexer"], outputCol="featuresVec")
df_calced = assembler.transform(df_calced)
缩放向量

# standarded scaler the features column
stander = StandardScaler(inputCol="featuresVec", outputCol="features")
stander_fit = stander.fit(df_calced)
df_calced = stander_fit.transform(df_calced)
建模

从合并&清理完的数据集中随机提取70%、15%、15%分别作为训练、测试、验证数据集:

# use 70% data as train dataset, and 15% for test and val dataset
train_dataset, test_dataset, val_dataset = df_calced.randomSplit([0.7, 0.15, 0.15], seed = 58)
# build  the evaluator
evaluator = BinaryClassificationEvaluator(rawPredictionCol="prediction",labelCol="Churn")
# build linear regression model
lr = LinearRegression(labelCol="Churn", featuresCol="features", fitIntercept=False, regParam=0.0, solver="normal")
model = lr.fit(train_dataset)

# train the dataset by using linear regression model
train_pre = model.transform(train_dataset)
test_pre = model.transform(test_dataset)

# use the evaluator the evaluate train and test dataset
print("LinearRegression Accuracy for train dataset:", evaluator.evaluate(train_pre))
print("LinearRegression Accuracy for test dataset:", evaluator.evaluate(test_pre))

# final result
# LinearRegression Accuracy for train dataset: 0.6678041271593208
# LinearRegression Accuracy for test dataset: 0.9106971716581354
# build logistregression model
logist_lr = LogisticRegression(labelCol="Churn", featuresCol="features")
logist_model = logist_lr.fit(train_dataset)

# train the dataset by using logist regression model
train_pre_logist = logist_model.transform(train_dataset)
test_pre_logist = logist_model.transform(test_dataset)

# use the evaluator the evaluate train and test dataset
print("LogistRegression Accuracy for train dataset:", evaluator.evaluate(train_pre_logist))
print("LogistRegression Accuracy for test dataset:", evaluator.evaluate(test_pre_logist))

#Final result
# LogistRegression Accuracy for train dataset: 0.5971902287939127
# LogistRegression Accuracy for test dataset: 0.6027563842723956
# build RandomForest model
forest = RandomForestClassifier(labelCol="Churn", featuresCol="features", maxDepth=10)
forest_model = forest.fit(train_dataset)

# train the dataset by using Random Forest model
train_pre_forest = forest_model.transform(train_dataset)
test_pre_forest = forest_model.transform(test_dataset)

# use the evaluator the evaluate train and test dataset
print("RandomForest Regression Accuracy for train dataset:", evaluator.evaluate(train_pre_forest))
print("RandomForest Regression Accuracy for test dataset:", evaluator.evaluate(test_pre_forest))

# Final result
# RandomForest Regression Accuracy for train dataset: 0.6351333140056861
# RandomForest Regression Accuracy for test dataset: 0.8192344933184481
网格搜索,超参数调优(以下部分仅运行在小数据集换境,AWS不知道为啥老是error404)

首先为各个模型创建超参数字典:

# create hyper-paramter dict for each module
linearregression_dict = {
    lr.regParam: [0, 0.01, 0.1],
    lr.fitIntercept: [True, False],
    lr.maxIter: [10, 30, 50]
}
logistregression_dict = {
    logist_lr.maxIter: [10, 30, 50],
    logist_lr.fitIntercept: [True, False],
    logist_lr.regParam: [0, 0.01, 0.1]
}
forest_dict = {forest.maxDepth : [*range(10, 40, 10)], 
            forest.minInstancesPerNode : [*range(1, 30, 6)], 
            forest.maxBins : [*range(2, 33, 8)], 
            forest.numTrees: [*range(3, 30, 9)]}

合并各个字典为一体,以便后面的操作:

# combine all to ({module_name}, {module}, {param}, {evaluators})
all_module_dicts = [linearregression_dict, logistregression_dict, forest_dict]
params_maps = [reduce(lambda gb, param: gb.addGrid(*param), 
                    module.items(), 
                    ParamGridBuilder()).build() for module in all_module_dicts]

all_module_names = ["LinearRegression", "LogistRegression", "RandomForestRegression"]
all_modules = [lr, logist_lr, forest]
all_evaluates = [binary_evaluator] + [evaluator] * 2
all_module_combine = list(zip(all_module_names,all_modules, params_maps, all_evaluates))

开始拟合各个模型的各个超参数

# cross valiator every hyper-paramter
modules = {}
for module_name, module, param, eva in all_module_combine:
    cross = CrossValidator(estimator=module, estimatorParamMaps=param, evaluator=eva ,numFolds=3)
    cModel = cross.fit(train)
    
    train_result = cModel.transform(train)
    test_result = cModel.transform(test)
    accuracy, f1 = ("accuracy", "f1") if isinstance(eva, MulticlassClassificationEvaluator) else ("areaUnderPR", "areaUnderROC")
    print(f"{module_name} {accuracy.capitalize()} for Train dataset:", eva.evaluate(train_result, {eva.metricName: accuracy}))
    print(f"{module_name} {f1.capitalize()} for Train dataset:", eva.evaluate(train_result, {eva.metricName: f1}))

    print(f"{module_name} {accuracy.capitalize()} for Test dataset:", eva.evaluate(test_result, {eva.metricName: accuracy}))
    print(f"{module_name} {f1.capitalize()} for Test dataset:", eva.evaluate(test_result, {eva.metricName: f1}))
    
    print(f"Best params for {module_name}:", cModel.getEstimatorParamMaps()[np.argmax(cModel.avgMetrics)])
    print()
    
    modules[module_name] = {"module": cModel, "evaluator": eva}

最后运行得到结果:

LinearRegression Areaunderpr for Train dataset: 0.9709799102289998
LinearRegression Areaunderroc for Train dataset: 0.9107775693141545
LinearRegression Areaunderpr for Test dataset: 0.9882510716003329
LinearRegression Areaunderroc for Test dataset: 0.9357142857142857
Best params for LinearRegression: {Param(parent='LinearRegression_fb672c955ba6', name='regParam', doc='regularization parameter (>= 0).'): 0.01, Param(parent='LinearRegression_fb672c955ba6', name='fitIntercept', doc='whether to fit an intercept term.'): True, Param(parent='LinearRegression_fb672c955ba6', name='maxIter', doc='max number of iterations (>= 0).'): 10}

LogistRegression Accuracy for Train dataset: 0.8417721518987342
LogistRegression F1 for Train dataset: 0.8464320489636945
LogistRegression Accuracy for Test dataset: 0.8787878787878788
LogistRegression F1 for Test dataset: 0.8787878787878788
Best params for LogistRegression: {Param(parent='LogisticRegression_8f718c7ac443', name='maxIter', doc='max number of iterations (>= 0).'): 10, Param(parent='LogisticRegression_8f718c7ac443', name='fitIntercept', doc='whether to fit an intercept term.'): False, Param(parent='LogisticRegression_8f718c7ac443', name='regParam', doc='regularization parameter (>= 0).'): 0.0}

RandomForestRegression Accuracy for Train dataset: 0.8544303797468354
RandomForestRegression F1 for Train dataset: 0.857910562223808
RandomForestRegression Accuracy for Test dataset: 0.8181818181818182
RandomForestRegression F1 for Test dataset: 0.8371628371628371
Best params for RandomForestRegression: {Param(parent='RandomForestClassifier_4a8ffb100f5f', name='maxDepth', doc='Maximum depth of the tree. (>= 0) E.g., depth 0 means 1 leaf node; depth 1 means 1 internal node + 2 leaf nodes.'): 10, Param(parent='RandomForestClassifier_4a8ffb100f5f', name='minInstancesPerNode', doc='Minimum number of instances each child must have after split. If a split causes the left or right child to have fewer than minInstancesPerNode, the split will be discarded as invalid. Should be >= 1.'): 13, Param(parent='RandomForestClassifier_4a8ffb100f5f', name='maxBins', doc='Max number of bins for discretizing continuous features.  Must be >=2 and >= number of categories for any categorical feature.'): 18, Param(parent='RandomForestClassifier_4a8ffb100f5f', name='numTrees', doc='Number of trees to train (>= 1).'): 21}

最后的测试结果可以看到是线性回归的准确率最高,最优超参数分别为:regParam=0.01,fitIntercept=True,maxIter=10,所以利用线性回归作为最终模型来测试验证集:

# base on final result , use linearregression as the final model
final_module_dict = modules["LinearRegression"]
final_module = final_module_dict["module"]
val_tr = final_module.transform(val)
final_evalutor = final_module_dict["evaluator"]
print("Val dataset accuracy:", final_evalutor.evaluate(val_pre, {final_evalutor.metricName: "areaUnderPR"}))
# Final result
# Val dataset areaUnderPR: 0.9615486013517344

总结:

评价指标

机器学习深度学习结果定义只有4种:

Accuracy(准确率)

字面意思,就是预测正确的概率,那么很容易总结:


image.png

当一份训练数据不同类别或者说不同特征的条目差距较大时,准确率就会收到较高的影响,占比越大的条目对准确率影响越大。

Precision(精确率)

表示模型预测正确的正样本(预测正确的负样本不算)数量和预测为正的样本数量的比值:


image.png
Recall(召回率)

表示模型预测正确的正样本(预测正确的负样本不算)数量和实际上为正的样本数量的比值:


image.png

概括起来就是精准率 = 我们预测我们关注的事件,它相应的有多准确。(比如我预测这场球皇马会赢,而"这场球"就是所关注的事件,结合最终球赛成绩来判断精准率)
召回率 = 我们关注的事件真实的发生了,在真实发生的我们关注的事件中,我们成功预测了多少。(比如比赛成绩出来了,我一开始预测皇马在5场比赛中有2场会赢,而最终皇马赢了3场,那么我们的召回率就是3/2)
但是Precision和Recall是一对相爱相杀的存在,当样本量差不多的时候,其中一方越高意味着另外一方越低,所以最终需要F1-Score来衡量一个模型的健壮程度。

F1-Score

F1-Score是取Precision和Recall的调和平均值:


image.png

难点

过程中发现超参数调优是比较难的,而且由于数据集庞大,每次运行都要花飞好长时间,在AWS里面时间意味着美元;但完成了第一版项目以后觉得各个模型之间的优势劣势和侧重点依然不太清晰明了,以上三个模型都是在心里牢记的模型,至于哪一个更适用于本次项目,脑海里始终是"二元分类线性回归较好",对概念还不是很明确,可能在以后的工作中才能慢慢熟悉。

代码的改进

一开始没有使用网格搜索导致完全使用参数默认值去判定模型的优劣,这是不准确的,后来经过多次搜索后慢慢掌握了网格搜索的用法(课程里面的恕在下愚笨),才慢慢通过网格搜索来对超参数进行调优。

参考:
https://stackoverflow.com/questions/52498970/how-to-get-the-best-hyperparameter-value-after-crossvalidation-in-pyspark
https://spark.apache.org/docs/2.0.1/api/java/org/apache/spark/ml/evaluation/BinaryClassificationEvaluator.html
https://spark.apache.org/docs/latest/api/python/pyspark.mllib.html#pyspark.mllib.evaluation.BinaryClassificationMetrics
https://stackoverflow.com/questions/37707305/pyspark-multiple-conditions-in-when-clause

上一篇下一篇

猜你喜欢

热点阅读