安装工具
不得不说本人是把能踩的坑都踩了一遍。
有啥坑?
首先是CUDA版本不够,去更新CUDA,按照官网教程就彳亍,没什么好说的,就8记了,注意C盘空间要留大。
我第一次安装的时候就是C盘空间不够,后来只能重装系统清理,把D盘空间给C盘分了200个G,顺便清了好多垃圾 = =
如果直接去搜怎么安装PyTorch,大家会告诉你去[PyTorch官网]下载PyTorch,选好自己的版本,用python pip下,我得到的命令是
pip3 install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu116
,这样装的PyTorch版本是1.13.0
下好之后,装pytorch-geometric,按照教程换好命令之后死活下不下来,最开始是pip报错:error: subprocess-exited-with-error
。
根据报错加一个参数--use-pep517
,然后又报错HTTP 403,搜了一下发现是没有权限访问。
我干脆打开pytorch-geometric,进wheel,发现里面根!本!没!有1.13.0版本的!!!
之前装ruby也是这个问题 = =
安装PyTorch
去PyTorch的github找怎么安装旧版本PyTorch(我直接手动在python包里删新版本了,懒得找卸载命令),发现是在https://pytorch.org/get-started/previous-versions/,找到Windows CUDA11.6的安装命令是conda install pytorch==1.12.1 torchvision==0.13.1 torchaudio==0.12.1 cudatoolkit=11.6 -c pytorch -c conda-forge
python -c "import torch; print(torch.__version__)"
测一下,是1.12.1没错,泪目
安装pytorch-geometric
1 2 3 4 5
| pip install torch-scatter -f https://pytorch-geometric.com/whl/torch-1.12.1+cu116.html pip install torch-sparse -f https://pytorch-geometric.com/whl/torch-1.12.1+cu116.html pip install torch-cluster -f https://pytorch-geometric.com/whl/torch-1.12.1+cu116.html pip install torch-spline-conv -f https://pytorch-geometric.com/whl/torch-1.12.1+cu116.html pip install torch-geometric
|
终于success了,泪目
测试一下
1 2 3 4 5 6 7
| import torch from torch_geometric.data import Data edge_index = torch.tensor([[0, 1, 1, 2], [1, 0, 2, 1]], dtype=torch.long) x = torch.tensor([[-1], [0], [1]], dtype=torch.float) data = Data(x=x, edge_index=edge_index) print(data)
|
输出Data(edge_index=[2, 4], x=[3, 1])
bingo~
快速上手
官方教程
图数据处理
图被用来训练一对对象(点)之间的关系(边)。PyG中一个单独的图是用实例torch_geometric.data.Data
描述的,它默认有以下属性:
data.x
:节点特征矩阵,形如[num_nodes, num_node_features]
data.edge_index
:图的连接,COO格式,形如[2, num_edges]
,类型为torch.long
data.edge_attr
:边缘特征矩阵,形如[num_edges, num_edge_features]
data.y
:训练目标,形状任意,节点级或图形级均可。
data.pos
:形如[num_nodes, num_dimensions]
的节点位置矩阵。
COO
这是一种存储稀疏矩阵的方式。将图写成邻接矩阵的形式后,用三组数据来存储图,这三组数据分别为行、列、权,此处只需要两组数据,即行、列。
GCN
model.py
双层GCN公式:
图卷积:forward计算了一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class GraphConvolution(Module): 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)
def forward(self, input, adj): support = torch.mm(adj, input) output = torch.mm(support, self.weight) if self.bias is not None: return output +self.bias else: return output
def __repr__(self): return self.__class__.__name__+"("+str(self.in_features)+"->"+str(self.out_features)+")"
|
双层GCN:下面的代码计算,其中是第一层图卷积(gc1),把它当成第二层的输入进行第二次图卷积,log_softmax之后得到输出。
1 2 3 4 5 6 7 8 9 10 11 12
| class GCN(nn.Module): def __init__(self, nfeat, nhid, nclass, dropout): super(GCN, self).__init__() self.gc1 = GraphConvolution(nfeat, nhid) self.gc2 = GraphConvolution(nhid, nclass) self.dropout = dropout
def forward(self, x, adj): 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)
|
load_data.py
one-hot编码
什么是one-hot编码
简而言之,能够将现实生活中的变量类别转为机器学习算法易于利用的矩阵形式。
1 2 3 4 5 6 7 8 9 10 11
| ''' 先将所有由字符串表示的标签数组用set保存,set的重要特征就是元素没有重复, 因此表示成set后可以直接得到所有标签的总数,随后为每个标签分配一个编号,创建一个单位矩阵, 单位矩阵的每一行对应一个one-hot向量,也就是np.identity(len(classes))[i, :], 再将每个数据对应的标签表示成的one-hot向量,类型为numpy数组 ''' def encode_onehot(labels): 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_onehot
|
加载数据
读取文件cora.content。
genfromtxt运行两个主循环:第一个循环以字符串序列转换文件的每一行。第二个循环将(按照delimiter划分行得到的字符串(默认情况下,任何连续的空格充当分隔符))每个字符串转换为适当的数据类型。
sp.csr_matrix对数据进行压缩处理。
1 2 3 4 5 6 7 8 9
| def load_data(path="./cora/", dataset="cora"): 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])
|
其中[:,1:-1]
表示取矩阵第2列到倒数第2列,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| In [1]: import numpy
In [2]: matrix = [[0, 1, 2, 3], [4, 5, 6, 7]]
In [3]: matrix = numpy.array(matrix)
In [4]: matrix[:,1] Out[4]: array([1, 5])
In [5]: matrix[:,1:-1] Out[5]: array([[1, 2], [5, 6]])
|
取矩阵第一列建立图。
由于文件中节点并非是按顺序排列的,因此建立一个编号为0-(node_size-1)的哈希表idx_map,idx_map哈希表中每一项为id: number,即节点id对应的编号为number。如
1 2 3 4 5 6
| In [6]: idx = ['a','b','c']
In [7]: idx_map = {j: i for i, j in enumerate(idx)}
In [8]: idx_map Out[8]: {'a': 0, 'b': 1, 'c': 2}
|
1 2
| idx = np.array(idx_features_labels[:, 0], dtype=np.int32) idx_map = {j:i for i, j in enumerate(idx)}
|
看不懂了
读取文件cora.cities
edges_unordered为直接从边表文件中直接读取的结果,是一个(edge_num, 2)的数组,每一行表示一条边两个端点的idx (被引用论文,引用论文)。
边的edges_unordered中存储的是端点id,要将每一项的id换成编号。
在idx_map中以idx作为键查找得到对应节点的编号,reshape成与edges_unordered形状一样的数组。
获得邻接矩阵,sp.coo_matrix((data,(row,col)),shape) 返回一个压缩后的matrix
根据coo矩阵性质,这一段的作用就是,网络有多少条边,邻接矩阵就有多少个1,
所以先创建一个长度为edge_num的全1数组,每个1的填充位置就是一条边中两个端点的编号,
即edges[:, 0], edges[:, 1],矩阵的形状为(node_size, node_size)
1 2 3
| 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)
|
构建对称邻接矩阵(把构建的有向图邻接矩阵转换为无向图邻接矩阵)。
对feature进行规范化。
normalize():
adj + sp.eye(adj.shape[0])
:
1 2 3
| 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]))
|
分别构建训练集、验证集、测试集的范围
1 2 3
| idx_train = range(140) idx_val = range(200, 500) idx_test = range(500, 1500)
|
features.todense()
:将稀疏矩阵转为稠密矩阵(即将压缩的矩阵转为未压缩的矩阵),类型是matrix
1 2
| adj = torch.FloatTensor(np.array(adj.todense())) features = torch.FloatTensor(np.array(features.todense()))
|
np.where(labels)[1]
取labels中非零元素的列索引构成一维元组,此时labels变为一维的LongTensor,每个元素为对应<class_label>
在onehot后的索引。
邻接矩阵转为tensor处理。
1 2 3 4 5
| 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_test
|
将矩阵进行行规范化,即每个元素除以这一行的和
1 2 3 4 5 6 7 8
| def normalize_features(mx): 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 mx
|
1 2 3 4 5 6
| def sparse_mx_to_torch_sparse_tensor(sparse_mx): 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 = sparse_mx.shape return torch.sparse.FloatTensor(indices, values, shape)
|
后面看不动了