BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

一篇长文教你学会推荐系统的矩阵分解

| 作者 Nicolas Hug 关注 1 他的粉丝 ,译者 CarolGuo 关注 2 他的粉丝 发布于 2017年10月17日. 估计阅读时间: 50 分钟 | 如何结合区块链技术,帮助企业降本增效?让我们深度了解几个成功的案例。

十年前,Netflix发起了Netflix奖公开竞赛,目标在于设计最新的算法来预测电影评级。竞赛历时3年,很多研究团队开发了各种不同的预测算法,其中矩阵分解技术因效果突出从众多算法中脱颖而出。

该篇文章有两个目的:

  • 介绍如何用矩阵分解来建模电影评级。我将用具体的例子来阐述PCA和SVD的工作原理。你可能已经读过一些解释,譬如用户和项目被表示为潜在因子向量空间里的向量,评级被定义为两个向量之间的点积。如果你认为这些没有道理,希望你在读完这篇文章后能明白其含义。

  • 派生和实现一种基于矩阵分解预测评级的算法。该算法很简单,仅需要10行Python代码。我将应用该算法并在实际数据集上评估它的性能。

我试图让本文的数学内容通俗易懂,但不会过于简化事情,并避免枯燥的语句。我希望该文对机器学习初学者有所帮助,对有经验的人而言有所见解。

这篇文章分为四部分。第一部分清楚地定义了将阐述的问题,提出了一些对PCA的见解。在第二部分,我们将回顾SVD,看看它是如何对评级建模。第三部分展示了如何将SVD知识应用于评级预测中,并派生出一个基于矩阵分解的预测算法。在最后一部分,我们将调用Surprise库,用Python代码实现一个矩阵分解算法。

第一部分:PCA浅谈

问题描述

在这里,我们将谈论的问题是评级预测问题。我们的数据是评级历史数据,即用户对项目的评级,值区间是[1,5]。我们可以把数据放在一个稀疏矩阵\(R\)中:

\(\begin{align*} R = \begin{pmatrix} 1 & \color{#e74c3c}{?} & 2 & \color{#e74c3c}{?} & \color{#e74c3c}{?}\\ \color{#e74c3c}{?} & \color{#e74c3c}{?} & \color{#e74c3c}{?} & \color{#e74c3c}{?} & 4\\ 2 & \color{#e74c3c}{?} & 4 & 5 & \color{#e74c3c}{?}\\ \color{#e74c3c}{?} & \color{#e74c3c}{?} & 3 & \color{#e74c3c}{?} & \color{#e74c3c}{?}\\ \color{#e74c3c}{?} & 1 & \color{#e74c3c}{?} & 3 & \color{#e74c3c}{?}\\ 5 & \color{#e74c3c}{?} & \color{#e74c3c}{?} & \color{#e74c3c}{?} & 2\\ \end{pmatrix} \begin{matrix} \text{Alice}\\ \text{Bob}\\ \text{Charlie}\\ \text{Daniel}\\ \text{Eric}\\ \text{Frank}\\ \end{matrix} \end{align*}\)

矩阵的每一行对应一个给定用户,每一列对应一个给定项目。譬如,在上面的矩阵中,Alice对第一个项目的评级是1,Charlie对第三个项目的评级是4。在我们的问题中,我们将认为项目是电影,在后面会交替使用“项目”和“电影”这两个术语。

矩阵\(R\)是稀疏的(99%以上的内容是缺失的),我们的目标是预测那些缺失的内容,即问号处的值。

有很多不同的技术可用于评级预测,在这里我们将分解矩阵\(R\)。该矩阵分解与SVD(Singular Value Decomposition,奇异值分解)是根本相关的。SVD是线性代数的亮点之一。它的结果很美丽。如果有人告诉你数学很差劲,你可以向他们展示看看SVD能做什么。

这篇文章的目的在于解释如何能将SVD用于预测评级。但在我们进入第二部分的SVD之前,我们需要回顾一下什么是PCA。PCA只稍稍逊色于SVD,但也仍然很酷。

PCA简介

让我们暂时忘记推荐问题。我们将用到Olivetti数据集,它是40个人的脸部灰度图像集,一共有400个图像。第一组10个人的图像如下:

每一个图像的大小是64 x 64像素。我们将扁平化每个图像,于是可以得到400个向量,每个向量里有64 x 64 = 4096 个成员。我们可以将数据集表示为一个400 x 4096矩阵:

\(\begin{align*} % <![CDATA[ \newcommand{\horzbar}{\Rule{2.5ex}{0.5pt}{0.1pt}} \newcommand{\vertbar}{\Rule{0.5pt}{1pt}{2.5ex}} X= \begin{pmatrix} \horzbar & \text{Face 1} & \horzbar\\ \horzbar & \text{Face 2} & \horzbar\\ & \vdots &\\ \horzbar & \text{Face 400} & \horzbar\\ \end{pmatrix} %]]> \end{align*}\)

PCA(Principal Component Analysis,主成分分析)算法可以将400个这样的人展现为下面的图:

看起来很恐怖吧?

我们将这些人叫做主成分,他们所代表的脸部图像叫做特征脸部。可以用特征脸部做很多有趣的事情,如脸部识别优化你的tinder匹配。它们之所以被叫做特征脸部,是因为它们实际是\(X\)的协方差矩阵的特征向量(但这里我们不会给出细节,感兴趣的读者可以参看文末的参考文献)。

这里,我们获取了400个主成分,因为原始的矩阵\(X\)有400行(更确切的说,是因为\(X\)有400个评级)。你可能已经猜到,每一个主成分实际上是一个向量,与原始的脸部有着相同的维度,即有64 x 64 = 4096 个像素。

就我们而言,我们会把这些图像叫做“令人毛骨悚然的图像”。现在,关于它们的一个惊人的事情是,它们可以恢复所有的原始面孔。看看下面这些动画GIF(大约10秒长):

让我来解释下怎么回事。400个原始脸部中的每一个(即矩阵的400个原始行中的每一行)可以表示为这些令人毛骨悚然的图像的(线性)组合。也就是说,我们可以将第一个原始脸部的像素值表示为第一个令人毛骨悚然的图像的一小部分,加上第二个令人毛骨悚然的图像的一小部分,再加上第三个令人毛骨悚然的图像的一小部分,等等,直到最后一个令人毛骨图像的家伙。这一方法对其他原始脸部也适用:它们都能被表示为每一个令人毛骨悚然的图像的一小部分。我们可以将其表示为数学的方式如下:

\(\begin{align*} \text{Face 1}~=~&\alpha_1 \cdot \color{#048BA8}{\text{Creepy guy #1}}\\ +~ &\alpha_2 \cdot \color{#048BA8}{\text{Creepy guy #2}}\\ +~ &\cdots\\ +~ &\alpha_{400} \cdot \color{#048BA8}{\text{Creepy guy #400}} \end{align*}\)

\(\begin{align*} \text{Face 2}~=~&\beta_1 \cdot \color{#048BA8}{\text{Creepy guy #1}}\\ +~ &\beta_2 \cdot \color{#048BA8}{\text{Creepy guy #2}}\\ +~ &\cdots\\ +~ &\beta_{400} \cdot \color{#048BA8}{\text{Creepy guy #400}} \end{align*}\)

上面的动态图正是对这些数学公式的翻译:gif的第一帧是第一个令人毛骨悚然的图像的贡献,第二帧是第二个令人毛骨悚然的图像的贡献,等等,直到最后一个。

如果你想尝试重现这些动态图,我写了一个Python notebook

注意:用那些令人毛骨悚然的图像来表示原始脸部,这一点并没有什么特别的。我们可以随机选取400个独立的向量(每个向量是64 x 64 = 4096像素),仍然能够重现这些脸部。降低维度是让这些令人毛骨悚然的图像在PCA中变得特别的原因,我们将在第三部分讨论这些。

潜在因子

实际上,我们对那些令人毛骨悚然的图像有些过于严厉了。它们并不恐怖,它们很典型。PCA的目标就在于揭示典型的向量,每一个令人毛骨悚然的或典型的家伙代表着隐藏在数据下的一个特定的方面。理想情况下,第一个令人毛骨悚然的图像将代表譬如一个典型的年长一些的人,第二个令人毛骨悚然的图像将代表一个典型的滑头滑脑的人,而其他令人毛骨悚然的图像可以代表一些如喜欢微笑的人、表情悲伤的人、有着大鼻子的人等等概念。有了这些概念,我们可以将脸部定义为或多或少年长、或多或少滑头滑脑、或多或少微笑,等等。实际上,PCA所揭示的概念并没有那么清晰,并没有一个清楚的语义能让我们像在这里一样把任何令人毛骨悚然的图像或典型图像相关联起来。但是,有一个重要的事实是,每一个令人毛骨悚然的或典型的图像捕获了数据的某一个特征。我们把这些特征叫做潜在因子(潜在,是因为它们一直在那里,我们只需要用PCA来揭示它们)。每个主成分(令人毛骨悚然的或典型的图像)捕获一个特定的潜在因素。

现在,这是很好玩的,但是我们对用矩阵分解进行推荐感兴趣,对吧?那么我们的矩阵分解在哪里,它与推荐有什么关系? PCA实际上是即插即用的方法,它适用于任何矩阵。如果矩阵包含图像,它将显示一些典型的图像,可以构建所有初始图像。如果矩阵包含土豆,PCA将显示一些可以恢复所有原始土豆的典型土豆。如果你的矩阵包含评分,那么接下来我们来看看如何做。

PCA用于(密集的)评级矩阵

除非另有说明,我们的评级矩阵\(R\)是完全密集的,即没有缺失的内容。所有评分都是已知的。这当然不是实际推荐问题中的场景,但请与我一样如此假设。

用于用户上的PCA

我们的评级矩阵如下所示,行代表用户,列代表电影:

\(\begin{align*} % <![CDATA[ R = \begin{pmatrix} \horzbar & \text{Alice} & \horzbar\\ \horzbar & \text{Bob} & \horzbar\\ & \vdots &\\ \horzbar & \text{Zoe} & \horzbar\\ \end{pmatrix} %]]> \end{align*}\)

看起来是不是很熟悉? 我们没有用像素来表示每一行的脸部,相反,现在我们用评级来表示用户。就像之前PCA给出一些典型的家伙,现在它将给我们一些典型的用户,或者一些典型的评价者。

在理想情况下,与典型用户相关的概念将具有明确的语义含义:我们将获得典型的动作电影迷、典型的浪漫电影迷、典型的喜剧迷,等等。实际上,典型用户背后的语义并没有被明确定义,但为了简单起见,我们假设它们如此(这不会改变任何东西,只是为了直觉或解释)。

于是,我们的每个初始用户(Alice,Bob等)可以表示为典型用户的组合。例如,Alice可以被定义为动作电影迷的一小部分、喜剧电影迷的一小部分、浪漫电影迷的一大部分,等等。至于Bob,他可以更加热衷于动作电影:

\(\begin{align*} \text{Alice} &= 10\% \color{#048BA8}{\text{ 动作电影迷}} + 10\% \color{#048BA8}{\text{ 喜剧电影迷}} + 50\% \color{#048BA8}{\text{ 浪漫电影迷}} +\cdots\\ \text{Bob} &= 50\% \color{#048BA8}{\text{ 动作电影迷}} + 30\% \color{#048BA8}{\text{ 喜剧电影迷}} + 10\% \color{#048BA8}{\text{ 浪漫电影迷}} +\cdots\\ \text{Zoe} &= \cdots \end{align*}\)

该表示方法对所有用户都适用。(实际上,系数不一定是百分比,但表示成百分比更方便我们思考)。

用于电影上的PCA

如果我们对评级矩阵进行转置会发生什么?我们没有把用户当成行,而是把电影当作行,由评级定义如下:

\(\begin{align*} R^T = \begin{pmatrix} \horzbar & \text{Titanic} & \horzbar\\ \horzbar & \text{Toy Story} & \horzbar\\ & \vdots &\\ \horzbar & \text{Fargo} & \horzbar\\ \end{pmatrix} \end{align*}\)

在这种情况下,PCA不会显示典型的人脸或典型的用户,而是显示典型的电影。再次,我们将把典型的电影背后的语义意义相结合,这些典型的电影可以构造我们所有的原始电影。对所有其他电影也是如此。

第二部分: SVD背后的模型

我们来回顾一下第一部分的内容:

  • 矩阵\(R\)上的PCA将给出典型的用户。这些典型的用户当然被表示为向量(长度与用户相同,就像令人毛骨悚然的图像与原始脸部长度相同)。因为它们是向量,我们可以将它们放在我们称为\(U\)的矩阵的列中。
  • 矩阵\(R^T\)上的PCA将给出典型的电影。这些典型的电影也是向量(长度与电影相同),我们可以将它们放在我们称为\(M\)的矩阵的列中。

矩阵分解

SVD能为我们做什么呢?简而言之,SVD是\(R\)\(R^T\)上的PCA。

SVD能同时给出矩阵\(U\)\(M\)。你能同时拿到典型的用户和典型的电影。通过将\(R\)分解为三部分,SVD给出\(U\)\(M\)。下面是矩阵分解表达式:

\(\begin{align*} R=MΣU^T \end{align*}\)

SVD算法以矩阵\(R\)为输入,输出\(M\)\(Σ\)\(U\),使得:

  • \(R\)等于\(MΣU^T\)
  • \(M\)的列能重造\(R\)的所有列(我们已经知道了这一点)。
  • \(U\)的列能重造\(R\)的所有行(我们已经知道了这一点)。
  • \(M\)的列是正交的,\(U\)的列也是正交的。主成分都是正交的。这实际上是PCA(和SVD)的一个及其重要的特征,但对于推荐而言我们并不关心(之后会讲到这点)。
  • \(Σ\)是一个对角矩阵(之后会讲到这点)。

我们可以基本总结上述所有这些点为:\(M\)的列是跨越\(R\)的列空间的正交基,\(U\)的列是跨越\(R\)的行空间的正交基。

SVD背后的模型

当我们计算和使用评级矩阵\(R\)的SVD时,实际上,我们是在对评级进行特别的、有意义的建模。我们将在这里描述该建模。

简单起见,我们将忘记矩阵\(Σ\):它是一个对角矩阵,所以它仅仅是充当\(M\)\(U^T\)的标量。因此,我们将假装我们已经将其合并到其中的一个矩阵了。我们的矩阵分解是:

\(\begin{align*} R=MΣU^T \end{align*}\)

现在,用这种分解,让我们来看看用户\(u\)对项目\(i\)的评级,我们将其表示为\(r_{ui}\)

\(\begin{align*} % <![CDATA[ \newcommand{\horzbar}{\Rule{2.5ex}{0.5pt}{0.1pt}} \newcommand{\vertbar}{\Rule{0.5pt}{1pt}{2.5ex}} \begin{pmatrix} &&&&\\ &&r_{ui}&&\\ &&&&\\ \end{pmatrix} = \begin{pmatrix} &&&&\\ &\horzbar&p_u& \horzbar&\\ &&&&\\ \end{pmatrix} \begin{pmatrix} &&\vertbar&&\\ &&q_i&&\\ &&\vertbar&&\\ \end{pmatrix}\\ %]]> \end{align*}\)

由于定义矩阵乘法的定义方式,\(r_{ui}\)的值是两个向量的点积的结果:向量\(p_u\),它是\(M\)的一行,特定于用户\(u\);向量\(q_i\),它是\(U^T\)的列,特定于项目\(i\)

\(\begin{align*} r_{ui}=p_u⋅q_i, \end{align*}\)

其中⋅表示点积。是否还记得我们是如何定义用户和项目的?

\(\begin{align*} % <![CDATA[ \begin{array}{ll} \text{Alice} & = 10\% \color{#048BA8}{\text{ 动作电影迷}} &+ 10\% \color{#048BA8}{\text{ 喜剧电影迷}} &+ 50\% \color{#048BA8}{\text{ 浪漫电影迷}} &+\cdots\\ \text{Bob} &= 50\% \color{#048BA8}{\text{ 动作电影迷}}& + 30\% \color{#048BA8}{\text{ 喜剧电影迷}} &+ 10\% \color{#048BA8}{\text{ 浪漫电影迷}} &+\cdots\\ \text{Titanic} &= 20\% \color{#048BA8}{\text{ 动作片}}& + 00\% \color{#048BA8}{\text{ 喜剧片}} &+ 70\% \color{#048BA8}{\text{ 浪漫片}} &+\cdots\\ \text{Toy Story} &= 30\% \color{#048BA8}{\text{ 动作片 }} &+ 60\% \color{#048BA8}{\text{ 喜剧片}}&+ 00\% \color{#048BA8}{\text{ 浪漫片}} &+\cdots\\ \end{array} %]]> \end{align*}\)

那么,向量\(p_u\)\(q_i\)的值恰好对应于我们分配给每个潜在因子的系数:

\(\begin{align*} p_\text{Alice} &= (10\%,~~ 10\%,~~ 50\%,~~ \cdots)\\ p_\text{Bob} &= (50\%,~~ 30\%,~~ 10\%,~~ \cdots)\\ q_\text{Titanic} &= (20\%,~~ 00\%,~~ 70\%,~~ \cdots )\\ q_\text{Toy Story} &= (30\%,~~ 60\%,~~00\%,~~ \cdots ) \end{align*}\)

向量\(p_u\)表示用户\(u\)对每个潜在因素的亲和度。类似地,向量\(q_i\)表示项目\(i\)对潜在因素的亲和度。Alice被表示为10%,10%,50%,...),这意味着她对动作片和喜剧片只有一点兴趣,但她似乎喜欢浪漫片。对于Bob,他似乎更喜欢动作电影超过其他任何事情。我们还可以看到,Titanic主要是一部浪漫电影,绝对没有半点喜剧的元素。

所以,当我们使用\(R\)的SVD时,我们将用\(u\)户对项目\(i\)的评级建模如下:

\(\begin{align*} r_{ui}= p_u \cdot q_i = \sum_{f \in \text{latent factors}} \text{affinity of } u \text{ for } f \times \text{affinity of } i \text{ for }f \end{align*}\)

换句话说,如果\(u\)具有\(i\)所认可的因子,那么评级\(r_{ui}\)将会很高。相反,如果\(i\)不是\(u\)喜欢的项目(即系数不匹配),那么评级\(r_{ui}\)将会很低。在我们的例子中,Alice对Titanic的评价将会很高,而Bob的评分要低得多,因为他对浪漫电影的热情不高。然而,他对“玩具总动员”的评级将高于Alice。

我们现在有足够的知识将SVD应用于推荐任务。在此之前,我们假定矩阵\(R\)是密集的。在实际情况下,它显然是稀疏的(因为我们的目标正是使它更密集)。我们将在下一部分中谈到如何做到这一点。

第三部分:用于推荐的SVD

现在既然我们已经很好地了解了什么是SVD以及如何对评级建模,我们就可以触及问题的核心了:将SVD应用于推荐。或者,将SVD用于预测缺失的评级。我们先回到稀疏矩阵\(R\)

\(\begin{align*} % <![CDATA[ R= \begin{pmatrix} 1 & \color{#e74c3c}{?} & 2 & \color{#e74c3c}{?} & \color{#e74c3c}{?}\\ \color{#e74c3c}{?} & \color{#e74c3c}{?} & \color{#e74c3c}{?} & \color{#e74c3c}{?} & 4\\ 2 & \color{#e74c3c}{?} & 4 & 5 & \color{#e74c3c}{?}\\ \color{#e74c3c}{?} & \color{#e74c3c}{?} & 3 & \color{#e74c3c}{?} & \color{#e74c3c}{?}\\ \color{#e74c3c}{?} & 1 & \color{#e74c3c}{?} & 3 & \color{#e74c3c}{?}\\ 5 & \color{#e74c3c}{?} & \color{#e74c3c}{?} & \color{#e74c3c}{?} & 2\\ \end{pmatrix} %]]> \end{align*}\)

我们的目标是预测矩阵中问号的值。

遇到问题

然而,\(R\)的SVD并没有定义。它不存在,所以根本不可能计算它。但是不用担心,我们接下来讲讲怎么办。

如果\(R\)是密集的,我们可以很容易计算\(M\)\(U\)\(M\)的行是\(RR^T\)的特征向量,\(U\)的列是\(R^TR\)的特征向量。相关联的特征值组成对角矩阵\(Σ\)。有一些很有效的算法可以计算这些。

但是,\(R\)是稀疏的,矩阵\(RR^T\)\(R^TR\)并不存在,所以它们的特征向量也不存在,而且我们不能把\(R\)分解为\(MΣU^T\)的乘积。但是,有一些办法。曾被用过一段时间的第一个选择是,对\(R\)的缺失内容进行填充,如,行(或列)的平均值。一旦得到密集矩阵,我们就可以用传统算法来计算其SVD。这种方法可行,但结果往往有很高的偏见。我们宁愿用另外一种方法,基于最小化问题。

替代方法

计算\(RR^T\)\(R^TR\)的特征向量并不是计算密集矩阵\(R\)的SVD的唯一方法。实际上,我们可以找到矩阵\(M\)\(U\),如果我们能找到所有满足如下条件的向量\(p_u\)\(q_i\)\(p_u\)组成\(M\)的行,\(q_i\)组成\(U^T\)的列):

  • 对所有的\(u\)\(i\)\(r_{ui}=p_u⋅q_i\)

  • 所有的向量\(p_u\)是相互正交的,所有的向量\(q_i\)也如此。

对所有的用户和项目,找出这种向量\(p_u\)\(q_i\)可通过解决下面的优化问题(同时遵循正交约束)来完成:

\(\begin{align*} min_{p_u, q_i\\p_u \perp p_v\\ q_i \perp q_j}\sum_{r_{ui} \in R} (r_{ui} - p_u \cdot q_i)^2 \end{align*}\)

它可被理解为,找到向量\(p_u\)\(q_i\)使得总和最小。也就是说,我们试图尽可能将\(r_{ui}\)的值与它们的实际值\(p_u⋅q_i\)相匹配。

我在这里滥用符号,将\(R\)既称为矩阵又称为一组评级。一旦我们知道使得这个总和最小(这里的最小值为零)的向量\(p_u\)\(q_i\)的值,我们可以重构\(M\)\(U\),从而得到SVD。

那么当\(R\)稀疏时,即当矩阵中某些评级缺失时,我们该怎么办? Simon Funk的答案是我们应该不要废弃。我们仍然解决同样的优化问题:

\(\begin{align*} \min_{p_u, q_i}\sum_{r_{ui} \in R} (r_{ui} - p_u \cdot q_i)^2. \end{align*}\)

唯一的区别是,这次,某些评级是缺失的,即\(R\)不完整。请注意,我们并没有将缺少的项目视为零:我们纯粹是忽略它们。此外,我们将会忘记正交性约束,因为即使它们对于解释有用,通常,限制向量也不能帮助我们获得更准确的预测。

Simon Funk因他的这一解决方案曾名列Netflix奖的前3名。他的算法被其他团队大量使用、研究和改进。

算法

这个优化问题不是凸的。也就是说,很难找到使得该总和最小的向量\(p_u\)\(q_i\)的值(并且最优解甚至可能不是唯一的)。然而,有大量技术可以找到近似解。我们将在这里使用SGD(Stochastic Gradient Descent,随机梯度下降)方法。

梯度下降是一种非常经典的技术,用于查找函数的最小值(有时是局部的)。如果你听说过用于训练神经网络的反向传播,那么backprop只是一种计算梯度的技术,它后来被用于梯度下降。SGD是梯度下降的十亿种变体之一。我们不会详细介绍SGD如何工作(在网上可以找到很多好的资源),但一般情况如下。

当带参数\(θ\)的函数\(f\)被表示如下时:

\(\begin{align*} f(\theta) = \sum_k f_k(\theta), \end{align*}\)

SGD过程通过下列步骤来最小化\(f\) (即,找到\(θ\)使得\(f(θ)\)尽可能小):

  1. 随机初始化\(θ\)
  2. 对于给定的次数,重复下面的步骤:
    • 对所有\(k\),重复下面的步骤:
      • 计算\(\frac{∂f_{k}}{∂θ}\)
      • 更新\(θ\)\(θ←θ+α⋅\frac{∂f_{k}}{∂θ}\),其中\(α\)是学习速率(一个很小的值)。

在我们的情况下,参数\(θ\)对应于所有的向量\(p_u\)\(q_i\)(我们将其表示为(\(p_∗\)\(q_*\))),而我们想最小化的函数\(f\)表示为

\(\begin{align*} f(p_*, q_*) = \sum_{r_{ui} \in R} (r_{ui} - p_u \cdot q_i)^2 =\sum_{r_{ui} \in R} f_{ui}(p_u, q_i), \end{align*}\)

其中\(f_{ui}\)被定义为\(f_{ui}(p_u, q_i) = (r_{ui} - p_u \cdot q_i)^2\)

因此,为了应用SGD,对任意\(p_u\)\(q_i\),我们需要找到导数\(f_{ui}\)的值。

  • 对于给定向量\(p_u\)\(f_{ui}\)的导数由下式给出:

\(\begin{align*} \frac{\partial f_{ui}}{\partial p_u} = \frac{\partial}{\partial p_u} (r_{ui} - p_u \cdot q_i)^2 = - 2 q_i (r_{ui} - p_u \cdot q_i) \end{align*}\)

  • 对称地,对于给定向量\(q_i\)\(f_{ui}\)的导数由下式给出:

\(\begin{align*} \frac{\partial f_{ui}}{\partial q_i} = \frac{\partial}{\partial q_i} (r_{ui} - p_u \cdot q_i)^2 = - 2 p_u (r_{ui} - p_u \cdot q_i) \end{align*}\)

那么,SGD过程就变为:

  1. 随机初始化所有的向量\(p_u\)\(q_i\)
  2. 对给定的次数(如,迭代数),重复下面的步骤:
    • 对所有已知的评级\(r_{ui}\),重复下面的步骤:
      • 计算\(\frac{∂f_{ui}}{∂p_u}\)\(\frac{∂{f_{ui}}}{∂q_i}\)
      • 更新\(p_u\)\(q_i\)\(p_u←p_u+α⋅q_i(r_{ui}−p_u ⋅q_i)\)\(q_i←q_i+α⋅p_u(r_{ui}−p_u⋅q_i)\) 。这里我们避免了乘以常数2,而是把它合并到了学习速率\(α\)中。

注意在这个算法中,\(p_u\)(和\(q_i\))中的不同因子都是同时更新的。Funk的原始算法有些不同:他实际上训练了第一个因子,然后是第二个因子,然后是第三个,等等,这使得他的算法更加符合SVD风格。关于这一点,可以在Aggarwal的关于推荐系统的教科书中找到很好的讨论。

一旦计算了所有的\(p_u\)\(q_i\)向量,我们可以用下面的公式来预测评级:

\(\begin{align*} \hat{r}_{ui}=p_u⋅q_i. \end{align*}\)

\(\hat{r}_{ui}\)是一个估计值,而不是实际值。

降维

在进入Python算法实现部分之前,我们还要决定一个事情:向量\(p_u\)\(q_i\)的大小应是多少?我们很确认的是,所有向量\(p_u\)\(q_i\)的长度要相同,否则我们不能进行点积运算。

为了回答这个问题,我们回顾一下第一部分里的PCA和令人毛骨悚然的图像。那些令人毛骨悚然的图像能重构所有的原始脸部图像。

\(\begin{align*} \text{Face 1}~=~&\alpha_1 \cdot \color{#048BA8}{\text{Creepy guy #1}}\\ +~ &\alpha_2 \cdot \color{#048BA8}{\text{Creepy guy #2}}\\ +~ &\cdots\\ +~ &\alpha_{400} \cdot \color{#048BA8}{\text{Creepy guy #400}} \end{align*}\)

\(\begin{align*} \text{Face 2}~=~&\beta_1 \cdot \color{#048BA8}{\text{Creepy guy #1}}\\ +~ &\beta_2 \cdot \color{#048BA8}{\text{Creepy guy #2}}\\ +~ &\cdots\\ +~ &\beta_{400} \cdot \color{#048BA8}{\text{Creepy guy #400}} \end{align*}\)

但是,实际上,我们不需要用到所有的特征图像来很好地近似每一个原始脸部。我实际上撒谎了:第一部分里你所看到的动态图只用到了前200个令人毛骨悚然的图像(而不是400个)。但你看不出区别,是吧?为了进一步说明这一点,这里是第一个原始脸部的重建,用到了1到400个令人毛骨悚然的图像,每次增加40个令人毛骨悚然的图像进入重建。

最后一张图片是最完美的重建,你可以看到,仅仅用80个特征图像(第三个图片)就足够识别原始的脸部。

你可能想知道为什么第一张照片看起来不像第一个令人毛骨悚然的图像,为什么令人毛骨悚然的图像比原始图片有更高的对比度:这是因为PCA首先减去所有图像的平均值。你看到的第一张图像实际上对应于第一个令人毛骨悚然的图像加上平均脸的贡献。如果这对你没有意义,不用担心。我们在做推荐时并不在乎这些。

所以这里给出的信息是:你不需要所有的令人毛骨悚然的或典型的图像来很好地近似初始矩阵。SVD和推荐问题同样如此:你不需要所有典型的电影或所有典型的用户来获得良好的近似值

这意味着,当我们把用户和项目表示为下面的形式时(SVD能给出这些):

\(\begin{align*} % <![CDATA[ \begin{array}{ll} \text{Alice} & = 10\% \color{#048BA8}{\text{ 动作电影迷}} &+ 10\% \color{#048BA8}{\text{ 喜剧电影迷}} &+ 50\% \color{#048BA8}{\text{ 浪漫电影迷}} &+\cdots\\ \text{Bob} &= 50\% \color{#048BA8}{\text{ 动作电影迷}}& + 30\% \color{#048BA8}{\text{ 喜剧电影迷}} &+ 10\% \color{#048BA8}{\text{ 浪漫电影迷}} &+\cdots\\ \text{Titanic} &= 20\% \color{#048BA8}{\text{ 动作片}}& + 00\% \color{#048BA8}{\text{ 喜剧片}} &+ 70\% \color{#048BA8}{\text{ 浪漫片}} &+\cdots\\ \text{Toy Story} &= 30\% \color{#048BA8}{\text{ 动作片 }} &+ 60\% \color{#048BA8}{\text{ 喜剧片}}&+ 00\% \color{#048BA8}{\text{ 浪漫片}} &+\cdots\\ \end{array} %]]> \end{align*}\)

我们可以用一小部分典型的电影或用户得到一个良好的解决方案。这里,我们将把\(p_u\)\(q_i\)的大小限制在10。也就是说,我们将只考虑10个潜在因子。

你有权对此持怀疑态度,但实际上我们对这个近似值有很好的保证。关于SVD(和PCA)的一个奇妙的结果是,当我们仅使用k个因子时,我们获得原始矩阵的最佳低阶近似(了解少数因子)。尽管细节很有趣,答案是有点技术性的,超出了本文的范围(虽然很有趣),所以我推荐斯坦福大学课程笔记(事实4.2)。(注意:在课程笔记的第5节中,作者提出了一种从SVD中恢复缺失条目的方法,这种启发式技术是我们上面提到的技术,但它不是最有效的方法。推荐中最有效的方法是优化已知的评级,正如我们用SGD所做的那样)。

第四部分:算法实现

在前一部分,我们已经描述了如何用随机梯度下降方法寻找SVD问题的近似解。其步骤如下:

  1. 随机初始化所有的向量\(p_u\)\(q_i\),每个向量长度为10。
  2. 对给定的次数(如,迭代数),重复下面的步骤:
    • 对所有已知的评级\(r_{ui}\),重复下面的步骤:
      • 计算\(\frac{∂f_{ui}}{∂p_u}\)\(\frac{∂{f_{ui}}}{∂q_i}\)
      • 更新\(p_u\)\(q_i\)\(p_u←p_u+α⋅q_i(r_{ui}−p_u ⋅q_i)\)\(q_i←q_i+α⋅p_u(r_{ui}−p_u⋅q_i)\)。这里我们避免了乘以常数2,而是把它合并到了学习速率\(α\)中。

下面是该算法的Python实现。

def SGD(data):
    '''Learn the vectors p_u and q_i with SGD.
       data is a dataset containing all ratings + some useful info (e.g. number
       of items/users).
    '''
    n_factors = 10  # number of factors
    alpha = .01  # learning rate
    n_epochs = 10  # number of iteration of the SGD procedure
    # Randomly initialize the user and item factors.
    p = np.random.normal(0, .1, (data.n_users, n_factors))
    q = np.random.normal(0, .1, (data.n_items, n_factors))
    # Optimization procedure
    for _ in range(n_epochs):
        for u, i, r_ui in data.all_ratings():
            err = r_ui - np.dot(p[u], q[i])
            # Update vectors p_u and q_i
            p[u] += alpha * err * q[i]
            q[i] += alpha * err * p[u]

 一共只有10行代码,非常简洁。

一旦我们运行SGD过程,就可以估计所有的向量\(p_u\)\(q_i\)的点积来预测所有的评级:

def estimate(u, i):
    '''Estimate rating of user u for item i.'''
    return np.dot(p[u], q[i])

就这样,我们的任务完成了。想要自己尝试吗? 我做了一个小的ipython notebook,可以在一个实际的数据集上运行该算法。我们将用到surprise库,它是一个很好的库,用于快速实施评级预测算法(但我可能会带有偏见,因为我是surprise的主要开发人员)。要使用它,你只需要先使用pip来安装它:

pip install scikit-surprise

我们的算法性能如何?

正如你在notebook上可以看到的,我们得到约0.98的RMSE值。RMSE表示均方根误差,计算公式如下:

\(\begin{align*} \text{RMSE} = \sqrt{\sum_{u,i} (\hat{r}_{ui} - r_{ui})^2}. \end{align*}\)

你可以将其视为某种平均错误,其中大的错误被严重处罚(因为它们是平方的)。RMSE为0意味着所有的预测都是完美的。RMSE为0.5意味着,平均而言,每个预测值偏离实际值0.5。

那么0.98是一个好的RMSE?其实真的不是那么糟糕。如该notebook所示,某邻域算法只能达到1的RMSE。一个更复杂的MF算法(与我们的算法相同,除了评级是无偏的和我们使用了正则化)可以达到约0.96的RMSE。这个算法在文献中被称为“SVD”,但现在你知道它不能是一个真正的SVD,因为有缺失的评级。它只是(大部分)灵感来自SVD。

总结

我希望你现在可以了解到PCA和SVD之美,以及如何调整SVD用于解决推荐问题。如果你想体验一下surprise库,文档中有很多很酷的东西。

如果你发现本文有用(或没用),请在我的博客的评论中告诉我。文中的任何错误都是我自己的,非常感谢任何形式的批评。

感谢Pierre Poulain对本文的宝贵意见。

参考文献

本文中,我试图避免理论考虑,而提供可视化的具体例子来说明如何使用PCA和SVD。但理论方面不容忽视,它其实很有趣。如果你想进一步深入了解到目前为止涵盖的各种主题,可能需要阅读一些资源:

  • Jeremy KunPCASVD上的帖子很棒。这篇文章的第一部分受到这两个帖子的启发。
  • 我个人认为,Aggarwal关于推荐系统的教科书是最好的推荐系统资源。你可以找到关于各种矩阵分解变体的细节,其中涵盖了大量其他科目。
  • 如果你想更多地了解“SVD”算法及其可能的扩展,请从BellKor团队查看文章。他们获得了100万美元的Netflix奖金。
  • PCA可以用于很多有趣的东西,而不仅仅是乱七八糟的脸部。Jonathon Shlens的教程提供了对PCA作为对角化过程的深刻见解,并提供了与SVD的链接。此外,斯坦福大学课程笔记涵盖了我们提出的一些主题(低等级近似,等),更加以理论为导向。
  • 对于线性代数,唯一值得阅读的书是Gilbert Strang的“LA简介”。他的麻省理工学院课程的技术含金量也很高。
  • 当然,Surprise是一个很好的推荐系统库(但是我也可能带有偏见)。

查看英文原文:Understanding matrix factorization for recommendation


感谢蔡芳芳对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT