位置: IT常识 - 正文

图卷积神经网络GCN、GAT的原理及Pytorch实现(图卷积神经网络原理)

发布时间:2024-01-08
图卷积神经网络GCN、GAT的原理及Pytorch实现

目录

一、前言

二、图的概念

三、GNN图神经网络

四、GNN与CNN、RNN的区别

五、GNN原理

5.1 邻接矩阵

5.2 聚合操作

5.3 多层迭代

六、GCN图卷集神经网络

七、GCN的Pytorch实现

7.1 数据集介绍

7.2 代码详解

7.3 代码运行结果

八、GAT 图注意力网络 Graph Attention Network

8.1 引入Attention机制

8.2 采用Multi-Head Attention

8.3 代码实现


一、前言

推荐整理分享图卷积神经网络GCN、GAT的原理及Pytorch实现(图卷积神经网络原理),希望有所帮助,仅作参考,欢迎阅读内容。

文章相关热门搜索词:图卷积神经网络代码,图卷积神经网络介绍,图卷积神经网络代码,密集图传播模型 图卷积神经网络,基于空间的图卷积神经网络,密集图传播模型 图卷积神经网络,图卷积神经网络代码,图卷积神经网络和图神经网络区别,内容如对您有帮助,希望把文章链接给更多的朋友!

ICLR作为机器学习方向的顶会,最近看了ICLR2023 Openreview的论文投稿分析,通过2022和2023年论文关键词、标题高频词等信息的可视化比较。根据前十的关键词频率排名频率来看,基本上和去年保持一致,大火的领域依旧大火。但是可以明显看到前五名关键词的频率差距逐渐减少。 有意思的是representation learning这一关键词终于又重回前三,再次为「国际学习表征会议」(ICLR)正名。graph neural network这一关键词则是掉了一名,与representation learning交换了位置,但相比于去年的频率仍然火爆。GCN作为GNN的变种,依然是一个发论文的热门。

Keyword20222023reinforcement learning11deep learning22representation learning43graph neural network34transformer55federate learning76self-supervised learning67contrastive learning108robustness99generative model810

从排名变化上来看,尽管graph neural network在关键词频率排名降低了一名,但是在标题中graph却涨了一名。

Title20222023representation11graph32data63reinforcement24transformer75training56image107efficient98language159federate1410二、图的概念

在讨论GNN之前,我们先来了解一下什么是图。在计算机科学中,图是由节点和边两部分组成的一种数据结构。图G可以通过节点集合V和它包含的边E来进行描述。如下图所示:

三、GNN图神经网络

GNN全称----图神经网络,它是一种直接作用于图结构上的神经网络。我们可以把图中的每一个节点 V 当作个体对象,而每一条边 E 当作个体与个体间的某种联系,所有节点组成的关系网就是最后的图 U。

二元组的定义 图G是一个有序二元组(V,E),其中V称为顶集(Vertices Set),E称为边集(Edges set),E与V不相交。它们亦可写成V(G)和E(G)。 E的元素都是二元组,用(x,y)表示,其中x,y∈V。

三元组的定义 图G是指一个三元组(V,E,I),其中V称为顶集,E称为边集,E与V不相交;I称为关联函数,I将E中的每一个元素映射到 。如果e被映射到(u,v),那么称边e连接顶点u,v,而u,v则称作e的端点,u,v此时关于e相邻。同时,若两条边i,j有一个公共顶点u,则称i,j关于u相邻。

我们利用图神经网络的目的就是整合特征

GNN的主要目的就是用图结构提取特征,最终是做节点分类还是关联预测由我们自己决定。

四、GNN与CNN、RNN的区别

都是提取特征的神经网络,那为什么要利用图模型来提取呢?CNN的卷积和RNN的递归方式不行吗? 答案还真不行,或者说十分麻烦。 

因为GNN面向的输入对象其实都是结构不规则、不固定的数据结构,而CNN面向的图像数据和RNN面向的文本数据的格式都是固定的,所以自然不能混为一谈。因此,面对本身结构、彼此关系都不固定的节点特征,必须需要借助图结构来表征它们的内在联系。

五、GNN原理5.1 邻接矩阵

首先引入邻接矩阵(Adjacency Matrix)的概念,它来表示节点与节点间的连接关系,即Edge的关系,矩阵的具体样式如下图所示:

5.2 聚合操作

GNN的输入一般是每个节点的起始特征向量和表示节点间关系的邻接矩阵,有了这两个输入信息,接下来就是聚合操作了。所谓的聚合,其实就是将周边与节点 Vi 有关联的节点{Va , Vb , . . .}加权到Vi上,当作一次特征更新。同理,对图中的每个节点进行聚合操作,更新所有图节点的特征。

聚合操作的方式多种多样,可根据任务的不同自由选择,如下图所示:

当然对这个图节点进行完了一次聚合操作后,还需要再进行一波 w 的加权,这里的 w 需要网络自己学习。

5.3 多层迭代

CNN,RNN都可以有多个层,那么GNN也当然可以。一次图节点聚合操作与 w ww 加权,可以理解为一层,后面再重复进行聚合、加权,就是多层迭代了。一般GNN只要3~5层即可,所以训练GNN对算力要求很低。如下图所示:

六、GCN图卷集神经网络

论文:Semi-Supervised Classification with Graph Convolutional Networks(ICLR2017)

(https://arxiv.org/abs/1609.02907)

GCN,图卷积神经网络,实际上跟CNN的作用一样,就是一个特征提取器,只不过它的对象是图数据。GCN精妙地设计了一种从图数据中提取特征的方法,从而让我们可以使用这些特征去对图数据进行节点分类(node classification)、图分类(graph classification)、边预测(link prediction),还可以顺便得到图的嵌入表示(graph embedding),可见用途广泛。因此现在人们脑洞大开,让GCN到各个领域中发光发热。

GCN的核心部分是什么样子:

假设我们手头有一批图数据,其中有N个节点(node),每个节点都有自己的特征,我们设这些节点的特征组成一个N×D维的矩阵X,然后各个节点之间的关系也会形成一个N×N维的矩阵A,也称为邻接矩阵(adjacency matrix)。X和A便是我们模型的输入。

如下图:

Labeled graph(标签图),图的路径和节点标签

Degree matrix(度矩阵) ,每个节点的度,即与每个节点连接的节点数量

Adjacency matrix(邻接矩阵),每个节点连接的节点位置

Laplacian matrix(拉普拉斯矩阵),类比离散拉普拉斯算子定义图上拉普拉斯算子为节点与邻居节点特征信息f差异的和

L = D - A

GCN也是一个神经网络层,它的层与层之间的传播方式是:

这个公式中: 

A波浪=A+I,I是单位矩阵D波浪是A波浪的度矩阵(degree matrix),公式为H是每一层的特征,对于输入层的话,H就是Xσ是非线性激活函数

我们先不用考虑为什么要这样去设计一个公式。我们现在只用知道:

这个部分,是可以事先算好的,因为D波浪由A计算而来,而A是我们的输入之一。

所以对于不需要去了解数学原理、只想应用GCN来解决实际问题的人来说,你只用知道:哦,这个GCN设计了一个牛逼的公式,用这个公式就可以很好地提取图的特征。这就够了,毕竟不是什么事情都需要知道内部原理,这是根据需求决定的。

为了直观理解,我们用论文中的一幅图:

上图中的GCN输入一个图,通过若干层GCN每个node的特征从X变成了Z,但是,无论中间有多少层,node之间的连接关系,即A,都是共享的。

假设我们构造一个两层的GCN,激活函数分别采用ReLU和Softmax,则整体的正向传播的公式为:

最后,我们针对所有带标签的节点计算cross entropy损失函数: 

就可以训练一个node classification的模型了。由于即使只有很少的node有标签也能训练,作者称他们的方法为半监督分类。

当然,你也可以用这个方法去做graph classification、link prediction,只是把损失函数给变化一下即可。

最终的层特征传播公式:

因为即使不训练,完全使用随机初始化的参数W,GCN提取出来的特征就以及十分优秀了!这跟CNN不训练是完全不一样的,后者不训练是根本得不到什么有效特征的。

我们看论文原文:

然后作者做了一个实验,使用一个俱乐部会员的关系网络,使用随机初始化的GCN进行特征提取,得到各个node的embedding,然后可视化:

可以发现,在原数据中同类别的node,经过GCN的提取出的embedding,已经在空间上自动聚类了。而这种聚类结果,可以和DeepWalk、node2vec这种经过复杂训练得到的node embedding的效果媲美了。

说的夸张一点,比赛还没开始,GCN就已经在终点了。还没训练就已经效果这么好,那给少量的标注信息,GCN的效果就会更加出色。

其他关于GCN的点滴:

对于很多网络,我们可能没有节点的特征,这个时候可以使用GCN吗?答案是可以的,如论文中作者对那个俱乐部网络,采用的方法就是用单位矩阵 I 替换特征矩阵 X。

我没有任何的节点类别的标注,或者什么其他的标注信息,可以使用GCN吗?当然,就如前面讲的,不训练的GCN,也可以用来提取graph embedding,而且效果还不错。

GCN网络的层数多少比较好?论文的作者做过GCN网络深度的对比研究,在他们的实验中发现,GCN层数不宜多,2-3层的效果就很好了。

七、GCN的Pytorch实现7.1 数据集介绍

1. 数据集结构 论文中所使用的数据集合是Cora数据集下载地址https://linqs-data.soe.ucsc.edu/public/lbc/cora.tgz

总共有三部分构成:cora.content cora.cites 和README。

README: 对数据集内容的描述;

cora.content: 里面包含有每一篇论文各自独立的信息;

31336 0 0 0 0 0 0 0 0 0 0 0 0 0 ... Neural_Networks1061127 0 0 0 0 0 0 0 0 0 0 0 0 1 ... Rule_Learning

 ......

该文件总共包含2078行,每一行代表一篇论文,由论文编号、论文词向量(1433维)和论文的类别三个部分组成

cora.cites: 里面包含有各论文之间的相互引用记录

该文件总共包含5429行,每一行是两篇论文的编号,表示右边的论文引用左边的论文。

35 103335 10348235 10351535 105067935 110396035 110398535 110919935 111291135 111343835 111383135 111433135 1117476

2. 数据集内容分析 该数据集总共有2078个样本,而且每个样本都为一篇论文。根据README可知,所有的论文被分为了7个类别,分别为:

基于案列的论文                 Case_Based

基于遗传算法的论文          Genetic_Algorithms

基于神经网络的论文          Neural_Networks

基于概率方法的论文          Probabilistic_Methods

基于强化学习的论文          Reinforcement_Learning

基于规则学习的论文          Rule_Learning

理论描述类的论文              Theory

此外,为了区分论文的类别,使用一个1433维的词向量,对每一篇论文进行描述,该向量的每个元素都为一个词语是否在论文中出现,如果出现则为“1”,否则为“0”。

3. 数据流变化

图卷积神经网络GCN、GAT的原理及Pytorch实现(图卷积神经网络原理)

(1) idx_features_labels (数据包含id,features和labels,维度为2708*1433)

idx_features_labels = np.genfromtxt("{}{}.content".format(path, dataset), dtype=np.dtype(str))

为content里面的内容,包含idx(论文id),features(论文词向量表示),labels(论文标签)

(2) features(节点的特征,维度为 2708 * 1433,类型为 np.ndarray,稀疏矩阵样式为行列,值)

features = sp.csr_matrix(idx_features_labels[:, 1:-1], dtype=np.float32)

(3)  labels(节点的标签,维度为2708*7,总共包括7个类别,类型为 np.ndarray)

labels = encode_onehot(idx_features_labels[:, -1])

构建图

(4) idx (节点的id,维度为2708*1)

idx = np.array(idx_features_labels[:, 0], dtype=np.int32)

(5) idx_map (对每个id进行编号,dict:2708)

idx_map = {j: i for i, j in enumerate(idx)}

{31336: 0, 1061127: 1, 1106406: 2, 13195: 3, 37879: 4, 1126012: 5, 1107140: 6, 1102850: 7, 31349: 8, 1106418: 9, 1123188: 10, 1128990: 11, 109323: 12, 217139: 13, 31353: 14, 32083: 15, 1126029: 16, 1118017: 17, 49482: 18, 753265: 19, 249858: 20, 1113739: 21, 48766: 22, 646195: 23, 1126050: 24, 59626: 25, 340299: 26, 354004: 27, 242637: 28, 1106492: 29, 74975: 30, 1152272: 31, 100701: 32, 66982: 33, 13960: 34, 13966: 35, 66990: 36, 182093: 37, 182094: 38, 13972: 39, 13982: 40, 16819: 41, 273152: 42, 237521: 43, 1153703: 44, 32872: 45, 284025: 46, 218666: 47, 16843: 48, 1153724: 49, 1153728: 50, 158098: 51, 8699: 52, 1134865: 53, 28456: 54, 248425: 55, 1112319: 56, 28471: 57, 175548: 58, 696345: 59, 28485: 60, 1139195: 61, 35778: 62, 28491: 63, 310530: 64, 1153784: 65, 1481: 66, 1153786: 67, 13212: 68, 1111614: 69, 5055: 70, 4329: 71, 330148: 72, 1105062: 73, 4330: 74, 5062: 75, 4335: 76, 158812: 77, 40124: 78, 1103610: 79, 688361: 80, 302545: 81, 20534: 82, 1031453: 83, 5086: 84, 193742: 85, 58268: 86, 424: 87, 40151: 88, 636098: 89, 260121: 90, 950052: 91, 434: 92, 1131270: 93, 1131274: 94, 1131277: 95, 1110947: 96, 662279: 97, 1139928: 98, 153063: 99... 

(6) edges_unordered(构建节点间的连接信息,维度为5429*2)

edges_unordered = np.genfromtxt("{}{}.cites".format(path, dataset), dtype=np.int32)

(7) edges(根据 idx_map和edge_undered构建边的信息,维度为5429*2)

edges = np.array(list(map(idx_map.get, edges_unordered.flatten())), dtype=np.int32).reshape(edges_unordered.shape) map函数会根据提供的函数对指定序列做映射,flatten()函数就是降到一维 

(备注:163对应cora.cites文件的第一列的第一个编号,402对应cora.cites文件中的第二列的第一个编号)

(8) adj (构建邻接矩阵,维度为2708*2708)

adj = sp.coo_matrix((np.ones(edges.shape[0]), (edges[:, 0], edges[:, 1])), shape=(labels.shape[0], labels.shape[0]), dtype=np.float32)

# build symmetric adjacency matrixadj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj)

(9) 特征归一化函数

#特征归一化函数# 归一化函数实现的方式:对传入特征矩阵的每一行分别求和,取到数后就是每一行非零元素归一化的值,然后与传入特征矩阵进行点乘def normalize(mx): """Row-normalize sparse matrix""" #对稀疏矩阵进行正则化处理 rowsum = np.array(mx.sum(1)) #会得到一个(2708,1)的矩阵 r_inv = np.power(rowsum, -1).flatten() #数组元素求-1次方,得到(2708,)的元组 # 在计算倒数的时候存在一个问题,如果原来的值为0,则其倒数为无穷大,因此需要对r_inv中无穷大的值进行修正,更改为0 r_inv[np.isinf(r_inv)] = 0. r_mat_inv = sp.diags(r_inv) mx = r_mat_inv.dot(mx) return mxfeatures = normalize(features)

adj = normalize(adj + sp.eye(adj.shape[0]))

7.2 代码详解

代码总览(后面会上传到github上)

1. utils.py

import numpy as npimport scipy.sparse as spimport torch#特征独热码处理def encode_onehot(labels): # 将所有的标签整合成一个不重复的列表 classes = set(labels) # set() 函数创建一个无序不重复元素集 '''enumerate()函数生成序列,带有索引i和值c。 这一句将string类型的label变为int类型的label,建立映射关系 np.identity(len(classes)) 为创建一个classes的单位矩阵 创建一个字典,索引为 label, 值为独热码向量(就是之前生成的矩阵中的某一行)''' classes_dict = {c: np.identity(len(classes))[i, :] for i, c in enumerate(classes)} # 为所有的标签生成相应的独热码 # map() 会根据提供的函数对指定序列做映射。 # 这一句将string类型的label替换为int类型的label labels_onehot = np.array(list(map(classes_dict.get, labels)), dtype=np.int32) return labels_onehot#数据加载和处理def load_data(path="../data/cora/", dataset="cora"): """Load citation network dataset (cora only for now)""" print('Loading {} dataset...'.format(dataset)) #数据包含id,features和labels idx_features_labels = np.genfromtxt("{}{}.content".format(path, dataset), dtype=np.dtype(str)) #节点的特征,维度为 2708 * 1433,类型为 np.ndarray features = sp.csr_matrix(idx_features_labels[:, 1:-1], dtype=np.float32) #节点的标签,总共包括7个类别,类型为 np.ndarray labels = encode_onehot(idx_features_labels[:, -1]) # build graph #节点的id idx = np.array(idx_features_labels[:, 0], dtype=np.int32) #每个id进行编号 idx_map = {j: i for i, j in enumerate(idx)} # edges_unordered = np.genfromtxt("{}{}.cites".format(path, dataset), dtype=np.int32) edges = np.array(list(map(idx_map.get, edges_unordered.flatten())), dtype=np.int32).reshape(edges_unordered.shape) #map会根据提供的函数对指定序列做映射,flatten()就是降到一维 adj = sp.coo_matrix((np.ones(edges.shape[0]), (edges[:, 0], edges[:, 1])), shape=(labels.shape[0], labels.shape[0]), dtype=np.float32) # build symmetric adjacency matrix adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj) features = normalize(features) adj = normalize(adj + sp.eye(adj.shape[0])) idx_train = range(140) idx_val = range(200, 500) idx_test = range(500, 1500) features = torch.FloatTensor(np.array(features.todense())) labels = torch.LongTensor(np.where(labels)[1]) adj = sparse_mx_to_torch_sparse_tensor(adj) idx_train = torch.LongTensor(idx_train) idx_val = torch.LongTensor(idx_val) idx_test = torch.LongTensor(idx_test) return adj, features, labels, idx_train, idx_val, idx_test#特征归一化函数def normalize(mx): """Row-normalize sparse matrix""" #对稀疏矩阵进行正则化处理 rowsum = np.array(mx.sum(1)) #会得到一个(2708,1)的矩阵 r_inv = np.power(rowsum, -1).flatten() #数组元素求-1次方 # 在计算倒数的时候存在一个问题,如果原来的值为0,则其倒数为无穷大,因此需要对r_inv中无穷大的值进行修正,更改为0 r_inv[np.isinf(r_inv)] = 0. r_mat_inv = sp.diags(r_inv) mx = r_mat_inv.dot(mx) return mx#精度计算函数def accuracy(output, labels): # 使用type_as(tesnor)将张量转换为给定类型的张量 preds = output.max(1)[1].type_as(labels) # 记录等于preds的label eq:equal correct = preds.eq(labels).double() correct = correct.sum() return correct / len(labels)#将scipy稀疏矩阵转换为Torch稀疏张量def sparse_mx_to_torch_sparse_tensor(sparse_mx): """Convert a scipy sparse matrix to a torch sparse tensor.""" """ numpy中的ndarray转化成pytorch中的tensor : torch.from_numpy() pytorch中的tensor转化成numpy中的ndarray : numpy() """ sparse_mx = sparse_mx.tocoo().astype(np.float32) indices = torch.from_numpy( np.vstack((sparse_mx.row, sparse_mx.col)).astype(np.int64)) values = torch.from_numpy(sparse_mx.data) shape = torch.Size(sparse_mx.shape) return torch.sparse.FloatTensor(indices, values, shape)

2. models.py

import torch.nn as nnimport torch.nn.functional as Ffrom pygcn.layers import GraphConvolutionclass GCN(nn.Module): def __init__(self, nfeat, nhid, nclass, dropout): super(GCN, self).__init__() self.gc1 = GraphConvolution(nfeat, nhid) #卷积层1:输入的特征为nfeat,维度是2708,输出的特征为nhid,维度是16 self.gc2 = GraphConvolution(nhid, nclass) #卷积层2:输入的特征为nhid,维度是16,输出的特征为nclass,维度是7(即类别的结果) self.dropout = dropout def forward(self, x, adj): #forward是向前传播函数,最终得到网络向前传播的方式为:relu–>fropout–>gc2–>softmax x = F.relu(self.gc1(x, adj)) x = F.dropout(x, self.dropout, training=self.training) x = self.gc2(x, adj) return F.log_softmax(x, dim=1)

3. layers.py

import mathimport torchfrom torch.nn.parameter import Parameterfrom torch.nn.modules.module import Module#layers中主要定义了图数据实现卷积操作的层,类似于CNN中的卷积层,只是一个层而已class GraphConvolution(Module): """ Simple GCN layer, similar to https://arxiv.org/abs/1609.02907 """ #GraphConvolution作为一个类,首先需要定义其相关属性。主要定义了其输入特征in_feature、输出特征out_feature两个输入, # 以及权重weight和偏移向量bias两个参数,同时调用了其参数初始化的方法 def __init__(self, in_features, out_features, bias=True): super(GraphConvolution, self).__init__() self.in_features = in_features self.out_features = out_features self.weight = Parameter(torch.FloatTensor(in_features, out_features)) if bias: self.bias = Parameter(torch.FloatTensor(out_features)) else: self.register_parameter('bias', None) self.reset_parameters() #为了让每次训练产生的初始参数尽可能的相同,从而便于实验结果的复现,可以设置固定的随机数生成种子 def reset_parameters(self): stdv = 1. / math.sqrt(self.weight.size(1)) #标准偏差 self.weight.data.uniform_(-stdv, stdv) if self.bias is not None: self.bias.data.uniform_(-stdv, stdv) #此处主要定义的是本层的前向传播,通常采用的是 A ∗ X ∗ W A * X * WA∗X∗W的计算方法。由于A是一个sparse变量,因此其与X进行卷积的结果也是稀疏矩阵 def forward(self, input, adj): support = torch.mm(input, self.weight) output = torch.spmm(adj, support) if self.bias is not None: return output + self.bias else: return output #__repr__()方法是类的实例化对象用来做“自我介绍”的方法,默认情况下,它会返回当前对象的“类名+object at+内存地址”, # 而如果对该方法进行重写,可以为其制作自定义的自我描述信息 def __repr__(self): return self.__class__.__name__ + ' (' \ + str(self.in_features) + ' -> ' \ + str(self.out_features) + ')'

4. train.py

from __future__ import divisionfrom __future__ import print_functionimport timeimport argparseimport numpy as npimport torchimport torch.nn.functional as Fimport torch.optim as optimfrom pygcn.utils import load_data, accuracyfrom pygcn.models import GCNimport matplotlib.pyplot as plt# Training settingsparser = argparse.ArgumentParser()parser.add_argument('--no-cuda', action='store_true', default=False, help='Disables CUDA training.')parser.add_argument('--fastmode', action='store_true', default=False, help='Validate during training pass.')parser.add_argument('--seed', type=int, default=42, help='Random seed.')parser.add_argument('--epochs', type=int, default=200, help='Number of epochs to train.')parser.add_argument('--lr', type=float, default=0.01, help='Initial learning rate.')parser.add_argument('--weight_decay', type=float, default=5e-4, help='Weight decay (L2 loss on parameters).')parser.add_argument('--hidden', type=int, default=16, help='Number of hidden units.')parser.add_argument('--dropout', type=float, default=0.5, help='Dropout rate (1 - keep probability).')args = parser.parse_args()args.cuda = not args.no_cuda and torch.cuda.is_available()np.random.seed(args.seed)torch.manual_seed(args.seed)if args.cuda: torch.cuda.manual_seed(args.seed)# Load dataadj, features, labels, idx_train, idx_val, idx_test = load_data()# Model and optimizermodel = GCN(nfeat=features.shape[1], nhid=args.hidden, nclass=labels.max().item() + 1, dropout=args.dropout)optimizer = optim.Adam(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)if args.cuda: model.cuda() features = features.cuda() adj = adj.cuda() labels = labels.cuda() idx_train = idx_train.cuda() idx_val = idx_val.cuda() idx_test = idx_test.cuda()def train(): loss_history = [] val_acc_history = [] t = time.time() model.train() if not args.fastmode: # Evaluate validation set performance separately, # deactivates dropout during validation run. model.eval() output = model(features, adj) t_total = time.time() for epoch in range(args.epochs): optimizer.zero_grad() output = model(features, adj) loss_train = F.nll_loss(output[idx_train], labels[idx_train]) acc_train = accuracy(output[idx_train], labels[idx_train]) loss_train.backward() optimizer.step() loss_val = F.nll_loss(output[idx_val], labels[idx_val]) acc_val = accuracy(output[idx_val], labels[idx_val]) loss_history.append(loss_val.item()) val_acc_history.append(acc_val.item()) print('Epoch: {:04d}'.format(epoch+1), 'loss_train: {:.4f}'.format(loss_train.item()), 'acc_train: {:.4f}'.format(acc_train.item()), 'loss_val: {:.4f}'.format(loss_val.item()), 'acc_val: {:.4f}'.format(acc_val.item()), 'time: {:.4f}s'.format(time.time() - t)) print("Optimization Finished!") print("Total time elapsed: {:.4f}s".format(time.time() - t_total)) return loss_history,val_acc_historydef test(): model.eval() output = model(features, adj) # test_mask_logits = output[mask] loss_test = F.nll_loss(output[idx_test], labels[idx_test]) acc_test = accuracy(output[idx_test], labels[idx_test]) print("Test set results:", "loss= {:.4f}".format(loss_test.item()), "accuracy= {:.4f}".format(acc_test.item()))#绘制loss和acc曲线def plot_loss_with_acc(loss_history, val_acc_history): fig = plt.figure() ax1 = fig.add_subplot(111) ax1.plot(range(len(loss_history)), loss_history, c=np.array([255, 71, 90]) / 255.) plt.ylabel('Loss') ax2 = fig.add_subplot(111, sharex=ax1, frameon=False) ax2.plot(range(len(val_acc_history)), val_acc_history, c=np.array([79, 179, 255]) / 255.) ax2.yaxis.tick_right() ax2.yaxis.set_label_position("right") plt.ylabel('ValAcc') plt.xlabel('Epoch') plt.title('Training Loss & Validation Accuracy') plt.show()# Train modelloss, val_acc = train()plot_loss_with_acc(loss, val_acc)#Testingtest()# 绘制测试数据的TSNE降维图output = model(features, adj)output = output.cpu()output = output[idx_test].detach().numpy()print(output)print(output.shape)from sklearn.manifold import TSNEtsne = TSNE()out = tsne.fit_transform(output)fig = plt.figure()labels_test = labels[idx_test].detach().cpu().numpy()for i in range(7): indices = labels_test == i # print(indices) x, y = out[indices].T plt.scatter(x, y, label=str(i))plt.legend()plt.show()7.3 代码运行结果

(1)loss和acc训练曲线

200epochs训练之后,Test set results: loss= 0.6594 accuracy= 0.8360 

(2)测试数据的TSNE降维图

八、GAT 图注意力网络 Graph Attention Network

       图神经网络 GNN 把深度学习应用到图结构 (Graph) 中,其中的图卷积网络 GCN 可以在 Graph 上进行卷积操作。但是 GCN 存在一些缺陷:依赖拉普拉斯矩阵,不能直接用于有向图;模型训练依赖于整个图结构,不能用于动态图;卷积的时候没办法为邻居节点分配不同的权重。因此 2018 年图注意力网络 GAT (Graph Attention Network) 被提出,解决 GCN 存在的问题。 

论文下载地址:https://arxiv.org/abs/1710.10903

GCN 存在下面的缺点:

GCN 假设图是无向的,因为利用了对称的拉普拉斯矩阵 (只有邻接矩阵 A 是对称的,拉普拉斯矩阵才可以正交分解),不能直接用于有向图。GCN 的作者为了处理有向图,需要对 Graph 结构进行调整,要把有向边划分成两个节点放入 Graph 中。例如 e1、e2 为两个节点,r 为 e1,e2 的有向关系,则需要把 r 划分为两个关系节点 r1 和 r2 放入图中。连接 (e1, r1)、(e2, r2)。

GCN 不能处理动态图,GCN 在训练时依赖于具体的图结构,测试的时候也要在相同的图上进行。因此只能处理 transductive 任务,不能处理 inductive 任务。transductive 指训练和测试的时候基于相同的图结构,例如在一个社交网络上,知道一部分人的类别,预测另一部分人的类别。inductive 指训练和测试使用不同的图结构,例如在一个社交网络上训练,在另一个社交网络上预测。

GCN 不能为每个邻居分配不同的权重,GCN 在卷积时对所有邻居节点均一视同仁,不能根据节点重要性分配不同的权重。

       2018 年图注意力网络 GAT 被提出,用于解决 GCN 的上述问题,论文是《GRAPH ATTENTION NETWORKS》(ICLR2018)。GAT 采用了 Attention 机制,可以为不同节点分配不同权重,训练时依赖于成对的相邻节点,而不依赖具体的网络结构,可以用于 inductive 任务。 

8.1 引入Attention机制

假设 Graph 包含 N 个节点,每个节点的特征向量为 hi,维度是 F,如下所示:

节点特征向量 h 

对节点特征向量 h 进行线性变换,可以得到新的特征向量 h'i,维度是 F',如下所示,W 为线性变换的矩阵:

节点特征向量线性变换 h'

节点 j 是节点 i 的邻居,则可以使用 Attention 机制计算节点 j 对于节点 i 的重要性,即 Attention Score:

Attention Score

GAT 具体的 Attention 做法如下,把节点 i、j 的特征向量 h'i、h'j 拼接在一起,然后和一个 2F' 维的向量 a 计算内积。激活函数采用 LeakyReLU,公式如下:

GAT Attention 计算方式

Attention 如下图所示: 

GAT Attention 示意图 

经过 Attention 之后节点 i 的特征向量如下:

Attention 后的特征向量

8.2 采用Multi-Head Attention

GAT 也可以采用 Multi-Head Attention,即多个 Attention,如下图所示:

GAT Multi-Head Attention

如果有 K 个 Attention,则需要把 K 个 Attention 生成的向量拼接在一起,如下: 

K 个 Attention 输出结果拼接

但是如果是最后一层,则 K 个 Attention 的输出不进行拼接,而是求平均。 

最后一层 Attention 输出结果

8.3 代码实现

数据集与上一节(第七节)中的一致,代码基本一致,只是稍作处理

utils.py

import numpy as npimport scipy.sparse as spimport torchdef encode_onehot(labels): # The classes must be sorted before encoding to enable static class encoding. # In other words, make sure the first class always maps to index 0. classes = sorted(list(set(labels))) classes_dict = {c: np.identity(len(classes))[i, :] for i, c in enumerate(classes)} labels_onehot = np.array(list(map(classes_dict.get, labels)), dtype=np.int32) return labels_onehotdef load_data(path="./data/cora/", dataset="cora"): """Load citation network dataset (cora only for now)""" print('Loading {} dataset...'.format(dataset)) idx_features_labels = np.genfromtxt("{}{}.content".format(path, dataset), dtype=np.dtype(str)) features = sp.csr_matrix(idx_features_labels[:, 1:-1], dtype=np.float32) labels = encode_onehot(idx_features_labels[:, -1]) # build graph idx = np.array(idx_features_labels[:, 0], dtype=np.int32) idx_map = {j: i for i, j in enumerate(idx)} edges_unordered = np.genfromtxt("{}{}.cites".format(path, dataset), dtype=np.int32) edges = np.array(list(map(idx_map.get, edges_unordered.flatten())), dtype=np.int32).reshape(edges_unordered.shape) adj = sp.coo_matrix((np.ones(edges.shape[0]), (edges[:, 0], edges[:, 1])), shape=(labels.shape[0], labels.shape[0]), dtype=np.float32) # build symmetric adjacency matrix adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj) features = normalize_features(features) adj = normalize_adj(adj + sp.eye(adj.shape[0])) idx_train = range(140) idx_val = range(200, 500) idx_test = range(500, 1500) adj = torch.FloatTensor(np.array(adj.todense())) features = torch.FloatTensor(np.array(features.todense())) labels = torch.LongTensor(np.where(labels)[1]) idx_train = torch.LongTensor(idx_train) idx_val = torch.LongTensor(idx_val) idx_test = torch.LongTensor(idx_test) return adj, features, labels, idx_train, idx_val, idx_testdef normalize_adj(mx): """Row-normalize sparse matrix""" rowsum = np.array(mx.sum(1)) r_inv_sqrt = np.power(rowsum, -0.5).flatten() r_inv_sqrt[np.isinf(r_inv_sqrt)] = 0. r_mat_inv_sqrt = sp.diags(r_inv_sqrt) return mx.dot(r_mat_inv_sqrt).transpose().dot(r_mat_inv_sqrt)def normalize_features(mx): """Row-normalize sparse matrix""" rowsum = np.array(mx.sum(1)) r_inv = np.power(rowsum, -1).flatten() r_inv[np.isinf(r_inv)] = 0. r_mat_inv = sp.diags(r_inv) mx = r_mat_inv.dot(mx) return mxdef accuracy(output, labels): preds = output.max(1)[1].type_as(labels) correct = preds.eq(labels).double() correct = correct.sum() return correct / len(labels)

models.py

import torchimport torch.nn as nnimport torch.nn.functional as Ffrom layers import GraphAttentionLayer, SpGraphAttentionLayerclass GAT(nn.Module): def __init__(self, nfeat, nhid, nclass, dropout, alpha, nheads): """Dense version of GAT.""" super(GAT, self).__init__() self.dropout = dropout self.attentions = [GraphAttentionLayer(nfeat, nhid, dropout=dropout, alpha=alpha, concat=True) for _ in range(nheads)] for i, attention in enumerate(self.attentions): self.add_module('attention_{}'.format(i), attention) self.out_att = GraphAttentionLayer(nhid * nheads, nclass, dropout=dropout, alpha=alpha, concat=False) def forward(self, x, adj): x = F.dropout(x, self.dropout, training=self.training) x = torch.cat([att(x, adj) for att in self.attentions], dim=1) x = F.dropout(x, self.dropout, training=self.training) x = F.elu(self.out_att(x, adj)) return F.log_softmax(x, dim=1)class SpGAT(nn.Module): def __init__(self, nfeat, nhid, nclass, dropout, alpha, nheads): """Sparse version of GAT.""" super(SpGAT, self).__init__() self.dropout = dropout self.attentions = [SpGraphAttentionLayer(nfeat, nhid, dropout=dropout, alpha=alpha, concat=True) for _ in range(nheads)] for i, attention in enumerate(self.attentions): self.add_module('attention_{}'.format(i), attention) self.out_att = SpGraphAttentionLayer(nhid * nheads, nclass, dropout=dropout, alpha=alpha, concat=False) def forward(self, x, adj): x = F.dropout(x, self.dropout, training=self.training) x = torch.cat([att(x, adj) for att in self.attentions], dim=1) x = F.dropout(x, self.dropout, training=self.training) x = F.elu(self.out_att(x, adj)) return F.log_softmax(x, dim=1)

layers.py

import numpy as npimport torchimport torch.nn as nnimport torch.nn.functional as Fclass GraphAttentionLayer(nn.Module): """ Simple GAT layer, similar to https://arxiv.org/abs/1710.10903 """ def __init__(self, in_features, out_features, dropout, alpha, concat=True): super(GraphAttentionLayer, self).__init__() self.dropout = dropout self.in_features = in_features self.out_features = out_features self.alpha = alpha self.concat = concat self.W = nn.Parameter(torch.empty(size=(in_features, out_features))) nn.init.xavier_uniform_(self.W.data, gain=1.414) self.a = nn.Parameter(torch.empty(size=(2*out_features, 1))) nn.init.xavier_uniform_(self.a.data, gain=1.414) self.leakyrelu = nn.LeakyReLU(self.alpha) def forward(self, h, adj): Wh = torch.mm(h, self.W) # h.shape: (N, in_features), Wh.shape: (N, out_features) e = self._prepare_attentional_mechanism_input(Wh) zero_vec = -9e15*torch.ones_like(e) attention = torch.where(adj > 0, e, zero_vec) attention = F.softmax(attention, dim=1) attention = F.dropout(attention, self.dropout, training=self.training) h_prime = torch.matmul(attention, Wh) if self.concat: return F.elu(h_prime) else: return h_prime def _prepare_attentional_mechanism_input(self, Wh): # Wh.shape (N, out_feature) # self.a.shape (2 * out_feature, 1) # Wh1&2.shape (N, 1) # e.shape (N, N) Wh1 = torch.matmul(Wh, self.a[:self.out_features, :]) Wh2 = torch.matmul(Wh, self.a[self.out_features:, :]) # broadcast add e = Wh1 + Wh2.T return self.leakyrelu(e) def __repr__(self): return self.__class__.__name__ + ' (' + str(self.in_features) + ' -> ' + str(self.out_features) + ')'class SpecialSpmmFunction(torch.autograd.Function): """Special function for only sparse region backpropataion layer.""" @staticmethod def forward(ctx, indices, values, shape, b): assert indices.requires_grad == False a = torch.sparse_coo_tensor(indices, values, shape) ctx.save_for_backward(a, b) ctx.N = shape[0] return torch.matmul(a, b) @staticmethod def backward(ctx, grad_output): a, b = ctx.saved_tensors grad_values = grad_b = None if ctx.needs_input_grad[1]: grad_a_dense = grad_output.matmul(b.t()) edge_idx = a._indices()[0, :] * ctx.N + a._indices()[1, :] grad_values = grad_a_dense.view(-1)[edge_idx] if ctx.needs_input_grad[3]: grad_b = a.t().matmul(grad_output) return None, grad_values, None, grad_bclass SpecialSpmm(nn.Module): def forward(self, indices, values, shape, b): return SpecialSpmmFunction.apply(indices, values, shape, b)class SpGraphAttentionLayer(nn.Module): """ Sparse version GAT layer, similar to https://arxiv.org/abs/1710.10903 """ def __init__(self, in_features, out_features, dropout, alpha, concat=True): super(SpGraphAttentionLayer, self).__init__() self.in_features = in_features self.out_features = out_features self.alpha = alpha self.concat = concat self.W = nn.Parameter(torch.zeros(size=(in_features, out_features))) nn.init.xavier_normal_(self.W.data, gain=1.414) self.a = nn.Parameter(torch.zeros(size=(1, 2*out_features))) nn.init.xavier_normal_(self.a.data, gain=1.414) self.dropout = nn.Dropout(dropout) self.leakyrelu = nn.LeakyReLU(self.alpha) self.special_spmm = SpecialSpmm() def forward(self, input, adj): dv = 'cuda' if input.is_cuda else 'cpu' N = input.size()[0] edge = adj.nonzero().t() h = torch.mm(input, self.W) # h: N x out assert not torch.isnan(h).any() # Self-attention on the nodes - Shared attention mechanism edge_h = torch.cat((h[edge[0, :], :], h[edge[1, :], :]), dim=1).t() # edge: 2*D x E edge_e = torch.exp(-self.leakyrelu(self.a.mm(edge_h).squeeze())) assert not torch.isnan(edge_e).any() # edge_e: E e_rowsum = self.special_spmm(edge, edge_e, torch.Size([N, N]), torch.ones(size=(N,1), device=dv)) # e_rowsum: N x 1 edge_e = self.dropout(edge_e) # edge_e: E h_prime = self.special_spmm(edge, edge_e, torch.Size([N, N]), h) assert not torch.isnan(h_prime).any() # h_prime: N x out h_prime = h_prime.div(e_rowsum) # h_prime: N x out assert not torch.isnan(h_prime).any() if self.concat: # if this layer is not last layer, return F.elu(h_prime) else: # if this layer is last layer, return h_prime def __repr__(self): return self.__class__.__name__ + ' (' \ + str(self.in_features) + ' -> '\ + str(self.out_features) + ')'

train.py

from __future__ import divisionfrom __future__ import print_functionimport osimport globimport timeimport randomimport argparseimport numpy as npimport torchimport torch.nn as nnimport torch.nn.functional as Fimport torch.optim as optimfrom torch.autograd import Variablefrom utils import load_data, accuracyfrom models import GAT, SpGATimport matplotlib.pyplot as plt# Training settingsparser = argparse.ArgumentParser()parser.add_argument('--no-cuda', action='store_true', default=False, help='Disables CUDA training.')parser.add_argument('--fastmode', action='store_true', default=False, help='Validate during training pass.')parser.add_argument('--sparse', action='store_true', default=False, help='GAT with sparse version or not.')parser.add_argument('--seed', type=int, default=72, help='Random seed.')parser.add_argument('--epochs', type=int, default=10000, help='Number of epochs to train.')parser.add_argument('--lr', type=float, default=0.005, help='Initial learning rate.')parser.add_argument('--weight_decay', type=float, default=5e-4, help='Weight decay (L2 loss on parameters).')parser.add_argument('--hidden', type=int, default=8, help='Number of hidden units.')parser.add_argument('--nb_heads', type=int, default=8, help='Number of head attentions.')parser.add_argument('--dropout', type=float, default=0.6, help='Dropout rate (1 - keep probability).')parser.add_argument('--alpha', type=float, default=0.2, help='Alpha for the leaky_relu.')parser.add_argument('--patience', type=int, default=100, help='Patience')args = parser.parse_args()args.cuda = not args.no_cuda and torch.cuda.is_available()random.seed(args.seed)np.random.seed(args.seed)torch.manual_seed(args.seed)if args.cuda: torch.cuda.manual_seed(args.seed)# Load dataadj, features, labels, idx_train, idx_val, idx_test = load_data()# Model and optimizerif args.sparse: model = SpGAT(nfeat=features.shape[1], nhid=args.hidden, nclass=int(labels.max()) + 1, dropout=args.dropout, nheads=args.nb_heads, alpha=args.alpha)else: model = GAT(nfeat=features.shape[1], nhid=args.hidden, nclass=int(labels.max()) + 1, dropout=args.dropout, nheads=args.nb_heads, alpha=args.alpha)optimizer = optim.Adam(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)if args.cuda: model.cuda() features = features.cuda() adj = adj.cuda() labels = labels.cuda() idx_train = idx_train.cuda() idx_val = idx_val.cuda() idx_test = idx_test.cuda()features, adj, labels = Variable(features), Variable(adj), Variable(labels)def train(epoch): t = time.time() model.train() optimizer.zero_grad() output = model(features, adj) loss_train = F.nll_loss(output[idx_train], labels[idx_train]) acc_train = accuracy(output[idx_train], labels[idx_train]) loss_train.backward() optimizer.step() if not args.fastmode: # Evaluate validation set performance separately, # deactivates dropout during validation run. model.eval() output = model(features, adj) loss_val = F.nll_loss(output[idx_val], labels[idx_val]) acc_val = accuracy(output[idx_val], labels[idx_val]) print('Epoch: {:04d}'.format(epoch+1), 'loss_train: {:.4f}'.format(loss_train.data.item()), 'acc_train: {:.4f}'.format(acc_train.data.item()), 'loss_val: {:.4f}'.format(loss_val.data.item()), 'acc_val: {:.4f}'.format(acc_val.data.item()), 'time: {:.4f}s'.format(time.time() - t)) return loss_val.data.item(),acc_val.data.item()def compute_test(): model.eval() output = model(features, adj) loss_test = F.nll_loss(output[idx_test], labels[idx_test]) acc_test = accuracy(output[idx_test], labels[idx_test]) print("Test set results:", "loss= {:.4f}".format(loss_test.data.item()), "accuracy= {:.4f}".format(acc_test.data.item()))# Train modelt_total = time.time()loss_values = []acc_values = []bad_counter = 0best = args.epochs + 1best_epoch = 0for epoch in range(args.epochs): loss,acc = train(epoch) loss_values.append(loss) acc_values.append(acc) torch.save(model.state_dict(), '{}.pkl'.format(epoch)) if loss_values[-1] < best: best = loss_values[-1] best_epoch = epoch bad_counter = 0 else: bad_counter += 1 if bad_counter == args.patience: break files = glob.glob('*.pkl') for file in files: epoch_nb = int(file.split('.')[0]) if epoch_nb < best_epoch: os.remove(file)# print(loss_values)# print(acc_values)#绘制loss和acc曲线def plot_loss_with_acc(loss_history, val_acc_history): fig = plt.figure() ax1 = fig.add_subplot(111) ax1.plot(range(len(loss_history)), loss_history, c=np.array([255, 71, 90]) / 255.) plt.ylabel('Loss') ax2 = fig.add_subplot(111, sharex=ax1, frameon=False) ax2.plot(range(len(val_acc_history)), val_acc_history, c=np.array([79, 179, 255]) / 255.) ax2.yaxis.tick_right() ax2.yaxis.set_label_position("right") plt.ylabel('ValAcc') plt.xlabel('Epoch') plt.title('Training Loss & Validation Accuracy') # plt.show()#绘制loss和acc曲线plot_loss_with_acc(loss_values,acc_values)files = glob.glob('*.pkl')for file in files: epoch_nb = int(file.split('.')[0]) if epoch_nb > best_epoch: os.remove(file)print("Optimization Finished!")print("Total time elapsed: {:.4f}s".format(time.time() - t_total))# Restore best modelprint('Loading {}th epoch'.format(best_epoch))model.load_state_dict(torch.load('{}.pkl'.format(best_epoch)))# Testingcompute_test()# 绘制测试数据的TSNE降维图output = model(features, adj)output = output.cpu()output = output[idx_test].detach().numpy()# print(output)# print(output.shape)from sklearn.manifold import TSNEtsne = TSNE()out = tsne.fit_transform(output)fig = plt.figure()labels_test = labels[idx_test].detach().cpu().numpy()for i in range(7): indices = labels_test == i # print(indices) x, y = out[indices].T plt.scatter(x, y, label=str(i))plt.legend()plt.show()

8.4 模型结果

(1)模型训练曲线图:

Test set results: loss= 0.6537 accuracy= 0.8470 结果比没有加Attention的GCN要好

(2)聚类效果如下图:

参考文献:

1. ICLR 2023 Open Review投稿文章一览,投稿量暴涨46%,「Diffusion」、「Mask」关键词成为新热点 - 知乎

2. GNN的理解与研究_江南綿雨的博客-CSDN博客_gnn

3. 跳出公式,看清全局,图神经网络(GCN)原理详解_kisssfish的博客-CSDN博客_gcn原理

4. pytorch框架下—GCN代码详细解读_MelvinDong的博客-CSDN博客_gcn代码解读

5. GNN学习笔记(四):Cora数据集读取与分析_花锄的博客-CSDN博客_cora数据

6. https://baijiahao.baidu.com/s?id=1671028964544884749 (GAT 图注意力网络)

本文链接地址:https://www.jiuchutong.com/zhishi/289586.html 转载请保留说明!

上一篇:Salzburg with Salzach river, Austria (© MacEaton/Alamy)

下一篇:攀牙湾安达曼海的红树林,泰国 (© Ratnakorn Piyasirisorost/Getty Images)(缅甸安达曼海)

  • 京东白条怎么提升额度(京东白条怎么提前还款全部金额)

    京东白条怎么提升额度(京东白条怎么提前还款全部金额)

  • 美颜相机可以把字去除吗(美颜相机可以把照片改为漫画风格吗)

    美颜相机可以把字去除吗(美颜相机可以把照片改为漫画风格吗)

  • 微信怎么更改密码(微信怎么更改密码支付)

    微信怎么更改密码(微信怎么更改密码支付)

  • 支付宝可以视频聊天吗(支付宝可以视频扫脸吗)

    支付宝可以视频聊天吗(支付宝可以视频扫脸吗)

  • 影响网速的因素有哪些(影响电脑网络的速度的因素)

    影响网速的因素有哪些(影响电脑网络的速度的因素)

  • qq待办提醒是什么意思(qq消息待办提醒什么意思)

    qq待办提醒是什么意思(qq消息待办提醒什么意思)

  • 苹果X什么时候上市的(苹果x什么时候停产的)

    苹果X什么时候上市的(苹果x什么时候停产的)

  • 华为手机换电池对手机有影响吗(华为手机换电池会丢失数据吗)

    华为手机换电池对手机有影响吗(华为手机换电池会丢失数据吗)

  • iphonex可以更新13.4.1吗(iPhonex可以更新16.1.1)

    iphonex可以更新13.4.1吗(iPhonex可以更新16.1.1)

  • K30充满电有提示吗(k30pro充满电还是显示在充电)

    K30充满电有提示吗(k30pro充满电还是显示在充电)

  • ipad有没有录音功能(ipad有没有录音转文字功能)

    ipad有没有录音功能(ipad有没有录音转文字功能)

  • chmul00是什么版本(chmcl00是什么型号)

    chmul00是什么版本(chmcl00是什么型号)

  • cpu后面带h是什么意思(cpu后面的h是什么)

    cpu后面带h是什么意思(cpu后面的h是什么)

  • wps表格查找快捷键(wps表格查找快捷键ctrl加什么)

    wps表格查找快捷键(wps表格查找快捷键ctrl加什么)

  • 收藏视频怎么发快手上(收藏视频怎么发朋友圈微信)

    收藏视频怎么发快手上(收藏视频怎么发朋友圈微信)

  • itsohoo是什么品牌(it shoes什么品牌)

    itsohoo是什么品牌(it shoes什么品牌)

  • 小米9se与小米cc9对比(小米9se与小米8对比)

    小米9se与小米cc9对比(小米9se与小米8对比)

  • 华为p30pro尺寸长宽高(华为p30 pro长宽)

    华为p30pro尺寸长宽高(华为p30 pro长宽)

  • 微信位置共享可以改变自己的位置吗(微信位置共享可以说话吗)

    微信位置共享可以改变自己的位置吗(微信位置共享可以说话吗)

  • 苹果手机怎么刷机详细教程(苹果手机怎么刷机)

    苹果手机怎么刷机详细教程(苹果手机怎么刷机)

  • win10待机唤醒后白屏怎么办(Win10待机唤醒后白屏怎么办)

    win10待机唤醒后白屏怎么办(Win10待机唤醒后白屏怎么办)

  • Linux使用xinetd服务的管理方法案例详解(如何使用linux服务器)

    Linux使用xinetd服务的管理方法案例详解(如何使用linux服务器)

  • 福克兰群岛上的南跳岩企鹅 (© Heike Odermatt/Minden Pictures)(福克兰群岛属于哪国)

    福克兰群岛上的南跳岩企鹅 (© Heike Odermatt/Minden Pictures)(福克兰群岛属于哪国)

  • 文化传媒公司的税种及税率
  • 房产税的计税依据是含税还是不含税
  • 分成收入计入什么科目
  • 企业购入免税农产品
  • 法人名下的车辆费用如何进公司帐
  • 资产减值损失属于什么科目借方增加还是减少
  • 年度汇算清缴需要注意的项目
  • 职工伤残补助金被单位扣留违反什么法
  • 收到外商投入资金
  • 公司购买网络交换机入什么会计科目
  • 销售不同税率的货物会计处理
  • 补交以前年度的社保账务处理
  • 公司没有给员工买社保怎么赔偿
  • 过路费抵扣税率是多少
  • 村集体收入所得税率多少
  • 办公室零食知乎
  • 小规模差额征税季度不超过30万的税收优惠
  • 代购商品委托书怎么写
  • 固定资产明细账有哪些
  • 企业为员工支付房租交个税吗
  • 固定资产报废相关规定
  • 怎么进入登录
  • 美元汇款怎么汇
  • 预提开发成本超过10%部分计算的销售成本转回
  • 失控发票是什么
  • s24evmon.exe - s24evmon是什么进程 有什么用
  • 购货方收到红字发票要认证吗
  • 解决http请求下无法开启麦克风问题
  • linux安装linux
  • 当月认证抵扣的进项税发票一定要入帐做成本吗?
  • 存货按实际成本计价是指每种存货的收发结存
  • 生育津贴差额账务处理
  • 网络通信的整个流程
  • 哈德良长城和中国长城的区别
  • vue中的use
  • r在数据分析中表示什么意思
  • css选择器nth
  • 普通发票收款人填管理员可以吗
  • 企业的应交税金一般通过什么科目核算
  • 供应商发票多开了3毛钱能做到财务费吗
  • python的socket
  • 企业开户行信息是什么意思
  • 小规模纳税人能开专票吗2023
  • 固定资产减值准备借贷方向
  • 地税发票丢失应如何处理
  • 个人转租房屋需要交房产税吗
  • 银行代发的工资公司要求返还然后发一半
  • 长期股权投资的账务处理
  • 产品包装设计费属什么费用
  • 信用卡什么情况下会降额度
  • 进项税转出的金额是什么
  • 投资收益的账务怎么处理
  • 应交个人所得税借方余额表示什么
  • 什么是中型企业
  • 工程尾款要计入什么科目
  • 所附原始凭证的作用是
  • win8旗舰版升级win10
  • win8系统运行慢怎么办
  • windowsserver2003ftp服务器怎么搭建
  • bios更改
  • xp桌面字体有阴影怎么去掉
  • xp系统蓝屏解决
  • mmc.exe是什么
  • centos基本环境
  • w7打穿越火线
  • win10 运行
  • linux 删除 用户
  • cocos2dx 4.0
  • 浅析js动态创建方法
  • linux管道定义
  • linux怎么使用c语言
  • node.js [superAgent] 请求使用示例
  • eclipse创建android项目
  • android view动画
  • 车库契税怎么收费
  • 吉林省地方税务局
  • 深圳国税局官网登录
  • 请问一般纳税人的业务协调税率是多少
  • 工会经费怎么缴纳不了
  • 党员e先锋中的支部云课堂在哪
  • 免责声明:网站部分图片文字素材来源于网络,如有侵权,请及时告知,我们会第一时间删除,谢谢! 邮箱:opceo@qq.com

    鄂ICP备2023003026号