Knowledge Graph Convolutional Networks for Recommender Systems
Hongwei Wang, Miao Zhao, Xing Xie, Wenjie Li, Minyi Guo.
In Proceedings of The 2019 Web Conference (WWW 2019)
本文作者源码:https://github.com/hwwang55/KGCN
本人对KGCN的一些代码注释,有兴趣的道友可以看看
https://github.com/Archerxzs/KGCN-notes
文章最后附上本人对代码的解析
为了缓解协同过滤的推荐系统的稀疏性和冷启动问题
所以收集用户和项目的属性,并且这些属性不是孤立的,所以就形成了知识图KG
从KG中每个实体的邻居中取样作为它们的邻域,然后在计算给定实体的表示时将邻居信息和偏差结合起来
traditional method:Collaborative Filtering(CF)
问题 :交互矩阵的稀疏性和冷启动
**解决方法:**用用户和项目的属性来弥补稀疏性提高性能 KG
KG的好处:
KG的挑战:
高维
异构
常见解决方法:
本文解决方法:
M 个 u s e r s M个users M个users U = { u 1 , u 2 , . . . , u M } U=\{u_1,u_2,...,u_M\} U={u1,u2,...,uM}
N 个 i t e m s N个items N个items V = { v 1 , v 2 , . . . , v N } V=\{v_1,v_2,...,v_N\} V={v1,v2,...,vN}
u s e r s − i t e m s 交 互 矩 阵 users-items交互矩阵 users−items交互矩阵 Y ∈ R M × N = { y u v = 1 u 和 v 有 交 互 y u v = 0 u 和 v 无 交 互 Y∈R^{M×N}=\left\{ \begin{aligned} y_{uv}=1 \ u和v有交互 \\ y_{uv}=0 \ u和v无交互 \end{aligned} \right. Y∈RM×N={yuv=1 u和v有交互yuv=0 u和v无交互
知 识 图 G ( 实 体 − 关 系 − 实 体 ( h , r , t ) ) 知识图G(实体-关系-实体(h,r,t)) 知识图G(实体−关系−实体(h,r,t))
目 标 : 给 定 u s e r s − i t e m s 交 互 矩 阵 Y 和 知 识 图 G , 预 测 u s e r u 是 否 会 对 之 前 未 交 互 过 的 i t e m v 感 兴 趣 目标:给定users-items交互矩阵Y和知识图G,预测user\ u是否会对之前未交互过的item\ v感兴趣 目标:给定users−items交互矩阵Y和知识图G,预测user u是否会对之前未交互过的item v感兴趣
y ^ u v \widehat{y}_{uv} y uv=F(u,v|\Theta,Y,G) $
- y ^ u v : u s e r u 和 i t e m v 交 互 的 概 率 \widehat{y}_{uv}:user\ u和item\ v交互的概率 y uv:user u和item v交互的概率
- Θ : 函 数 F 的 模 型 参 数 \Theta:函数F的模型参数 Θ:函数F的模型参数
N ( v ) : 和 v 直 接 相 连 的 e n t i t y 集 合 N(v):和v直接相连的entity集合 N(v):和v直接相连的entity集合
r e i , e j : e n t i t y e i 和 e n t i t y e j 的 关 系 r_{e_i,e_j}:entity\ e_i和entity\ e_j的关系 rei,ej:entity ei和entity ej的关系
f u n c t i o n g : R d × R d − > R ( 例 如 内 积 ) : 计 算 用 户 和 关 系 的 分 数 function\ g:R^d×R^d->R(例如内积):计算用户和关系的分数 function g:Rd×Rd−>R(例如内积):计算用户和关系的分数
a g g r e g a t o r s a g g : R d × R d − > R d : 聚 合 器 aggregators\ agg:R^d×R^d->R^d:聚合器 aggregators agg:Rd×Rd−>Rd:聚合器
π r u = g ( u , r ) { u ∈ R d u s e r u 的 向 量 r ∈ R d r e l a t i o n r 的 向 量 d 向 量 的 纬 度 \pi^u_r=g(u,r)\begin{cases} u∈R^d \ user\ u的向量 \\ r∈R^d \ relation\ r的向量 \\ d \ 向量的纬度 \end{cases} πru=g(u,r)⎩⎪⎨⎪⎧u∈Rd user u的向量r∈Rd relation r的向量d 向量的纬度 表示了 r e l a t i o n r relation\ r relation r对 u s e r u user\ u user u的重要性
v N ( v ) u = Σ e ∈ N ( v ) π ~ r v , e u e v^u_{N(v)}=\Sigma_{e∈N(v)}\widetilde{\pi}^u_{r_{v,e}}e vN(v)u=Σe∈N(v)π rv,eue i t e m v item\ v item v的拓扑邻域结构
v S ( v ) u : e n t i t y v 的 所 有 邻 域 表 示 v^u_{S(v)}:entity\ v的所有邻域表示 vS(v)u:entity v的所有邻域表示
将 e n t i t y v entity\ v entity v的表示 和 其邻域表示 v S ( v ) u v^u_{S(v)} vS(v)u聚合为一个向量
将user的向量和relation的向量进行乘积取均值(说明relation对user的重要性,比如一个用户可能对该电影的演员感兴趣,另一个用户对该电影的体裁感兴趣,每个用户对电影的关系的感兴趣程度是不一样的)
如果上面说的那个分数和当前实体的邻域实体向量进行内积取均值得到当前实体的邻域实体的集合表示
最后将这个邻域集合表示和当前实体相加,算出一个当前实体的最终向量表示
Movielens-20M阈值设为4
其他数据集(较为稀疏)不设阈值
{ S V D ( 奇 异 值 分 解 , 使 用 内 积 建 立 u s e r − i t e m 交 互 模 型 ) L i b F M ( 基 于 特 征 的 分 解 模 型 , 将 u s e r I D 和 i t e m I D 连 接 起 来 作 为 输 入 ) l i b F M + T r a n s E ( 附 加 上 T r a n s E 学 习 后 的 实 体 来 扩 展 l i b F M ) P E R ( 将 K G 视 为 异 构 网 络 , 提 取 基 于 元 路 径 的 特 征 来 表 示 u s e r s 和 i t e m s 的 连 通 性 ) ) C K E ( C F + 结 构 化 知 识 ) R i p p l e N e t ( 传 播 用 户 偏 好 来 推 荐 ) \begin{cases} SVD(奇异值分解,使用内积建立user-item交互模型)& \\ LibFM(基于特征的分解模型,将userID和itemID连接起来作为输入)& \\libFM+TransE(附加上TransE学习后的实体来扩展libFM)\\PER(将KG视为异构网络,提取基于元路径的特征来表示users和items的连通性))\\CKE(CF+结构化知识)\\RippleNet(传播用户偏好来推荐) \end{cases} ⎩⎪⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎪⎧SVD(奇异值分解,使用内积建立user−item交互模型)LibFM(基于特征的分解模型,将userID和itemID连接起来作为输入)libFM+TransE(附加上TransE学习后的实体来扩展libFM)PER(将KG视为异构网络,提取基于元路径的特征来表示users和items的连通性))CKE(CF+结构化知识)RippleNet(传播用户偏好来推荐)
S V D 、 L i b F M SVD、LibFM SVD、LibFM为无KG方法
AUC( area under curve:ROC曲线下与坐标轴围成的面积 ) 取值范围在0.5和1之间
ROC曲线的坐标轴:横轴为召回率(Recall),纵轴为精确率(Precision)
精确率(Precision):分类正确的正样本个数占模型判定为正样本的样本个数的比例
召回率(Recall):分类正确的正样本个数占真正的正样本个数的比例
F 1 = 2 ⋅ P r e c i s i o n ⋅ R e c a l l P r e c i s i o n + R e c a l l F1=\frac{2·Precision·Recall}{Precision+Recall} F1=Precision+Recall2⋅Precision⋅Recall精确率和召回率的调和平均值
结果
KGCN在book和music数据集的表现要高于movie,说明KGCN可以很好的解决稀疏问题
无KG的方法(SVD,LibFM)的表现要好于部分KG方法(PER,CKE),说明人工设计的meta-path和TransR系列的规则化不能充分利用KG
LibFM+TransE的方法在大多数情况下都要好于LibFM,说明KG对于推荐有帮助
PER在所有方法中表现最差,因为在实际情况中很难定义较好原路径
RippleNet较其他方法表现较好,因为使用了多跳邻域结构,说明KG中捕获邻域信息对推荐是有帮助的
users:138159 user的数量
items:16954 movie的数量
interactions:13501622 user和movie的交互数量(有/无交互)
entities:102569 movie+item的数量
relations:32 KG中的关系的数量(制片人、导演、语言、体裁。。。)
triples:499474 KG中的三元组的数量
1、ratings_final.txt:userId、movieId、0/1
user1、movie1、1:user1对movie1有评分,且评分大于阈值
user1、movie1、0:user1对movie1无评分==(纠正:不是没大于阈值)==
即上面说的 u s e r − i t e m 交 互 矩 阵 Y user-item交互矩阵Y user−item交互矩阵Y
2、kg_final.txt:movieId、relationId、itemId
泰坦尼克号的Id、“导演“的id、 詹姆斯·卡梅隆的Id
泰坦尼克号的Id、“语言“的id、 英语的Id
泰坦尼克号的Id、“体裁“的id、 爱情的Id
泰坦尼克号的id、“主演”的id、莱昂纳多·迪卡普里奥的id
即上面说的 知 识 图 G 知识图G 知识图G
data_loader.py:
n_user:user的数量138159
n_item:item(即movie)的数量16954
n_entity:item(制片人、导演、体裁...)+movie的数量102569
n_relation:关系的数量32
rating_final.txt中的数据
train_data 0.6
eval_data 0.2
test_data 0.2
kg_final.txt中的数据
adj_entity:(n_entity,neighbour_size)(即102569,4)纬矩阵,随机挑选当前邻居实体中的四个作为样本
(eg:泰坦尼克号(詹姆斯·卡梅隆、英语、爱情、莱昂纳多·迪卡普里奥),花木兰(。。。。。))
adj_relation:(n_entity,neighbour_size)(即102569,4)纬矩阵,随机挑选当前邻居关系中的四个作为样本
(eg:泰坦尼克号(导演、语言、体裁、主演),花木兰(。。。。))
1、将relation和user的得分和领域entity的向量聚合起来得到一个当前entity的领域表示,然后将这个领域表示和自身表示根据聚合器聚合起来,最后得到自身entity的表示
π r u = g ( u , r ) { u ∈ R d u s e r u 的 向 量 r ∈ R d r e l a t i o n r 的 向 量 d 向 量 的 纬 度 \pi^u_r=g(u,r)\begin{cases} u∈R^d \ user\ u的向量 \\ r∈R^d \ relation\ r的向量 \\ d \ 向量的纬度 \end{cases} πru=g(u,r)⎩⎪⎨⎪⎧u∈Rd user u的向量r∈Rd relation r的向量d 向量的纬度 表示了 r e l a t i o n r relation\ r relation r对 u s e r u user\ u user u的重要性
v N ( v ) u = Σ e ∈ N ( v ) π ~ r v , e u e v^u_{N(v)}=\Sigma_{e∈N(v)}\widetilde{\pi}^u_{r_{v,e}}e vN(v)u=Σe∈N(v)π rv,eue i t e m v item\ v item v的拓扑邻域结构
v S ( v ) u : e n t i t y v 的 所 有 邻 域 表 示 v^u_{S(v)}:entity\ v的所有邻域表示 vS(v)u:entity v的所有邻域表示
将 e n t i t y v entity\ v entity v的表示 和 其邻域表示 v S ( v ) u v^u_{S(v)} vS(v)u聚合为一个向量
2、将上面计算得到的entity的表示和user的表示内积计算得到一个概率(训练这个概率和标签的损失,使得这个概率达到最大(user和entity的表示越来越近))
预处理数据集,保证数据集的id等信息是统一的
解析一些超参数,并指定实验的数据集
将数据集中原本user/item的id替换为新的连续的id
convert_rating():
以movie数据集为例,
评分大于等于4(满分为5)的评分视为正样本(user、item、1)
未进行评分的视为负样本(user、item、0)
最终数据集在ratings_final.txt
convert_kg():
以movie数据集为例,
kg中存放entity之间的交互(entity包括movie和一些movie的相关信息(例如:体裁、制片人、导演等等))
最终KG数据集在kg_final.txt
从文件中加载数据至内存
load_rating():
将ratings_final.txt转换为npy文件,并从中读取数据至内存,并计算user和item(movie)的数量
按照设定好的训练、测试、验证集的比例随机产生数据集train_data、eval_data、test_data(格式为:user、item、0/1)
返回:n_user、n_item、train_data、eval_data、test_data
load_kg():
将kg_final.txt转换为npy文件,并从中读取数据至内存,并计算entity(movie和movie有关的item)和relation的数量
construct_kg():
将kg_final文件中的头实体和尾实体分别作为key构建成一个字典
{“霸王别姬”“张国荣”,“演员”)},{“张国荣”“霸王别姬”,“演员”)}
construct_adj():
adj_entity和adj_relation :[entity_num,neighbor_size](矩阵大小)int64
从上面说的字典中获取当前实体的邻域节点,并形成矩阵
adj_entity[“霸王别姬”]=[“张国荣”,“陈凯歌”,“剧情”,“1993”]
adj_rellation[“霸王别姬”]=[“演员”,“导演”,“体裁”,“出品时间”]
返回:n_entity、n_relation、adj_entity、adj_relation
KGCN的模型构建
**__init__(args, n_user, n_entity, n_relation, adj_entity, adj_relation)*初始化模型需要的参数和一些计算图
**_parse_args(args, adj_entity, adj_relation)*解析参数
**_build_inputs()*使用占位,构建输入,每次输入长度为batch_size
user_indices:user的序号 类似这样:[0 0 0 … 1104 1104 1105]
item_indices:item(movie)的序号
labels:具体的label(0/1)
**_build_model(n_user, n_entity, n_relation)*构建模型,在此生成embeddding
user_emb_matrix、entity_emb_matrix、relation_emb_matrix
上面三个为[n_entity,dim]纬矩阵,经过初始化,可训练
user_embeddings
通过tf.nn.embedding_lookup从user_emb_matrix中先随机抽取和user_indices对应的向量,后续可被训练
item_embeddings、aggregators
因为使用需要对movie数据进行扩展升华(即使用周围实体信息来丰富自身),所以要先获取到周围节点在生成向量
get_neighbors(item_indices):
item_indices的shape为65536
首先进行扩展纬度,变为:(65536,1)
然后封为列表entities,此时len(entities)=1,entities[0]=(65536,1)纬的矩阵,在创建一个relations的列表
接下来去寻找当前节点的邻居(参数规定为寻找2次,每次找4个邻居)
i=0:第一次找邻居
从adj_entity切片,切片大小entities[0] (65536,1)纬矩阵,切出来为(65536,1,4)纬矩阵,然后reshape为(65536,4)纬矩阵
同理,和邻居的关系的矩阵为(65536,4)纬矩阵
此时,entities[0]=(65536,1)纬矩阵 entities[1]=(65536,4)纬矩阵
relations[0]=(65536,4)纬矩阵
i=1:第二次找邻居
从adj_entity切片,切片大小entities[1] (65536,4)纬矩阵,切出来为(65536,4,4)纬矩阵,然后reshape为(65536,16)纬矩阵
同理,和邻居的关系的矩阵为(65536,16)纬矩阵
此时,entities[0]=(65536,1)纬矩阵 entities[1]=(65536,4)纬矩阵 entities[2]=(65536,16)纬矩阵
relations[0]=(65536,4)纬矩阵 relations[1]=(65536,16)纬矩阵
返回:entities,relations
aggregate(entities, relations):
将从上面获取到的节点的邻居节点和邻居关系用来丰富自身节点并转换为向量
首先,entity_vectors通过tf.nn.embedding_lookup从entity_emb_matrix中先随机抽取和entities对应的向量,后续可被训练
ev[0]:(65536,1,32) ev[1]:(65536,4,32) ev[2]:(65536,16,32)
同理 relation_vectors
rv[0]:(65536,4,32) rv[1]:(65536,16,32)
接着,经过迭代,使用累加器来生成相应的向量
i=0:第一次迭代,累加器激活函数使用relu
h=0:
vector = aggregator(
#(65536, 1, 32)
self_vectors=entity_vectors[0],
#(65536, 4, 32)===>(65536, 1,4, 32)
neighbor_vectors=tf.reshape(entity_vectors[1], shape),
#(65536, 4, 32)==>(65536,1,4,32)
neighbor_relations=tf.reshape(relation_vectors[0], shape),
#[65536,32]
user_embeddings=self.user_embeddings)
将参数传递至累加器计算用户和关系的分数(表示关系对用户的重要性)
将user_embeddings进行reshape为(65536,1,1,32)
计算user_embeddings和neighbor_relations的乘积的均值分数user_relation_scores(65536,1,4)
将分数进行softmax归一化user_relation_scores_normalized(65536,1,4)
将user_relation_scores_normalized扩展纬度为(65536,1,4,1)
计算user_relation_scores_normalized和neighbor_vectors的乘积的均值neighbors_aggregated(65536,1,32)
返回:neighbors_aggregated(65536,1,32)
此时:entity_vectors_next_iter[0]=[65536,1,32]
h=1:
vector = aggregator(
#(65536, 4, 32)
self_vectors=entity_vectors[1],
#(65536, 16, 32)===>(65536, 4,4, 32)
neighbor_vectors=tf.reshape(entity_vectors[2], shape),
#(65536, 16, 32)==>(65536,4,4,32)
neighbor_relations=tf.reshape(relation_vectors[1], shape),
#[65536,32]
user_embeddings=self.user_embeddings)
将user_embeddings进行reshape为(65536,1,1,32)
计算user_embeddings和neighbor_relations的乘积的均值分数user_relation_scores(655364,4)
将分数进行softmax归一化user_relation_scores_normalized(65536,4,4)
将user_relation_scores_normalized扩展纬度为(65536,4,4,1)
计算user_relation_scores_normalized和neighbor_vectors的乘积的均值neighbors_aggregated(65536,4,32)
返回:neighbors_aggregated(65536,4,32)
此时:entity_vectors_next_iter[0]=[65536,1,32],entity_vectors_next_iter[1]=[65536,4,32]
i=0 此时要结束了
entity_vectors_next_iter赋值给entity_vectors
此时:entity_vectors[0]=[65536,1,32],entity_vectors[1]=[65536,4,32]
i=1:第二次迭代,累加器激活函数使用tanh
h=0:
vector = aggregator(
#(65536, 1, 32)
self_vectors=entity_vectors[0],
#(65536, 4, 32)===>(65536, 1,4, 32)
neighbor_vectors=tf.reshape(entity_vectors[1], shape),
#(65536, 4, 32)==>(65536,1,4,32)
neighbor_relations=tf.reshape(relation_vectors[0], shape),
#[65536,32]
user_embeddings=self.user_embeddings)
将user_embeddings进行reshape为(65536,1,1,32)
计算user_embeddings和neighbor_relations的乘积的均值分数user_relation_scores(65536,1,4)
将分数进行softmax归一化user_relation_scores_normalized(65536,1,4)
将user_relation_scores_normalized扩展纬度为(65536,1,4,1)
计算user_relation_scores_normalized和neighbor_vectors的乘积的均值neighbors_aggregated(65536,1,32)
返回:neighbors_aggregated(65536,1,32)
此时:entity_vectors_next_iter[0]=[65536,1,32]
i=1 此时要结束了
entity_vectors_next_iter赋值给entity_vectors
此时:entity_vectors[0]=[65536,1,32]
两次迭代结束后
将最终的entity_vectors进行reshape为res(65536,32)
返回:res,aggregators(两次使用的累加器)
此时生成了item_embeddings向量和aggregators的累加器
scores
使用user_embeddings和item_embeddings的乘积的和值,算出user和item的得分score(65536)(就是user选择这个movie的概率)
scores_normalized
对scores进行sigmoid归一化