本节课的主要内容是针对使用协同滤波算法进行电影评分预测的应用场景,分别使用Excel
、Fast.AI
、Pytorch
、自主构建的网络,来实现相应功能。
所用数据为MovieLens
数据,其中中的评分表存储了用户id,电影id,以及5星制的评分。数据格式如下:
一. 使用Excel与矩阵分解算法实现推荐系统
整理为以用户id为索引的评分表,格式如下:
将缺省数据的空格填充为0。然后对用户和电影使用内置矩阵的形式标识,即每个用户使用一个向量替代,每部电影也使用向量替代。某个用户对某部电影的评论分数表示为用户向量与电影向量的内积。定义损失函数为预测评分与真实评分的均方误差根(RMSE
,Root Mean Square Error
)。在Excel
中选择“数据”→“规划求解”,设置目标单元格(即存储均方误差根的单元格),可变单元格,以及求解方法,开始求解即可。
之所以将这种方法称为矩阵分解法,是由于用户评分矩阵最终被表示为用户矩阵和电影矩阵的相乘的形式。
二. 使用Fast.AI
实现协同滤波算法
使用Fast.AI
的API,构造协同滤波推荐系统的逻辑流程,与前面几节所述的图片分类器、销量预测系统、影评倾向分析等的流程一致,即生成满足模型要求的数据、获得学习器、进行训练,代码如下:
cf = CollabFilterDataset.from_csv(path, 'ratings.csv', 'userId', 'movieId', 'rating')
learn = cf.get_learner(n_factors, val_idxs, 64, opt_fn=optim.Adam)
learn.fit(1e-2, 2, wds=wd, cycle_len=1, cycle_mult=2)
其中n_factors=50
,设置了内置矩阵的大小;wd=2E-4
是正则化系数。
三. 在Pytorch基础上实现协同滤波算法
1. 整理数据
用户id可能不是从0开始的,而且会非常大。我们以其索引,来进行替代。
# 获取用户id集合
u_uniq = ratings.userId.unique()
# 生成用户id和索引值的映射
user2idx = {o:i for i,o in enumerate(u_uniq)}
# 用索引值替换数据中的用户id
ratings.userId = ratings.userId.apply(lambda x: user2idx[x])
对电影id也同样如此操作。
2. 构造网络层对象
网络层对象派生自Pytorch
的nn.module
,在其初始化函数__init__()
中,声明对应于用户和电影的内置矩阵,并采用何恺明大神的参数初始化策略进行初始。
定义前向计算函数:forward()
,其中为方便,传入了ColumnarModelData
数据模型。在定义前向传播计算式时,注意函数传入的为批量数据,尽量使用矢量化表达方式,以充分利用GPU加速。
3. 声明优化策略
opt = optim.SGD(model.parameters(), 1e-1, weight_decay=wd, momentum=0.9)
其中model.parameters()
指明了需要优化更新的参数,1E-1
是学习速率,接下来的两个参数将在后面叙述。
4. 编写优化循环
一个较为简单的优化循环版本是Fast.AI
的fit()
函数,调用语法如下:
fit(model, data, 3, opt, F.mse_loss)
参数依次是模型、数据、遍历次数、优化方法、损失函数。
5. 改进
- (1) 加入偏置项
某个用户可能对某部电影打分整体偏低,而某部电影可能又十分流行,因此得分整体偏高。这些都是和单一对象有关,因此不应该是用户参数和电影参数相乘的形式(相乘即意味着关系的耦合)。基于这个考虑,为用户和电影添加偏置项,即某个用户对某部电影的评分,表示为用户向量和电影向量的内积,再加上二者的偏置项。 - (2) 使用
sigmoid()
函数限制输出的评分位于1~5分之间。
四. 协同滤波算法的神经网络版本
上述过程中,我们强制定义评分为用户向量和电影向量的内积再加上偏置项的关系,事实上,这对我们得到的评分系统做了很大的限制。而若采用神经网络的架构,我们可以去除这种约束,并让模型自主学习用户向量和电影向量之间的关系。具体代码实现描述如下:
1. 声明网络结构
使用nn.Linear()
定义线性加权层,所需参数为输入向量维度、输出向量维度。其中第一层的输入向量维度为用户向量和电影向量的维度之和。
使用nn.Dropout()
定义弃置策略,所需参数为弃置率。
self.lin1 = nn.Linear(n_factors*2, nh)
self.lin2 = nn.Linear(nh, 1)
self.drop1 = nn.Dropout(p1)
self.drop2 = nn.Dropout(p2)
2. 定义前向计算函数
按照神经网络的组织形式,表述前向计算函数,如一个线性加权层的输出,按照一定比例弃置后,通过非线性单元,上述过程可表示为
F.relu(self.lin1(self.drop1(data_batch)))
之后按照上一小节的3.声明优化策略
和4.编写优化循环
步骤继续。
梯度下降算法的改进
1. 冲量算法
现在讨论在声明优化策略时,传入的momentum
参数。
假设模型参数为
ωn
ω
n
,学习速率为
r
r
,梯度为,则常规的随机梯度算法中,参数更新公式可表示为
ωn=ωn−1−r⋅gn
ω
n
=
ω
n
−
1
−
r
⋅
g
n
。
如果某次求到的梯度比较小,那么参数值的更新就比较小,会使得优化变慢,或是算法陷在某个局部最小值的小范围内而出不来。引入冲量参数就是为了解决这一问题。
其原理是每次更新参数时,更新步长不仅要考虑当前所求得的梯度,还要考虑之前一步所更新的步长:
ωn=ωn−1−r⋅mn,mn=u⋅mn−1+(1−u)⋅gn
ω
n
=
ω
n
−
1
−
r
⋅
m
n
,
m
n
=
u
⋅
m
n
−
1
+
(
1
−
u
)
⋅
g
n
。(注:课程中给出的线性加权公式,而一般文献中采用的是在梯度上额外再加上冲量项。)
2. Adam
算法
在Adam
算法中,引入梯度平方的冲量形式:
sn=t⋅sn−1+(1−t)⋅g2n
s
n
=
t
⋅
s
n
−
1
+
(
1
−
t
)
⋅
g
n
2
;Adam
的参数更新算法:
ωn=ωn−1−r⋅mnsn√
ω
n
=
ω
n
−
1
−
r
⋅
m
n
s
n
。
Adam
算法的优化速度比随机梯度算法更快,但找到的解不如随机梯度算法找到的解优质。近期,又提出了AdamW
算法,即带权重衰减的Adam
算法。
3. AdamW
算法
即在损失函数中加入了权重系数的2阶正则项。假设原来的损失函数为
L(ω⃗ )=∑‖yi→^−y⃗ i‖2
L
(
ω
→
)
=
∑
‖
y
i
→
^
−
y
→
i
‖
2
,那么现在损失函数就变为
L(ω⃗ )=∑‖yi→^−y⃗ i‖2+λ‖ω⃗ ‖2
L
(
ω
→
)
=
∑
‖
y
i
→
^
−
y
→
i
‖
2
+
λ
‖
ω
→
‖
2
。这其实是另外一种避免过拟合的策略,限制了参数
ω
ω
的复杂程度,使得优化算法“适可而止”。其中
λ
λ
就是在声明优化算法时所传进去的weight_decay
参数。
备注
- 在nn.module中定义的参数均是变量(
Variable
),而不是张量(Tensor
),要获取相应张量,调用使用所定义参数的data
属性。 - 在Pytorch中,针对
Tensor
,函数有原位操作形式:即函数名后接下划线。 - 对nn.module对象中定义的变量,调用
.squeeze()
方法,可以获取矩阵运算的广播特性。
一些有用的链接
- 一篇介绍何恺明大神的网络参数初始化参数的策略。
- 一篇介绍深度学习中优化算法的文章:包括随机梯度算法、Adam算法等。
- 课程wiki : 本节课程的一些相关资源,包括课程笔记、课上提到的博客地址等。