第八章:协同过滤深入探讨
原文:
www.bookstack.cn/read/th-fastai-book/d4afd9df315bb076.md
译者:飞龙
解决的一个常见问题是有一定数量的用户和产品,您想推荐哪些产品最有可能对哪些用户有用。存在许多变体:例如,推荐电影(如 Netflix 上),确定在主页上为用户突出显示什么,决定在社交媒体动态中显示什么故事等。解决这个问题的一般方法称为协同过滤,工作原理如下:查看当前用户使用或喜欢的产品,找到其他使用或喜欢类似产品的用户,然后推荐那些用户使用或喜欢的其他产品。
例如,在 Netflix 上,您可能观看了很多科幻、充满动作并且是上世纪 70 年代制作的电影。Netflix 可能不知道您观看的这些电影的特定属性,但它将能够看到观看了与您观看相同电影的其他人也倾向于观看其他科幻、充满动作并且是上世纪 70 年代制作的电影。换句话说,要使用这种方法,我们不一定需要了解电影的任何信息,只需要知道谁喜欢观看它们。
这种方法可以解决更一般的一类问题,不一定涉及用户和产品。实际上,在协同过滤中,我们更常用项目这个术语,而不是产品。项目可以是人们点击的链接、为患者选择的诊断等。
关键的基础概念是潜在因素。在 Netflix 的例子中,我们假设您喜欢老式、充满动作的科幻电影。但您从未告诉 Netflix 您喜欢这类电影。Netflix 也不需要在其电影表中添加列,说明哪些电影属于这些类型。尽管如此,必须存在一些关于科幻、动作和电影年龄的潜在概念,这些概念对于至少一些人的电影观看决策是相关的。
在本章中,我们将解决这个电影推荐问题。我们将从获取适合协同过滤模型的一些数据开始。
数据初探
我们无法访问 Netflix 的完整电影观看历史数据集,但有一个很好的数据集可供我们使用,称为MovieLens。该数据集包含数千万部电影排名(电影 ID、用户 ID 和数字评分的组合),尽管我们只会使用其中的 10 万部作为示例。如果您感兴趣,可以尝试在完整的 2500 万推荐数据集上复制这种方法,您可以从他们的网站上获取。
该数据集可通过通常的 fastai 函数获得:
from fastai.collab import * from fastai.tabular.all import * path = untar_data(URLs.ML_100k)
根据README,主表位于文件u.data中。它是以制表符分隔的,列分别是用户、电影、评分和时间戳。由于这些名称没有编码,我们需要在使用 Pandas 读取文件时指定它们。以下是打开此表并查看的方法:
ratings = pd.read_csv(path/'u.data', delimiter='\t', header=None, names=['user','movie','rating','timestamp']) ratings.head()
用户 | 电影 | 评分 | 时间戳 | |
0 | 196 | 242 | 3 | 881250949 |
1 | 186 | 302 | 3 | 891717742 |
2 | 22 | 377 | 1 | 878887116 |
3 | 244 | 51 | 2 | 880606923 |
4 | 166 | 346 | 1 | 886397596 |
尽管这包含了我们需要的所有信息,但这并不是人类查看这些数据的特别有用的方式。图 8-1 将相同数据交叉制表成了一个人类友好的表格。
图 8-1. 电影和用户的交叉表
我们只选择了一些最受欢迎的电影和观看电影最多的用户,作为这个交叉表示例。这个表格中的空单元格是我们希望我们的模型学会填充的内容。这些是用户尚未评论电影的地方,可能是因为他们还没有观看。对于每个用户,我们希望找出他们最有可能喜欢哪些电影。
如果我们知道每个用户对电影可能属于的每个重要类别的喜好程度,比如流派、年龄、喜欢的导演和演员等,以及我们对每部电影的相同信息,那么填写这个表格的一个简单方法是将这些信息相乘,然后使用组合。例如,假设这些因子的范围在-1 到+1 之间,正数表示更强的匹配,负数表示更弱的匹配,类别是科幻、动作和老电影,那么我们可以表示电影《最后的绝地武士》如下:
last_skywalker = np.array([0.98,0.9,-0.9])
在这里,例如,我们将非常科幻评分为 0.98,非常不老评分为-0.9。我们可以表示喜欢现代科幻动作电影的用户如下:
user1 = np.array([0.9,0.8,-0.6])
现在我们可以计算这种组合之间的匹配:
(user1*last_skywalker).sum()
2.1420000000000003
当我们将两个向量相乘并将结果相加时,这被称为点积。它在机器学习中被广泛使用,并构成了矩阵乘法的基础。我们将在第十七章中更多地研究矩阵乘法和点积。
术语:点积
将两个向量的元素相乘,然后将结果相加的数学运算。
另一方面,我们可以表示电影《卡萨布兰卡》如下:
casablanca = np.array([-0.99,-0.3,0.8])
这种组合之间的匹配如下所示:
(user1*casablanca).sum()
-1.611
由于我们不知道潜在因子是什么,也不知道如何为每个用户和电影评分,我们应该学习它们。
学习潜在因子
在指定模型的结构和学习模型之间,实际上几乎没有什么区别,因为我们可以使用我们的一般梯度下降方法。
这种方法的第一步是随机初始化一些参数。这些参数将是每个用户和电影的一组潜在因子。我们将不得不决定要使用多少个。我们将很快讨论如何选择这些,但为了说明,让我们现在使用 5 个。因为每个用户将有一组这些因子,每部电影也将有一组这些因子,我们可以在交叉表中的用户和电影旁边显示这些随机初始化的值,然后我们可以填写这些组合的点积。例如,图 8-2 显示了在 Microsoft Excel 中的样子,顶部左侧的单元格公式显示为示例。
这种方法的第二步是计算我们的预测。正如我们讨论过的,我们可以通过简单地将每部电影与每个用户进行点积来实现这一点。例如,如果第一个潜在用户因子代表用户喜欢动作电影的程度,第一个潜在电影因子代表电影是否有很多动作,那么如果用户喜欢动作电影并且电影中有很多动作,或者用户不喜欢动作电影并且电影中没有任何动作,这两者的乘积将特别高。另一方面,如果存在不匹配(用户喜欢动作电影但电影不是动作片,或者用户不喜欢动作电影但电影是动作片),乘积将非常低。
图 8-2. 交叉表中的潜在因子
第三步是计算我们的损失。我们可以使用任何损失函数,让我们现在选择均方误差,因为这是一种合理的表示预测准确性的方法。
这就是我们需要的全部内容。有了这个,我们可以使用随机梯度下降来优化我们的参数(潜在因素),以最小化损失。在每一步中,随机梯度下降优化器将使用点积计算每部电影与每个用户之间的匹配,并将其与每个用户给出的每部电影的实际评分进行比较。然后它将计算这个值的导数,并通过学习率乘以这个值来调整权重。经过多次这样的操作,损失会变得越来越好,推荐也会变得越来越好。
要使用通常的Learner.fit
函数,我们需要将我们的数据放入DataLoaders
中,所以让我们现在专注于这一点。
创建 DataLoaders
在展示数据时,我们宁愿看到电影标题而不是它们的 ID。表u.item
包含 ID 与标题的对应关系:
movies = pd.read_csv(path/'u.item', delimiter='|', encoding='latin-1', usecols=(0,1), names=('movie','title'), header=None) movies.head()
电影 | 标题 | |
0 | 1 | 玩具总动员(1995) |
1 | 2 | 黄金眼(1995) |
2 | 3 | 四个房间(1995) |
3 | 4 | 短小(1995) |
4 | 5 | 复制猫(1995) |
我们可以将这个表与我们的ratings
表合并,以获得按标题分类的用户评分:
ratings = ratings.merge(movies) ratings.head()
用户 | 电影 | 评分 | 时间戳 | 标题 | |
0 | 196 | 242 | 3 | 881250949 | 科洛亚(1996) |
1 | 63 | 242 | 3 | 875747190 | 科洛亚(1996) |
2 | 226 | 242 | 5 | 883888671 | 科洛亚(1996) |
3 | 154 | 242 | 3 | 879138235 | 科洛亚(1996) |
4 | 306 | 242 | 5 | 876503793 | 科洛亚(1996) |
然后我们可以从这个表构建一个DataLoaders
对象。默认情况下,它将使用第一列作为用户,第二列作为项目(这里是我们的电影),第三列作为评分。在我们的情况下,我们需要更改item_name
的值,以使用标题而不是 ID:
dls = CollabDataLoaders.from_df(ratings, item_name='title', bs=64) dls.show_batch()
用户 | 标题 | 评分 | |
0 | 207 | 四个婚礼和一个葬礼(1994) | 3 |
1 | 565 | 日残余(1993) | 5 |
2 | 506 | 小孩(1995) | 1 |
3 | 845 | 追求艾米(1997) | 3 |
4 | 798 | 人类(1993) | 2 |
5 | 500 | 低俗法则(1986) | 4 |
6 | 409 | 无事生非(1993) | 3 |
7 | 721 | 勇敢的心(1995) | 5 |
8 | 316 | 精神病患者(1960) | 2 |
9 | 883 | 判决之夜(1993) | 5 |
为了在 PyTorch 中表示协同过滤,我们不能直接使用交叉表表示,特别是如果我们希望它适应我们的深度学习框架。我们可以将我们的电影和用户潜在因素表表示为简单的矩阵:
n_users = len(dls.classes['user']) n_movies = len(dls.classes['title']) n_factors = 5 user_factors = torch.randn(n_users, n_factors) movie_factors = torch.randn(n_movies, n_factors)
要计算特定电影和用户组合的结果,我们必须查找电影在我们的电影潜在因素矩阵中的索引,以及用户在我们的用户潜在因素矩阵中的索引;然后我们可以在两个潜在因素向量之间进行点积。但查找索引不是我们的深度学习模型知道如何执行的操作。它们知道如何执行矩阵乘积和激活函数。
幸运的是,我们可以将查找索引表示为矩阵乘积。技巧是用单热编码向量替换我们的索引。这是一个例子,展示了如果我们将一个向量乘以一个表示索引 3 的单热编码向量会发生什么:
one_hot_3 = one_hot(3, n_users).float() user_factors.t() @ one_hot_3
tensor([-0.4586, -0.9915, -0.4052, -0.3621, -0.5908])
它给我们的结果与矩阵中索引 3 处的向量相同:
user_factors[3]
tensor([-0.4586, -0.9915, -0.4052, -0.3621, -0.5908])
如果我们一次为几个索引这样做,我们将得到一个独热编码向量的矩阵,这个操作将是一个矩阵乘法!这将是使用这种架构构建模型的一种完全可接受的方式,只是它会比必要的使用更多的内存和时间。我们知道没有真正的基础原因来存储独热编码向量,或者通过搜索找到数字 1 的出现 - 我们应该能够直接使用整数索引到数组中。因此,大多数深度学习库,包括 PyTorch,都包括一个特殊的层,它就是这样做的;它使用整数索引到一个向量中,但其导数的计算方式使其与使用独热编码向量进行矩阵乘法时完全相同。这被称为嵌入。
术语:嵌入
通过一个独热编码矩阵相乘,使用计算快捷方式,可以通过直接索引来实现。这是一个非常简单概念的相当花哨的词。您将独热编码矩阵相乘的东西(或者使用计算快捷方式,直接索引)称为嵌入矩阵。
在计算机视觉中,我们有一种非常简单的方法通过其 RGB 值获取像素的所有信息:彩色图像中的每个像素由三个数字表示。这三个数字给我们红色、绿色和蓝色,这足以让我们的模型在之后工作。
对于手头的问题,我们没有同样简单的方法来描述用户或电影。可能与流派有关:如果给定用户喜欢爱情片,他们可能会给爱情片更高的评分。其他因素可能是电影是更注重动作还是对话,或者是否有一个特定的演员,用户可能特别喜欢。
我们如何确定用来描述这些数字的数字?答案是,我们不确定。我们将让我们的模型学习它们。通过分析用户和电影之间的现有关系,我们的模型可以自己找出看起来重要或不重要的特征。
这就是嵌入。我们将为我们的每个用户和每个电影分配一个特定长度的随机向量(这里,n_factors=5
),并将使它们成为可学习的参数。这意味着在每一步,当我们通过比较我们的预测和目标来计算损失时,我们将计算损失相对于这些嵌入向量的梯度,并根据 SGD(或其他优化器)的规则更新它们。
一开始,这些数字没有任何意义,因为我们是随机选择的,但在训练结束时,它们将有意义。通过学习关于用户和电影之间关系的现有数据,没有任何其他信息,我们将看到它们仍然获得一些重要特征,并且可以将大片与独立电影、动作片与爱情片等区分开来。
我们现在有能力从头开始创建我们的整个模型。
从头开始协同过滤
在我们可以用 PyTorch 编写模型之前,我们首先需要学习面向对象编程和 Python 的基础知识。如果您以前没有进行过面向对象编程,我们将在这里为您进行快速介绍,但我们建议您在继续之前查阅教程并进行一些练习。
面向对象编程中的关键思想是类。我们在本书中一直在使用类,比如DataLoader
、String
和Learner
。Python 还让我们很容易地创建新类。这是一个简单类的示例:
class Example: def __init__(self, a): self.a = a def say(self,x): return f'Hello {self.a}, {x}.'
这其中最重要的部分是一个特殊的方法叫做__init__
(发音为dunder init)。在 Python 中,任何像这样用双下划线包围的方法都被认为是特殊的。它表示与这个方法名称相关联一些额外的行为。对于__init__
,这是 Python 在创建新对象时将调用的方法。因此,这是你可以在对象创建时设置任何需要初始化的状态的地方。当用户构造类的实例时包含的任何参数都将作为参数传递给__init__
方法。请注意,在类内定义的任何方法的第一个参数是self
,因此你可以使用它来设置和获取任何你需要的属性:
ex = Example('Sylvain') ex.say('nice to meet you')
'Hello Sylvain, nice to meet you.'
还要注意,创建一个新的 PyTorch 模块需要继承自Module
。继承是一个重要的面向对象的概念,在这里我们不会详细讨论——简而言之,它意味着我们可以向现有类添加额外的行为。PyTorch 已经提供了一个Module
类,它提供了一些我们想要构建的基本基础。因此,我们在定义类的名称后面添加这个超类的名称,如下面的示例所示。
你需要知道创建一个新的 PyTorch 模块的最后一件事是,当调用你的模块时,PyTorch 将调用你的类中的一个名为forward
的方法,并将包含在调用中的任何参数传递给它。这是定义我们的点积模型的类:
class DotProduct(Module): def __init__(self, n_users, n_movies, n_factors): self.user_factors = Embedding(n_users, n_factors) self.movie_factors = Embedding(n_movies, n_factors) def forward(self, x): users = self.user_factors(x[:,0]) movies = self.movie_factors(x[:,1]) return (users * movies).sum(dim=1)
如果你以前没有见过面向对象的编程,不用担心;在这本书中你不需要经常使用它。我们在这里提到这种方法只是因为大多数在线教程和文档将使用面向对象的语法。
请注意,模型的输入是一个形状为batch_size x 2
的张量,其中第一列(x[:, 0]
)包含用户 ID,第二列(x[:, 1]
)包含电影 ID。如前所述,我们使用嵌入层来表示我们的用户和电影潜在因子的矩阵:
x,y = dls.one_batch() x.shape
torch.Size([64, 2])
现在我们已经定义了我们的架构并创建了参数矩阵,我们需要创建一个Learner
来优化我们的模型。在过去,我们使用了特殊函数,比如cnn_learner
,为特定应用程序为我们设置了一切。由于我们在这里从头开始做事情,我们将使用普通的Learner
类:
model = DotProduct(n_users, n_movies, 50) learn = Learner(dls, model, loss_func=MSELossFlat())
现在我们准备拟合我们的模型:
learn.fit_one_cycle(5, 5e-3)
epoch | train_loss | valid_loss | time |
0 | 1.326261 | 1.295701 | 00:12 |
1 | 1.091352 | 1.091475 | 00:11 |
2 | 0.961574 | 0.977690 | 00:11 |
3 | 0.829995 | 0.893122 | 00:11 |
4 | 0.781661 | 0.876511 | 00:12 |
我们可以做的第一件事是让这个模型更好一点,强制这些预测值在 0 到 5 之间。为此,我们只需要使用sigmoid_range
,就像第六章中那样。我们经验性地发现,最好让范围略微超过 5,所以我们使用(0, 5.5)
:
class DotProduct(Module): def __init__(self, n_users, n_movies, n_factors, y_range=(0,5.5)): self.user_factors = Embedding(n_users, n_factors) self.movie_factors = Embedding(n_movies, n_factors) self.y_range = y_range def forward(self, x): users = self.user_factors(x[:,0]) movies = self.movie_factors(x[:,1]) return sigmoid_range((users * movies).sum(dim=1), *self.y_range)
model = DotProduct(n_users, n_movies, 50) learn = Learner(dls, model, loss_func=MSELossFlat()) learn.fit_one_cycle(5, 5e-3)
epoch | train_loss | valid_loss | time |
0 | 0.976380 | 1.001455 | 00:12 |
1 | 0.875964 | 0.919960 | 00:12 |
2 | 0.685377 | 0.870664 | 00:12 |
3 | 0.483701 | 0.874071 | 00:12 |
4 | 0.385249 | 0.878055 | 00:12 |
这是一个合理的开始,但我们可以做得更好。一个明显缺失的部分是,有些用户在推荐中只是更积极或更消极,有些电影只是比其他电影更好或更差。但在我们的点积表示中,我们没有任何方法来编码这两件事。如果你只能说一部电影,例如,它非常科幻,非常动作导向,非常不老旧,那么你实际上没有办法说大多数人是否喜欢它。
这是因为在这一点上我们只有权重;我们没有偏差。如果我们为每个用户有一个可以添加到我们的分数中的单个数字,对于每部电影也是如此,那么这将非常好地处理这个缺失的部分。因此,首先让我们调整我们的模型架构:
class DotProductBias(Module): def __init__(self, n_users, n_movies, n_factors, y_range=(0,5.5)): self.user_factors = Embedding(n_users, n_factors) self.user_bias = Embedding(n_users, 1) self.movie_factors = Embedding(n_movies, n_factors) self.movie_bias = Embedding(n_movies, 1) self.y_range = y_range def forward(self, x): users = self.user_factors(x[:,0]) movies = self.movie_factors(x[:,1]) res = (users * movies).sum(dim=1, keepdim=True) res += self.user_bias(x[:,0]) + self.movie_bias(x[:,1]) return sigmoid_range(res, *self.y_range)
让我们尝试训练这个模型,看看效果如何:
model = DotProductBias(n_users, n_movies, 50) learn = Learner(dls, model, loss_func=MSELossFlat()) learn.fit_one_cycle(5, 5e-3)
epoch | train_loss | valid_loss | time |
0 | 0.929161 | 0.936303 | 00:13 |
1 | 0.820444 | 0.861306 | 00:13 |
2 | 0.621612 | 0.865306 | 00:14 |
3 | 0.404648 | 0.886448 | 00:13 |
4 | 0.292948 | 0.892580 | 00:13 |
但是,结果并不比之前更好(至少在训练结束时)。为什么呢?如果我们仔细观察这两次训练,我们会发现验证损失在中间停止改善并开始变差。正如我们所见,这是过拟合的明显迹象。在这种情况下,没有办法使用数据增强,所以我们将不得不使用另一种正则化技术。一个有帮助的方法是权重衰减。
Weight Decay
权重衰减,或L2 正则化,包括将所有权重的平方和添加到损失函数中。为什么这样做?因为当我们计算梯度时,它会为梯度增加一个贡献,鼓励权重尽可能小。
为什么它可以防止过拟合?这个想法是,系数越大,损失函数中的峡谷就会越尖锐。如果我们以抛物线的基本例子y = a * (x**2)
为例,a
越大,抛物线就越狭窄:
因此,让我们的模型学习高参数可能导致它用一个过于复杂、具有非常尖锐变化的函数拟合训练集中的所有数据点,这将导致过拟合。
限制我们的权重过大会阻碍模型的训练,但会产生一个更好泛化的状态。回顾一下理论,权重衰减(或wd
)是一个控制我们在损失中添加的平方和的参数(假设parameters
是所有参数的张量):
loss_with_wd = loss + wd * (parameters**2).sum()
然而,在实践中,计算那个大和并将其添加到损失中将非常低效(也许在数值上不稳定)。如果你还记得一点高中数学,你可能会记得p**2
关于p
的导数是2*p
,所以将那个大和添加到我们的损失中,实际上等同于这样做:
parameters.grad += wd * 2 * parameters
实际上,由于wd
是我们选择的一个参数,我们可以使它变为两倍大,所以在这个方程中我们甚至不需要*2
。要在 fastai 中使用权重衰减,在调用fit
或fit_one_cycle
时传递wd
即可(可以同时传递):
model = DotProductBias(n_users, n_movies, 50) learn = Learner(dls, model, loss_func=MSELossFlat()) learn.fit_one_cycle(5, 5e-3, wd=0.1)
epoch | train_loss | valid_loss | time |
0 | 0.972090 | 0.962366 | 00:13 |
1 | 0.875591 | 0.885106 | 00:13 |
2 | 0.723798 | 0.839880 | 00:13 |
3 | 0.586002 | 0.823225 | 00:13 |
4 | 0.490980 | 0.823060 | 00:13 |
好多了!
创建我们自己的嵌入模块
到目前为止,我们使用Embedding
而没有考虑它是如何工作的。让我们重新创建DotProductBias
,不使用这个类。我们需要为每个嵌入初始化一个随机权重矩阵。然而,我们必须小心。回想一下第四章中提到的,优化器要求能够从模块的parameters
方法中获取模块的所有参数。然而,这并不是完全自动发生的。如果我们只是将一个张量作为Module
的属性添加,它不会包含在parameters
中:
class T(Module): def __init__(self): self.a = torch.ones(3) L(T().parameters())
(#0) []
要告诉Module
我们希望将一个张量视为参数,我们必须将其包装在nn.Parameter
类中。这个类不添加任何功能(除了自动为我们调用requires_grad_
)。它只用作一个“标记”,以显示要包含在parameters
中的内容:
class T(Module): def __init__(self): self.a = nn.Parameter(torch.ones(3)) L(T().parameters())
(#1) [Parameter containing: tensor([1., 1., 1.], requires_grad=True)]
所有 PyTorch 模块都使用nn.Parameter
来表示任何可训练参数,这就是为什么我们直到现在都不需要显式使用这个包装器:
class T(Module): def __init__(self): self.a = nn.Linear(1, 3, bias=False) t = T() L(t.parameters())
(#1) [Parameter containing: tensor([[-0.9595], [-0.8490], [ 0.8159]], requires_grad=True)]
type(t.a.weight)
torch.nn.parameter.Parameter
我们可以创建一个张量作为参数,进行随机初始化,如下所示:
def create_params(size): return nn.Parameter(torch.zeros(*size).normal_(0, 0.01))
让我们再次使用这个来创建DotProductBias
,但不使用Embedding
:
class DotProductBias(Module): def __init__(self, n_users, n_movies, n_factors, y_range=(0,5.5)): self.user_factors = create_params([n_users, n_factors]) self.user_bias = create_params([n_users]) self.movie_factors = create_params([n_movies, n_factors]) self.movie_bias = create_params([n_movies]) self.y_range = y_range def forward(self, x): users = self.user_factors[x[:,0]] movies = self.movie_factors[x[:,1]] res = (users*movies).sum(dim=1) res += self.user_bias[x[:,0]] + self.movie_bias[x[:,1]] return sigmoid_range(res, *self.y_range)
然后让我们再次训练它,以检查我们是否得到了与前一节中看到的大致相同的结果:
model = DotProductBias(n_users, n_movies, 50) learn = Learner(dls, model, loss_func=MSELossFlat()) learn.fit_one_cycle(5, 5e-3, wd=0.1)
epoch | train_loss | valid_loss | time |
0 | 0.962146 | 0.936952 | 00:14 |
1 | 0.858084 | 0.884951 | 00:14 |
2 | 0.740883 | 0.838549 | 00:14 |
3 | 0.592497 | 0.823599 | 00:14 |
4 | 0.473570 | 0.824263 | 00:14 |
现在,让我们看看我们的模型学到了什么。
解释嵌入和偏差
我们的模型已经很有用,因为它可以为我们的用户提供电影推荐,但看到它发现了什么参数也很有趣。最容易解释的是偏差。以下是偏差向量中值最低的电影:
movie_bias = learn.model.movie_bias.squeeze() idxs = movie_bias.argsort()[:5] [dls.classes['title'][i] for i in idxs]
['Children of the Corn: The Gathering (1996)', 'Lawnmower Man 2: Beyond Cyberspace (1996)', 'Beautician and the Beast, The (1997)', 'Crow: City of Angels, The (1996)', 'Home Alone 3 (1997)']
想想这意味着什么。它表明对于这些电影中的每一部,即使用户与其潜在因素非常匹配(稍后我们将看到,这些因素往往代表动作水平、电影年龄等等),他们通常仍然不喜欢它。我们本可以简单地按照电影的平均评分对其进行排序,但查看学到的偏差告诉我们更有趣的事情。它告诉我们不仅仅是电影是人们不喜欢观看的类型,而且即使是他们本来会喜欢的类型,人们也倾向于不喜欢观看!同样地,以下是偏差最高的电影:
idxs = movie_bias.argsort(descending=True)[:5] [dls.classes['title'][i] for i in idxs]
['L.A. Confidential (1997)', 'Titanic (1997)', 'Silence of the Lambs, The (1991)', 'Shawshank Redemption, The (1994)', 'Star Wars (1977)']
因此,例如,即使您通常不喜欢侦探电影,您可能会喜欢LA 机密!
直接解释嵌入矩阵并不那么容易。对于人类来说,因素太多了。但有一种技术可以提取出这种矩阵中最重要的基础方向,称为主成分分析(PCA)。我们不会在本书中详细讨论这个,因为您要成为深度学习从业者并不特别重要,但如果您感兴趣,我们建议您查看 fast.ai 课程面向程序员的计算线性代数。图 8-3 显示了基于两个最强的 PCA 组件的电影的外观。
图 8-3. 基于两个最强的 PCA 组件的电影表示
我们可以看到模型似乎已经发现了经典与流行文化电影的概念,或者这里代表的是广受好评。
杰里米说
无论我训练多少模型,我永远不会停止被这些随机初始化的数字组合所感动和惊讶,这些数字通过如此简单的机制训练,竟然能够自己发现关于我的数据的东西。我几乎觉得可以欺骗,我可以创建一个能够做有用事情的代码,而从未真正告诉它如何做这些事情!
我们从头开始定义了我们的模型,以教给您内部情况,但您可以直接使用 fastai 库来构建它。我们将在下一节看看如何做到这一点。
使用 fastai.collab
我们可以使用 fastai 的collab_learner
使用先前显示的确切结构创建和训练协同过滤模型:
learn = collab_learner(dls, n_factors=50, y_range=(0, 5.5))
learn.fit_one_cycle(5, 5e-3, wd=0.1)
epoch | train_loss | valid_loss | time |
0 | 0.931751 | 0.953806 | 00:13 |
1 | 0.851826 | 0.878119 | 00:13 |
2 | 0.715254 | 0.834711 | 00:13 |
3 | 0.583173 | 0.821470 | 00:13 |
4 | 0.496625 | 0.821688 | 00:13 |
FastAI 之书(面向程序员的 FastAI)(四)(2)https://developer.aliyun.com/article/1483408