Spark ALS 协同过滤算法实践

3 篇文章 0 订阅
2 篇文章 0 订阅

个人总结,有误请指出
ALS算法
bibili上硅谷课程

理论

协同过滤算法

在这里插入图片描述
上图中横坐标代表用户,纵坐标代表商品,每一个格子,代表第I个用户的对第I个商品的评分。这个矩阵是一个稀疏矩阵,而这些没有值得位置正是我们要推测的值。每个格子的的评分大体上可以看做一个独立事件,是很难准确的通过其他独立事件去推测的。因为它的可能实在太多了。

所以综上我们提出两个问题:
由于它每一个格子的可能性太多,那我们可以简单的理解为求解一个推测值的复杂度是很高的,为m*n,m,n分别为矩阵的横纵长度。
由于每个用户对每个商品的评分行为是独立的事件很难准确的推测其他的值。当然我们是可以通过一些规则去人为的确定的,比如:用户A, B喜欢商品1,用户A喜欢商品2,那么推测用户B也喜欢,这样去计算一个评分。但是怎么转化为计算机能够理解的方式呢?

问题

  • 怎么降低计算的复杂度
  • 用什么样的规则去推测未知值,计算机更方便的计算
  • 计算方法的误差怎么收敛以得到更准确的推测
ALS 怎么求解以上问题

问题一
可以将MN的大矩阵,拆解为Mk 的矩阵和k*N的矩阵的相乘从而降低计算的复杂度,变为(M+N)*K的复杂度。

问题二
问题一的方案把怎么填充推测值这个问题转化为了矩阵的相乘问题。矩阵与向量是有相似性的,所以我们如果能把用户映射到一个向量中组成一个矩阵(MK),商品映射到一个矩阵(KM),两个矩阵相乘,求解用户向量与商品向量之间的相识度,那么我们就把一个评分问题求解转换为了一个相似度求解问题。
(此处为个人理解)用户向量使用用户喜欢那些商品的来表达为一个向量,商品反之。

问题三
比如用户喜欢SKII化妆品,相似的商品向量为欧莱雅化妆品,看似我们推测结果是对的,但是算法其实是不能真正的意识到这两项商品的相识度的,只是通过向量求解发现的。那么就说明,你向计算机说了一个识别相似度的方法,但是实际上这个方法为了让计算机理解而去弱化了一些人思维上的东西,比如我们知道这个品牌一般都是买女性化妆品的,所以这个化妆品推荐给这为女士一定是8,9不离十的,但是计算机它理解不了。综上,那么这个算法是一定会有误差的,所以我们要减少误差
减少误差的方法
使用ALS,我们最熟悉的名字,最小二乘法。其实就随机的确认矩阵相乘的一方,然后去求解另一方的矩阵,然后再反过来。不断的尝试使用我们已知的评分去验证,直到收敛到一个相对稳定的矩阵A和矩阵B

基于物品相识度实践

实时计算评分规则

在实践中,我们需要一个评分的规则

  • 显示规则
    比如,给用户一个问卷,有一个打分系统
  • 隐式规则
    根据用户的行为
    比如:
    • 用户浏览商品 1分
    • 用户加购商品 2分
    • 用户立即购买商品 3分
模型训练

模型训练还是使用测试训练集与预测集交叉校验的方法测出一个相对合理的参数。

  • 注意事项:
    • 注意冷启动的问题,首先要给新来的用户按照热榜推荐
    • 商品相识度的计算使用余弦相识度
      参考代码:
# -*- coding: utf-8 -*-

from __future__ import print_function

from pyspark.ml.recommendation import ALS
from pyspark.sql import SparkSession
from pyspark.ml.linalg import Vectors
from pyspark.sql.functions import udf, col, row_number, concat_ws, explode
from pyspark.sql.types import FloatType
from pyspark.sql import Window

# GP推荐数据训练生成相关推荐列表,用ALS算法实现,将结果落地到ES中
if __name__ == "__main__":
    spark = SparkSession.builder.appName("GPBPRecommendALS") \
        .config("spark.some.config.option", "some-value") \
        .getOrCreate()

    # 从数据库中取得用户评分权重信息
    formsql = '''
        (select t.key,t2.cid_int,t.spu_id,t.rating
          from dwr.dwr_user_rating t
              inner join dwr.dwr_user_transcoding t2 on t2.cid = t.cid
        ) tmp
    '''
    jdbcDF = spark.read \
        .format("jdbc") \
        .option("url", "jdbc:postgresql://10.8.5.51:6432/insight") \
        .option("dbtable", formsql) \
        .option("user", "bi") \
        .option("password", "izene123") \
        .load()

    jdbcDF = jdbcDF.repartition(100)
    
    # 读取cid映射关系编码表
    cidCode = spark.read \
        .format("jdbc") \
        .option("url", "jdbc:postgresql://10.8.5.51:6432/insight") \
        .option("dbtable", "dwr.dwr_user_transcoding") \
        .option("user", "bi") \
        .option("password", "izene123") \
        .load()

    # 在训练数据上使用ALS建立推荐模型
    # 注意,我们将冷启动策略设置为“下降”,以确保我们不会得到NaN评估指标
    # maxIter:最大迭代次数
    # regParam:正则化参数
    # coldStartStrategy:预测时处理未知或新用户/item的策略。这在交叉验证或生产场景中可能很有用,对于处理模型在训练数据中没有看到的用户/项目id。支持的值:“nan”、“drop”。
    als = ALS(maxIter=20, regParam=0.02, userCol="cid_int", itemCol="spu_id", ratingCol="rating",
              coldStartStrategy="drop")
    model = als.fit(jdbcDF)

    # 为每个用户生成前10部电影推荐
    userRecs = model.recommendForAllUsers(20)

    # 将数组形式的信息转换行的形式
    userRecs = userRecs.select("*", explode("recommendations").alias("recomm_explode"))
    userRecs = userRecs.withColumn("spu_id", col("recomm_explode").getItem("spu_id"))
    userRecs = userRecs.withColumn("rating", col("recomm_explode").getItem("rating"))
    userRecs = userRecs.drop("recommendations", "recomm_explode", "col")

    userRecs = userRecs.withColumn("key", concat_ws('-', "cid_int", "spu_id"))

    userRecs.write.format('org.elasticsearch.spark.sql') \
        .option('es.nodes', '10.8.5.182') \
        .option('es.port', '9200') \
        .option('es.resource', 'gp_recommend/als') \
        .option('es.mapping.id', 'key') \
        .save(mode='overwrite')
    print("gp_recommend save successful!")

    # cid转码映射关系表,保存到ES中
    cidCode.write.format('org.elasticsearch.spark.sql') \
        .option('es.nodes', '10.8.5.182') \
        .option('es.port', '9200') \
        .option('es.resource', 'gp_user_transcoding/data') \
        .option('es.mapping.id', 'cid') \
        .save(mode='ignore')
    print("gp_user_transcoding save successful!")

    # 商品的特征矩阵
    itemDF = model.itemFactors
    
    # 从数据库中取得商品销售站点信息
    spuSiteSql = '''
        (select t4.code as site_code,t.spu_id::int4 as site_spu_id from dwi.dwi_product_price t
            join dwi.dwi_product t2 on t.spu_id = t2.spu_id
            join  dwi.dwi_tb_ms_user_area t3 on t.country_id = t3.id
            join dim.dim_site t4 on substring(t4.name from '-([A-Z]{2})') = t3.two_char
            join dwi.dwi_product_sku_stock t5 on t.sku_id = t5.sku_id and t.warehouse = t5.warehouse
    where t2.sale_states = 'N000100100' -- 在售状态
      and t4.platform_code = 'N002620800'
    group by t4.code,t.spu_id::int4
       having sum(t5.sku_stock) > 0 --过滤掉没有库存的商品
        ) tmp
    '''
    spuSiteDF = spark.read \
        .format("jdbc") \
        .option("url", "jdbc:postgresql://10.8.5.51:6432/insight") \
        .option("dbtable", spuSiteSql) \
        .option("user", "bi") \
        .option("password", "izene123") \
        .load()
    
    itemDF = itemDF.withColumnRenamed("id", "spu_id")
    # 所有商品关联到可销售站点
    itemDF = itemDF.join(spuSiteDF).where(itemDF.spu_id == spuSiteDF.site_spu_id).drop("site_spu_id")
    
    # 重命名字段,为自关联做区分
    itemDF2 = itemDF.withColumnRenamed("spu_id", "spu_id2").withColumnRenamed("features", "features2").withColumnRenamed("site_code", "site_code2")

    # 笛卡尔积,两两比对商品,排除自己跟自己的数据
    itemDF = itemDF.crossJoin(itemDF2).where(itemDF.spu_id != itemDF2.spu_id2).where(itemDF.site_code == itemDF2.site_code2)

    # 计算余弦相似度udf函数
    def fun_cim(features1, features2):
        try:
            x = Vectors.dense(features1)
            y = Vectors.dense(features2)
            return float(1 - x.dot(y) / (x.norm(2) * y.norm(2)))
        except Exception:
            return 0

    udf_cim = udf(fun_cim, FloatType())

    # 添加余弦相似度计算列cosine_similarity
    itemDF = itemDF.withColumn("cosine_similarity", udf_cim(itemDF.features, itemDF.features2)).drop("features", "features2" ,"site_code2")

    # 每个物品只保留top20
    w = Window.partitionBy(col("spu_id"), col("site_code")).orderBy(col("cosine_similarity").desc())
    itemDF = itemDF.withColumn("rn", row_number().over(w)).where(col("rn") < 21).drop("rn")
    itemDF = itemDF.withColumn("key", concat_ws("-", "spu_id", "site_code", "spu_id2"))

    itemDF.write.format('org.elasticsearch.spark.sql') \
        .option('es.nodes', '10.8.5.182') \
        .option('es.port', '9200') \
        .option('es.resource', 'spu_similarity/cosine') \
        .option('es.mapping.id', 'key') \
        .save(mode='overwrite')
    print("spu_similarity save successful!")

    spark.stop()

怎么处理用户实时喜爱变化

以上方法,只能实现离线计算,怎么实现实时计算呢?
在基于商品相识度模型直接为每个用户推荐出10个商品的时候,如果我们直接给用户。那么会衍生出以下两个疑问

  • 这个十个商品对于用户来说有优先级吗
  • 实时的过程中,怎么给用户反馈

如果有个相似的商品,用户在刚才1分钟前对它的评分极差,而我们直接给它放在了推荐列表的第一个,那不就尴尬了?
所以我们需求一个规则来确定这些备选商品的优先级及实时根据用户的评分给与反馈。

规则:
通过近k次对商品的评分然后与备选商品的相识度*实时商品评分的和的平均值再加上一个惩罚项和奖励项来确定推荐商品的优先级展示。
在这里插入图片描述

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值