IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    [原]Spark ALS算法推荐结果一样?

    fansy1990发表于 2016-10-05 16:41:12
    love 0

    在进行Spark ALS算法进行试验的时候发现模型对所有用户其推荐结果是一样的,即针对所有用户建模得到的模型对其推荐的项目是一样的,只是评分有比较小的差异。下面就分3个部分来进行分析,分别是实验过程及结果描述,ALS算法原理,问题分析及解决。

    实验过程及结果

    此部分参考:https://databricks-training.s3.amazonaws.com/movie-recommendation-with-mllib.html(里面有两个部分有点问题,下面会有描述,不过也可能是我理解错误了)。1. 上传数据到HDFS,需要上传ratings。dat  文件以及movies.dat文件,上传后到数据如下图所示:

    2. 打开Spark shell 进行Spark ALS算法调用;

    3. 加载电影信息数据以及评分数据,其代码如下所示:

    // 设置日志级别
    sc.setLogLevel(“WARN”)
    // 导入必要的包
    import org.apache.spark.mllib.recommendation._
    
    // 加载movies 数据到map
    val movies = sc.textFile("/user/root/als/movies.dat").map{line => val fields = line.split("::") ; (fields(0).toInt,fields(1))}.collect.toMap
    
    // 加载评分数据
    val ratings = sc.textFile("/user/root/als/ratings.dat").map{line => val fields = line.split("::");val rating = Rating(fields(0).toInt,fields(1).toInt,fields(2).toDouble);val timestamp = fields(3).toLong %10; (timestamp,rating)}
    
    // 输出统计信息
    println(ratings.count)
    println(ratings.map(_._2.user).distinct.count)
    println(ratings.map(_._2.product).distinct.count)

    4.  分割数据到训练集和测试集,其代码如下所示:

    // 分训练集
    val training = ratings.filter(x=>x._1<6).values.cache()
    
    // 分测试集
    val test = ratings.filter(x=>x._1>=6).values.cache()
    
    training.count
    test.count

    5. 设置参数,进行建模,其代码如下所示:

    val rank = 10
     val lambda = 10.0
     val iter = 20
    val model = ALS.train(training,rank,iter,lambda)

    6. 编写均方根误差函数,并对测试数据集求其均方误差根,其代码如下所示:

    // 建立均方根误差函数
    import org.apache.spark.rdd.RDD
    
    def computeRMSE(model:MatrixFactorizationModel, data:RDD[Rating]): Double = {
    val usersProducts = data.map(x=>(x.user,x.product))
    val ratingsAndPredictions = data.map{case Rating(user,product,rating)=>((user,product),rating)}.join(model.predict(usersProducts).map{case Rating(user,product,rating)=>((user,product),rating)}).values ; math.sqrt(ratingsAndPredictions.map(x=>(x._1-x._2)*(x._1-x._2)).mean())}
    
    // 使用测试集,计算均方根误差
     val testRMSE = computeRMSE(model,test)
    得到的均方根误差为:


    7. 预测某用户的推荐电影,并格式化输出,其代码如下所示:

    val userid = 1
    // 求得userid对应用户评分过的所有电影
     val user1RatedMovieIds = ratings.filter(_._2.user==userid).map(_._2.product).collect.toSeq
    // 过滤得到用户userid对应的潜在推荐电影 
    val cands = sc.parallelize(movies.keys.filter(!user1RatedMovieIds.contains(_)).toSeq)
    // 使用模型来对潜在推荐电影进行评分,并按照预测评分进行排序,取前10
    val recommendations = model.predict(cands.map((userid,_))).collect.sortBy(- _.rating).take(10)
     var i =1
    println(“用户”+userid+”的推荐结果为:”)
     recommendations.foreach{rec => println("%2d".format(i)+": "+movies(rec.product)+", predictRating: "+rec.rating);i+=1}

    8.经过如上的代码调用,其推荐结果为:


    9. 修改第7步的userid为2,3,看到其推荐结果是一样的,只是预测评分是不一样的,这也就是如题所述的问题了。


    最后,Spark als 中的两处引用错误:

    1. 参数设置为如网址中的值(iter=20,lambda=10.0,rank=10)其测试数据集得到的均方根误差不可能是0.8左右,所以其参数值肯定不是这样设置的;
    2. 在进行推荐的时候,设置的用户ID为0。其一,原始数据中并没有这个id的数据;其二,原文中说可以设置该用户(即用户id为0 的用户)看过的电影评分,然后再来推荐,这个等于是新用户推荐了,但是如果是Spark als算法那么需要计算其用户特征向量才能对其进行推荐,这个在原文中并没有体现。所以,针对用户0来进行推荐还是有问题的。


    另外,Spark ALS对新用户推荐思路如下:

    1. 得到新用户0评分向量;
    2. 设置均方根误差函数;
    3. 随机新用户0特征向量(rank *1的向量);
    4. 使用梯度下降法来更新特征向量,得到最优特征向量;
    5. 使用最优特征向量以及所有电影的特征向量,得到各个电影的预测评分,并根据评分最高进行top推荐;

    ALS算法原理

    在分析上面的问题前,先了解下ALS算法的原理,这里只是大概分析下,如果想深入了解这个算法,请自行查找相关资料,进行了解。


    注意:电影评分最高5分,最低1分。

    如上表所示,是一个数据集,数据集中一共有5个电影、4个用户,并且这4个用户对5个电影中的某些电影看过,并给过评分,比如用户Tom对《釜山行》评分为5分,对《潜伏3》评分为1分,但是没有看过《招魂2》,所以没有显示评分,为“?”。

    现在,假如说我们给电影定义两个标签,比如“动作”、“恐怖”,并且已经有人(比如电影影评人等)帮我们给每个电影根据这两个标签打过分,那么现在就会有这样的一个数据,如下表所示。


    根据上面的评分,如果我们可以构造这样的一个列向量Θ,满足下面的公式:


    那么,我们就可以预测上面的空格部分的数值了(注意4,5,4和1中间有个空格)。当然,上面是针对Tom用户来说的,如果我们针对Kate、John、Fansy都可以得到这样的一个列向量,那么针对所有用户就可以得到下面的一个二维矩阵了。


    其中,、代表用户Tom的特征值,其他用户以此类推。所以,其实这里我们也可以理解为使用了、就完全可以代表用户Tom了,、就可以完全代表用户Fansy了。

    注意:这里需要说明一点是,如果根据不完整的用户电影评分列表以及电影的标签评分,我们是可以在一定的误差范围内推算出Θ矩阵的。

    好了,那么其实我们就已经完成了这个算法了。但是,好像有什么地方不对?影评人对这些电影的评价是否完全一致?是否影评人可以看完所有电影,并给这些电影画上标签,并给出评分?可以想象,这是一个很难完成的任务(当然,如果有时间也是可以完成的)。那怎么办呢?

    一种很自然的想法是可以随机电影标签评分(可以理解为电影特征矩阵),然后求得Θ矩阵(可以理解为用户特征矩阵)。当然,这时就会有很大的误差,所以就需要根据我们推导得到的Θ矩阵,再反推回电影特征矩阵。以此反复循环,就可以保证我们预测的电影评分和实际电影评分(用户已经评价过的电影评分)的全局均方根误差在一定阈值内。这个时候我们就可以说算法建模完成,并且得到了算法模型的参数:电影特征矩阵和用户特征矩阵(本例中是2维的,这个也是这个算法的参数之一,rank)。接着,就可以根据这两个矩阵针对用户还没有评分过的电影进行评分预测,进而得到可以推荐给用户的电影(根据预测评分大小取出评分top10即可)。

    问题分析及解决

    问题分析:

    这个问题可以从下面两个方面来看:

    1. 针对测试集求得均方根误差,得到的值是3.7左右,这个值其实是比较大的(当然,这个“大”也是需要有一定经验看出来的),这就可以从一方面说明咱们建立的模型是有问题的(如果你看不出,还可以从下面的角度考虑)。
    2. 最直观的结果,使用模型对所有用户进行推荐,发现基本所有用户其推荐结果(即电影)都是一样的,不过只是电影的预测评分不一样。而且,所有预测的评分是比较小的,仔细看的话,会发现居然基本都是接近0的;

    当然,这个问题也可以从另外的角度来分析。如果预测评分基本都是接近0的,那么用户特征向量,项目特征向量是否也是接近0?那怎么看呢?

    在得到模型后,可以通过下面的方法查看某个用户或某个电影的特征向量,代码如下:


    看到的结果也可以说,确实,不管是用户特征向量或者项目特征向量,其值都是非常接近0的,这肯定是有问题的,一般是介于-1~1之间的某个小数,但是肯定不是接近0的一个数。所以,如果你使用Spark ALS算法,怎么去评价这个算法的优劣就可以从这个几个方面来分析(当然,也可以使用准确率以及召回率来评判)。

    问题解决:

    问题的解决很简单,一行代码即可搞定:

    val model = ALS.train(trainging,10,20,0.01);

    即修改lambda的值为 0.01即可。修改完成后,再来看看上面分析的两个问题。

    1. 测试数据集均方根误差:


    2. 模型的用户特征向量以及项目特征向量及最后的推荐结果


    针对用户1,2,3,其推荐结果为:


    从上面的结果可以看出,不仅各个用户推荐的电影不一样 了,而且其预测评分也不是接近0;另外,其测试数据集的误差仅为0.9,和之前的3.8相差好几倍。

    那是怎么想到要这样设置的呢?可以那就要在了解算法的基础上来设置此参数;

    1. 循环次数iter,这个设置的越大肯定是越精确,但是设置的越大也就意味着越耗时;
    2. rank ,特征向量纬度,这个设置就要看了,如果太小拟合的就会不够,误差就很大;如果设置很大,就会导致模型大泛化能力较差;所以就需要自己把握一个度了,一般情况下10~100都是可以的;
    3. lambda也是和rank一样的,如果设置很大就可以防止过拟合问题,如果设置很小,其实可以理解为直接设置为0,那么就不会有防止过拟合的功能了;怎么设置呢?可以从0.0001 ,0.0003,0.001,0.003,0.01,0.03,0.1,0.3,1,3,10这样每次大概3倍的设置,先大概看下哪个值效果比较好,然后在那个比较好的值(比如说0.01)前后再设置一个范围,比如(0.003,0.3)之间,间隔设置小点,即0.003,0.005,0.007,0.009,0.011,,,,。当然,如果机器性能够好,而且你够时间,可以直接设置从0到100,间隔很小,然后一组参数一组的试试也是可以的。

    总结

    1. Spark MLlib里面的算法学习可以先使用一个实例来看看其运行结果是否和自己的预期一样;
    2. 大概了解该算法的算法原理,知道怎么调节参数,求得一个参数的大概范围,然后编程实现求得最优的一组参数值;
    3. 动手实践

    分享,成长,快乐

    脚踏实地,专注

    转载请注明blog地址:http://blog.csdn.net/fansy1990





沪ICP备19023445号-2号
友情链接