首页 > 编程语言 >基于隐式反馈的推荐算法MFLogLoss

基于隐式反馈的推荐算法MFLogLoss

时间:2023-01-02 17:23:21浏览次数:60  
标签:vector MFLogLoss record self item 算法 bias user 隐式

论文:Christopher C. Johnson. Logistic matrix factorization for implicit feedback data [C]. In: Proceedings of the Workshop on Distributed Machine Learning and Matrix Computations at NeurIPS 2014.https://web.stanford.edu/~rezab/nips2014workshop/submits/logmat.pdf

数据集:ML1M数据集 https://grouplens.org/datasets/movielens/1m/

单类协同过滤问题定义和数据集描述

问题定义

单类反馈:能获得的数据并没有评分这种类型的数据,而是一种行为是否发生,比如用户听音乐,可获取的数据大都是单类反馈的,即用户是否听过这首歌。

单类协同过滤:用户与物品之间发生过交集,就作上标记1,表示用户可能喜欢,作为正样本,而没有发生过行为的用户-物品对,也就是缺失值,用户也可能会喜欢。但是缺失值相对于正样本来说是非常多的,所以从缺失值中采样一部分作为负样本。然后通过模型训练,使得用户对正样本的预测分数越高越好,而对负样本的预测分数越低越好。

数据集描述

所有的评分数据都包含在ratings.dat中,其结构为‘UserID::MovieID::Rating::Timestamp’

  • UserID:用户的ID,范围在1到6040

  • MovieID:电影的ID,范围在1到3952

  • Rating:用户u对电影i的评分,共五个阶段

  • Timestamp:评分行为的发生时间

每一个用户至少有20条评分。

数据集描述

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

符号说明

符号 含义
\(n\) 用户数量
\(m\) 物品数量
\(u\in\left\{1,2,...,n\right\}\) 用户 id
\(i,i'\in\left\{1,2,...,m\right\}\) 物品 id
\(P\) 正样本集(观测到的用户-物品对)
\(A,|A|=\rho|P|\) 负样本集(没有观测到的用户-物品对)
\(r_{ui}\) 对于具体的用户 u 和物品 i,如果(u,i)属于正样本,则为1,负样本为-1
\(b_u\in{R}\) 用户 bias
\(b_i\in{R}\) 物品 bias
\(d\in{R}\) 特征维度
\(V_i\in{R}^{1*d}\) 物品 i 在物品空间的特征向量
\(V\in{R}^{m*d}\) 物品的特征矩阵
\(U_u\in{R}^{1*d}\) 用户 u 在用户空间的特征向量
\(U\in{R}^{n*d}\) 用户的特征矩阵

方法形式化

用户 u 对物品 i 的预测分数:

\[\hat{r}_{ui}=b_u+b_i+U_{u\cdot}V{i\cdot}^T \]

MFLogLoss 的目标函数:

\[min\sum_{(u,i)\in{P\bigcup{A}}}f_{ui} \]

\[f_{ui}=log(1+exp(-r_{ui}\hat{r}_{ui}))+\frac{\alpha_{u}}{2}||U_{u\cdot}||_F^2+\frac{\alpha_{v}}{2}||V_{i\cdot}||_F^2+\frac{\beta_u}{2}b_u^2 +\frac{\beta_v}{2}b_i^2 \]

根据目标函数对正样本和采样到的负样本进行计算,然后计算用户特征、用户 bias、物品特征、物品 bias 的梯度:

\[\nabla{b_u}=\frac{\partial{f_{ui}}}{\partial{b_u}}=-e_{ui}+\beta_ub_u \]

\[\nabla{b_i}=\frac{\partial{f_{ui}}}{\partial{b_i}}=-e_{ui}+\beta_vb_i \]

\[\nabla{V_{i\cdot}}=\frac{\partial{f_{ui}}}{\partial{V_{i\cdot}}}=-e_{ui}U_{u\cdot}+\alpha_vV_{i\cdot} \]

\[\nabla{U_{u\cdot}}=\frac{\partial{f_{ui}}}{\partial{U_{u\cdot}}}=-e_{ui}V_{i\cdot}+\alpha_uU_{u\cdot} \]

\[e_{ui}=\frac{r_{ui}}{1+exp(r_{ui}\hat{r}_{ui})} \]

然后根据梯度对参数进行更新学习:

\[b_u=b_u-\gamma\nabla{b_u} \]

\[b_i=b_i-\gamma\nabla{b_i} \]

\[V_{i\cdot}=V_{i\cdot}-\gamma\nabla{V_{i\cdot}} \]

\[U_{u\cdot}=U_{u\cdot}-\gamma\nabla{U_{u\cdot}} \]

算法伪代码

1:初始化模型参数:用户特征、物品特征、用户bias、物品bias

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

3: 随机选取负样本集合,大小是正样本的 \(\rho\) 倍

4: for 将正样本和负样本的并集按照随机的顺序对每个 (u,i) do

5: 计算预测分数:$ \hat{r}{ui}=b_u+b_i+U{u\cdot}V{i\cdot}^T$

6: 计算各个参数的梯度

7: 使用梯度对参数进行更新

8: end for

9:end for

核心代码

# MFLogLoss模型
class MFLogLoss:
    def __init__(self, user_set, item_set, observed_record_list, dimensions=20, learning_rate=0.01, alpha_user=0.001,
                 alpha_item=0.001, beta_user=0.001, beta_item=0.001, rho=3):
        self.users = user_set  # 先是用于保存用户id的集合,初始化特征向量后,保存用户的特征
        self.items = item_set  # 物品特征
        self.userBias = {}  # 用户bias
        self.itemBias = {}  # 物品bias
        self.observedRecords = observed_record_list  # 正样本
        self.unobservedRecords = []  # 所有负样本
        self.dimensions = dimensions  # 特征维度
        self.learning_rate = learning_rate  # 学习率
        self.alpha_user = alpha_user  # 用户特征正则化系数
        self.alpha_item = alpha_item  # 物品特征正则化系数
        self.beta_user = beta_user  # 用户bias正则化系数
        self.beta_item = beta_item  # 物品bias正则化系数
        self.rho = rho  # 负样本采样比例
        self.loss = 0  # 损失

    def initialize(self):
        users_dict = {}  # 用户特征 字典结构: {用户id: 用户特征向量,...}
        items_dict = {}  # 物品特征
        for user in self.users:
            user_vector = np.random.rand(self.dimensions)  # 初始化 0-1 均匀分布
            users_dict[user] = (user_vector - 0.5) * 0.01
            self.userBias.setdefault(user, 0)  # 该用户bias初始化为0
        for item in self.items:  # 物品特征初始化
            item_vector = np.random.rand(self.dimensions)
            items_dict[item] = (item_vector - 0.5) * 0.01
            self.itemBias.setdefault(item, 0)
        self.users = users_dict
        self.items = items_dict
        m = len(self.items)  # 物品数量
        n = len(self.users)  # 用户数量
        mu = len(self.observedRecords) / m / n  # 训练集中正样本数量
        for record in self.observedRecords:
            self.userBias[record[0]] += 1  # 该用户在训练集正样本中出现的次数
            self.itemBias[record[1]] += 1  # 该物品在训练集正样本中出现的次数
        for userIndex in self.userBias:
            self.userBias[userIndex] = self.userBias[userIndex] / m - mu  # 初始化用户bias
        for itemIndex in self.itemBias:
            self.itemBias[itemIndex] = self.itemBias[itemIndex] / n - mu  # 初始化物品bias

    def unobservedRecords_initialize(self):
        temp_unobservedRecords = []  # 存储所有用户和物品的pair对
        for user in self.users:
            for item in self.items:
                temp_unobservedRecords.append((user, item))
        self.unobservedRecords = list(set(temp_unobservedRecords).difference(set(self.observedRecords)))  # 去除掉训练集中出现的正样本,剩余的就是所有负样本(没有观察到的记录)

    def train(self, epochs):
        for epoch in range(epochs):
            self.loss = 0  # 初始化loss
            sampleUnobservedRecords = random.sample(self.unobservedRecords, self.rho * len(self.observedRecords))  # 从所有负样本集中取出正样本集大小三倍的负样本子集
            unionRecords = []  # 合并正样本和负样本子集,作为整个训练集
            for record in self.observedRecords:
                unionRecords.append((record[0], record[1], 1))  # 正样本的rui为1
            for record in sampleUnobservedRecords:
                unionRecords.append((record[0], record[1], -1))  # 负样本的rui为-1
            random.shuffle(unionRecords)  # 打乱训练集
            for record in unionRecords:  # 遍历训练集
                user_vector = self.users[record[0]]  # 当前record记录的用户特征向量
                item_vector = self.items[record[1]]  # 当前record记录的物品特征向量
                user_bias = self.userBias[record[0]]  # 当前record记录的用户bias
                item_bias = self.itemBias[record[1]]  # 当前record记录的物品bias
                rui = record[2]  # rui
                error = self.loss_function(user_vector, item_vector, user_bias, item_bias, rui)  # 计算这条record所产生的损失
                self.loss += error  # 累加到此次epoch总共的损失
                eui = rui / (1 + np.exp(rui * (user_bias + item_bias + np.dot(user_vector, item_vector)))) # 梯度所使用到的公共部分
                grad_user = -eui * item_vector + self.alpha_user * user_vector  # 当前用户特征向量的梯度
                grad_item = -eui * user_vector + self.alpha_item * item_vector  # 当前物品特征向量的梯度
                grad_user_bias = -eui + self.beta_user * user_bias  # 当前用户bias的梯度
                grad_item_bias = -eui + self.beta_item * item_bias  # 当前物品bias的梯度
                self.users[record[0]] -= self.learning_rate * grad_user  # 更新用户特征
                self.items[record[1]] -= self.learning_rate * grad_item  # 更新物品特征
                self.userBias[record[0]] -= self.learning_rate * grad_user_bias  # 更新用户bias
                self.itemBias[record[1]] -= self.learning_rate * grad_item_bias  # 更新物品bias

    # 损失函数
    def loss_function(self, user_vector, item_vector, user_bias, item_bias, rui):
        return np.log(1 + np.exp(-rui * (user_bias + item_bias + np.dot(user_vector, item_vector)))) + \
               self.alpha_user * 0.5 * math.pow(np.linalg.norm(user_vector, ord=2), 2) + \
               self.alpha_item * 0.5 * math.pow(np.linalg.norm(item_vector, ord=2), 2) + \
               self.beta_user * 0.5 * math.pow(user_bias, 2) + self.beta_item * 0.5 * math.pow(item_bias, 2)

实验结果和分析(含不同活跃程度用户上的结果)

首先在 100K 小数据集上进行测试,得到的 top5 的 precision 和 recall 与课件上的结果相似,说明训练程序和测试程序没有问题。

100K训练结果

100K 不同活跃程度用户的结果:

100K不同活跃程度用户

然后,再在 1M 数据集上进行训练,实验结果如下:

1M训练结果

1M 不同活跃程度用户的结果:

1M不同活跃程度用户

分析:图8每一个点表示一个用户,横坐标表示该用户的活跃度(训练集正样本数),纵坐标表示对该用户预测前五的物品在测试集中命中次数。

  • 命中次数为0的样本中,不会出现活跃度大于800的用户,一定程度上说明活跃度高的预测准确度越高。
  • 命中次数为0和5的用户个数相对来说较少。

基于深度学习的单类协同过滤推荐方法和实验

模型示意图

模型示意图

核心代码

# 初始化用户和物品的特征和bias
def initialize(rating_matrix):
    users_num = rating_matrix.shape[0]  # 训练集中用户的个数
    items_num = rating_matrix.shape[1]  # 训练集中物品的个数
    users_vector = torch.rand(users_num, 20, requires_grad=True)  # 初始化用户特征矩阵,users_num*20
    users_vector.data.add_(-0.5)
    users_vector.data.mul_(0.01)
    items_vector = torch.rand(items_num, 20, requires_grad=True)  # 初始化物品特征矩阵,items_num*20
    items_vector.data.add_(-0.5)
    items_vector.data.mul_(0.01)
    users_ui = rating_matrix.apply(lambda x: my_sum(x), axis=1).astype(float)  # 初始化用户特征
    items_ui = rating_matrix.apply(lambda x: my_sum(x), axis=0).astype(float)  # 初始化物品特征
    mu = users_ui.sum() / items_num / users_num
    # users_bias = torch.tensor(data=users_ui.values, requires_grad=True, dtype=torch.float)
    users_bias = torch.from_numpy(users_ui.values)
    users_bias.data.div_(items_num)
    users_bias.add_(-mu)
    # items_bias = torch.from_numpy(items_ui.values)
    items_bias = torch.from_numpy(items_ui.values)
    items_bias.data.div_(users_num)
    items_bias.add_(-mu)
    return users_vector, items_vector, users_bias, items_bias


# MFLogLoss模型
class MFLogLoss(torch.nn.Module):
    def __init__(self, alpha_user=0.001, alpha_item=0.001, beta_user=0.001, beta_item=0.001, rho=3):
        super().__init__()
        self.alpha_user = alpha_user  # 用户特征正则化系数
        self.alpha_item = alpha_item  # 物品特征正则化系数
        self.beta_user = beta_user  # 用户bias正则化系数
        self.beta_item = beta_item  # 物品bias正则化系数
        self.rho = rho  # 负样本采样比例

    # 前向传播函数,定义了损失函数
    # user_vector 表示用户特征向量,item_vector 表示物品特征向量,user_bias 表示用户bias, item_bias表示物品bias,rui表示该记录是正样本还是负样本
    def forward(self, user_vector, item_vector, user_bias, item_bias, rui):
        predict = torch.dot(user_vector, item_vector) + user_bias + item_bias  # 预测分数
        error = torch.log(1 + torch.exp(-rui * predict))  # log误差
        user_vector_regularization = self.alpha_user * torch.pow(torch.norm(user_vector), 2) / 2  # 用户特征正则化项
        item_vector_regularization = self.alpha_user * torch.pow(torch.norm(item_vector), 2) / 2  # 用户特征正则化项
        user_bias_regularization = self.beta_user * torch.pow(user_bias, 2) / 2  # 用户bias正则化项
        item_bias_regularization = self.beta_item * torch.pow(item_bias, 2) / 2  # 物品bias正则化项
        return error + user_vector_regularization + item_vector_regularization + user_bias_regularization + item_bias_regularization


def train(model, users_vector, items_vector, users_bias, items_bias, observed_record_list, unobserved_record_list, users_index_dict, items_col_dict, epochs):
    for epoch in range(epochs):
        sampleUnobservedRecords = random.sample(unobserved_record_list, model.rho * len(observed_record_list))  # 从所有负样本集中取出正样本集大小三倍的负样本子集
        union_records = []  # 合并正样本和负样本子集,作为整个训练集
        for record in observed_record_list:
            union_records.append((record[0], record[1], 1))
        for record in sampleUnobservedRecords:
            union_records.append((record[0], record[1], -1))
        random.shuffle(union_records)  # 打乱训练集
        total_loss = 0  # 当前epoch所产生的损失
        for record in union_records:
            rui = record[2]
            model.optimizer.zero_grad()  # 模型优化器梯度置零
            # loss = model(user_vector, item_vector, user_bias, item_bias, rui)
            # 执行model的前向传播函数,计算一次迭代的总loss
            loss = model(users_vector[users_index_dict[record[0]]], items_vector[items_col_dict[record[1]]], users_bias[users_index_dict[record[0]]], items_bias[items_col_dict[record[1]]], rui)
            # 计算用户特征矩阵和物品特征矩阵的梯度
            loss.backward()
            # 梯度更新
            model.optimizer.step()
            # 累计损失,如果直接累加loss,单次loss无法释放,会造成内存溢出,无法训练,因此要加loss.item()
            total_loss += loss.item()
        print("epoch: ", epoch, "loss:", total_loss)
    # 迭代训练完成之后,保存用户和物品的特征矩阵和bias向量
    torch.save(users_vector, "pytorchResult1M/users_vector.pt")
    torch.save(items_vector, "pytorchResult1M/items_vector.pt")
    torch.save(users_bias, "pytorchResult1M/users_bias.pt")
    torch.save(items_bias, "pytorchResult1M/items_bias.pt")

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

首先,在 100K 的小数据集上进行训练,训练结果如下:

100K结果

在 100K 上的训练结果达到了预期效果,然后再在 1M 数据上进行训练,训练结果如下:

1M结果

分析:在100K数据集上,框架训练的结果不管是precision还是recall都略低于不用框架训练的结果。而在1M数据集上,框架训练的结果反而略高于不用框架训练的结果。

标签:vector,MFLogLoss,record,self,item,算法,bias,user,隐式
From: https://www.cnblogs.com/fireonfire/p/17020212.html

相关文章

  • 每日算法之删除链表中重复的结点
    JZ76删除链表中重复的结点题目给定一个二叉树,返回该二叉树的之字形层序遍历,(第一层从左向右,下一层从右向左,一直这样交替)例如,给定的二叉树是{1,2,3,#,#,4,5}该二叉树之......
  • 莫队算法
    概念莫队算法是由莫涛提出的算法,可以解决一类离线区间询问问题,适用性极为广泛。同时将其加以扩展,便能轻松处理树上路径询问以及支持修改操作。假设\(n=m\),对于序列上的......
  • 分布式 id 生成器(雪花算法)
    分布式id生成器(雪花算法)有时我们需要能够生成类似MySQL自增ID这样不断增大,同时又不会重复的id。以支持业务中的高并发场景。比较典型的,电商促销时,短时间内会有大量的订......
  • 基础算法
    快速排序快速排序采用分治的思想左端点放指针i,右端点放指针j,向中间移动,当出现i>=x或j<=x停止,将这两个数交换,当i>j的时候证明整个序列遍历结束voidkuaipai(intl,intr......
  • 分享:github的算法项目
    ​​https://github.com/0voice/algorithm-structure​​......
  • 分享:一个可视化算法网站(所有常见的算法)
    网站:​​https://visualgo.net/zh​​各种算法说明:排序算法将一串数组(一个列表)中的元素(整数,数字,字符串等)按某种顺序(增大,减小,字典顺序等)重新排列。有很多种不同的排序算法,每......
  • 基于matlab的有参考图像质量评价,使用多种算法进行图像质量评价仿真
    1.算法概述图像质量评价(ImageQualityAssessment,IQA)是图像处理中的基本技术之一,主要通过对图像进行特性分析研究,然后评估出图像优劣(图像失真程度)。图像质量评价在图像处......
  • A*算法利用不在位数作为评价指标解决八数码问题(python)
    一、  程序设计思想:在一个3*3的方棋盘上放置着1,2,3,4,5,6,7,8总共八个数码,每个数码占一格,且有一个空格。这些数码可以在棋盘上移动,其移动规则是:与空格相邻的数码方......
  • A*算法利用曼哈顿距离作为评价指标解决八数码问题(python)
    1.题目说明在一个3*3的方棋盘上放置着1,2,3,4,5,6,7,8八个数码,每个数码占一格,且有一个空格。这些数码可以在棋盘上移动,其移动规则是:与空格相邻的数码方格可以移入空格......
  • 代码随想录算法训练营第四天LeetCode24,19,02
    代码随想录算法训练营第四天|LeetCode24,19,02.07,142LeetCode24两两交换链表中的节点题目链接:https://leetcode.cn/problems/swap-nodes-in-pairs/description///采用虚......