目录
一、BPR算法的作用
把每个用户对应的所有商品按照喜好排序,一个更朴素的思想就是用户交互过的项目的优先级一定没有交互过的项目优先级高,这就是BPR算法的核心内容。
二、显式反馈和隐式反馈
显式反馈:用户明确喜欢和不喜欢的商品,例如用户的评分。
隐式反馈:用户浏览过的商品,并没有明确表示喜欢或者不喜欢,这种类型只能全部认为是正反馈,即喜欢的商品。用户对商品的交互行为,如购买,浏览都属于隐式反馈。
1 显示与隐式反馈的特征
隐式反馈 | 显式反馈 | |
---|---|---|
准确度 | 底 | 高 |
丰富的 | 高 | 底 |
获取数据 | 容易 | 困难 |
数据噪音 | 较难识别 | 较易识别 |
上下文敏感 | 是 | 是 |
用户偏好的表达能力 | 只有正样本 | 包含正负样本 |
评估比较标准 | 相对比较 | 绝对比较 |
1、显式反馈
表 2-2 显式反馈模型的评价方法
模型判定推荐的 | 模型判定不推荐的 | |
测试集中被推荐的(被检索到) | True positives(TP 正类判断为正类)(搜到的也想要的) | False positives(FP 负类判断为正类)(搜到的但没用的) |
测试集中不被推荐的(未被检索到) | False negatives(FN 正类判断为负类)(没搜到,然而实际想要的 | True negatives(TN 负类判定为负类)(没搜到也没用的) |
先假定一个具体场景作为例子。
假如某个班级有男生80人,女生20人,共计100人.目标是找出所有女生.
某人挑选出50个人,其中20人是女生,另外还错误的把30个男生也当作女生挑选出来了.
作为评估者的你需要来评估(evaluation)下他的工作。
我们需要先需要定义TP,FN,FP,TN四种分类情况。
按照前面例子,我们需要从一个班级中的人中寻找所有女生,如果把这个任务当成一个分类器的话,那么女生就是我们需要的,而男生不是,所以我们称女生为"正类",而男生为"负类"。
相关(Relevant),正类 | 无关(NonRelevant),负类 | |
被检索到(Retrieved) | TP=20 true positives(TP 正类判定为正类,例子中就是正确的判定"这位是女生") | FP=30 false positives(FP 负类判定为正类,“存伪”,例子中就是分明是男生却判断为女生) |
未被检索到(Not Retrieved) | FN=0 false negatives(FN 正类判定为负类,“去真”,例子中就是,分明是女生,这哥们却判断为男生–梁山伯同学犯的错就是这个) | TN=50 true negatives(TN 负类判定为负类,也就是一个男生被判断为男生) |
通过这张表,我们可以很容易得到例子中这几个分类的值:TP=20,FP=30,FN=0,TN=50。
我们可以通过计算模型分类的准确率,和召回率进来通过计算F-Measure的值来对模型分类的好坏进行评价。
下面结合例子中这几个分类的值:TP=20,FP=30,FN=0,TN=50介绍准确率,召回率,和F-measure的概念与计算方法。
2、隐式反馈
隐性反馈数据有诸多弊端,例如不明确,具有噪点数据,但是由于他广泛存在,我们有时甚至只能利用它,所以还是要详细研究一下,通过对隐式反馈的合理降噪以及数据修剪来提升物品推荐的可行度。
显性反馈数据可以看出用户对某一物品的偏好值,例如评分机制,8分和10分的区别,而隐性反馈数据没办法衡量偏好值,只能认为用户浏览同一内容越多,越有可能喜好这个内容,也即置信度越大。
隐式反馈数据的处理方式
在使用隐式反馈的情况下,我们会发现观察到的数据均为正例(因为用户对物品交互过才会被观察到),而那些没有被观察到的数据(即用户还没有产生行为的物品),分为两种情况,一种是用户对该物品确实没有星期(负类),另一种是缺失值(即用户以后可能会产生行为的物品),而在传统的个性化推荐中通常是计算用户u对物品i的个性化分数,然后根据个性化分数进行排序。而其处理数据的方式为把所有观察到的隐式反馈作为正类,而其余数据作为负类。
在负类被填零的情况下,我们优化目标变成了希望在预测时观测到的数据预测为1,其余的均为0,于是产生的问题是,我们希望模型在以后预测的缺失值,在训练时却都被认为时负类数据。因此这个模型训练的足够好,那么最总都得到的结果就是这些未观察的样本最后预测值都是0.
而针对与BPR算法是根据隐式反馈数据来进行比较的,通过对问题进行贝叶斯分析得到的最大后验概率来对item进行排序,进而产生推荐。
三、BPR算法
1、概念
BPR(Bayesian Personalized Ranking),中文名称为贝叶斯个性化排序,是当下推荐系统中常用的一种推荐算法。与其他的基于用户评分矩阵的方法不同的是BPR主要采用用户的隐式反馈(如点击、收藏、加入购物车等),通过对问题进行贝叶斯分析得到的最大后验概率来对item进行排序,进而产生推荐。
2、相关定义
U代表所有用户user集合;I代表所有物品item集合;
S代表所有用户的隐式反馈
如下图所示,只要用户对某个物品产生过行为(如点击、收藏、加入购物车等),就标记为+,所有+样本构成了S。那些为观察到的数据(即用户没有产生行为的数据)标记为?
3、建模思路
在BPR算法中,我们将任意用户u对应的物品进行标记,如果用户u在同时有物品i和j的时候对i产生了行为,那么我们就得到了一个三元组<u,i,j>,它表示对用户u来说,i的排序要比j靠前。但是如果一个用户对两个物品同时产生过行为,或者同时没有产生行为,则无法构成偏好对。如果对于用户u来说我们有m组这样的反馈,那么我们就可以得到m组用户u对应的训练样本。可以看到BPR采用了pairwise的方式。
那么注意,对于每个三元组样本<u,i,j>,i必然时产生过行为的物品,而j必然时未被产生过行为的物品,因此只包括下图右边分解后+的数据,不包括-的数据。
四、BPR优化
五、算法流程
下面简要总结下BPR的算法训练流程:
输入:训练集D三元组,梯度步长α,正则化参数λ,分解矩阵维度k。
输出:模型参数,矩阵W,H
1. 随机初始化矩阵W,H
2. 迭代更新模型参数:
3. 如果W,H收敛,则算法结束,输出W,H,否则回到步骤2.
当我们拿到W,H后,就可以计算出每一个用户u对应的任意一个商品的排序分:最终选择排序分最高的若干商品输出。
六、结束
BPR是基于矩阵分解的一种排序算法,但是和funkSVD之类的算法比,它不是做全局的评分优化,而是针对每一个用户自己的商品喜好分贝做排序优化。因此在迭代优化的思路上完全不同。同时对于训练集的要求也是不一样的,funkSVD只需要用户物品对应评分数据二元组做训练集,而BPR则需要用户对商品的喜好排序三元组做训练集。
七、代码实现
主函数代码
# !/usr/bin/env python
# @Time:2021/4/6 19:21
# @Author:华阳
# @File:Basical BPR.py
# @Software:PyCharm
import random
from collections import defaultdict
import numpy as np
from sklearn.metrics import roc_auc_score
import scores
'''
函数说明:BPR类(包含所需的各种参数)
Parameters:
无
Returns:
无
'''
class BPR:
#用户数
user_count = 943
#项目数
item_count = 1682
#k个主题,k数
latent_factors = 20
#步长α
lr = 0.01
#参数λ
reg = 0.01
#训练次数
train_count = 10000
#训练集
train_data_path = 'train.txt'
#测试集
test_data_path = 'test.txt'
#U-I的大小
size_u_i = user_count * item_count
# 随机设定的U,V矩阵(即公式中的Wuk和Hik)矩阵
U = np.random.rand(user_count, latent_factors) * 0.01 #大小无所谓
V = np.random.rand(item_count, latent_factors) * 0.01
biasV = np.random.rand(item_count) * 0.01
#生成一个用户数*项目数大小的全0矩阵
test_data = np.zeros((user_count, item_count))
print("test_data_type",type(test_data))
#生成一个一维的全0矩阵
test = np.zeros(size_u_i)
#再生成一个一维的全0矩阵
predict_ = np.zeros(size_u_i)
#获取U-I数据对应
'''
函数说明:通过文件路径,获取U-I数据
Paramaters:
输入要读入的文件路径path
Returns:
输出一个字典user_ratings,包含用户-项目的键值对
'''
def load_data(self, path):
user_ratings = defaultdict(set)
with open(path, 'r') as f:
for line in f.readlines():
u, i = line.split(" ")
u = int(u)
i = int(i)
user_ratings[u].add(i)
return user_ratings
'''
函数说明:通过文件路径,获取测试集数据
Paramaters:
测试集文件路径path
Returns:
输出一个numpy.ndarray文件(n维数组)test_data,其中把含有反馈信息的数据置为1
'''
#获取测试集的评分矩阵
def load_test_data(self, path):
file = open(path, 'r')
for line in file:
line = line.split(' ')
user = int(line[0])
item = int(line[1])
self.test_data[user - 1][item - 1] = 1
'''
函数说明:对训练集数据字典处理,通过随机选取,(用户,交互,为交互)三元组,更新分解后的两个矩阵
Parameters:
输入要处理的训练集用户项目字典
Returns:
对分解后的两个矩阵以及偏置矩阵分别更新
'''
def train(self, user_ratings_train):
for user in range(self.user_count):
# 随机获取一个用户
u = random.randint(1, self.user_count) #找到一个user
# 训练集和测试集的用于不是全都一样的,比如train有948,而test最大为943
if u not in user_ratings_train.keys():
continue
# 从用户的U-I中随机选取1个Item
i = random.sample(user_ratings_train[u], 1)[0] #找到一个item,被评分
# 随机选取一个用户u没有评分的项目
j = random.randint(1, self.item_count)
while j in user_ratings_train[u]:
j = random.randint(1, self.item_count) #找到一个item,没有被评分
#构成一个三元组(uesr,item_have_score,item_no_score)
# python中的取值从0开始
u = u - 1
i = i - 1
j = j - 1
#BPR
r_ui = np.dot(self.U[u], self.V[i].T) + self.biasV[i]
r_uj = np.dot(self.U[u], self.V[j].T) + self.biasV[j]
r_uij = r_ui - r_uj
loss_func = -1.0 / (1 + np.exp(r_uij))
# 更新2个矩阵
self.U[u] += -self.lr * (loss_func * (self.V[i] - self.V[j]) + self.reg * self.U[u])
self.V[i] += -self.lr * (loss_func * self.U[u] + self.reg * self.V[i])
self.V[j] += -self.lr * (loss_func * (-self.U[u]) + self.reg * self.V[j])
# 更新偏置项
self.biasV[i] += -self.lr * (loss_func + self.reg * self.biasV[i])
self.biasV[j] += -self.lr * (-loss_func + self.reg * self.biasV[j])
'''
函数说明:通过输入分解后的用户项目矩阵得到预测矩阵predict
Parameters:
输入分别后的用户项目矩阵
Returns:
输出相乘后的预测矩阵,即我们所要的评分矩阵
'''
def predict(self, user, item):
predict = np.mat(user) * np.mat(item.T)
return predict
#主函数
def main(self):
#获取U-I的{1:{2,5,1,2}....}数据
user_ratings_train = self.load_data(self.train_data_path)
#获取测试集的评分矩阵
self.load_test_data(self.test_data_path)
#将test_data矩阵拍平
for u in range(self.user_count):
for item in range(self.item_count):
if int(self.test_data[u][item]) == 1:
self.test[u * self.item_count + item] = 1
else:
self.test[u * self.item_count + item] = 0
#训练
for i in range(self.train_count):
self.train(user_ratings_train) #训练10000次完成
predict_matrix = self.predict(self.U, self.V) #将训练完成的矩阵內积
# 预测
self.predict_ = predict_matrix.getA().reshape(-1) #.getA()将自身矩阵变量转化为ndarray类型的变量
print("predict_new",self.predict_)
self.predict_ = pre_handel(user_ratings_train, self.predict_, self.item_count)
auc_score = roc_auc_score(self.test, self.predict_)
print('AUC:', auc_score)
# Top-K evaluation
scores.topK_scores(self.test, self.predict_, 5, self.user_count, self.item_count)
'''
函数说明:对结果进行修正,即用户已经产生交互的用户项目进行剔除,只保留没有产生用户项目的交互的数据
Paramaters:
输入用户项目字典集,以及一维的预测矩阵,项目个数
Returns:
输出修正后的预测评分一维的预测矩阵
'''
def pre_handel(set, predict, item_count):
# Ensure the recommendation cannot be positive items in the training set.
for u in set.keys():
for j in set[u]:
predict[(u - 1) * item_count + j - 1] = 0
return predict
if __name__ == '__main__':
#调用类的主函数
bpr = BPR()
bpr.main()
计算模型指标的代码
# -*- coding: utf-8 -*-
"""scores.ipynb
Automatically generated by Colaboratory.
Original file is located at
https://colab.research.google.com/drive/17qoo1U4Iw58GRDDIyCaB2GmbRUg1gTPd
"""
import heapq
import numpy as np
import math
#计算项目top_K分数
def topK_scores(test, predict, topk, user_count, item_count):
PrecisionSum = np.zeros(topk+1)
RecallSum = np.zeros(topk+1)
F1Sum = np.zeros(topk+1)
NDCGSum = np.zeros(topk+1)
OneCallSum = np.zeros(topk+1)
DCGbest = np.zeros(topk+1)
MRRSum = 0
MAPSum = 0
total_test_data_count = 0
for k in range(1, topk+1):
DCGbest[k] = DCGbest[k - 1]
DCGbest[k] += 1.0 / math.log(k + 1)
for i in range(user_count):
user_test = []
user_predict = []
test_data_size = 0
for j in range(item_count):
if test[i * item_count + j] == 1.0:
test_data_size += 1
user_test.append(test[i * item_count + j])
user_predict.append(predict[i * item_count + j])
if test_data_size == 0:
continue
else:
total_test_data_count += 1
predict_max_num_index_list = map(user_predict.index, heapq.nlargest(topk, user_predict))
predict_max_num_index_list = list(predict_max_num_index_list)
hit_sum = 0
DCG = np.zeros(topk + 1)
DCGbest2 = np.zeros(topk + 1)
for k in range(1, topk + 1):
DCG[k] = DCG[k - 1]
item_id = predict_max_num_index_list[k - 1]
if user_test[item_id] == 1:
hit_sum += 1
DCG[k] += 1 / math.log(k + 1)
# precision, recall, F1, 1-call
prec = float(hit_sum / k)
rec = float(hit_sum / test_data_size)
f1 = 0.0
if prec + rec > 0:
f1 = 2 * prec * rec / (prec + rec)
PrecisionSum[k] += float(prec)
RecallSum[k] += float(rec)
F1Sum[k] += float(f1)
if test_data_size >= k:
DCGbest2[k] = DCGbest[k]
else:
DCGbest2[k] = DCGbest2[k-1]
NDCGSum[k] += DCG[k] / DCGbest2[k]
if hit_sum > 0:
OneCallSum[k] += 1
else:
OneCallSum[k] += 0
# MRR
p = 1
for mrr_iter in predict_max_num_index_list:
if user_test[mrr_iter] == 1:
break
p += 1
MRRSum += 1 / float(p)
# MAP
p = 1
AP = 0.0
hit_before = 0
for mrr_iter in predict_max_num_index_list:
if user_test[mrr_iter] == 1:
AP += 1 / float(p) * (hit_before + 1)
hit_before += 1
p += 1
MAPSum += AP / test_data_size
print('MAP:', MAPSum / total_test_data_count)
print('MRR:', MRRSum / total_test_data_count)
print('Prec@5:', PrecisionSum[4] / total_test_data_count)
print('Rec@5:', RecallSum[4] / total_test_data_count)
print('F1@5:', F1Sum[4] / total_test_data_count)
print('NDCG@5:', NDCGSum[4] / total_test_data_count)
print('1-call@5:', OneCallSum[4] / total_test_data_count)
return
原文链接:https://blog.csdn.net/weixin_46099084/article/details/109011670