位置: IT常识 - 正文

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

编辑:rootadmin
图卷积神经网络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)(缅甸安达曼海)

  • 新手电商必须掌握的八项技巧 新手电商必备八项攻略(新手做电商需要多少钱)

    新手电商必须掌握的八项技巧 新手电商必备八项攻略(新手做电商需要多少钱)

  • word形状填充在哪里(word形状填充在哪)

    word形状填充在哪里(word形状填充在哪)

  • 荣耀play4tpro支持呼吸灯吗(荣耀play4tpro支持多少瓦快充)

    荣耀play4tpro支持呼吸灯吗(荣耀play4tpro支持多少瓦快充)

  • win10系统特洛伊木马杀不掉(w10总是提示有特洛伊)

    win10系统特洛伊木马杀不掉(w10总是提示有特洛伊)

  • 微信响三声对方忙线中(微信响了三声对方忙是挂掉了吗?)

    微信响三声对方忙线中(微信响了三声对方忙是挂掉了吗?)

  • 没开通花呗收款可以收到顾客的花呗付款吗(没开通花呗收款也能收花呗)

    没开通花呗收款可以收到顾客的花呗付款吗(没开通花呗收款也能收花呗)

  • 快手保存图片作者能看见吗(快手保存图片作者)

    快手保存图片作者能看见吗(快手保存图片作者)

  • 苹果接电话可以录音吗(苹果接电话可以解锁吗)

    苹果接电话可以录音吗(苹果接电话可以解锁吗)

  • 抖音bgm怎么找(抖音bgm怎么找原曲)

    抖音bgm怎么找(抖音bgm怎么找原曲)

  • 荣耀30s支持内存扩展吗(荣耀30s支持内存卡扩展吗)

    荣耀30s支持内存扩展吗(荣耀30s支持内存卡扩展吗)

  • 探探怎么没有直播权限(探探为什么没有直播入口)

    探探怎么没有直播权限(探探为什么没有直播入口)

  • 苹果手机看电影一会就黑屏(苹果手机看电影不能全屏怎么回事)

    苹果手机看电影一会就黑屏(苹果手机看电影不能全屏怎么回事)

  • dns地址一般填什么(dns写啥)

    dns地址一般填什么(dns写啥)

  • 限制网速限制多少合适(网络限速设置多少合适)

    限制网速限制多少合适(网络限速设置多少合适)

  • 手机进水无限重启原因(手机进水无限重启几天能好)

    手机进水无限重启原因(手机进水无限重启几天能好)

  • 安卓8g跟12g差别(安卓8g和12g差别)

    安卓8g跟12g差别(安卓8g和12g差别)

  • 位图放大后会失真吗(位图放大后图像不会失真,和分辨率无关)

    位图放大后会失真吗(位图放大后图像不会失真,和分辨率无关)

  • qq名片不小心点赞怎么取消(qq名片不小心点赞别人怎么取消)

    qq名片不小心点赞怎么取消(qq名片不小心点赞别人怎么取消)

  • 手机qq怎么发文件夹(手机qq怎么发文件给别的邮箱)

    手机qq怎么发文件夹(手机qq怎么发文件给别的邮箱)

  • 微信群活动账单是什么情况(微信群活动账单怎么弄)

    微信群活动账单是什么情况(微信群活动账单怎么弄)

  • 淘宝投诉卖家撤销后可以再次投诉吗(淘宝投诉卖家撤销投诉不能再申请吗)

    淘宝投诉卖家撤销后可以再次投诉吗(淘宝投诉卖家撤销投诉不能再申请吗)

  • w7电脑蓝屏0x0000003b(w7电脑蓝屏0x0000006b)

    w7电脑蓝屏0x0000003b(w7电脑蓝屏0x0000006b)

  • ipadair3充电时间(ipad air4充电时间)

    ipadair3充电时间(ipad air4充电时间)

  • win10开机一直转圈黑屏(win10开机一直转圈圈很久)

    win10开机一直转圈黑屏(win10开机一直转圈圈很久)

  • 手机边框缝隙怎么修复(手机边框缝隙大有什么影响)

    手机边框缝隙怎么修复(手机边框缝隙大有什么影响)

  • 小规模纳税人印花税最新政策2023
  • 企业名称税号
  • 新建厂房装修费是否计入固定资产
  • 什么是差额税金
  • 个人名字的发票专用章
  • 免税苗木普通发票
  • 出资款在现金流中怎么体现
  • 以前年度损益调整
  • 个体工商户报税一年几次
  • 网上报税超过了时间还能报吗
  • 小规模纳税人减征增值税
  • 防雨棚设计规范
  • 企业纳税成本管理方案
  • 企业安全防护措施有哪些
  • 开立基本账户所需资料
  • 制造企业售后服务方案
  • 第三方代缴社保能查到原单位吗
  • 其他业务收入的核算内容
  • 固定资产折旧成本费用科目
  • 进口货物的价格国内销售需要考虑的税
  • 发生广告费用会计分录
  • 增值税的计税公式为
  • 固定成本与变动成本的区别
  • 出租土地收入记什么科目
  • 工会经费的开支必须取得发票么
  • bios设置密码有什么用
  • 外观设计专利费减
  • win10双系统修改名称
  • 微信公众号开发php源码
  • linux中的
  • pc程序是什么
  • lnmgr.exe是什么
  • fp是什么文件
  • php是基于
  • 公司转让债务债权协议
  • 软件开发企业进项税额抵扣
  • uniapp怎么开启路由拦截
  • uniapp跨域解决方案
  • 测试工程师有前途么
  • c#怎么使用
  • 缴纳医疗保险费会计分录
  • gunzip命令压缩
  • python填写表格
  • 极速开票怎么打不开
  • 公务交通通讯补贴
  • pythonsorted函数的作用
  • 固定资产可收回金额怎么算
  • 资产负债表日后事项包括哪些
  • mongodb配置文件详解
  • 合同权益如何进行转让
  • SQL高级应用之同服务器上复制表到另一数据库中并实现去重复
  • 以前年度是什么意思
  • 视同销售是怎么回事?
  • 结算备付金会计分录
  • 房贷利息抵个税怎么申请
  • 融资性无形资产的入账价值
  • 房产契税新政策2023
  • 事业单位非税收入帐务处理
  • 公司报销发票需要查验真假吗
  • 员工体检的发票在哪里开
  • 金税盘的维护费每年怎么抵扣
  • 私营企业固定资产法律制度
  • 电脑更新windows11后开机一黑屏
  • ubuntu和windows文件互传
  • cocos2d::DrawPrimitives和DrawNode分别实现画板功能
  • python如何搭建环境
  • 为什么你应该使用手机
  • js数组菜鸟教程
  • 动态修改ip
  • bootstrap基础
  • chrome excel
  • 如何用js实现一个简单的计算器
  • 全面解析日本失去的十年
  • js发送请求的几种方式
  • javascript修改
  • 简述javascript
  • javascript define的用法
  • js如何判断输入输出
  • 北京通州国税局进面分数2022
  • 江苏单位医保如何查询
  • 免责声明:网站部分图片文字素材来源于网络,如有侵权,请及时告知,我们会第一时间删除,谢谢! 邮箱:opceo@qq.com

    鄂ICP备2023003026号

    网站地图: 企业信息 工商信息 财税知识 网络常识 编程技术

    友情链接: 武汉网站建设