面向显式反馈的基于矩阵分解的推荐算法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得到的数据集的描述:

数据集描述

基于矩阵分解的协同过滤推荐方法和实验

符号说明

符号 含义
n 用户数量
m 物品数量
u{1,2,...,n} 用户ID
i,i{1,2,...,m} 物品ID
G,e.g.,G={1,2,3,4,5} 评分集(评分范围)评分
R{G?}nm 评分矩阵
yui{0,1} 1表示用户u有对物品i进行评分,反之没有
Θ={(u,i,rui)} 观察到的评分记录
p=u,iyui=|Θ| 观察到的评分数量
p/n/m 密度(稀疏度)
μR 全局评分均值
buR 用户偏差
biR 物品偏差
dR 向量维度
UuR1d 用户特征向量
URnd 用户特征矩阵
ViR1d 物品特征向量
VRnd 物品特征矩阵
αu,αv 权衡参数

方法形式化

PMF的优化函数:

minu=1ni=1myui[12(ruiUuViT)2+αu2||Uu||2+αv2||Vv||2]

使用SGD方法使得损失函数最小化,对于每条记录(用户id、物品id、评分)都计算该记录产生的损失:

fui=12(ruiUuViT)2+αu2||Uu||2+αv2||Vv||2

然后计算该记录用户特征向量和物品特征向量的梯度:

Uu=fuiUu=(ruiUuViT)Vi+αuUu

Vi=fuiVi=(ruiUuViT)Uu+αvVi

再然后对此用户的特征向量和此物品的特征向量进行更新:

Uu=UuλUu

Vi=ViλVi

迭代完一次之后,降低学习率:

λ=λ0.9

算法伪代码

1:初始化模型参数(用户矩阵,物品矩阵,学习率,用户正则化权重,物品正则化权重)

2:for t = 1,...,T do

3: for j = 1,...,p do

4: 训练集中取recordj(u,i,rui)数据

5: 计算用户矩阵和物品矩阵的梯度

6: 更新用户矩阵和物品矩阵的参数

7: end for

8: 降低学习率 λ=λ0.9

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条记录,用户和电影都未完全包含训练集中出现的用户和电影。

实验结果如下:

平均误差

参考误差:

参考误差

不同活跃度的用户推荐效果:

不同活跃度的用户推荐效果

分析:

  1. PMF在均方根误差上均好于User-based CF、Item-based CF、Pure SVD和RSVD。
  2. PMF在平均绝对误差上优于Item-based CF、Pure SVD,略差于User-based CF和RSVD。
  3. 以测试集中用户的评分数量作为此用户的活跃度,评分越多,表示越活跃。可以看出活跃度低的用户,无论是均方根误差还是平均绝对误差,有很多大于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

实验结果和分析(含与非深度学习方法的比较)

pytorch误差

训练过程

分析:

  1. 使用框架实现的PMF不论是均方根误差还是均绝对误差均不如非框架实现的PMF推荐效果好。
  2. 如果优化器使用SGD,会出现上图的现象,损失虽然一开始会变小,但是从第五次迭代开始就成爆炸式增长。而改用Adam优化器就则会顺利的得到正确的实验结果。
posted @   FireOnFire  阅读(135)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示