diff --git a/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.2 协同过滤-UserCF.md b/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.2 协同过滤-UserCF.md new file mode 100644 index 00000000..2a34353e --- /dev/null +++ b/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.2 协同过滤-UserCF.md @@ -0,0 +1,345 @@ +# 协同过滤算法 + +## 基本思想 + +协同过滤(Collaborative Filtering)推荐算法是最经典、最常用的推荐算法。基本思想是: + ++ 根据用户之前的喜好以及其他兴趣相近的用户的选择来给用户推荐物品。 + + + 基于对用户历史行为数据的挖掘发现用户的喜好偏向, 并预测用户可能喜好的产品进行推荐。 + + 一般是仅仅基于用户的行为数据(评价、购买、下载等), 而不依赖于项的任何附加信息(物品自身特征)或者用户的任何附加信息(年龄, 性别等)。 ++ 目前应用比较广泛的协同过滤算法是基于邻域的方法,主要有: + + 基于用户的协同过滤算法(UserCF):给用户推荐和他兴趣相似的其他用户喜欢的产品。 + + 基于物品的协同过滤算法(ItemCF):给用户推荐和他之前喜欢的物品相似的物品。 + +不管是 UserCF 还是 ItemCF 算法, 重点是计算用户之间(或物品之间)的相似度。 + +## 相似性度量方法 + +1. **杰卡德(Jaccard)相似系数** + + `Jaccard` 系数是衡量两个集合的相似度一种指标,计算公式如下: + $$ + sim_{uv}=\frac{|N(u) \cap N(v)|}{|N(u)| \cup|N(v)|} + $$ + + + 其中 $N(u)$,$N(v)$ 分别表示用户 $u$ 和用户 $v$ 交互物品的集合。 + + + 对于用户 $u$ 和 $v$ ,该公式反映了两个交互物品交集的数量占这两个用户交互物品并集的数量的比例。 + + 由于杰卡德相似系数一般无法反映具体用户的评分喜好信息,所以常用来评估用户是否会对某物品进行打分, 而不是预估用户会对某物品打多少分。 + +2. **余弦相似度** + 余弦相似度衡量了两个向量的夹角,夹角越小越相似。余弦相似度的计算如下,其与杰卡德(Jaccard)相似系数只是在分母上存在差异: + $$ + sim_{uv}=\frac{|N(u) \cap N(v)|}{\sqrt{|N(u)|\cdot|N(v)|}} + $$ + 从向量的角度进行描述,令矩阵 $A$ 为用户-物品交互矩阵,矩阵的行表示用户,列表示物品。 + + + 设用户和物品数量分别为 $m,n$,交互矩阵$A$就是一个 $m$ 行 $n$ 列的矩阵。 + + + 矩阵中的元素均为 $0/1$。若用户 $i$ 对物品 $j$ 存在交互,那么 $A_{i,j}=1$,否则为 $0$ 。 + + + 那么,用户之间的相似度可以表示为: + $$ + sim_{uv} = cos(u,v) =\frac{u\cdot v}{|u|\cdot |v|} + $$ + + + 向量 $u,v$ 在形式都是 one-hot 类型,$u\cdot v$ 表示向量点积。 + + 上述用户-物品交互矩阵在现实中是十分稀疏的,为了节省内存,交互矩阵会采用字典进行存储。在 `sklearn` 中,余弦相似度的实现: + + ```python + from sklearn.metrics.pairwise import cosine_similarity + + i = [1, 0, 0, 0] + j = [1, 0, 1, 0] + cosine_similarity([i, j]) + ``` + +3. **皮尔逊相关系数** + + 在用户之间的余弦相似度计算时,将用户向量的内积展开为各元素乘积和: + $$ + sim_{uv} = \frac{\sum_i r_{ui}*r_{vi}}{\sqrt{\sum_i r_{ui}^2}\sqrt{\sum_i r_{vi}^2}} + $$ + + 其中,$r_{ui},r_{vi}$ 分别表示用户 $u$ 和用户 $v$ 对物品 $i$ 是否有交互(或具体评分值)。 + + 皮尔逊相关系数与余弦相似度的计算公式非常的类似,如下: + $$ + sim(u,v)=\frac{\sum_{i\in I}(r_{ui}-\bar r_u)(r_{vi}-\bar r_v)}{\sqrt{\sum_{i\in I }(r_{ui}-\bar r_u)^2}\sqrt{\sum_{i\in I }(r_{vi}-\bar r_v)^2}} + $$ + + 其中,$r_{ui},r_{vi}$ 分别表示用户 $u$ 和用户 $v$ 对物品 $i$ 是否有交互(或具体评分值); + + $\bar r_u, \bar r_v$ 分别表示用户 $u$ 和用户 $v$ 交互的所有物品交互数量或者评分的平均值; + + 相较于余弦相似度,皮尔逊相关系数通过使用用户的平均分对各独立评分进行修正,减小了用户评分偏置的影响。在`scipy`中,皮尔逊相关系数的实现: + + ```python + from scipy.stats import pearsonr + + i = [1, 0, 0, 0] + j = [1, 0.5, 0.5, 0] + pearsonr(i, j) + ``` + +**适用场景** + ++ $Jaccard$ 相似度表示两个集合的交集元素个数在并集中所占的比例 ,所以适用于隐式反馈数据(0-1)。 ++ 余弦相似度在度量文本相似度、用户相似度、物品相似度的时候都较为常用。 ++ 皮尔逊相关度,实际上也是一种余弦相似度。不过先对向量做了中心化,范围在 $-1$ 到 $1$。 + + 相关度量的是两个变量的变化趋势是否一致,两个随机变量是不是同增同减。 + + 不适合用作计算布尔值向量(0-1)之间相关度。 + +# 基于用户的协同过滤 + +## 基本思想 + +基于用户的协同过滤(UserCF): + ++ 例如,我们要对用户 $A$ 进行物品推荐,可以先找到和他有相似兴趣的其他用户。 ++ 然后,将共同兴趣用户喜欢的,但用户 $A$ 未交互过的物品推荐给 $A$。 + +image-20210629232540289 + +## 计算过程 + +以下图为例,给用户推荐物品的过程可以形象化为一个猜测用户对物品进行打分的任务,表格里面是5个用户对于5件物品的一个打分情况,就可以理解为用户对物品的喜欢程度。 + +![image-20210629232622758](http://ryluo.oss-cn-chengdu.aliyuncs.com/图片image-20210629232622758.png) + +UserCF算法的两个步骤: + ++ 首先,根据前面的这些打分情况(或者说已有的用户向量)计算一下 Alice 和用户1, 2, 3, 4的相似程度, 找出与 Alice 最相似的 n 个用户。 + ++ 根据这 n 个用户对物品 5 的评分情况和与 Alice 的相似程度会猜测出 Alice 对物品5的评分。如果评分比较高的话, 就把物品5推荐给用户 Alice, 否则不推荐。 + +**具体过程:** + +1. 计算用户之间的相似度 + + + 根据 1.2 节的几种方法, 我们可以计算出各用户之间的相似程度。对于用户 Alice,选取出与其最相近的 $N$ 个用户。 + +2. 计算用户对新物品的评分预测 + + + 常用的方式之一:利用目标用户与相似用户之间的相似度以及相似用户对物品的评分,来预测目标用户对候选物品的评分估计: + $$ + R_{\mathrm{u}, \mathrm{p}}=\frac{\sum_{\mathrm{s} \in S}\left(w_{\mathrm{u}, \mathrm{s}} \cdot R_{\mathrm{s}, \mathrm{p}}\right)}{\sum_{\mathrm{s} \in S} w_{\mathrm{u}, \mathrm{s}}} + $$ + + + 其中,权重 $w_{u,s}$ 是用户 $u$ 和用户 $s$ 的相似度, $R_{s,p}$ 是用户 $s$ 对物品 $p$ 的评分。 + + + 另一种方式:考虑到用户评分的偏置,即有的用户喜欢打高分, 有的用户喜欢打低分的情况。公式如下: + $$ + R_{\mathrm{u}, \mathrm{p}}=\bar{R}_{u} + \frac{\sum_{\mathrm{s} \in S}\left(w_{\mathrm{u}, \mathrm{s}} \cdot \left(R_{s, p}-\bar{R}_{s}\right)\right)}{\sum_{\mathrm{s} \in S} w_{\mathrm{u}, \mathrm{s}}} + $$ + + + 其中,$\bar{R}_{s}$ 表示用户 $s$ 对物品的历史平均评分。 + +3. 对用户进行物品推荐 + + + 在获得用户 $u$ 对不同物品的评价预测后, 最终的推荐列表根据预测评分进行排序得到。 + +**手动计算:** + +根据上面的问题, 下面手动计算 Alice 对物品 5 的得分: + + +1. 计算 Alice 与其他用户的相似度(基于皮尔逊相关系数) + + + 手动计算 Alice 与用户 1 之间的相似度: + + >用户向量 $\text {Alice}:(5,3,4,4) , \text{user1}:(3,1,2,3) , \text {user2}:( 4,3,4,3) , \text {user3}:(3,3,1,5) , \text {user4}:(1,5,5,2) $ + > + >+ 计算Alice与user1的余弦相似性: + >$$ + >\operatorname{sim}(\text { Alice, user1 })=\cos (\text { Alice, user } 1)=\frac{15+3+8+12}{\operatorname{sqrt}(25+9+16+16) * \operatorname{sqrt}(9+1+4+9)}=0.975 + >$$ + > + >+ 计算Alice与user1皮尔逊相关系数: + > + $Alice\_ave =4 \quad user1\_ave =2.25 $ + > + 向量减去均值: $\text {Alice}:(1,-1, 0,0) \quad \text { user1 }:(0.75,-1.25,-0.25,0.75)$ + > + >+ 计算这俩新向量的余弦相似度和上面计算过程一致, 结果是 0.852 。 + > + + + 基于 sklearn 计算所有用户之间的皮尔逊相关系数。可以看出,与 Alice 相似度最高的用户为用户1和用户2。 + + 图片 + +2. **根据相似度用户计算 Alice对物品5的最终得分** + 用户1对物品5的评分是3, 用户2对物品5的打分是5, 那么根据上面的计算公式, 可以计算出 Alice 对物品5的最终得分是 + $$ + P_{Alice, 物品5}=\bar{R}_{Alice}+\frac{\sum_{k=1}^{2}\left(w_{Alice,user k}\left(R_{userk, 物品5}-\bar{R}_{userk}\right)\right)}{\sum_{k=1}^{2} w_{Alice, userk}}=4+\frac{0.85*(3-2.4)+0.7*(5-3.8)}{0.85+0.7}=4.87 + $$ + + + 同样方式,可以计算用户 Alice 对其他物品的评分预测。 + +3. **根据用户评分对用户进行推荐** + + + 根据 Alice 的打分对物品排个序从大到小:$$物品1>物品5>物品3=物品4>物品2$$。 + + 如果要向 Alice 推荐2款产品的话, 我们就可以推荐物品 1 和物品 5 给 Alice。 + + 至此, 基于用户的协同过滤算法原理介绍完毕。 + +## UserCF编程实现 + +1. 建立实验使用的数据表: + + ```python + import numpy as np + import pandas as pd + + + def loadData(): + users = {'Alice': {'A': 5, 'B': 3, 'C': 4, 'D': 4}, + 'user1': {'A': 3, 'B': 1, 'C': 2, 'D': 3, 'E': 3}, + 'user2': {'A': 4, 'B': 3, 'C': 4, 'D': 3, 'E': 5}, + 'user3': {'A': 3, 'B': 3, 'C': 1, 'D': 5, 'E': 4}, + 'user4': {'A': 1, 'B': 5, 'C': 5, 'D': 2, 'E': 1} + } + return users + ``` + + + 这里使用字典来建立用户-物品的交互表。 + + 字典`users`的键表示不同用户的名字,值为一个评分字典,评分字典的键值对表示某物品被当前用户的评分。 + + 由于现实场景中,用户对物品的评分比较稀疏。如果直接使用矩阵进行存储,会存在大量空缺值,故此处使用了字典。 + +2. 计算用户相似性矩阵 + + + 由于训练数据中共包含 5 个用户,所以这里的用户相似度矩阵的维度也为 $5 \times 5$。 + + ```python + user_data = loadData() + similarity_matrix = pd.DataFrame( + np.identity(len(user_data)), + index=user_data.keys(), + columns=user_data.keys(), + ) + + # 遍历每条用户-物品评分数据 + for u1, items1 in user_data.items(): + for u2, items2 in user_data.items(): + if u1 == u2: + continue + vec1, vec2 = [], [] + for item, rating1 in items1.items(): + rating2 = items2.get(item, -1) + if rating2 == -1: + continue + vec1.append(rating1) + vec2.append(rating2) + # 计算不同用户之间的皮尔逊相关系数 + similarity_matrix[u1][u2] = np.corrcoef(vec1, vec2)[0][1] + + print(similarity_matrix) + ``` + + ``` + 1 2 3 4 5 + 1 1.000000 0.852803 0.707107 0.000000 -0.792118 + 2 0.852803 1.000000 0.467707 0.489956 -0.900149 + 3 0.707107 0.467707 1.000000 -0.161165 -0.466569 + 4 0.000000 0.489956 -0.161165 1.000000 -0.641503 + 5 -0.792118 -0.900149 -0.466569 -0.641503 1.000000 + ``` + +3. 计算与 Alice 最相似的 `num` 个用户 + + ```python + target_user = ' Alice ' + num = 2 + # 由于最相似的用户为自己,去除本身 + sim_users = similarity_matrix[target_user].sort_values(ascending=False)[1:num+1].index.tolist() + print(f'与用户{target_user}最相似的{num}个用户为:{sim_users}') + ``` + + ``` + 与用户 Alice 最相似的2个用户为:['user1', 'user2'] + ``` + +4. 预测用户 Alice 对物品 `E` 的评分 + + ```python + weighted_scores = 0. + corr_values_sum = 0. + + target_item = 'E' + # 基于皮尔逊相关系数预测用户评分 + for user in sim_users: + corr_value = similarity_matrix[target_user][user] + user_mean_rating = np.mean(list(user_data[user].values())) + + weighted_scores += corr_value * (user_data[user][target_item] - user_mean_rating) + corr_values_sum += corr_value + + target_user_mean_rating = np.mean(list(user_data[target_user].values())) + target_item_pred = target_user_mean_rating + weighted_scores / corr_values_sum + print(f'用户{target_user}对物品{target_item}的预测评分为:{target_item_pred}') + ``` + + ``` + 用户 Alice 对物品E的预测评分为:4.871979899370592 + ``` + +## UserCF优缺点 + +User-based算法存在两个重大问题: + + +1. 数据稀疏性 + + 一个大型的电子商务推荐系统一般有非常多的物品,用户可能买的其中不到1%的物品,不同用户之间买的物品重叠性较低,导致算法无法找到一个用户的邻居,即偏好相似的用户。 + + 这导致UserCF不适用于那些正反馈获取较困难的应用场景(如酒店预订, 大件物品购买等低频应用)。 + +1. 算法扩展性 + + 基于用户的协同过滤需要维护用户相似度矩阵以便快速的找出 $TopN$ 相似用户, 该矩阵的存储开销非常大,存储空间随着用户数量的增加而增加。 + + 故不适合用户数据量大的情况使用。 + +由于UserCF技术上的两点缺陷, 导致很多电商平台并没有采用这种算法, 而是采用了ItemCF算法实现最初的推荐系统。 + + + +# 算法评估 + +由于UserCF和ItemCF结果评估部分是共性知识点, 所以在这里统一标识。 + +## 召回率 + +对用户 $u$ 推荐 $N$ 个物品记为 $R(u)$, 令用户 $u$ 在测试集上喜欢的物品集合为$T(u)$, 那么召回率定义为: +$$ +\operatorname{Recall}=\frac{\sum_{u}|R(u) \cap T(u)|}{\sum_{u}|T(u)|} +$$ ++ 含义:在模型召回预测的物品中,预测准确的物品占用户实际喜欢的物品的比例。 + +## 精确率 +精确率定义为: +$$ +\operatorname{Precision}=\frac{\sum_{u} \mid R(u) \cap T(u)|}{\sum_{u}|R(u)|} +$$ ++ 含义:推荐的物品中,对用户准确推荐的物品占总物品的比例。 ++ 如要确保召回率高,一般是推荐更多的物品,期望推荐的物品中会涵盖用户喜爱的物品。而实际中,推荐的物品中用户实际喜爱的物品占少数,推荐的精确率就会很低。故同时要确保高召回率和精确率往往是矛盾的,所以实际中需要在二者之间进行权衡。 + +## 覆盖率 +覆盖率反映了推荐算法发掘长尾的能力, 覆盖率越高, 说明推荐算法越能将长尾中的物品推荐给用户。 +$$ +\text { Coverage }=\frac{\left|\bigcup_{u \in U} R(u)\right|}{|I|} +$$ + ++ 含义:推荐系统能够推荐出来的物品占总物品集合的比例。 + + 其中 $|I|$ 表示所有物品的个数; + + 系统的用户集合为$U$; + + 推荐系统给每个用户推荐一个长度为 $N$ 的物品列表$R(u)$. + ++ 覆盖率表示最终的推荐列表中包含多大比例的物品。如果所有物品都被给推荐给至少一个用户, 那么覆盖率是100%。 + +## 新颖度 +用推荐列表中物品的平均流行度度量推荐结果的新颖度。 如果推荐出的物品都很热门, 说明推荐的新颖度较低。 由于物品的流行度分布呈长尾分布, 所以为了流行度的平均值更加稳定, 在计算平均流行度时对每个物品的流行度取对数。 + +- O’scar Celma 在博士论文 "[Music Recommendation and Discovery in the Long Tail](http://mtg.upf.edu/static/media/PhD_ocelma.pdf) " 中研究了新颖度的评测。 + + + +# 参考资料 + +* [基于用户的协同过滤来构建推荐系统:https://mp.weixin.qq.com/s/ZtnaQrVIpVOPJpqMdLWOcw](https://mp.weixin.qq.com/s/ZtnaQrVIpVOPJpqMdLWOcw) +* [协同过滤算法概述:https://chenk.tech/posts/8ad63d9d.html](https://chenk.tech/posts/8ad63d9d.html) +* B站黑马推荐系统实战课程 diff --git a/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.2 协同过滤.md b/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.2 协同过滤.md deleted file mode 100644 index 53c5c78c..00000000 --- a/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.2 协同过滤.md +++ /dev/null @@ -1,502 +0,0 @@ -### 1. 协同过滤算法 - -协同过滤(Collaborative Filtering)推荐算法是最经典、最常用的推荐算法。 - -所谓协同过滤, 基本思想是**根据用户之前的喜好以及其他兴趣相近的用户的选择来给用户推荐物品**(基于对用户历史行为数据的挖掘发现用户的喜好偏向, 并预测用户可能喜好的产品进行推荐),**一般是仅仅基于用户的行为数据(评价、购买、下载等), 而不依赖于项的任何附加信息(物品自身特征)或者用户的任何附加信息(年龄, 性别等)**。目前应用比较广泛的协同过滤算法是基于邻域的方法, 而这种方法主要有下面两种算法: - - -* **基于用户的协同过滤算法(UserCF)**: 给用户推荐和他兴趣相似的其他用户喜欢的产品 -* **基于物品的协同过滤算法(ItemCF)**: 给用户推荐和他之前喜欢的物品相似的物品 - -不管是UserCF还是ItemCF算法, 非常重要的步骤之一就是计算用户和用户或者物品和物品之间的相似度, 所以下面先整理常用的相似性度量方法, 然后再对每个算法的具体细节进行展开。 - -
- -### 2. 相似性度量方法 - -1. **杰卡德(Jaccard)相似系数** - 这个是衡量两个集合的相似度一种指标。两个用户$u$和$v$交互商品交集的数量占这两个用户交互商品并集的数量的比例,称为两个集合的杰卡德相似系数,用符号$sim_{uv}$表示,其中$N(u),N(v)$分别表示用户$u$和用户$v$交互商品的集合。 - $$ - sim_{uv}=\frac{|N(u) \cap N(v)|}{\sqrt{|N(u)| \cup|N(v)|}} - $$ - 由于杰卡德相似系数一般无法反映具体用户的评分喜好信息, 所以常用来评估用户是否会对某商品进行打分, 而不是预估用户会对某商品打多少分。 - -
- -2. **余弦相似度** - 余弦相似度衡量了两个向量的夹角,夹角越小越相似。首先从集合的角度描述余弦相似度,相比于Jaccard公式来说就是分母有差异,不是两个用户交互商品的并集的数量,而是两个用户分别交互的商品数量的乘积,公式如下: - $$ - sim_{uv}=\frac{|N(u) \cap N(v)|}{\sqrt{|N(u)|\cdot|N(v)|}} - $$ - 从向量的角度进行描述,令矩阵$A$为用户-商品交互矩阵(因为是TopN推荐并不需要用户对物品的评分,只需要知道用户对商品是否有交互就行),即矩阵的每一行表示一个用户对所有商品的交互情况,有交互的商品值为1没有交互的商品值为0,矩阵的列表示所有商品。若用户和商品数量分别为$m,n$的话,交互矩阵$A$就是一个$m$行$n$列的矩阵。此时用户的相似度可以表示为(其中$u\cdot v$指的是向量点积): - $$ - sim_{uv} = cos(u,v) =\frac{u\cdot v}{|u|\cdot |v|} - $$ - 上述用户-商品交互矩阵在现实情况下是非常的稀疏了,为了避免存储这么大的稀疏矩阵,在计算用户相似度的时候一般会采用集合的方式进行计算。理论上向量之间的相似度计算公式都可以用来计算用户之间的相似度,但是会根据实际的情况选择不同的用户相似度度量方法。 - - 这个在具体实现的时候, 可以使用`cosine_similarity`进行实现: - - ```python - from sklearn.metrics.pairwise import cosine_similarity - i = [1, 0, 0, 0] - j = [1, 0.5, 0.5, 0] - cosine_similarity([i, j]) - ``` - -
- -3. **皮尔逊相关系数** - - 皮尔逊相关系数的公式与余弦相似度的计算公式非常的类似,首先对于上述的余弦相似度的计算公式写成求和的形式,其中$r_{ui},r_{vi}$分别表示用户$u$和用户$v$对商品$i$是否有交互(或者具体的评分值): - $$ - sim_{uv} = \frac{\sum_i r_{ui}*r_{vi}}{\sqrt{\sum_i r_{ui}^2}\sqrt{\sum_i r_{vi}^2}} - $$ - 如下是皮尔逊相关系数计算公式,其中$r_{ui},r_{vi}$分别表示用户$u$和用户$v$对商品$i$是否有交互(或者具体的评分值),$\bar r_u, \bar r_v$分别表示用户$u$和用户$v$交互的所有商品交互数量或者具体评分的平均值。 - $$ - sim(u,v)=\frac{\sum_{i\in I}(r_{ui}-\bar r_u)(r_{vi}-\bar r_v)}{\sqrt{\sum_{i\in I }(r_{ui}-\bar r_u)^2}\sqrt{\sum_{i\in I }(r_{vi}-\bar r_v)^2}} - $$ - 所以相比余弦相似度,皮尔逊相关系数通过使用用户的平均分对各独立评分进行修正,减小了用户评分偏置的影响。具体实现, 我们也是可以调包, 这个计算方式很多, 下面是其中的一种: - - ```python - from scipy.stats import pearsonr - - i = [1, 0, 0, 0] - j = [1, 0.5, 0.5, 0] - pearsonr(i, j) - ``` - - -下面是基于用户协同过滤和基于物品协同过滤的原理讲解。 - -
- -### 3. 基于用户的协同过滤 - -基于用户的协同过滤(以下用UserCF表示),思想其实比较简单,**当一个用户A需要个性化推荐的时候, 我们可以先找到和他有相似兴趣的其他用户, 然后把那些用户喜欢的, 而用户A没有听说过的物品推荐给A**。 - -image-20210629232540289 - -**UserCF算法主要包括两个步骤:** - - -1. 找到和目标用户兴趣相似的集合 -2. 找到这个集合中的用户喜欢的, 且目标用户没有听说过的物品推荐给目标用户。 - -上面的两个步骤中, 第一个步骤里面, 我们会基于前面给出的相似性度量的方法找出与目标用户兴趣相似的用户, 而第二个步骤里面, 如何基于相似用户喜欢的物品来对目标用户进行推荐呢? 这个要依赖于目标用户对相似用户喜欢的物品的一个喜好程度, 那么如何衡量这个程度大小呢? 为了更好理解上面的两个步骤, 下面拿一个具体的例子把两个步骤具体化。 - -
- -**以下图为例,此例将会用于本文各种算法中** - -![image-20210629232622758](http://ryluo.oss-cn-chengdu.aliyuncs.com/图片image-20210629232622758.png) - -给用户推荐物品的过程可以**形象化为一个猜测用户对商品进行打分的任务**,上面表格里面是5个用户对于5件物品的一个打分情况,就可以理解为用户对物品的喜欢程度 - -应用UserCF算法的两个步骤: - - -1. 首先根据前面的这些打分情况(或者说已有的用户向量)计算一下Alice和用户1, 2, 3, 4的相似程度, 找出与Alice最相似的n个用户 -2. 根据这n个用户对物品5的评分情况和与Alice的相似程度会猜测出Alice对物品5的评分, 如果评分比较高的话, 就把物品5推荐给用户Alice, 否则不推荐。 - -关于第一个步骤, 上面已经给出了计算两个用户相似性的方法, 这里不再过多赘述, 这里主要解决第二个问题, 如何产生最终结果的预测。 - -
- -**最终结果的预测** - -根据上面的几种方法, 我们可以计算出向量之间的相似程度, 也就是可以计算出Alice和其他用户的相近程度, 这时候我们就可以选出与Alice最相近的前n个用户, 基于他们对物品5的评价猜测出Alice的打分值, 那么是怎么计算的呢? - -这里常用的方式之一是**利用用户相似度和相似用户的评价加权平均获得用户的评价预测**, 用下面式子表示: - -$$ -R_{\mathrm{u}, \mathrm{p}}=\frac{\sum_{\mathrm{s} \in S}\left(w_{\mathrm{u}, \mathrm{s}} \cdot R_{\mathrm{s}, \mathrm{p}}\right)}{\sum_{\mathrm{s} \in S} w_{\mathrm{u}, \mathrm{s}}} -$$ -这个式子里面, 权重$w_{u,s}$是用户$u$和用户$s$的相似度, $R_{s,p}$是用户$s$对物品$p$的评分。 - -还有一种方式如下, 这种方式考虑的更加前面, 依然是用户相似度作为权值, 但后面不单纯的是其他用户对物品的评分, 而是**该物品的评分与此用户的所有评分的差值进行加权平均, 这时候考虑到了有的用户内心的评分标准不一的情况**, 即有的用户喜欢打高分, 有的用户喜欢打低分的情况。 - -$$ -P_{i, j}=\bar{R}_{i}+\frac{\sum_{k=1}^{n}\left(S_{i, k}\left(R_{k, j}-\bar{R}_{k}\right)\right)}{\sum_{k=1}^{n} S_{i, k}} -$$ -所以这一种计算方式更为推荐。下面的计算将使用这个方式。这里的$S_{i,k}$与上面的$w_{u,s}$的意思是类似的,表示的是用户i和用户k之间的相似度。 - -在获得用户$u$对不同物品的评价预测后, 最终的推荐列表根据预测评分进行排序得到。 至此,基于用户的协同过滤算法的推荐过程完成。 - -根据上面的问题, 下面手算一下: - -Aim: 猜测Alice对物品5的得分: - - -1. **计算Alice与其他用户的相似度(这里使用皮尔逊相关系数)** - - - img - - 这里我们使用皮尔逊相关系数, 也就是Alice与用户1的相似度是0.85。同样的方式, 我们就可以计算与其他用户的相似度, 这里可以使用numpy的相似度函数得到用户的相似性矩阵: - - 图片 - - 从这里看出, Alice用户和用户2,用户3,用户4的相似度是0.82, 0.7, 0, -0.79。 所以如果n=2, 找到与Alice最相近的两个用户是用户1, 和Alice的相似度是0.85, 用户2, 和Alice相似度是0.7 - -2. **根据相似度用户计算Alice对物品5的最终得分** - 用户1对物品5的评分是3, 用户2对物品5的打分是5, 那么根据上面的计算公式, 可以计算出Alice对物品5的最终得分是 -$$ - P_{Alice, 物品5}=\bar{R}_{Alice}+\frac{\sum_{k=1}^{2}\left(S_{Alice,user k}\left(R_{userk, 物品5}-\bar{R}_{userk}\right)\right)}{\sum_{k=1}^{2} S_{Alice, userk}}=4+\frac{0.85*(3-2.4)+0.7*(5-3.8)}{0.85+0.7}=4.87 -$$ - -3. **根据用户评分对用户进行推荐** - 这时候, 我们就得到了Alice对物品5的得分是4.87, 根据Alice的打分对物品排个序从大到小:$$物品1>物品5>物品3=物品4>物品2$$ - 这时候,如果要向Alice推荐2款产品的话, 我们就可以推荐物品1和物品5给Alice - - 至此, 基于用户的协同过滤算法原理介绍完毕。 - -### 4. UserCF编程实现 - -这里简单的通过编程实现上面的案例,为后面的大作业做一个热身, 梳理一下上面的过程其实就是三步: 计算用户相似性矩阵、得到前n个相似用户、计算最终得分。 - -所以我们下面的程序也是分为这三步: - -1. **首先, 先把数据表给建立起来** -这里我采用了字典的方式, 之所以没有用pandas, 是因为上面举得这个例子其实是个个例, 在真实情况中, 我们知道, 用户对物品的打分情况并不会这么完整, 会存在大量的空值, 所以矩阵会很稀疏, 这时候用DataFrame, 会有大量的NaN。故这里用字典的形式存储。 用两个字典, 第一个字典是物品-用户的评分映射, 键是物品1-5, 用A-E来表示, 每一个值又是一个字典, 表示的是每个用户对该物品的打分。 第二个字典是用户-物品的评分映射, 键是上面的五个用户, 用1-5表示, 值是该用户对每个物品的打分。 -```python -# 定义数据集, 也就是那个表格, 注意这里我们采用字典存放数据, 因为实际情况中数据是非常稀疏的, 很少有情况是现在这样 -def loadData(): - items={'A': {1: 5, 2: 3, 3: 4, 4: 3, 5: 1}, - 'B': {1: 3, 2: 1, 3: 3, 4: 3, 5: 5}, - 'C': {1: 4, 2: 2, 3: 4, 4: 1, 5: 5}, - 'D': {1: 4, 2: 3, 3: 3, 4: 5, 5: 2}, - 'E': {2: 3, 3: 5, 4: 4, 5: 1} - } - users={1: {'A': 5, 'B': 3, 'C': 4, 'D': 4}, - 2: {'A': 3, 'B': 1, 'C': 2, 'D': 3, 'E': 3}, - 3: {'A': 4, 'B': 3, 'C': 4, 'D': 3, 'E': 5}, - 4: {'A': 3, 'B': 3, 'C': 1, 'D': 5, 'E': 4}, - 5: {'A': 1, 'B': 5, 'C': 5, 'D': 2, 'E': 1} - } - return items,users - -items, users = loadData() -item_df = pd.DataFrame(items).T -user_df = pd.DataFrame(users).T -``` - -
- -2. **计算用户相似性矩阵** - 这个是一个共现矩阵, 5*5,行代表每个用户, 列代表每个用户, 值代表用户和用户的相关性,这里的思路是这样, 因为要求用户和用户两两的相关性, 所以需要用双层循环遍历用户-物品评分数据, 当不是同一个用户的时候, 我们要去遍历物品-用户评分数据, 在里面去找这两个用户同时对该物品评过分的数据放入到这两个用户向量中。 因为正常情况下会存在很多的NAN, 即可能用户并没有对某个物品进行评分过, 这样的不能当做用户向量的一部分, 没法计算相似性。 还是看代码吧, 感觉不太好描述: - -```python -"""计算用户相似性矩阵""" -similarity_matrix = pd.DataFrame(np.zeros((len(users), len(users))), index=[1, 2, 3, 4, 5], columns=[1, 2, 3, 4, 5]) - -# 遍历每条用户-物品评分数据 -for userID in users: - for otheruserId in users: - vec_user = [] - vec_otheruser = [] - if userID != otheruserId: - for itemId in items: # 遍历物品-用户评分数据 - itemRatings = items[itemId] # 这也是个字典 每条数据为所有用户对当前物品的评分 - if userID in itemRatings and otheruserId in itemRatings: # 说明两个用户都对该物品评过分 - vec_user.append(itemRatings[userID]) - vec_otheruser.append(itemRatings[otheruserId]) - # 这里可以获得相似性矩阵(共现矩阵) - similarity_matrix[userID][otheruserId] = np.corrcoef(np.array(vec_user), np.array(vec_otheruser))[0][1] - #similarity_matrix[userID][otheruserId] = cosine_similarity(np.array(vec_user), np.array(vec_otheruser))[0][1] - -``` -这里的similarity_matrix就是我们的用户相似性矩阵, 长下面这样: - -![image-20210629232932058](http://ryluo.oss-cn-chengdu.aliyuncs.com/图片image-20210629232932058.png) - - -有了相似性矩阵, 我们就可以得到与Alice最相关的前n个用户。 - -
- -3. **计算前n个相似的用户** - -```python -"""计算前n个相似的用户""" -n = 2 -similarity_users = similarity_matrix[1].sort_values(ascending=False)[:n].index.tolist() # [2, 3] 也就是用户1和用户2 -``` - -
- -4. **计算最终得分** - 这里就是上面的那个公式了。 - -```python -"""计算最终得分""" -base_score = np.mean(np.array([value for value in users[1].values()])) -weighted_scores = 0. -corr_values_sum = 0. -for user in similarity_users: # [2, 3] - corr_value = similarity_matrix[1][user] # 两个用户之间的相似性 - mean_user_score = np.mean(np.array([value for value in users[user].values()])) # 每个用户的打分平均值 - weighted_scores += corr_value * (users[user]['E']-mean_user_score) # 加权分数 - corr_values_sum += corr_value -final_scores = base_score + weighted_scores / corr_values_sum -print('用户Alice对物品5的打分: ', final_scores) -user_df.loc[1]['E'] = final_scores -user_df - -``` -结果如下: - -![image-20210629233014318](http://ryluo.oss-cn-chengdu.aliyuncs.com/图片image-20210629233014318.png) - -至此, 我们就用代码完成了上面的小例子, 有了这个评分, 我们其实就可以对该用户做推荐了。 这其实就是微型版的UserCF的工作过程了。 - -**注意:基于用户协同过滤的完整代码参考源代码文件中的UserCF.py** - -
- -### 5. UserCF优缺点 - -User-based算法存在两个重大问题: - - -1. 数据稀疏性。 -一个大型的电子商务推荐系统一般有非常多的物品,用户可能买的其中不到1%的物品,不同用户之间买的物品重叠性较低,导致算法无法找到一个用户的邻居,即偏好相似的用户。**这导致UserCF不适用于那些正反馈获取较困难的应用场景**(如酒店预订, 大件商品购买等低频应用) - -1. 算法扩展性。 -基于用户的协同过滤需要维护用户相似度矩阵以便快速的找出Topn相似用户, 该矩阵的存储开销非常大,存储空间随着用户数量的增加而增加,**不适合用户数据量大的情况使用**。 - -由于UserCF技术上的两点缺陷, 导致很多电商平台并没有采用这种算法, 而是采用了ItemCF算法实现最初的推荐系统。 - -
- -### 6. 基于物品的协同过滤 - -基于物品的协同过滤(ItemCF)的基本思想是预先根据所有用户的历史偏好数据计算物品之间的相似性,然后把与用户喜欢的物品相类似的物品推荐给用户。比如物品a和c非常相似,因为喜欢a的用户同时也喜欢c,而用户A喜欢a,所以把c推荐给用户A。**ItemCF算法并不利用物品的内容属性计算物品之间的相似度, 主要通过分析用户的行为记录计算物品之间的相似度, 该算法认为, 物品a和物品c具有很大的相似度是因为喜欢物品a的用户大都喜欢物品c**。 - -![图片](http://ryluo.oss-cn-chengdu.aliyuncs.com/JavagdvaYX0HSW4PdssV.png!thumbnail) - - - -**基于物品的协同过滤算法主要分为两步:** - - -* 计算物品之间的相似度 -* 根据物品的相似度和用户的历史行为给用户生成推荐列表(购买了该商品的用户也经常购买的其他商品) - -基于物品的协同过滤算法和基于用户的协同过滤算法很像, 所以我们这里直接还是拿上面Alice的那个例子来看。 - -![图片](http://ryluo.oss-cn-chengdu.aliyuncs.com/JavaE306yXB4mGmjIxbn.png!thumbnail) - -如果想知道Alice对物品5打多少分, 基于物品的协同过滤算法会这么做: - - -1. 首先计算一下物品5和物品1, 2, 3, 4之间的相似性(它们也是向量的形式, 每一列的值就是它们的向量表示, 因为ItemCF认为物品a和物品c具有很大的相似度是因为喜欢物品a的用户大都喜欢物品c, 所以就可以基于每个用户对该物品的打分或者说喜欢程度来向量化物品) -2. 找出与物品5最相近的n个物品 -3. 根据Alice对最相近的n个物品的打分去计算对物品5的打分情况 - -
- -**下面我们就可以具体计算一下, 首先是步骤1:** - -![图片](http://ryluo.oss-cn-chengdu.aliyuncs.com/Java8A7fzLNkv1ll3tc5.png!thumbnail) - -由于计算比较麻烦, 这里直接用python计算了: - -图片 - -根据皮尔逊相关系数, 可以找到与物品5最相似的2个物品是item1和item4(n=2), 下面基于上面的公式计算最终得分: - -$$ -P_{Alice, 物品5}=\bar{R}_{物品5}+\frac{\sum_{k=1}^{2}\left(S_{物品5,物品 k}\left(R_{Alice, 物品k}-\bar{R}_{物品k}\right)\right)}{\sum_{k=1}^{2} S_{物品k, 物品5}}=\frac{13}{4}+\frac{0.97*(5-3.2)+0.58*(4-3.4)}{0.97+0.58}=4.6 -$$ - - -这时候依然可以向Alice推荐物品5。下面也是简单编程实现一下, 和上面的差不多: - -```python -"""计算物品的相似矩阵""" -similarity_matrix = pd.DataFrame(np.ones((len(items), len(items))), index=['A', 'B', 'C', 'D', 'E'], columns=['A', 'B', 'C', 'D', 'E']) - -# 遍历每条物品-用户评分数据 -for itemId in items: - for otheritemId in items: - vec_item = [] # 定义列表, 保存当前两个物品的向量值 - vec_otheritem = [] - #userRagingPairCount = 0 # 两件物品均评过分的用户数 - if itemId != otheritemId: # 物品不同 - for userId in users: # 遍历用户-物品评分数据 - userRatings = users[userId] # 每条数据为该用户对所有物品的评分, 这也是个字典 - - if itemId in userRatings and otheritemId in userRatings: # 用户对这两个物品都评过分 - #userRagingPairCount += 1 - vec_item.append(userRatings[itemId]) - vec_otheritem.append(userRatings[otheritemId]) - - # 这里可以获得相似性矩阵(共现矩阵) - similarity_matrix[itemId][otheritemId] = np.corrcoef(np.array(vec_item), np.array(vec_otheritem))[0][1] - #similarity_matrix[itemId][otheritemId] = cosine_similarity(np.array(vec_item), np.array(vec_otheritem))[0][1] - -``` -这里就是物品的相似度矩阵了, 大概长下面这个样子: - -![图片](http://ryluo.oss-cn-chengdu.aliyuncs.com/Javal1MDREEuPUevQgc9.png!thumbnail) - - -**然后也是得到与物品5相似的前n个物品, 计算出最终得分来。** - -```python -"""得到与物品5相似的前n个物品""" -n = 2 -similarity_items = similarity_matrix['E'].sort_values(ascending=False)[:n].index.tolist() # ['A', 'D'] - -"""计算最终得分""" -base_score = np.mean(np.array([value for value in items['E'].values()])) -weighted_scores = 0. -corr_values_sum = 0. -for item in similarity_items: # ['A', 'D'] - corr_value = similarity_matrix['E'][item] # 两个物品之间的相似性 - mean_item_score = np.mean(np.array([value for value in items[item].values()])) # 每个物品的打分平均值 - weighted_scores += corr_value * (users[1][item]-mean_item_score) # 加权分数 - corr_values_sum += corr_value -final_scores = base_score + weighted_scores / corr_values_sum -print('用户Alice对物品5的打分: ', final_scores) -user_df.loc[1]['E'] = final_scores -user_df - -``` -结果如下: - -![图片](http://ryluo.oss-cn-chengdu.aliyuncs.com/JavaOthA6tSgTpK0Cm3E.png!thumbnail) - -**注意:基于商品的协同过滤算法的完整代码参考源代码文件中的ItemCF.py** - -
- -### 7. 算法评估 - -由于UserCF和ItemCF结果评估部分是共性知识点, 所以在这里统一标识。 这里介绍评测指标: - -1. 召回率 - - 对用户u推荐N个物品记为$R(u)$, 令用户u在测试集上喜欢的物品集合为$T(u)$, 那么召回率定义为: -$$ - \operatorname{Recall}=\frac{\sum_{u}|R(u) \cap T(u)|}{\sum_{u}|T(u)|} -$$ - 这个意思就是说, 在用户真实购买或者看过的影片里面, 我模型真正预测出了多少, 这个考察的是模型推荐的一个全面性。 - -2. 准确率 - 准确率定义为: -$$ -\operatorname{Precision}=\frac{\sum_{u} \mid R(u) \cap T(u)|}{\sum_{u}|R(u)|} -$$ - 这个意思再说, 在我推荐的所有物品中, 用户真正看的有多少, 这个考察的是我模型推荐的一个准确性。 - 为了提高准确率, 模型需要把非常有把握的才对用户进行推荐, 所以这时候就减少了推荐的数量, 而这往往就损失了全面性, 真正预测出来的会非常少,所以实际应用中应该综合考虑两者的平衡。 - -3. 覆盖率 - 覆盖率反映了推荐算法发掘长尾的能力, 覆盖率越高, 说明推荐算法越能将长尾中的物品推荐给用户。 -$$ - \text { Coverage }=\frac{\left|\bigcup_{u \in U} R(u)\right|}{|I|} -$$ - -4. 该覆盖率表示最终的推荐列表中包含多大比例的物品。如果所有物品都被给推荐给至少一个用户, 那么覆盖率是100%。 - -5. 新颖度 - 用推荐列表中物品的平均流行度度量推荐结果的新颖度。 如果推荐出的物品都很热门, 说明推荐的新颖度较低。 由于物品的流行度分布呈长尾分布, 所以为了流行度的平均值更加稳定, 在计算平均流行度时对每个物品的流行度取对数。 - -
- -### 8. 协同过滤算法的权重改进 - -image-20200923122142218 - -* 基础算法 - 图1为最简单的计算物品相关度的公式, 分子为同时喜好itemi和itemj的用户数 -* 对热门物品的惩罚 - 图1存在一个问题, 如果 item-j 是很热门的商品,导致很多喜欢 item-i 的用户都喜欢 item-j,这时 $w_{ij}$ 就会非常大。同样,几乎所有的物品都和 item-j 的相关度非常高,这显然是不合理的。所以图2中分母通过引入 $N(j)$ 来对 item-j 的热度进行惩罚。如果物品很热门, 那么$N(j)$就会越大, 对应的权重就会变小。 -* 对热门物品的进一步惩罚 - 如果 item-j 极度热门,上面的算法还是不够的。举个例子,《Harry Potter》非常火,买任何一本书的人都会购买它,即使通过图2的方法对它进行了惩罚,但是《Harry Potter》仍然会获得很高的相似度。这就是推荐系统领域著名的 Harry Potter Problem。
如果需要进一步对热门物品惩罚,可以继续修改公式为如图3所示,通过调节参数 $α$,$α $越大,惩罚力度越大,热门物品的相似度越低,整体结果的平均热门程度越低。 -* 对活跃用户的惩罚 - 同样的,Item-based CF 也需要考虑活跃用户(即一个活跃用户(专门做刷单)可能买了非常多的物品)的影响,活跃用户对物品相似度的贡献应该小于不活跃用户。图4为集合了该权重的算法。 - -
- -### 9. 协同过滤算法的问题分析 - -协同过滤算法存在的问题之一就是**泛化能力弱**, 即协同过滤无法将两个物品相似的信息推广到其他物品的相似性上。 导致的问题是**热门物品具有很强的头部效应, 容易跟大量物品产生相似, 而尾部物品由于特征向量稀疏, 导致很少被推荐**。 比如下面这个例子: - -![图片](http://ryluo.oss-cn-chengdu.aliyuncs.com/JavaxxhHm3BAtMfsy2AV.png!thumbnail) - -A, B, C, D是物品, 看右边的物品共现矩阵, 可以发现物品D与A、B、C的相似度比较大, 所以很有可能将D推荐给用过A、B、C的用户。 但是物品D与其他物品相似的原因是因为D是一件热门商品, 系统无法找出A、B、C之间相似性的原因是其特征太稀疏, 缺乏相似性计算的直接数据。 所以这就是协同过滤的天然缺陷:**推荐系统头部效应明显, 处理稀疏向量的能力弱**。 - -为了解决这个问题, 同时增加模型的泛化能力,2006年,**矩阵分解技术(Matrix Factorization,MF**)被提出, 该方法在协同过滤共现矩阵的基础上, 使用更稠密的隐向量表示用户和物品, 挖掘用户和物品的隐含兴趣和隐含特征, 在一定程度上弥补协同过滤模型处理稀疏矩阵能力不足的问题。 具体细节等后面整理, 这里先铺垫一下。 - -
- -### 10. 课后思考 - -1.**什么时候使用UserCF,什么时候使用ItemCF?为什么?** - -答案: - -> 1. UserCF -> 由于是基于用户相似度进行推荐, 所以具备更强的社交特性, 这样的特点非常适于**用户少, 物品多, 时效性较强的场合**, 比如新闻推荐场景, 因为新闻本身兴趣点分散, 相比用户对不同新闻的兴趣偏好, 新闻的及时性,热点性往往更加重要, 所以正好适用于发现热点,跟踪热点的趋势。 另外还具有推荐新信息的能力, 更有可能发现惊喜, 因为看的是人与人的相似性, 推出来的结果可能更有惊喜,可以发现用户潜在但自己尚未察觉的兴趣爱好。 -> -> 对于用户较少, 要求时效性较强的场合, 就可以考虑UserCF。 -> -> 2. ItemCF -> 这个更适用于兴趣变化较为稳定的应用, 更接近于个性化的推荐, 适合**物品少,用户多,用户兴趣固定持久, 物品更新速度不是太快的场合**, 比如推荐艺术品, 音乐, 电影。 -> 下面是UserCF和ItemCF的优缺点对比: (来自项亮推荐系统实践) - - - -2.**协同过滤在计算上有什么缺点?有什么比较好的思路可以解决(缓解)?** - -答案: - -> **较差的稀疏向量处理能力** -> -> 第一个问题就是**泛化能力弱**, 即协同过滤无法将两个物品相似的信息推广到其他物品的相似性上。 导致的问题是**热门物品具有很强的头部效应, 容易跟大量物品产生相似, 而尾部物品由于特征向量稀疏, 导致很少被推荐**。 比如下面这个例子: -> -> ![图片](http://ryluo.oss-cn-chengdu.aliyuncs.com/JavaxxhHm3BAtMfsy2AV.png!thumbnail) -> -> A, B, C, D是物品, 看右边的物品共现矩阵, 可以发现物品D与A、B、C的相似度比较大, 所以很有可能将D推荐给用过A、B、C的用户。 但是物品D与其他物品相似的原因是因为D是一件热门商品, 系统无法找出A、B、C之间相似性的原因是其特征太稀疏, 缺乏相似性计算的直接数据。 所以这就是协同过滤的天然缺陷:**推荐系统头部效应明显, 处理稀疏向量的能力弱**。 -> -> 为了解决这个问题, 同时增加模型的泛化能力,2006年,**矩阵分解技术(Matrix Factorization,MF**)被提出, 该方法在协同过滤共现矩阵的基础上, 使用更稠密的隐向量表示用户和物品, 挖掘用户和物品的隐含兴趣和隐含特征, 在一定程度上弥补协同过滤模型处理稀疏矩阵能力不足的问题。 具体细节等后面整理, 这里先铺垫一下。 - - - -**3.上面介绍的相似度计算方法有什么优劣之处?** - -> cosine相似度还是比较常用的, 一般效果也不会太差, 但是对于评分数据不规范的时候, 也就是说, 存在有的用户喜欢打高分, 有的用户喜欢打低分情况的时候,有的用户喜欢乱打分的情况, 这时候consine相似度算出来的结果可能就不是那么准确了, 比如下面这种情况: -> ->![图片](http://ryluo.oss-cn-chengdu.aliyuncs.com/JavaWKvITKBhYOkfXrzs.png!thumbnail) -> ->这时候, 如果用余弦相似度进行计算, 会发现用户d和用户f比较相似, 而实际上, 如果看这个商品喜好的一个趋势的话, 其实d和e比较相近, 只不过e比较喜欢打低分, d比较喜欢打高分。 所以对于这种用户评分偏置的情况, 余弦相似度就不是那么好了, 可以考虑使用下面的皮尔逊相关系数。 - - - -4.**协同过滤还存在其他什么缺陷?有什么比较好的思路可以解决(缓解)?** - -答案: - -> 协同过滤的特点就是完全没有利用到物品本身或者是用户自身的属性, 仅仅利用了用户与物品的交互信息就可以实现推荐,比较简单高效, 但这也是它的一个短板所在, 由于无法有效的引入用户年龄, 性别,商品描述,商品分类,当前时间,地点等一系列用户特征、物品特征和上下文特征, 这就造成了有效信息的遗漏,不能充分利用其它特征数据。 -> -> 为了解决这个问题, 在推荐模型中引用更多的特征,**推荐系统慢慢的从以协同过滤为核心到了以逻辑回归模型为核心**, 提出了能够综合不同类型特征的机器学习模型。 -> -> 演化图左边的时间线梳理完毕: -> -> ![图片](http://ryluo.oss-cn-chengdu.aliyuncs.com/JavabisIdQvpBy1HM5FG.png!thumbnail) - - - - - -### 11. 参考资料 - -* [基于用户的协同过滤来构建推荐系统:https://mp.weixin.qq.com/s/ZtnaQrVIpVOPJpqMdLWOcw](https://mp.weixin.qq.com/s/ZtnaQrVIpVOPJpqMdLWOcw) -* [协同过滤算法概述:https://chenk.tech/posts/8ad63d9d.html](https://chenk.tech/posts/8ad63d9d.html) -* B站黑马推荐系统实战课程 - - - - - diff --git a/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.3 协同过滤-ItemCF.md b/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.3 协同过滤-ItemCF.md new file mode 100644 index 00000000..232055ea --- /dev/null +++ b/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.3 协同过滤-ItemCF.md @@ -0,0 +1,269 @@ +# 基于物品的协同过滤 + +## 基本思想 + +基于物品的协同过滤(ItemCF): + ++ 预先根据所有用户的历史行为数据,计算物品之间的相似性。 ++ 然后,把与用户喜欢的物品相类似的物品推荐给用户。 + +举例来说,如果用户 1 喜欢物品 A ,而物品 A 和 C 非常相似,则可以将物品 C 推荐给用户1。ItemCF算法并不利用物品的内容属性计算物品之间的相似度, 主要通过分析用户的行为记录计算物品之间的相似度, 该算法认为, 物品 A 和物品 C 具有很大的相似度是因为喜欢物品 A 的用户极可能喜欢物品 C。 + +![图片](http://ryluo.oss-cn-chengdu.aliyuncs.com/JavagdvaYX0HSW4PdssV.png!thumbnail) + +## 计算过程 + +基于物品的协同过滤算法和基于用户的协同过滤算法很像, 所以我们这里直接还是拿上面 Alice 的那个例子来看。 + +![图片](http://ryluo.oss-cn-chengdu.aliyuncs.com/JavaE306yXB4mGmjIxbn.png!thumbnail) + +如果想知道 Alice 对物品5打多少分, 基于物品的协同过滤算法会这么做: + ++ 首先计算一下物品5和物品1, 2, 3, 4之间的相似性。 + ++ 在Alice找出与物品 5 最相近的 n 个物品。 + ++ 根据 Alice 对最相近的 n 个物品的打分去计算对物品 5 的打分情况。 + +**手动计算:** + +1. 手动计算物品之间的相似度 + + >物品向量: $物品 1(3,4,3,1) ,物品2(1,3,3,5) ,物品3(2,4,1,5) ,物品4(3,3,5,2) ,物品5(3,5,41)$ + > + >+ 下面计算物品 5 和物品 1 之间的余弦相似性: + > $$ + > \operatorname{sim}(\text { 物品1, 物品5 })=\operatorname{cosine}(\text { 物品1, 物品5 } )=\frac{9+20+12+1}{\operatorname{sqrt}(9+16+9+1)+\operatorname{sqrt}(9+25+16+1)} + > $$ + > + >+ 皮尔逊相关系数类似。 + > + +2. 基于 `sklearn` 计算物品之间的皮尔逊相关系数: + +图片 + +3. 根据皮尔逊相关系数, 可以找到与物品5最相似的2个物品是 item1 和 item4, 下面基于上面的公式计算最终得分: + +$$ +P_{Alice, 物品5}=\bar{R}_{物品5}+\frac{\sum_{k=1}^{2}\left(w_{物品5,物品 k}\left(R_{Alice, 物品k}-\bar{R}_{物品k}\right)\right)}{\sum_{k=1}^{2} w_{物品k, 物品5}} \\ +=\frac{13}{4}+\frac{0.97*(5-3.2)+0.58*(4-3.4)}{0.97+0.58}=4.6 +$$ + +## ItemCF编程实现 + +1. 构建物品-用户的评分矩阵 + + ```python + import numpy as np + import pandas as pd + + + def loadData(): + items = {'A': {'Alice': 5.0, 'user1': 3.0, 'user2': 4.0, 'user3': 3.0, 'user4': 1.0}, + 'B': {'Alice': 3.0, 'user1': 1.0, 'user2': 3.0, 'user3': 3.0, 'user4': 5.0}, + 'C': {'Alice': 4.0, 'user1': 2.0, 'user2': 4.0, 'user3': 1.0, 'user4': 5.0}, + 'D': {'Alice': 4.0, 'user1': 3.0, 'user2': 3.0, 'user3': 5.0, 'user4': 2.0}, + 'E': {'user1': 3.0, 'user2': 5.0, 'user3': 4.0, 'user4': 1.0} + } + return items + ``` + +2. 计算物品间的相似度矩阵 + + ```python + item_data = loadData() + + similarity_matrix = pd.DataFrame( + np.identity(len(item_data)), + index=item_data.keys(), + columns=item_data.keys(), + ) + + # 遍历每条物品-用户评分数据 + for i1, users1 in item_data.items(): + for i2, users2 in item_data.items(): + if i1 == i2: + continue + vec1, vec2 = [], [] + for user, rating1 in users1.items(): + rating2 = users2.get(user, -1) + if rating2 == -1: + continue + vec1.append(rating1) + vec2.append(rating2) + similarity_matrix[i1][i2] = np.corrcoef(vec1, vec2)[0][1] + + print(similarity_matrix) + ``` + + ``` + A B C D E + A 1.000000 -0.476731 -0.123091 0.532181 0.969458 + B -0.476731 1.000000 0.645497 -0.310087 -0.478091 + C -0.123091 0.645497 1.000000 -0.720577 -0.427618 + D 0.532181 -0.310087 -0.720577 1.000000 0.581675 + E 0.969458 -0.478091 -0.427618 0.581675 1.000000 + ``` + +3. 从 Alice 购买过的物品中,选出与物品 `E` 最相似的 `num` 件物品。 + + ```python + target_user = ' Alice ' + target_item = 'E' + num = 2 + + sim_items = [] + sim_items_list = similarity_matrix[target_item].sort_values(ascending=False).index.tolist() + for item in sim_items_list: + # 如果target_user对物品item评分过 + if target_user in item_data[item]: + sim_items.append(item) + if len(sim_items) == num: + break + print(f'与物品{target_item}最相似的{num}个物品为:{sim_items}') + ``` + + ``` + 与物品E最相似的2个物品为:['A', 'D'] + ``` + +4. 预测用户 Alice 对物品 `E` 的评分 + + ```python + target_user_mean_rating = np.mean(list(item_data[target_item].values())) + weighted_scores = 0. + corr_values_sum = 0. + + target_item = 'E' + for item in sim_items: + corr_value = similarity_matrix[target_item][item] + user_mean_rating = np.mean(list(item_data[item].values())) + + weighted_scores += corr_value * (item_data[item][target_user] - user_mean_rating) + corr_values_sum += corr_value + + target_item_pred = target_user_mean_rating + weighted_scores / corr_values_sum + print(f'用户{target_user}对物品{target_item}的预测评分为:{target_item_pred}') + ``` + + ``` + 用户 Alice 对物品E的预测评分为:4.6 + ``` + +# 协同过滤算法的权重改进 + +* base 公式 + $$ + w_{i j}=\frac{|N(i) \bigcap N(j)|}{|N(i)|} + $$ + + + 该公式表示同时喜好物品 $i$ 和物品 $j$ 的用户数,占喜爱物品 $i$ 的比例。 + + 缺点:若物品 $j$ 为热门物品,那么它与任何物品的相似度都很高。 + +* 对热门物品进行惩罚 + $$ + w_{i j}=\frac{|N(i) \cap N(j)|}{\sqrt{|N(i)||N(j)|}} + $$ + + + * 根据 base 公式在的问题,对物品 $j$ 进行打压。打压的出发点很简单,就是在分母再除以一个物品 $j$ 被购买的数量。 + * 此时,若物品 $j$ 为热门物品,那么对应的 $N(j)$ 也会很大,受到的惩罚更多。 + +* 控制对热门物品的惩罚力度 + $$ + w_{i j}=\frac{|N(i) \cap N(j)|}{|N(i)|^{1-\alpha}|N(j)|^{\alpha}} + $$ + + * 除了第二点提到的办法,在计算物品之间相似度时可以对热门物品进行惩罚外。 + * 可以在此基础上,进一步引入参数 $\alpha$ ,这样可以通过控制参数 $\alpha$来决定对热门物品的惩罚力度。 + +* 对活跃用户的惩罚 + + * 在计算物品之间的相似度时,可以进一步将用户的活跃度考虑进来。 + $$ + w_{i j}=\frac{\sum_{\operatorname{\text {u}\in N(i) \cap N(j)}} \frac{1}{\log 1+|N(u)|}}{|N(i)|^{1-\alpha}|N(j)|^{\alpha}} + $$ + + + 对于异常活跃的用户,在计算物品之间的相似度时,他的贡献应该小于非活跃用户。 + +# 协同过滤算法的问题分析 + +协同过滤算法存在的问题之一就是泛化能力弱: + ++ 即协同过滤无法将两个物品相似的信息推广到其他物品的相似性上。 ++ 导致的问题是**热门物品具有很强的头部效应, 容易跟大量物品产生相似, 而尾部物品由于特征向量稀疏, 导致很少被推荐**。 + +比如下面这个例子: + +![图片](http://ryluo.oss-cn-chengdu.aliyuncs.com/JavaxxhHm3BAtMfsy2AV.png!thumbnail) + ++ 左边矩阵中,$A, B, C, D$ 表示的是物品。 ++ 可以看出,$D $ 是一件热门物品,其与 $A、B、C$ 的相似度比较大。因此,推荐系统更可能将 $D$ 推荐给用过 $A、B、C$ 的用户。 ++ 但是,推荐系统无法找出 $A,B,C$ 之间相似性的原因是交互数据太稀疏, 缺乏相似性计算的直接数据。 + +所以这就是协同过滤的天然缺陷:**推荐系统头部效应明显, 处理稀疏向量的能力弱**。 + +为了解决这个问题, 同时增加模型的泛化能力。2006年,**矩阵分解技术(Matrix Factorization, MF**)被提出: + ++ 该方法在协同过滤共现矩阵的基础上, 使用更稠密的隐向量表示用户和物品, 挖掘用户和物品的隐含兴趣和隐含特征。 ++ 在一定程度上弥补协同过滤模型处理稀疏矩阵能力不足的问题。 + +# 课后思考 + +1. **什么时候使用UserCF,什么时候使用ItemCF?为什么?** + +> (1)UserCF +> +> + 由于是基于用户相似度进行推荐, 所以具备更强的社交特性, 这样的特点非常适于**用户少, 物品多, 时效性较强的场合**。 +> +> + 比如新闻推荐场景, 因为新闻本身兴趣点分散, 相比用户对不同新闻的兴趣偏好, 新闻的及时性,热点性往往更加重要, 所以正好适用于发现热点,跟踪热点的趋势。 +> + 另外还具有推荐新信息的能力, 更有可能发现惊喜, 因为看的是人与人的相似性, 推出来的结果可能更有惊喜,可以发现用户潜在但自己尚未察觉的兴趣爱好。 +> +> (2)ItemCF +> +> + 这个更适用于兴趣变化较为稳定的应用, 更接近于个性化的推荐, 适合**物品少,用户多,用户兴趣固定持久, 物品更新速度不是太快的场合**。 +> + 比如推荐艺术品, 音乐, 电影。 + + + +2.**协同过滤在计算上有什么缺点?有什么比较好的思路可以解决(缓解)?** + +> 该问题答案参考上一小节的**协同过滤算法的问题分析**。 + + + +**3.上面介绍的相似度计算方法有什么优劣之处?** + +> cosine相似度计算简单方便,一般较为常用。但是,当用户的评分数据存在 bias 时,效果往往不那么好。 +> +> + 简而言之,就是不同用户评分的偏向不同。部分用户可能乐于给予好评,而部分用户习惯给予差评或者乱评分。 +> + 这个时候,根据cosine 相似度计算出来的推荐结果效果会打折扣。 +> +> 举例来说明,如下图(`X,Y,Z` 表示物品,`d,e,f`表示用户): +> +> ![图片](http://ryluo.oss-cn-chengdu.aliyuncs.com/JavaWKvITKBhYOkfXrzs.png!thumbnail) +> +> + 如果使用余弦相似度进行计算,用户 d 和 e 之间较为相似。但是实际上,用户 d 和 f 之间应该更加相似。只不过由于 d 倾向于打高分,e 倾向于打低分导致二者之间的余弦相似度更高。 +> + 这种情况下,可以考虑使用皮尔逊相关系数计算用户之间的相似性关系。 + + + +4.**协同过滤还存在其他什么缺陷?有什么比较好的思路可以解决(缓解)?** + +> + 协同过滤的优点就是没有使用更多的用户或者物品属性信息,仅利用用户和物品之间的交互信息就能完成推荐,该算法简单高效。 +> + 但这也是协同过滤算法的一个弊端。由于未使用更丰富的用户和物品特征信息,这也导致协同过滤算法的模型表达能力有限。 +> + 对于该问题,逻辑回归模型(LR)可以更好地在推荐模型中引入更多特征信息,提高模型的表达能力。 + + + +# 参考资料 + +* [基于用户的协同过滤来构建推荐系统:https://mp.weixin.qq.com/s/ZtnaQrVIpVOPJpqMdLWOcw](https://mp.weixin.qq.com/s/ZtnaQrVIpVOPJpqMdLWOcw) +* [协同过滤算法概述:https://chenk.tech/posts/8ad63d9d.html](https://chenk.tech/posts/8ad63d9d.html) +* B站黑马推荐系统实战课程 + + + + + diff --git a/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.3 矩阵分解.md b/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.3 矩阵分解.md deleted file mode 100644 index 126648aa..00000000 --- a/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.3 矩阵分解.md +++ /dev/null @@ -1,311 +0,0 @@ -### 1. 隐语义模型与矩阵分解 - -协同过滤算法的特点就是完全没有利用到物品本身或者是用户自身的属性, 仅仅利用了用户与物品的交互信息就可以实现推荐,是一个可解释性很强, 非常直观的模型, 但是也存在一些问题, 第一个就是处理稀疏矩阵的能力比较弱, 所以**为了使得协同过滤更好处理稀疏矩阵问题, 增强泛化能力**, 从协同过滤中衍生出矩阵分解模型(Matrix Factorization,MF)或者叫隐语义模型, 两者差不多说的一个意思, 就是在协同过滤共现矩阵的基础上, 使用更稠密的隐向量表示用户和物品, 挖掘用户和物品的隐含兴趣和隐含特征, 在一定程度上弥补协同过滤模型处理稀疏矩阵能力不足的问题。 - -
- -### 2. 隐语义模型 - -隐语义模型最早在文本领域被提出,用于找到文本的隐含语义。在2006年, 被用于推荐中, **它的核心思想是通过隐含特征(latent factor)联系用户兴趣和物品(item), 基于用户的行为找出潜在的主题和分类, 然后对item进行自动聚类,划分到不同类别/主题(用户的兴趣)**。 - -这么说可能有点抽象,所以下面拿项亮老师《推荐系统实践》里面的那个例子看一下: ->如果我们知道了用户A和用户B两个用户在豆瓣的读书列表, 从他们的阅读列表可以看出,用户A的兴趣涉及侦探小说、科普图书以及一些计算机技术书, 而用户B的兴趣比较集中在数学和机器学习方面。 那么如何给A和B推荐图书呢?

先说说协同过滤算法, 这样好对比不同: ->* 对于UserCF,首先需要找到和他们看了同样书的其他用户(兴趣相似的用户),然后给他们推荐那些用户喜欢的其他书。 ->* 对于ItemCF,需要给他们推荐和他们已经看的书相似的书,比如作者B看了很多关于数据挖掘的书,可以给他推荐机器学习或者模式识别方面的书。 -> ->
而如果是隐语义模型的话, 它会先通过一些角度把用户兴趣和这些书归一下类, 当来了用户之后, 首先得到他的兴趣分类, 然后从这个分类中挑选他可能喜欢的书籍。 - -
- -这里就看到了隐语义模型和协同过滤的不同, 这里说的角度其实就是这个隐含特征, 比如书籍的话它的内容, 作者, 年份, 主题等都可以算隐含特征,如果这个例子还不是很清晰的话, 那么下面再举个更为具体的例子, 看看是如何通过隐含特征来划分开用户兴趣和物品的。但是在这之前, 相信通过上面这个例子, 我们已经隐隐约约感受到了协同过滤和隐语义模型的区别了, 下面放上王喆老师《深度学习推荐系统》的一个原理图作为对比, 区别简直一目了然: - -在这里插入图片描述 - -
- -我们下面拿一个音乐评分的例子来具体看一下隐特征矩阵的含义。 - -假设每个用户都有自己的听歌偏好, 比如A喜欢带有**小清新的**, **吉他伴奏的**, **王菲**的歌曲,如果一首歌正好**是王菲唱的, 并且是吉他伴奏的小清新**, 那么就可以将这首歌推荐给这个用户。 也就是说是**小清新, 吉他伴奏, 王菲**这些元素连接起了用户和歌曲。 当然每个用户对不同的元素偏好不同, 每首歌包含的元素也不一样, 所以我们就希望找到下面的两个矩阵: -1. **潜在因子—— 用户矩阵Q** - 这个矩阵表示不同用户对于不同元素的偏好程度, 1代表很喜欢, 0代表不喜欢, 比如下面这样: - -
- 在这里插入图片描述 -
-
- -2. **潜在因子——音乐矩阵P** - 表示每种音乐含有各种元素的成分, 比如下表中, 音乐A是一个偏小清新的音乐, 含有小清新的Latent Factor的成分是0.9, 重口味的成分是0.1, 优雅成分0.2..... - -
- 在这里插入图片描述 -
-
- -**利用上面的这两个矩阵, 我们就能得出张三对音乐A的喜欢程度:** - ->张三对**小清新**的偏好 * 音乐A含有**小清新**的成分 + 张三对**重口味**的偏好 * 音乐A含有**重口味**的成分 + 张三对**优雅**的偏好 * 音乐A含有**优雅**的成分...., - -下面是对应的两个隐向量: - -
-在这里插入图片描述 -
- -根据隐向量其实就可以得到张三对音乐A的打分,即: $$0.6 * 0.9 + 0.8 * 0.1 + 0.1 * 0.2 + 0.1 * 0.4 + 0.7 * 0 = 0.69$$ - 按照这个计算方式, 每个用户对每首歌其实都可以得到这样的分数, 最后就得到了我们的评分矩阵: - -
-在这里插入图片描述 -
- -这里的红色表示用户没有打分,我们通过隐向量计算得到的。 - -上面例子中的小清晰, 重口味, 优雅这些就可以看做是隐含特征, 而通过这个隐含特征就可以把用户的兴趣和音乐的进行一个分类, 其实就是找到了每个用户每个音乐的一个隐向量表达形式(embedding的原理其实也是这样, 那里是找到每个词的隐向量表达), 这个隐向量就可以反映出用户的兴趣和物品的风格,并能将相似的物品推荐给相似的用户等。 **有没有感觉到是把协同过滤算法进行了一种延伸, 把用户的相似性和物品的相似性通过了一个叫做隐向量的方式进行表达** - -但是, 真实的情况下我们其实是没有上面那两个矩阵的, 音乐那么多, 用户那么多, 我们没有办法去找一些隐特征去表示出这些东西, 另外一个问题就是即使能表示也不一定准, 对于每个用户或者每个物品的风格,我们每个人都有不同的看法。 所以事实上, 我们有的只有用户的评分矩阵, 也就是最后的结果, 并且一般这种矩阵长这样: - -
-在这里插入图片描述 -
- -这种矩阵非常的稀疏,如果直接基于用户相似性或者物品相似性去填充这个矩阵是不太容易的, 并且很容易出现长尾问题, 所以矩阵分解就可以比较容易的解决这个问题。 - -
- -矩阵分解模型其实就是在**想办法基于这个评分矩阵去找到上面例子中的那两个矩阵, 也就是用户兴趣和物品的隐向量表达, 然后就把这个评分矩阵分解成Q和P两个矩阵乘积的形式, 这时候就可以基于这两个矩阵去预测某个用户对某个物品的评分了。 然后基于这个评分去进行推荐**。这就是矩阵分解算法的原理。 - -
- -### 3. 矩阵分解算法的原理 - -在矩阵分解的算法框架下, **我们就可以通过分解协同过滤的共现矩阵来得到用户和物品的隐向量**, 就是上面的用户矩阵Q和物品矩阵P, 这也是“矩阵分解”名字的由来。 - -
-在这里插入图片描述 -
- -矩阵分解算法将$m\times n$维的共享矩阵$R$分解成$m \times k$维的用户矩阵$U$和$k \times n$维的物品矩阵$V$相乘的形式。 其中$m$是用户数量, $n$是物品数量, $k$是隐向量维度, 也就是隐含特征个数, 只不过这里的隐含特征变得不可解释了, 即我们不知道具体含义了, 要模型自己去学。$k$的大小决定了隐向量表达能力的强弱, $k$越大, 表达信息就越强, 理解起来就是把用户的兴趣和物品的分类划分的越具体。 - -那么如果有了用户矩阵和物品矩阵的话, 我们就知道了如果想计算用户$u$对物品$i$的评分, 只需要 -$$ -\operatorname{Preference}(u, i)=r_{u i}=p_{u}^{T} q_{i}=\sum_{f=1}^{F} p_{u, k} q_{k,i} -$$ -这里的$p_u$就是用户$u$的隐向量, 就类似与上面的张三向量, 注意这是列向量, $q_i$是物品$i$的隐向量, 就类似于上面的音乐A向量, 这个也是列向量, 所以才用了$p_{u}^{T} q_{i}$得到了一个数, 也就是用户的最终评分, 计算过程其实和上面例子中一样。 这里的$p_{u,k}$和$q_{i,k}$是模型的参数, 也正是我们想办法要计算的, $p_{u,k}$度量的是用户$u$的兴趣和第$k$个隐类的关系, 而$q_{i,k}$度量了第$k$个隐类和物品$i$之间的关系。 - -
- -### 4. 矩阵分解算法的求解 - -谈到矩阵分解, 最常用的方法是特征值分解(EVD)或者奇异值分解(SVD), 关于这两个的具体原理可以参考下面的链接[奇异值分解(SVD)的原理详解及推导](https://blog.csdn.net/wuzhongqiang/article/details/108168238),但是这两种方式在这里不适用。 - -首先是EVD, 它要求分解的矩阵是方阵, 显然用户-物品矩阵不满足这个要求, 而传统的SVD分解, 会要求原始矩阵是稠密的, 而我们这里的这种矩阵一般情况下是非常稀疏的, 如果想用奇异值分解, 就必须对缺失的元素进行填充, 而一旦补全, 空间复杂度就会非常高, 且补的不一定对。 然后就是SVD分解计算复杂度非常高, 而我们的用户-物品矩阵非常大, 所以基本上无法使用。 - -
- -### 5. Basic SVD - -2006年的Netflix Prize之后, Simon Funk公布了一个矩阵分解算法叫做**Funk-SVD**, 后来被Netflix Prize的冠军Koren称为**Latent Factor Model(LFM)**。 Funk-SVD的思想很简单: **把求解上面两个矩阵的参数问题转换成一个最优化问题, 可以通过训练集里面的观察值利用最小化来学习用户矩阵和物品矩阵**。 - -我们上面已经知道了, 如果有了用户矩阵和物品矩阵的话, 我们就知道了如果想计算用户$u$对物品$i$的评分, 只需要 -$$ -\operatorname{Preference}(u, i)=r_{u i}=p_{u}^{T} q_{i}=\sum_{f=1}^{F} p_{u, k} q_{k,i} -$$ -而现在, 我们有真实的$r_{u,i}$, 但是没有$p_{u}^{T} q_{i}$, 那么我们可以初始化一个啊, 随机初始化一个用户矩阵$U$和一个物品矩阵$V$, 然后不就有$p_{u}^{T} q_{i}$了? 当然你说, 随机初始化的肯定不准啊, 但是, 有了$p_{u}^{T} q_{i}$之后, 我们就可以计算一个猜测的$\hat{r}_{u i}$, 即 -$$ -\hat{r}_{u i}=p_{u}^{T} q_{i} -$$ - -这时候, 肯定是不准, 那么这个猜测的和真实值之间就会有一个误差: -$$ -e_{u i}=r_{u i}-\hat{r}_{u i} -$$ - -有了误差, 我们就可以计算出总的误差平方和: -$$ -\operatorname{SSE}=\sum_{u, i} e_{u i}^{2}=\sum_{u, i}\left(r_{u i}-\sum_{k=1}^{K} p_{u,k} q_{k, i}\right)^{2} -$$ -有了损失, 我们就可以想办法进行训练, 把SSE降到最小, 那么我们的两个矩阵参数就可以算出来。所以就把这个问题转成了最优化的的问题, 而我们的目标函数就是: - -$$ -\min _{\boldsymbol{q}^{*}, \boldsymbol{p}^{*}} \sum_{(u, i) \in K}\left(\boldsymbol{r}_{\mathrm{ui}}-p_{u}^{T} q_{i}\right)^{2} -$$ - -这里的$K$表示所有用户评分样本的集合。 - -有了目标函数, 那么我们就可以使用梯度下降算法来降低损失。 那么我们需要对目标函数求偏导, 得到梯度。 我们的目标函数如果是上面的SSE, 我们下面来推导一下最后的导数: - -$$ -\operatorname{SSE}=\sum_{u, i} e_{u i}^{2}=\sum_{u, i}\left(r_{u i}-\sum_{k=1}^{K} p_{u,k} q_{k,i}\right)^{2} -$$ -首先我们求SSE在$p_{u,k}$(也就是Q矩阵的第$u$行$k$列)的梯度: -$$ -\frac{\partial}{\partial p_{u,k}} S S E=\frac{\partial}{\partial p_{u,k}}\left(e_{u i}^{2}\right) =2e_{u i} \frac{\partial}{\partial p_{u,k}} e_{u i}=2e_{u i} \frac{\partial}{\partial p_{u,k}}\left(r_{u i}-\sum_{k=1}^{K} p_{u,k} q_{k,i}\right)=-2e_{u i} q_{k,i} -$$ -然后求SSE在$q_{k,i}$处(也就是V矩阵的第$k$行$i$列)的梯度: - -$$ -\frac{\partial}{\partial q_{k,i}} S S E=\frac{\partial}{\partial p_{k,i}}\left(e_{u i}^{2}\right) =2e_{u i} \frac{\partial}{\partial p_{k,i}} e_{u i}=2e_{u i} \frac{\partial}{\partial p_{k,i}}\left(r_{u i}-\sum_{k=1}^{K} p_{u,k} q_{k,i}\right)=-2e_{u i} p_{u,k} -$$ -为了让公式更为简单, 把前面的2给他越掉, 即可以令SSE等于: -$$ -\operatorname{SSE}=\frac{1}{2} \sum_{u, i} e_{u i}^{2}=\frac{1}{2} \sum_{u, i}\left(r_{u i}-\sum_{k=1}^{K} p_{u k} q_{k i}\right)^{2} -$$ - -这时候, 梯度就没有前面的系数了, 有了梯度, 接下来我们就可以用梯度下降算法更新梯度了: -$$ -p_{u, k}=p_{u,k}-\eta (-e_{ui}q_{k,i})=p_{u,k}+\eta e_{ui}q_{k,i} \\ q_{k, i}=q_{k, i}-\eta (-e_{ui}p_{u,k})=q_{k, i}+\eta e_{ui}p_{u,k} -$$ - -这里的$\eta$是学习率, 控制步长用的, 但上面这个有个问题就是当参数很多的时候, 就是两个矩阵很大的时候, 往往容易陷入过拟合的困境, 这时候, 就需要在目标函数上面加上正则化的损失, 就变成了RSVD, 关于RSVD的详细内容, 可以参考下面给出的链接, 由于篇幅原因, 这里不再过多的赘述。 - -但在实际中, 单纯的$\hat{r}_{u i}=p_{u}^{T} q_{i}$也是不够的, 还要考虑其他的一些因素, 比如一个评分系统, 有些固有的属性和用户物品无关, 而用户也有些属性和物品无关, 物品也有些属性和用户无关。 因此, Netfix Prize中提出了另一种LFM, 在原来的基础上加了偏置项, 来消除用户和物品打分的偏差, 即预测公式如下: -$$ -\hat{r}_{u i}=\mu+b_{u}+b_{i}+p_{u}^{T} \cdot q_{i} -$$ -这个预测公式加入了3项偏置$\mu,b_u,b_i$, 作用如下: - -- $\mu$: 训练集中所有记录的评分的全局平均数。 在不同网站中, 因为网站定位和销售物品不同, 网站的整体评分分布也会显示差异。 比如有的网站中用户就喜欢打高分, 有的网站中用户就喜欢打低分。 而全局平均数可以表示网站本身对用户评分的影响。 -- $b_u$: 用户偏差系数, 可以使用用户$u$给出的所有评分的均值, 也可以当做训练参数。 这一项表示了用户的评分习惯中和物品没有关系的那种因素。 比如有些用户比较苛刻, 对什么东西要求很高, 那么他评分就会偏低, 而有些用户比较宽容, 对什么东西都觉得不错, 那么评分就偏高 -- $b_i$: 物品偏差系数, 可以使用物品$i$收到的所有评分的均值, 也可以当做训练参数。 这一项表示了物品接受的评分中和用户没有关系的因素。 比如有些物品本身质量就很高, 因此获得的评分相对比较高, 有的物品本身质量很差, 因此获得的评分相对较低。 - -加了用户和物品的打分偏差之后, 矩阵分解得到的隐向量更能反映不同用户对不同物品的“真实”态度差异, 也就更容易捕捉评价数据中有价值的信息, 从而避免推荐结果有偏。 注意此时的$SSE$会发生变化: -$$ -\begin{array}{l} -\operatorname{SSE}=\frac{1}{2} \sum_{u, i} e_{u i}^{2}+\frac{1}{2} \lambda \sum_{u}\left|\boldsymbol{p}_{u}\right|^{2}+\frac{1}{2} \lambda \sum_{i}\left|\boldsymbol{q}_{i}\right|^{2}+\frac{1}{2} \lambda \sum_{u} \boldsymbol{b}_{u}^{2}+\frac{1}{2} \lambda \sum_{u} \boldsymbol{b}_{i}^{2} \\ -=\frac{1}{2} \sum_{u, i}\left(\boldsymbol{r}_{u i}-\boldsymbol{\mu}-\boldsymbol{b}_{u}-\boldsymbol{b}_{i}-\sum_{k=1}^{K} \boldsymbol{p}_{u k} \boldsymbol{q}_{k i}\right)^{2}+\frac{1}{2} \lambda \sum_{u}\left|\boldsymbol{p}_{u}\right|^{2}+\frac{1}{2} \lambda \sum_{i}\left|\boldsymbol{q}_{i}\right|^{2}+\frac{\mathbf{1}}{2} \lambda \sum_{u} \boldsymbol{b}_{u}^{2}+\frac{1}{2} \lambda \sum_{u} \boldsymbol{b}_{i}^{2} -\end{array} -$$ -此时如果把$b_u$和$b_i$当做训练参数的话, 那么它俩的梯度是: - -$$ -\frac{\partial}{\partial b_{u}} S S E=-e_{u i}+\lambda b_{u} \\ \frac{\partial}{\partial b_{i}} S S E=-e_{u i}+\lambda b_{i} -$$ -更新公式为: -$$ -\begin{aligned} -\boldsymbol{b}_{u}&=\boldsymbol{b}_{\boldsymbol{u}}+\boldsymbol{\eta}\left(\boldsymbol{e}_{u i}-\lambda \boldsymbol{b}_{\boldsymbol{u}}\right) \\ -\boldsymbol{b}_{\boldsymbol{i}} &=\boldsymbol{b}_{\boldsymbol{i}}+\boldsymbol{\eta}\left(\boldsymbol{e}_{\boldsymbol{u} i}-\lambda \boldsymbol{b}_{\boldsymbol{i}}\right) -\end{aligned} -$$ -而对于$p_{u,k}$和$p_{k,i}$, 导数没有变化, 更新公式也没有变化。 - -
- -### 6. 编程实现 - -我们这里用代码实现一下上面的算法来预测上一篇文章里面的那个预测Alice对物品5的评分, 看看矩阵分解到底是怎么进行预测或者是推荐的。 我把之前的例子拿过来: - -
-在这里插入图片描述 -
- -任务就是根据这个评分矩阵, 猜测Alice对物品5的打分。 - -在实现SVD之前, 先来回忆一下ItemCF和UserCF对于这个问题的做法, 首先ItemCF的做法, 根据已有的用户打分计算物品之间的相似度, 得到物品的相似度矩阵, 根据这个相似度矩阵, 选择出前K个与物品5最相似的物品, 然后基于Alice对这K个物品的得分, 猜测Alice对物品5的得分, 有一个加权的计算公式。 UserCF的做法是根据用户对其他物品的打分, 计算用户之间的相似度, 选择出与Alice最相近的K个用户, 然后基于那K个用户对物品5的打分计算出Alice对物品5的打分。 但是, 这两种方式有个问题, 就是如果矩阵非常稀疏的话, 当然这个例子是个特例, 一般矩阵都是非常稀疏的, 那么预测效果就不好, 因为两个相似用户对同一物品打分的概率以及Alice同时对两个相似物品打分的概率可能都比较小。 另外, 这两种方法显然没有考虑到全局的物品或者用户, 只是基于了最相似的例子, 很可能有偏。 - -那么SVD在解决这个问题上是这么做的: -1. 首先, 它会先初始化用户矩阵P和物品矩阵Q, P的维度是`[users_num, F]`, Q的维度是`[item_nums, F]`, 这个F是隐向量的维度。 也就是把通过隐向量的方式把用户的兴趣和F的特点关联了起来。 初始化这两个矩阵的方式很多, 但根据经验, 随机数需要和1/sqrt(F)成正比。 下面代码中会发现。 -2. 有了两个矩阵之后, 我就可以根据用户已经打分的数据去更新参数, 这就是训练模型的过程, 方法很简单, 就是遍历用户, 对于每个用户, 遍历它打分的物品, 这样就拿到了该用户和物品的隐向量, 然后两者相乘加上偏置就是预测的评分, 这时候与真实评分有个差距, 根据上面的梯度下降就可以进行参数的更新 - -这样训练完之后, 我们就可以得到用户Alice和物品5的隐向量, 根据这个就可以预测Alice对物品5的打分。 下面的代码的逻辑就是上面这两步, 这里使用带有偏置项和正则项的那个SVD算法: - -```python -class SVD(): - def __init__(self, rating_data, F=5, alpha=0.1, lmbda=0.1, max_iter=100): - self.F = F # 这个表示隐向量的维度 - self.P = dict() # 用户矩阵P 大小是[users_num, F] - self.Q = dict() # 物品矩阵Q 大小是[item_nums, F] - self.bu = dict() # 用户偏差系数 - self.bi = dict() # 物品偏差系数 - self.mu = 1.0 # 全局偏差系数 - self.alpha = alpha # 学习率 - self.lmbda = lmbda # 正则项系数 - self.max_iter = max_iter # 最大迭代次数 - self.rating_data = rating_data # 评分矩阵 - - # 初始化矩阵P和Q, 方法很多, 一般用随机数填充, 但随机数大小有讲究, 根据经验, 随机数需要和1/sqrt(F)成正比 - cnt = 0 # 统计总的打分数, 初始化mu用 - for user, items in self.rating_data.items(): - self.P[user] = [random.random() / math.sqrt(self.F) for x in range(0, F)] - self.bu[user] = 0 - cnt += len(items) - for item, rating in items.items(): - if item not in self.Q: - self.Q[item] = [random.random() / math.sqrt(self.F) for x in range(0, F)] - self.bi[item] = 0 - self.mu /= cnt - - # 有了矩阵之后, 就可以进行训练, 这里使用随机梯度下降的方式训练参数P和Q - def train(self): - for step in range(self.max_iter): - for user, items in self.rating_data.items(): - for item, rui in items.items(): - rhat_ui = self.predict(user, item) # 得到预测评分 - # 计算误差 - e_ui = rui - rhat_ui - - self.bu[user] += self.alpha * (e_ui - self.lmbda * self.bu[user]) - self.bi[item] += self.alpha * (e_ui - self.lmbda * self.bi[item]) - # 随机梯度下降更新梯度 - for k in range(0, self.F): - self.P[user][k] += self.alpha * (e_ui*self.Q[item][k] - self.lmbda * self.P[user][k]) - self.Q[item][k] += self.alpha * (e_ui*self.P[user][k] - self.lmbda * self.Q[item][k]) - - self.alpha *= 0.1 # 每次迭代步长要逐步缩小 - - # 预测user对item的评分, 这里没有使用向量的形式 - def predict(self, user, item): - return sum(self.P[user][f] * self.Q[item][f] for f in range(0, self.F)) + self.bu[user] + self.bi[item] + self.mu -``` -下面我建立一个字典来存放数据, 之所以用字典, 是因为很多时候矩阵非常的稀疏, 如果用pandas的话, 会出现很多Nan的值, 反而不好处理。 - -```python -# 定义数据集, 也就是那个表格, 注意这里我们采用字典存放数据, 因为实际情况中数据是非常稀疏的, 很少有情况是现在这样 -def loadData(): - rating_data={1: {'A': 5, 'B': 3, 'C': 4, 'D': 4}, - 2: {'A': 3, 'B': 1, 'C': 2, 'D': 3, 'E': 3}, - 3: {'A': 4, 'B': 3, 'C': 4, 'D': 3, 'E': 5}, - 4: {'A': 3, 'B': 3, 'C': 1, 'D': 5, 'E': 4}, - 5: {'A': 1, 'B': 5, 'C': 5, 'D': 2, 'E': 1} - } - return rating_data - -# 接下来就是训练和预测 -rating_data = loadData() -basicsvd = SVD(rating_data, F=10) -basicsvd.train() -for item in ['E']: - print(item, basicsvd.predict(1, item)) - -## 结果: -E 3.252210242858994 -``` -通过这个方式, 得到的预测评分是3.25, 这个和隐向量的维度, 训练次数和训练方式有关, 这里只说一下这个东西应该怎么用, 具体结果可以不用纠结。 - -
- -### 7. 课后思考 - -1. 矩阵分解算法后续有哪些改进呢?针对这些改进,是为了解决什么的问题呢?请大家自行探索 - - RSVD,消除用户和物品打分偏差等。 - -2. 矩阵分解的优缺点分析 - - * 优点: - * 泛化能力强: 一定程度上解决了稀疏问题 - * 空间复杂度低: 由于用户和物品都用隐向量的形式存放, 少了用户和物品相似度矩阵, 空间复杂度由$n^2$降到了$(n+m)*f$ - * 更好的扩展性和灵活性:矩阵分解的最终产物是用户和物品隐向量, 这个深度学习的embedding思想不谋而合, 因此矩阵分解的结果非常便于与其他特征进行组合和拼接, 并可以与深度学习无缝结合。 - - 但是, 矩阵分解算法依然是只用到了评分矩阵, 没有考虑到用户特征, 物品特征和上下文特征, 这使得矩阵分解丧失了利用很多有效信息的机会, 同时在缺乏用户历史行为的时候, 无法进行有效的推荐。 所以为了解决这个问题, **逻辑回归模型及后续的因子分解机模型**, 凭借其天然的融合不同特征的能力, 逐渐在推荐系统领域得到了更广泛的应用。 - - - -### 8. 参考资料 - -* 王喆 - 《深度学习推荐系统》 -* 项亮 - 《推荐系统实战》 -* [奇异值分解(SVD)的原理详解及推导](https://blog.csdn.net/wuzhongqiang/article/details/108168238) -* [Matrix factorization techniques for recommender systems论文](https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=5197422&tag=1) -* [隐语义模型(LFM)和矩阵分解(MF)](https://blog.csdn.net/wuzhongqiang/article/details/108173885) diff --git a/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.4 矩阵分解.md b/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.4 矩阵分解.md new file mode 100644 index 00000000..7065d455 --- /dev/null +++ b/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.4 矩阵分解.md @@ -0,0 +1,370 @@ +# 隐语义模型与矩阵分解 + +协同过滤算法的特点: + ++ 协同过滤算法的特点就是完全没有利用到物品本身或者是用户自身的属性, 仅仅利用了用户与物品的交互信息就可以实现推荐,是一个可解释性很强, 非常直观的模型。 ++ 但是也存在一些问题,处理稀疏矩阵的能力比较弱。 + +为了使得协同过滤更好处理稀疏矩阵问题, 增强泛化能力。从协同过滤中衍生出矩阵分解模型(Matrix Factorization, MF)或者叫隐语义模型: + ++ 在协同过滤共现矩阵的基础上, 使用更稠密的隐向量表示用户和物品。 ++ 通过挖掘用户和物品的隐含兴趣和隐含特征, 在一定程度上弥补协同过滤模型处理稀疏矩阵能力不足的问题。 + + + +# 隐语义模型 + +隐语义模型最早在文本领域被提出,用于找到文本的隐含语义。在2006年, 被用于推荐中, 它的核心思想是通过隐含特征(latent factor)联系用户兴趣和物品(item), 基于用户的行为找出潜在的主题和分类, 然后对物品进行自动聚类,划分到不同类别/主题(用户的兴趣)。 + +以项亮老师《推荐系统实践》书中的内容为例: + +>如果我们知道了用户A和用户B两个用户在豆瓣的读书列表, 从他们的阅读列表可以看出,用户A的兴趣涉及侦探小说、科普图书以及一些计算机技术书, 而用户B的兴趣比较集中在数学和机器学习方面。 那么如何给A和B推荐图书呢? 先说说协同过滤算法, 这样好对比不同: +>* 对于UserCF,首先需要找到和他们看了同样书的其他用户(兴趣相似的用户),然后给他们推荐那些用户喜欢的其他书。 +>* 对于ItemCF,需要给他们推荐和他们已经看的书相似的书,比如作者B看了很多关于数据挖掘的书,可以给他推荐机器学习或者模式识别方面的书。 +> +>而如果是隐语义模型的话, 它会先通过一些角度把用户兴趣和这些书归一下类, 当来了用户之后, 首先得到他的兴趣分类, 然后从这个分类中挑选他可能喜欢的书籍。 + +隐语义模型和协同过滤的不同主要体现在隐含特征上, 比如书籍的话它的内容, 作者, 年份, 主题等都可以算隐含特征。 + +以王喆老师《深度学习推荐系统》中的一个原理图为例,看看是如何通过隐含特征来划分开用户兴趣和物品的。 + +在这里插入图片描述 + +## 音乐评分实例 + +假设每个用户都有自己的听歌偏好, 比如用户 A 喜欢带有**小清新的**, **吉他伴奏的**, **王菲**的歌曲,如果一首歌正好**是王菲唱的, 并且是吉他伴奏的小清新**, 那么就可以将这首歌推荐给这个用户。 也就是说是**小清新, 吉他伴奏, 王菲**这些元素连接起了用户和歌曲。 + +当然每个用户对不同的元素偏好不同, 每首歌包含的元素也不一样, 所以我们就希望找到下面的两个矩阵: + +1. 潜在因子—— 用户矩阵Q + 这个矩阵表示不同用户对于不同元素的偏好程度, 1代表很喜欢, 0代表不喜欢, 比如下面这样: + +
+ 在这里插入图片描述 +
+2. 潜在因子——音乐矩阵P + 表示每种音乐含有各种元素的成分, 比如下表中, 音乐A是一个偏小清新的音乐, 含有小清新的Latent Factor的成分是0.9, 重口味的成分是0.1, 优雅成分0.2... + +
+ 在这里插入图片描述 +
+**计算张三对音乐A的喜爱程度** + +利用上面的这两个矩阵,将对应向量进行内积计算,我们就能得出张三对音乐A的喜欢程度: + +
+在这里插入图片描述 +
+ ++ 张三对**小清新**的偏好 * 音乐A含有**小清新**的成分 + 张三对**重口味**的偏好 * 音乐A含有**重口味**的成分 + 张三对**优雅**的偏好 * 音乐A含有**优雅**的成分... + ++ 根据隐向量其实就可以得到张三对音乐A的打分,即: $$0.6 * 0.9 + 0.8 * 0.1 + 0.1 * 0.2 + 0.1 * 0.4 + 0.7 * 0 = 0.68$$。 + +**计算所有用户对不同音乐的喜爱程度** + +按照这个计算方式, 每个用户对每首歌其实都可以得到这样的分数, 最后就得到了我们的评分矩阵: + +
+在这里插入图片描述 +
++ 红色部分表示用户没有打分,可以通过隐向量计算得到的。 + +**小结** + ++ 上面例子中的小清晰, 重口味, 优雅这些就可以看做是隐含特征, 而通过这个隐含特征就可以把用户的兴趣和音乐的进行一个分类, 其实就是找到了每个用户每个音乐的一个隐向量表达形式(与深度学习中的embedding等价) ++ 这个隐向量就可以反映出用户的兴趣和物品的风格,并能将相似的物品推荐给相似的用户等。 **有没有感觉到是把协同过滤算法进行了一种延伸, 把用户的相似性和物品的相似性通过了一个叫做隐向量的方式进行表达** + ++ 现实中,类似于上述的矩阵 $P,Q$ 一般很难获得。有的只是用户的评分矩阵,如下: + +
+ 在这里插入图片描述 +
+ + + 这种矩阵非常的稀疏,如果直接基于用户相似性或者物品相似性去填充这个矩阵是不太容易的。 + + 并且很容易出现长尾问题, 而矩阵分解就可以比较容易的解决这个问题。 + ++ 矩阵分解模型: + + + 基于评分矩阵,将其分解成Q和P两个矩阵乘积的形式,获取用户兴趣和物品的隐向量表达。 + + 然后,基于两个分解矩阵去预测某个用户对某个物品的评分了。 + + 最后,基于预测评分去进行物品推荐。 + + + +# 矩阵分解算法 + +## 算法原理 + +在矩阵分解的算法框架下, **可以通过分解协同过滤的共现矩阵(评分矩阵)来得到用户和物品的隐向量**,原理如下:。 + +
+在这里插入图片描述 +
++ 矩阵分解算法将 $m\times n$ 维的共享矩阵 $R$ ,分解成 $m \times k$ 维的用户矩阵 $U$ 和 $k \times n$ 维的物品矩阵 $V$ 相乘的形式。 ++ 其中,$m$ 是用户数量, $n$ 是物品数量, $k$ 是隐向量维度, 也就是隐含特征个数。 ++ 这里的隐含特征没有太好的可解释性,需要模型自己去学习。 ++ 一般而言, $k$ 越大隐向量能承载的信息内容越多,表达能力也会更强,但相应的学习难度也会增加。所以,我们需要根据训练集样本的数量去选择合适的数值,在保证信息学习相对完整的前提下,降低模型的学习难度。 + +## 评分预测 + +在分解得到用户矩阵和物品矩阵后,若要计算用户 $u$ 对物品 $i$ 的评分,公式如下: +$$ +\operatorname{Preference}(u, i)=r_{u i}=p_{u}^{T} q_{i}=\sum_{k=1}^{K} p_{u, k} q_{i,k} +$$ ++ 其中,向量 $p_u$ 表示用户 $u$ 的隐向量,向量 $q_i$ 表示物品 $i$ 的隐向量。 ++ 用户向量和物品向量的内积 $p_{u}^{T} q_{i}$ 可以表示为用户 $u$ 对物品 $i$ 的预测评分。 ++ $p_{u,k}$ 和 $q_{i,k}$ 是模型的参数, $p_{u,k}$ 度量的是用户 $u$ 的兴趣和第 $k$ 个隐类的关系,$q_{i,k}$ 度量了第 $k$ 个隐类和物品 $i$ 之间的关系。 + +## 矩阵分解求解 + +常用的矩阵分解方法有特征值分解(EVD)或者奇异值分解(SVD), 具体原理可参考: + +> [奇异值分解svd原理详解及推导](https://blog.csdn.net/wuzhongqiang/article/details/108168238) + ++ 对于 EVD, 它要求分解的矩阵是方阵, 绝大部分场景下用户-物品矩阵不满足这个要求。 ++ 传统的 SVD 分解, 会要求原始矩阵是稠密的。但现实中用户的评分矩阵是非常稀疏的。 + + 如果想用奇异值分解, 就必须对缺失的元素进行填充(比如填 0 )。 + + 填充不但会导致空间复杂度增高,且补全内容不一定准确。 + + 另外,SVD 分解计算复杂度非常高,而用户-物品的评分矩阵较大,不具备普适性。 + +## FunkSVD + +2006年的Netflix Prize之后, Simon Funk公布了一个矩阵分解算法叫做**Funk-SVD**, 后来被 Netflix Prize 的冠军Koren称为**Latent Factor Model(LFM)**。 + +Funk-SVD的思想很简单: **把求解上面两个矩阵的参数问题转换成一个最优化问题, 可以通过训练集里面的观察值利用最小化来学习用户矩阵和物品矩阵**。 + +**算法过程** + +1. 根据前面提到的,在有用户矩阵和物品矩阵的前提下,若要计算用户 $u$ 对物品 $i$ 的评分, 可以根据公式: + $$ + \operatorname{Preference}(u, i)=r_{u i}=p_{u}^{T} q_{i}=\sum_{k=1}^{K} p_{u, k} q_{i,k} + $$ + + + 其中,向量 $p_u$ 表示用户 $u$ 的隐向量,向量 $q_i$ 表示物品 $i$ 的隐向量。 + +2. 随机初始化一个用户矩阵 $U$ 和一个物品矩阵 $V$,获取每个用户和物品的初始隐语义向量。 + +3. 将用户和物品的向量内积 $p_{u}^{T} q_{i}$, 作为用户对物品的预测评分 $\hat{r}_{u i}$。 + + + $\hat{r}_{u i}=p_{u}^{T} q_{i}$ 表示的是通过建模,求得的用户 $u$ 对物品的预测评分。 + + 在用户对物品的评分矩阵中,矩阵中的元素 $r_{u i}$ 才是用户对物品的真实评分。 + +4. 对于评分矩阵中的每个元素,计算预测误差 $e_{u i}=r_{u i}-\hat{r}_{u i}$,对所有训练样本的平方误差进行累加: + $$ + \operatorname{SSE}=\sum_{u, i} e_{u i}^{2}=\sum_{u, i}\left(r_{u i}-\sum_{k=1}^{K} p_{u,k} q_{i,k}\right)^{2} + $$ + + + 从上述公式可以看出,$SSE$ 建立起了训练数据和预测模型之间的关系。 + + + 如果我们希望模型预测的越准确,那么在训练集(已有的评分矩阵)上的预测误差应该仅可能小。 + + + 为方便后续求解,给 $SSE$ 增加系数 $1/2$ : + $$ + \operatorname{SSE}=\frac{1}{2} \sum_{u, i} e_{u i}^{2}=\frac{1}{2} \sum_{u, i}\left(r_{u i}-\sum_{k=1}^{K} p_{u k} q_{i k}\right)^{2} + $$ + +5. 前面提到,模型预测越准确等价于预测误差越小,那么优化的目标函数变为: + $$ + \min _{\boldsymbol{q}^{*}, \boldsymbol{p}^{*}} \frac{1}{2} \sum_{(u, i) \in K}\left(\boldsymbol{r}_{\mathrm{ui}}-p_{u}^{T} q_{i}\right)^{2} + $$ + + + $K$ 表示所有用户评分样本的集合,**即评分矩阵中不为空的元素**,其他空缺值在测试时是要预测的。 + + 该目标函数需要优化的目标是用户矩阵 $U$ 和一个物品矩阵 $V$。 + +6. 对于给定的目标函数,可以通过梯度下降法对参数进行优化。 + + + 求解目标函数 $SSE$ 关于用户矩阵中参数 $p_{u,k}$ 的梯度: + $$ + \frac{\partial}{\partial p_{u,k}} S S E=\frac{\partial}{\partial p_{u,k}}\left(\frac{1}{2}e_{u i}^{2}\right) =e_{u i} \frac{\partial}{\partial p_{u,k}} e_{u i}=e_{u i} \frac{\partial}{\partial p_{u,k}}\left(r_{u i}-\sum_{k=1}^{K} p_{u,k} q_{i,k}\right)=-e_{u i} q_{i,k} + $$ + + + 求解目标函数 $SSE$ 关于 $q_{i,k}$ 的梯度: + $$ + \frac{\partial}{\partial q_{i,k}} S S E=\frac{\partial}{\partial q_{i,k}}\left(\frac{1}{2}e_{u i}^{2}\right) =e_{u i} \frac{\partial}{\partial q_{i,k}} e_{u i}=e_{u i} \frac{\partial}{\partial q_{i,k}}\left(r_{u i}-\sum_{k=1}^{K} p_{u,k} q_{i,k}\right)=-e_{u i} p_{u,k} + $$ + +7. 参数梯度更新 + $$ + p_{u, k}=p_{u,k}-\eta (-e_{ui}q_{i, k})=p_{u,k}+\eta e_{ui}q_{i, k} \\ + q_{i, k}=q_{i,k}-\eta (-e_{ui}p_{u,k})=q_{i, k}+\eta e_{ui}p_{u, k} + $$ + + + 其中,$\eta$ 表示学习率, 用于控制步长。 + + 但上面这个有个问题就是当参数很多的时候, 就是两个矩阵很大的时候, 往往容易陷入过拟合的困境, 这时候, 就需要在目标函数上面加上正则化的损失, 就变成了RSVD, 关于RSVD的详细内容, 可以参考下面给出的链接, 由于篇幅原因, 这里不再过多的赘述。 + +**加入正则项** + +为了控制模型的复杂度。在原有模型的基础上,加入 $l2$ 正则项,来防止过拟合。 + ++ 当模型参数过大,而输入数据发生变化时,可能会造成输出的不稳定。 + ++ $l2$ 正则项等价于假设模型参数符合0均值的正态分布,从而使得模型的输出更加稳定。 + +$$ +\min _{\boldsymbol{q}^{*}, \boldsymbol{p}^{*}} \frac{1}{2} \sum_{(u, i) \in K}\left(\boldsymbol{r}_{\mathrm{ui}}-p_{u}^{T} q_{i}\right)^{2} ++ \lambda\left(\left\|p_{u}\right\|^{2}+\left\|q_{i}\right\|^{2}\right) +$$ + +## BiasSVD + +在推荐系统中,评分预测除了与用户的兴趣偏好、物品的特征属性相关外,与其他的因素也相关。例如: + ++ 例如,对于乐观的用户来说,它的评分行为普遍偏高,而对批判性用户来说,他的评分记录普遍偏低,即使他们对同一物品的评分相同,但是他们对该物品的喜好程度却并不一样。 ++ 对物品来说也是类似的。以电影为例,受大众欢迎的电影得到的评分普遍偏高,而一些烂片的评分普遍偏低,这些因素都是独立于用户或产品的因素,和用户对产品的的喜好无关。 + +因此, Netfix Prize中提出了另一种LFM, 在原来的基础上加了偏置项, 来消除用户和物品打分的偏差, 即预测公式如下: +$$ +\hat{r}_{u i}=\mu+b_{u}+b_{i}+p_{u}^{T} \cdot q_{i} +$$ +这个预测公式加入了3项偏置参数 $\mu,b_u,b_i$, 作用如下: + +- $\mu$: 该参数反映的是推荐模型整体的平均评分,一般使用所有样本评分的均值。 +- $b_u$:用户偏差系数。可以使用用户 $u$ 给出的所有评分的均值, 也可以当做训练参数。 + - 这一项表示了用户的评分习惯中和物品没有关系的那种因素。 比如有些用户比较苛刻, 对什么东西要求很高, 那么他评分就会偏低, 而有些用户比较宽容, 对什么东西都觉得不错, 那么评分就偏高 +- $b_i$:物品偏差系数。可以使用物品 $i$ 收到的所有评分的均值, 也可以当做训练参数。 + - 这一项表示了物品接受的评分中和用户没有关系的因素。 比如有些物品本身质量就很高, 因此获得的评分相对比较高, 有的物品本身质量很差, 因此获得的评分相对较低。 + +加了用户和物品的打分偏差之后, 矩阵分解得到的隐向量更能反映不同用户对不同物品的“真实”态度差异, 也就更容易捕捉评价数据中有价值的信息, 从而避免推荐结果有偏。 + +**优化函数** + +在加入正则项的FunkSVD的基础上,BiasSVD 的目标函数如下: +$$ +\begin{aligned} +\min _{q^{*}, p^{*}} \frac{1}{2} \sum_{(u, i) \in K} &\left(r_{u i}-\left(\mu+b_{u}+b_{i}+q_{i}^{T} p_{u}\right)\right)^{2} \\ +&+\lambda\left(\left\|p_{u}\right\|^{2}+\left\|q_{i}\right\|^{2}+b_{u}^{2}+b_{i}^{2}\right) +\end{aligned} +$$ +可得偏置项的梯度更新公式如下: + ++ $\frac{\partial}{\partial b_{i}} S S E=-e_{u i}+\lambda b_{i}$ ++ $ \frac{\partial}{\partial b_{u}} S S E=-e_{u i}+\lambda b_{u} \ $ + +# 编程实现 + +本小节,使用如下图表来预测Alice对物品5的评分: + +
+在这里插入图片描述 +
+基于矩阵分解算法的流程如下: + +1. 首先, 它会先初始化用户矩阵 $P$ 和物品矩阵 $Q$ , $P$ 的维度是`[users_num, K]`,$Q$ 的维度是`[items_num, K]`, + + + 其中,`F`表示隐向量的维度。 也就是把通过隐向量的方式把用户的兴趣和`F`的特点关联了起来。 + + + 初始化这两个矩阵的方式很多, 但根据经验, 随机数需要和`1/sqrt(F)`成正比。 + +2. 根据预测评分和真实评分的偏差,利用梯度下降法进行参数更新。 + + + 遍历用户及其交互过的物品,对已交互过的物品进行评分预测。 + + 由于预测评分与真实评分存在偏差, 再根据第3节的梯度更新公式更新参数。 + +3. 训练完成后,利用用户向量与目标物品向量的内积进行评分预测。 + +**完整代码如下:** + +```python +import random +import math + + +class BiasSVD(): + def __init__(self, rating_data, F=5, alpha=0.1, lmbda=0.1, max_iter=100): + self.F = F # 这个表示隐向量的维度 + self.P = dict() # 用户矩阵P 大小是[users_num, F] + self.Q = dict() # 物品矩阵Q 大小是[item_nums, F] + self.bu = dict() # 用户偏置系数 + self.bi = dict() # 物品偏置系数 + self.mu = 0 # 全局偏置系数 + self.alpha = alpha # 学习率 + self.lmbda = lmbda # 正则项系数 + self.max_iter = max_iter # 最大迭代次数 + self.rating_data = rating_data # 评分矩阵 + + for user, items in self.rating_data.items(): + # 初始化矩阵P和Q, 随机数需要和1/sqrt(F)成正比 + self.P[user] = [random.random() / math.sqrt(self.F) for x in range(0, F)] + self.bu[user] = 0 + for item, rating in items.items(): + if item not in self.Q: + self.Q[item] = [random.random() / math.sqrt(self.F) for x in range(0, F)] + self.bi[item] = 0 + + # 采用随机梯度下降的方式训练模型参数 + def train(self): + cnt, mu_sum = 0, 0 + for user, items in self.rating_data.items(): + for item, rui in items.items(): + mu_sum, cnt = mu_sum + rui, cnt + 1 + self.mu = mu_sum / cnt + + for step in range(self.max_iter): + # 遍历所有的用户及历史交互物品 + for user, items in self.rating_data.items(): + # 遍历历史交互物品 + for item, rui in items.items(): + rhat_ui = self.predict(user, item) # 评分预测 + e_ui = rui - rhat_ui # 评分预测偏差 + + # 参数更新 + self.bu[user] += self.alpha * (e_ui - self.lmbda * self.bu[user]) + self.bi[item] += self.alpha * (e_ui - self.lmbda * self.bi[item]) + for k in range(0, self.F): + self.P[user][k] += self.alpha * (e_ui * self.Q[item][k] - self.lmbda * self.P[user][k]) + self.Q[item][k] += self.alpha * (e_ui * self.P[user][k] - self.lmbda * self.Q[item][k]) + # 逐步降低学习率 + self.alpha *= 0.1 + + + # 评分预测 + def predict(self, user, item): + return sum(self.P[user][f] * self.Q[item][f] for f in range(0, self.F)) + self.bu[user] + self.bi[ + item] + self.mu + + +# 通过字典初始化训练样本,分别表示不同用户(1-5)对不同物品(A-E)的真实评分 +def loadData(): + rating_data={1: {'A': 5, 'B': 3, 'C': 4, 'D': 4}, + 2: {'A': 3, 'B': 1, 'C': 2, 'D': 3, 'E': 3}, + 3: {'A': 4, 'B': 3, 'C': 4, 'D': 3, 'E': 5}, + 4: {'A': 3, 'B': 3, 'C': 1, 'D': 5, 'E': 4}, + 5: {'A': 1, 'B': 5, 'C': 5, 'D': 2, 'E': 1} + } + return rating_data + +# 加载数据 +rating_data = loadData() +# 建立模型 +basicsvd = BiasSVD(rating_data, F=10) +# 参数训练 +basicsvd.train() +# 预测用户1对物品E的评分 +for item in ['E']: + print(item, basicsvd.predict(1, item)) + +# 预测结果:E 3.685084274454321 +``` +# 课后思考 + +1. 矩阵分解算法后续有哪些改进呢?针对这些改进,是为了解决什么的问题呢?请大家自行探索RSVD,消除用户和物品打分偏差等。 + +2. 矩阵分解的优缺点分析 + + * 优点: + * 泛化能力强: 一定程度上解决了稀疏问题 + * 空间复杂度低: 由于用户和物品都用隐向量的形式存放, 少了用户和物品相似度矩阵, 空间复杂度由$n^2$降到了$(n+m)*f$ + * 更好的扩展性和灵活性:矩阵分解的最终产物是用户和物品隐向量, 这个深度学习的embedding思想不谋而合, 因此矩阵分解的结果非常便于与其他特征进行组合和拼接, 并可以与深度学习无缝结合。 + + + 缺点: + + 矩阵分解算法依然是只用到了评分矩阵, 没有考虑到用户特征, 物品特征和上下文特征, 这使得矩阵分解丧失了利用很多有效信息的机会。 + + 同时在缺乏用户历史行为的时候, 无法进行有效的推荐。 + + 为了解决这个问题, **逻辑回归模型及后续的因子分解机模型**, 凭借其天然的融合不同特征的能力, 逐渐在推荐系统领域得到了更广泛的应用。 + +# 参考资料 + +* 王喆 - 《深度学习推荐系统》 +* 项亮 - 《推荐系统实战》 +* [奇异值分解(SVD)的原理详解及推导](https://blog.csdn.net/wuzhongqiang/article/details/108168238) +* [Matrix factorization techniques for recommender systems论文](https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=5197422&tag=1) +* [隐语义模型(LFM)和矩阵分解(MF)](https://blog.csdn.net/wuzhongqiang/article/details/108173885) diff --git a/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.4 FM.md b/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.5 FM.md similarity index 100% rename from docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.4 FM.md rename to docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.5 FM.md diff --git a/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.5 GBDT+LR.md b/docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.6 GBDT+LR.md similarity index 100% rename from docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.5 GBDT+LR.md rename to docs/第一章 推荐系统基础/1.1 基础推荐算法/1.1.6 GBDT+LR.md