Spark排序算法系列之(MLLib、ML)GBDT+LR使用方式介绍(模型训练、保存、加载、预测)

转载请注明出处:http://blog.csdn.net/gamer_gyt
博主微博:http://weibo.com/234654758
Github:https://github.com/thinkgamer
公众号:搜索与推荐Wiki
个人网站:http://thinkgamer.github.io


【Spark排序算法系列】主要介绍的是目前推荐系统或者广告点击方面用的比较广的几种算法,和他们在Spark中的应用实现,本篇文章主要介绍LR算法。

本系列还包括(持续更新):

  • Spark排序算法系列之GBDT(梯度提升决策树)
  • Spark排序算法系列之LR(逻辑回归)
  • Spark排序算法系列之XGBoost
  • Spark排序算法系列之FTRL(Follow-the-regularized-Leader)
  • Spark排序算法系列之FM与FFM

背景

关于LR和GBDT的介绍和Spark实现可参考上边的链接,这里再简单做下说明。

LR 是最成熟、业界使用最广泛的模型,由于其简单、可解释、易大规模并行、线上预测速度快等优点被广泛应用。但其对特征工程强依赖,其预测能力完全取决于特征的丰富度,费时吃力且不一定有效。

GBDT 是非常经典的统计学习模型,是一种非参数学习模型,能较好的拟合非线性。其基于Boosting思想,迭代计算出一系列简单决策树,其中后一棵树用于拟合他前边所有树的残差。其优点是:学习能力强、可解释性好、能拟合数据中复杂的非线性模式、擅长处理连续型特征、相比LR/SVM降低了人工处理特征的工作量;缺点是:模型复杂度高、资源消耗严重。

GBDT+LR gbdt模型的基模型是单棵CART决策树,其原理使得他能发现特征的相对重要度,并进行自动特征组合——从根节点到叶子节点的一条路径即是一组用多个特征对样本进行判别的规则,不同类别的样本通过GBDT往往会激活不同的叶子节点集,因此样本所激活的叶子节点的分布情况就可以视为一种特征的组合。 于是将GBDT模型本身用作一种特征的组合器,用他自动构造有效的特征组合,然后再将这些特征输入简单分类器执行最终的分类任务。

图中共有两棵树,x为一条输入样本,遍历两棵树后,x样本分别落到两颗树的叶子节点上,每个叶子节点对应LR一维特征,那么通过遍历树,就得到了该样本对应的所有LR特征。构造的新特征向量是取值0/1的。举例来说:上图有两棵树,左树有三个叶子节点,右树有两个叶子节点,最终的特征即为五维的向量。对于输入x,假设他落在左树第一个节点,编码[1,0,0],落在右树第二个节点则编码[0,1],所以整体的编码为[1,0,0,0,1],这类编码作为特征,输入到LR中进行分类。

Spark实现

main 函数调用GBDT函数,生成新的特征,相关参数

// 和下边代码中加载的gbdt模型使用的数据集一致
val file = "data/new_sample_libsvm_data.txt"
val gbdt_model_path = "model/gbdt/model.obj"
// 是否重新训练gbdt模型,若为false 则为从文件中加载
val is_train_gbdt = false
// 是否将原始特征拼接在经过gbdt转化后的特征后边
val is_append_feature = false

创建spark对象并加载数据集

val spark = SparkSession.builder()
	.master("local[5]")
	.appName("LR_With_Gbdt")
	.getOrCreate()
Logger.getRootLogger.setLevel(Level.WARN)

// 加载数据
val data = MLUtils.loadLibSVMFile(spark.sparkContext,file)

创建新的数据集

val newData = is_train_gbdt match{
	case true =>{
		// 重新训练gbdt模型
		None
	}
	case false => {
		// 加载指定的模型
		val model: GradientBoostedTreesModel = loadModel(gbdt_model_path).get
		// gbdt模型返回树的节点,这里使用的是训练好的gbdt模型,且假设训练好的模型已经是较优的
		getNodeListWithGBDT(data, model, spark, is_append_feature)
	}
}

创建LR模型进行预测

val split = newData.get.randomSplit(Array(0.7,0.3))
	val (train:RDD[LabeledPoint], test:RDD[LabeledPoint]) = (split(0),split(1))
	// 使用新构造的特征训练LR模型
	val model = new LogisticRegressionWithLBFGS().setNumClasses(2).run(train)
	test.map( line=>{
		val predictScore = model.predict(line.features)
		(line.label, predictScore)
	}).take(10).foreach(println)

gbdt生成新的特征实现

def getLeafNodes(node: Node) :Array[Int]= {
	var treeLeafNodes = new Array[Int](0)
	if(node.isLeaf){
		treeLeafNodes = treeLeafNodes.:+(node.id)  // :+ 方法往数组中追加内容
	}else{
		treeLeafNodes =  treeLeafNodes ++ getLeafNodes(node.leftNode.get)
		treeLeafNodes =  treeLeafNodes ++ getLeafNodes(node.rightNode.get)
	}
	treeLeafNodes
}

def predictModify(node: Node, features: linalg.Vector) : Int= {
	val split = node.split
	if(node.isLeaf){
		node.id // 如果深度为1,则返回编号为0
	}
	else{
		// Features: Feature = 407 threshold = 72.0 featureType = Continuous categories = List()
		// split.get.featureType 获取节点类型 Continuous 表示还有子节点 Categorical 表示是叶节点
		// split.get.threshold 节点分隔值
		if (split.get.featureType == FeatureType.Continuous) {
			if (features(split.get.feature) <= split.get.threshold) {
//					println("Continuous Left Node")
				predictModify(node.leftNode.get, features)
			} else {
//				println("Continuous Right Node")
				predictModify(node.rightNode.get, features)
			}
		} else {
			if (split.get.categories.contains(features(split.get.feature))) {
//				println("Categorical Left Node")
				predictModify(node.leftNode.get, features)
			} else {
//				println("Categorical Right Node")
				predictModify(node.rightNode.get, features)
			}
		}
	}
}

def getNodeListWithGBDT(data: RDD[LabeledPoint], model: GradientBoostedTreesModel, spark: SparkSession, isAppend: Boolean): Option[RDD[LabeledPoint]] = {
	val numTrees = model.numTrees
	// 存放每棵树的叶子节点编号
	val treeLeafArray = new Array[Array[Int]](numTrees)
	for(i <- 0.until(numTrees)){        // for(i <- 0 until numTrees){
		treeLeafArray(i) = getLeafNodes(model.trees(i).topNode)
    //		println(treeLeafArray(i).mkString(","))
	}

	// gbdt构造新特征
	val newData:RDD[LabeledPoint] = data.map(line => {
		var newFeature = new Array[Double](0)
		// 遍历每组特征落在每棵树的节点编号
		for(i <- 0.until(numTrees)){
			// 获取特征所在的节点编号
			val treePredict = predictModify(model.trees(i).topNode,line.features)
			// gbdt是二叉树
			val treeArray = new Array[Double]( (model.trees(i).numNodes+1)/2)
			treeArray(treeLeafArray(i).indexOf(treePredict)) = 1
			newFeature = newFeature ++ treeArray
		}
		if(isAppend){
			new LabeledPoint(line.label, Vectors.dense(newFeature ++ line.features.toArray))
		}else{
			new LabeledPoint(line.label, Vectors.dense(newFeature))
		}
	})
	newData.take(10).foreach(println)
	Option(newData)
}

def loadModel(path: String): Option[GradientBoostedTreesModel] = {
	try{ // 尝试加载模型
		val in = new ObjectInputStream( new FileInputStream( path))
		val model = Option(in.readObject().asInstanceOf[GradientBoostedTreesModel])
		in.close()
		model
	}catch {
		case ex:ClassNotFoundException => {
			println(ex.printStackTrace())
			None
		}
		case ex:IOException => {
			println(ex.printStackTrace())
			None
		}
		case _: Throwable => {
			throw new Exception
		}
	}
}

GBDT+LR局限性

GBDT+LR能取代特征工程,真正地解放特征交叉、组合、高阶特征设计这些过程吗?笔者认为并不能。主要原因是GBDT模型的结构并不是为了特征组合而专门设计的,只是它的基模型——决策树的天然结构使其恰好有类似于特征组合的功能,于是就将其拿来做这项工作。此外,它构造组合特征的形式比较单一:贪心地以最小化平方误差或基尼指数的方式构造决策路径,并以其对应的一组分支规则形成特征组合,这实际上并没有解决二阶乃至高阶特征交叉的问题,且对稀疏特征的学习能力较弱。因此在后续的迭代中,我们将尽快跟进FM/FFM, GBDT+FM, DNN, Wide & Deep等特征组合能力更强的模型。

参考资料

  • http://ihuafan.com/算法/gbdtlr-push-notification#特征和模型训练
  • https://www.deeplearn.me/1944.html

Spark排序算法系列之(MLLib、ML)GBDT+LR使用方式介绍(模型训练、保存、加载、预测)_第1张图片
打开微信扫一扫,关注微信公众号【搜索与推荐Wiki】

你可能感兴趣的:(搜索与排序)