结构与关系:图神经网络及其在 Pytorch 中的实现
原文towardsdatascience.com/structure-and-relationships-graph-neural-networks-and-a-pytorch-implementation-c9d83b71c041?sourcecollection_archive---------1-----------------------#2024-03-05了解图神经网络的数学背景及其在 pytorch 中回归问题的实现https://medium.com/ns650?sourcepost_page---byline--c9d83b71c041--------------------------------https://towardsdatascience.com/?sourcepost_page---byline--c9d83b71c041-------------------------------- Najib Sharifi 博士·发表于 Towards Data Science ·12 分钟阅读·2024 年 3 月 5 日–简介相互连接的图形数据无处不在从分子结构到社交网络以及城市的设计结构。图神经网络GNN正逐渐成为一种强大的方法用于建模和学习此类数据的空间和图形结构。它已被应用于蛋白质结构及其他分子应用如药物发现并且还被用于建模如社交网络等系统。最近标准的 GNN 与其他机器学习模型的思想相结合开发出了一些令人兴奋的创新应用。其中一项发展是将 GNN 与序列模型结合 —— 空间-时间图神经网络Spatio-Temporal GNN它能够捕捉数据的时间和空间因此得名依赖性仅此一点就可以应用于行业/研究中的许多挑战/问题。尽管图神经网络GNN取得了令人兴奋的进展但相关资源仍然很少这使得它对许多人来说难以接触。在这篇简短的文章中我想提供一个图神经网络的简要介绍涵盖数学描述以及使用 pytorch 库的回归问题。通过揭示 GNN 背后的原理我们能够更深入地理解其能力和应用。图神经网络的数学描述图 G 可以定义为 G (V, E)其中 V 是节点集合E 是它们之间的边。图通常通过邻接矩阵 A 来表示表示节点之间边的存在与否即 aij 的值为 1 表示节点 i 和节点 j 之间有边连接否则为 0。如果图有 n 个节点则 A 的维度为 (n × n)。邻接矩阵在图 1中演示。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/d1cd932311a015e28e70b76c55cf5f74.png图 1. 三个不同图的邻接矩阵每个节点以及边但为了简化我们稍后会回到边将有一组特征例如如果节点是一个人特征可能包括年龄、性别、身高、职业等。如果每个节点有 f 个特征则特征矩阵 X 为 (n × f)。在某些问题中每个节点可能还具有目标标签该标签可能是一组分类标签或数值如图 2所示。单节点计算为了学习任何节点与其邻居之间的相互依赖关系我们需要考虑其邻居的特征。这使得 GNN 能够通过图来学习数据的结构表示。考虑一个节点 j 及其 Nj 个邻居GNN 会转换每个邻居的特征聚合这些特征然后更新节点 i 的特征空间。以下是这些步骤的描述。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/0858dd5d95be8a673a8737ac19200814.png图 2. 一个节点j的示意图具有特征 xj 和标签 yj以及邻居节点i、2、3每个节点都有自己的特征嵌入和相应的标签。邻居特征转换可以通过多种方式进行例如通过 MLP 网络或线性变换例如https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/db48d782c7e6519c78c2b446c9387fb5.png其中 w 和 b 表示变换的权重和偏置。信息聚合即来自每个邻居节点的信息会被聚合https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/e43f038327837089cafa9a3abafc5bf4.png聚合步骤的性质可以有多种不同的方法例如求和、平均、最小/最大池化和拼接https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/f59a108b5b916e5cbaef95e8541114e9.png在聚合步骤之后最后一步是更新节点 jhttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/013599689b3480c1ca7ea900b40e5cbf.png更新可以通过 MLP 使用拼接的节点特征和邻居信息聚合mj来完成或者我们可以使用线性变换即https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/3fa2a673ce468e1b1ea4a29c32af8aff.png其中 U 是一个可学习的权重矩阵它通过非线性激活函数此处使用 ReLU将原始节点特征xj与聚合的邻居特征mj结合起来。这就是在单层中更新单个节点的过程相同的过程应用于图中的所有其他节点数学上这可以通过邻接矩阵来表示。图级计算对于一个包含 n 个节点的图每个节点有f个特征我们可以将所有特征连接成一个矩阵https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/9498e7314928b00500195a4c89693cb8.png因此邻居特征变换和聚合步骤可以写作https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/630123572a8f0bc225a7721705bb0c4b.png其中 I 是单位矩阵这有助于包括每个节点的自身特征否则我们只考虑来自节点 j 邻居的变换特征而不考虑其自身特征。最后一步是根据连接数对每个节点进行归一化即对于具有 Nj 个连接的节点 j特征变换可以这样进行https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/7c08e50dcb433ee0320f2e71594840e3.png上面的方程可以调整为https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/f92e5051e1e2e2d6438067a44dff65a1.png其中 D 是度矩阵是每个节点连接数的对角矩阵。然而更常见的是这一步归一化是通过以下方式进行的https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/39326a6cee6a5013846a077ad197452b.png这就是图卷积网络GCN方法它使得 GNN 能够学习节点之间的结构和关系。然而GCN 的一个问题是邻居特征变换的权重向量在所有邻居中是共享的即所有邻居被认为是相等的但通常并非如此因此不能很好地代表真实系统。为了解决这个问题可以使用图注意力网络GAT来计算邻居特征对目标节点的重要性从而允许不同的邻居根据它们的相关性以不同的方式贡献于目标节点特征的更新。注意力系数是通过一个可学习的矩阵来确定的如下所示https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/d2f4ddfbeb15c5f5c453135d44fdda98.png其中 W 是共享的可学习特征线性变换Wa 是一个可学习的权重向量eij 是原始的注意力分数表示节点 i 的特征对节点 j 的重要性。注意力分数使用 SoftMax 函数进行归一化https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/6af7ddab8bee47d14788676c8a01cf14.png现在可以使用注意力系数来计算特征聚合步骤https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/8a40d22ae79e2116e75022bfdf4fc1ad.png这就是单层的情况我们可以构建多个层来增加模型的复杂性这在图 3中进行了演示。增加层数将允许模型学习更多的全局特征并捕捉更复杂的关系但也很容易导致过拟合因此应始终使用正则化技术来防止过拟合。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/781e51a407ec5c36ad5efd87597a4996.png图 3. 一个多层 GNN 模型的示意图最后一旦从网络中获得所有节点的最终特征向量就可以形成特征矩阵 Hhttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/849a73048c3e915b58d2294221aa16f7.png该特征矩阵可以用于执行多种任务例如节点或图的分类。这也将是 GCN/GAT 数学描述部分的结束。GCN 回归示例让我们实现一个回归示例目标是训练一个网络来预测一个节点的值前提是已知其他所有节点的值即每个节点有一个单一的特征这是一个标量值。本示例的目的是利用图中编码的固有关系信息准确预测每个节点的数值。需要注意的是我们输入所有节点的数值除了目标节点目标节点的值用 0 进行掩码然后预测目标节点的值。对于每个数据点我们对所有节点重复这一过程。也许这看起来像是一个奇怪的任务但让我们看看是否能够根据其他节点的值预测任何节点的预期值。使用的数据是来自工业的传感器系列的相应仿真数据下面示例中的图结构是基于实际过程结构的。我在代码中提供了注释以便更容易理解。你可以在这里找到数据集的副本注意这是我自己的数据通过仿真生成。这段代码和训练过程远未优化但它的目的是展示 GNN 的实现并让人对其工作原理有直观的理解。我目前的实现方式中有一个问题除了用于学习目的外这种方法绝对不应该继续使用那就是将节点特征值进行掩码并从邻居节点的特征中预测它。当前你需要对每个节点进行循环效率不高一种更好的方法是在聚合步骤中停止模型将自身特征包含进来这样你就不需要一个节点一个节点地处理。但我认为通过当前的方法更容易为模型构建直观理解)数据预处理导入必要的库和从 CSV 文件中读取传感器数据。将所有数据归一化到 0 到 1 的范围内。importpandasaspdimporttorchfromtorch_geometric.dataimportData,Batchfromsklearn.preprocessingimportStandardScaler,MinMaxScalerfromsklearn.model_selectionimporttrain_test_splitimportnumpyasnpfromtorch_geometric.dataimportDataLoader# load and scale the datasetdfpd.read_csv(SensorDataSynthetic.csv).dropna()scalerMinMaxScaler()df_scaledpd.DataFrame(scaler.fit_transform(df),columnsdf.columns)使用 PyTorch 张量定义图中节点之间的连接性边缘索引——即这提供了系统的图形拓扑。nodes_order[Sensor1,Sensor2,Sensor3,Sensor4,Sensor5,Sensor6,Sensor7,Sensor8]# define the graph connectivity for the dataedgestorch.tensor([[0,1,2,2,3,3,6,2],# source nodes[1,2,3,4,5,6,2,7]# target nodes],dtypetorch.long)从 CSV 导入的数据具有表格结构但为了在 GNN 中使用必须将其转换为图形结构。数据的每一行一个观察值表示一个图。迭代每一行以创建数据的图形表示。为每个节点/传感器创建一个掩码以指示数据的存在1或缺失0从而提供处理缺失数据的灵活性。在大多数系统中可能存在没有数据的项目因此需要处理缺失数据的灵活性。将数据划分为训练集和测试集。graphs[]# iterate through each row of data to create a graph for each observation# some nodes will not have any data, not the case here but created a mask to allow us to deal with any nodes that do not have data availablefor_,rowindf_scaled.iterrows():node_features[]node_data_mask[]fornodeinnodes_order:ifnodeindf_scaled.columns:node_features.append([row[node]])node_data_mask.append(1)# mask value of to indicate present of dataelse:# missing nodes feature if necessarynode_features.append(2)node_data_mask.append(0)# data not presentnode_features_tensortorch.tensor(node_features,dtypetorch.float)node_data_mask_tensortorch.tensor(node_data_mask,dtypetorch.float)# Create a Data object for this row/graphgraph_dataData(xnode_features_tensor,edge_indexedges.t().contiguous(),masknode_data_mask_tensor)graphs.append(graph_data)#### splitting the data into train, test observation# Split indicesobservation_indicesdf_scaled.index.tolist()train_indices,test_indicestrain_test_split(observation_indices,test_size0.05,random_state42)# Create training and testing graphstrain_graphs[graphs[i]foriintrain_indices]test_graphs[graphs[i]foriintest_indices]图形可视化使用上述边缘索引创建的图结构可以通过 networkx 进行可视化。importnetworkxasnximportmatplotlib.pyplotasplt Gnx.Graph()forsrc,dstinedges.t().numpy():G.add_edge(nodes_order[src],nodes_order[dst])plt.figure(figsize(10,8))posnx.spring_layout(G)nx.draw(G,pos,with_labelsTrue,node_colorlightblue,edge_colorgray,node_size2000,font_weightbold)plt.title(Graph Visualization)plt.show()模型定义让我们定义模型。该模型包含两个 GAT 卷积层第一个层将节点特征转换到 8 维空间第二个 GAT 层则将其进一步缩减为 8 维表示。GNN图神经网络对过拟合非常敏感正则化dropout会在每个 GAT 层后应用并使用用户定义的概率来防止过拟合。dropout 层在训练过程中随机将输入张量的某些元素置零。GAT 卷积层的输出结果通过一个全连接线性层以将 8 维的输出映射到最终的节点特征在本例中每个节点对应一个标量值。对目标节点的值进行掩码处理如前所述本任务的目的是基于邻居节点的值回归目标节点的值。这也是将目标节点的值掩码或替换为零的原因。fromtorch_geometric.nnimportGATConvimporttorch.nn.functionalasFimporttorch.nnasnnclassGNNModel(nn.Module):def__init__(self,num_node_features):super(GNNModel,self).__init__()self.conv1GATConv(num_node_features,16)self.conv2GATConv(16,8)self.fcnn.Linear(8,1)# Outputting a single value per nodedefforward(self,data,target_node_idxNone):x,edge_indexdata.x,data.edge_index edge_indexedge_index.T xx.clone()# Mask the target nodes feature with a value of zero!# Aim is to predict this value from the features of the neighboursiftarget_node_idxisnotNone:x[target_node_idx]torch.zeros_like(x[target_node_idx])xF.relu(self.conv1(x,edge_index))xF.dropout(x,p0.05,trainingself.training)xF.relu(self.conv2(x,edge_index))xF.relu(self.conv3(x,edge_index))xF.dropout(x,p0.05,trainingself.training)xself.fc(x)returnx训练模型初始化模型并定义优化器、损失函数以及包括学习率、权重衰减用于正则化、批处理大小和训练轮次在内的超参数。modelGNNModel(num_node_features1)batch_size8optimizertorch.optim.Adam(model.parameters(),lr0.0002,weight_decay1e-6)criteriontorch.nn.MSELoss()num_epochs200train_loaderDataLoader(train_graphs,batch_size1,shuffleTrue)model.train()训练过程相对标准每个图一个数据点都会通过模型的前向传播遍历每个节点并预测目标节点。预测产生的损失会在定义的批处理大小上累积然后通过反向传播更新 GNN。forepochinrange(num_epochs):accumulated_loss0optimizer.zero_grad()loss0forbatch_idx,datainenumerate(train_loader):maskdata.maskforiinrange(1,data.num_nodes):ifmask[i]1:# Only train on nodes with dataoutputmodel(data,i)# get predictions with the target node masked# check the feed forward part of the modeltargetdata.x[i]predictionoutput[i].view(1)losscriterion(prediction,target)#Update parameters at the end of each set of batchesif(batch_idx1)%batch_size0or(batch_idx1)len(train_loader):loss.backward()optimizer.step()optimizer.zero_grad()accumulated_lossloss.item()loss0average_lossaccumulated_loss/len(train_loader)print(fEpoch{epoch1}, Average Loss:{average_loss})测试训练好的模型使用测试数据集将每个图像通过训练后的模型的前向传播并根据每个节点的邻居节点的值预测其值。test_loaderDataLoader(test_graphs,batch_size1,shuffleTrue)model.eval()actual[]pred[]fordataintest_loader:maskdata.maskforiinrange(1,data.num_nodes):outputmodel(data,i)predictionoutput[i].view(1)targetdata.x[i]actual.append(target)pred.append(prediction)可视化测试结果使用 iplot我们可以将节点的预测值与真实值进行可视化对比。importplotly.graph_objectsasgofromplotly.offlineimportiplot actual_values_float[value.item()forvalueinactual]pred_values_float[value.item()forvalueinpred]scatter_tracego.Scatter(xactual_values_float,ypred_values_float,modemarkers,markerdict(size10,opacity0.5,colorrgba(255,255,255,0),linedict(width2,colorrgba(152, 0, 0, .8),)),nameActual vs Predicted)line_tracego.Scatter(x[min(actual_values_float),max(actual_values_float)],y[min(actual_values_float),max(actual_values_float)],modelines,markerdict(colorblue),namePerfect Prediction)data[scatter_trace,line_trace]layoutdict(titleActual vs Predicted Values,xaxisdict(titleActual Values),yaxisdict(titlePredicted Values),autosizeFalse,width800,height600)figdict(datadata,layoutlayout)iplot(fig)https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/cf42365780e85ca2cddbafe8d8993e00.png尽管没有对模型架构或超参数进行精细调优实际上模型表现不错我们可以进一步调优模型以提高准确性。这篇文章到此为止。GNN 相较于其他机器学习分支来说相对较新看到这个领域的发展以及它应用于不同问题将是非常令人兴奋的。最后感谢你花时间阅读这篇文章希望它对你理解 GNN 或其数学背景有所帮助。在你离开之前个人而言我非常喜欢花时间学习新概念并将这些概念应用于新的问题和挑战我相信大多数阅读这些文章的人也有同样的感受。我认为能够做这件事是一种特权每个人都应该拥有这种特权但并不是每个人都能拥有。我们每个人都有责任改变这一点为每个人创造更加光明的未来。请考虑向 UniArk 捐款UniArk.org以帮助来自世界上常常被大学和国家忽视的群体——受迫害的少数群体无论是种族、宗教还是其他方面的才华横溢的学生。UniArk 深入寻找最遥远、最贫困地区的人才与潜力——那些发展中国家的偏远地区。您的捐款将成为压迫社会中某个人的希望灯塔。我希望您能帮助 UniArk 保持这盏灯塔的光明。除非另有注明所有图片均由作者提供