面向显式反馈的基于矩阵分解的推荐算法PMF
论文:Ruslan Salakhutdinov and Andriy Mnih. Probabilistic Matrix Factorization [C]. NeurIPS 2007.
https://proceedings.neurips.cc/paper/2007/file/d7322ed717dedf1eb4e6e52a37ea7bcd-Paper.pdf
数据集:ML100K数据集 https://grouplens.org/datasets/movielens/100k/
协同过滤问题定义和数据集描述
问题定义
构造损失函数,然后根据包含用户行为的数据集训练得到用户矩阵(每行向量表示一个用户)和物品矩阵(每行向量表示一个物品),使得损失越小越好,即Ui(用户i特征向量)与Vj(物品j特征向量)的内积与训练集中的Rij(用户i对物品j的评分)越接近越好。
然后通过训练得到的用户矩阵和物品矩阵预测没有发生行为的用户-物品之间的分数,再根据分数向此用户进行物品推荐。
协同就是利用群体的行为来做决策(推荐),过滤,就是从可行的决策(推荐)方案(标的物)中将用户喜欢的方案(标的物)找(过滤)出来。
关键就是如何构建好的损失函数,考虑更多的因素,进而得到更优质的用户表征和物品表征。
数据集描述
本次作业使用Minnesota 大学GroupLens项目组创办的MovieLens100K数据集。
整个数据集包含了:
-
来自943个用户、1682部电影的10000条评分,评分区间为 [1,2,3,4,5]。
-
每个用户至少都评分了20部电影。
实际使用到的是MovieLens100K数据集下的u1.base和u1.test,前者作为训练集,后者作为测试集。数据格式如下:
-
userId :每个用户的Id,1~943
-
movieId:每部电影的Id,1~1682
-
rating:用户评分。
-
timestamp:时间戳,用户评分时的时间。
以下是使用pandas得到的数据集的描述:
基于矩阵分解的协同过滤推荐方法和实验
符号说明
符号 | 含义 |
---|---|
用户数量 | |
物品数量 | |
用户ID | |
物品ID | |
评分集(评分范围)评分 | |
评分矩阵 | |
1表示用户u有对物品i进行评分,反之没有 | |
观察到的评分记录 | |
观察到的评分数量 | |
密度(稀疏度) | |
全局评分均值 | |
用户偏差 | |
物品偏差 | |
向量维度 | |
用户特征向量 | |
用户特征矩阵 | |
物品特征向量 | |
物品特征矩阵 | |
权衡参数 |
方法形式化
PMF的优化函数:
使用SGD方法使得损失函数最小化,对于每条记录(用户id、物品id、评分)都计算该记录产生的损失:
然后计算该记录用户特征向量和物品特征向量的梯度:
再然后对此用户的特征向量和此物品的特征向量进行更新:
迭代完一次之后,降低学习率:
算法伪代码
1:初始化模型参数(用户矩阵,物品矩阵,学习率,用户正则化权重,物品正则化权重)
2:for t = 1,...,T do
3: for j = 1,...,p do
4: 训练集中取recordj(u,i,rui)数据
5: 计算用户矩阵和物品矩阵的梯度
6: 更新用户矩阵和物品矩阵的参数
7: end for
8: 降低学习率
9:end for
核心代码
# PMF 模型
class PMF:
# PMF 模型初始化,已经设置默认参数
def __init__(self, user_set, item_set, record_list, dimensions=20, learning_rate=0.01, alpha_user=0.1, alpha_item=0.1):
# 创建PMF时,表示用户id的set集合。调用vector_initialize函数后,表示用户的特征矩阵 {用户id:用户特征向量,...}
self.users = user_set
# 同上
self.items = item_set
# 训练集中的记录列表
self.records = record_list
# 用户和物品的特征维度,默认为20
self.dimensions = dimensions
# 学习率,默认为0.01
self.learning_rate = learning_rate
# 用户正则化的超参数,默认为0.1
self.alpha_user = alpha_user
# 物品正则化的超参数,默认为0.1
self.alpha_item = alpha_item
# 训练过程中的损失
self.loss = 0
# 初始化用户特征和物品特征
def vector_initialize(self):
# 用户和物品的特征使用字典来保存,Key是ID,Value是相应的特征向量
users_dict = {}
items_dict = {}
# 用户特征初始化
for user in self.users:
# 生成维度20,服从0~1的均匀分布的向量
user_vector = np.random.rand(self.dimensions)
# 保存此用户的特征向量
users_dict[user] = (user_vector - 0.5) * 0.01
# 物品特征初始化
for item in self.items:
item_vector = np.random.rand(self.dimensions)
items_dict[item] = (item_vector - 0.5) * 0.01
# 更新模型的两个属性
self.users = users_dict
self.items = items_dict
# 使用随机梯度下降方法训练用户和物品的特征
def train(self, epochs):
# 迭代次数
for epoch in range(epochs):
# 每次迭代开始,将模型的属性loss置0
self.loss = 0
# 遍历评分记录
for record in self.records:
# 该记录的用户特征向量
user = self.users[record[0]]
# 该记录的物品特征向量
item = self.items[record[1]]
# 该记录的用户对物品的评分
rating = int(record[2])
# 计算损失
error = self.loss_function(user, item, rating)
# 损失累加
self.loss += error
# 计算该用户特征向量的梯度
grad_user = -(rating - np.dot(user, item)) * item + self.alpha_user * user
# 计算该物品特征向量的梯度
grad_item = -(rating - np.dot(user, item)) * user + self.alpha_item * item
# 根据梯度对特征向量进行更新
self.users[record[0]] -= self.learning_rate * grad_user
self.items[record[1]] -= self.learning_rate * grad_item
# 每迭代完一次,学习率降低
self.learning_rate = self.learning_rate * 0.9
# 打印每次迭代的损失
print("epoch: ", epoch, "loss:", self.loss)
# 训练完之后,将用户特征向量进行保存
with codecs.open("pureResult/user_vector", "w") as f1:
for u in self.users.keys():
f1.write(str(u) + "\t")
f1.write(str(list(self.users[u])))
f1.write("\n")
# 将物品特征向量进行保存
with codecs.open("pureResult/item_vector", "w") as f2:
for i in self.items.keys():
f2.write(str(i) + "\t")
f2.write(str(list(self.items[i])))
f2.write("\n")
# 损失函数定义
def loss_function(self, user, item, rating):
return 0.5 * math.pow((rating - np.dot(user, item)), 2) + \
0.5 * self.alpha_user * math.pow(np.linalg.norm(user, ord=2), 2) + \
0.5 * self.alpha_item * math.pow(np.linalg.norm(item, ord=2), 2)
实验结果和分析(含不同活跃程度用户上的结果)
在测试集 u1.test 进行模型测试,测试集中包含20000条记录,用户和电影都未完全包含训练集中出现的用户和电影。
实验结果如下:
参考误差:
不同活跃度的用户推荐效果:
分析:
- PMF在均方根误差上均好于User-based CF、Item-based CF、Pure SVD和RSVD。
- PMF在平均绝对误差上优于Item-based CF、Pure SVD,略差于User-based CF和RSVD。
- 以测试集中用户的评分数量作为此用户的活跃度,评分越多,表示越活跃。可以看出活跃度低的用户,无论是均方根误差还是平均绝对误差,有很多大于1,甚至有大于4。而当活跃度高于30后,误差均小于1。符合常识:活跃度越高的用户,用于预测的数量越多,对此用户推荐的效果也越好,反之相反。
基于深度学习的协同过滤推荐方法和实验
模型示意图
核心代码
# 使用pytorch构造PMF模型
class PMF(torch.nn.Module):
def __init__(self, alpha_user=0.1, alpha_item=0.1):
super().__init__()
# 用户正则化的超参数,默认为0.1
self.alpha_user = alpha_user
# 物品正则化的超参数,默认为0.1
self.alpha_item = alpha_item
# 前向传播函数,定义了损失函数
# users 表示用户特征矩阵,items 表示物品特征矩阵,matrix 表示评分矩阵
def forward(self, users, items, matrix):
# 用用户特征矩阵和物品特征矩阵进行矩阵相乘得到评分预测矩阵
predict_rating = torch.mm(users, items.t())
# 得到评分矩阵有数据的标志矩阵,有评分的位置标志为1,反之为0,用于求损失函数时只对有评分的记录进行损失求和
not_zero = (matrix != -1).type(torch.FloatTensor)
# 评分与预测之间的误差平方求和的二分之一
error = torch.sum(torch.div(torch.pow((matrix - predict_rating), 2), 2) * not_zero)
# 用户正则化
users_regularization = self.alpha_user * torch.sum(torch.pow(users.norm(2, dim=1), 2)) / 2
# 物品正则化
items_regularization = self.alpha_item * torch.sum(torch.pow(items.norm(2, dim=1), 2)) / 2
return error + users_regularization + items_regularization
# 模型训练,model:PMF模型,users:用户特征矩阵,items:物品特征矩阵,ratings:评分矩阵,epochs:迭代次数
def train(model, users, items, ratings, epochs):
for epoch in range(epochs):
# 模型优化器梯度置零
model.optimizer.zero_grad()
# 执行model的前向传播函数,计算一次迭代的总loss
loss = PMF_model(users, items, ratings)
# 计算用户特征矩阵和物品特征矩阵的梯度
loss.backward()
# 梯度更新
model.optimizer.step()
# 打印此次迭代的loss
print("epoch: ", epoch, "loss:", loss)
# 迭代训练完成之后,保存用户特征矩阵和物品特征矩阵,用于测试
torch.save(users, "pytorchResult/users.pt")
torch.save(items, "pytorchResult/items.pt")
# 用户特征矩阵和物品特征矩阵的初始化,rating_matrix 表示评分矩阵
def vector_initialize(rating_matrix):
# 评分矩阵的行数表示用户数量
users_number = rating_matrix.shape[0]
# 评分矩阵的列数表示物品数量
items_number = rating_matrix.shape[1]
# 服从均匀分布随机初始化一个用户数*20的矩阵,每行表示相应的用户特征向量
user_vector = torch.rand(users_number, 20, requires_grad=True)
# 对矩阵每个元素-0.5
user_vector.data.add_(-0.5)
# 对矩阵每个元素*0.01
user_vector.data.mul_(0.01)
# 服从均匀分布随机初始化一个物品数*20的矩阵,每行表示相应的物品特征向量
item_vector = torch.rand(items_number, 20, requires_grad=True)
item_vector.data.add_(-0.5)
item_vector.data.mul_(0.01)
return user_vector, item_vector
实验结果和分析(含与非深度学习方法的比较)
分析:
- 使用框架实现的PMF不论是均方根误差还是均绝对误差均不如非框架实现的PMF推荐效果好。
- 如果优化器使用SGD,会出现上图的现象,损失虽然一开始会变小,但是从第五次迭代开始就成爆炸式增长。而改用Adam优化器就则会顺利的得到正确的实验结果。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步