赛题介绍
竞赛题目
在真实的业务场景下,我们往往需要对所有商品的一个子集构建个性化推荐模型。在完成这件任务的过程中,我们不仅需要利用用户在这个商品子集上的行为数据,往往还需要利用更丰富的用户行为数据。定义如下符号:
U -- 用户集合
I -- 商品全集
P -- 商品子集,
D -- 用户对商品全集的行为数据集合
那么我们的目标是利用D来构造U中用户对P中商品的推荐模型。
数据说明
本场比赛提供20000用户的完整行为数据以及百万级的商品信息。竞赛数据包含两个部分。
第一部分是用户在商品全集上的移动端行为数据(D),表名为tianchi_fresh_comp_train_user_2w,包含如下字段:
字段 | 字段说明 | 提取说明 |
---|---|---|
user_id | 用户标识 | 抽样 & 字段脱敏 |
item_id | 商品标识 | 字段脱敏 |
behavior_type | 用户对商品的行为类型 | 包括浏览、收藏、加购物车、购买,对应取值分别是1、2、3、4. |
user_geohash | 用户位置的空间标识,可以为空 | 由经纬度通过保密算法生成 |
item_category | 商品分类标识 | 字段脱敏 |
time | 行为时间 | 精确到小时级别 |
第二部分是商品子集(P),表名是tianchi_fresh_comp_train_item_2w,包含如下字段:
字段 | 字段说明 | 提取说明 |
---|---|---|
item_id | 商品标识 | 抽样 & 字段脱敏 |
item_geohash | 商品位置的空间标识,可以为空 | 由经纬度通过保密的算法生成 |
item_category | 商品分类标识 | 字段脱敏 |
训练数据包含了抽样出来的一定量用户在一个月时间(11.18~12.18)之内的移动端行为数据(D),评分数据是这些用户在这个一个月之后的一天(12.19)对商品子集(P)的购买数据。参赛者要使用训练数据建立推荐模型,并输出用户在接下来一天对商品子集购买行为的预测结果。
评分数据格式
具体计算公式如下:参赛者完成用户对商品子集的购买预测之后,需要将结果放入指定格式的数据表(非分区表)中,要求结果表名为:tianchi_mobile_recommendation_predict.csv,且以utf-8格式编码;包含user_id和item_id两列(均为string类型),要求去除重复。
评估指标
比赛采用经典的精确度(precision)、召回率(recall)和F1值作为评估指标。具体计算公式如下:
其中PredictionSet为算法预测的购买数据集合,ReferenceSet为真实的答案购买数据集合。我们以F1值作为最终的唯一评测标准。
解决方案
als
这里采用交替最小二乘法,通过用户对商品的打分矩阵,提取出用户和商品在隐式因子(Latent Factor,可以这样理解,一系列兴趣因子,用户、商品在这些兴趣因子上分别有一系列的打分取值)上的取值向量,通过用户与商品特征向量的内积得出用户对该商品的兴趣度,从大到小排序筛选出用户可能购买的商品。
pyspark.ml.recommendation下集成了该预测器,通过ParamGrid来设置超参数组合,再通过交叉验证来得到各个超参数组合下的模型,最后通过训练模型并评估指标得出最优的模型,具体的代码如下:
def getModel(scores_rdd1_ds):
n = 0.001
l = []
while n < 50:
l.append(n)
n = n * 8
from pyspark.ml.tuning import CrossValidator, ParamGridBuilder
from pyspark.ml.evaluation import RegressionEvaluator
from pyspark.ml.recommendation import ALS
als = ALS(userCol="user", itemCol="product", ratingCol="rating")
paramGrid = ParamGridBuilder().addGrid(als.rank, range(10, 101, 10)) \
.addGrid(als.maxIter, range(10, 71, 10)) \
.addGrid(als.regParam, l) \
.addGrid(als.implicitPrefs, [True, False]).build()
evaluator = RegressionEvaluator(predictionCol="prediction", labelCol="rating")
cross_val = CrossValidator(estimator=als, estimatorParamMaps=paramGrid, evaluator=evaluator, parallelism=4,
numFolds=2, collectSubModels=True)
cvModel = cross_val.fit(scores_rdd1_ds)
cross_val.getCollectSubModels()
return cvModel.bestModel
这里通过对用户对指定商品的动作类型之和来计算得分:
users=spark.read.parquet('hdfs://master:8020/tmp.db/tianchi_fresh_comp_train_user-parquet')
users.createOrReplaceTempView('users')
scores_rdd=spark.sql('select user_id,item_id,sum(behavior_type) as score from users group by user_id,item_id').rdd
import pyspark.mllib.recommendation as rd
scores_rdd1=scores_rdd.map(lambda r:rd.Rating(r.user_id,r.item_id,r.score))
sc.setCheckpointDir("hdfs://master:8020/tmp.db/als")
scores_rdd1.checkpoint()
scores_rdd1_ds = sqlContext.createDataFrame(scores_rdd1)
bestModel = getModel(scores_rdd1_ds)
bestModel.save(hadoop_preffix+'/models/bestModel.model')
预测用户对商品子集的推荐结果的代码如下:
def predict():
items = spark.read.parquet('hdfs://master:8020/tmp.db/tianchi_fresh_comp_train_item-parquet')
item_item_ids = items.select('item_id').withColumnRenamed('item_id', 'product')
item_item_ids.checkpoint() # 拿到item_ids列表
result = bestModel.recommendForItemSubset(item_item_ids, 20) # 对每个商品分别推荐20个用户
from pyspark.sql.functions import explode_outer
rr = result.select("product", explode_outer('recommendations')).select('product', 'col.user', 'col.rating')
rr = rr.orderBy(rr.rating.desc()).distinct() # 将商品-推荐用户的数据展开
rr.checkpoint()
cc = rr.count()
rr = rr.limit(int(cc * 0.6))
rr.select('user', 'product').withColumnRenamed('user','user_id').withColumnRenamed('product','item_id').write.csv('hdfs://master:8020/tmp.db/recommend0108_2.csv') # 推荐数据的前60%
return rr
通过采样评估预测的误差度:
# 计算实际得分与预测得分之间误差的 均方根
def calc_err(test,model):
pre_data = test.map(lambda r: ((r.user, r.product), r.rating))
predicted_data = model.predictAll(pre_data.keys())
predicted_data = predicted_data.map(lambda r: ((r.user, r.product), r.rating))
err = predicted_data.join(pre_data).values()\
.map(lambda x: (x[0] - x[1]) ** 2).reduce(lambda a,b:a+b)
import math
err = math.sqrt(err / test.count())
return err
test=scores_rdd1.sample(False,0.2)
calc_err(test, bestModel)
这些方法运行的时间比较长,为了让程序在出现意外后可以重启在原来的基础上继续运行,使用了StreamingContext,完整代码如下:
from pyspark.streaming import StreamingContext
from pyspark.sql.context import SQLContext
from pyspark.sql.session import SparkSession
def setupFunc():
streamingContext = StreamingContext(sc, batchDuration=20)
sc = streamingContext.sparkContext
spark = SparkSession(sc)
sqlContext = SQLContext(sc, spark)
users = spark.read.parquet('hdfs://master:8020/tmp.db/tianchi_fresh_comp_train_user-parquet')
users.createOrReplaceTempView('users')
scores_rdd = spark.sql('select user_id,item_id,sum(behavior_type) as score from users group by user_id,item_id').rdd
import pyspark.mllib.recommendation as rd
scores_rdd1 = scores_rdd.map(lambda r: rd.Rating(r.user_id, r.item_id, r.score))
sc.setCheckpointDir("hdfs://master:8020/tmp.db/als")
scores_rdd1.checkpoint()
# model=rd.ALS.train(scores_rdd1,50,50,0.01)
# # model.predict(10001082,214252945)
# model.save(sc,"hdfs://master:8020/tmp.db/als.model")
# model.userFeatures()
def getModel(scores_rdd1_ds):
n = 0.001
l = []
while n < 50:
l.append(n)
n = n * 8
from pyspark.ml.tuning import CrossValidator, ParamGridBuilder
from pyspark.ml.evaluation import RegressionEvaluator
from pyspark.ml.recommendation import ALS
als = ALS(userCol="user", itemCol="product", ratingCol="rating")
paramGrid = ParamGridBuilder().addGrid(als.rank, range(10, 101, 10)) \
.addGrid(als.maxIter, range(10, 71, 10)) \
.addGrid(als.regParam, l) \
.addGrid(als.implicitPrefs, [True, False]).build()
evaluator = RegressionEvaluator(predictionCol="prediction", labelCol="rating")
cross_val = CrossValidator(estimator=als, estimatorParamMaps=paramGrid, evaluator=evaluator, parallelism=4,
numFolds=2, collectSubModels=True)
cvModel = cross_val.fit(scores_rdd1_ds)
cross_val.getCollectSubModels()
return cvModel.bestModel
scores_rdd1_ds = sqlContext.createDataFrame(scores_rdd1)
bestModel = getModel(scores_rdd1_ds)
def predict():
items = spark.read.parquet('hdfs://master:8020/tmp.db/tianchi_fresh_comp_train_item-parquet')
item_item_ids = items.select('item_id').withColumnRenamed('item_id', 'product')
item_item_ids.checkpoint()
result = bestModel.recommendForItemSubset(item_item_ids, 20)
from pyspark.sql.functions import explode_outer
rr = result.select("product", explode_outer('recommendations')).select('product', 'col.user', 'col.rating')
rr = rr.orderBy(rr.rating.desc()).distinct()
rr.checkpoint()
cc = rr.count()
rr = rr.limit(int(cc * 0.6))
rr.select('user', 'product').withColumnRenamed('user', 'user_id').withColumnRenamed('product',
'item_id').write.csv(
'hdfs://master:8020/tmp.db/recommend0108_2.csv')
return rr
rr = predict()
# 计算实际得分与预测得分之间误差的 均方根
def calc_err(test, model):
pre_data = test.map(lambda r: ((r.user, r.product), r.rating))
predicted_data = model.predictAll(pre_data.keys())
predicted_data = predicted_data.map(lambda r: ((r.user, r.product), r.rating))
err = predicted_data.join(pre_data).values() \
.map(lambda x: (x[0] - x[1]) ** 2).reduce(lambda a, b: a + b)
import math
err = math.sqrt(err / test.count())
return err
test = scores_rdd1.sample(False, 0.2)
calc_err(test, bestModel)
return streamingContext
hadoop_preffix="hdfs://master:8020/tmp.db"
streamingContext=StreamingContext.getOrCreate(hadoop_preffix+'/als',setupFunc)
streamingContext.start()
streamingContext.awaitTermination()