《Deep Matrix Factorization Models for Recommender Systems》DMF模型及python代码

    xiaoxiao2022-07-14  161

    推荐系统通常使用user-item评分,隐式反馈和辅助信息进行个性化推荐。矩阵分解通过计算user之间或者item之间的相似性来达到针对某个user推荐一些item。在本文中,我们提出了一种新的基于神经网络结构的矩阵分解模型。首先,我们构造一个具有显式评级和非偏好隐式反馈的用户项矩阵。通过这个矩阵作为输入,我们提出了一个深层结构学习架构,以便为user和item学习一个共同的低维空间。其次,我们设计了一种基于二元交叉熵的新损失函数,其中我们考虑了显式评级和隐式反馈,以便更好地进行优化。实验结果表明了我们提出的模型和损失函数的有效性。在几个基准数据集上,我们的模型优于其他最先进的方法。我们还进行了大量实验来评估不同实验环境中的性能。

    模型

    对于 M M M个用户 U = { u 1 , … , u M } \mathcal{U}=\left\{u_{1}, \dots, u_{M}\right\} U={u1,,uM} N N N个物品 V = { v 1 , … , v N } \mathcal{V}=\left\{v_{1}, \dots, v_{N}\right\} V={v1,,vN} 评分矩阵 R ∈ R M × N R \in \mathbb{R}^{M \times N} RRM×N,有两种方式构建user-item交互矩阵 Y ∈ R M × N Y \in \mathbb{R}^{M \times N} YRM×N ( 1 ) Y i j = { 0 ,  if  R i j = u n k 1 ,  otherwise  Y_{i j}=\left\{\begin{array}{ll}{0,} & {\text { if } R_{i j}=u n k} \\ {1,} & {\text { otherwise }}\end{array}\right.\tag{$1$} Yij={0,1, if Rij=unk otherwise (1) ( 2 ) Y i j = { 0 ,  if  R i j = u n k R i j ,  otherwise  Y_{i j}=\left\{\begin{array}{ll}{0,} & {\text { if } \quad R_{i j}=u n k} \\ {R_{i j},} & {\text { otherwise }}\end{array}\right.\tag{$2$} Yij={0,Rij, if Rij=unk otherwise (2) 本文采用公式(2)进行构建user-item交互矩阵。

    网络各层求解公式,其中激活函数选择ReLU。 ( 3 ) l 1 = W 1 x l i = f ( W i − 1 l i − 1 + b i ) , i = 2 , … , N − 1 h = f ( W N l N − 1 + b N ) \begin{array}{c}{l_{1}=W_{1} x} \\ {l_{i}=f\left(W_{i-1} l_{i-1}+b_{i}\right), i=2, \ldots, N-1} \\ {h=f\left(W_{N} l_{N-1}+b_{N}\right)}\end{array}\tag{$3$} l1=W1xli=f(Wi1li1+bi),i=2,,N1h=f(WNlN1+bN)(3) p i p_i pi q i q_i qi分别通过两个网络训练得到 ( 7 ) p i = f θ N U ( … f θ 3 U ( W U 2 f θ 2 u ( Y i ∗ W U 1 ) ) … ) q j = f θ N I ( … f θ 3 I ( W V 2 f θ 2 I ( Y ∗ j T W V 1 ) ) … ) \begin{aligned} p_{i} &=f_{\theta_{N}^{U}}\left(\ldots f_{\theta_{3}^{U}}\left(W_{U 2} f_{\theta_{2}^{u}}\left(Y_{i *} W_{U 1}\right)\right) \ldots\right) \\ q_{j} &=f_{\theta_{N}^{I}}\left(\ldots f_{\theta_{3}^{I}}\left(W_{V 2} f_{\theta_{2}^{I}}\left(Y_{* j}^{T} W_{V 1}\right)\right) \ldots\right) \end{aligned}\tag{$7$} piqj=fθNU(fθ3U(WU2fθ2u(YiWU1)))=fθNI(fθ3I(WV2fθ2I(YjTWV1)))(7) 通过计算余弦相似度求解user对item的评分 ( 8 ) Y ^ i j = F D M F ( u i , v j ∣ Θ ) = cosine ⁡ ( p i , q j ) = p i T q j ∥ p i ∥ ∥ q j ∥ \hat{Y}_{i j}=F^{D M F}\left(u_{i}, v_{j} | \Theta\right)=\operatorname{cosine}\left(p_{i}, q_{j}\right)=\frac{p_{i}^{T} q_{j}}{\left\|p_{i}\right\|\left\|q_{j}\right\|}\tag{$8$} Y^ij=FDMF(ui,vjΘ)=cosine(pi,qj)=piqjpiTqj(8) 模型的损失函数 ( 11 ) L = − ∑ ( i , j ) ∈ Y + ∪ Y − ( Y i j max ⁡ ( R ) log ⁡ Y ^ i j + ( 1 − Y i j max ⁡ ( R ) ) log ⁡ ( 1 − Y ^ i j ) ) \begin{aligned} L=-\sum_{(i, j) \in Y+\cup Y^{-}} &\left (\frac{Y_{i j}}{\max (R)} \log \hat{Y}_{i j}\right.+ \left(1-\frac{Y_{i j}}{\max (R)}\right) \log \left(1-\hat{Y}_{i j})\right) \end{aligned}\tag{$11$} L=(i,j)Y+Y(max(R)YijlogY^ij+(1max(R)Yij)log(1Y^ij))(11)

    模型最终的输出 ( 13 ) Y ^ i j o = max ⁡ ( μ , Y ^ i j ) \hat{Y}_{i j}^{o}=\max \left(\mu, \hat{Y}_{i j}\right)\tag{$13$} Y^ijo=max(μ,Y^ij)(13) Y Y Y:交互矩阵 Y + Y^+ Y+ : : Y$中观测到的交互,即显式反馈 Y − Y^- Y Y Y Y中的全部0元素 Y s a m p l e d − Y^{-}_{sampled} Ysampled:负样本,可以是 Y − Y^- Y,也可以是 Y − Y^- Y的部分采样 Y + ∪ Y s a m p l e d − Y^+ ∪ Y^-_{sampled} Y+Ysampled:所有训练样本 Y i ∗ Y_{i*} Yi:交互矩阵中的第 i i i行,表示用户 i i i对所有物品的评分 Y ∗ j Y_{*j} Yj:交互矩阵中的第 j j j列,表示所有用户对物品 j j j的评分

    python

    读取MovieLens数据集

    import numpy as np import sys class Dataset(): def __init__(self, filename): self.data, self.shape = self.getData(filename) self.train, self.test = self.getTrainTest() self.trainDict = self.getTrainDict() def getData(self, filename): if filename == 'ml-1m': print('my baby, now loading ml-1m data') data = [] filepath = './data/ml-1m/ratings.dat' num_u = 0 # 用户个数 num_i = 0 # 电影个数 max_rating = 0.0 # 最大评分 with open(filepath, 'r') as f: for line in f: if line: lines = line[:-1].split("::") user_id = int(lines[0]) movie_id = int(lines[1]) score = float(lines[2]) time = int(lines[3]) data.append((user_id, movie_id, score, time)) if user_id > num_u: num_u = user_id if movie_id > num_i: num_i = movie_id if score > max_rating: max_rating = score self.maxRate = max_rating print("users number : {}----- movies number : {}----- max Rating : {}----- Data size : {} ".format(num_u, num_i, self.maxRate, len(data))) return data, [num_u, num_i] else: print("Don't find the file") sys.exit() def getTrainTest(self): data = self.data data = sorted(data, key=lambda x:(x[0], x[3])) #按照用户数、评论时间排序 train = [] test = [] for i in range(len(data)-1): # 每个用户的最后一条数据作为评论数据 user = data[i][0]-1 movie = data[i][1]-1 rate = data[i][2] if data[i][0] != data[i+1][0]: test.append((user, movie, rate)) else: train.append((user, movie, rate)) test.append((data[-1][0]-1, data[-1][1]-1, data[-1][2])) print("train" , len(train)) #print("train[0]" , len(train[0])) print("test" , len(test)) return train, test def getTrainDict(self): dataDict = {} for i in self.train: dataDict[(i[0], i[1])] = i[2] return dataDict def getEmbedding(self): # 评分矩阵 train_matrix = np.zeros([self.shape[0], self.shape[1]]) for i in self.train: user = i[0] movie = i[1] rating = i[2] train_matrix[user,movie] = rating print("embedding shape: " , train_matrix.shape) return np.array(train_matrix) def getInstances(self, data, negNum): user = [] movie = [] rate = [] for i in data: user.append(i[0]) movie.append(i[1]) rate.append(i[2]) for t in range(negNum): j = np.random.randint(self.shape[1]) while (i[0], j) in self.trainDict: j = np.random.randint(self.shape[1]) user.append(i[0]) movie.append(j) rate.append(0.0) return np.array(user), np.array(movie), np.array(rate) def getTestNeg(self, testData, negNum): user = [] movie = [] for s in testData: tmp_user = [] tmp_movie = [] u = s[0] m = s[1] tmp_user.append(u) tmp_movie.append(m) neglist = [] neglist.append(m) for t in range(negNum): j = np.random.randint(self.shape[1]) while (u, j) in self.trainDict or j in neglist: j = np.random.randint(self.shape[1]) neglist.append(j) tmp_user.append(u) tmp_movie.append(j) user.append(tmp_user) movie.append(tmp_movie) return [np.array(user), np.array(movie)]

    实现DFM模型

    import tensorflow as tf import numpy as np import argparse import sys import os import heapq import math class Model(): def __init__(self, dataName="ml-1m", negNum=7, userLayer=[512,64], itemLayer=[1024,64], reg=1e-3, lr=0.0001, maxEpochs=50, batchSize=256, earlyStop=5, checkPoint="./checkPoint", topK=10): self.dataName = dataName self.dataSet = Dataset(self.dataName) self.shape = self.dataSet.shape self.maxRate = self.dataSet.maxRate self.train = self.dataSet.train self.test = self.dataSet.test self.negNum = negNum self.testNeg = self.dataSet.getTestNeg(self.test, 99) self.add_embedding_matrix() self.add_placeholders() self.userLayer = userLayer self.itemLayer = itemLayer self.add_model() self.add_loss() self.lr = lr self.add_train_step() self.checkPoint = checkPoint self.init_sess() self.maxEpochs = maxEpochs self.batchSize = batchSize self.topK = topK self.earlyStop = earlyStop def add_placeholders(self): self.user = tf.placeholder(tf.int32) self.item = tf.placeholder(tf.int32) self.rate = tf.placeholder(tf.float32) self.drop = tf.placeholder(tf.float32) def add_embedding_matrix(self): self.user_item_embedding = tf.convert_to_tensor(self.dataSet.getEmbedding(), dtype=tf.float32) self.item_user_embedding = tf.transpose(self.user_item_embedding) #print(tf.shape(self.user_item_embedding)) #print("self.dataSet.getEmbedding(): ", self.dataSet.getEmbedding().shape) def add_model(self): user_input = tf.nn.embedding_lookup(self.user_item_embedding, self.user) item_input = tf.nn.embedding_lookup(self.item_user_embedding, self.item) #print(tf.shape(user_input)) def init_variable(shape, name): return tf.Variable(tf.truncated_normal(shape=shape, dtype=tf.float32, stddev=0.01), name=name) with tf.name_scope("User_Layer"): user_W1 = init_variable([self.shape[1], self.userLayer[0]], "user_w1") user_out = tf.matmul(user_input, user_W1) for i in range(0, len(self.userLayer)-1): W = init_variable([self.userLayer[i], self.userLayer[i+1]], "user_w"+str(i+2)) b = init_variable([self.userLayer[i+1]], "user_b"+str(i+2)) user_out = tf.nn.relu(tf.add(tf.matmul(user_out, W), b)) with tf.name_scope("Item_Layer"): item_W1 = init_variable([self.shape[0], self.itemLayer[0]], "item_w1") item_out = tf.matmul(item_input, item_W1) for i in range(0, len(self.itemLayer)-1): W = init_variable([self.itemLayer[i], self.itemLayer[i+1]], "item_w"+str(i+2)) b = init_variable([self.itemLayer[i+1]], "item_b"+str(i+2)) item_out = tf.nn.relu(tf.add(tf.matmul(item_out, W), b)) norm_user_output = tf.sqrt(tf.reduce_sum(tf.square(user_out), axis=1)) norm_item_output = tf.sqrt(tf.reduce_sum(tf.square(item_out), axis=1)) self.y_ = tf.reduce_sum(tf.multiply(user_out, item_out), axis=1, keepdims=False) / (norm_item_output*norm_user_output) self.y_ = tf.maximum(1e-6, self.y_) def add_loss(self): regRate = self.rate / self.maxRate losses = regRate * tf.log(self.y_) + (1-regRate) * tf.log(1 - self.y_) self.loss = tf.reduce_sum(losses) def add_train_step(self): optimizer = tf.train.AdamOptimizer(self.lr) self.train_step = optimizer.minimize(self.loss) def init_sess(self): # self.config = tf.ConfigProto() # self.config.gpu_options.allow_growth = True #self.config.allow_soft_placement = True #self.sess = tf.Session(config=self.config) self.sess = tf.Session() self.sess.run(tf.global_variables_initializer()) self.saver = tf.train.Saver() if os.path.exists(self.checkPoint): [os.remove(f) for f in os.listdir(self.checkPoint)] else: os.makedirs(self.checkPoint) def run(self): best_hr = -1 best_NDCG = -1 best_epoch = -1 print("start training") for epoch in range(self.maxEpochs): print("="*20+"Epoch", epoch, "="*20) self.run_epoch(self.sess) print("="*50) print("start Evaluation") hr, NDCG = self.evaluate(self.sess, self.topK) print("Epoch ", epoch, "HR: {}, NDCG: {}".format(hr, NDCG)) if hr > best_hr or NDCG > best_NDCG: best_hr = hr best_NDCG = NDCG best_epoch = epoch self.saver.save(self.sess, self.checkPoint) if epoch - best_epoch > self.earlyStop: print("normal earlystop") break print("="*20+"Epoch ", epoch, "End"+"="*20) print("Best HR: {}, Best_NDCG: {} at Epoch: {}".format(best_hr, best_NDCG, best_epoch)) print("training complete") def run_epoch(self, sess, verbose=10): train_u, train_i, train_r = self.dataSet.getInstances(self.train, self.negNum) train_len = len(train_u) shuffled_idx = np.random.permutation(np.arange(train_len)) train_u = train_u[shuffled_idx] train_i = train_i[shuffled_idx] train_r = train_r[shuffled_idx] num_batches = len(train_u) // self.batchSize + 1 losses = [] for i in range(num_batches): min_idx = i * self.batchSize max_idx = np.min([train_len, (i+1)*self.batchSize]) train_u_batch = train_u[min_idx:max_idx] train_i_batch = train_i[min_idx:max_idx] train_r_batch = train_r[min_idx:max_idx] feed_dict = self.create_feed_dict(train_u_batch, train_i_batch, train_r_batch) _, tmp_loss = sess.run([self.train_step, self.loss], feed_dict=feed_dict) losses.append(tmp_loss) if verbose and i%verbose == 0: sys.stdout.write("\r{} / {} : loss = {}".format(i, num_batches, np.mean(losses[-verbose:]))) sys.stdout.flush() loss = np.mean(losses) print("\n Mean loss in this epoch is: {}".format(loss)) return loss def create_feed_dict(self, u, i, r=None, drop=None): return {self.user : u, self.item : i, self.rate : r, self.drop : drop} def evaluate(self, sess, topK): def getHitRatio(ranklist, targetItem): for item in ranklist: if item == targetItem: return 1 return 0 def getNDCG(ranklist, targetItem): for i in range(len(ranklist)): item = ranklist[i] if item == targetItem: return math.log(2) / math.log(i+2) return 0 hr = [] NDCG = [] testUser = self.testNeg[0] testItem = self.testNeg[1] for i in range(len(testUser)): target = testItem[i][0] feed_dict = self.create_feed_dict(testUser[i], testItem[i]) predict = sess.run(self.y_, feed_dict=feed_dict) item_score_dict = {} for j in range(len(testItem[i])): item = testItem[i][j] item_score_dict[item] = predict[j] ranklist = heapq.nlargest(topK, item_score_dict, key=item_score_dict.get) tmp_hr = getHitRatio(ranklist, target) tmp_NDCG = getNDCG(ranklist, target) hr.append(tmp_hr) NDCG.append(tmp_NDCG) return np.mean(hr), np.mean(NDCG)

    参考文献

    论文原文: https://www.ijcai.org/proceedings/2017/0447.pdf

    最新回复(0)