Spark内的网格搜索主要有两种评估流程,分别是 交叉验证 和 训练验证集拆分,这篇文章主要介绍训练验证集拆分
的具体流程
训练集(train):训练模型
验证集(val):评估模型
测试集(test):一旦找到了最佳参数,就开始最终训练
使用训练集训练多个网络模型,再使用验证集测试这些网络,找到得分最高的那个网络作为我们选择的最佳网络,再将训练集和验证集合并,重新训练这个最佳网络,得到最佳网络参数。
用于验证回归模型的评估算法,如:ALS,线性回归等……
val metricName: Param[String]
"rmse" (default): root mean squared error
"mse": mean squared error
"r2": R2 metric
"mae": mean absolute error
用于验证二分分类模型的评估算法:如判断(1,0)或(是,否)
val metricName: Param[String]
param for metric name in evaluation (supports "areaUnderROC" (default), "areaUnderPR")
用于判断多分类,当然适用于上面的二分类
val metricName: Param[String]
param for metric name in evaluation (supports "f1" (default), "weightedPrecision", "weightedRecall", "accuracy")
用于聚类模型的评估 越接近1,表明效果越好
val metricName: Param[String]
param for metric name in evaluation (supports "silhouette" (default))
记录一次较为简易的模型训练过程:
训练模型分为三步:
- 1.训练集训练模型
- 2.验证集评估模型
- 3.测试集最终训练
import org.apache.spark.ml.clustering.KMeans
import org.apache.spark.ml.evaluation.ClusteringEvaluator
//数据集
//dataset:测试集 training:训练集 vali:验证集
val dataset = spark.read.format("libsvm").load("data/mllib/sample_kmeans_data.txt")
//将测试集按7:3的比例切分为 训练集和验证集
val Array(training,vali)=dataset.randomSplit(Array(0.7,0.3))
// 训练Kmeans模型
//Kmeans超参数
val kmeans = new KMeans().setK(2).setSeed(1L)
// 使用训练集训练模型
val model = kmeans.fit(training)
// Kmeans为聚类模型,使用聚类指标评估
val evaluator = new ClusteringEvaluator()
// 使用验证集参与评估
val predictions = model.transform(vali)
val silhouette = evaluator.evaluate(predictions)
println(silhouette)
//若评估效果符合预期,即silhouette接近1
val real_model=kmeans.fit(dataset)
根据上文所说的模型评估Example
,我们可以通过变量silhouette的值,来不断调整模型的参数,使其接近于1。这里有个较为方便的方法,快速找到较为合适的参数——网格搜索
网格搜索算法是一种通过遍历给定的参数组合来优化模型表现的方法
为何使用:超参数选择不恰当,就会出现欠拟合或者过拟合的问题
内容: 网格搜索,搜索的是参数,即在指定的参数范围内,按步长依次调整参数,利用调整的参数训练学习器,从所有的参数中找到在验证集上精度最高的参数,这其实是一个训练和比较的过程。
Grid Search:一种调参手段;穷举搜索:在所有候选的参数选择中,通过循环遍历,尝试每一种可能性,表现最好的参数就是最终的结果
用法:网格搜索适用于三四个(或者更少)的超参数(当超参数的数量增长时,网格搜索的计算复杂度会呈现指数增长,这时候则使用随机搜索),用户列出一个较小的超参数值域,这些超参数至于的笛卡尔积(排列组合)为一组组超参数。网格搜索算法使用每组超参数训练模型并挑选验证集误差最小的超参数组合
缺点:遍历所有组合,比较耗时
import org.apache.spark.ml.clustering.KMeans
import org.apache.spark.ml.evaluation.ClusteringEvaluator
import org.apache.spark.ml.tuning.{ParamGridBuilder, TrainValidationSplit, TrainValidationSplitModel}
//数据集
val dataset = spark.read.format("libsvm").load("data/mllib/sample_kmeans_data.txt")
// 训练Kmeans模型
//Kmeans超参数
val kmeans = new KMeans()
/**
* 网格搜索:
* 对所有addGrid()内的超参数数组进行排列组合,rmse越小,模型精确度越高
* 排列组合的参数不建议太多,网格搜索相当于所有组合遍历一遍
* 这里会对maxIter和k进行排列组合 如:
* 第一次训练 maxIter=200,k=5
* 第二次训练 maxIter=200,k=10
* ……
* 所有排列组合训练完后,根据评估模型,筛选出最合适的模型
*/
val paramGrid = new ParamGridBuilder()
.addGrid(kmeans.maxIter, Array(200, 400, 600))
.addGrid(kmeans.k, Array(5, 10, 20))
.build()
// Kmeans为聚类模型,使用聚类指标评估
val evaluator = new ClusteringEvaluator()
val trainValidationSplit = new TrainValidationSplit()
//设置预测模型
.setEstimator(kmeans)
//设置评估模型
.setEvaluator(evaluator)
//训练集、验证集划分 训练集为$ratio 验证集为1-$ratio
.setTrainRatio(0.7)
//网格搜索参数
.setEstimatorParamMaps(paramGrid)
//预测seed
.setSeed(1L)
//训练
//该方法将自动完成`模型评估Example`中的一二步,找到最适合的评估模型后,用测试集dataset训练最终模型
val final_model=trainValidationSplit.fit(dataset)
//打印参数列表
println(final_model.bestModel.parent.extractParamMap())
ALS模型网格调参时遇到了一些坑,这里列举一下有坑的地方(其实都是同一个原因造成的)
1.模型的最优参数,每次都是网格搜索排列组合的第一个
如:
val paramGrid = new ParamGridBuilder()
.addGrid(als.maxIter, Array(500,800,1000))
.addGrid(als.rank, Array(5,10,15))
.build()
上述代码设置的网格参数,在使用网格搜索遍历后,最优参数必是 maxIter=500,rank=5
2.查看rmse时,全是NaN
model.validationMetrics=Array(NaN,NaN,Nan)
先说结论:
造成这些结果的主要原因,还是ALS冷启动策略设置错误的缘故。ALS模型默认遇到未知UserCol的用户时(即没参与过运算的userId),会将prediction置为NaN。而评估模型进行计算时,若prediction的值有Nan数据,会导致最后的评估结果值也为NaN。如上述第二点所示。
设一个评分表,有userCol,itemCol,rating三个字段,且全表数据不会重复。
UserCol | ItemCol | Rating |
---|---|---|
A | a | 5.0 |
B | b | 5.0 |
C | c | 1.0 |
D | d | 2.0 |
E | e | 2.0 |
TrainValidationSplit方法在遍历最优参数时,是将训练集和验证集是按照setTrainRatio($ratio)的比例随机分配,假设ratio=0.8,则训练集与验证集的比例则为8:2,上表将有四条数据(ABCD)参与训练,一条数据(E)参与验证。因ALS模型只能预测参与计算的数据,验证集用户E的prediction=NaN。
TrainValidationSplit遍历过程的大致代码:
……
val est = $(estimator)
val eval = $(evaluator)
val epm = $(estimatorParamMaps)
val Array(trainingDataset, validationDataset) =
dataset.randomSplit(Array($(trainRatio), 1 - $(trainRatio)), $(seed))
trainingDataset.cache()
validationDataset.cache()
……
val metricFutures = epm.zipWithIndex.map { case (paramMap, paramIndex) =>
Future[Double] {
val model = est.fit(trainingDataset, paramMap).asInstanceOf[Model[_]]
if (collectSubModelsParam) {
subModels.get(paramIndex) = model
}
// TODO: duplicate evaluator to take extra params from input
val metric = eval.evaluate(model.transform(validationDataset, paramMap))
logDebug(s"Got metric $metric for model trained with $paramMap.")
metric
}(executionContext)
}
……
val (bestMetric, bestIndex) =
if (eval.isLargerBetter) metrics.zipWithIndex.maxBy(_._1)
else metrics.zipWithIndex.minBy(_._1)
logInfo(s"Best set of parameters:\n${epm(bestIndex)}")
logInfo(s"Best train validation split metric: $bestMetric.")
val bestModel = est.fit(dataset, epm(bestIndex)).asInstanceOf[Model[_]]