使用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 可以查看集群状态:

集群截图

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

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

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

结果

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容