使用Spark运行协同过滤推荐算法

接近一个月没写博客,6月的课程很繁忙,各种Presentation,各种考试,各种课程报告,期末考完了,写篇博客记录下之前的一个作业。

研究生期间有门课,大数据分析与决策。这门课有个作业是做个Presentation,实现基于矩阵分解的推荐算法,要求在包含超过1000万条数据的数据集上运行。

数据集

数据不难搞到,老师直接指定MovieLens数据集。MovieLens是一个含有1700万条电影评分数据的数据集,评分数据文件rating.csv里面的数据格式很简单:一个二维表,表头 userId,movieId,rating。

然后,1700多万行评分记录,整个csv文件都有几百兆,我用 vim 打开都卡了一下,用 VS Code打开更是卡的不行。

算法

算法,矩阵分解。就是把评分矩阵R(n个用户m个电影)分解成两个矩阵乘积。U矩阵是用户的特征矩阵,V矩阵是物品的特征矩阵。



如果R矩阵里的每个评分都是已知的,那么这个矩阵分解起来就比较容易,但是现在问题是R矩阵里面大多数评分是空缺的,UV就不好分解了。这个时候可以定义一个误差函数,通过一个最优化过程来实现分解。至于预测结果,就通过UV乘积的某行某列去查就可以了。

因为之前完全没有接触过推荐算法,也没有接触过Spark,这个题目乍一看比较吓人,其实也没有多吓人。重大研究生的水平老师们都清楚,一门课而已,并非真的叫你把算法重新实现一遍,直接调库就好了。其实实现一个单机版本也没有多难,把误差函数定义好,梯度下降方法也不难写,但是考虑到:

  • 评分矩阵很大很稀疏,我粗略的算了一下,直接把评分矩阵(13Wx3W)在内存里展开,需要 30GB,算上分解出来的矩阵,可能需要64G内存的电脑,远远超出32bit程序的内存限制
  • 最优化的参数太多,想要收敛到最小不知道会耗费多少精力

所以直接调Spark的mllib的ALS算法,因为Spark的API不是很熟,代码也是抄的网上的:

# coding: utf-8
import sys
from os.path import join

from pyspark.sql import SparkSession
from pyspark.sql import Row
from pyspark.mllib.recommendation import ALS

reload(sys)
sys.setdefaultencoding("utf-8")

# 训练模型的时候只使用了 ratings.csv 文件
def train_model(training, num_iterations=10, rank=1, lambda_=0.01):
    return ALS.train(training.rdd, iterations=num_iterations, rank=rank, lambda_=lambda_, seed=0)

              

def recommend_for_all(model, movies, result_path, file_out_flag=False):
    def parse_recommendations(line):
        res = []
        for item in line[1]:
            movie_name = b_movies.value[item[1]]
            res.append((movie_name, float(item[2])))
        return Row(user=line[0], recommendations=res)

    b_movies = spark.sparkContext.broadcast(dict((int(l[0]), l[1]) for l in movies.collect()))
    products_for_all_users = model.recommendProductsForUsers(10).map(parse_recommendations).toDF()
    recommendation_result = products_for_all_users.repartition(1).orderBy(products_for_all_users.user).rdd \
        .map(lambda l: Row(str(l.user) + "," + str(list((r[0], r[1]) for r in l.recommendations))))\
        .toDF().repartition(1)
    if file_out_flag:  # 输出到文件
        recommendation_result.write.text(result_path)


def recommend_for_users(model, movies, user_ids):
    def recommend_for_one_user(model, user_id):
        b_movies = spark.sparkContext.broadcast(dict((int(l[0]), l[1]) for l in movies.collect()))
        recommendations = model.recommendProducts(user_id, 10)
        result = []
        for item in recommendations:
            result.append((b_movies.value[item.product], item.rating))
        print "Recommendations for user ", user_id, ": ", result

    if isinstance(user_ids, list):
        for user in user_ids:
            recommend_for_one_user(model, user)
    elif isinstance(user_ids, int):
        recommend_for_one_user(model, user_ids)


def main(spark, data_path, result_path):
    movies = spark.read.csv(join(data_path, "movies.csv")).rdd.map(
        lambda l: Row(int(l[0]), l[1], l[2])).toDF(["movieId", "title", "genres"])
    ratings = spark.read.csv(join(data_path, "ratings.csv")).rdd.map(
        lambda l: Row(int(l[0]), int(l[1]), float(l[2]))).toDF(["userId", "movieId", "rating"])

    all_users = ratings.select("userId").distinct()
    all_movies = ratings.select("movieId").distinct()
    print "Got %d ratings from %d users on %d movies." \
          % (ratings.count(), all_users.count(), all_movies.count())

    # 划分训练集和测试集
    training, test = ratings.randomSplit([0.8, 0.2], 1)

    model = train_model(training)

    # 为以下所有用户作出推荐并输出到终端
    #user_ids = list(u.userId for u in all_users.take(3))
    #print "userIds: ", user_ids
    #recommend_for_users(model, movies, user_ids)

    # 为所有的用户作出推荐并输出到文件
    file_out_flag = True
    recommend_for_all(model, movies, result_path, file_out_flag)

import os

if __name__ == "__main__":
    if len(sys.argv) == 3:
        data_path = sys.argv[1]  # 数据目录
        result_path = sys.argv[2]  # 保存结果目录
    else:
        print "Usage: $SPARK_HOME/bin/spark-submit --master spark://master:7077 " \
              "movieLens.py /data_path/ /result/path"
        sys.exit()

    spark = SparkSession \
        .builder \
        .appName("MovieLens Recommendation") \
        .getOrCreate()
    real_data_path = os.environ.get('SPARK_HOME') + '/' + data_path 
    main(spark, real_data_path, result_path)

    spark.stop()

以上程序要求机器有Python2.7,装了numpy:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple numpy

部署

部署稍微讲究一下,因为没跑过千万级的数据,我一开始就搭建了一个Spark集群。这个Spark集群没有运行在 hdfs 上,所以我需要把数据集文件拷到每台机器上,而且文件的位置必须一模一样,不然Python脚本读文件会出错,这里我把数据集放到了Spark目录下面(通过环境变量拿路径)。

关于Spark环境的搭建,因为我没有上Hadoop,使用的也是默认的集群管理器,所以环境搭建很简单。先确保Java环境搭好,然后把Spark下载下来解压到/usr/share/spark,之后设置环境变量SPARK_HOME,把$SPARK_HOME$/bin,$SPARK_HOME$/sbin添加到PATH里就好了。

然后启动Master结点:

start-master.sh -h 0.0.0.0 -p 7077

再启动Slave结点:

start-slave.sh spark://masterIP:7077 

这个时候访问 http://masterIP:8080 可以查看集群状态:

使用Spark运行协同过滤推荐算法_第1张图片
集群截图

我在这里用了4台Ubuntu系统的电脑,搭了一个有23GB内存,13个核的集群。然后我在任何一台有Python脚本的电脑上用以下命令提交任务到 Master 上:

spark-submit --master spark://masterIP:7077 movieLens.py ml-20m/ result/  
# movieLens.py 是代码,后面两个参数是Python的__main__()的参数。

这个配置的集群基本7分钟左右就可以在1700万条记录上跑完协同过滤算法,这个过程只能说又锻炼了一遍我配置环境的能力,看着这么大的数据在集群里这么短时间跑完还是很开心的。

使用Spark运行协同过滤推荐算法_第2张图片
结果

你可能感兴趣的:(使用Spark运行协同过滤推荐算法)