图神经协同过滤(CF,Collaborative Filtering)能够根据user-item二部交互图同时学习user和item的embedding,目前已被证明是有效的。在交互图中,考虑到节点间的隐式反馈(没有边相连)依然有可能在某种程度上反映出用户的积极行为(例如相同偏好用户更有可能互相分享相似的item,尽管它们在user-item二部图上没有直接相连的边),因此传统的图协同过滤方法直接使用二部图作为输入显然是不够用的。为此,作者试图通过一种增强图的训练方式提高CF的表现能力,将节点embedding的学习和图结构的学习进行互相增强。
,首先表示 M 个user和 N 个item交互行为的二部无向图可以描述为下面邻接矩阵 A ,它是显式的(已经存在的交互行为):
目标是找到一个具有边权重矩阵a的更好的残差图, 这样我们可以更好地为用户和项目嵌入学习服务,从而提高CF性能。目前,基于图的CF由两个模块组成:残差图学习和节点嵌入学习。这两个模块不是孤立的,而是密切相关的。一方面,残差图学习模块需要依赖当前学习的用户和项目嵌入,以便找到A的可能链接
由于先前的图协同过滤GCF模型(例如NGCF、LightGCN)直接将显式交互图 A 作为输入来学习节点embedding,而忽略了节点间的隐式关系(没有显示边相连,但存在潜在的积极行为),作者提出通过学习一个增强图 来提高GCF的表现能力,可以看出 同时包含了显式 A 和隐式 两个方面的图结构信息。由于 A 是确定的,因此 AE 的学习就放在 AR 的学习上
其中的子矩阵 S 表示user-item二部图中的潜在的交互可能性,作者使用cos相似度进行计算,并保留每个用户topk相似度的边作为潜在交互:
这里是一个sigmoid函数,它将计算出的相似性转换为范围(0,1),W1和W2是两个可训练的矩阵
然而,学习到的相似矩阵S是稠密的,很难用于图卷积。与使用阈值截取,因为它可以灵活地控制学习图的边缘。实际上,对于每个用户,我们都会保留具有top-K计算相似度的边缘。稀疏相似矩阵计算如下:
原始图形中的边权重A等于1,但在具有权重矩阵A的增强图形中有所不同. 原因是学习的残差图有两种边:一种是已经出现在原始图中的旧边,另一种是与原始图相比新添加的边。因此,在具有残差图结构的增强图中,旧边用大于1的权重值重新加权,而新边用小于1的值加权。这表明我们的增强图可以同时添加缺少的边和重新加权现有边。
一个 K 跳GNN的节点embedding更新/学习规则定义为堆叠 K 层的邻居聚合器 AGG(⋅)
作者抛弃传统GNN涉及非线性变换的操作,只保留邻居聚合过程:
则经过第 K 层AGG 得到GCF的输出结果,相当于已经聚合了 K 阶邻居信息的user和item的embedding(分别是 u 和 v ),然后就可以用在下游推荐任务上,通过求取内积的方式预测未来状态下user对item的交互可能性。
(1)边缘约束
一个直接的想法是最小化实际存在的交互图 A 与相似度计算后得到的潜在预测图之间的均方损失,先确保预测的结果和实际的结果趋于一致:
我们将原始图的边缘视为基本真值正反馈。因此,学习的残差图应该保留这些边。然而,此边向约束仅学习单个链接相关性,但无法捕获全局图形属性。下面,我们通过局部-全局一致性学习来介绍全局图的属性。
(2)图形约束
由于上式只考虑了图的局部结构而忽略了图的全局属性信息(可以先看Deep Graph Infomax这篇文章),作者尝试最大化节点局部embedding与全局embedding之间的互信息来保持图的全局信息,具体的操作就是对一条边相连的两个节点(一端为user另一端为item)进行 concatenation+MLP 操作后得到局部embedding hai⊂ℜ2d ,则增强图 AE 上所有 Z 条边得到的全局embedding g 则是通过 Sign(⋅) 函数计算得到:
首先是常用的BPR损失,让已观测到的交互得分大于未观测到的交互得分:
然后将BPR损失与上面用于增强图学习优化的目标函数(局部重构+全局优化)进行联合优化训练:
用户分成五组,每个用户的交互次数分别超过0、8、16、32和64
当交互次数增加时,所有模型的性能都会增加,这意味着高质量的用户表示需要更多的交互。一般来说,我们提出的EGLN在大多数组上表现出最佳性能,但在最密集的组中失败。我们猜测原因是CF模型可以在充分交互的情况下实现良好的性能,因此我们提出的增强型图学习模块在该场景中不需要。
对于稀疏数据集,更深的图卷积可以帮助聚集更多的邻居,这有利于表示学习。然而,对于密集的数据集,太深的传播层很容易导致图上的过度平滑。
总而言之,选择合理的参数很重要
def get_simi_matrix_old(user_matrix, item_matrix, w1, w2, adj_matrix, topk1, topk2, gama):#adj 是 dense-u-i
user_rep = tf.matmul(user_matrix, w1)#6040*16 做个特征变换
item_rep = tf.matmul(item_matrix, w2)#6040*16
user_emb1 = tf.nn.l2_normalize(user_rep, axis=1) #做个标准化
item_emb1 = tf.nn.l2_normalize(item_rep, axis=1)
sim_matrix = tf.nn.sigmoid(tf.matmul(user_emb1, tf.transpose(item_emb1)))#6040*3952 每个用户对每个物品的相似度值 # [m,n]
loss_simi_adj = gama * tf.reduce_mean(tf.square(sim_matrix - adj_matrix))#用稠密矩阵 算loss
user_topk = tf.nn.top_k(sim_matrix, topk1)#6040*4 得到
user_topk_values = tf.reshape(user_topk.values, [-1])#24160 存储user-topk4的值
user_topk_columns = tf.cast(tf.reshape(user_topk.indices, [-1, 1]), dtype=tf.int64)#(24160,1) 得到topk中每个值得索引 tf.cast数据类型转换
user_all_rows = np.reshape(np.arange(user_count), [-1, 1])#6040,1
user_all_rows = tf.to_int64(user_all_rows,name='ToInt64')
user_topk_rows = tf.reshape(tf.tile(user_all_rows, multiples=[1, topk1]), [-1, 1])# 0000 1111 2222 3333...... 24160
user_topk_indexs = tf.concat([user_topk_rows, user_topk_columns], 1)#24160*2 存储topk行标 +每个用户top-4的索引值
user_item_sparse_simi = tf.SparseTensor(indices=user_topk_indexs, values=user_topk_values,
dense_shape=[user_count, item_count])#6040*3952
item_topk = tf.nn.top_k(tf.transpose(sim_matrix), topk2)#3952*6
item_topk_values = tf.reshape(item_topk.values, [-1])
item_topk_columns = tf.cast(tf.reshape(item_topk.indices, [-1, 1]), dtype=tf.int64)
item_all_rows = np.reshape(np.arange(item_count), [-1, 1])
item_all_rows = tf.to_int64(item_all_rows, name='ToInt64')
item_topk_rows = tf.reshape(tf.tile(item_all_rows, multiples=[1, topk2]), [-1, 1])
item_topk_indexs = tf.concat([item_topk_rows, item_topk_columns], 1)#23712*2
item_user_sparse_simi = tf.SparseTensor(indices=item_topk_indexs, values=item_topk_values,
dense_shape=[item_count, user_count]) #3952*6040
return user_item_sparse_simi, item_user_sparse_simi, loss_simi_adj, user_topk_indexs
user_item_simi_matrix, item_user_simi_matrix, loss_s, add_edges = get_simi_matrix_old(input_user_emb, input_item_emb, #add_edges 24160*2
sim_w1, sim_w2, adj_matrix_dense,
topk_u, topk_v, gama)
公式11,Loss_s
loss_simi_adj = gama * tf.reduce_mean(tf.square(sim_matrix - adj_matrix))#用稠密矩阵 算loss
add_sparse_user_matrix = tf.sparse_add(user_item_adj_matrix, user_item_simi_matrix)#6040*3952 #原始稀疏adj+潜在交互稀疏adj
add_sparse_item_matrix = tf.sparse_add(item_user_adj_matrix, item_user_simi_matrix)#3952*6040
user_item_final_matrix = nor_sparse_matrix(add_sparse_user_matrix)
item_user_final_matrix = nor_sparse_matrix(add_sparse_item_matrix)
def model_gcn(_user_emb, _item_emb, _layer):
user_emb_layer1 = tf.sparse_tensor_dense_matmul(user_item_final_matrix, _item_emb) + _user_emb
item_emb_layer1 = tf.sparse_tensor_dense_matmul(item_user_final_matrix, _user_emb) + _item_emb
user_emb_layer2 = tf.sparse_tensor_dense_matmul(user_item_final_matrix, item_emb_layer1) + user_emb_layer1
item_emb_layer2 = tf.sparse_tensor_dense_matmul(item_user_final_matrix, user_emb_layer1) + item_emb_layer1
user_emb_layer3 = tf.sparse_tensor_dense_matmul(user_item_final_matrix, item_emb_layer2) + user_emb_layer2
item_emb_layer3 = tf.sparse_tensor_dense_matmul(item_user_final_matrix, user_emb_layer2) + item_emb_layer2
user_emb_layer4 = tf.sparse_tensor_dense_matmul(user_item_final_matrix, item_emb_layer3) + user_emb_layer3
item_emb_layer4 = tf.sparse_tensor_dense_matmul(item_user_final_matrix, user_emb_layer3) + item_emb_layer3
if _layer == 1:
final_user_emb, final_item_emb = user_emb_layer1, item_emb_layer1
if _layer == 2:
final_user_emb, final_item_emb = user_emb_layer2, item_emb_layer2
if _layer == 3:
final_user_emb, final_item_emb = user_emb_layer3, item_emb_layer3
if _layer == 4:
final_user_emb, final_item_emb = user_emb_layer4, item_emb_layer4
return final_user_emb, final_item_emb
final_user_emb, final_item_emb = model_gcn(user_emb, item_emb, layer)#原始user—embed item-embed
按行随机打乱embedding矩阵 F 得到负属性矩阵 F~ ,然后通过聚合操作得到 h~ ;
def shuffle_embedding(input_emb_u, input_emb_v, rate): #输入初始的user—embed item——embed
mid = int(dimension * rate)
mid = 29
fixed_emb_u, dynamic_emb_u = tf.split(input_emb_u, [mid, dimension - mid], 1) #6040*32 固定6040*29 动态6040*3
dynamic_emb_u = tf.gather(tf.transpose(dynamic_emb_u), tf.random.shuffle(tf.range(dimension - mid)))#3*6040
out_emb_u = tf.concat([fixed_emb_u, tf.transpose(dynamic_emb_u)], 1)#6040*32
fixed_emb_v, dynamic_emb_v = tf.split(input_emb_v, [mid, dimension - mid], 1)
dynamic_emb_v = tf.gather(tf.transpose(dynamic_emb_v), tf.random.shuffle(tf.range(dimension - mid)))
out_emb_v = tf.concat([fixed_emb_v, tf.transpose(dynamic_emb_v)], 1)
return out_emb_u, out_emb_v # 扰乱后的6040*32 3952*32
# 32=29+3 将后三个进行随机组合,再贴回去,达到embedd扰动
_shuffle_user_emb, _shuffle_item_emb = shuffle_embedding(user_emb, item_emb, shuffle_rate)
def model_gcn(_user_emb, _item_emb, _layer):
user_emb_layer1 = tf.sparse_tensor_dense_matmul(user_item_final_matrix, _item_emb) + _user_emb
item_emb_layer1 = tf.sparse_tensor_dense_matmul(item_user_final_matrix, _user_emb) + _item_emb
user_emb_layer2 = tf.sparse_tensor_dense_matmul(user_item_final_matrix, item_emb_layer1) + user_emb_layer1
item_emb_layer2 = tf.sparse_tensor_dense_matmul(item_user_final_matrix, user_emb_layer1) + item_emb_layer1
user_emb_layer3 = tf.sparse_tensor_dense_matmul(user_item_final_matrix, item_emb_layer2) + user_emb_layer2
item_emb_layer3 = tf.sparse_tensor_dense_matmul(item_user_final_matrix, user_emb_layer2) + item_emb_layer2
user_emb_layer4 = tf.sparse_tensor_dense_matmul(user_item_final_matrix, item_emb_layer3) + user_emb_layer3
item_emb_layer4 = tf.sparse_tensor_dense_matmul(item_user_final_matrix, user_emb_layer3) + item_emb_layer3
if _layer == 1:
final_user_emb, final_item_emb = user_emb_layer1, item_emb_layer1
if _layer == 2:
final_user_emb, final_item_emb = user_emb_layer2, item_emb_layer2
if _layer == 3:
final_user_emb, final_item_emb = user_emb_layer3, item_emb_layer3
if _layer == 4:
final_user_emb, final_item_emb = user_emb_layer4, item_emb_layer4
return final_user_emb, final_item_emb
shuffle_user_emb, shuffle_item_emb = model_gcn(_shuffle_user_emb, _shuffle_item_emb, layer)
u_input = tf.placeholder("int32", [None, 1])
i_input = tf.placeholder("int32", [None, 1])
j_input = tf.placeholder("int32", [None, 1])
ua = tf.gather_nd(final_user_emb, u_input) #原始GCN聚合后的表示
vi = tf.gather_nd(final_item_emb, i_input) #原始GCN聚合后的表示
vj = tf.gather_nd(final_item_emb, j_input)
Rai = tf.reduce_sum(tf.multiply(ua, vi), 1, keepdims=True)
Raj = tf.reduce_sum(tf.multiply(ua, vj), 1, keepdims=True)
auc = tf.reduce_mean(tf.to_float((Rai - Raj) > 0))
bprloss = -tf.reduce_mean(tf.log(tf.clip_by_value(tf.nn.sigmoid(Rai - Raj), 1e-9, 1.0)))
regulation = lamda * tf.reduce_mean(tf.square(ua) + tf.square(vi) + tf.square(vj))
loss_r = bprloss + regulation
def make_discriminator_bilinear1(_lo_emb, _gl_emb):
'''
input: _lo_emb[None,64], _gl_emb[None,64]
output: label[None,1]
'''
emb_d1 = tf.matmul(_lo_emb, bilinear_w1) #特征变换变换 ?*64
emb_d2 = tf.multiply(emb_d1, _gl_emb) #计算局部与全局互信息 20480*64
emb_d3 = tf.reduce_sum(emb_d2, 1, keepdims=True) + bilinear_b1 #20480*1
return emb_d3
def local_global_v1():
'''
pos:, neg:
'''
pos_local_emb = tf.concat([tf.sigmoid(ua), tf.sigmoid(vi)], 1)
neg_local_emb = tf.concat([tf.sigmoid(ua), tf.sigmoid(vj)], 1)
# avg_global_emb = tf.reduce_mean(pos_local_emb, 0, keepdims=True)
add_user_list, add_item_list = tf.split(add_edges, 2, axis=1)#24160 #user-topk 索引 row(user) col(item)
all_user_emb_1 = tf.gather_nd(final_user_emb, all_user_list) # [178725,32] 原始adj的 user索引 0(*14)
all_item_emb_1 = tf.gather_nd(final_item_emb, all_item_list) # [178725,32] 原始adj的 item索引
all_user_emb_2 = tf.gather_nd(final_user_emb, add_user_list)# [24160,32] 潜在图adj的 user索引
all_item_emb_2 = tf.gather_nd(final_item_emb, add_item_list)# [24160,32] 潜在图adj的 item索引
all_user_emb = tf.concat([all_user_emb_1, all_user_emb_2], 0) #202885*32
all_item_emb = tf.concat([all_item_emb_1, all_item_emb_2], 0) #202885*32
all_edge_emb = tf.concat([tf.sigmoid(all_user_emb), tf.sigmoid(all_item_emb)], 1) # [202885,64]
avg_global_emb = tf.reduce_mean(all_edge_emb, 0, keepdims=True)#1*64
get_shape = tf.reduce_sum(ua, 1, keepdims=True)
global_emb = tf.tile(avg_global_emb, [batch_size, 1])#20480*64 将1*64的扩展为20480*64,行复制
one_label = tf.ones_like(get_shape, dtype=tf.float32)
zero_label = tf.zeros_like(get_shape, dtype=tf.float32)
real_predict = make_discriminator_bilinear1(pos_local_emb, global_emb)
fake_predict = make_discriminator_bilinear1(neg_local_emb, global_emb)
d_loss_all = tf.nn.sigmoid_cross_entropy_with_logits(logits=real_predict, labels=one_label) + \
tf.nn.sigmoid_cross_entropy_with_logits(logits=fake_predict, labels=zero_label)
loss_d = alpha * tf.reduce_mean(d_loss_all)
return loss_d