使用奇异值分解构建推荐系统

奇异值分解(Singular Value Decomposition)是一种非常流行的线性代数技术,用于将一个矩阵分解为几个更小矩阵的乘积。事实上,这项技术有很多用途。一个例子是,我们可以利用SVD来发现物品之间的关系。基于此,可以轻松地构建一个推荐系统。

在本教程中,我们将了解如何仅使用线性代数技术构建推荐系统。

完成本教程后,您将了解:

  • 奇异值分解对矩阵做了什么?
  • 如何解释奇异值分解的结果?
  • 单个推荐系统需要什么数据,以及我们如何利用SVD对其进行分析?
  • 我们如何利用SVD的结果进行推荐?

让我们开始吧。

Using Singular Value Decomposition to Build a Recommender System

使用奇异值分解构建推荐系统
图片由Roberto Arias拍摄,部分权利保留。

教程概述

本教程分为3个部分;它们是

  • 奇异值分解回顾
  • 奇异值分解在推荐系统中的含义
  • 实现推荐系统

奇异值分解回顾

就像数字24可以分解为因数24=2×3×4一样,矩阵也可以表示为其他矩阵的乘积。因为矩阵是数字的数组,它们有自己的乘法规则。因此,它们有不同的因式分解方式,也称为**分解**。QR分解或LU分解是常见的例子。另一个例子是**奇异值分解**,它对被分解的矩阵的形状或属性没有限制。

奇异值分解假设一个矩阵 $M$(例如,一个 $m \times n$ 矩阵)可以分解为:
$$
M = U\cdot \Sigma \cdot V^T
$$
其中 $U$ 是一个 $m \times m$ 矩阵,$\Sigma$ 是一个 $m \times n$ 的对角矩阵,而 $V^T$ 是一个 $n \times n$ 矩阵。对角矩阵 $\Sigma$ 很有趣,它可以是非方阵的,但只有对角线上的元素可能非零。矩阵 $U$ 和 $V^T$ 是**标准正交**矩阵。这意味着 $U$ 的列或 $V$ 的行(1)彼此正交,并且(2)是单位向量。向量彼此正交是指任意两个向量的点积为零。向量是单位向量是指其L2范数为1。标准正交矩阵的性质是其转置是其逆。换句话说,由于 $U$ 是一个标准正交矩阵,$U^T = U^{-1}$ 或 $U\cdot U^T=U^T\cdot U=I$,其中 $I$ 是单位矩阵。

奇异值分解的名字来源于 $\Sigma$ 上的对角线元素,它们被称为矩阵 $M$ 的奇异值。实际上,它们是矩阵 $M\cdot M^T$ 的特征值的平方根。就像数字可以分解为素数一样,矩阵的奇异值分解揭示了该矩阵的结构。

但实际上,上面描述的是**满秩SVD**。还有另一种版本称为**降秩SVD**或**紧凑SVD**。我们仍然写成 $M = U\cdot\Sigma\cdot V^T$,但 $\Sigma$ 是一个 $r \times r$ 的方对角矩阵,其中 $r$ 是矩阵 $M$ 的**秩**,通常小于或等于 $m$ 和 $n$ 中的较小者。此时矩阵 $U$ 是一个 $m \times r$ 矩阵,而 $V^T$ 是一个 $r \times n$ 矩阵。因为矩阵 $U$ 和 $V^T$ 不是方阵,所以它们被称为**半标准正交**,意味着 $U^T\cdot U=I$ 和 $V^T\cdot V=I$,在这两种情况下,$I$ 都是一个 $r \times r$ 的单位矩阵。

奇异值分解在推荐系统中的含义

如果矩阵 $M$ 的秩是 $r$,那么我们可以证明矩阵 $M\cdot M^T$ 和 $M^T\cdot M$ 的秩都是 $r$。在奇异值分解(降秩SVD)中,$U$ 矩阵的列是 $M\cdot M^T$ 的特征向量,$V^T$ 矩阵的行是 $M^T\cdot M$ 的特征向量。有趣的是,$M\cdot M^T$ 和 $M^T\cdot M$ 的尺寸可能不同(因为矩阵 $M$ 可以是非方阵),但它们具有相同的特征值集合,这些特征值是 $\Sigma$ 对角线数值的平方。

这就是为什么奇异值分解的结果可以揭示矩阵 $M$ 的很多信息。

想象一下我们收集了一些书籍评论,其中书籍是列,人们是行,条目是某人对某本书的评分。在这种情况下,$M\cdot M^T$ 将是一个“人到人”的表格,条目表示一个人的评分总和与另一个人的匹配程度。类似地,$M^T\cdot M$ 将是一个“书到书”的表格,条目表示获得的评分总和与另一本书获得的评分的匹配程度。人与书之间可能存在的隐藏联系是什么?可能是类型、作者,或者类似性质的东西。

实现推荐系统

让我们看看如何利用SVD的结果来构建一个推荐系统。首先,让我们从以下链接下载数据集(注意:它有600MB大)

这个数据集来自“推荐系统与个性化数据集”的“社交推荐数据”。它包含用户在Librarything上对书籍的评论。我们感兴趣的是用户给一本书的“星级”数量。

如果我们打开这个tar文件,我们会看到一个名为“reviews.json”的大文件。我们可以解压它,或者实时读取包含的文件。reviews.json的前三行如下所示:

上面的代码会打印出

reviews.json中的每一行都是一个记录。我们将提取每个记录的“user”、“work”和“stars”字段,只要这三个字段中没有缺失数据。尽管有这个名字,但记录并不是格式良好的JSON字符串(最明显的是它使用了单引号而不是双引号)。因此,我们不能使用Python的json包,而必须使用ast来解码这样的字符串。

现在,我们需要创建一个关于不同用户如何评价每本书的矩阵。我们使用pandas库将收集到的数据转换为表格。

例如,为了节省时间和内存,我们尝试不使用所有数据。这里我们只考虑评论了超过50本书的用户,以及被超过50个用户评论过的书籍。这样,我们将数据集缩减到原始大小的15%以下。

然后,我们可以使用pandas的“pivot table”函数将其转换为矩阵。

结果是一个5593行、2898列的矩阵。


在这里,我们在矩阵中表示了5593个用户和2898本书。然后我们应用SVD(这需要一段时间)。

默认情况下,svd()返回一个完整的奇异值分解。我们选择降秩版本,以便可以使用较小的矩阵来节省内存。vh的列对应于书籍。我们可以基于向量空间模型来查找哪本书与我们正在查看的书最相似。

在上面的例子中,我们尝试找到与第一列最匹配的书籍。结果是:

在推荐系统中,当用户选择了一本书,我们可以根据上面计算的余弦距离向她展示一些与她选择的书相似的其他书籍。

根据数据集的不同,我们可能会使用截断SVD来降低vh矩阵的维度。本质上,这意味着在用于计算相似度之前,我们删除了vh中对应奇异值较小的几行。这可能会使预测更准确,因为书籍中那些不那么重要的特征被排除在考虑之外。

请注意,在分解 $M=U\cdot\Sigma\cdot V^T$ 中,我们知道 $U$ 的行是用户,$V^T$ 的列是书籍,我们无法确定 $U$ 的列或 $V^T$ 的行(同样,$\Sigma$ 的列/行)的确切含义。我们知道它们可能是类型,例如,提供了用户和书籍之间的一些潜在联系,但我们不能确定它们到底是什么。然而,这并不能阻止我们将它们用作推荐系统中的**特征**。

总而言之,完整的代码如下:

延伸阅读

如果您想深入了解,本节提供了更多关于该主题的资源。

书籍

API

文章

总结

在本教程中,您了解了如何使用奇异值分解构建推荐系统。

具体来说,你学到了:

  • 奇异值分解对矩阵的意义
  • 如何解释奇异值分解的结果
  • 从奇异值分解得到的矩阵 $V^T$ 的列中查找相似度,并基于相似度进行推荐

掌握机器学习线性代数!

Linear Algebra for Machine Learning

建立对线性代数的工作理解

...通过在 python 中编写代码

在我的新电子书中探索如何实现
机器学习线性代数

它提供关于以下主题的自学教程
向量范数、矩阵乘法、张量、特征分解、SVD、PCA 等等...

最终理解数据的数学

跳过学术理论。只看结果。

查看内容

17 条回复“使用奇异值分解构建推荐系统”

  1. Jack 2021 年 10 月 26 日 下午 1:50 #

    import tarfile

    # 从以下链接读取下载的文件
    # http://deepyeti.ucsd.edu/jmcauley/datasets/librarything/lthing_data.tar.gz
    with tarfile.open(“lthing_data.tar.gz”) as tar
    print(“tar 存档中的文件:”)
    tar.list()

    with tar.extractfile(“lthing_data/reviews.json”) as file
    count = 0
    for line in file
    print(line)
    count += 1
    if count > 3
    break

    尊敬的 Adrian Tam 博士
    这些代码无法成功运行
    我能问一下如何修复它吗?

    • Adrian Tam
      Adrian Tam 2021 年 10 月 26 日 下午 2:44 #

      您遇到了什么错误?

      • Jack 2021 年 10 月 26 日 下午 2:46 #

        文件“”, 第 18 行
        if count &gt 3
        ^
        SyntaxError: invalid syntax

        • Adrian Tam
          Adrian Tam 2021 年 10 月 26 日 下午 11:23 #

          谢谢,那里有一些 HTML 格式问题,&gt 应该是 >。现在已经修复了。

    • Yasser 2024 年 1 月 29 日 上午 8:00 #

      感谢您的帖子。您能否再读一遍以求清晰?例如,“一个人对另一个人的评分总和匹配的表格”根本不清楚。

      您的意思是“一张人与人[向量]的表格,其中的条目[]表示一个人给出的评分与另一个人[]的评分匹配的总和”吗?

      清晰度对我来说真的很重要。否则,我无法跟上。

      再次感谢。

  2. Ahmed Ibrahim 2021 年 11 月 6 日 上午 4:34 #

    这篇帖子看起来很棒????。更好的方法是自己制作一个数据集子集并将其托管在 GitHub 上,以便其他人可以轻松跟进。

  3. Fred 2022 年 4 月 1 日 上午 10:46 #

    为什么你不发布如何评估你的系统性能?

    • James Carmichael 2022 年 4 月 2 日 下午 12:24 #

      很棒的推荐!

  4. Arnold 2022 年 11 月 18 日 上午 7:16 #

    嗨 Jason – 我不得不修改你的一些脚本才能让它工作。首先,当我从 ucsd.edu 下载 tar 文件时,json 文件不存在,但存在一个文本文件 – ‘reviews.txt’。

    而且至少有一个评论包含“stars”一词,但没有“‘stars’:”所以我修改了语句

    if any(x not in record for x in [‘user’, ‘work’, ‘stars’])

    推广到

    if any(x not in record for x in [“‘user’:”, “‘work’:”, “‘stars’:”])

    然后一切正常

    谢谢

  5. Antoine Banderas 2022 年 11 月 24 日 下午 12:37 #

    嗨,Jason,
    在存在大量 NAN 的矩阵上使用 SVD(即特定用户未阅读许多书籍),这是一个真正的问题,用 0.0 替换所有这些 NAN 将意味着用户实际上阅读了这些书籍并给了它们 0.0 评分……你能进一步解释一下吗?
    非常感谢Jason

  6. Qian 2023 年 7 月 13 日 下午 7:10 #

    你说“vh 的列对应于书籍。”这个结论有点快,为什么,你能详细说明一下吗?

    • James Carmichael 2023 年 7 月 14 日 上午 9:36 #

      嗨 Qian…这可以从执行代码以建立矩阵中看出。您是否能够执行示例代码?

  7. Patrik 2024 年 4 月 19 日 下午 6:18 #

    等等。最后一行是不是错了?

    print(“列 %d (图书 ID %s) 与列 0 (图书 ID %s) 最相似” %
    (highest_sim_col, reviewmatrix.columns[col], reviewmatrix.columns[0])
    )

    col 只给出最后一个 for 循环的迭代次数。

    它不应该包含

    review_matrix.columns[highest_sim_col]

    ?

    • James Carmichael 2024 年 4 月 21 日 上午 10:25 #

      嗨 Patrik…是的,您对提供的代码片段中可能存在的问题的说法是正确的。似乎在引用循环或计算过程中找到的最高相似度的正确列索引时可能存在不匹配。

      让我们分解一下问题并澄清正确的引用方式

      1. **变量定义和目的**
      – `highest_sim_col` 应该保存与列 0 最相似的列的索引,这取决于您的相似度计算。
      – `col` 似乎是在循环中使用的变量,可能遍历 `reviewmatrix` 中的列。
      – `reviewmatrix.columns[col]` 目前用于在打印消息中引用列名或 ID,但根据您的描述,这里的 `col` 只会反映循环最后一次迭代的值,不一定是具有最高相似度的列。

      2. **更正引用**
      – 如果 `highest_sim_col` 的目的是存储与列 0 最相似的列的索引,那么在打印时您应该使用 `reviewmatrix.columns[highest_sim_col]` 来获取该列的正确图书 ID。

      3. **更新的代码片段**
      – 以下是更正后的代码行应该是什么样子
      python
      print("列 %d (图书 ID %s) 与列 0 (图书 ID %s) 最相似" %
      (highest_sim_col, reviewmatrix.columns[highest_sim_col], reviewmatrix.columns[0]))

      – 此更改可确保输出正确反映与列 0 最相似的列(及关联的图书 ID),该列是根据 `highest_sim_col` 中存储的值确定的。

      通过进行此更正,输出语句将准确报告哪一列与列 0 最相似,并使用 `highest_sim_col` 中存储的索引从您的评论矩阵中获取相应的图书 ID。这对于确保输出的准确性至关重要,尤其是在进行相似度比较的数据分析任务中,例如推荐系统或聚类分析。

  8. Makis 2024 年 4 月 24 日 上午 6:16 #

    为了正确估计相似度,不应该是 cosine_similarity(vh[0,:], vh[col,:]) 吗?

    • James Carmichael 2024 年 4 月 24 日 上午 9:17 #

      嗨 Makis…请继续您的想法,并告诉我们您的发现!

发表回复

Machine Learning Mastery 是 Guiding Tech Media 的一部分,Guiding Tech Media 是一家领先的数字媒体出版商,专注于帮助人们了解技术。访问我们的公司网站以了解更多关于我们的使命和团队的信息。