说明
分为三部分:
(1)Corpus类:相当于是一个语料库吧,用来存储 训练用的样本的, 产生过交互的为正样本, 以没有在当前用户产生交互的样本为负例样本。
(2)LFM类:主模型部分,包含数据加载、训练、预测的过程。
这里是目的是构建一个兴趣矩阵, 这个兴趣矩阵的形状为(len(self.user_ids),len(self.item_ids)) 然而这样一个非常大的兴趣矩阵是无法直接保存创建的,所以这里的方法是 利用矩阵分解的方式,使用(len(self.user_ids),假定类目分类数量class_count) * (假定类目分类数量class_count,len(self.user_ids))来代表 通过这样矩阵分解的方式。
我们要维护的矩阵就可以小的多的多,但是对于以上两个矩阵该怎么确定,矩阵内的参数呢,这个时候就需要使用梯度下降的方式了。 所以这里其实训练的就是上面两个矩阵的参数,利用两个矩阵来计算还原 用户对商品的兴趣度。 而对矩阵参数训练的方式则是使用梯度下降的方式,具体训练的样本来说的话,可以主要看下Corpus函数,这里是采取与用户产生过交互的样本 为正例样本,以整个样本集中没有产生过的样本为负例样本。 这意思就是 对应相当于标识看过的为兴趣度高 1,没看过的兴趣度低 0。
具体应用可以参照下_predict 函数,是找潜在的用户还没看过的电影里面,逐一根据特征 利用得到q和p矩阵,代入计算得到兴趣值。 针对每个用户,利用兴趣值进行排序, 找到前n个作为推荐吧。
(3)主训练训练启动部分,这里使用的数据集此处下载 https://pan.baidu.com/s/1RXUuqSqkCJNp4fY5ZdPqKA 提取码:jya7
注:训练学习到一个整体的兴趣矩阵,利用已知的交互情况作为训练集,让模型学习到对未知的情况的兴趣捕获, 但是这里有个问题是只基于位置感知,而没有特征作为支撑的话,这样的兴趣预测能准吗?
代码
Corpus类
# coding: utf-8 -*-
import random
import pickle
import pandas as pd
import numpy as np
from math import exp
class Corpus:
'''
这个类是用来干嘛的呢?
看明白了,这相当于是一个语料库吧,用来存储 训练用的样本的, 产生过交互的为正样本, 以没有在当前用户产生交互的样本为负例样本
set([3,4])^set([2,3,4]) = {2} (看明白 ^的作用)
'''
items_dict_path = '../data/lfm_items.dict'
@classmethod
def pre_process(cls):
file_path = '../data/ratings.csv'
cls.frame = pd.read_csv(file_path)
cls.user_ids = set(cls.frame['UserID'].values)
cls.item_ids = set(cls.frame['MovieID'].values)
cls.items_dict = {user_id: cls._get_pos_neg_item(user_id) for user_id in list(cls.user_ids)}
cls.save()
@classmethod
def _get_pos_neg_item(cls, user_id):
"""
Define the pos and neg item for user.
pos_item mean items that user have rating, and neg_item can be items
that user never see before.
Simple down sample method to solve unbalance sample.
"""
print('Process: {}'.format(user_id))
pos_item_ids = set(cls.frame[cls.frame['UserID'] == user_id]['MovieID'])
neg_item_ids = cls.item_ids ^ pos_item_ids
# neg_item_ids = [(item_id, len(self.frame[self.frame['MovieID'] == item_id]['UserID'])) for item_id in neg_item_ids]
# neg_item_ids = sorted(neg_item_ids, key=lambda x: x[1], reverse=True)
neg_item_ids = list(neg_item_ids)[:len(pos_item_ids)]
item_dict = {}
for item in pos_item_ids: item_dict[item] = 1
for item in neg_item_ids: item_dict[item] = 0
return item_dict
@classmethod
def save(cls):
f = open(cls.items_dict_path, 'wb')
pickle.dump(cls.items_dict, f)
f.close()
@classmethod
def load(cls):
f = open(cls.items_dict_path, 'rb')
items_dict = pickle.load(f)
f.close()
return items_dict
LFM类
class LFM:
'''
好吧, 看了一遍代码,算是完全的明白了,这里是目的是构建一个兴趣矩阵, 这个兴趣矩阵的形状为(len(self.user_ids),len(self.item_ids))
然而这样一个非常大的兴趣矩阵是无法直接保存创建的,所以这里的方法是 利用矩阵分解的方式,使用(len(self.user_ids),假定类目分类数量class_count) * (假定类目分类数量class_count,len(self.user_ids))来代表
通过这样矩阵分解的方式,我们要维护的矩阵就可以小的多的多,但是对于以上两个矩阵该怎么确定,矩阵内的参数呢,这个时候就需要使用梯度下降的方式了。
所以这里其实训练的就是上面两个矩阵的参数,利用两个矩阵来计算还原 用户对商品的兴趣度。
而对矩阵参数训练的方式则是使用梯度下降的方式,具体训练的样本来说的话,可以主要看下Corpus函数,这里是采取与用户产生过交互的样本
为正例样本,以整个样本集中没有产生过的样本为负例样本。 这意思就是 对应相当于标识看过的为兴趣度高 1,没看过的兴趣度低 0。
具体应用可以参照下_predict 函数,是找潜在的用户还没看过的电影里面,逐一根据特征 利用得到q和p矩阵,代入计算得到兴趣值。
针对每个用户,利用兴趣值进行排序, 找到前n个作为推荐吧。
* 明白了吧,真的是一看代码就完全理解了, 这里目的就是得到用户 对 商品大的 兴趣值矩阵, 只不过是使用假定的类目数 class_num下的
p和q矩阵来模拟计算, 通过计算最大兴趣度的 矩阵进行推荐即可。 这里的训练是训练矩阵参数,样本是按照交互记录进行的假定吧(是个二分类,用其他项计算的是概率)。
'''
def __init__(self):
self.class_count = 5
self.iter_count = 5
self.lr = 0.02
self.lam = 0.01
self._init_model()
def _init_model(self):
"""
Get corpus and initialize model params.
"""
file_path = '../data/ratings.csv'
self.frame = pd.read_csv(file_path)
self.user_ids = set(self.frame['UserID'].values)
self.item_ids = set(self.frame['MovieID'].values)
self.items_dict = Corpus.load()
array_p = np.random.randn(len(self.user_ids), self.class_count)
array_q = np.random.randn(len(self.item_ids), self.class_count)
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))
def _predict(self, user_id, item_id):
"""
Calculate interest between user_id and item_id.
p is the look-up-table for user's interest of each class.
q means the probability of each item being classified as each class.
"""
p = np.mat(self.p.ix[user_id].values)
q = np.mat(self.q.ix[item_id].values).T
r = (p * q).sum()
logit = 1.0 / (1 + exp(-r))
return logit
def _loss(self, user_id, item_id, y, step):
"""
Loss Function define as MSE, the code write here not that formula you think.
"""
e = y - self._predict(user_id, item_id)
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):
"""
Use SGD as optimizer, with L2 p, q square regular.
e.g: E = 1/2 * (y - predict)^2, predict = matrix_p * matrix_q
derivation(E, p) = -matrix_q*(y - predict), derivation(E, q) = -matrix_p*(y - predict),
derivation(l2_square,p) = lam * p, derivation(l2_square, q) = lam * q
delta_p = lr * (derivation(E, p) + derivation(l2_square,p))
delta_q = lr * (derivation(E, q) + derivation(l2_square, q))
"""
gradient_p = -e * self.q.ix[item_id].values
l2_p = self.lam * self.p.ix[user_id].values
delta_p = self.lr * (gradient_p + l2_p)
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):
"""
Train model.
"""
for step in range(0, self.iter_count):
for user_id, item_dict in self.items_dict.items():
item_ids = list(item_dict.keys())
random.shuffle(item_ids)
for item_id in item_ids:
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 predict(self, user_id, top_n=10):
"""
Calculate all item user have not meet before and return the top n interest items.
"""
self.load()
user_item_ids = set(self.frame[self.frame['UserID'] == user_id]['MovieID'])
other_item_ids = self.item_ids ^ user_item_ids
interest_list = [self._predict(user_id, item_id) for item_id in other_item_ids]
candidates = sorted(zip(list(other_item_ids), interest_list), key=lambda x: x[1], reverse=True)
return candidates[:top_n]
def save(self):
"""
Save model params.
"""
f = open('../data/lfm.model', 'wb')
pickle.dump((self.p, self.q), f)
f.close()
def load(self):
"""
Load model params.
"""
f = open('../data/lfm.model', 'rb')
self.p, self.q = pickle.load(f)
f.close()
主函数部分
# -*- coding: utf-8 -*-
import time
import os
from model.lfm import LFM, Corpus
print('Start..')
start = time.time()
if not os.path.exists('../data/lfm_items.dict'):
Corpus.pre_process()
if not os.path.exists('../data/lfm.model'):
LFM().train()
movies = LFM().predict(user_id=1)
for movie in movies:
print(movie)
print('Cost time: %f' % (time.time() - start))