PyG异构图处理实战:从RGCN到HGT的电商推荐系统构建

📅 2026/8/5 14:03:15
PyG异构图处理实战:从RGCN到HGT的电商推荐系统构建
1. 项目概述为什么PyG的异构图处理是图神经网络进阶的必经之路如果你已经跟着PyTorch GeometricPyG的教程走过了前两课处理过同构图Homogeneous Graph上的节点分类、链接预测那么恭喜你你已经掌握了图神经网络GNN的“标准动作”。但现实世界的数据远比教科书里的同构图要复杂和精彩。社交网络中用户、帖子、话题是不同类型的节点电商系统里用户、商品、店铺、品牌之间存在着五花八门的关系学术引用网络中论文、作者、会议、关键词也交织成一张复杂的网。这些就是异构图Heterogeneous Graph。“第十八课.Pytorch-geometric入门(三)”这个标题直指PyG框架中处理异构图的模块。这不仅是PyG学习的进阶核心更是将GNN从实验室推向真实业务场景的关键一跃。我见过不少朋友在学完基础的GCN、GAT后面对公司里复杂的业务数据感到无从下手本质就是卡在了如何将异构的业务关系“翻译”成GNN能理解的格式这一步。PyG提供的torch_geometric.data.HeteroData类以及一系列为异构图设计的卷积层如HGTConv,HANConv就是解决这个问题的“瑞士军刀”。本篇文章我将以一个模拟的电商场景为例带你从零开始手把手拆解PyG处理异构图的完整流程。我们会涵盖从构建异构图数据对象、理解其核心数据结构到实现针对异构图的神经网络模型最后完成训练和预测。过程中我会穿插大量我在实际项目中踩过的坑和总结的经验比如如何处理动态变化的节点类型、如何设计有效的元路径Meta-path等。无论你是想用GNN分析多模态数据还是构建复杂的推荐系统这篇文章都能为你提供可直接复现的代码和经过验证的思路。2. 异构图核心概念与PyG数据结构全解析在写第一行代码之前我们必须把几个核心概念和它们在PyG中的对应物彻底理清。这能帮你建立正确的心理模型避免后续编码时一头雾水。2.1 同构与异构从单一到多元的本质区别同构图是所有节点类型相同、所有边类型也相同的图。比如一个论文引用网络所有节点都是“论文”所有边都是“引用”关系。它的数据可以用一个简单的Data对象表示x(节点特征),edge_index(边连接),y(节点标签)。异构图则包含多种类型的节点和边。它可以用一个多元组来形式化定义G (V, E, R, T)。其中V是节点集合E是边集合R是关系类型集合T是节点类型集合。关键在|T| 1或|R| 1。在PyG中我们用HeteroData类来封装这种复杂结构。你可以把它想象成一个字典的字典或者一个分门别类的容器。2.2 HeteroData对象异构图的“万能容器”HeteroData对象是理解PyG异构图处理的基石。它内部维护着多个独立的、按类型分隔的特征存储空间。import torch from torch_geometric.data import HeteroData # 初始化一个空的异构图数据对象 hetero_data HeteroData() # 假设我们有三种节点类型user, product, category # 两种边类型user_buys_product, product_belongs_to_category # 1. 添加节点特征 # 语法hetero_data[node_type].x feature_tensor hetero_data[user].x torch.randn(1000, 64) # 1000个用户每个64维特征 hetero_data[product].x torch.randn(5000, 128) # 5000个商品128维特征 hetero_data[category].x torch.randn(50, 32) # 50个类别32维特征 # 2. 添加边索引连接关系 # 语法hetero_data[edge_type].edge_index edge_index_tensor # edge_index是一个形状为[2, num_edges]的LongTensor存储(src_node, dst_node)对 user_buys_product_edge_index torch.randint(0, 1000, (2, 20000)) # 随机生成2万条购买边 # 注意这里需要确保src索引在user节点范围内dst索引在product节点范围内 hetero_data[user, buys, product].edge_index user_buys_product_edge_index product_belongs_to_edge_index torch.randint(0, 5000, (2, 5000)) hetero_data[product, belongs_to, category].edge_index product_belongs_to_edge_index # 3. 添加边特征可选 # hetero_data[edge_type].edge_attr edge_attr_tensor print(hetero_data) # 输出会清晰地显示节点和边的类型及其数量 # HeteroData( # user{ x[1000, 64] }, # product{ x[5000, 128] }, # category{ x[50, 32] }, # (user, buys, product){ edge_index[2, 20000] }, # (product, belongs_to, category){ edge_index[2, 5000] } # )注意HeteroData中节点类型的顺序非常重要当你通过整数索引引用节点时例如在edge_index中这个索引是相对于该节点类型列表的局部索引而不是全局索引。‘user’节点的索引0和‘product’节点的索引0代表的是两个完全不同的实体。2.3 元路径与元关系异构图表征学习的“导航图”在异构图中由于节点类型不同直接定义“邻居”变得模糊。一个用户的邻居可以是它购买的商品也可以是和它购买相同商品的其他用户通过商品节点间接相连。为了在这种复杂关系中定义有意义的语义我们引入了元路径Meta-path。元路径是定义在节点类型序列上的一种路径模式它抽象了特定的语义关系。例如在电商图中用户-购买-商品-属于-类别这条元路径连接了用户和商品类别可以理解为“用户的兴趣类别”。用户-购买-商品-购买-用户这条元路径连接了两个用户可以理解为“购买了相同商品的用户”即“兴趣相似的用户”。在PyG中许多异构图卷积层如HANConv需要你显式地定义一组元路径。模型会沿着每条元路径进行信息传播和聚合从而学习到包含不同语义的节点表征。实操心得一如何设计有效的元路径不要盲目列举所有可能的类型序列。应该从业务逻辑出发。问自己在我的场景中哪些连接模式蕴含了有价值的推理信息例如在欺诈检测中“用户-登录-设备-登录-用户”这条路径可能暗示设备共享是风险信号。通常与领域专家讨论或进行简单的图统计分析如计算不同元路径实例的个数和分布是设计元路径的好起点。3. 构建一个真实的电商异构图数据集理论说再多不如动手建一个图。我们接下来构建一个稍具规模的模拟电商异构图数据集并为其添加一些真实的复杂性。3.1 数据模拟与节点/边创建我们将模拟以下数据用户1000个特征包括年龄归一化、活跃等级one-hot。商品5000个特征包括价格归一化、品类编码one-hot。类别50个特征为随机生成的嵌入。关系购买关系2万条从用户到商品。属于关系5千条从商品到类别每个商品属于一个类别。浏览关系5万条从用户到商品比购买更稀疏的关系。import numpy as np from torch_geometric.data import HeteroData def generate_hetero_ecommerce_data(): data HeteroData() np.random.seed(42) torch.manual_seed(42) # --- 生成节点数据 --- num_users 1000 num_products 5000 num_categories 50 # 用户特征年龄 活跃等级(3级) user_age torch.rand(num_users, 1) # 模拟年龄已归一化 user_active torch.nn.functional.one_hot(torch.randint(0, 3, (num_users,)), num_classes3).float() data[user].x torch.cat([user_age, user_active], dim-1) # [1000, 4] # 商品特征价格 品类(10个一级品类) product_price torch.rand(num_products, 1) product_class torch.nn.functional.one_hot(torch.randint(0, 10, (num_products,)), num_classes10).float() data[product].x torch.cat([product_price, product_class], dim-1) # [5000, 11] # 类别特征随机嵌入 data[category].x torch.randn(num_categories, 32) # [50, 32] # --- 生成边数据 --- # 1. 购买关系 (user - product) num_buys 20000 buy_user_idx torch.randint(0, num_users, (num_buys,)) buy_product_idx torch.randint(0, num_products, (num_buys,)) data[user, buys, product].edge_index torch.stack([buy_user_idx, buy_product_idx]) # 可以为购买边添加权重例如购买次数 data[user, buys, product].edge_attr torch.randint(1, 5, (num_buys, 1)).float() # 2. 属于关系 (product - category) 每个商品属于一个类别 num_belongs num_products # 每个商品一个类别 belong_product_idx torch.arange(num_products) belong_category_idx torch.randint(0, num_categories, (num_products,)) data[product, belongs_to, category].edge_index torch.stack([belong_product_idx, belong_category_idx]) # 3. 浏览关系 (user - product) 比购买更频繁 num_views 50000 view_user_idx torch.randint(0, num_users, (num_views,)) view_product_idx torch.randint(0, num_products, (num_views,)) data[user, views, product].edge_index torch.stack([view_user_idx, view_product_idx]) # 浏览时长作为边特征 data[user, views, product].edge_attr torch.rand(num_views, 1) * 10 # 模拟0-10分钟的浏览时长 # --- 添加图级别任务标签例如预测用户是否会购买某个商品--- # 这是一个链接预测任务我们需要正样本和负样本 # 这里先用购买边作为正样本 data[user, buys, product].edge_label torch.ones(num_buys) # 负样本可以通过随机采样生成在训练时动态生成更常见 return data hetero_graph generate_hetero_ecommerce_data() print(“节点类型:”, hetero_graph.node_types) print(“边类型:”, hetero_graph.edge_types) print(“图包含的元关系:”, hetero_graph.metadata())3.2 数据转换与常用操作构建好HeteroData对象后我们经常需要进行一些操作。1. 转换为同构图用于某些需要同构输入的算法# 方法1忽略节点类型将所有节点视为同一类型会丢失类型信息 from torch_geometric.transforms import ToUndirected # 注意直接转换可能不合适因为特征维度可能不同。通常需要先统一特征维度。 # 方法2通过添加虚拟节点类型进行转换更常见 # 例如将异构图转换为一个以“商品”为中心的二分图同构图需要复杂的处理。 # 更常见的做法是直接使用异构图卷积层。2. 划分训练、验证、测试集针对节点或边PyG提供了transforms.RandomLinkSplit用于链接预测任务的边划分它专门支持HeteroData。from torch_geometric.transforms import RandomLinkSplit # 假设我们对‘buys’关系进行链接预测 transform RandomLinkSplit( num_val0.1, # 10%的边作为验证集 num_test0.1, # 10%的边作为测试集 disjoint_train_ratio0.3, # 训练边中30%不参与消息传递仅用于监督 neg_sampling_ratio1.0, # 为每个正样本生成1个负样本 add_negative_train_samplesTrue, # 为训练集也添加负样本 edge_types[(user, buys, product)], # 指定要分割的边类型 rev_edge_types[(product, rev_buys, user)] # 自动添加反向边类型 ) train_data, val_data, test_data transform(hetero_graph) # 现在每个data对象都包含了 edge_label 和 edge_label_index3. 异构图可视化简单检查直接可视化复杂的异构图很困难。通常我们使用统计方法来检查# 检查每个节点类型的数量 for node_type in hetero_graph.node_types: print(f“{node_type} 节点数: {hetero_graph[node_type].num_nodes}”) # 检查每种边类型的数量 for edge_type in hetero_graph.edge_types: print(f“{edge_type} 边数: {hetero_graph[edge_type].num_edges}”) # 检查特征维度 for node_type in hetero_graph.node_types: if hasattr(hetero_graph[node_type], ‘x’): print(f“{node_type} 特征维度: {hetero_graph[node_type].x.shape}”)实操心得二处理特征维度不一致问题这是新手最常见的坑。不同类型的节点特征维度x.shape[1]通常不同。而大多数GNN层要求输入特征维度一致。解决方案有两种1在模型的第一层为每种节点类型使用一个独立的线性投影层将其映射到统一的隐藏维度。2在数据预处理阶段手动为每种节点类型设计或学习一个统一的特征提取器。PyG的异构图卷积层通常支持第一种方式。4. 异构图神经网络模型实战从RGCN到HGT有了数据接下来就是模型。PyG提供了多种异构图卷积层我们选择两个最具代表性的来深入讲解RGCN和HGT。4.1 RGCN关系图卷积网络RGCN是同构图GCN在异构图的直接扩展。它为每种关系类型分配独立的权重矩阵在进行邻居聚合时根据边的类型选择不同的权重。import torch.nn as nn import torch.nn.functional as F from torch_geometric.nn import RGCNConv class HeteroRGCN(nn.Module): def __init__(self, hidden_channels, out_channels, node_types, edge_types): super().__init__() # 第一层为每种节点类型创建独立的嵌入层解决特征维度不一 self.node_embeddings nn.ModuleDict({ node_type: nn.Linear(hetero_graph[node_type].x.size(-1), hidden_channels) for node_type in node_types }) # RGCN卷积层 # 参数说明 # hidden_channels: 输入和输出特征维度 # num_relations: 关系类型的数量即 len(edge_types) self.conv1 RGCNConv(hidden_channels, hidden_channels, num_relationslen(edge_types)) self.conv2 RGCNConv(hidden_channels, out_channels, num_relationslen(edge_types)) # 如果需要为每种节点类型输出不同的维度可以在这里定义多个输出层 def forward(self, x_dict, edge_index_dict, edge_type_tensor): # x_dict: 字典key为节点类型value为特征Tensor # edge_index_dict: 字典key为边类型元组value为edge_index # RGCN需要将异构图转换为特定的输入格式一个edge_index和一个edge_type张量 # 1. 统一节点特征维度 x_dict {node_type: self.node_embeddings[node_type](x) for node_type, x in x_dict.items()} # 2. 将异构图数据转换为RGCN需要的格式这是一个关键步骤 # 我们需要将所有边合并成一个大的edge_index并创建一个对应的edge_type向量 # 其中每个边的类型用一个整数表示 edge_indices [] edge_types [] # 为每种边类型分配一个唯一的整数ID edge_type_to_id {et: i for i, et in enumerate(edge_index_dict.keys())} for edge_type, edge_index in edge_index_dict.items(): edge_indices.append(edge_index) edge_types.append(torch.full((edge_index.size(1),), edge_type_to_id[edge_type], dtypetorch.long)) # 合并所有边 full_edge_index torch.cat(edge_indices, dim1) full_edge_type torch.cat(edge_types, dim0).to(full_edge_index.device) # 3. 同样需要将所有节点特征合并成一个大的张量并建立全局索引映射 # 这里简化处理假设我们只对‘user’节点进行分类 x x_dict[‘user’] # 只取用户节点特征 # 注意此时full_edge_index中的节点索引需要是全局索引而非类型局部索引。 # 构建异构图时我们需要维护一个从节点类型局部索引到全局索引的映射。 # 由于篇幅这里省略了全局索引构建的复杂代码。在实际中可以使用PyG的to_homogeneous转换。 # 4. 应用RGCN层 x self.conv1(x, full_edge_index, full_edge_type).relu() x F.dropout(x, p0.5, trainingself.training) x self.conv2(x, full_edge_index, full_edge_type) return xRGCN的局限性RGCN虽然直观但它要求将所有节点和边“压平”到同构表示中这丢失了节点类型的语义信息。同时它为每种关系分配独立权重当关系类型非常多时例如知识图谱中有上千种关系参数会急剧膨胀容易过拟合。4.2 HGT异构图TransformerHGT是异构图上的Transformer它被认为是处理大规模异构图的SOTA方法之一。它引入了节点类型感知和边类型感知的注意力机制。节点类型特定参数为每种节点类型设计独立的线性变换用于生成Q, K, V。边类型特定参数为每种边类型设计独立的权重矩阵用于计算注意力得分和消息传递。异构互注意力计算注意力时同时考虑源节点类型、目标节点类型和边类型。from torch_geometric.nn import HGTConv, Linear class HeteroHGT(nn.Module): def __init__(self, hidden_channels, out_channels, num_heads, node_types, edge_types, num_layers2): super().__init__() self.hidden_channels hidden_channels self.node_types node_types self.edge_types edge_types # 1. 为每种节点类型创建特征投影层 self.lin_dict nn.ModuleDict() for node_type in node_types: # 将原始特征投影到统一的隐藏维度 self.lin_dict[node_type] Linear(-1, hidden_channels) # 2. 堆叠多层HGT卷积层 self.convs nn.ModuleList() for _ in range(num_layers): conv HGTConv(hidden_channels, hidden_channels, metadata(node_types, edge_types), num_headsnum_heads, groupsum) self.convs.append(conv) # 3. 输出层例如为用户节点生成预测 self.lin_out Linear(hidden_channels, out_channels) def forward(self, x_dict, edge_index_dict): # 投影节点特征 x_dict {node_type: self.lin_dict[node_type](x) for node_type, x in x_dict.items()} # 逐层应用HGT卷积 for conv in self.convs: x_dict conv(x_dict, edge_index_dict) # HGTConv内部已经包含了非线性激活和残差连接 # 返回用户节点的最终表征用于下游任务如分类 return self.lin_out(x_dict[‘user’])HGT的优势参数高效通过共享的注意力机制和类型特定的偏置项避免了RGCN的参数爆炸问题。语义丰富显式建模节点和边类型能更好地捕获异构语义。可扩展性强其Transformer架构适合大规模图且支持小批量训练。实操心得三如何选择异构图模型如果图关系类型少50且结构相对简单可以从RGCN或HAN基于元路径的注意力网络开始它们更易于理解和实现。如果图关系类型多、结构复杂、规模大HGT是更好的选择它在许多基准数据集上表现优异。如果计算资源有限可以考虑SimpleHGN等轻量级模型。永远不要忘记基线模型尝试将异构图通过添加虚拟节点等方式转换为同构图然后用普通的GCN/GAT跑一下。这个基线性能能帮你判断引入复杂异构模型是否真的带来了增益。5. 模型训练、评估与调试全流程模型定义好了我们将其应用于链接预测任务预测用户是否会购买某个商品。5.1 链接预测任务实战我们将使用之前用RandomLinkSplit划分好的数据。import torch_geometric.transforms as T from torch_geometric.loader import LinkNeighborLoader from sklearn.metrics import roc_auc_score # 1. 数据准备使用之前生成的hetero_graph和transform # 假设我们已经有了 train_data, val_data, test_data # 2. 创建邻居加载器用于小批量训练 # 链接预测需要以边为中心进行采样 train_loader LinkNeighborLoader( datatrain_data, num_neighbors[20, 10], # 每层采样的邻居数 edge_label_index((user, buys, product), train_data[(user, buys, product)].edge_label_index), edge_labeltrain_data[(user, buys, product)].edge_label, batch_size128, shuffleTrue, ) # 类似地创建验证和测试的loadershuffleFalse val_loader LinkNeighborLoader(...) test_loader LinkNeighborLoader(...) # 3. 初始化模型、优化器 device torch.device(cuda if torch.cuda.is_available() else cpu) model HeteroHGT(hidden_channels64, out_channels1, num_heads4, node_typeshetero_graph.node_types, edge_types[(user, buys, product), (product, belongs_to, category), (user, views, product)]).to(device) optimizer torch.optim.Adam(model.parameters(), lr0.001) criterion nn.BCEWithLogitsLoss() # 二分类交叉熵损失 # 4. 训练循环 def train(): model.train() total_loss 0 for batch in train_loader: batch batch.to(device) optimizer.zero_grad() # 获取当前batch的节点特征和边索引字典 x_dict {node_type: batch[node_type].x for node_type in batch.node_types} edge_index_dict {} for edge_type in batch.edge_types: if hasattr(batch[edge_type], edge_index): edge_index_dict[edge_type] batch[edge_type].edge_index # 前向传播获取用户节点的表征 # 注意我们的HGT模型只返回用户表征。对于链接预测我们需要商品表征。 # 我们需要修改模型使其返回所有节点类型的表征或者使用另一个模型来获取商品表征。 # 这里为了简化假设我们有一个能返回所有节点表征的模型 model。 # h_dict model(x_dict, edge_index_dict) # 返回所有节点表征的字典 # user_emb h_dict[user][batch[user].batch] # 获取batch中用户的嵌入 # product_emb h_dict[product][batch[product].batch] # 获取batch中商品的嵌入 # pred (user_emb * product_emb).sum(dim-1) # 内积作为预测分数 # 由于HGT示例只输出了用户表征这里我们采用一个简化策略 # 使用一个共享的HGT编码器然后分别用两个线性层得到用户和商品的最终链接预测向量。 # 定义一个新的模型类 HGTLinkPrediction它包含一个HGT编码器和两个输出投影层。 # 以下为训练步骤的伪代码逻辑 # h_dict self.hgt_encoder(x_dict, edge_index_dict) # user_emb self.user_lin(h_dict[user]) # product_emb self.product_lin(h_dict[product]) # 通过采样得到的正负边索引从user_emb和product_emb中取出对应的嵌入做内积。 # pred (user_emb[edge_label_index[0]] * product_emb[edge_label_index[1]]).sum(dim-1) # loss criterion(pred, batch.edge_label) # loss.backward() # optimizer.step() # total_loss float(loss) # return total_loss / len(train_loader) # 5. 验证/测试函数 def test(loader): model.eval() preds [] labels [] with torch.no_grad(): for batch in loader: batch batch.to(device) # ... (类似训练的前向传播但不计算梯度) # 收集预测值和真实标签 # preds.append(pred.sigmoid().cpu()) # labels.append(batch.edge_label.cpu()) # preds torch.cat(preds, dim0).numpy() # labels torch.cat(labels, dim0).numpy() # auc roc_auc_score(labels, preds) # return auc # 训练循环 for epoch in range(1, 101): loss train() val_auc test(val_loader) print(fEpoch: {epoch:03d}, Loss: {loss:.4f}, Val AUC: {val_auc:.4f}) # 保存最佳模型...5.2 模型评估与性能分析对于链接预测AUC是最常用的评估指标。训练过程中要密切关注训练集和验证集AUC的差距以防止过拟合。如果训练AUC很高但验证AUC很低可能是过拟合。可以尝试增加Dropout率、使用更小的隐藏层、对异构图边进行Dropouttorch_geometric.nn.models.HeteroDictDropout、添加L2正则化。如果训练和验证AUC都低可能是模型能力不足或特征信息不够。可以尝试增加模型深度/宽度、使用更复杂的异构图卷积层如HGT、引入更丰富的节点/边特征、设计更好的元路径。评估时注意数据泄露确保验证集和测试集的边在训练时完全不可见。RandomLinkSplit通过disjoint_train_ratio参数可以确保一部分训练边也不参与消息传递用于监督学习这能更真实地评估模型泛化能力。5.3 调试与可视化技巧梯度检查在训练初期检查各层梯度是否正常。如果出现梯度消失或爆炸需要调整初始化或学习率。激活值分布使用torch.nn.utils.stateless.functional_call或手动钩子检查各层输出激活值的均值和方差确保没有饱和如sigmoid输出全为0或1。注意力权重可视化针对HGT/HAN对于基于注意力的模型可以提取注意力权重观察模型更关注哪些类型的邻居或哪些元路径。这有助于理解模型行为和进行可解释性分析。# 以HAN为例获取元路径注意力权重 # model.convs[0].attn_src 或 model.convs[0].attn_dst 可能存储了注意力系数 # 需要根据具体模型实现来访问使用TensorBoard或Weights Biases实时监控损失、AUC等指标曲线方便调整超参数。6. 常见问题、避坑指南与进阶方向6.1 高频问题速查表问题现象可能原因解决方案运行时错误维度不匹配1. 不同节点类型的特征维度不同但模型期望统一维度。2.edge_index中的节点索引超出了该类型节点的范围。1. 在模型第一层为每种节点类型添加独立的线性投影层。2. 检查数据生成逻辑确保edge_index的每个维度索引与其对应的节点类型数量一致。使用data.validate()进行检查。训练Loss为NaN1. 学习率过高。2. 特征值或梯度值过大。3. 图中存在自循环或重复边未处理。1. 降低学习率如从1e-3降到1e-4。2. 对节点特征进行标准化如LayerNorm。3. 使用T.ToUndirected()和T.RemoveDuplicatedEdges()等transform清理数据。模型不收敛Loss震荡1. 数据噪声大。2. 批次大小不合适。3. 优化器选择不当。1. 清洗数据或尝试更鲁棒的损失函数。2. 调整批次大小通常增大批次更稳定。3. 尝试AdamW优化器并搭配适当的权重衰减。内存溢出OOM1. 图太大无法全图加载。2. 邻居采样层数或数量过多。1.必须使用邻居采样器如NeighborLoader,LinkNeighborLoader。2. 减少采样层数如num_neighbors[15, 10, 5]或每层采样数。3. 使用CPU进行数据加载GPU只负责计算。预测性能差1. 特征工程不足。2. 模型结构不适合数据。3. 元路径或关系定义不合理。1. 尝试添加更丰富的节点特征如预训练嵌入。2. 换用更复杂的模型如从RGCN换到HGT。3. 重新审视业务逻辑设计或自动学习更有意义的元路径。6.2 独家避坑技巧从简单开始先用一个简单的模型比如只有一层投影层一层RGCN跑通流程确保数据加载、训练循环没问题再逐步增加模型复杂度。善用data.validate()在将HeteroData对象送入模型之前调用data.validate()方法它能检查很多常见的数据不一致问题。注意反向边的添加许多异构图算法默认边是有向的。如果你的关系本质是无向的如“用户-认识-用户”或者需要双向消息传递记得使用ToUndirected()变换或在定义边类型时显式添加反向边。处理动态异构图如果图中的节点或边类型会动态增加例如新上线一种商品品类考虑使用更灵活的数据结构或图数据库进行管理并在模型设计时预留处理未知类型的能力如使用零初始化或一个统一的“未知类型”嵌入。6.3 进阶方向与资源推荐掌握了PyG异构图的基础后你可以向这些方向深入动态异构图研究如何建模随时间变化的图和关系。可以关注torch_geometric.temporal模块。异构图上的自监督学习在没有充足标签的情况下利用图的自身结构进行预训练。例如异构图上的对比学习如HeCo, DMGI。可扩展性与分布式训练对于十亿级规模的图需要学习如何使用PyG的torch_geometric.distributed模块或与DGL等框架配合进行分布式训练。与知识图谱结合很多知识图谱就是天然的异构图。可以探索使用RGCN、CompGCN、HGT等模型进行知识图谱补全、实体分类等任务。实践项目OGBOpen Graph Benchmark在ogbn-mag学术异构图等标准数据集上复现和刷榜。推荐系统在MovieLens用户-电影-标签、Amazon数据集用户-商品-品类上构建推荐模型。学术论文引用网络在DBLP或Aminer数据集上预测论文的发表会议或关键词。我个人在从同构图转向异构图的实践中最大的体会是对业务的理解深度直接决定了你构建的异构图模型的上限。模型结构可以调参但节点类型、边类型、元路径的设计需要你深入理解数据背后的故事。花时间做好数据探索和业务分析往往比盲目尝试十个新模型更有效。最后PyG的异构图模块仍在快速发展多查阅官方文档和论文保持对社区新动态的关注是持续进步的关键。