精华内容
参与话题
问答
  • 协同过滤算法是推荐系统中最重要也是最常用的算法之一,本课程以项目实现为主,讲解基于商品的协同过滤算法应用,通过不断对算法进行优化,提升推荐结果的准确率与召回率。
  • 推荐系统实战系列课程旨在帮助同学们快速掌握推荐系统领域常用算法及其建模应用实例。全程基于Python及其开源工具实战演示各大推荐引擎构建方法,基于真实数据集进行建模与应用实战。整体风格通俗易懂,适合入门与...
  • 推荐系统实战系列视频教程(Python版,2020年9月新课),推荐系统实战系列课程旨在帮助同学们快速掌握推荐系统领域常用算法及其建模应用实例。全程基于Python及其开源工具实战演示各大推荐引擎构建方法,基于真实数据...
  • 推荐系统实战系列(Python),2020年新课,推荐系统实战系列课程旨在帮助同学们快速掌握推荐系统领域常用算法及其建模应用实例。 全程基于Python及其开源工具实战演示各大推荐引擎构建方法,基于真实数据集进行建模与...
  • 推荐系统实战第01课 推荐系统简介

    千次阅读 2019-07-02 20:18:28
    推荐系统实战_第01课 推荐系统简介推荐系统介绍(what why where)1、推荐系统是什么(**what**)2、为什么推荐系统越来越火?(why)3、**推荐系统需要做怎么做?** (How)4、**寻求解决信息过载的思路 **5、推荐...

    推荐系统介绍(what why where)

    Industrial Recommendation System

    1、推荐系统是什么(what)

    •分类⽬录(1990s):覆盖少量热门⽹站。Hao123 Yahoo
    •搜索引擎(2000s):通过搜索词明确需求。Google Baidu
    •推荐系统(2010s):不需要⽤户提供明确的需求,通过分析⽤ 户的历史⾏为给⽤户的兴趣进⾏建模,从⽽主动给⽤户推荐能 够满⾜他们兴趣和需求的信息。
    •2020s?

    **一种数学定义: **

    • 设C为全体用户集合 n

    • 设S为全部商品/推荐内容集合

    • 设u是评判把sis_i推荐cic_i的好坏评判函数

    • 推荐是对于c∈C,找到s∈S,使得u最大,即

    cC,sc=argmax(u(c,s))sS\forall c \in C, s^{\prime}_{c}=\operatorname{argmax}(u(c, s)) s \in S

    • 部分场景下是通过TOP N推荐

    搜索 vs 推荐的区别

    在这里插入图片描述

    搜索引擎热力图和长尾理论图

    在这里插入图片描述

    2、为什么推荐系统越来越火?(why)


    推荐系统存在的前提:

    • 信息过载
    • ⽤户需求不明确

    推荐系统的⽬标:

    • ⾼效连接⽤户和物品,发现长尾商品
    • 留住⽤户和内容⽣产者,实现商业⽬标

    推荐系统意义

    1、对用户而言:

    找到好玩的东西

    帮助决策

    发现新鲜事物

    2、对商家而言:

    • 提供个性化服务,提高信任度和粘性

    • 增加营收 …

    3、推荐系统需要做怎么做? (How)

    根据用户的:

    a) 历史行为

    b) 社交关系

    c) 兴趣点

    d) 所处上下文环境

    e) …

    去判断用户的当前需求/感兴趣的item

    4、**寻求解决信息过载的思路 **

    • 之前解决相信过载的方式有:

    分类导航页 => 雅虎

    搜索引擎 => 谷歌,必应,度娘

    • 但是,人总是期望计算机尽量多地服务

      我们不愿意去想搜索词

      希望系统自动挖掘自己的兴趣点

      希望系统能给我们惊喜

    • 推荐系统就在这种环境下产生的。目前有今日头条,虾米音乐,电商猜你喜欢,豆瓣…等等。

    5、推荐系统结构

    在这里插入图片描述

    推荐系统评估

    在这里插入图片描述

    一、评估指标

    •准确性 •满意度 •覆盖率 •多样性 •新颖性 •惊喜度
    •信任度 •实时性 •鲁棒性 •可扩展性 •商业⽬标 •⽤户留存

    用户的偏好的显示反馈和隐式反馈(Explicit vs Implicit)

    在这里插入图片描述

    1、准确性

    **①准确性(学术界) **

    评分预测:

    打分系统 设ruir_{u i}为用户u对物品i的实际评分, r^ui\hat{r}_{u i}为预测分,则有如下误差判定标准

    • 均方根误差

    RMSE=u,iT(ruir^ui)2T \mathrm{RMSE}=\sqrt{\frac{\sum_{u, \mathrm{i} \in T}\left(r_{ui}-\hat{r}_{u i}\right)^{2}}{|T|}}

    • 平均绝对误差

    MAE=u,iTruir^uiT \mathrm{MAE}=\frac{\sum_{u, i \in T}\left|r_{u i}-\hat{r}_{u i}\right|}{|T|}
    topN推荐

    设R(u)为根据训练建立的模型在测试集上的推荐, T(u)为测试集上用户的选择

    准确率 vs 召回率

    Recall=uUR(u)T(u)uUR(u) \operatorname{Recall}=\frac{\sum_{u \in U}|R(u) \cap T(u)|}{\sum_{u \in U}|R(u)|}

    Recall=uUR(u)T(u)uUT(u) \operatorname{Recall}=\frac{\sum_{u \in U}|R(u) \cap T(u)|}{\sum_{u \in U}|T(u)|}

    ②准确性(⼯业界)

    在这里插入图片描述

    2、覆盖度

    1、覆盖率
    Coverage=uUR(u)I Coverage=\frac{\left|\bigcup_{u \in U} R(u)\right|}{|I|}

    在这里插入图片描述

    2、信息熵

    衡量一个事件的不确定性的。候选的越多,说明不确定性越强。
    H=i=1np(i)logp(i) H=-\sum_{i=1}^{n} p(i) \log p(i)

    在这里插入图片描述

    当信息熵越大,推荐的越是均匀,照顾的长尾越大的。

    3、基尼系数

    衡量财产分配平稳程度的指标(判断分配平等程度的指标)。

    设实际收入分配曲线和收入分配绝对平等曲线之间的面积为A,实际收入分配曲线右下方的面积为B。并以A除以(A+B)的商表示不平等程度。

    G=1n1j=1n(2jn1)p(ij) G=\frac{1}{n-1} \sum_{j=1}^{n}(2 j-n-1) p\left(i_{j}\right)

    p(ij)j p\left(i_{j}\right)表示通过流行度排序第j小的商品出现的次数的概率。

    在这里插入图片描述

    在这里插入图片描述

    4、分层流量占比

    3、多样性

    优秀的推荐系统能保证推荐结果列表中物品的丰富性(两 两之间的差异性)。
    设s(i,j)表示物品i 和 j之间的相似度,多样性表示如下:
    Diversity(R(u))=1i,jR(u),ijs(i,j)12R(u)(R(u)1) Diversity(R(u))=1-\frac{\sum_{i, j \in R(u), i \neq j} s(i, j)}{\frac{1}{2}|R(u)|(|R(u)|-1)}

    Diversity=1UuUDiversity(R(u)) Diversity=\frac{1}{|U|} \sum_{u \in U} \text {Diversity}(R(u))

    多样性&新颖性&惊喜性

    • 多样性:推荐列表中两两物品的不相似性。(相似性如何度量?)

    • 新颖性:未曾关注的类别、作者;推荐结果的平均流⾏度

    • 惊喜性:历史不相似(惊)但很满意(喜)

    • 信任度:提供可靠的推荐理由

    • 实时性:实时更新程度

    往往需要牺牲准确性

    • 使⽤历史⾏为预测⽤户对某个物品的喜爱程度
    • 系统过度强调实时性

    4、Exploitation & Exploration (开发与探索)

    什么是Bandit算法(为选择而生)

    我们会遇到很多选择的场景。上哪个大学,学什么专业,去哪家公司,中午吃什么等等。这些事情,都让选择困难症的我们头很大。那么,有算法能够很好地对付这些问题吗?

    当然有!那就是Bandit算法。

    Bandit算法来源于历史悠久的赌博学,它要解决的问题是这样的[1]:

    一个赌徒,要去摇老虎机,走进赌场一看,一排老虎机,外表一模一样,但是每个老虎机吐钱的概率可不一样,他不知道每个老虎机吐钱的概率分布是什么,那么每次该选择哪个老虎机可以做到最大化收益呢?这就是多臂赌博机问题(Multi-armed bandit problem, K-armed bandit problem, MAB)。

    怎么解决这个问题呢?最好的办法是去试一试,不是盲目地试,而是有策略地快速试一试,这些策略就是Bandit算法。

    这个多臂问题,推荐系统里很多问题都与它类似:

    1. 假设一个用户对不同类别的内容感兴趣程度不同,那么我们的推荐系统初次见到这个用户时,怎么快速地知道他对每类内容的感兴趣程度?这就是推荐系统的冷启动。
    2. 假设我们有若干广告库存,怎么知道该给每个用户展示哪个广告,从而获得最大的点击收益?是每次都挑效果最好那个么?那么新广告如何才有出头之日?
    3. 我们的算法工程师又想出了新的模型,有没有比A/B test更快的方法知道它和旧模型相比谁更靠谱?
    4. 如果只是推荐已知的用户感兴趣的物品,如何才能科学地冒险给他推荐一些新鲜的物品?

    •Exploitation:选择现在可能最佳的⽅案

    •Exploration:选择现在不确定的⼀些⽅案,但未来可能会有⾼收益 的⽅案。

    在做两类决策的过程中,不断更新对所有决策的不确定性的认知,优化 长期的⽬标函数

    在这里插入图片描述

    推荐系统里面有两个经典问题:EE和冷启动。前者涉及到平衡准确和多样,后者涉及到产品算法运营等一系列。

    Multi-armed bandit problem

    在这里插入图片描述

    1. 推荐系统领域,领域有两个经典问题:EE问题和用户冷启动问题。[2]

    ​ EE问题:上面提到过的exploit-explore问题;比如已知某用户对A产品感兴趣,那么在大多数时间要给他推送A的链接才能赚钱,这就是exploit;但是为了赚更多的钱,就需要知道他还对哪些产品感兴趣,那么在有些时候就可以尝试一下给他推送B,C,D,E等选择看用户的反应,这就是explore

    用户冷启动问题:面对新用户时如何用尽量少的次数,猜出用户的大致兴趣。[2]

    1. 临床测试场景clinical trials

    ​ 假设有一批用于治疗某种疾病的新药需要进行测试,目标肯定是在尽量少的测试体上进行尝试,找出效果最好的那一种然后应用于其他病患。由于测试需要等待并观察,尽早找出效果最好的药物类型也有利于救助更多的病患;同时,该方法也有助于减少承受不良药物副作用的测试体数量/增加病患存活率。[3]

    **Bandit算法-原理 **

    • Epsilon-Greedy:以1-epsilon的概率选取当前收益最⼤大的臂, 以epsilon的概率随机选取⼀一个臂。(缺点是后续观察后就找到规律了,就不必要继续以当前概率探索)

    • Upper Confidence Bound:均值越⼤大,标准差越⼩小,被选中的 概率会越来越⼤大

    xj(t)+2lntTj,t \overline{x}_{j}(t)+\sqrt{\frac{2 \ln t}{T_{j, t}}}

    (首先探索出了一些一定的价值,大概知道每个老虎机的收益率,Upper的意思是认为我们后面探索的是会更好。右边的上量表示总共试过多少,右边下量表示这个老虎机试过多少次,当前老虎机探索的越多,给的机会就越小。目的是相对均匀的选择老虎机去做探索,不让一个老虎机做过多的探索。)

    • Thompson Sampling:每个老虎机臂维护一个beta(wins, lose)分布, 每次⽤用现有的beta分布产⽣生⼀一个随机数,选择随机数最⼤大的臂继续做探索。探索完之后,去更新beta分布。

    区别:

    Upper Confidence Bound(UCB):对于一个老虎机来讲,每次返回的策略结果一样。从收敛的角度来讲,ucb比较慢。

    Thompson Sampling(汤普森采样):有一定的随机策略,他是靠分布产生一个随机数,他不是一个确定的事件。只是满足一个分布。从收敛的角度来讲,汤普森采样实现简单,计算量复杂度小。

    Bandit算法代码实现

    import numpy as np
    import matplotlib.pyplot as plt
    import math
    number_of_bandits=10
    number_of_arms=10
    number_of_pulls=10000
    epsilon=0.3
    min_temp = 0.1
    decay_rate=0.999
    
    def pick_arm(q_values,counts,strategy,success,failure):
    	global epsilon
    	if strategy=="random":
    		return np.random.randint(0,len(q_values))
    
    	if strategy=="greedy":
    		best_arms_value = np.max(q_values)
    		best_arms = np.argwhere(q_values==best_arms_value).flatten()
    		return best_arms[np.random.randint(0,len(best_arms))]
    
    
    	if strategy=="egreedy" or strategy=="egreedy_decay": 
    		if  strategy=="egreedy_decay": 
    			epsilon=max(epsilon*decay_rate,min_temp)
    		if np.random.random() > epsilon:
    			best_arms_value = np.max(q_values)
    			best_arms = np.argwhere(q_values==best_arms_value).flatten()
    			return best_arms[np.random.randint(0,len(best_arms))]
    		else:
    			return np.random.randint(0,len(q_values))
    
    	if strategy=="ucb":
    		total_counts = np.sum(counts)
    		q_values_ucb = q_values + np.sqrt(np.reciprocal(counts+0.001)*2*math.log(total_counts+1.0))
    		best_arms_value = np.max(q_values_ucb)
    		best_arms = np.argwhere(q_values_ucb==best_arms_value).flatten()
    		return best_arms[np.random.randint(0,len(best_arms))]
    
    	if strategy=="thompson":
    		sample_means = np.zeros(len(counts))
    		for i in range(len(counts)):
    			sample_means[i]=np.random.beta(success[i]+1,failure[i]+1)
    		return np.argmax(sample_means)
    
    
    fig = plt.figure()
    ax = fig.add_subplot(111)
    for st in ["greedy","random","egreedy","egreedy_decay","ucb","thompson"]:
    
    	best_arm_counts = np.zeros((number_of_bandits,number_of_pulls))
    
    	for i in range(number_of_bandits):
    		arm_means = np.random.rand(number_of_arms)
    		best_arm = np.argmax(arm_means)
    
    		q_values = np.zeros(number_of_arms)
    		counts = np.zeros(number_of_arms)
    		success=np.zeros(number_of_arms)
    		failure=np.zeros(number_of_arms)
    
    		for j in range(number_of_pulls):
    			a = pick_arm(q_values,counts,st,success,failure)
    
    			reward = np.random.binomial(1,arm_means[a])
    			counts[a]+=1.0
    			q_values[a]+= (reward-q_values[a])/counts[a]
    
    			success[a]+=reward
    			failure[a]+=(1-reward)
    			best_arm_counts[i][j] = counts[best_arm]*100.0/(j+1)
    		epsilon=0.3
    
    
    	ys = np.mean(best_arm_counts,axis=0)
    	xs = range(len(ys))
    	ax.plot(xs, ys,label = st)
    
    plt.xlabel('Steps')
    plt.ylabel('Optimal pulls')
    
    plt.tight_layout()
    plt.legend()
    plt.ylim((0,110))
    plt.show()        
    
    

    Bandit算法-效果

    在这里插入图片描述

    Bandit算法的应用

    •兴趣探索

    •冷启动探索(新用户来了,不晓得他的兴趣爱好)

    •LinUCB:加⼊特征信息。⽤User和Item的特征预估回报及其置信区 间,选择置信区间上界最⼤的Item推荐,观察回报后更新线性关系的参 数,以此达到试验学习的⽬的。

    •COFIBA:bandit结合协同过滤

    •基于⽤户聚类挑选最佳的Item(相似⽤户集体决策的Bandit);

    •基于⽤户的反馈情况调整User和Item的聚类(协同过滤部分)

    EE实践

    •兴趣扩展:相似话题,搭配推荐

    •⼈群算法:userCF、⽤户聚类

    •Bandit算法

    •graph walking

    •平衡个性化推荐和热门推荐⽐例

    •随机丢弃⽤户⾏为历史

    •随机扰动模型参数

    眼前的苟且&远⽅的⽥野

    • 探索伤害⽤户体验,可能导致⽤户流失

    • 探索带来的长期收益(留存率)评估周期长,KPI压⼒⼤

    • 如何平衡实时兴趣和长期兴趣?

    • 如何平衡短期产品体验和长期系统⽣态?

    • 如何平衡⼤众⼜味和⼩众需求?

    • 如何避免劣币趋势良币?

    二、评估方法

    • 问卷调查:成本⾼

    • 离线评估:

    ​ 只能在⽤户看到过的候选集上做评估,且跟线上真实效果存在偏差

    ​ 只能评估少数指标

    ​ 速度快,不损害⽤户体验

    • 在线评估:A/B testing

    (离线只能评估比较少的指标,比如说准确率,覆盖度,多样性。 但是留存率等只能在在线评估。)

    实践:离线评估和在线评估相结合,定期做问卷调查。

    AB testing

    单层实验:以某种分流的⽅法(随机、uid%100),给每个实验组分配⼀定的流量。每个实验组配置不同的实验参数。

    在这里插入图片描述

    •只能⽀持少量实验,不利于迭代

    •实验之间不独⽴,策略之间可能相互影响

    •分流⽅式不灵活

    多层重叠实验框架

    •保留单层实验框架易⽤,快速的优点的同时,增加可扩展性,灵活 性,健壮性。

    •核⼼思路:将参数划分到N个⼦集,每个⼦集都关联⼀个实验层, 每个请求会被N个实验处理,同⼀个参数不能出现在多个层中。

    在这里插入图片描述

    分配函数(流量在每层被打散的⽅法)如何设计?如何保 证每层流量分配的均匀性和正交性?

    •如何处理实验样本的过滤(eg只选取某个地区的⽤户、只 选取新⽤户)?

    •分配多⼤的流量可以使实验置信?

    《Overlapping Experiment Infrastructure: More, Better, Faster Experimentation》

    在这里插入图片描述

    推荐系统实践

    如何解决冷启动问题

    推荐系统的冷启动问题

    •⽤户冷启动:如何为新⽤户做个性化推荐

    •物品冷启动:如何将新物品推荐给⽤户(协同过滤)

    •系统冷启动:⽤户冷启动+物品冷启动 本质是推荐系统依赖历史数据,没有历史数据⽆法预测⽤户偏好

    ⽤户冷启动

    1.收集⽤户特征

    •⽤户注册信息:性别、年龄、地域

    •设备信息:定位、⼿机型号、app列表

    •社交信息、推⼴素材、安装来源

    在这里插入图片描述

    2.制造粗粒度选项,引导⽤户填写兴趣

    在这里插入图片描述

    3.transfer learning:使⽤其它站点的⾏为数据。例如腾讯视频 &QQ⾳乐,今⽇头条&抖⾳

    在这里插入图片描述

    4.新⽼⽤户推荐策略的差异

    • 新⽤户在冷启动阶段更倾向于热门排⾏榜,⽼⽤户会更加需要长尾 推荐(参考Performance of recommender algorithms on top-n recommendation tasks,Netflix@RecSys2010)

    • 推荐候选的代表性&多样性

    • Explore Exploit⼒度

    • 使⽤单独的特征和模型预估

    • 保护⽤户体验(物品冷启动探索、⼴告、推送)

    物品冷启动

    • 物品冷启动时会遇到哪些问题?
    • 如何做物品冷启动推荐?和⽤户冷启动策略有哪些异同?
    • 如何评估物品冷启动推荐的质量及带来的收益?
    • 实验传统的ab testing⽅法是否能够验证冷启动优化效果?

    ⼯业界推荐系统架构

    推荐系统架构

    在这里插入图片描述

    推荐系统架构-Netflix,2013

    在这里插入图片描述

    推荐系统架构-Taobao,2015

    在这里插入图片描述
    推荐系统架构-YouTube,2016

    在这里插入图片描述

    用户历史消息、特征、模型、候选池等

    召回仓、预估仓、重排仓等。

    推荐系统发展阶段

    •1.0:关联规则、热门推荐等统计⽅法

    •2.0:矩阵分解、协同过滤等ML⽅法,离线计算推荐列表

    •3.0:召回 + learning to rank重排序

    •4.0:召回&排序实时化

    •5.0:end2end深度学习,⼀切皆embedding

    •6.0:智能化推荐系统

    推荐系统的发展趋势

    在这里插入图片描述

    学术界&⼯业界区别

    在这里插入图片描述

    推荐系统学习规划

    在这里插入图片描述

    推荐系统学习资料

    在这里插入图片描述

    • 《Item-Based Collaborative Filtering Recommendation Algorithms 》

    • 《 Factorization Meets the Neighborhood: a Multifaceted Collaborative Filtering Model 》

    • 《Matrix factorization techniques for recommender systems》

    ⼯业实践学习资料

    •Facebook实践:recommending items to more than a billion people

    •Quora是如何做推荐的? •《Real-time Personalization using Embeddings for Search Ranking at Airbnb》 •《Deep Neural Networks for YouTube Recommendations》

    •《Wide & Deep Learning for Recommender Systems》

    •《Ad Click Prediction: a View from the Trenches

    展开全文
  • 精心打造出了机器学习与推荐系统课程,将机器学习理论与推荐系统项目实战并重,对机器学习和推荐系统基础知识做了系统的梳理和阐述,并通过电影推荐网站的具体项目进行了实战演练,为有志于增加大数据项目经验、扩展...
  • 量身定制打造的电商推荐系统项目,就是以经过修改的中文亚马逊电商数据集作为依托,并以某电商网站真实的业务架构作为基础来实现的,其中包含了离线推荐与实时推荐体系,综合利用了协同过滤算法以及基于内容的推荐...
  • 推荐系统实战 --- 基于音乐播放推荐

    千次阅读 多人点赞 2020-11-19 19:36:09
    推荐系统每个人都很熟悉这个名词,但是有多少人真正了解过推荐系统的概念。 这期博客将以基于网易云音乐的推荐系统进行一个简单的介绍。 (读到这里不要喷我,我不是网易公司的人但是每个人都有朋友的对吧) 学习此...

    写在前面

    推荐系统每个人都很熟悉这个名词,但是有多少人真正了解过推荐系统的概念。
    这期博客将以基于网易云音乐的推荐系统进行一个简单的介绍。
    (读到这里不要喷我,我不是网易公司的人但是每个人都有朋友的对吧)

    • 学习此课程需要有一定的 Python基础机器学习基础
    • 推荐系统常用库为 Surprise 和 lightfm。
    • 我们这节课使用 Surprise 入门。

    准备数据阶段

    任何的机器学习算法解决问题,首先要考虑的是数据,数据从何而来?

    • 对于酷狗音乐/网易音乐这样的企业而言,用户的收藏和播放数据是可以直接获得的
    • 我们找一个取巧的方式,包含用户音乐兴趣信息,同时又可以获取的数据是什么? 对的,是歌单信息

    网易云歌单
    曾经教了那么多爬虫入门知识,如果认真学了的话,那么这个数据集应该是可以轻松拿到的。我相信我的每个粉丝都可以做到。

    一、原始数据

    利用爬虫技术获取网易云音乐歌单,存储为json格式,此处仅展示原始数据的格式:

    # 每首歌单的格式
    每个歌单的格式
    {
        "result": {
            "id": 111450065,
            "status": 0,
            "commentThreadId": "A_PL_0_111450065",
            "trackCount": 120,
            "updateTime": 1460164523907,
            "commentCount": 227,
            "ordered": true,
            "anonimous": false,
            "highQuality": false,
            "subscribers": [],
            "playCount": 687070,
            "trackNumberUpdateTime": 1460164523907,
            "createTime": 1443528317662,
            "name": "带本书去旅行吧,人生最美好的时光在路上。",
            "cloudTrackCount": 0,
            "shareCount": 149,
            "adType": 0,
            "trackUpdateTime": 1494134249465,
            "userId": 39256799,
            "coverImgId": 3359008023885470,
            "coverImgUrl": "http://p1.music.126.net/2ZFcuSJ6STR8WgzkIi2U-Q==/3359008023885470.jpg",
            "artists": null,
            "newImported": false,
            "subscribed": false,
            "privacy": 0,
            "specialType": 0,
            "description": "现在是一年中最美好的时节,世界上很多地方都不冷不热,有湛蓝的天空和清冽的空气,正是出游的好时光。长假将至,你是不是已经收拾行装准备出发了?行前焦虑症中把衣服、洗漱用品、充电器之类东西忙忙碌碌地丢进箱子,打进背包的时候,我打赌你肯定会留个位置给一位好朋友:书。不是吗?不管是打发时间,小读怡情,还是为了做好攻略备不时之需,亦或是为了小小地装上一把,你都得有一本书傍身呀。读大仲马,我是复仇的伯爵;读柯南道尔,我穿梭在雾都的暗夜;读村上春树,我是寻羊的冒险者;读马尔克斯,目睹百年家族兴衰;读三毛,让灵魂在撒哈拉流浪;读老舍,嗅着老北京的气息;读海茵莱茵,于科幻狂流遨游;读卡夫卡,在城堡中审判……读书的孩子不会孤单,读书的孩子永远幸福。",
            "subscribedCount": 10882,
            "totalDuration": 0,
            "tags": [
                "旅行",
                "钢琴",
                "安静"]
            "creator": {
                "followed": false,
                "remarkName": null,
                "expertTags": [
                    "古典",
                    "民谣",
                    "华语"
                ],
                "userId": 39256799,
                "authority": 0,
                "userType": 0,
                "gender": 1,
                "backgroundImgId": 3427177752524551,
                "city": 360600,
                "mutual": false,
                "avatarUrl": "http://p1.music.126.net/TLRTrJpOM5lr68qJv1IyGQ==/1400777825738419.jpg",
                "avatarImgIdStr": "1400777825738419",
                "detailDescription": "",
                "province": 360000,
                "description": "",
                "birthday": 637516800000,
                "nickname": "有梦人生不觉寒",
                "vipType": 0,
                "avatarImgId": 1400777825738419,
                "defaultAvatar": false,
                "djStatus": 0,
                "accountStatus": 0,
                "backgroundImgIdStr": "3427177752524551",
                "backgroundUrl": "http://p1.music.126.net/LS96S_6VP9Hm7-T447-X0g==/3427177752524551.jpg",
                "signature": "漫无目的的乱听,听着,听着,竟然灵魂出窍了。更多精品音乐美图分享请加我微信hu272367751。微信是我的精神家园,有我最真诚的分享。",
                "authStatus": 0}
            "tracks": [{歌曲1},{歌曲2}, ...]
         }
    }
    
    # 每首歌曲的格式
    {
    	"id": 29738501,
    	"name": "跟着你到天边 钢琴版",
    	"duration": 174001,
    	"hearTime": 0,
    	"commentThreadId": "R_SO_4_29738501",
    	"score": 40,
    	"mvid": 0,
    	"hMusic": null,
    	"disc": "",
    	"fee": 0,
    	"no": 1,
    	"rtUrl": null,
    	"ringtone": null,
    	"rtUrls": [],
    	"rurl": null,
    	"status": 0,
    	"ftype": 0,
    	"mp3Url": "http://m2.music.126.net/vrVa20wHs8iIe0G8Oe7I9Q==/3222668581877701.mp3",
    	"audition": null,
    	"playedNum": 0,
    	"copyrightId": 0,
    	"rtype": 0,
    	"crbt": null,
    	"popularity": 40,
    	"dayPlays": 0,
    	"alias": [],
    	"copyFrom": "",
    	"position": 1,
    	"starred": false,,
    	"starredNum": 0
    	"bMusic": {
    	    "name": "跟着你到天边 钢琴版",
    	    "extension": "mp3",
    	    "volumeDelta": 0.0553125,
    	    "sr": 44100,
    	    "dfsId": 3222668581877701,
    	    "playTime": 174001,
    	    "bitrate": 96000,
    	    "id": 52423394,
    	    "size": 2089713
    	},
    	"lMusic": {
    	    "name": "跟着你到天边 钢琴版",
    	    "extension": "mp3",
    	    "volumeDelta": 0.0553125,
    	    "sr": 44100,
    	    "dfsId": 3222668581877701,
    	    "playTime": 174001,
    	    "bitrate": 96000,
    	    "id": 52423394,
    	    "size": 2089713
    	},
    	"mMusic": {
    	    "name": "跟着你到天边 钢琴版",
    	    "extension": "mp3",
    	    "volumeDelta": -0.000265076,
    	    "sr": 44100,
    	    "dfsId": 3222668581877702,
    	    "playTime": 174001,
    	    "bitrate": 128000,
    	    "id": 52423395,
    	    "size": 2785510
    	},
    	"artists": [
    	    {
    		"img1v1Url": "http://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg",
    		"name": "群星",
    		"briefDesc": "",
    		"albumSize": 0,
    		"img1v1Id": 0,
    		"musicSize": 0,
    		"alias": [],
    		"picId": 0,
    		"picUrl": "http://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg",
    		"trans": "",
    		"id": 122455
    	    }
    	],
    	"album": {
    	    "id": 3054006,
    	    "status": 2,
    	    "type": null,
    	    "tags": "",
    	    "size": 69,
    	    "blurPicUrl": "http://p1.music.126.net/2XLMVZhzVZCOunaRCOQ7Bg==/3274345629219531.jpg",
    	    "copyrightId": 0,
    	    "name": "热门华语248",
    	    "companyId": 0,
    	    "songs": [],
    	    "description": "",
    	    "pic": 3274345629219531,
    	    "commentThreadId": "R_AL_3_3054006",
    	    "publishTime": 1388505600004,
    	    "briefDesc": "",
    	    "company": "",
    	    "picId": 3274345629219531,
    	    "alias": [],
    	    "picUrl": "http://p1.music.126.net/2XLMVZhzVZCOunaRCOQ7Bg==/3274345629219531.jpg",
    	    "artists": [
    		{
    		    "img1v1Url": "http://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg",
    		    "name": "群星",
    		    "briefDesc": "",
    		    "albumSize": 0,
    		    "img1v1Id": 0,
    		    "musicSize": 0,
    		    "alias": [],
    		    "picId": 0,
    		    "picUrl": "http://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg",
    		    "trans": "",
    		    "id": 122455
    		}
    	    ],
    	    "artist": {
    		"img1v1Url": "http://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg",
    		"name": "",
    		"briefDesc": "",
    		"albumSize": 0,
    		"img1v1Id": 0,
    		"musicSize": 0,
    		"alias": [],
    		"picId": 0,
    		"picUrl": "http://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg",
    		"trans": "",
    		"id": 0
    	    }
    	}
    }
    

    二、解析基础歌单数据

    抽取 歌单名称,歌单id,收藏数,所属分类 4个歌单维度的信息
    抽取 歌曲id,歌曲名,歌手,歌曲热度 等4个维度信息歌曲的信息

    之后我们将歌单维度用##分隔,将歌曲维度使用:::分隔
    组织成如下格式:

    【情怀录】泱泱华夏,千古风华##古风,华语,怀旧##392991828##36319 33891487:::礼仪之邦:::HITA:::100.0 31168297:::俯仰河山:::吾恩:::100.0 101085:::为龙:::河图:::100.0 407761300:::踏行:::安九、叶里:::90.0 32432818:::君临天下:::小魂:::90.0 285177:::千年流光:::清莞:::85.0 149875:::重回汉唐:::孙异:::85.0 184408:::笔墨稠:::音频怪物:::100.0 29535956:::声律启蒙:::哈辉:::90.0 28381289:::雅乐寻踪:::i2star:::90.0 185882:::本草纲目:::周杰伦:::100.0 359939:::千年食谱颂:::洛天依:::100.0 31789164:::同衣:::平纱落雁音乐团队:::45.0 33789691:::年事:::冬子:::60.0 29772419:::二十四节气歌:::李秋林:::70.0 28577127:::贺新婚:::小义:::100.0 31587571:::百家姓:::极泷:::40.0 30612309:::李:::李玉刚:::95.0 105863:::中国字画:::极泷:::55.0 33872294:::千本中国通史:::兮曰Crandy:::90.0 459555084:::万神纪(人声本家·正式版):::肥皂菌丨珉珉的猫咪丨:::100.0 366409:::女娲:::墨明棋妙:::85.0 27946227:::纪年轩辕刺:::ZHUCOOL:::90.0 81635:::哪吒:::EDIQ:::95.0 31421175:::诸子百家:::群星:::95.0 26619450:::春秋争霸:::极泷:::60.0 29747545:::大秦 酬知己:::小魂:::90.0 33314024:::君梦成骸:::吾恩:::90.0 34586733:::千秋月别西楚将:::王胖子:::100.0 31587566:::大风歌:::极泷:::85.0 28535272:::三国物语 序章 乱世长歌:::群星:::90.0 29019518:::宇内三分:::五色石南叶:::85.0 31445810:::权御天下:::小魂:::100.0 26619451:::大隋风云:::极泷:::90.0 184567:::风华录:::音频怪物:::100.0 28863164:::且志凌烟:::Assen捷:::90.0 29764306:::盛世芳华:::墨明棋妙:::90.0 29415010:::东京梦华录 (念白版):::墨明棋妙:::75.0 33789671:::大宋豪情:::冬子:::65.0 184407:::日月之尊:::音频怪物:::95.0 28953753:::秦淮八艳:::玉璇玑:::100.0 28971030:::红尘夜歌:::银临:::85.0 27904145:::中华颂:::满江:::85.0 452804956:::华夏之章(人声版):::小旭PRO:::80.0 26217044:::龙文:::谭晶:::95.0 420500021:::千年游 :::李宇春:::100.0

    三、将歌单数据解析为surprise库支持的格式

    定义 parse_song_info 函数来将我们的歌曲分开,四个参数分别是歌曲的网站id,歌曲的名字,歌曲的作者,歌曲的热度,但我们推荐所需的参数并不是需要全部返回,我们只需要此歌曲的网站id、以及用户给的评分、时间戳即可。
    定义 parse_playlist_line 函数将我们的歌单内部id筛选后与我们歌曲分开后返回的四个参数作为总和,我们就能得到Surprise所需的四个参数:userid(内部id) itemid(歌曲网站id) rating(用户评分) timestamp(时间戳,可有可无) 。

    # 解析成userid itemid rating timestamp行格式
    
    import json
    import sys
    import random
    
    def is_null(s): 
        return len(s.split(","))>2
    
    def parse_song_info(song_info):
        try:
            song_id, name, artist, popularity = song_info.split(":::")
            return ",".join([song_id,str(float(random.randint(1,11))),'1300000'])
        except Exception as e:
            return ""
    
    def parse_playlist_line(in_line):
        try:
            contents = in_line.strip().split("\t")
            name, tags, playlist_id, subscribed_count = contents[0].split("##")
            songs_info = map(lambda x:playlist_id+","+parse_song_info(x), contents[1:])
            songs_info = filter(is_null, songs_info)
            return "\n".join(songs_info)
        except Exception as e:
            return False
            
    def parse_file(in_file, out_file):
        out = open(out_file, 'w')
        for line in open(in_file,encoding='utf-8'):
            result = parse_playlist_line(line)
            if(result):
                out.write(result.strip()+"\n")
        out.close()
    

    调用上述程序,我们就能将华语流行音乐歌单解析为suprise格式。

    path = "./data/output/popular/"
    parse_file("./data/popular.playlist", path+"popular_music_suprise_format.txt")
    

    看一下解析结果
    Surprise四个参数

    四、保存 歌单id=>歌单名 和 歌曲id=>歌曲名 的字典

    我们了解并获取了 Surprise 库所需的四个参数。但是我们平时见到网易云音乐的推荐列表中,我们并没有看到一大堆看不懂的id代码在上面,而是做好的歌曲id映射到歌曲名字上给我们返回到网页或软件中。
    parse_playlist_get_info函数接收刚才制作的txt、歌单映射字典、歌曲映射字典三个参数。
    分别将歌单id与歌单名做键值对映射,歌曲id与歌曲名做键值对映射。

    import pickle
    import sys
    
    def parse_playlist_get_info(in_line, playlist_dic, song_dic):
        contents = in_line.strip().split("\t")
        name, tags, playlist_id, subscribed_count = contents[0].split("##")
        playlist_dic[playlist_id] = name
        for song in contents[1:]:
            try:
                song_id, song_name, artist, popularity = song.split(":::")
                song_dic[song_id] = song_name+"\t"+artist
            except:
                print("song format error")
                print(song+"\n")
    
    def parse_file(in_file, out_playlist, out_song):
        #从歌单id到歌单名称的映射字典
        playlist_dic = {}
        #从歌曲id到歌曲名称的映射字典
        song_dic = {}
        for line in open(in_file,encoding='utf-8'):
            parse_playlist_get_info(line, playlist_dic, song_dic)
        #把映射字典保存在二进制文件中
        pickle.dump(playlist_dic, open(out_playlist,"wb")) 
        #可以通过 playlist_dic = pickle.load(open("playlist.pkl","rb"))重新载入
        pickle.dump(song_dic, open(out_song,"wb"))
    

    打印一下playlist_dic字典检查结果
    playlist_dic
    打印一下song_dic字典检查结果
    song_dic


    Python推荐系统Surprise

    Surprise官网
    在推荐系统的建模过程中,我们将用到python库 Surprise(Simple Python RecommendatIon System Engine),是scikit系列中的一个(很多同学用过scikit-learn和scikit-image等库)。

    简单易用,同时支持多种推荐算法:

    算法类名 说明
    random_pred.NormalPredictor Algorithm predicting a random rating based on the distribution of the training set, which is assumed to be normal.
    baseline_only.BaselineOnly Algorithm predicting the baseline estimate for given user and item.
    knns.KNNBasic A basic collaborative filtering algorithm.
    knns.KNNWithMeans A basic collaborative filtering algorithm, taking into account the mean ratings of each user.
    knns.KNNBaseline A basic collaborative filtering algorithm taking into account a baseline rating.
    matrix_factorization.SVD The famous SVD algorithm, as popularized by Simon Funk during the Netflix Prize.
    matrix_factorization.SVDpp The SVD++ algorithm, an extension of SVD taking into account implicit ratings.
    matrix_factorization.NMF A collaborative filtering algorithm based on Non-negative Matrix Factorization.
    slope_one.SlopeOne A simple yet accurate collaborative filtering algorithm.
    co_clustering.CoClustering A collaborative filtering algorithm based on co-clustering.

    其中基于近邻的方法(协同过滤)可以设定不同的度量准则。

    相似度度量标准 度量标准说明
    cosine Compute the cosine similarity between all pairs of users (or items).
    msd Compute the Mean Squared Difference similarity between all pairs of users (or items).
    pearson Compute the Pearson correlation coefficient between all pairs of users (or items).
    pearson_baseline Compute the (shrunk) Pearson correlation coefficient between all pairs of users (or items) using baselines for centering instead of means.

    支持不同的评估准则

    评估准则 准则说明
    rmse Compute RMSE (Root Mean Squared Error).
    msd Compute MAE (Mean Absolute Error).
    fcp Compute FCP (Fraction of Concordant Pairs).

    Surprise使用案例

    基本使用方法如下

    # 可以使用上面提到的各种推荐系统算法
    from surprise import SVD
    from surprise import Dataset
    from surprise.model_selection import cross_validate
    
    # 默认载入movielens数据集
    data = Dataset.load_builtin('ml-100k')
    # k折交叉验证(k=3)
    # data.split(n_folds=3)
    # 试一把SVD矩阵分解
    algo = SVD()
    # 在数据集上测试一下效果
    cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=5, verbose=True)
    

    输出结果
    load_builtin() 方法将提供下载 movielens-100k 数据集(如果尚未下载),并将其保存.surprise_data在主目录中的文件夹中(也可以选择将其保存在其他地方)。
    该cross_validate() 函数根据 cv 参数运行交叉验证程序,并计算一些accuracy措施。我们在这里使用经典的5倍交叉验证,但可以使用更漂亮的迭代器。

    简单介绍一下Reader参数:

    • line_format 中编写自己数据集文本格式的切分结构。
    • sep 中编写数据间隔用什么切分。

    我们也可以载入自己的方法:

    import os
    from surprise import Reader
    # 指定文件所在路径
    file_path = os.path.expanduser('~/.surprise_data/ml-100k/ml-100k/u.data')
    # 告诉文本阅读器,文本的格式是怎么样的
    reader = Reader(line_format='user item rating timestamp', sep='\t')
    # 加载数据
    data = Dataset.load_from_file(file_path, reader=reader)
    # 手动切分成5折(方便交叉验证)
    # data.split(n_folds=5)
    

    算法调参(让推荐系统有更好的效果)

    这里实现的算法用到的算法无外乎也是SGD等,因此也有一些超参数会影响最后的结果,我们同样可以用sklearn中常用到的网格搜索交叉验证(GridSearchCV)来选择最优的参数。简单的例子如下所示:

    # 定义好需要优选的参数网格
    param_grid = {'n_epochs': [5, 10], 'lr_all': [0.002, 0.005],
                  'reg_all': [0.4, 0.6]}
    # 使用网格搜索交叉验证
    grid_search = GridSearch(SVD, param_grid, measures=['RMSE', 'FCP'])
    # 在数据集上找到最好的参数
    data = Dataset.load_builtin('ml-100k')
    data.split(n_folds=3)
    grid_search.evaluate(data)
    # 输出调优的参数组 
    # 输出最好的RMSE结果
    print(grid_search.best_score['RMSE'])
    # >>> 0.96117566386
    
    # 输出对应最好的RMSE结果的参数
    print(grid_search.best_params['RMSE'])
    # >>> {'reg_all': 0.4, 'lr_all': 0.005, 'n_epochs': 10}
    
    # 最好的FCP得分
    print(grid_search.best_score['FCP'])
    # >>> 0.702279736531
    
    # 对应最高FCP得分的参数
    print(grid_search.best_params['FCP'])
    # >>> {'reg_all': 0.6, 'lr_all': 0.005, 'n_epochs': 10}
    

    使用协同过滤基于movielens数据集构建模型并进行预测

    KNNWithMeans基本的假设是用户和物品的评分有高低,考虑了每个用户打分均值或者每个item打分的均值,去除参考用户打分整体偏高和偏低的影响。
    基于用户相似度(user-based)和基于物品相似度(item-based)的计算公式如下:
    KNNWithMeans公式

    这是一个movielens的例子。使用KNNWithMeans。
    使用协同过滤、矩阵分解使用场景:我想听一首和这首歌相类似的一首歌。

    from surprise import KNNWithMeans
    from surprise import Dataset
    from surprise.model_selection import cross_validate
    
    # 默认载入movielens数据集
    data = Dataset.load_builtin('ml-100k')
    
    algo = KNNWithMeans()
    result = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3, verbose=True)
    

    运行结果:
    运行
    我们大致看看data数据集究竟长什么样。

    data.raw_ratings[:10]
    # 解释:('196', '242', 3.0, '881250949')为196号用户给242号电影给了3.0分
    # -> [('196', '242', 3.0, '881250949'),
    # ('186', '302', 3.0, '891717742'),
    # ('22', '377', 1.0, '878887116'),
    # ('244', '51', 2.0, '880606923'),
    # ('166', '346', 1.0, '886397596'),
    # ('298', '474', 4.0, '884182806'),
    # ('115', '265', 2.0, '881171488'),
    # ('253', '465', 5.0, '891628467'),
    # ('305', '451', 3.0, '886324817'),
    # ('6', '86', 3.0, '883603013')]
    

    read_item_names函数与之前给出的映射函数类似,都是为了给使用者返回一个他们看得懂的名字,而不是返回一个id值。

    def read_item_names():
        """
        获取电影名到电影id 和 电影id到电影名的映射
        """
        file_name = (os.path.expanduser('~') + '/.surprise_data/ml-100k/ml-100k/u.item')
        rid_to_name = {} # 电影id -> 电影名
        name_to_rid = {} # 电影名 -> 电影id
        with open(file_name, 'r', encoding='ISO-8859-1') as f: # 官网给的编码格式ISO-8859-1
            for line in f:
                line = line.split('|')
                rid_to_name[line[0]] = line[1]
                name_to_rid[line[1]] = line[0]
    
        return rid_to_name, name_to_rid
    

    首先我们先把数据load_builtin()读取出来。
    build_full_trainset()会将data的稀疏训练集构建出一个矩阵。
    sim_options为超参数,将给出基于xx的协同过滤。

    • ‘name’: ‘pearson_baseline’ 度量准则为pearson_baseline距离
    • ‘user_based’: False 为基于物品的协同过滤
    import os
    
    from surprise import KNNBaseline
    from surprise import Dataset
    
    # 首先,用算法计算相互间的相似度
    data = Dataset.load_builtin('ml-100k')
    trainset = data.build_full_trainset()
    sim_options = {'name': 'pearson_baseline', 'user_based': False}
    algo = KNNBaseline(sim_options=sim_options)
    algo.fit(trainset)
    

    接下来的内容比较核心的推荐算法内容。
    我们所能看到网易云音乐等推荐系统都是以歌推歌,但是计算机并不了解歌曲的样式。
    要先将歌曲的名字转换为其对应的网页id号。(使用 name_to_rid 转换)
    其次将网页的id号转变为推荐系统认识的内部id号。(使用 algo.trainset.to_inner_iid() 函数转换)
    使用 get_neighbors() 函数将内部id号的近邻几个内部id号推荐返回回来。(此处返回一个列表格式)
    使用列表推倒式将内部id转换为网页id号,再转到音乐名字,返回给客户端。
    电影推荐也是这样的。

    # 获取电影名到电影id 和 电影id到电影名的映射
    rid_to_name, name_to_rid = read_item_names()
    toy_story_raw_id = name_to_rid['Toy Story (1995)']
    print(toy_story_raw_id)
    # -> '1'
    toy_story_inner_id = algo.trainset.to_inner_iid(toy_story_raw_id)
    print(toy_story_inner_id)
    # -> 24
    # 找到最近的10个邻居
    toy_story_neighbors = algo.get_neighbors(toy_story_inner_id, k=15)
    print(toy_story_neighbors)
    # -> [433, 101, 302, 309, 971, 95, 26, 561, 816, 347, 904, 663, 231, 715, 471]
    
    # 从近邻的id映射回电影名称
    toy_story_neighbors = (algo.trainset.to_raw_iid(inner_id) for inner_id in toy_story_neighbors)
    toy_story_neighbors = (rid_to_name[rid] for rid in toy_story_neighbors)
    
    print('The 10 nearest neighbors of Toy Story are:')
    for movie in toy_story_neighbors:
        print(movie)
    # -> The 10 nearest neighbors of Toy Story are:
    #Beauty and the Beast (1991)
    #Raiders of the Lost Ark (1981)
    #That Thing You Do! (1996)
    #Lion King, The (1994)
    #Craft, The (1996)
    #Liar Liar (1997)
    #Aladdin (1992)
    #Cool Hand Luke (1967)
    #Winnie the Pooh and the Blustery Day (1968)
    #Indiana Jones and the Last Crusade (1989)
    #Eddie (1996)
    #Transformers: The Movie, The (1986)
    #Jurassic Park (1993)
    #Mouse Hunt (1997)
    #Apollo 13 (1995)
    

    使用协同过滤基于网易云音乐数据构建模型并进行预测

    上一节我们了解了推荐系统的原理,能够拿到每一首歌邻近的几首歌。
    之后我们将做对于新歌或者一些线下并没有计算出来的歌曲去做预测。

    基于用户的协同过滤

    主要思想:找出和当前用户兴趣相近的用户,针对网易云音乐歌单数据而言,这里的用户就是歌单。
    方法与之前相同:歌单名称->歌单id->内部id->找出近邻内部id->近邻歌单id->近邻歌单名称

    print("开始训练模型...")
    #sim_options = {'user_based': False}
    #algo = KNNBaseline(sim_options=sim_options)
    algo = KNNBaseline()
    algo.fit(trainset)
    
    current_playlist = list(name_id_dic.keys())[39]
    print("歌单名称", current_playlist)
    
    # 取出近邻
    # 映射名字到id
    playlist_id = name_id_dic[current_playlist]
    print("歌单id", playlist_id)
    # 取出来对应的内部user id => to_inner_uid
    playlist_inner_id = algo.trainset.to_inner_uid(playlist_id)
    print("内部id", playlist_inner_id)
    
    playlist_neighbors = algo.get_neighbors(playlist_inner_id, k=15)
    
    # 把歌曲id转成歌曲名字
    # to_raw_uid映射回去
    playlist_neighbors = (algo.trainset.to_raw_uid(inner_id)
                           for inner_id in playlist_neighbors)
    playlist_neighbors = (id_name_dic[playlist_id]
                           for playlist_id in playlist_neighbors)
    print("和歌单 《", current_playlist, "》 最接近的10个歌单为:\n")
    for playlist in playlist_neighbors:
        print(playlist, algo.trainset.to_inner_uid(name_id_dic[playlist]))
    # ->开始训练模型...
    #Estimating biases using als...
    #Computing the msd similarity matrix...
    #Done computing similarity matrix.
    #歌单名称 当城市有了音乐,你是否也有了思念?
    #歌单id 493527432
    #内部id 39
    #
    #和歌单 《 当城市有了音乐,你是否也有了思念? 》 最接近的10个歌单为:
    #
    #[小风收集]21世纪年轻人的音乐 24
    #70后的8910年代 216
    #《我是歌手3》原唱大碟(完整版) 279
    #盘点云村破万评论的华语歌曲Top50 405
    #【华语】那些充满着画面感的歌 557
    #夏日清凉,听什么爱啊情啊?开心就好 724
    #世界那么大,还好遇见你! 877
    #华语经典 895
    #流行歌手的古风和中国风 917
    #听歌啊,有时候需要一些缘分 955
    #初中,高中,大学毕业 1006
    #华语乐坛四十载之经典和冷门的国语与粤语歌 1030
    #致我们终将逝去的青春(华语精选) 26
    #中国风●周杰伦●方文山作品 90
    #老老老老歌曲 108
    

    基于协同过滤的用户评分预测

    根据项目之间的相似性预测用户对未评分项目的评分,解决协同过滤推荐的数据稀疏性问题。在此基础上,分别从用户评分、用户属性、用户历史行为等角度对用户的相似度进行衡量,并综合各种相似度用来计算目标用户的最近邻居。最后,综合利用项目评分预测和用户多相似度,提出一种改进的协同过滤推荐算法。

    我们首先使用 pickle 提取本地的字典文件

    import pickle
    # 重建歌曲id到歌曲名的映射字典
    song_id_name_dic = pickle.load(open(path+"popular_song.pkl","rb"))
    print("加载歌曲id到歌曲名的映射字典完成...")
    

    song_id_name_dic 组成:
    song_id_name_dic

    我们有了一个网页id映射名字的字典,根据上文我们需要获取一个名字映射网页id的字典。

    # 重建歌曲名到歌曲id的映射字典
    song_name_id_dic = {}
    for song_id in song_id_name_dic:
        song_name_id_dic[song_id_name_dic[song_id]] = song_id
    print("加载歌曲名到歌曲id的映射字典完成...")
    

    song_name_id_dic 组成:
    song_name_id_dic
    预测一下某内部编码的推荐结果。
    predict()对每个得分进行计算,然后遍历所有得分得到TOP-N。

    #内部编码的4号用户
    user_inner_id = 4
    user_rating = trainset.ur[user_inner_id]
    items = map(lambda x:x[0], user_rating)
    for song in items:
        print(algo.predict(user_inner_id, song, r_ui=1), song_id_name_dic[algo.trainset.to_raw_iid(song)])
    

    我们可以看到这个预测结果与 est 有关,内部编码为4的音乐的用户评价得分平均为5.00
    此预测结果与每个用户打分机制相同,会根据每个用户给每个音乐打分相同的预测返回。
    所以预测结果大部分都会是评价在5.00附近的音乐返回。
    预测结果

    基于矩阵分解的用户评分预测

    矩阵分解,简单来说,就是把原来的大矩阵,近似分解成两个小矩阵的乘积,实际推荐计算时不再使用大矩阵,而是使用分解得到的两个小矩阵。

    假设用户物品评分矩阵为 R, 维度为mxn,即 m 个用户, n 个物品。选择一个很小的数 k,k 比 m 和 n 都小很多,然后通过算法生成两个矩阵 P 和 Q,这两个矩阵的要求如下:P 的维度是 mxk,Q 的维度是 nxk, P 和 Q 的转置相乘结果为 R,即R=P*Q^T。也就是说分解得到的矩阵P和Q可以还原成原始的矩阵R。

    ### 使用NMF
    from surprise import NMF
    from surprise import Dataset
    
    file_path = os.path.expanduser(path+'./popular_music_suprise_format.txt')
    # 指定文件格式
    reader = Reader(line_format='user item rating timestamp', sep=',')
    # 从文件读取数据
    music_data = Dataset.load_from_file(file_path, reader=reader)
    # 构建数据集和建模
    algo = NMF()
    trainset = music_data.build_full_trainset()
    algo.fit(trainset)
    
    user_inner_id = 4
    user_rating = trainset.ur[user_inner_id]
    items = map(lambda x:x[0], user_rating)
    for song in items:
        print(algo.predict(algo.trainset.to_raw_uid(user_inner_id), algo.trainset.to_raw_iid(song), r_ui=1), song_id_name_dic[algo.trainset.to_raw_iid(song)])
    

    预测结果如下:
    预测结果截图

    模型保存与加载

    Surprise库中自带dump库可以保存训练的模型。
    并可以使用load加载本地模型。

    import surprise
    surprise.dump.dump('./model/recommendation.model', algo=algo)
    # 可以用下面的方式载入
    algo = surprise.dump.load('./model/recommendation.model')
    

    不同的推荐系统算法评估

    import os
    from surprise import Reader, Dataset
    # 指定文件路径
    file_path = os.path.expanduser(path+'./popular_music_suprise_format.txt')
    # 指定文件格式
    reader = Reader(line_format='user item rating timestamp', sep=',')
    # 从文件读取数据
    music_data = Dataset.load_from_file(file_path, reader=reader)
    
    from surprise.model_selection import cross_validate
    

    使用BaselineOnly

    from surprise import BaselineOnly
    algo = BaselineOnly()
    result = cross_validate(algo, music_data, measures=['RMSE', 'MAE'], cv=5, verbose=True)
    # -> Estimating biases using als...
    #Estimating biases using als...
    #Estimating biases using als...
    #Estimating biases using als...
    #Estimating biases using als...
    #Evaluating RMSE, MAE of algorithm BaselineOnly on 5 split(s).
    #
    #                  Fold 1  Fold 2  Fold 3  Fold 4  Fold 5  Mean    Std     
    #MAE (testset)     0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  
    #RMSE (testset)    0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  
    #Fit time          0.90    0.93    0.75    0.72    0.76    0.81    0.09    
    #Test time         0.64    0.63    0.40    0.41    0.42    0.50    0.11
    

    使用基础版协同过滤

    from surprise import KNNBasic
    algo = KNNBasic()
    result = cross_validate(algo, music_data, measures=['RMSE', 'MAE'], cv=5, verbose=True)
    # -> Computing the msd similarity matrix...
    #Done computing similarity matrix.
    #Computing the msd similarity matrix...
    #Done computing similarity matrix.
    #Computing the msd similarity matrix...
    #Done computing similarity matrix.
    #Computing the msd similarity matrix...
    #Done computing similarity matrix.
    #Computing the msd similarity matrix...
    #Done computing similarity matrix.
    #Evaluating RMSE, MAE of algorithm KNNBasic on 5 split(s).
    #
    #                  Fold 1  Fold 2  Fold 3  Fold 4  Fold 5  Mean    Std     
    #MAE (testset)     0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  
    #RMSE (testset)    0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  
    #Fit time          0.13    0.24    0.20    0.21    0.21    0.20    0.04    
    #Test time         2.01    2.12    1.99    1.98    1.98    2.01    0.06
    

    使用均值协同过滤

    from surprise import KNNWithMeans
    algo = KNNWithMeans()
    result = cross_validate(algo, music_data, measures=['RMSE', 'MAE'], cv=5, verbose=True)
    # -> Computing the msd similarity matrix...
    #Done computing similarity matrix.
    #Computing the msd similarity matrix...
    #Done computing similarity matrix.
    #Computing the msd similarity matrix...
    #Done computing similarity matrix.
    #Computing the msd similarity matrix...
    #Done computing similarity matrix.
    #Computing the msd similarity matrix...
    #Done computing similarity matrix.
    #Evaluating RMSE, MAE of algorithm KNNWithMeans on 5 split(s).
    #
    #                  Fold 1  Fold 2  Fold 3  Fold 4  Fold 5  Mean    Std     
    #MAE (testset)     0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  
    #RMSE (testset)    0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  
    #Fit time          0.30    0.30    0.25    0.28    0.27    0.28    0.02    
    #Test time         2.85    2.17    2.21    2.16    1.93    2.27    0.31
    

    使用协同过滤baseline

    from surprise import KNNBaseline
    algo = KNNBaseline()
    result = cross_validate(algo, music_data, measures=['RMSE', 'MAE'], cv=5, verbose=True)
    # -> Estimating biases using als...
    #Computing the msd similarity matrix...
    #Done computing similarity matrix.
    #Estimating biases using als...
    #Computing the msd similarity matrix...
    #Done computing similarity matrix.
    #Estimating biases using als...
    #Computing the msd similarity matrix...
    #Done computing similarity matrix.
    #Estimating biases using als...
    #Computing the msd similarity matrix...
    #Done computing similarity matrix.
    #Estimating biases using als...
    #Computing the msd similarity matrix...
    #Done computing similarity matrix.
    #Evaluating RMSE, MAE of algorithm KNNBaseline on 5 split(s).
    #
    #                  Fold 1  Fold 2  Fold 3  Fold 4  Fold 5  Mean    Std     
    #MAE (testset)     0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  
    #RMSE (testset)    0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  0.0000  
    #Fit time          0.86    0.87    1.08    0.96    0.92    0.94    0.08    
    #Test time         2.47    2.36    3.00    2.57    2.48    2.57    0.22
    

    使用SVD

    from surprise import SVD
    algo = SVD()
    result = cross_validate(algo, music_data, measures=['RMSE', 'MAE'], cv=5, verbose=True)
    # -> Evaluating RMSE, MAE of algorithm SVD on 5 split(s).
    #
    #                  Fold 1  Fold 2  Fold 3  Fold 4  Fold 5  Mean    Std     
    #MAE (testset)     0.0165  0.0167  0.0167  0.0165  0.0166  0.0166  0.0001  
    #RMSE (testset)    0.0365  0.0366  0.0368  0.0364  0.0369  0.0366  0.0002  
    #Fit time          12.87   12.77   13.10   12.83   12.82   12.88   0.12    
    #Test time         0.63    0.48    0.65    0.46    0.61    0.56    0.08
    

    使用SVD++

    from surprise import SVDpp
    algo = SVDpp()
    result = cross_validate(algo, music_data, measures=['RMSE', 'MAE'], cv=5, verbose=True)
    

    使用NMF

    from surprise import NMF
    algo = NMF()
    result = cross_validate(algo, music_data, measures=['RMSE', 'MAE'], cv=5, verbose=True)
    
    展开全文
  • 【32】最新北风网人工智能+机器学习+深度学习+推荐系统实战第3期视频教程 .txt【32】最新北风网人工智能+机器学习+深度学习+推荐系统实战第3期视频教程 .txt【32】最新北风网人工智能+机器学习+深度学习+推荐系统...
  • spark 推荐系统实战

    2020-04-06 16:53:50
    音乐推荐系统 Audioscrobbler 提供了一个公开的数据集,该数据集记录了听众播放过哪些艺术家的歌曲。last.fm 公司使用这些音乐播放记录构建了一个强大的音乐推荐引擎。这个推荐引擎系统覆盖了数百万的用户。 ...

    问题背景

    音乐推荐系统 Audioscrobbler 提供了一个公开的数据集,该数据集记录了听众播放过哪些艺术家的歌曲。last.fm 公司使用这些音乐播放记录构建了一个强大的音乐推荐引擎。这个推荐引擎系统覆盖了数百万的用户。

    Audioscrobbler 数据集只记录了播放数据,如“Bob 播放了一首 Prince 的歌曲”,但没有歌曲的评分信息。这种类型的数据通常被称为隐式反馈数据,因为用户和艺术家的关系是通过其他行动隐含体现出来的,而不是通过显式的评分或点赞得到的。

    数据集的下载地址为:http://www.iro.umontreal.ca/~lisa/datasets/profiledata_06-May2005.tar.gz

    主要的数据集在文件 user_artist_data.txt 中,它包含 141 000 个用户和 160 万个艺术家,记录了约 2420 万条用户播放艺术家歌曲的信息,其中包括播放次数信息。

    推荐算法:交替最小二乘推荐算法

    数据集提供的隐式反馈数据,只记录了用户和歌曲之间的交互情况。我们的学习算法不需要任何用户和艺术家的属性信息。举个例子,根据两个用户播放过许多相同歌曲来判断他们可能都喜欢某首歌,这类算法通常称为协同过滤算法

    Audioscrobbler 数据集包含了数千万条某个用户播放了某个艺术家歌曲次数的信息,看起来是很大。但从另一方面来看数据集又很小而且不充足,因为数据集是稀疏的。虽然数据集覆盖 160 万个艺术家,但平均来算,每个用户只播放了大约 171 个艺术家的歌曲。有的用户只播放过一个艺术家的歌曲。对这类用户,我们也希望算法能给出像样的推荐。最后,我们希望算法的扩展性好,不但能用于构建大型模型,而且推荐速度快,通常都要求推荐是接近实时的,也就是在一秒内给出推荐。

    矩阵分解模型

    本实例用的是一种矩阵分解模型。数学上,把用户和产品数据当成一个大矩阵 A,其具有如下性质:

    1. 矩阵第 i 行和第 j 列上的元素有值,代表用户 i 播放过艺术家 j 的音乐。
    2. 矩阵 A 是稀疏的:A 中大多数元素都是 0,因为相对于所有可能的用户 - 艺术家组合,只有很少一部分组合会出现在数据中。

    算法将 AA 分解为两个小矩阵 XXYY 的乘积。矩阵 XX 和矩阵 YY 非常细长,因为 AA 有很多行和列。XXYY^\top 的行很多而列很少,列数用 k 表示,这 k 个列用于解释数据中的交互关系。
    矩阵分解
    由于 kk 的值小,矩阵分解算法只能是某种近似,如图所示 。

    矩阵分解算法有时称为补全( 矩阵分解算法有时称为补全算法,因为原始矩阵 AA 可能 非常稀疏,但乘积 XYXY^\top是稠密的,即使该矩阵存在非零元素数量也常少。因此模型只是对 因此模型只是对 A 的一种近似。

    原始 AA 中大量元素是缺失的(元素值为 0),算法为这些缺失元素生成(补全)了一个值 。

    两个矩阵分别有 一行对应每用户和艺术家。每行的值很少,只kk个 。每个值代表了对应模型的一个隐含特征。因此矩阵表示了用户和艺术家怎样关联到这些隐含特征,而隐含特征可能就对应偏好或类别。于是问题简化为用户特征矩阵 X 和艺术家矩阵 Y 的乘积,该结果是对整个稠密用户-艺术家相互关系矩阵的完整估计。

    XYXY^\top 应该尽可能逼近 A,毕竟这是所有后续工作的基础,但它不能也应该完全复制 AA。然而想直接时得到XXYY的最优解是不可能的。如果 YY 已知,求 XX 的最优解是非常容易,反之亦然。但 XXYY 事先都是未知的。

    接下来介绍如何求解XXYY,就要用到交替最小二乘算法。

    交替最小二乘法

    虽然 YY 是未知的,但我们可以把它初始化为随机行向量矩阵。

    接着运用简单线性代数知识, 就能在给定 AAYY 的条件下求出XX的最优解。

    实际上, XiX_iAiA_iYY 的函数:
    Ai=XiY A_i = X_i Y^\top

    因此可以很容易分开计算 XX 的每一行。所以我们可将其并行化:
    AiY(YY)1=Xi A_iY(Y^\top Y)^{-1} = X_i

    这里给出方程式只是为了说明行向量计算方法,实践中从来不会对矩阵求逆,我们会借助于 QR分解 之类的方法。

    同理,我们可以由 XX 计算每个计算每个 YjY_j。然后又可以由 YY 计算 XX,这样反复下去,就是算法名 称中“交替”的来由。

    只要这个过程一直继续,XXYY最终会收敛得到一个合适的结果。

    scala

    package com.cloudera.datascience.recommender
    
    import scala.collection.Map
    import scala.collection.mutable.ArrayBuffer
    import scala.util.Random
    import org.apache.spark.broadcast.Broadcast
    import org.apache.spark.ml.recommendation.{ALS, ALSModel}
    import org.apache.spark.sql.{DataFrame, Dataset, SparkSession}
    import org.apache.spark.sql.functions._
    
    
    var base = "file:///home/bigdata/"
    val rawUserArtistData = spark.read.textFile(base + "user_artist_data.txt")
    val rawArtistData = spark.read.textFile(base + "artist_data.txt")
    val rawArtistAlias = spark.read.textFile(base + "artist_alias.txt")
    
    
    
    
      import spark.implicits._
    
      def preparation(
          rawUserArtistData: Dataset[String],
          rawArtistData: Dataset[String],
          rawArtistAlias: Dataset[String]): Unit = {
    
        rawUserArtistData.take(5).foreach(println)
    
        val userArtistDF = rawUserArtistData.map { line =>
          val Array(user, artist, _*) = line.split(' ')
          (user.toInt, artist.toInt)
        }.toDF("user", "artist")
    
        userArtistDF.agg(min("user"), max("user"), min("artist"), max("artist")).show()
    
        val artistByID = buildArtistByID(rawArtistData)
        val artistAlias = buildArtistAlias(rawArtistAlias)
    
        val (badID, goodID) = artistAlias.head
        artistByID.filter($"id" isin (badID, goodID)).show()
      }
    
      def model(
          rawUserArtistData: Dataset[String],
          rawArtistData: Dataset[String],
          rawArtistAlias: Dataset[String]): Unit = {
    
        val bArtistAlias = spark.sparkContext.broadcast(buildArtistAlias(rawArtistAlias))
    
        val trainData = buildCounts(rawUserArtistData, bArtistAlias).cache()
    
        val model = new ALS().
          setSeed(Random.nextLong()).
          setImplicitPrefs(true).
          setRank(10).
          setRegParam(0.01).
          setAlpha(1.0).
          setMaxIter(5).
          setUserCol("user").
          setItemCol("artist").
          setRatingCol("count").
          setPredictionCol("prediction").
          fit(trainData)
    
        trainData.unpersist()
    
        model.userFactors.select("features").show(truncate = false)
    
        val userID = 2093760
    
        val existingArtistIDs = trainData.
          filter($"user" === userID).
          select("artist").as[Int].collect()
    
        val artistByID = buildArtistByID(rawArtistData)
    
        artistByID.filter($"id" isin (existingArtistIDs:_*)).show()
    
        val topRecommendations = makeRecommendations(model, userID, 5)
        topRecommendations.show()
    
        val recommendedArtistIDs = topRecommendations.select("artist").as[Int].collect()
    
        artistByID.filter($"id" isin (recommendedArtistIDs:_*)).show()
    
        model.userFactors.unpersist()
        model.itemFactors.unpersist()
      }
    
      def evaluate(
          rawUserArtistData: Dataset[String],
          rawArtistAlias: Dataset[String]): Unit = {
    
        val bArtistAlias = spark.sparkContext.broadcast(buildArtistAlias(rawArtistAlias))
    
        val allData = buildCounts(rawUserArtistData, bArtistAlias)
        val Array(trainData, cvData) = allData.randomSplit(Array(0.9, 0.1))
        trainData.cache()
        cvData.cache()
    
        val allArtistIDs = allData.select("artist").as[Int].distinct().collect()
        val bAllArtistIDs = spark.sparkContext.broadcast(allArtistIDs)
    
        val mostListenedAUC = areaUnderCurve(cvData, bAllArtistIDs, predictMostListened(trainData))
        println(mostListenedAUC)
    
        val evaluations =
          for (rank     <- Seq(5,  30);
               regParam <- Seq(1.0, 0.0001);
               alpha    <- Seq(1.0, 40.0))
          yield {
            val model = new ALS().
              setSeed(Random.nextLong()).
              setImplicitPrefs(true).
              setRank(rank).setRegParam(regParam).
              setAlpha(alpha).setMaxIter(20).
              setUserCol("user").setItemCol("artist").
              setRatingCol("count").setPredictionCol("prediction").
              fit(trainData)
    
            val auc = areaUnderCurve(cvData, bAllArtistIDs, model.transform)
    
            model.userFactors.unpersist()
            model.itemFactors.unpersist()
    
            (auc, (rank, regParam, alpha))
          }
    
        evaluations.sorted.reverse.foreach(println)
    
        trainData.unpersist()
        cvData.unpersist()
      }
    
      def recommend(
          rawUserArtistData: Dataset[String],
          rawArtistData: Dataset[String],
          rawArtistAlias: Dataset[String]): Unit = {
    
        val bArtistAlias = spark.sparkContext.broadcast(buildArtistAlias(rawArtistAlias))
        val allData = buildCounts(rawUserArtistData, bArtistAlias).cache()
        val model = new ALS().
          setSeed(Random.nextLong()).
          setImplicitPrefs(true).
          setRank(10).setRegParam(1.0).setAlpha(40.0).setMaxIter(20).
          setUserCol("user").setItemCol("artist").
          setRatingCol("count").setPredictionCol("prediction").
          fit(allData)
        allData.unpersist()
    
        val userID = 2093760
        val topRecommendations = makeRecommendations(model, userID, 5)
    
        val recommendedArtistIDs = topRecommendations.select("artist").as[Int].collect()
        val artistByID = buildArtistByID(rawArtistData)
        artistByID.join(spark.createDataset(recommendedArtistIDs).toDF("id"), "id").
          select("name").show()
    
        model.userFactors.unpersist()
        model.itemFactors.unpersist()
      }
    
      def buildArtistByID(rawArtistData: Dataset[String]): DataFrame = {
        rawArtistData.flatMap { line =>
          val (id, name) = line.span(_ != '\t')
          if (name.isEmpty) {
            None
          } else {
            try {
              Some((id.toInt, name.trim))
            } catch {
              case _: NumberFormatException => None
            }
          }
        }.toDF("id", "name")
      }
    
      def buildArtistAlias(rawArtistAlias: Dataset[String]): Map[Int,Int] = {
        rawArtistAlias.flatMap { line =>
          val Array(artist, alias) = line.split('\t')
          if (artist.isEmpty) {
            None
          } else {
            Some((artist.toInt, alias.toInt))
          }
        }.collect().toMap
      }
    
      def buildCounts(
          rawUserArtistData: Dataset[String],
          bArtistAlias: Broadcast[Map[Int,Int]]): DataFrame = {
        rawUserArtistData.map { line =>
          val Array(userID, artistID, count) = line.split(' ').map(_.toInt)
          val finalArtistID = bArtistAlias.value.getOrElse(artistID, artistID)
          (userID, finalArtistID, count)
        }.toDF("user", "artist", "count")
      }
    
      def makeRecommendations(model: ALSModel, userID: Int, howMany: Int): DataFrame = {
        val toRecommend = model.itemFactors.
          select($"id".as("artist")).
          withColumn("user", lit(userID))
        model.transform(toRecommend).
          select("artist", "prediction").
          orderBy($"prediction".desc).
          limit(howMany)
      }
    
      def areaUnderCurve(
          positiveData: DataFrame,
          bAllArtistIDs: Broadcast[Array[Int]],
          predictFunction: (DataFrame => DataFrame)): Double = {
    
        // What this actually computes is AUC, per user. The result is actually something
        // that might be called "mean AUC".
    
        // Take held-out data as the "positive".
        // Make predictions for each of them, including a numeric score
        val positivePredictions = predictFunction(positiveData.select("user", "artist")).
          withColumnRenamed("prediction", "positivePrediction")
    
        // BinaryClassificationMetrics.areaUnderROC is not used here since there are really lots of
        // small AUC problems, and it would be inefficient, when a direct computation is available.
    
        // Create a set of "negative" products for each user. These are randomly chosen
        // from among all of the other artists, excluding those that are "positive" for the user.
        val negativeData = positiveData.select("user", "artist").as[(Int,Int)].
          groupByKey { case (user, _) => user }.
          flatMapGroups { case (userID, userIDAndPosArtistIDs) =>
            val random = new Random()
            val posItemIDSet = userIDAndPosArtistIDs.map { case (_, artist) => artist }.toSet
            val negative = new ArrayBuffer[Int]()
            val allArtistIDs = bAllArtistIDs.value
            var i = 0
            // Make at most one pass over all artists to avoid an infinite loop.
            // Also stop when number of negative equals positive set size
            while (i < allArtistIDs.length && negative.size < posItemIDSet.size) {
              val artistID = allArtistIDs(random.nextInt(allArtistIDs.length))
              // Only add new distinct IDs
              if (!posItemIDSet.contains(artistID)) {
                negative += artistID
              }
              i += 1
            }
            // Return the set with user ID added back
            negative.map(artistID => (userID, artistID))
          }.toDF("user", "artist")
    
        // Make predictions on the rest:
        val negativePredictions = predictFunction(negativeData).
          withColumnRenamed("prediction", "negativePrediction")
    
        // Join positive predictions to negative predictions by user, only.
        // This will result in a row for every possible pairing of positive and negative
        // predictions within each user.
        val joinedPredictions = positivePredictions.join(negativePredictions, "user").
          select("user", "positivePrediction", "negativePrediction").cache()
    
        // Count the number of pairs per user
        val allCounts = joinedPredictions.
          groupBy("user").agg(count(lit("1")).as("total")).
          select("user", "total")
        // Count the number of correctly ordered pairs per user
        val correctCounts = joinedPredictions.
          filter($"positivePrediction" > $"negativePrediction").
          groupBy("user").agg(count("user").as("correct")).
          select("user", "correct")
    
        // Combine these, compute their ratio, and average over all users
        val meanAUC = allCounts.join(correctCounts, Seq("user"), "left_outer").
          select($"user", (coalesce($"correct", lit(0)) / $"total").as("auc")).
          agg(mean("auc")).
          as[Double].first()
    
        joinedPredictions.unpersist()
    
        meanAUC
      }
    
      def predictMostListened(train: DataFrame)(allData: DataFrame): DataFrame = {
        val listenCounts = train.groupBy("artist").
          agg(sum("count").as("prediction")).
          select("artist", "prediction")
        allData.
          join(listenCounts, Seq("artist"), "left_outer").
          select("user", "artist", "prediction")
      }
    
      preparation(rawUserArtistData, rawArtistData, rawArtistAlias)
      model(rawUserArtistData, rawArtistData, rawArtistAlias)
      evaluate(rawUserArtistData, rawArtistAlias)
      recommend(rawUserArtistData, rawArtistData
    
    展开全文
  • 目录 前置部分 赛题传送门 赛题简介 ...itemCF 的文章推荐 ...给每个用户根据物品的协同过滤推荐文章 召回字典转换成df 生成提交文件 注意 前置部分 赛题传送门 https://tianchi.aliyun.com/compet.

    目录

     

    前置部分

    赛题传送门

    赛题简介

    数据概况

    结果提交

    评价方式理解

    赛题理解

    Baseline(itemcf_base)

    导包

    df节省内存函数

    读取采样或全量数据

    获取 用户 - 文章 - 点击时间字典

    获取点击最多的Topk个文章

    itemCF的物品相似度计算

    itemCF 的文章推荐

    给每个用户根据物品的协同过滤推荐文章

    召回字典转换成df

    生成提交文件

    注意


    前置部分

    赛题传送门

    https://tianchi.aliyun.com/competition/entrance/531842/introduction?spm=5176.12281973.1005.1.3dd52448IarE5N

    赛题简介

    场景:新闻推荐场景

    任务:要求我们根据用户历史浏览点击新闻文章的数据信息预测用户未来的点击行为, 即用户的最后一次点击的新闻文章。

    数据概况

    数据规模:30万users,300万次点击,36万多不同的新闻文章(每篇文章用embedding向量表示)

    训练集:20万用户的点击日志数据

    测试集A:5万用户的点击日志数据

    测试集B:5万用户的点击日志数据

    训练集是用来训练模型的,测试集A是用来调整和优化模型参数的,测试集B用来线上对模型进行评分的。

    数据字段表

     

    简单地说一下目前对字段的理解,

    • 点击时间戳:因为使用的是用户历史数据训练来完成预测任务,所以我们可以从两方面进行考虑。

      • 第一,日期越靠近“今天”的越能反映到用户最新的兴趣情况。应该可以用lstm或者attention机制进行加权。

      • 第二,停留在某篇文章的事件越长,大概率表明用户对该类型的新闻比较感兴趣,感觉这里可以构建一个文章阅读时间特征。

    • 点击环境:不太明确字段的意思,暂定认为是手机网络还是wifi.

    • 城市/地区:可以构造城市x地区的特征,或者直接以地区作为特征,看看某个用户是否会对某个地区的时事新闻进行关注。

    结果提交

    提交前请确保预测结果的格式与sample_submit.csv中的格式一致,以及提交文件后缀名为csv。其格式如下:

    user_id,article_1,article_2,article_3,article_4,article_5

    其中user_id为用户id, article_1,article_2,article_3,article_4,article_5为预测用户点击新闻文章Top5的article_id`依概率从高到低排序,例如:

     

    评价方式理解

    理解评价方式, 我们需要结合着最后的提交文件来看, 根据sample.submit.csv, 我们最后提交的格式是针对每个用户, 我们都会给出五篇文章的推荐结果,按照点击概率从前往后排序。 而真实的每个用户最后一次点击的文章只会有一篇的真实答案, 所以我们就看我们推荐的这五篇里面是否有命中真实答案的。比如对于user1来说, 我们的提交会是:

    user1, article1, article2, article3, article4, article5.

    评价指标的公式如下:

    假如article1就是真实的用户点击文章,也就是article1命中, 则s(user1,1)=1, s(user1,2-4)都是0, 如果article2是用户点击的文章, 则s(user,2)=1/2,s(user,1,3,4,5)都是0。也就是score(user)=命中第几条的倒数。如果都没中, 则score(user1)=0。 这个是合理的, 因为我们希望的就是命中的结果尽量靠前,而此时分数正好比较高。

    赛题理解

    本次任务与普通的结构化比赛不太一样,主要从两个方面进行考虑:

    • 任务上,这是预测最后一次点击的文章。而不是像之前是预测一个数回归问题或者数据属于哪一类的分类问题

    • 数据上,本次给的是已脱敏的点击日志数据,而不是特征+标签的数据。

    基于第二个方面,我们的想法是把该预测问题转成一个监督学习的问题(特征+标签),然后我们才能进行MLDL等建模预测。因此,又产生了如下几个问题

    • 如何转成一个监督学习问题呢?

      • 暂未解决

    • 转成一个什么样的监督学习问题呢?

      • 由于我们是预测用户最后一次点击的新闻文章,从36万篇文章中预测某一篇的话我们首先可能会想到这可能是一个多分类的问题(36万类里面选1), 但是如此庞大的分类问题, 我们做起来可能比较困难, 那么能不能转化一下? 既然是要预测最后一次点击的文章, 那么如果我们能预测出某个用户最后一次对于某一篇文章会进行点击的概率, 是不是就间接性的解决了这个问题呢?概率最大的那篇文章不就是用户最后一次可能点击的新闻文章吗? 这样就把原问题变成了一个点击率预测的问题(用户, 文章) --> 点击的概率(软分类), 而这个问题, 就是我们所熟悉的监督学习领域分类问题了.

    • 我们能利用的特征又有哪些呢?

      • 暂未解决

    • 又有哪些模型可以尝试呢?

      • 转换成点击率预估问题,我们首先可以使用LR模型,其次还有FM,wide&deep以及deepfm等模型。

    • 此次面对数万级别的文章推荐,我们又有哪些策略呢?

      • 暂未解决

    Baseline(itemcf_base)

    导包

    # import packages
    import time, math, os
    from tqdm import tqdm  //进度条
    import gc
    import pickle  //将对象序列化
    import random
    from datetime import datetime
    from operator import itemgetter
    import numpy as np
    import pandas as pd
    import warnings
    from collections import defaultdict  //初始化字典
    warnings.filterwarnings('ignore')
    data_path = './data_raw/'
    save_path = './tmp_results/'

     

    df节省内存函数

    # 节约内存的一个标配函数
    def reduce_mem(df):
        starttime = time.time()
        numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
        start_mem = df.memory_usage().sum() / 1024**2
        for col in df.columns:
            col_type = df[col].dtypes
            if col_type in numerics:
                c_min = df[col].min()
                c_max = df[col].max()
                if pd.isnull(c_min) or pd.isnull(c_max):
                    continue
                if str(col_type)[:3] == 'int':
                    if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
                        df[col] = df[col].astype(np.int8)
                    elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
                        df[col] = df[col].astype(np.int16)
                    elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
                        df[col] = df[col].astype(np.int32)
                    elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
                        df[col] = df[col].astype(np.int64)
                else:
                    if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
                        df[col] = df[col].astype(np.float16)
                    elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
                        df[col] = df[col].astype(np.float32)
                    else:
                        df[col] = df[col].astype(np.float64)
        end_mem = df.memory_usage().sum() / 1024**2
        print('-- Mem. usage decreased to {:5.2f} Mb ({:.1f}% reduction),time spend:{:2.2f} min'.format(end_mem,
                                                                                                               100*(start_mem-end_mem)/start_mem,
                                                                                                               (time.time()-starttime)/60))
        return df

     

    读取采样或全量数据

    # debug模式:从训练集中划出一部分数据来调试代码
    def get_all_click_sample(data_path, sample_nums=10000):
        """
            训练集中采样一部分数据调试
            data_path: 原数据的存储路径
            sample_nums: 采样数目(这里由于机器的内存限制,可以采样用户做)
        """
        all_click = pd.read_csv(data_path + 'train_click_log.csv')
        all_user_ids = all_click.user_id.unique()
    ​
        sample_user_ids = np.random.choice(all_user_ids, size=sample_nums, replace=False) 
        all_click = all_click[all_click['user_id'].isin(sample_user_ids)]
        
        all_click = all_click.drop_duplicates((['user_id', 'click_article_id', 'click_timestamp']))
        return all_click
    ​
    # 读取点击数据,这里分成线上和线下,如果是为了获取线上提交结果应该讲测试集中的点击数据合并到总的数据中
    # 如果是为了线下验证模型的有效性或者特征的有效性,可以只使用训练集
    def get_all_click_df(data_path='./data_raw/', offline=True):
        if offline:
            all_click = pd.read_csv(data_path + 'train_click_log.csv')
        else:
            trn_click = pd.read_csv(data_path + 'train_click_log.csv')
            tst_click = pd.read_csv(data_path + 'testA_click_log.csv')
    ​
            all_click = trn_click.append(tst_click)
        
        all_click = all_click.drop_duplicates((['user_id', 'click_article_id', 'click_timestamp']))
        return all_click
    # 全量训练集
    all_click_df = get_all_click_df(offline=False)

     

    获取 用户 - 文章 - 点击时间字典

    # 根据点击时间获取用户的点击文章序列   {user1: {item1: time1, item2: time2..}...}
    def get_user_item_time(click_df):
        
        click_df = click_df.sort_values('click_timestamp')
        
        def make_item_time_pair(df):
            return list(zip(df['click_article_id'], df['click_timestamp']))
        
        user_item_time_df = click_df.groupby('user_id')['click_article_id', 'click_timestamp'].apply(lambda x: make_item_time_pair(x))\
                                                                .reset_index().rename(columns={0: 'item_time_list'})
        user_item_time_dict = dict(zip(user_item_time_df['user_id'], user_item_time_df['item_time_list']))
        
        return user_item_time_dict

     

    获取点击最多的Topk个文章

    # 获取近期点击最多的文章
    def get_item_topk_click(click_df, k):
        topk_click = click_df['click_article_id'].value_counts().index[:k]
        return topk_click

     

    itemCF的物品相似度计算

    def itemcf_sim(df):
        """
            文章与文章之间的相似性矩阵计算
            :param df: 数据表
            :item_created_time_dict:  文章创建时间的字典
            return : 文章与文章的相似性矩阵
            思路: 基于物品的协同过滤(详细请参考上一期推荐系统基础的组队学习), 在多路召回部分会加上关联规则的召回策略
        """
        
        user_item_time_dict = get_user_item_time(df)
        
        # 计算物品相似度
        i2i_sim = {}
        item_cnt = defaultdict(int)
        for user, item_time_list in tqdm(user_item_time_dict.items()):
            # 在基于商品的协同过滤优化的时候可以考虑时间因素
            for i, i_click_time in item_time_list:
                item_cnt[i] += 1
                i2i_sim.setdefault(i, {})
                for j, j_click_time in item_time_list:
                    if(i == j):
                        continue
                    i2i_sim[i].setdefault(j, 0)
                    
                    i2i_sim[i][j] += 1 / math.log(len(item_time_list) + 1)
                    
        i2i_sim_ = i2i_sim.copy()
        for i, related_items in i2i_sim.items():
            for j, wij in related_items.items():
                i2i_sim_[i][j] = wij / math.sqrt(item_cnt[i] * item_cnt[j])
        
        # 将得到的相似性矩阵保存到本地
        pickle.dump(i2i_sim_, open(save_path + 'itemcf_i2i_sim.pkl', 'wb'))
        
        return i2i_sim_
    i2i_sim = itemcf_sim(all_click_df)

    i2i是一个双层字典。{i1:{j1:w11,j2:w12,j3:w13},i2:{j1:w21},{j2,w22},{j3:w23}}

    itemCF 的文章推荐

    # 基于商品的召回i2i
    def item_based_recommend(user_id, user_item_time_dict, i2i_sim, sim_item_topk, recall_item_num, item_topk_click):
        """
            基于文章协同过滤的召回
            :param user_id: 用户id
            :param user_item_time_dict: 字典, 根据点击时间获取用户的点击文章序列   {user1: {item1: time1, item2: time2..}...}
            :param i2i_sim: 字典,文章相似性矩阵
            :param sim_item_topk: 整数, 选择与当前文章最相似的前k篇文章
            :param recall_item_num: 整数, 最后的召回文章数量
            :param item_topk_click: 列表,点击次数最多的文章列表,用户召回补全        
            return: 召回的文章列表 {item1:score1, item2: score2...}
            注意: 基于物品的协同过滤(详细请参考上一期推荐系统基础的组队学习), 在多路召回部分会加上关联规则的召回策略
        """
        
        # 获取用户历史交互的文章
        user_hist_items = user_item_time_dict[user_id]
        
        item_rank = {}
        for loc, (i, click_time) in enumerate(user_hist_items):
            for j, wij in sorted(i2i_sim[i].items(), key=lambda x: x[1], reverse=True)[:sim_item_topk]:
                if j in user_hist_items:
                    continue
                    
                item_rank.setdefault(j, 0)
                item_rank[j] +=  wij
        
        # 不足10个,用热门商品补全
        if len(item_rank) < recall_item_num:
            for i, item in enumerate(item_topk_click):
                if item in item_rank.items(): # 填充的item应该不在原来的列表中
                    continue
                item_rank[item] = - i - 100 # 随便给个负数就行
                if len(item_rank) == recall_item_num:
                    break
        
        item_rank = sorted(item_rank.items(), key=lambda x: x[1], reverse=True)[:recall_item_num]
            
        return item_rank

     

    给每个用户根据物品的协同过滤推荐文章

    # 定义
    user_recall_items_dict = collections.defaultdict(dict)
    ​
    # 获取 用户 - 文章 - 点击时间的字典
    user_item_time_dict = get_user_item_time(all_click_df)
    ​
    # 去取文章相似度
    i2i_sim = pickle.load(open(save_path + 'itemcf_i2i_sim.pkl', 'rb'))
    ​
    # 相似文章的数量
    sim_item_topk = 10
    ​
    # 召回文章数量
    recall_item_num = 10
    ​
    # 用户热度补全(还是属于召回部分,召回就是一个粗排)
    item_topk_click = get_item_topk_click(all_click_df, k=50)  
    ​
    for user in tqdm(all_click_df['user_id'].unique()):
        user_recall_items_dict[user] = item_based_recommend(user, user_item_time_dict, i2i_sim, 
                                                            sim_item_topk, recall_item_num, item_topk_click)

     

    召回字典转换成df

    # 将字典的形式转换成df
    user_item_score_list = []
    ​
    for user, items in tqdm(user_recall_items_dict.items()):
        for item, score in items:
            user_item_score_list.append([user, item, score])
    ​
    recall_df = pd.DataFrame(user_item_score_list, columns=['user_id', 'click_article_id', 'pred_score'])

     

    生成提交文件

    # 生成提交文件
    def submit(recall_df, topk=5, model_name=None):
        recall_df = recall_df.sort_values(by=['user_id', 'pred_score'])
        recall_df['rank'] = recall_df.groupby(['user_id'])['pred_score'].rank(ascending=False, method='first')
        
        # 判断是不是每个用户都有5篇文章及以上
        tmp = recall_df.groupby('user_id').apply(lambda x: x['rank'].max())
        assert tmp.min() >= topk
        
        del recall_df['pred_score']
        submit = recall_df[recall_df['rank'] <= topk].set_index(['user_id', 'rank']).unstack(-1).reset_index()
        
        submit.columns = [int(col) if isinstance(col, int) else col for col in submit.columns.droplevel(0)]
        # 按照提交格式定义列名
        submit = submit.rename(columns={'': 'user_id', 1: 'article_1', 2: 'article_2', 
                                                      3: 'article_3', 4: 'article_4', 5: 'article_5'})
        
        save_name = save_path + model_name + '_' + datetime.today().strftime('%m-%d') + '.csv'
        submit.to_csv(save_name, index=False, header=True)
    # 获取测试集
    tst_click = pd.read_csv(data_path + 'testA_click_log.csv')
    tst_users = tst_click['user_id'].unique()
    ​
    # 从所有的召回数据中将测试集中的用户选出来
    tst_recall = recall_df[recall_df['user_id'].isin(tst_users)]
    ​
    # 生成提交文件
    submit(tst_recall, topk=5, model_name='itemcf_baseline')

    注意

    • 由于训练使用了全量数据集(即训练集和测试集,所以可以直接用id从召回数据中把测试集的用户选出来,作为提交结果。)

    • baseline只对召回结果(并且是只有基于协同过滤召回的结果进行排序),召回的方法是先用itemCF方法(即物品相似度)进行补全,数量不够的,再用用户热度(即点击文章次数的top)进行补全。

    • 因此,我们需要在baseline的基础上对召回结果进行改进,或者是精排操作。

    展开全文

空空如也

1 2 3 4 5 ... 20
收藏数 2,387
精华内容 954
关键字:

推荐系统实战