Spark2.1.0入门:模型选择和超参数调整(Python版)

大数据学习路线图

【版权声明】博客内容由厦门大学数据库实验室拥有版权,未经允许,请勿转载!
返回Spark教程首页
推荐纸质教材:林子雨、郑海山、赖永炫编著《Spark编程基础(Python版)》

模型选择和超参数调整

在机器学习中非常重要的任务就是模型选择,或者使用数据来找到具体问题的最佳的模型和参数,这个过程也叫做调试(Tuning)。调试可以在独立的估计器中完成(如逻辑斯蒂回归),也可以在包含多样算法、特征工程和其他步骤的工作流中完成。用户应该一次性调优整个工作流,而不是独立的调整PipeLine中的每个组成部分。

1、 交叉验证和训练-验证切分

MLlib支持交叉验证(CrossValidator)和训练验证分割(TrainValidationSplit)两个模型选择工具。使用这些工具要求包含如下对象:

1.估计器:待调试的算法或管线。

2.一系列参数表(ParamMaps):可选参数,也叫做“参数网格”搜索空间。

3.评估器:评估模型拟合程度的准则或方法。

模型选择工具工作原理如下:

1.将输入数据划分为训练数据和测试数据。

  1. 对于每个(训练,测试)对,遍历一组ParamMaps。用每一个ParamMap参数来拟合估计器,得到训练后的模型,再使用评估器来评估模型表现。

3.选择性能表现最优模型对应参数表。

更具体的,交叉验证CrossValidato将数据集切分成k折叠数据集合,并被分别用于训练和测试。例如,k=3时,CrossValidator会生成3个(训练数据,测试数据)对,每一个数据对的训练数据占2/3,测试数据占1/3。为了评估一个ParamMap,CrossValidator 会计算这3个不同的(训练,测试)数据集对在Estimator拟合出的模型上的平均评估指标。在找出最好的ParamMap后,CrossValidator 会使用这个ParamMap和整个的数据集来重新拟合Estimator。也就是说通过交叉验证找到最佳的ParamMap,利用此ParamMap在整个训练集上可以训练(fit)出一个泛化能力强,误差相对小的的最佳模型。

交叉验证的代价比较高昂,为此Spark也为超参数调优提供了训练-验证切分TrainValidationSplit。TrainValidationSplit创建单一的(训练,测试)数据集对。它使用trainRatio参数将数据集切分成两部分。例如,当设置trainRatio=0.75时,TrainValidationSplit将会将数据切分75%作为数据集,25%作为验证集,来生成训练、测试集对,并最终使用最好的ParamMap和完整的数据集来拟合评估器。相对于CrossValidator对每一个参数进行k次评估,TrainValidationSplit只对每个参数组合评估1次。因此它的评估代价没有这么高,但是当训练数据集不够大的时候其结果相对不够可信。

2、 使用交叉验证进行模型选择

使用CrossValidator的代价可能会异常的高。然而,对比启发式的手动调优,这是选择参数的行之有效的方法。下面示例示范了使用CrossValidator从整个网格的参数中选择合适的参数。

首先,导入必要的包:

from pyspark.ml.linalg import Vector,Vectors
from pyspark.ml.feature import HashingTF,Tokenizer
from pyspark.ml.tuning import CrossValidator, ParamGridBuilder
from pyspark.sql import Row
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
from pyspark.ml.feature import IndexToString, StringIndexer, VectorIndexer
from pyspark.ml.classification import LogisticRegression,LogisticRegressionModel
from pyspark.ml import Pipeline, PipelineModel

接下来,读取Irisi数据集,分别获取标签列和特征列,进行索引、重命名,并设置机器学习工作流。交叉验证在把原始数据集分割为训练集与测试集。值得注意的是,只有训练集才可以用在模型的训练过程中,测试集则必须在模型完成之后才被用来评估模型优劣的依据。此外,训练集中样本数量必须够多,一般至少大于总样本数的 50%,且两组子集必须从完整集合中均匀取样。

def f(x):
    rel = {}
    rel['features'] = Vectors.dense(float(x[0]),float(x[1]),float(x[2]),float(x[3]))
    rel['label'] = str(x[4])
    return rel


data = spark.sparkContext.textFile("file:///usr/local/spark/iris.txt").map(lambda line: line.split(',')).map(lambda p: Row(**f(p))).toDF()
trainingData, testData = data.randomSplit([0.7,0.3])
labelIndexer = StringIndexer().setInputCol("label").setOutputCol("indexedLabel").fit(data)
featureIndexer = VectorIndexer().setInputCol("features").setOutputCol("indexedFeatures").fit(data)
lr = LogisticRegression().setLabelCol("indexedLabel").setFeaturesCol("indexedFeatures").setMaxIter(50)
labelConverter =  IndexToString().setInputCol("prediction").setOutputCol("predictedLabel").setLabels(labelIndexer.labels)
lrPipeline = Pipeline().setStages([labelIndexer, featureIndexer, lr, labelConverter])

可以使用ParamGridBuilder方便构造参数网格。其中regParam参数定义规范化项的权重;elasticNetParam是Elastic net 参数,取值介于0和1之间。elasticNetParam设置2个值,regParam设置3个值。最终将有(3 * 2) = 6个不同的模型将被训练。

paramGrid = ParamGridBuilder().addGrid(lr.elasticNetParam, [0.2,0.8]).addGrid(lr.regParam, [0.01, 0.1, 0.5]).build()

再接下来,构建针对整个机器学习工作流的交叉验证类,定义验证模型、参数网格,以及数据集的折叠数,并调用fit方法进行模型训练。其中,对于回归问题评估器可选择RegressionEvaluator,二值数据可选择BinaryClassificationEvaluator,多分类问题可选择MulticlassClassificationEvaluator。评估器里默认的评估准则可通过setMetricName方法重写。

cv = CrossValidator().setEstimator(lrPipeline).setEvaluator(MulticlassClassificationEvaluator().setLabelCol("indexedLabel").setPredictionCol("prediction")).setEstimatorParamMaps(paramGrid).setNumFolds(3) 
cvModel = cv.fit(trainingData)

接下来,调动transform方法对测试数据进行预测,并打印结果及精度。

lrPredictions=cvModel.transform(testData)
lrPreRel = lrPredictions.select("predictedLabel", "label", "features", "probability").collect()
for item in lrPreRel:
    print(str(item['label'])+','+str(item['features'])+'-->prob='+str(item['probability'])+',predictedLabel'+str(item['predictedLabel']))

Iris-setosa,[4.7,3.2,1.6,0.2]-->prob=[0.0124531315584,3.61495729015e-06,0.987543253484],predictedLabelIris-setosa
Iris-setosa,[4.8,3.0,1.4,0.1]-->prob=[0.0189306218612,4.68811238788e-06,0.981064690026],predictedLabelIris-setosa
Iris-setosa,[4.8,3.4,1.9,0.2]-->prob=[0.00954865220928,2.15004457391e-06,0.990449197746],predictedLabelIris-setosa
Iris-setosa,[4.9,3.1,1.5,0.1]-->prob=[0.0164788072778,3.34188478227e-06,0.983517850837],predictedLabelIris-setosa
Iris-versicolor,[5.0,2.0,3.5,1.0]-->prob=[0.699989993024,0.295573613818,0.00443639315824],predictedLabelIris-versicolor
Iris-setosa,[5.0,3.0,1.6,0.2]-->prob=[0.0385031367429,1.48064614591e-05,0.961482056796],predictedLabelIris-setosa
Iris-setosa,[5.0,3.4,1.6,0.4]-->prob=[0.0125473025277,4.16328341655e-06,0.987448534189],predictedLabelIris-setosa
......  

evaluator = MulticlassClassificationEvaluator().setLabelCol("indexedLabel").setPredictionCol("prediction")
lrAccuracy = evaluator.evaluate(lrPredictions)

最后,还可以获取最优的逻辑斯蒂回归模型,并查看其具体的参数:

>>> bestModel= cvModel.bestModel
>>> lrModel = bestModel.stages[2]
>>> print("Coefficients: " + str(lrModel.coefficientMatrix) + "Intercept: "+str(lrModel.interceptVector)+ "numClasses: "+str(lrModel.numClasses)+"numFeatures: "+str(lrModel.numFeatures))

Coefficients: DenseMatrix([[ 0.57547193, -0.3505967 ,  0.09896991, -0.01073066],
             [-0.00950188, -2.63413762,  0.96959059,  3.81872308],
             [-0.63212011,  3.61557516, -1.24757936, -2.20366198]])Intercept: [0.0988591576682,-0.14796420754,0.0491050498716]numClasses: 3numFeatures: 4


>>> lr.explainParam(lr.regParam)

'regParam: regularization parameter (>= 0). (default: 0.0)'

>>> lr.explainParam(lr.elasticNetParam)

'elasticNetParam: the ElasticNet mixing parameter, in range [0, 1]. For alpha = 0, the penalty is an L2 penalty. For alpha = 1, it is an L1 penalty. (default: 0.0)'