异构图上的连接预测二

异构图上的连接预测二

码农世界 2024-05-29 前端 99 次浏览 0个评论

异构图上的连接预测二

  • 对整个大图进行transform处理
  • 获取批次子图
  • 定义GNN网络
  • 定义分类器:
  • 整合模型。
  • 开始训练:

    对整个大图进行transform处理

    详细描述过程都在代码中体现。

    transform = T.RandomLinkSplit(
        num_val=0.1, # 10%的 边 作为验证集,
        num_test=0.1, # 10%的 边 作为测试集
        disjoint_train_ratio=0.3, #训练集中 30%的边将不会出现在验证集和测试集中。
        neg_sampling_ratio=2.0, # 负采样比例为2.0,就是说生成的负样本数量是正样本数量的两倍,
                                # 什么是负样本呢? 就是图中不存在的边。
        add_negative_train_samples=False,  # 是否为训练集添加负样本呢?
        edge_types=("user", "rates", "movie"),# 指定边的类型,user到movie,关系是rates,即用户对电影的评分。
        rev_edge_types=("movie", "rev_rates", "user"),  # 同上,只不过反了,即电影被用户评分。
    )
    #
    train_data, val_data, test_data = transform(data)
    # print("Training data:")
    # print("==============")
    # print(train_data)
    # print()
    # print("Validation data:")
    # print("================")
    # print(val_data)
    # print()
    # print("Testing data:")
    # print("================")
    # print(test_data)
    #
    # print(train_data["user", "rates", "movie"].edge_label)
    # print(train_data["user", "rates", "movie"].edge_label_index)
    assert train_data["user", "rates", "movie"].num_edges == 56469
    assert train_data["user", "rates", "movie"].edge_label_index.size(1) == 24201
    assert train_data["movie", "rev_rates", "user"].num_edges == 56469
    # 没有负采样边(标签都为1)
    assert train_data["user", "rates", "movie"].edge_label.min() == 1
    assert train_data["user", "rates", "movie"].edge_label.max() == 1
    assert val_data["user", "rates", "movie"].num_edges == 80670
    assert val_data["user", "rates", "movie"].edge_label_index.size(1) == 30249
    assert val_data["movie", "rev_rates", "user"].num_edges == 80670
    # 负采样边比例为2
    assert val_data["user", "rates", "movie"].edge_label.long().bincount().tolist() == [20166, 10083]
    """
    训练边:验证边:测试边=0.8:0.1:0.1,总共100836
    100836*0.8=80670
    训练边中:消息传递:监督=0.7:0.3,训练边一共80670,其中消息边为80670 * 0.7=56469(edge_index);监督边24201
    验证边和测试边正样本(标签为1)各自为100836 * 0.1 ≈10083,由于有负采样,所以edge_label都为10083 * 3 =30249 
     (user, rates, movie)={
        edge_index=[2, 56469],
        edge_label=[24201],
        edge_label_index=[2, 24201],
      },
      理解下:
      edge_index=[2, 56469] 为啥这里有56469条边,而edge_label=[24201] 只有24201条边的标签呢?
      训练边中:消息传递:监督=0.7:0.3,训练边一共80670,其中消息边为80670 * 0.7=56469(edge_index);监督边24201
      因为edge_index包括了图中所有的边,而edge_label是用于监督的,在训练集中,占了80%,其中30%用于监督。
      什么?不理解监督什么意思吗??简单来说了你预测了一个东西,而且事先是知道该东西是啥玩意,即已知标签,将预测与标签进行对比。
    """
    # 获取需要的边标签索引和边标签
    edge_label_index = train_data['user','rates','movie'].edge_label_index # 标签对应的索引,那不就是监督边的索引吗?
    edge_label = train_data['user','rates','movie'].edge_label # 标签,监督边
    

    获取批次子图

    # 1-hop ,采样20个邻居,2-hop采样10个邻居
    train_loader = LinkNeighborLoader(
        # 这里其实相当于在整个图中取出多个子图
        data=train_data,
        num_neighbors=[20, 10],
        neg_sampling_ratio=2.0,
        edge_label_index=(('user','rates','movie'), edge_label_index),
        edge_label=edge_label,
        batch_size=128, # 该批次选择了128个初始节点
        shuffle=True,
        # 128个节点,然后第一层都挑选20个一阶节点,第二层挑选10个二阶节点。
        # 因为neg_sampling_ration = 2,也就是负样本的数量将是正样本的两本,那么总的数量就是128 *2 + 128 = 384
    )
    # 一个采样数据
    sampled_data = next(iter(train_loader))
    print("Sampled mini-batch:")
    print("===================")
    # print(sampled_data)
    assert sampled_data["user", "rates", "movie"].edge_label_index.size(1) == 3 * 128
    assert sampled_data["user", "rates", "movie"].edge_label.min() == 0
    assert sampled_data["user", "rates", "movie"].edge_label.max() == 1
    

    定义GNN网络

    在这里应该注意到这是GNN网络,用于处理同构图的。也就是边和节点类型都一样的图。

    class GNN(nn.Module):
        def __init__(self,hidden_channels):
            super(GNN,self).__init__()
            # 对子图进行处理咯,例如吧,电影的特征20,   输出为64,
            self.conv1 = SAGEConv(hidden_channels,hidden_channels)
            self.conv2 = SAGEConv(hidden_channels,hidden_channels)
            # x 的类型被注释为tensor,edge_index 的类型也是tensor,而->tensor 用于指示forward方法的返回类型是tensor
        def forward(self,x: Tensor, edge_index:Tensor)->Tensor:
            x = F.relu(self.conv1(x,edge_index))
            x = self.conv2(x,edge_index)
            return x
    

    定义分类器:

    你说分类器干嘛的?

    假设数据:

    x_user = [

    [0.1, 0.2, 0.3], # 用户1的嵌入向量

    [0.4, 0.5, 0.6], # 用户2的嵌入向量

    [0.7, 0.8, 0.9] # 用户3的嵌入向量

    ]

    x_movie = [

    [0.1, 0.2, 0.3], # 电影A的嵌入向量

    [0.4, 0.5, 0.6], # 电影B的嵌入向量

    [0.7, 0.8, 0.9] # 电影C的嵌入向量

    ]

    edge_label_index = [

    [0, 1, 2], # 用户的节点ID

    [0, 1, 2] # 对应电影的节点ID

    ]

    (0.1 * 0.1) + (0.2 * 0.2) + (0.3 * 0.3) = 0.01 + 0.04 + 0.09 = 0.14

    (0.4 * 0.4) + (0.5 * 0.5) + (0.6 * 0.6) = 0.16 + 0.25 + 0.36 = 0.77

    (0.7 * 0.7) + (0.8 * 0.8) + (0.9 * 0.9) = 0.49 + 0.64 + 0.81 = 1.94

    pred:tensor([0.14, 0.77, 1.94])

    用于预测用户对电影的评分。

    分类器通过点积操作计算用户和电影嵌入向量的相似度,从而预测用户对电影的评分。

    class Classifier(nn.Module):
        def forward(self,x_user:Tensor,x_movie:Tensor,edge_label_index:Tensor)->Tensor:
            # 将节点嵌入转换为边表示:
            edge_feat_user = x_user[edge_label_index[0]]
            edge_feat_movie = x_movie[edge_label_index[1]]
            return (edge_feat_user * edge_feat_movie).sum(dim=-1)
    

    整合模型。

    class Model(nn.Module):
        def __init__(self,hidden_channels):
            super().__init__()
            # 电影的特征维度是20
            self.movie_lin = nn.Linear(in_features=20,out_features=hidden_channels)
            # embedding操作,为用户生成向量,
            self.user_emb = nn.Embedding(data['user'].num_nodes,hidden_channels)
            # embedding操作,为电影生成向量,
            self.movie_emb = nn.Embedding(data['movie'].num_nodes,hidden_channels)
            self.gnn = GNN(hidden_channels)
            # 将同构图转变为异构图。
            self.gnn = to_hetero(self.gnn,metadata=data.metadata())
            self.classifer = Classifier()
        def forward(self,data:HeteroData)->Tensor:
            x_dict = {
                'user':self.user_emb(data['user'].node_id),
                # 其实可以不用相加的,但相加的话,可能学习效果会更好,
                # self.movie_lin(data['movie'].x) x其实是电影特征=>[128,20] =>[128,64]
                # self.movie_emb(data['movie'].node_id) [128,64]
                'movie':self.movie_lin(data['movie'].x) +self.movie_emb(data['movie'].node_id)
            }
            # model 初始化时已经调用了self.gnn = to_hetero(self.gnn,metadata=data.metadata()) 将其变为异构图gnn吗,能够对异构图进行处理
            x_dict = self.gnn(x_dict, data.edge_index_dict)
            pred = self.classifer(
                x_dict["user"],
                x_dict["movie"],
                data["user", "rates", "movie"].edge_label_index,
            )
            return pred
    

    开始训练:

    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print(f'运行在{device}上')
    model = Model(hidden_channels=64).to(device)
    optimizer = torch.optim.Adam(model.parameters(),lr=0.001)
    for epoch in range(1,6):
        total_loss = total_examples = 0
        for sampled_data in tqdm.tqdm(train_loader):
            sampled_data =sampled_data.to(device)
            # 梯度清零
            optimizer.zero_grad()
            # 运行
            pred = model(sampled_data)
            # 真实值
            ground_truth = sampled_data['user','rates','movie'].edge_label
            loss = F.binary_cross_entropy_with_logits(pred,ground_truth)
            loss.backward()
            optimizer.step()
            total_loss += float(loss) * pred.numel()
            total_examples += pred.numel()
        print(f"Epoch: {epoch:03d}, Loss: {total_loss / total_examples:.4f}")
    

转载请注明来自码农世界,本文标题:《异构图上的连接预测二》

百度分享代码,如果开启HTTPS请参考李洋个人博客
每一天,每一秒,你所做的决定都会改变你的人生!

发表评论

快捷回复:

评论列表 (暂无评论,99人围观)参与讨论

还没有评论,来说两句吧...

Top