课程11. 推荐系统
推荐系统
今天,我们将了解机器学习中应用最广泛、发展最为活跃的领域之一——推荐系统。
推荐系统是一套方法和算法,其任务是根据用户和对象的数据,预测哪些对象/实体(电影、书籍、产品等)会引起用户的兴趣。
在谈论推荐质量时,通常使用“相关”一词来代替“有趣”。相关性衡量的是某个对象当前满足用户需求的程度。“相关性”一词源自机器学习的一个相关领域,即信息检索。
推荐系统的例子
推荐系统几乎存在于我们日常使用的所有服务中。让我们看看哪些服务可以被推荐?
最著名的拥有自有推荐系统的视频平台或许是YouTube。另一个较年轻但也在积极发展的视频推荐平台是TikTok。
既然说到视频托管,我们首先想到的就是在线影院。在任何一家在线影院的网站上,你都能看到各种类型的电影和电视剧,其中总有一款是为你量身定制的。
您可能喜欢在空闲时间或工作时听音乐。流媒体音乐服务也提供推荐功能。这类服务的推荐通常是预先生成的播放列表,或者采用广播形式——即“即时”选择下一首音乐作品。
推荐在网店中扮演着重要的角色。有了推荐,你不仅可以找到适合自己的产品,商店也可以通过增加销量来赚钱。
正如您已经了解的,有很多内容值得推荐:
- 文本文章;
- 书籍;
- 社交网络上的帖子、群组和好友;
- 商品买卖广告;
- 咖啡馆和餐厅;
- 活动和活动;
- 空缺职位;
- 等等。
当一项服务需要提供良好的推荐时,所有这些示例有哪些共同点?
- 该服务必须拥有丰富多样的内容(产品),以便提供“什么”推荐。
- 该服务必须拥有庞大的受众群体,以便提供“谁”推荐。
推荐系统的主要任务是简化相关对象的搜索并丰富用户体验。
一切从何开始?
2006年,Netflix公司(当时它还不是一家在线影院)决定为全球社区设定一项宏伟的商业任务——提升其推荐质量。当时,这是首批类似kaggle的机器学习竞赛之一。为了奖励取得最佳成绩的专家团队,公司提供了100万美元的奖金。该竞赛以当时的巨额奖金命名——Netflix奖。竞赛本身持续了3年。
Netflix公布了一个包含48万用户、1.7万部电影和1亿条评分的数据集。在当时,这是一个相当庞大的数据量,个人电脑无法轻松处理。
这项竞赛极大地推动了推荐系统和大数据处理相关算法和技术的发展。
比赛结束时,上演了一出惊心动魄的戏剧:冠军队伍比亚军队伍提前20分钟提交了解决方案。两支队伍的速度完全相同,精度达到小数点后4位(这是主办方设定的精度)。
比赛期间,目标RMSE指标提升了10%,但由于算法过于繁琐,运行时间过长,所有获胜方案均未在公司得到全面实施。
推荐系统的一般工作方案
推荐系统的一般运作模式可以分为以下几个阶段:
candidates
– 从整个实体集合items
中选择候选对象的阶段;ranking
– 对选定的候选对象进行排序的阶段;business rules
– 应用业务规则的阶段。
该系统的所有阶段都具有严格的时间限制(每个请求约 200 毫秒),这是一项非常复杂的技术任务。有些阶段不一定需要在线计算,因此候选对象选择阶段(部分)可以离线执行。
在本课中,我们将重点介绍第一阶段(候选对象选择)。
任务设置
假设:
- U U U – 一组用户;
- I I I – 一组实体(产品、文档等)– 项目。
- R = { ( u , v ) : r u v } R = \{(u, v): r_{uv}\} R={(u,v):ruv} – 一组交互/评分/评估。
这组交互可以表示为一个稀疏矩阵 R R R,其中
- 行 – 用户;
- 列 – 实体;
- 交集 – 评分。
我们希望能够解决以下问题:
- 预测矩阵中的缺失元素;
- 能够评估用户/实体之间的相似度;
- 能够为用户/实体构建推荐。
一个自然而然的问题出现了:如何在 R 矩阵中选择正确的值?信号(反馈)有两种类型:
- 显式:
- 喜欢/不喜欢;
- 评分;
- 购买;
- 评论。
- 隐式:
- 点击/展示;
- 浏览深度;
- 加入购物车;
- 行为(例如搜索查询)。
两种信号的加权和可以用作 R 值。
推荐系统的类型
推荐系统主要有四种类型:
- 基于专家知识(基于知识)
- 基于内容(基于内容)
- 基于用户行为(协同过滤)
- 混合(混合)
基于知识的推荐是基于某些专家评估的推荐。例如,如果苹果发布了一款新的智能手机,几乎所有零售商都会在主页上悬挂横幅,提供购买新设备(或预订)的优惠。
基于内容的推荐利用对实体结构的直接了解。因此,如果我们为一家在线商店提供推荐,内容推荐将决定鞋子到鞋子、智能手机到智能手机以及书籍到书籍的推荐。
协同过滤是一组考虑用户在服务中的行为及其与推荐系统的交互的算法。这类算法被认为是最强大的,并且能够提供最佳质量。
混合推荐结合了内容算法和行为算法的优势;它们的任务是学习混合两种类型的推荐。
协同过滤
协同过滤算法可以分为两类:
-
基于记忆的算法——存储整个交互矩阵 R R R 的算法。用户相似度定义为矩阵行的相似度,实体相似度定义为矩阵列的相似度。
-
基于潜在因子模型的算法——学习用户和实体的向量表征。利用这些表征,可以部分恢复矩阵 R R R 中的值。用户/实体之间的相似度定义为相应向量表征之间的相似度。
为了说明协同过滤算法的工作原理,我们将使用精简的电影用户评分数据集 MovieLens。该数据集是科学界评估推荐系统质量的经典基准。
让我们使用一些标准 Linux 操作系统命令加载此数据集:
!wget https://files.grouplens.org/datasets/movielens/ml-1m.zip -O movilens-1m.zip
!rm -r ml-latest-small ml-1m || true
!unzip movilens-1m.zip
输出:
让我们加载推荐系统的库:
!pip install surprise implicit --upgrade
输出:
import numpy as np
import pandas as pd
from tqdm.auto import tqdm
import warnings
warnings.filterwarnings("ignore")
注:这是我们第一次遇到 tqdm
库。这个库实现了一个特殊的工具,用于以所谓的 progress_bar 形式可视化进程的进度。
让我们加载数据集并按用户和时间对其进行排序:
ratings = pd.read_csv('ml-1m/ratings.dat', sep='::', header=None, names=['userId', 'itemId', 'rating', 'ts'])
ratings.sort_values(by=['userId', 'ts'], inplace=True)
ratings.head()
输出:
让我们考虑一下已加载数据集的主要特征:
print('Number of users:', ratings['userId'].nunique())
print('Number of films:', ratings['itemId'].nunique())
print('Number of ratings:', len(ratings))
输出:
Number of users: 6040
Number of films: 3706
Number of ratings: 1000209
我们来看看数据集中电影的信息:
movies = pd.read_csv('ml-1m/movies.dat', sep='::', header=None, names=['itemId', 'name', 'tags'], encoding="ISO-8859-1")
movies['tags'] = movies['tags'].str.split('|')
movies.head()
输出:
mapping_movies_name = dict(zip(movies['itemId'], movies['name']))
item2item 推荐
让我们尝试解决以下问题。我们想要学习如何根据用户行为为每部电影推荐一个相似的电影列表。我们选取所有用户喜欢的电影。我们假设由同一个人一起观看的电影更有可能是相似的。
在用户对一部电影进行正面评价后,我们会向用户推荐一个按相似度排序的相似电影列表。
例如,你观看了电影《星球大战:第五集》,并且非常喜欢它,所以给了它很高的评分。Item2Item 方法建议立即从我们已知的用户偏好角度来查看哪些电影与《星球大战:第五集》相似。最简单的情况是,我们可以查看哪些电影经常与《星球大战》一起观看,然后直接推荐它们,但我们将采用一个更有趣的想法。
为此,学习如何根据这些电影收到的推荐来衡量它们的相似度将是一个不错的选择。为此,我们将使用称为 PMI 的指标。
PMI
假设某个对象由一组二元(通常称为分类)特征描述。PMI(逐点互信息)度量通常用于评估分类特征之间的关系。假设有两个二元(或分类)特征: x x x 和 y y y。
我们来介绍一下这些符号:
- p ( x , y ) p(x, y) p(x,y) 表示在一个对象中同时出现特征 x x x 和 y y y 的概率;
- p ( x ) p(x) p(x) 表示在一个对象中同时出现特征 x x x 的概率;
- p ( y ) p(y) p(y) 表示在一个对象中同时出现特征 y y y 的概率。
我们来考虑
x
x
x 和
y
y
y 在一个对象中独立出现的情况。那么:
p
(
x
,
y
)
=
p
(
x
)
⋅
p
(
y
)
p(x, y) = p(x)⋅p(y)
p(x,y)=p(x)⋅p(y)
也就是说,
p
(
x
,
y
)
p
(
x
)
⋅
p
(
y
)
=
1
\frac{p(x, y)}{p(x)⋅p(y)} = 1
p(x)⋅p(y)p(x,y)=1
如果
x
x
x 和
y
y
y 不独立,那么
p
(
x
,
y
)
=
p
(
x
)
⋅
p
(
y
∣
x
)
=
p
(
y
)
⋅
p
(
x
∣
y
)
p(x, y) = p(x)⋅p(y|x) = p(y)⋅p(x|y)
p(x,y)=p(x)⋅p(y∣x)=p(y)⋅p(x∣y)
也就是说, p ( y ∣ x ) p ( y ) = p ( x ∣ y ) p ( x ) = p ( x , y ) p ( x ) p ( y ) \frac{p(y|x)}{p(y)} = \frac{p(x|y)}{p(x)} = \frac{p(x, y)}{p(x)p(y)} p(y)p(y∣x)=p(x)p(x∣y)=p(x)p(y)p(x,y) 表示分量 x x x 和 y y y 之间的联系程度。例如,如果该值大于 1,我们可以肯定地说特征 x x x 和 y y y 经常同时出现,因为在给定特征 y y y 的情况下遇到特征 x x x 的概率大于在任何对象中遇到特征 y y y 的概率。反之亦然,如果该值小于 1,则在具有特征 y y y 的对象中遇到特征 x x x 的概率低于平均值。
使用中性值为 1 的值不太方便,因为与 1 进行比较才能获得定性结果。将中性值转换为 0 并均衡尺度会更加方便。这可以通过对结果表达式取对数来实现。因此,我们得到了 PMI 指标:
P M I ( x , y ) = log 2 p ( y ∣ x ) p ( y ) = log 2 p ( x , y ) p ( x ) p ( y ) = log 2 p ( x ∣ y ) p ( x ) , PMI(x, y) = \log_2 \frac{p(y|x)}{p(y)} = \log_2 \frac{p(x,y)}{p(x)p(y)} = \log_2 \frac{p(x|y)}{p(x)}, PMI(x,y)=log2p(y)p(y∣x)=log2p(x)p(y)p(x,y)=log2p(x)p(x∣y),
让我们使用一种常用的数理统计技巧:在离散情况下,我们可以用频率估计来代替概率:
p ( x , y ) = n x y N , p ( x ) = n x N , p ( y ) = n y N , p(x,y) = \frac{n_{xy}} {N}, ~~ p(x) = \frac{n_{x}} {N}, ~~ p(y) = \frac{n_{y}} {N}, p(x,y)=Nnxy, p(x)=Nnx, p(y)=Nny,
则
P M I ( x , y ) = log 2 n x , y N n x n y , PMI(x,y) = \log_2 \frac{n_{x,y} N}{n_x n_y}, PMI(x,y)=log2nxnynx,yN,
其中
- n x n_x nx 表示具有特征 x x x 的对象数量;
- n y n_y ny 表示具有特征 y y y 的对象数量;
- n x , y n_{x,y} nx,y 表示同时具有特征 x x x 和 y y y 的对象数量;
- N N N 表示对象的总数。
在实际计算中,为了简化计算,我们可以退一步思考:
P
M
I
(
x
,
y
)
PMI(x,y)
PMI(x,y) 值用于比较(排序)特征
x
x
x 和
y
y
y,因此为了简单起见,我们可以放弃对数和常数
N
N
N,转而使用值
s c o r e ( x , y ) = n x , y n x n y score(x, y) = \frac{n_{x,y}}{n_x n_y} score(x,y)=nxnynx,y
在我们的例子中,用户将充当一个对象,而分类特征将是用户对电影给予积极评价的事实。
示例
为了更好地理解本文提出的评估用户与对象亲密度的思路,我将给出以下示例:
假设某在线影院有三位用户:Pasha、Sveta 和 Misha。
他们每人都恰好喜欢他们看过的电影的一半(碰巧的是,他们喜欢电影)。例如,他们对《星球大战》所有剧集的喜欢程度表格如下:
import pandas as pd
import numpy as np
df = pd.DataFrame({'Episode I': [0, 1, 0], 'Episode II': [0, 1, 0], 'Episode III': [1, 0, 1], 'Episode IV': [1, 0, 1], 'Episode V': [0, 1, 0], 'Episode VI': [1, 0, 1]},
index = ['Pasha', 'Sveta', 'Misha']).T
df
输出:
我们可以看到,Pasha 和 Misha 总是喜欢同一集《星球大战》,但 Sveta 的观点与她朋友们的观点截然不同——她喜欢所有其他剧集。
在这种情况下,我们该如何评价用户品味的相似性呢?
当然,我们希望我们的指标能够检测出 Pasha 的品味与 Misha 的品味之间的最大相似度,以及与 Sveta 品味之间的差异。
我们首先计算一下 Pasha 喜欢某部随机电影的概率。
P_pasha = df['Pasha'].sum()/df.shape[0]
P_pasha
输出:
np.float64(0.5)
现在让我们来计算一下,假设 Sveta 已经喜欢某部电影,那么 Pasha 喜欢该电影的概率是多少。
films_liked_by_sveta = df[df['Sveta'] == 1]
n_sveta_liked = films_liked_by_sveta.shape[0]
P_pasha_cond_sveta = films_liked_by_sveta['Pasha'].sum()/n_sveta_liked
P_pasha_cond_sveta
输出:
np.float64(0.0)
这并不奇怪,因为他们连一部电影都不喜欢。
但如果我们为米莎计算相同的值,会发生什么呢?
films_liked_by_misha = df[df['Misha'] == 1]
n_misha_liked = films_liked_by_misha.shape[0]
P_pasha_cond_misha = films_liked_by_misha['Pasha'].sum()/n_misha_liked
P_pasha_cond_misha
输出:
np.float64(1.0)
这也是一个很明显的结果。现在我们来计算一下概率的比例。
# Pasha and Sveta
P_pasha_cond_sveta/P_pasha
# Pasha and Misha
P_pasha_cond_misha/P_pasha
输出:
np.float64(0.0)
np.float64(2.0)
取该值的对数,我们得到PMI
# PMI Pasha and Sveta
np.log2(P_pasha_cond_sveta/P_pasha + 1e-5)
# PMI Pasha and Misha
np.log2(P_pasha_cond_misha/P_pasha)
输出:
np.float64(-16.609640474436812)
np.float64(1.0)
PMI 比星座更能衡量人们的兼容性。
我们将只选择正面评价的电影,并计算每部电影的分数: n x n_x nx 和 n y n_y ny
ratings_pos = ratings[ratings['rating'] > 3]
counts = ratings_pos.groupby('itemId')['userId'].count().rename('cnt')
counts
输出:
让我们统计一下积极的用户评分
user_likes = ratings_pos.groupby('userId')['itemId'].agg(list)
user_likes
输出:
必要的功能包
from itertools import combinations
from collections import Counter
counts_pair = Counter()
让我们计算一下 n x y n_{xy} nxy 的值,并剔除那些见面次数少于 100 次的电影。由于我们的数据集相当庞大,这种做法有助于避免考虑大量不具信息量的配对。
%%time
# 我们计算每部电影在用户中的成对出现次数 – n_{xy}
for items in tqdm(user_likes):
for pair in combinations(items, 2):
counts_pair[pair] += 1
counts_pair[pair[::-1]] += 1
counts_pair = pd.DataFrame(
[(*pair, cnt) for pair, cnt in counts_pair.items()],
columns=['itemId_x', 'itemId_y', 'pair_cnt'],
)
# 我们将删除那些出现次数少于 100 次的电影(我们认为这是噪音)
counts_pair = counts_pair[counts_pair['pair_cnt'] > 100]
counts_pair.sort_values(by=['itemId_x', 'pair_cnt'], ascending=[True, False], inplace=True)
counts_pair.head()
输出:
现在让我们将所有计数器合并在一起。由于我们针对的是“itemId_x”构建推荐,因此“n(itemId_x)”的值在 PMI 中对于所有候选人都是相同的,因此我们无需除以它。别忘了按“score”的降序对推荐进行排序。
counts_merged = pd.merge(counts_pair, counts, left_on='itemId_y', right_index=True)
counts_merged['score'] = counts_merged['pair_cnt'] / counts_merged['cnt']
counts_merged.sort_values(by=['itemId_x', 'score'], ascending=[True, False], inplace=True)
counts_merged.head()
输出:
counts_merged.shape
输出:
(217034, 5)
让我们来看看每部电影的前 30 名推荐。
recos = counts_merged.groupby('itemId_x')['itemId_y'].agg(lambda x: list(x)[:30])
recos
输出:
def top_recomendations(item_x):
print('Recommendations for ', mapping_movies_name[item_x], ':', sep='')
print('=' * 60)
for i, item_y in enumerate(recos.loc[item_x], 1):
print('{:02d}\t{}'.format(i, mapping_movies_name[item_y]))
top_recomendations(1)
输出:
基于用户和基于项目的模型
简单推荐系统
它的工作原理是“向用户 u u u 推荐其他用户购买的与用户 u u u 购买的商品相同的商品”。或者说,那句著名的公式:“购买了某件商品的顾客也购买了其他商品”。
算法:
- 选择我们确定对给定对象 i 0 i_0 i0 给出正面评价(购买/高评价/访问网站相应页面)的用户。
这些用户可以通过以下公式表征,如果矩阵 R R R 中填充的单元格 ( r u i ≠ ∅ r_{ui} \neq ∅ rui=∅) 表示短语“用户 u u u 对产品 i i i 给予高评价”:
U ( i 0 ) = { u ∈ U ∣ r u i 0 ≠ ∅ } U(i_0) = \{u \in U | r_{ui_0} \neq ∅\} U(i0)={u∈U∣rui0=∅}
- 选择与产品 i 0 i_0 i0 足够相似的产品。为此,我们使用某种产品相似度度量 ( s y m sym sym),例如 - PMI:
I ( i 0 ) = { i ∈ I ∣ s i m ( i , i 0 ) > α } I(i_0) = \{i \in I | sim(i, i_0) > α \} I(i0)={i∈I∣sim(i,i0)>α}
在这种情况下,我们寻找与 i 0 i_0 i0 的相似度不小于某个 α α α 的对象,该 α α α 的值由我们自行控制。
- 我们从找到的列表中选择 N 个最佳产品
基于用户
现在我们设定一个任务,为某个用户 u 0 u_0 u0 构建推荐。
基于用户的模型和基于项目的模型非常相似,因此我们以基于用户的方法为例进行分析。
基于用户的协同过滤模型的思想可以用推荐的概念来表达:“与 u u u相似的顾客也购买了 I ( u ) I(u) I(u)”
算法如下:
- 对于用户 u 0 u_0 u0,根据某个相似度指标选择最相似的邻居。最相似的邻居数量可以固定,也可以限制最小相似度值 α \alpha α:
U ( u 0 ) = { u ∈ U ∣ s i m ( u , u 0 ) > α } U(u_0) = \{ u \in U | sim(u, u_0) > \alpha \} U(u0)={u∈U∣sim(u,u0)>α}
集合 U ( u 0 ) U(u_0) U(u0) 也称为协作。
- 对于每个实体(电影) i i i,如果至少有一位来自协作 U ( u 0 ) U(u_0) U(u0) 的用户 u u u 对其进行了评分,我们计算特征 r u 0 i r_{u_0i} ru0i:
r u 0 i = ∑ ( u , i ) ∈ R s i m ( u , u 0 ) ⋅ r u i ∑ ( u , i ) ∈ R s i m ( u , u 0 ) r_{u_0i} = \frac{\sum\limits_{(u, i) \in R}{sim(u, u_0) \cdot r_{ui}}}{\sum\limits_{(u, i) \in R}{sim(u, u_0)}} ru0i=(u,i)∈R∑sim(u,u0)(u,i)∈R∑sim(u,u0)⋅rui
r u 0 i r_{u_0i} ru0i 表示来自协作 U ( u 0 ) U(u_0) U(u0) 的用户对实体 i i i 的所有评分的加权平均值,其中协作 U ( u 0 ) U(u_0) U(u0) 中的用户 u u u 与用户 u 0 u_0 u0 的相似度作为权重。
也就是说,我们声称,通过查看与用户 u 0 u_0 u0 品味相似的用户对电影 i i i 的评分,并考虑到这种相似性,可以预测用户 u 0 u_0 u0 对电影 i i i 的评分。
例如,假设用户 Dima、Egor 和 Masha 与用户 Lesha 相似。我们知道 Dima 不喜欢《泰坦尼克号》,他给它打了 2 分;而 Egor 和 Masha 非常喜欢《泰坦尼克号》,他们分别给它打了 4 分和 5 分。但是 Dima 的品味与 Lesha 的不太相似,他们的相似度为 1;Masha 与 Lesha 稍微相似一些,他们的相似度为 2;而 Egor 是 Lesha 最好的朋友,他们的相似度为 7。
在这种情况下,很明显,为了预测 Lesha 会对《泰坦尼克号》有多喜欢,值得更多地关注 Egor 的评价,但也值得记住 Dima 和 Masha 的评价。我们计算 Egor 的相似度占总相似度的百分比:
7
7
+
1
+
2
=
0.7
\frac{7}{7 + 1 + 2} = 0.7
7+1+27=0.7
Masha 和 Dima 的相似度也类似:
2
7
+
1
+
2
=
0.2
\frac{2}{7 + 1 + 2} = 0.2
7+1+22=0.2
1
7
+
1
+
2
=
0.1
\frac{1}{7 + 1 + 2} = 0.1
7+1+21=0.1
我们将使用以下权重来综合考虑 Dima、Egor 和 Masha 的评价:
r L e s h a , T i t a n i c = 0.7 ⋅ 4 + 0.2 ⋅ 5 + 0.1 ⋅ 2 = 2.8 + 1 + 0.2 = 4 r_{Lesha, Titanic} = 0.7⋅4 + 0.2⋅5 + 0.1⋅2 = 2.8 + 1 + 0.2 = 4 rLesha,Titanic=0.7⋅4+0.2⋅5+0.1⋅2=2.8+1+0.2=4
也就是说,我们预测 Lesha 会像他最好的朋友 Egor 一样,给《泰坦尼克号》打 4 分。
- 我们根据得分 r u 0 i r_{u_0i} ru0i 选出排名靠前的候选人。
基于项目
基于项目的方法的思路如下:“除了 u u u 购买的物品外, I ( u ) I(u) I(u) 也经常购买”
对于基于项目的方法,算法非常相似:
- 对于用户 u 0 u_0 u0,选择与该用户交互过的实体相似的实体。
I ( u 0 ) = { i ∈ I ∣ ( i 0 , u 0 ) ∈ R ; s i m ( i , i 0 ) > β } I(u_0) = \{ i \in I | (i_0, u_0) \in R; sim(i, i_0) > \beta \} I(u0)={i∈I∣(i0,u0)∈R;sim(i,i0)>β}
在这种情况下,我们再次考虑对象的相似性,而不是用户的相似性。参数 β β β 决定了对象之间的接近程度。
- 对于每个实体(电影) i i i,其值的计算方式与我们在第 2 点“基于用户”中的方法类似,只是这次计算的是实体值,而不是用户值:
r u 0 i 0 = ∑ ( u 0 , i ) ∈ R s i m ( i , i 0 ) ⋅ r u 0 i ∑ ( u 0 , i 0 ) ∈ R s i m ( i , i 0 ) r_{u_0i_0} = \frac{\sum\limits_{(u_0, i) \in R} sim(i, i_0) \cdot r_{u_0i}}{\sum\limits_{(u_0, i_0) \in R} sim(i, i_0)} ru0i0=(u0,i0)∈R∑sim(i,i0)(u0,i)∈R∑sim(i,i0)⋅ru0i
- 我们根据得分 r u 0 i r_{u_0i} ru0i 选取最佳候选对象。
此类模型的缺点
-
所有给出的模型都是基于记忆的模型,因此为了寻求协作,它们需要存储整个评分矩阵 R R R。
-
对于基于用户和基于项目的模型,冷启动问题很重要:我们不知道该向新用户(以及非典型用户)推荐什么,也不知道如何处理新对象。
示例
注意numpy版本要小于2
pip install numpy==1.24.4 scikit-surprise
import numpy as np
import surprise
print(f"NumPy version: {np.__version__}")
print(f"Surprise version: {surprise.__version__}")
输出:
NumPy version: 1.24.4
Surprise version: 1.1.4
# 将最后 10 部电影的评分放在一边进行验证
ratings_train = ratings.groupby('userId').apply(lambda x: x.iloc[:-10]).reset_index(drop=True)
ratings_valid = ratings.groupby('userId').apply(lambda x: x.iloc[-10:]).reset_index(drop=True)
# 在此单元格中,我们使用 Surprice 模块的功能加载数据
# Surprice 是一个用于推荐系统的模块
# https://surpriselib.com/
from surprise import Dataset, Reader
ratings_surprise_train = Dataset.load_from_df(
ratings_train[['userId', 'itemId', 'rating']],
Reader(rating_scale=(1, 5), ),
).build_full_trainset()
ratings_surprise_valid = Dataset.load_from_df(
ratings_valid[['userId', 'itemId', 'rating']],
Reader(rating_scale=(1, 5), ),
).build_full_trainset()
在“Surprice”模块中,我们将选择KNNBasic算法,该算法实现了两种填充
R
R
R矩阵的概念之一:基于用户或基于项目。概念的选择受“user_based”字段的影响。
我们将考虑对其进行修改,预测用户评分与其“典型”评分之间的偏差。
这个想法非常合乎逻辑:假设你的推荐系统的用户中有一位影评人Anatoly和一位头脑简单、性格温和的Vadim。影评人Anatoly对所有电影都要求非常严格,即使是相当不错的电影也很少给出高分,而Vadim几乎对他看过的所有电影都给出了好评。在这种情况下,在不了解Anatoly和Vadim行为特征的情况下,不可能设计出一个能够同时为Anatoly和Vadim做出良好预测的推荐系统。但它可以预测任何用户对某部电影的喜爱程度会高于或低于某部普通电影。这种方法对 Anatoly 和 Vadim 都适用。
KNNBaseline 类就是以此理念为指导的。
%%time
# 我们使用 KNNBaseline 而不是 KNNBasic,因为前者能给出更好的结果
from surprise import KNNBasic, KNNBaseline
model = KNNBaseline(k=10, sim_options={
'user_based': True,
'name': 'pearson_baseline',
})
model = model.fit(ratings_surprise_train)
输出:
Estimating biases using als…
Computing the pearson_baseline
similarity matrix…
Done computing similarity matrix. CPU times: user
1min 19s, sys: 4.48 s, total: 1min 23s
Wall time: 1min 25s
%%time
from surprise import accuracy
_ = accuracy.rmse(model.test(ratings_surprise_valid.build_testset()))
输出:
RMSE: 0.9384
CPU times: user 42.3 s, sys: 130 ms, total: 42.4 s
Wall time: 43.4 s
基于潜在因子模型
使用特殊向量表示用户和实体的模型在现代机器学习模型中被广泛使用。其不可否认的优势在于以下两个方面:
- 我们不再需要存储整个用户-实体交互矩阵
R
R
R - 而是存储用户
p
u
p_u
pu 和实体
q
i
q_i
qi 的向量表示;
p u ∈ R m ; m ≪ ∣ I ∣ p_u \in \mathbb{R}^m; m \ll |I| pu∈Rm;m≪∣I∣
q i ∈ R n ; n ≪ ∣ U ∣ q_i \in \mathbb{R}^n; n \ll |U| qi∈Rn;n≪∣U∣
其中 ∣ I ∣ |I| ∣I∣ 表示对象总数, ∣ U ∣ |U| ∣U∣ 表示用户总数。
需要注意的是,在原始矩阵中,我们将用户评分向量视为该用户的特征,即每个用户都由 ∣ I ∣ |I| ∣I∣ 个值表征。类似地,每个对象都由 ∣ U ∣ |U| ∣U∣ 个评分表征(在这种情况下,没有评分也被视为评分)。现在,我们希望降低此类用户描述的维度,因为它显然是冗余的(例如,在 TF-IDF 的情况下)。潜在模型可以帮助我们做到这一点。
- 与基于记忆的模型相比,它们的质量要高得多。
用户/文档的相似度 - 向量表示之间的相似度。
有几种类型的模型使用这种方法:
- 矩阵分解;
- 分解机;
- 神经网络模型。
在本课中,我们将重点讨论矩阵分解。
矩阵分解
我们来构建以下优化问题:
∑ ( u , i ) ∈ R ( r u i − ⟨ p u , q i ⟩ ) 2 ⟶ min p u , q i \sum\limits_{(u, i) \in R} \left( r_{ui} - \left\langle p_u, q_i \right\rangle \right)^2 \longrightarrow \min_{p_u, q_i} (u,i)∈R∑(rui−⟨pu,qi⟩)2⟶pu,qimin
或者,以矩阵形式表示:
∥ R − P Q T ∥ 2 2 ⟶ min P , Q \left\| R - PQ^T \right\| _ 2 ^ 2 \longrightarrow \min_{P, Q} R−PQT 22⟶P,Qmin
其中:
- P ∈ R N × K P \in \mathbb{R}^{N \times K} P∈RN×K 是用户向量表示矩阵;
- Q ∈ R M × K Q \in \mathbb{R}^{M \times K} Q∈RM×K 是实体向量表示矩阵。
奇异值分解 (SVD)
在降维方法的课程中,您详细讨论了 SVD 分解的应用。让我们回顾一下 SVD 分解的公式:
R = U Σ V T R = U \Sigma V^T R=UΣVT
其中
- U U U 和 V V V 是酉矩阵(即 U U T = U T U = I U U^T = U^T U = I UUT=UTU=I,行和列构成一个正交基),
-
Σ
\Sigma
Σ 是一个由矩阵
R
R
R 的奇异值组成的对角矩阵。
基于奇异值分解 (SVD),我们构造了 SVD 变换,它使我们能够使用低维矩阵 U k , V k , Σ k U_k, V_k, Σ_k Uk,Vk,Σk 来近似矩阵 R R R:
R ≈ R ^ = U k Σ k V k T R ≈ \hat R = U_kΣ_kV_k^T R≈R^=UkΣkVkT
奇异值分解有一个非常重要的事实——它解决了我们上面提出的最小化问题:
∥ R − U k Σ k V k T ∥ 2 2 ⟶ min U k , Σ k , V k \left\| R - U_k \Sigma_k V_k^T \right\| _ 2 ^ 2 \longrightarrow \min\limits_{U_k, Σ_k, V_k} R−UkΣkVkT 22⟶Uk,Σk,Vkmin
因此,设矩阵 P = U k Σ k P = U_k \Sigma_k P=UkΣk,矩阵 Q k = V k Q_k = V_k Qk=Vk,我们可以立即得到问题的解。
在实践中,SVD 分解很少用作矩阵分解的算法,因为奇异值分解在大型矩阵上需要非常非常长的时间。这是因为,要计算分解所涉及的矩阵,必须找到矩阵
R
R
T
R R^T
RRT 和
R
T
R
R^T R
RTR 的特征值和特征向量,从计算的角度来看,这不是一项非常简单的任务。
import scipy
import scipy.sparse as sp
此外,我们将积极使用稀疏矩阵的机制。我们在课程中已经提到过它。事实上,矩阵
R
R
R 中的大多数估计值都是缺失的。这意味着可以不存储这些“空”信息,而只存储已填充单元格的坐标及其值。处理这种数据表示的机制在模块 scipy.sparse
中实现。
# 让我们创建一个稀疏矩阵 R,称之为 X_ratings
X_ratings = sp.coo_matrix((ratings['rating'], (ratings['userId'], ratings['itemId'])), dtype=float)
X_ratings = X_ratings.tocsr()
X_ratings
输出:
<Compressed Sparse Row sparse matrix of dtype ‘float64’
with 1000209 stored elements and shape (6041, 3953)>
为了在稀疏矩阵的情况下进行 svd 分解,我们使用函数 scipy.sparse.linalg.svds。从功能上讲,它与 numpy 中的类似函数并无区别。通过设置参数 k,我们可以同时执行 svd 变换。
# 构建奇异值分解
from scipy.sparse.linalg import svds
U, s, Vh = svds(X_ratings, k=16)
P, Q = U @ np.diag(s), Vh.T
# 计算近似矩阵 R 需要存储多少个数字?
print('用户矩阵的维度:', P.shape)
print('实体矩阵的维度:', Q.shape)
输出:
用户矩阵的维度: (6041, 16)
实体矩阵的维度: (3953, 16)
让我们基于向量表示的相似度构建一个 item2item 推荐器。为此,我们可以使用 sklearn.neighbors
模块中的 NearestNeighbors
类,它将帮助我们找到最相似的对象。
from sklearn.neighbors import NearestNeighbors
nn = NearestNeighbors(n_neighbors=31)
nn = nn.fit(Q)
item_x = 2628 # Star Wars: Episode I - The Phantom Menace (1999)
# item_x = 1 # Toy Story (1995)
recos = nn.kneighbors(Q[[item_x], :], return_distance=False)[:, 1:].ravel()
print('建议 ', mapping_movies_name[item_x], ':', sep='')
print('=' * 60)
for i, item_y in enumerate(recos, 1):
print('{:02d}\t{}'.format(i, mapping_movies_name[item_y]))
输出:
我们还可以根据某个用户的偏好向量
p
u
p_u
pu 为其构建推荐。
让我们选择某个特定的用户。
# 让我们看看用户的历史记录
user_id = 761 # Sci-Fi
#user_id = 4525 # War
#user_id = 5012 # Western
# user_id = 2160 # Comedy
pd.merge(ratings[ratings['userId'] == user_id], movies, on='itemId')
输出:
# 为该用户构建推荐
# 首先,我们来计算电影与用户的相似度得分,该得分可以用常规标量积来计算
P_user = P[[user_id], ]
recomendation_scores = (P_user @ Q.T)
# 不要忘记删除用户已经评价过的内容
already_known = X_ratings[user_id].nonzero()
recomendation_scores[already_known] = -np.inf
# 我们会找到30部最合适的电影
recos = np.argsort(recomendation_scores.ravel())[::-1][:30]
movies.set_index('itemId').loc[recos].reset_index()
输出:
# 让我们看看用户的历史记录
# user_id = 761 # Sci-Fi
# user_id = 4525 # War
user_id = 5012 # Western
#user_id = 2160 # Comedy
pd.merge(ratings[ratings['userId'] == user_id], movies, on='itemId')
输出:
# 为该用户构建推荐
# 首先,我们来计算电影与用户的相似度得分,该得分可以用常规标量积来计算
P_user = P[[user_id], ]
recomendation_scores = (P_user @ Q.T)
# 不要忘记删除用户已经评价过的内容
already_known = X_ratings[user_id].nonzero()
recomendation_scores[already_known] = -np.inf
# 我们会找到30部最合适的电影
recos = np.argsort(recomendation_scores.ravel())[::-1][:30]
movies.set_index('itemId').loc[recos].reset_index()
输出:
可以看出,这样的系统非常有效。它能够识别用户的电影类型偏好,并推荐他们喜欢的电影类型!
交替最小二乘法 (ALS)
现在假设我们对奇异值分解一无所知。该如何求解这样的优化问题?
L = ∑ ( u , i ) ∈ R ( r u i − ⟨ p u , q i ⟩ ) 2 ⟶ min p u , q i L = \sum\limits_{(u, i) \in R} \left( r_{ui} - \left\langle p_u, q_i \right\rangle \right)^2 \longrightarrow \min_{p_u, q_i} L=(u,i)∈R∑(rui−⟨pu,qi⟩)2⟶pu,qimin
你已经熟悉了随机梯度下降法,因此你知道这个问题可以通过同时更新 p u p_u pu 和 q i q_i qi 的权重来迭代求解,更新方向与 ∂ L ∂ p u \frac{\partial L}{\partial p_u} ∂pu∂L 和 ∂ L ∂ q i \frac{\partial L}{\partial q_i} ∂qi∂L 的梯度方向相反。
然而,还有另一种方法可以解决这个问题。假设用户 p u p_u pu 的向量固定不变,那么我们就得到了一个线性回归的优化问题,其中实体向量 q i q_i qi 作为线性回归的参数。同样,当向量 q i q_i qi 固定不变时,我们就得到了一个带有权重 p u p_u pu 的线性回归的优化问题。
因此,上述问题可以简化为关于 p u p_u pu 和 q i q_i qi 的线性回归的交替优化。该算法称为 ALS。在实践中,它被用于大数据的矩阵分解模型中。该算法的主要优势在于其简单性(我们已经知道如何很好地解决线性回归问题)和并行化的可能性。
让我们看一个例子,我们将再次为某个用户建立建议。
我们将从 implicit
库的 als
模块中选取 ALS 算法。implicit
是另一个用于推荐系统和协同过滤的流行库。
该库中的 ALS 接口与 sklearn
模型的标准接口非常接近。我们可以调用 .fit()
函数,该函数实现了上述算法,用于选择 factors
参数指定维度的 p_u
和 q_i
向量。
from implicit.als import AlternatingLeastSquares
model = AlternatingLeastSquares(factors=16)
model.fit(X_ratings)
让我们通过调用“recommend()”函数来构建建议。
# user_id = 761 # Sci-Fi
#user_id = 4525 # War
#user_id = 5012 # Western
user_id = 2160 # Comedy
recos, _ = model.recommend(user_id, X_ratings[user_id], N=30, filter_already_liked_items=True)
movies.set_index('itemId').loc[recos].reset_index()
输出:
现在我们有了用户和实体的向量表示。尝试将它们可视化,并在此基础上对最终模型的适用性进行评估是有意义的。
我们的向量维度为 16,因此我们需要使用降维方法将它们显示在平面上。建议使用 t-SNE 方法,你在之前的课程中学习过该方法。此外,你可以进行聚类,使可视化效果更加直观。我们将使用 KMeans 算法作为聚类方法。
# 实体(电影)的向量表示
item_embeddings = model.item_factors[movies['itemId']]
item_embeddings.shape
输出:
(3883, 16)
KMeans 将从 sklearn
中获取
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=13, n_init=10, max_iter=300, random_state=56)
clusters = kmeans.fit_predict(item_embeddings)
from sklearn.manifold import TSNE
# 让我们使用 t-SNE 算法
tsne = TSNE(n_jobs=8, method='exact', perplexity=30, learning_rate=150, n_iter=1000,
random_state=9876, verbose=1000)
item_embeddings_tsne = tsne.fit_transform(item_embeddings)
输出:
另一个有用的库是 plotly
,这次是用于可视化的。你可能之前听说过它。通常,Python 中有三个库被广泛用于数据可视化:matplotlib
、seaborn
和 plotly
。我们对后者很感兴趣,因为它可以创建交互式仪表盘。plotly
库比 matplotlib
稍微难一点。如果你想了解更多关于 plotly
的信息,我们建议你阅读教程
# 使用 plotly 库可视化矢量表示和聚类
import plotly.graph_objects as go
fig = go.Figure()
hovertext = movies['name'] + '; ' + movies['tags'].map(repr)
for i in range(kmeans.n_clusters):
mask_cl = np.where(clusters == i)[0]
fig.add_trace(go.Scattergl(
x=item_embeddings_tsne[mask_cl, 0],
y=item_embeddings_tsne[mask_cl, 1],
hovertext=hovertext[mask_cl],
mode='markers',
marker=dict(
# line=dict(width=1, color='white'),
),
showlegend=True,
name='cluster_{}'.format(i),
))
fig.update_layout(
autosize=False,
width=800,
height=800,
)
fig.layout.template = 'plotly_white'
fig.show()
输出:
结论
我们在这幅图中看到了什么?
我们发现,即使在用户偏好的二维投影中,也能追踪到某些模式。聚类算法检测到了清晰可辨的聚类。这表明,我们构建用户偏好多维向量表示的方法能够识别出偏好接近甚至一致的用户子集。这种相似性可能类似于类型偏好或其他因素,但无论如何,它都以聚类的形式表现出来。
这意味着这样的向量包含足够的信息,可以在其基础上构建推荐系统。
其他推荐质量问题
除了选择算法来解决候选生成问题之外,我们还需要处理许多其他问题:
- 如何证明推荐的合理性?
- 如何处理冷启动问题?
- 如何处理流行度偏差(某些商品的流行度)?
- 如何考虑用户的隐性偏好?
- 如何帮助用户摆脱“泡沫”?
- 如何考虑用户之间的联系?