一 应用领域
道路交通,动态预测
自动驾驶,无人机场景
化学,医疗等场景
物理模型相关
二 图基本模块定义
V Vertex点
E Edge 边(向量)
U Global 图 (例如:全局向量)
无论事多么复杂,我们利用图神经网络的目的就是整合特征
有向图 出度,入度
无向图 度 Degree
邻接矩阵
子图:所有边和点都在原图中
连通图:对于一个无向图,如果任何的节点i能够通过一些边到达节点j,则称之为连通图
连通分量:无向图G的一个极大联通子图陈伟G的一个联通分量(或连通分支)。连通图只有一个连通分量,即其自身;非连通的无向图有多个连通分量。
有向图连通性
强连通图:强连通图(Strongly Connected Graph)是指在有向图G中,如果对于每一对vi、vj,vi≠vj,从vi到vj和从vj到vi都存在路径,则称G是强连通图。有向图中的极大强连通子图称做有向图的强连通分量。
弱连通图:将有向图的所有的有向边替换为无向边,所得到的图称为原图的基图。如果一个有向图的基图是连通图,则有向图是弱连通图。
最短路径:在一个带权有向图中,从某一顶点到另一顶点可能有很多条路径,最短路径即权值之和最小的那条路径。
图直径:图论中, 图的直径是指任意两个顶点间距离的最大值.(距离是两个点之间的所有路的长度的最小值) 所有最短路径中的最大值
度中心性
度中心性= =度/总节点-1
特征向量中心性Eigenvector Centrality
一个节点的重要性既取决于其邻居节点的数量(即该节点的度),也取决于其邻居节点的重要性。
中介中心性Betweenness Centrality
Betweenness=经过该节点的最短路径/其余两两节点的最短路径
连接中心性 Closeness
PageRank
阻尼系数
import numpy as np
import pandas as pd
import networkx as nx
edges=pd.DataFrame()
#edges['sources']代表起始节点
edges['sources']=[1,1,1,2,2,3,3,4,4,5,5,5]
#edges['targets']代表终止节点
edges['targets']=[2,4,5,3,1,2,5,1,5,1,3,4]
#权值
edges['weights']=[1,1,1,1,1,1,1,1,1,1,1,1]
#具体解释为
# 1节点指向2节点权重为1
# 1节点指向4节点权重为1
# 1节点指向5节点权重为1
#定义图
G=nx.from_pandas_edgelist(edges,source='sources',target='targets',edge_attr='weights')
#degree
print(nx.degree(G))
#连通分量
print(list(nx.connected_components(G)))
#图直径
print(nx.diameter(G))
#度中心性
print(nx.degree_centrality(G))
#特征向量中心性
print(nx.eigenvector_centrality(G))
#betweenness
print(nx.betweenness_centrality(G))
#closeness
print(nx.closeness_centrality(G))
#pagerank
print(nx.pagerank(G))
#HITS
print(nx.hits(G))
结果
[(1, 3), (2, 2), (4, 2), (5, 3), (3, 2)]
[{1, 2, 3, 4, 5}]
2
{1: 0.75, 2: 0.5, 4: 0.5, 5: 0.75, 3: 0.5}
{1: 0.5298988890761731, 2: 0.35775191431708964, 4: 0.4271316779596084, 5: 0.5298988890761731, 3: 0.35775191431708964}
{1: 0.25, 2: 0.08333333333333333, 4: 0.0, 5: 0.25, 3: 0.08333333333333333}
{1: 0.8, 2: 0.6666666666666666, 4: 0.6666666666666666, 5: 0.8, 3: 0.6666666666666666}
{1: 0.24369622576677996, 2: 0.17225629712058638, 4: 0.16809495422526693, 5: 0.2436962257667799, 3: 0.17225629712058638}
({1: 0.24059715195481507, 2: 0.1624345647450478, 4: 0.19393656660027417, 5: 0.2405971519548151, 3: 0.1624345647450478}, {1: 0.2405971522393837, 2: 0.1624345646565165, 4: 0.19393656620819955, 5: 0.2405971522393837, 3: 0.1624345646565165})
三 邻接矩阵
图像可以作为邻居矩阵 ,A表示邻居之间的关系
GNN(A,X)
文本数据也可以表示图的形式,零阶矩阵表示连接关系
Graphs ->are ->all ->around ->us
Graphs | are | all | around | us | |
---|---|---|---|---|---|
Graphs | 1 | ||||
are | 1 | ||||
all | 1 | ||||
around | 1 | ||||
us |
GNN要求所有图的格式是一样的,考虑GCN
四 Graph embedding图嵌入
结合第2章的代码可以知道表示图,节点数和边数决定了维度。
4.1 DeepWalk(无向图)
简化了节点的长度
DeepWalk给出的方法是使用随机游走(RandomWalk)的方式在图中进行节点采样。
RandomWalk是一种可重复访问已访问节点的深度优先遍历算法。给定当前访问起始节点,从其邻居中随机采样节点作为下一个访问节点,重复此过程,直到访问序列长度满足预设条件。
获取足够数量的节点访问序列后,使用skip-gram model 进行向量学习。
【Graph Embedding】DeepWalk:算法原理,实现和应用 - 知乎 (zhihu.com)
4.2 Line(有向图)
大规模的图上,表示节点之间的结构信息。
一阶:局部的结构信息 (连接的节点很相似)
二阶:节点的邻居。共享邻居的节点可能是相似的 (不连接的节点A和B,但是A和B分别的连接点很相似,那么A和B也相似)邻居节点多的比较适用此办法
联合概率分布
经验概率分布
4.3 node2Vec
homophily:同质性
structural equivalence:结构等价性
BFS广度优先搜索
DFS深度优先搜索
4.4 Struct2Vec
对图的结构信息进行捕获,在其结构重要性大于邻居重要性时,有较好的效果。
4.5 SDNE
采用了多个非线性层的方式捕获一阶二阶的相似性。
四 消息传递
Source,Target
五 多层GCN的作用
GNN也可以有多层
GNN的本质就是更新各部分特征
其中输入是特征,输出也是特征,邻接矩阵也不会变的
(感受野)
六 图卷积GCN
图卷积和卷积有什么不同?
看起来都是利用周围的特征,但是在图中每个点的邻居是不确定的
节点分类,对每个结点进行预测,不同点是否有连接预测
整个图分类,部分图分类等,不同子图是否相似,异常检测等
GCN归根到底还是要完成特征提取操作,只不过输入对象不是固定格式
如何获取特征呢?
通常交给GCN两个东西就行:1.各节点输入特征 2.网络结构图(邻接矩阵)
很多文章,半监督任务也能解决
GCN的基本思想:
争对橙色节点,计算他的特征:平均其邻居特征(包括自身)后传入神经网络
网络层数
这个跟卷积类似,GCN也可以做多层,每一层输入的还是节点特征,然后将当前特征与网络结构图继续传入下层就可以不断算下去
A,D,F
七 Graph Neural Network图神经网络
7.1 GCN
图卷积网络(Graph Convolution Networks,GCN)
图卷积网络(Graph Convolutional Networks, GCN)详细介绍-CSDN博客
7.2 GraphSAGE
7.3 GAT
图注意力网络Graph Attention Network (GAT)
八 知识图谱结合图神经网络的模型模型
8.1 KGCN
KGCN提出于2019年。中心思想就是利用图神经网络的消息传递机制与基本推荐思想结合训练。在做KGCN模型时候,我们就把知识图谱是作为有权图,也就是关系会通过某种方式变为权重,而这个权重可被理解为是该关系影响用户行为的偏好程度。
计算过程
消息传递机制“Embedding”,Embedding 应该是一种映射,就像 Unicode 对应了某一个字符。在某种程度上,就是用来降维的,降维的原理就是矩阵乘法通俗讲解pytorch中nn.Embedding原理及使用 - 简书 (jianshu.com)
KGCN_基于知识图谱的推荐系统(KG+GCN)_kgcn代码-CSDN博客
lKGCN:推荐系统的知识图谱卷积神经网络(Hongwei et al.,2019) - 知乎
前置知识:
显式回馈(用户给物品打分)和隐式回馈(点击1,不点击0)
协同过滤:稀疏性,冷启动问题
需要明确:
1.user不属于entity
2.item是entity,但entity不一定是item(演员白鹿是一个实体,但是白鹿不能被推荐给用户,item理解为可以被推荐的东西)
3.user和item之间的关系是interaction(交互),entity和entity之间的关系叫relation.
所以在三元组其实是和interaction没有关系的。
摘要:
KGCN的特点:
1.端到端模型
2.通过感受野捕捉一个entity的邻域信息,且感受野可以扩展到高阶,以获取用户潜在的远距离兴趣
3.使用小批量方式进行训练
充分利用用用户-项目的属性信息。
将知识图谱引入推荐系统的三个优势
1.丰富了语义信息可以提升任务的精度
2.relation的多样性可以提升推荐项目的多样性
3.可解释性更强
RippleNet基本不关注relation的重要性
KGCN的关键思想:计算entity的representation时候聚集和 该entity的领域信息
这种设计的优势
1.能不或局部邻域结构并将其保存在当前entity中
2.relation和user都会影响邻域结构的权重,从而将知识图谱的语义和用户对于relation的个性化邢确这两个信息都引入进来
感受野:探查到附近的个数。(所以感受野的大小,合理化计算开销)
- user集合,item集合,interaction矩阵
- implicit feedback
- 知识图谱由entity和relation构成
- 学习预测函数,参数为interaction矩阵和知识图谱,用以预测某user和该user本身没有interaction的entity之间的交互概率
以单层KGCN Layer为例
直接与item v相连的entity集合
定义函数g,用以刻画user对于某relation的重要性score
公式算法
第三行的i表示:v目标节点的第几跳跃的节点
第二跳的时候将第一层和里面的v看成一个新的v。
步骤:
- 使用Microsoft Satori将上述数据集转化为KG
- 第二和第三类数据集相比于第一类更稀疏
- 三类数据集都是显式回馈,均将其转化为隐式
修改此算法的方向
- 构建邻域结构时基于超参数K,统一采样(即等概率采样),之后可以考虑non-uniform采样
- 模型是基于KG的item端进行建模,之后可以利用user端的信息
- 考虑设计算法,结合user端和item端
实践:
1.导入代码
GitHub - hwwang55/KGCN:知识图谱卷积网络的 tensorflow 实现
1.下载项目并且导入pycharm中
2.在terminal中下载movielens的数据集
wget http://files.grouplens.org/datasets/movielens/ml-20m.zip
$ wget http://files.grouplens.org/datasets/movielens/ml-20m.zip
$ unzip ml-20m.zip
$ mv ml-20m/ratings.csv data/movie/
其中出现的下载包,解压 压缩包命令没有成功,所以我手动操作的。(1.下载ml-20m的压缩包 2.解压此压缩包 3.将压缩包中的ratings.csv文件放在data/movie/下)
2.运行结果
点击率(CTR)
音乐集
3.解读整个项目
3.1 从preprocess.py开始
python preprocess.py -d movie
python代码输入参数参考:Python 讲堂 parse_args()详解_parser.parse_args-CSDN博客
import argparse
import numpy as np
RATING_FILE_NAME = dict({'movie': 'ratings.csv', 'book': 'BX-Book-Ratings.csv', 'music': 'user_artists.dat'})
SEP = dict({'movie': ',', 'book': ';', 'music': '\t'})
THRESHOLD = dict({'movie': 4, 'book': 0, 'music': 0})
'''
ratings_final.txt:user的序号,movie的序号,是否有关联
kg_final.txt:
'''
#将项目索引读取到实体 ID 文件:../数据/音乐/item_index2entity_id.txt
#item是entity,但是entity不一定是item
#读取的文件的部分数据如下
# 2 0
# 3 1
# 4 2
# 6 3
#输出为:item_index_old2new[2]=0,item_index_old2new[3]=1,item_index_old2new[4]=2,item_index_old2new[6]=3,
#entity_id2index[0]=0,entity_id2index[1]=1,entity_id2index[2]=2,entity_id2index[3]=3
def read_item_index_to_entity_id_file():
file = '../data/' + DATASET + '/item_index2entity_id.txt'
print('reading item index to entity id file: ' + file + ' ...')
i = 0
for line in open(file, encoding='utf-8').readlines():
item_index = line.strip().split('\t')[0]#电影id
satori_id = line.strip().split('\t')[1]#对应的序号 实体的序号,此项目自己排序的
item_index_old2new[item_index] = i #将item的序列号更新为实体的序号,等于将行号-1赋值了。
entity_id2index[satori_id] = i
i += 1
#转换评级
def convert_rating():
file = '../data/' + DATASET + '/' + RATING_FILE_NAME[DATASET]
#D:\PyCharmProjects\KGCN-master\KGCN-master\data\music\user_artists.dat
# userID artistID weight
# 2 51 13883
# 2 52 11690
# 2 53 11351
# 2 54 10300
print('reading rating file ...')
#set() 函数创建一个无序不重复元素集,可进行关系测试,删除重复数据,还可以计算交集、差集、并集等。
item_set = set(item_index_old2new.values())#所有新的item的id的set 这里set里面存的是item实体对应的序号了0,1,2,3
user_pos_ratings = dict()#用户-项目 大于阈值的正样本
user_neg_ratings = dict()#用户-项目 小于阈值的负样本
for line in open(file, encoding='utf-8').readlines()[1:]:#跳过表头
array = line.strip().split(SEP[DATASET])#strip( ) 是行首尾处理函数,'\t'为分隔符
# remove prefix and suffix quotation marks for BX dataset
if DATASET == 'book':
array = list(map(lambda x: x[1:-1], array))
item_index_old = array[1]#movie数据集里面的item就是movie 作者挑选一部分数据使用
#上一步在music里面item_index_old=artistID(51,52,53,54)艺术家的id
if item_index_old not in item_index_old2new: # 判断item_index_old2new里面有没有item_index_old这个键
continue#作者挑选一部分数据使用 忽视这个artistID
item_index = item_index_old2new[item_index_old]#给item重新赋值id 在itemId entityId 52 20 item_index= item_index_old2new[52]=20
#item_index=20代表这是entity的Id
user_index_old = int(array[0])#在ratings.csv中的原本userid
#user_index_old在原本的userId
rating = float(array[2])
if rating >= THRESHOLD[DATASET]:#THRESHOLD[music]=0
if user_index_old not in user_pos_ratings:
user_pos_ratings[user_index_old] = set()
user_pos_ratings[user_index_old].add(item_index)#user_pos_ratings[2] 20,这里一个userId对应user喜欢的所有的物品的entityId
else:
if user_index_old not in user_neg_ratings:
user_neg_ratings[user_index_old] = set()
user_neg_ratings[user_index_old].add(item_index)#userId不喜欢的所有entityId
print('converting rating file ...')
writer = open('../data/' + DATASET + '/ratings_final.txt', 'w', encoding='utf-8')
# ../data/music/ratings_final.txt
user_cnt = 0
user_index_old2new = dict()
for user_index_old, pos_item_set in user_pos_ratings.items():
if user_index_old not in user_index_old2new:#把旧的userid变为新的userid
user_index_old2new[user_index_old] = user_cnt
user_cnt += 1
user_index = user_index_old2new[user_index_old]
for item in pos_item_set:#user和item的关系初始化为1
writer.write('%d\t%d\t1\n' % (user_index, item)) #userId entityId 1 对喜欢的物品给的是1
unwatched_set = item_set - pos_item_set #此userId的用户不喜欢的item的集合
if user_index_old in user_neg_ratings:
unwatched_set -= user_neg_ratings[user_index_old]#把正样本和负样本的rating都去掉,留下一些和user没关系的rating
for item in np.random.choice(list(unwatched_set), size=len(pos_item_set), replace=False):
writer.write('%d\t%d\t0\n' % (user_index, item)) #userId entityId 0 对没有关系的物品给的是0
writer.close()
print('number of users: %d' % user_cnt)
print('number of items: %d' % len(item_set))
#注意这里没有处理user不喜欢的物品
#转换 KG 文件
def convert_kg():
print('converting kg file ...')
entity_cnt = len(entity_id2index)
relation_cnt = 0
writer = open('../data/' + DATASET + '/kg_final.txt', 'w', encoding='utf-8')
#../data/music/kg_final.txt
for line in open('../data/' + DATASET + '/kg.txt', encoding='utf-8'):
# 2086 music.artist.origin 3846
# 1601 film.person_or_entity_appearing_in_film.film 3847
# 3355 film.actor.film 3848
array = line.strip().split('\t')
head_old = array[0]
relation_old = array[1]
tail_old = array[2]
if head_old not in entity_id2index:#如果实体不在,就另外赋值id
entity_id2index[head_old] = entity_cnt
entity_cnt += 1
head = entity_id2index[head_old]
if tail_old not in entity_id2index:
entity_id2index[tail_old] = entity_cnt
entity_cnt += 1
tail = entity_id2index[tail_old]
if relation_old not in relation_id2index:
relation_id2index[relation_old] = relation_cnt
relation_cnt += 1
relation = relation_id2index[relation_old]
# print("entity_cnt=" + str(entity_cnt))
# print("relation_cnt=" + str(relation_cnt))
writer.write('%d\t%d\t%d\n' % (head, relation, tail))
writer.close()
print('number of entities (containing items): %d' % entity_cnt)
print('number of relations: %d' % relation_cnt)
#这个函数就是将原本的/kg.txt写入/kg_final.txt,如果实体没在原本的实体集合里面增加
if __name__ == '__main__':
#每次调用都seed()一下,表示种子相同,从而生成的随机数相同。随机数组相同
np.random.seed(555)
#ArgumentParser 对象包含将命令行解析成 Python 数据类型所需的全部信息
parser = argparse.ArgumentParser()
#给一个 ArgumentParser 添加程序参数信息是通过调用 add_argument() 方法完成的。
#name or flags - 一个命名或者一个选项字符串的列表
#type - 命令行参数应当被转换成的类型。
#default - 当参数未在命令行中出现时使用的值。
#help - 一个此选项作用的简单描述。
parser.add_argument('-d', type=str, default='movie', help='which dataset to preprocess')
#ArgumentParser 通过 parse_args() 方法解析参数
args = parser.parse_args()
#DATASET中的d代表add_argument中的名字,这里是d。DATASET=music
DATASET = args.d
#dict() 函数用于创建一个字典。
entity_id2index = dict()
relation_id2index = dict()
item_index_old2new = dict()
read_item_index_to_entity_id_file()
convert_rating()
convert_kg()
print('done')
处理的数据主要功能如下:
1.item-entity
#将item的序列号更新为实体的序号,等于将行号-1赋值了
2.user-item的关系 #userId entityId 1 对喜欢的物品给的是1
#userId entityId 0 对没有关系的物品给的是0
3.KG关系item-relation-item
# 2086 music.artist.origin 3846 # 1601 film.person_or_entity_appearing_in_film.film 3847 # 3355 film.actor.film 3848
输出的时候item会转换成entity
3.2 data_loader.py
import numpy as np
import os
def load_data(args):
n_user, n_item, train_data, eval_data, test_data = load_rating(args)
n_entity, n_relation, adj_entity, adj_relation = load_kg(args)
print('data loaded.')
'''
n_user:user的数量 13159
n_item:item的数量 16954
n_entity:KG中实体的数量 102569
n_relation:KG中关系的数量 32
train_data, eval_data, test_data:ratings_final.txt中的测试集合(userId,itemId,0/1)
adj_entity, adj_relation:KG中的实体-实体矩阵 实体-关系矩阵
'''
return n_user, n_item, n_entity, n_relation, train_data, eval_data, test_data, adj_entity, adj_relation
def load_rating(args):
print('reading rating file ...')
# reading rating file
rating_file = '../data/' + args.dataset + '/ratings_final'
if os.path.exists(rating_file + '.npy'):
rating_np = np.load(rating_file + '.npy')
else:
rating_np = np.loadtxt(rating_file + '.txt', dtype=np.int64)
np.save(rating_file + '.npy', rating_np)
n_user = len(set(rating_np[:, 0]))
n_item = len(set(rating_np[:, 1]))
train_data, eval_data, test_data = dataset_split(rating_np, args)
return n_user, n_item, train_data, eval_data, test_data
# user-item 0/1
def dataset_split(rating_np, args):
print('splitting dataset ...')
# train:eval:test = 6:2:2
eval_ratio = 0.2
test_ratio = 0.2
#shape[0]输出rating_np的行数,shape[0]输出矩阵的列数。
n_ratings = rating_np.shape[0]#多少条评论
#随机生产评估集
#在n_ratings数据集中,随机抽出20%不重复的数据
eval_indices = np.random.choice(list(range(n_ratings)), size=int(n_ratings * eval_ratio), replace=False)
left = set(range(n_ratings)) - set(eval_indices)
#下面代表将剩下的数据集,测试数据集大小为n_ratings * test_ratio,不重复
test_indices = np.random.choice(list(left), size=int(n_ratings * test_ratio), replace=False)
train_indices = list(left - set(test_indices))
#训练数据集的大小<1,可以再次降低训练集的大小
if args.ratio < 1:
train_indices = np.random.choice(list(train_indices), size=int(len(train_indices) * args.ratio), replace=False)
train_data = rating_np[train_indices]
eval_data = rating_np[eval_indices]
test_data = rating_np[test_indices]
return train_data, eval_data, test_data
# entity-relation-entity
def load_kg(args):
print('reading KG file ...')
# reading kg file
kg_file = '../data/' + args.dataset + '/kg_final'
if os.path.exists(kg_file + '.npy'):
kg_np = np.load(kg_file + '.npy')
else:
kg_np = np.loadtxt(kg_file + '.txt', dtype=np.int64)
np.save(kg_file + '.npy', kg_np)
#set(kg_np[:, 0])和set(kg_np[:, 2])的长度都包括了,求并集
n_entity = len(set(kg_np[:, 0]) | set(kg_np[:, 2]))
n_relation = len(set(kg_np[:, 1]))
#{肖申克的救赎:[(导演,弗兰克·达拉邦特),(主演,摩根·弗里曼),....],弗兰克·达拉邦特:[(导演,肖申克的救赎),...],....} 这里在代码里面是先写entity,再写relation(弗兰克·达拉邦特,导演)
#这个relation头尾直接交换觉得有问题
kg = construct_kg(kg_np)
#实体邻接矩阵、关系邻接矩阵 有4个邻居 都是(n_entity,4)纬矩阵
adj_entity, adj_relation = construct_adj(args, kg, n_entity)
return n_entity, n_relation, adj_entity, adj_relation
def construct_kg(kg_np):
print('constructing knowledge graph ...')
kg = dict()
for triple in kg_np:
head = triple[0]
relation = triple[1]
tail = triple[2]
# treat the KG as an undirected graph
if head not in kg:
kg[head] = []
kg[head].append((tail, relation))
if tail not in kg:
kg[tail] = []
kg[tail].append((head, relation))
return kg
# 构建邻接矩阵
def construct_adj(args, kg, entity_num):
print('constructing adjacency matrix ...')
# each line of adj_entity stores the sampled neighbor entities for a given entity
# each line of adj_relation stores the corresponding sampled neighbor relations
# 返回来一个给定形状和类型的用0填充的数组 adj_entity创建一个 实体行,要采样的邻居数为列数
adj_entity = np.zeros([entity_num, args.neighbor_sample_size], dtype=np.int64)
adj_relation = np.zeros([entity_num, args.neighbor_sample_size], dtype=np.int64)
for entity in range(entity_num):
neighbors = kg[entity]#list
n_neighbors = len(neighbors)
if n_neighbors >= args.neighbor_sample_size:
sampled_indices = np.random.choice(list(range(n_neighbors)), size=args.neighbor_sample_size, replace=False)
else:#邻居可重复
sampled_indices = np.random.choice(list(range(n_neighbors)), size=args.neighbor_sample_size, replace=True)
# relation放在最后面的[5,7,4,9]表示entity0和entity5,7,4,9相邻
adj_entity[entity] = np.array([neighbors[i][0] for i in sampled_indices])
adj_relation[entity] = np.array([neighbors[i][1] for i in sampled_indices])
return adj_entity, adj_relation
输出为
''' n_user:user的数量 13159 n_item:item的数量 16954 n_entity:KG中实体的数量 102569 n_relation:KG中关系的数量 32 train_data, eval_data, test_data:ratings_final.txt中的测试集合(userId,itemId,0/1) adj_entity, adj_relation:KG中的实体-实体矩阵 实体-关系矩阵 这两个矩阵一一对应,矩阵中的数字分别表示entity(矩阵的行数)与此行的数字的实体是邻居,关系对应第二个矩阵相应的位置。 '''