其他
【综述专栏】传统推荐方法相关论文和代码
在科学研究中,从方法论上来讲,都应“先见森林,再见树木”。当前,人工智能学术研究方兴未艾,技术迅猛发展,可谓万木争荣,日新月异。对于AI从业者来说,在广袤的知识森林中,系统梳理脉络,才能更好地把握趋势。为此,我们精选国内外优秀的综述文章,开辟“综述专栏”,敬请关注。
地址:https://zhuanlan.zhihu.com/p/437804827
01
1.1 论文
用户和物品的数据量庞大 推荐算法需要快速、及时的生成推荐列表 对于一个刚使用应用的用户,关于他的信息是稀疏的(冷启动) 推荐系统要及时的捕捉到当前用户每个新操作中的信息
用户数远远大于网站中的物品数 用户的交互数据过于稀疏,快速寻找相似用户是不现实的
1.2 代码
movies_titles.txt:电影的描述信息,每一行包括---电影ID、上映日期、电影名字
training_set.tar:训练数据集,包含17770个文件,每个文件是用户对于相应电影的评分,文件第一行表示电影的ID,然后接下来的每一行包括-用户ID、用户评分、评分日期
qualifying.txt:验证数据集 probe.txt:测试数据集
# coding=UTF-8
import os
import json
import random
import math
# 基于用户的协同过滤推荐算法
class UserCFRec:
# 初始化
def __init__(self, file_path, seed, k, n_items, K):
# 用户对电影的评分数据集路径
self.file_path = file_path
# 选取K个用户,只利用这些用户的评分信息进行推荐模型的训练
self.users_K = self.__select_K_users(K)
# 随机数种子
self.seed = seed
# 选取的近邻用户数
self.k = k
# 为每个用户推荐的电影数
self.n_items = n_items
# 训练集和测试集
self.train, self.test = self._load_and_split_data()
# 获取所有用户,并从中随机选取K个
def __select_K_users(self, K):
# train.json和test.json是指定的训练集和测试集
# 如果有的话使用指定的训练集和测试集
if os.path.exists("data/train.json") and os.path.exists("data/test.json"):
return list()
else:
print("随机选取K个用户!")
# 数据集中所有用户的集合
users = set()
# 获取所有用户
# 遍历训练数据集中每一个用户评分文件
for file in os.listdir(self.file_path):
# 获取文件名
one_path = "{}/{}".format(self.file_path, file)
print("{}".format(one_path))
# 打开每一个用户评分文件
with open(one_path, "r") as fp:
# 遍历文件中的每一行
for line in fp.readlines():
# 如果是第一行
# 是电影的ID
# 跳过
if line.strip().endswith(":"):
continue
# 否则是用户对于相应电影的评分
# 按照 ',' 分割出用户的ID
userID, _, _ = line.split(",")
# 添加进用户集合
users.add(userID)
# 随机选取K个用户
users_K = random.sample(list(users), K)
print(users_K)
return users_K
# 加载数据,并拆分为训练集和测试集
def _load_and_split_data(self):
train = dict()
test = dict()
# 如果指定了训练集和测试集
# 使用指定的训练集和测试集
if os.path.exists("data/train.json") and os.path.exists("data/test.json"):
print("从文件中加载训练集和测试集")
train = json.load(open("data/train.json"))
test = json.load(open("data/test.json"))
print("从文件中加载数据完成")
else:
# 设置产生随机数的种子,保证每次实验产生的随机结果一致
random.seed(self.seed)
# 遍历数据集
for file in os.listdir(self.file_path):
# 获取每一个电影评分文件
one_path = "{}/{}".format(self.file_path, file)
print("{}".format(one_path))
with open(one_path, "r") as fp:
# 第一行是电影的ID
movieID = fp.readline().split(":")[0]
# 后面每行是用户对于当前电影的评分
for line in fp.readlines():
if line.endswith(":"):
continue
# 以 ',' 分割每行
# 获取用户ID和电影评分
userID, rate, _ = line.split(",")
# 判断用户是否在所选择的K个用户中
if userID in self.users_K:
# 分割数据集
if random.randint(1, 50) == 1:
test.setdefault(userID, {})[movieID] = int(rate)
else:
train.setdefault(userID, {})[movieID] = int(rate)
print("加载数据到 data/train.json 和 data/test.json")
# 变为json格式
json.dump(train, open("data/train.json", "w"))
json.dump(test, open("data/test.json", "w"))
print("加载数据完成")
return train, test
def pearson(self, rating1, rating2):
# 计算两个用户之间的皮尔逊相关系数
# 是协方差除两个变量的标准差
# 计算用户之间的相似性
# rating1:用户1的评分记录,形式为
# {"电影ID1": 评分1, "电影ID2": 评分2, ...}
# rating2:用户2的评分记录,形式如
# {"电影ID1": 评分1, "电影ID2": 评分2, ...}
# 公式中的变量
# 自行查找一下'协方差简化计算公式','标准差简化计算公式'
sum_xy = 0
sum_x = 0
sum_y = 0
sum_x2 = 0
sum_y2 = 0
num = 0
# 如果两个用户同时对同一部电影进行了评分
for key in rating1.keys():
if key in rating2.keys():
num += 1
# 获取相应电影评分
x = rating1[key]
y = rating2[key]
sum_xy += x * y
sum_x += x
sum_y += y
sum_x2 += math.pow(x, 2)
sum_y2 += math.pow(y, 2)
if num == 0:
return 0
# 皮尔逊相关系数分母
# 两个标准差的乘积
# 上网查一下 '标准差简化公式'
# 就是下面这个
standard_deviation = math.sqrt(sum_x2 - math.pow(sum_x, 2) / num) * math.sqrt(sum_y2 - math.pow(sum_y, 2) / num)
# 分母不能为0
if standard_deviation == 0:
return 0
else:
# 分子是协方差简化公式,可自行查一下
return (sum_xy - (sum_x * sum_y) / num) / standard_deviation
def recommend(self, userID):
# 向userID进行推荐
# 获取跟当前用户最相似的用户
neighbor_user = dict()
# 获取训练集中的用户
for user in self.train.keys():
if userID != user:
# 计算皮尔逊相关系数
distance = self.pearson(self.train[userID], self.train[user])
neighbor_user[user] = distance
# 排序
# 获取最相似的用户
most_similar_user = sorted(neighbor_user.items(), key=lambda k: k[1], reverse=True)
# 要推荐的电影
movies = dict()
for (sim_user, sim) in most_similar_user[:self.k]:
for movieID in self.train[sim_user].keys():
movies.setdefault(movieID, 0)
# 累计评分
movies[movieID] += sim * self.train[sim_user][movieID]
recommended_movies = sorted(movies.items(), key=lambda k: k[1], reverse=True)
# 推荐电影列表
return recommended_movies
#基于物品的协同过滤推荐算法
class ItemCFRec:
def __init__(self, datafile, ratio):
# 用户对电影的评分数据集路径
self.datafile = datafile
# 测试集与训练集的分割比例
self.ratio = ratio
# 加载评分数据集
self.data = self.loadData()
# 分割数据集
self.trainData, self.testData = self.splitData(3, 47)
# 获取电影之间的共现矩阵
self.items_sim = self.ItemSimilarityBest()
# 加载评分数据到data
def loadData(self):
print("加载数据...")
data = []
for line in open(self.datafile):
# 评分数据集中每一行包含 用户ID 物品ID 评分 评分时间
userid, itemid, record, _ = line.split("::")
data.append((userid, itemid, int(record)))
return data
# 分割数据集
def splitData(self, k, seed, M=9):
# 0<k<M 分割参数
# M随机数上限
# seed随机数种子
print("训练数据集与测试数据集切分...")
train, test = {}, {}
random.seed(seed)
for user, item, record in self.data:
# 分割数据集
if random.randint(0, M) == k:
test.setdefault(user, {})
test[user][item] = record
else:
train.setdefault(user, {})
train[user][item] = record
return train, test
# 计算物品之间的相似度
def ItemSimilarityBest(self):
print("计算物品之间的相似度")
# 已经算完了,直接用
if os.path.exists("data/item_sim.json"):
itemSim = json.load(open("data/item_sim.json", "r"))
else:
# 物品相似度
itemSim = dict()
# 用户对每个不同电影的评分看作一次行为
# 计算行为次数
item_user_count = dict()
# 物品的共现矩阵
count = dict()
# 遍历训练数据集
# 获取共现矩阵
# 共现矩阵每个元素表示同时喜欢两个物品的用户数
# 是实对称稀疏矩阵
for user, item in self.trainData.items():
# 遍历当前用户所评分过的所有电影
for i in item.keys():
# 进行记录
item_user_count.setdefault(i, 0)
# 用户进行过评分
if self.trainData[str(user)][i] > 0.0:
# 记录行为次数
item_user_count[i] += 1
for j in item.keys():
count.setdefault(i, {}).setdefault(j, 0)
# 同时对两个电影进行了评分
if self.trainData[str(user)][i] > 0.0 and self.trainData[str(user)][j] > 0.0 and i != j:
count[i][j] += 1
# 共现矩阵 -> 相似度矩阵
# 遍历共现矩阵
for i, related_items in count.items():
itemSim.setdefault(i, dict())
for j, cuv in related_items.items():
itemSim[i].setdefault(j, 0)
# 电影i,j之间的相似性
# 是同时对电影i,j进行了评分的行为
# 除以各自的行为乘积再开根号
# 可以抑制热门商品的曝光程度
itemSim[i][j] = cuv / math.sqrt(item_user_count[i] * item_user_count[j])
# 变为json格式
json.dump(itemSim, open('data/item_sim.json', 'w'))
return itemSim
# 为用户进行推荐
def recommend(self, user, k=8, nitems=40):
# 为user进行推荐
# k是和用户每部喜欢的电影最相似的电影数
# nitems是推荐的电影数
result = dict()
# 获取当前用户进行过评分的电影和相应评分
u_items = self.trainData.get(user, {})
# i是评分过的电影
# pi是当前用户对其的评分
for i, pi in u_items.items():
# 从相似矩阵中获取k个和当前电影i最相似的电影和对应的相似值
for j, wj in sorted(self.items_sim[i].items(), key=lambda x: x[1], reverse=True)[0:k]:
# 如果用户已经喜欢这部电影了
# 不算,重新推荐
if j in u_items:
continue
# 加入到推荐结果中
result.setdefault(j, 0)
result[j] += pi * wj
return dict(sorted(result.items(), key=lambda x: x[1], reverse=True)[0:nitems])
02
2.1 论文
首先计算出用户和物品的隐向量 计算用户和物品隐向量内积 推荐给用户内积值比较高的物品
2.2 代码
import pickle
import random
from math import exp
import numpy as np
import pandas as pd
class MF:
# 模型初始化
def __init__(self):
# 用户、物品隐向量的维度
self.class_count = 5
# 训练次数
self.iter_count = 5
# 学习率
self.lr = 0.02
# 正则化系数
self.lam = 0.01
# 获取评分矩阵的分解矩阵p,q
self._init_model()
# 初始化
# 随机生成评分矩阵的分解矩阵p,q
def _init_model(self):
# 评分数据集路径
file_path = 'data/ratings.csv'
# 用户对所有电影的行为集合
pos_neg_path = 'data/lfm_items.dict'
# 读取评分
self.uiscores = pd.read_csv(file_path)
# 获取所有的用户ID
self.user_ids = set(self.uiscores['UserID'].values)
# 获取所有电影ID
self.item_ids = set(self.uiscores['MovieID'].values)
# 加载用户互动电影集合
self.items_dict = pickle.load(open(pos_neg_path, 'rb'))
# 先随机生成评分矩阵的分解矩阵
# 后面通过训练过程调整
array_p = np.random.randn(len(self.user_ids), self.class_count)
array_q = np.random.randn(len(self.item_ids), self.class_count)
# 转换为DataFrame格式
self.p = pd.DataFrame(array_p, columns=range(0, self.class_count), index=list(self.user_ids))
self.q = pd.DataFrame(array_q, columns=range(0, self.class_count), index=list(self.item_ids))
# 计算用户 user_id 对 item_id的感兴趣程度
def _predict(self, user_id, item_id):
# Pandas 的ix()函数会先以loc()获取指定索引的值
# 如果没获取到会继续尝试以iloc()获取
# 已经不推荐使用
# 这里可以改成loc[user_id]
# mat转换为矩阵
# 因为p,q要相乘
# 注意q进行了转置 .T
p = np.mat(self.p.ix[user_id].values)
q = np.mat(self.q.ix[item_id].values).T
r = (p * q).sum()
# 借助sigmoid函数,转化为是否感兴趣的概率
logit = 1.0 / (1 + exp(-r))
return logit
# 损失函数
def _loss(self, user_id, item_id, y, step):
e = pow((y - self._predict(user_id, item_id)), 2) / len(y)
print('Step: {}, user_id: {}, item_id: {}, y: {}, loss: {}'.format(step, user_id, item_id, y, e))
return e
# 带正则化的梯度下降
def _optimize(self, user_id, item_id, e):
# 梯度
gradient_p = -e * self.q.ix[item_id].values
# 正则化项
l2_p = self.lam * self.p.ix[user_id].values
# 梯度下降p更新的部分
delta_p = self.lr * (gradient_p + l2_p)
# q
# 同理
gradient_q = -e * self.p.ix[user_id].values
l2_q = self.lam * self.q.ix[item_id].values
delta_q = self.lr * (gradient_q + l2_q)
# 梯度下降更新
self.p.loc[user_id] -= delta_p
self.q.loc[item_id] -= delta_q
# 训练模型
def train(self):
for step in range(0, self.iter_count):
# 获取用户进行过评分和没进行过评分的电影集合
for user_id, item_dict in self.items_dict.items():
print('Step: {}, user_id: {}'.format(step, user_id))
# 获取集合中的每一个电影
item_ids = list(item_dict.keys())
random.shuffle(item_ids)
# 计算损失,进行更新
for item_id in item_ids:
# item_dict[item_id]是真实的评分标签
# 与模型的预测值计算损失
e = self._loss(user_id, item_id, item_dict[item_id], step)
self._optimize(user_id, item_id, e)
# 降低学习率
self.lr *= 0.9
# 保存模型
self.save()
# 保存模型
def save(self):
f = open('data/lfm.model', 'wb')
pickle.dump((self.p, self.q), f)
f.close()
03
3.1 论文
3.1.1 FM(Factorization Machines)
3.1.2 FFM(Field-aware Factorization Machines)
3.2 代码
class FM(nn.Module):
def __init__(self, dim, k):
super(FM, self).__init__()
# 特征维度
self.dim = dim
# 特征隐向量维度
self.k = k
# FM的一阶部分,公式的前两项
self.w = nn.Linear(self.dim, 1, bias=True)
# 初始化V矩阵
self.v = nn.Parameter(torch.rand(self.dim, self.k) / 100)
# 前向传递,建立计算图
def forward(self, x):
# 输入的X是特征矩阵
# 维度[batch_size,dim]
# 先计算公式的线性部分
linear = self.w(x)
# 计算二阶特征交叉部分
# 用的是论文中的化简公式
quadradic = 0.5 * torch.sum(torch.pow(torch.mm(x, self.v), 2) - torch.mm(torch.pow(x, 2), torch.pow(self.v, 2)))
# 转换为概率
return torch.sigmoid(linear + quadradic)
04
4.1 论文
4.1.1 GBDT+LR
4.1.2 LS-PLM
4.2 代码
# coding=UTF-8
import pandas as pd
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder
class GBDTAndLR:
def __init__(self):
# 训练数据集路径
self.file = "data/new_churn.csv"
# 获取数据
self.data = self.load_data()
# 拆分数据
self.train, self.test = self.split()
# 加载数据
def load_data(self):
return pd.read_csv(self.file)
# 拆分数据集
def split(self):
# train:test = 9:1 拆分
train, test = train_test_split(self.data, test_size=0.1, random_state=40)
return train, test
# 模型训练
def train_model(self):
# 获取特征和标签
lable = "Churn"
ID = "customerID"
x_columns = [x for x in self.train.columns if x not in [lable, ID]]
x_train = self.data[x_columns]
y_train = self.data[lable]
# 创建gbdt模型,并训练
gbdt = GradientBoostingClassifier()
gbdt.fit(x_train, y_train)
# 创建lr模型,并训练
lr = LogisticRegression()
lr.fit(x_train, y_train)
# 模型融合
# 就是把GBDT的输出作为LR的输入
gbdt_lr = LogisticRegression()
# one-hot编码
enc = OneHotEncoder()
# 100是迭代次数,调整维度
# 先对输入数据进行one-hot编码
# 然后输入到GBDT进行特征提取
# 然后将GBDT的输出作为LR的输入
enc.fit(gbdt.apply(x_train).reshape(-1, 100))
gbdt_lr.fit(enc.transform(gbdt.apply(x_train).reshape(-1, 100)), y_train)
return enc, gbdt, lr, gbdt_lr
本文目的在于学术交流,并不代表本公众号赞同其观点或对其内容真实性负责,版权归原作者所有,如有侵权请告知删除。
“综述专栏”历史文章
时序预测的DL-based方法总结:Attention、Transformer、GNN、GAN、...
Graph Neural Networks (GNN)综述简介
当BERT遇上知识图谱
零样本学习--一文先了解
小样本学习方法(FSL)演变过程
多智能体强化学习算法总结
A Theory of Domain Adaptation
NLP实操手册: 基于Transformer的深度学习架构的应用指南(综述)
顶会的宠儿:元学习(Meta-learning)的前世今生
多模态知识图谱前沿进展
图神经网络及其在视觉/医学图像中的应用
Transformer+self-attention超详解(亦个人心得)
DNN与两大门派,一念神魔,功不唐捐
最新最全的视觉 Transformer 综述请查收!
推荐系统里预训练的工作
更多综述专栏文章,
请点击文章底部“阅读原文”查看
分享、点赞、在看,给个三连击呗!