任务5 SVD分解的推荐应用
SVD原理可以参考其他博客, 我觉得svd实际上是利用矩阵分解来获取得到用户和物品的embeddding. 然后利用这些embedding来计算相似度进行推荐(这里用的是余弦相似度)
1. 代码实现
1. 相关类的定义
class CFModel(nn.Module):
'''
类描述:
用pytorch写的一个协同过滤函数,基于矩阵分解来实现的
成员变量:
users:
int类型
用户数目
movies:
int类型
电影数目
features:
int类型
embedding的维度, 默认是100维度
device:
string类型
用来训练的设备,可以是cpu也可以是GPU
方法:
__init__: 初始化函数
forward: 前向传播
'''
def __init__(self, users,movies, features=100, device='cpu'):
'''
函数说明:
初始化函数
输出:
users:
int类型
用户数目
movies:
int类型
电影数目
features:
int类型
embedding的维度, 默认是100维度
device:
string类型
用来训练的设备,可以是cpu也可以是GPU
'''
super(CFModel, self).__init__()
self.device = device
self.NUM_USER = users
self.NUM_MOVIE = movies
self.features = features
self.params = nn.ParameterDict(
{
'X': nn.Parameter(nn.init.normal_(torch.empty(self.NUM_USER, self.features), std=0.35), requires_grad=True),
'Y': nn.Parameter(nn.init.normal_(torch.empty(self.NUM_MOVIE, self.features), std=0.35), requires_grad=True),
}
)
def forward(self):
'''
函数描述:
就是简单的前向传播,在协同过滤里面,就是把用户和物品的embedding做个矩阵乘法
返回值:
tensor类型,维度为(用户数, 电影数)
用户对物品的打分矩阵
'''
return self.params['X'].mm(self.params['Y'].T)
class CollaborativeFiltering(object):
"""协同过滤类, 包含数据集, 训练方法等
member:
NUM_USER (int): 用户的数目
NUM_MOVIE(int): 电影的数目
rating(tensor): 打分矩阵
model(CFModel): 协同过滤对象
test_case(DataFrame): 测试集
lambda(float): 惩罚项系数
lr(float) : 学习率
device : 设备, 是用GPU还是用CPU来训练
method:
__init__: 初始化模型
loss_obj: 损失函数
RMSE : 评价指标,RMSE
train : 训练函数
save_model: 保存当前类当中的self.model的参数
load_model: 读取当前对象当中的self.model的参数
set_lambda: 设置惩罚项的大小
load_data: 读取数据集,并利用这些数据集来初始化一些参数
"""
def __init__(self, data_path,load_exist_model=False, name='ml-100k', features=100, Lambda = 0.2, lr=1e-3):
"""_summary_: 函数初始化
Args:
data_path (stirng): 数据集的路径
load_exist_model (bool, optional): 是否读取已经存在的模型. Defaults to False.
name (str, optional): 数据集的名称. Defaults to 'ml-100k'.
features (int, optional): embedding的维数. Defaults to 50.
Lambda (float, optional): 惩罚项的系数. Defaults to 0.2.
lr (float, optional): 学习率. Defaults to 1e-3.
"""
self.NUM_USER = None
self.NUM_MOVIE = None
self.rating = None
self.model = None
self.test_case = None
self.feature = features
self.Lambda = Lambda
self.lr = lr
self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
if load_exist_model:
self.load_entire_model()
return
self.load_data(datapath=data_path, name=name)
self.init_model()
def init_model(self):
'''
函数说明:
初始化当前对象当中的CFModule,并且将模型移植到对应的device上面
'''
self.model = CFModel(self.NUM_USER, self.NUM_MOVIE, self.feature,self.device)
self.model.to(self.device)
def Loss_obj(self, pred):
'''
函数说明:
计算损失函数:loss, 将每一个在rating矩阵当中大于0的位置的值,用真实值减去预测值来计算均方误差, 并且加上惩罚项
输入:
tensor
一个预测的矩阵, 这个矩阵的形状必须与rating矩阵的形状是一样的
输出:
float
对应的损失值
'''
index = (self.rating > 0).float()
loss = ((pred * index - self.rating)**2).sum()
# X_embedding = torch.norm(self.model.params['X'], dim=1)**2
# y_embedding = torch.norm(self.model.params['Y'], dim=1)**2
# return loss + self.Lambda * (X_embedding.sum() + y_embedding.sum())
return loss
def RMSE(self, ypred):
'''
函数说明:
计算测试集和预测当中的RMSE
输入:
ypred
输入的是预测的打分矩阵
输出:
float
测试集和真实数据的损失值
'''
predict = ypred.cpu().detach().numpy()
loss = 0.0
for i in self.test_case.values:
uid = i[0] - 1
item = i[1] - 1
target = i[2]
loss += (predict[uid][item] - target)**2
loss /= float(len(self.test_case))
return np.sqrt(loss)
def train(self, epoch):
"""_summary_
模型训练函数
Args:
epoch (int): 训练的epoch次数
Returns:
history(list): 训练过程中产生的损失值
metircs(list): 评估指标,每10个epoch记录一次
"""
optimizer = torch.optim.Adam(self.model.parameters(), lr=self.lr)
history = []
metrics = []
with tqdm(total=epoch) as t1:
for i in range(epoch):
optimizer.zero_grad()
pred= self.model.forward()
loss = self.Loss_obj(pred)
loss.backward()
history.append(loss.item())
optimizer.step()
if (i + 1) % 10 == 0:
metric = self.RMSE(pred)
metrics.append(metric)
t1.update(1)
return history, metrics
def save_model(self):
"""_summary_: 保存当前对象当中的self.model的模型参数
"""
torch.save(self.model.state_dict(), './model/params/UserCF_params.pkl')
def load_model(self):
"""_summary_: 读取模型参数给self.model
"""
self.model.load_state_dict(torch.load('./model/params/UserCF_params.pkl'))
def set_lambda(self, _lambda):
"""_summary_: 设置当前模型的惩罚项系数lambda
Args:
_lambda (float): 需要设置的惩罚项系数
"""
self.Lambda = _lambda
def load_data(self, datapath, name='ml-1m'):
"""_summary_: 读取数据集,并且划分训练集和测试集,最后利用数据集设置模型的一些参数
Args:
datapath (string): 数据集所在的路径
name (str, optional): 数据集的名称,可选的只有两种ml-100k, ml-1m. Defaults to 'ml-1m'.
"""
all_ratings = utils.loadData(filepath=datapath, name=name)
self.NUM_USER = all_ratings['userid'].max()
self.NUM_MOVIE = all_ratings['movieid'].max()
self.rating, self.test_case = utils.Split_Dataset_P(all_ratings, test_size=0.2)
self.rating = utils.rating2matrix(self.rating)
self.rating = torch.tensor(self.rating).to(self.device)
def save_entire_model(self, path='./model/params/', is_savemodel=True):
"""_summary_: 保存当前对象,首先先保存打分矩阵,然后保存测试集,最后保存模型的一些基本参数,最后可以选择的保存self.model的模型参数
Args:
path (str, optional): 保存模型的路径. Defaults to './model/params/'.
is_savemodel (bool, optional):是否保存self.model . Defaults to True.
"""
torch.save(self.rating, path+'rating.pt')
self.test_case.to_csv(path +'test_case.csv', index=False)
temp_dir = {k:v for k, v in self.__dict__.items() if k not in ['rating', 'test_case', 'model']}
torch.save(temp_dir, path + 'CollaborativeFiltering.pt')
if is_savemodel:
self.save_model()
print('保存模型完成')
def load_entire_model(self, path='./model/params/', is_loadmodel=True):
"""_summary_: 读取整个模型,先读取rating,在读取测试集,然后读取模型的一些基本参数
最后可以选择是否读取self.model的模型参数
Args:
path (str, optional): 模型参数的存储路径. Defaults to './model/params/'.
is_loadmodel (bool, optional): 是否读取self.model当中的模型参数. Defaults to True.
"""
self.rating = torch.load(path+'rating.pt')
self.test_case = pd.read_csv(path + 'test_case.csv')
temp_dir = torch.load(path + 'CollaborativeFiltering.pt')
for k, v in temp_dir.items():
self.__dict__[k] = v
if is_loadmodel:
self.init_model()
self.load_model()
1.2 开始训练
读取数据
model1 = CollaborativeFiltering('./data/', load_exist_model=False)
开始读取数据
用户人数为: 943
电影数目为: 1682
开始划分数据集
100%|██████████| 943/943 [00:07<00:00, 127.71it/s]
开始将数据转换成矩阵
用户人数为: 943
电影数目为: 1682
100%|██████████| 79619/79619 [00:06<00:00, 12641.68it/s]
训练500个epoch
history, metric = model1.train(500)
100%|██████████| 500/500 [00:10<00:00, 49.72it/s]
看看损失函数的变化曲线
import matplotlib.pyplot as plt
plt.plot(range(len(history)), history)
看看模型的RMSE
plt.plot(range(len(metric)), metric)
大概估计的分数也许是相差1.5分左右, 另外感觉训练并没有收敛到最小值点. 不过关系不大, 主要还是实现算法思路.