在工业界落地的PinSAGE图卷积算法原理及源码学习(三)数据加载及PinSAGE模型的定义与训练

在之前的两篇文章中,我们介绍了数据处理及图的定义采样,这篇文章是该系列的最后一篇文章——介绍数据加载及PinSAGE模型的定义与训练。

数据加载

这块涉及到的文件主要有model.py和sampler.py。

熟悉Pytorch搭建模型的同学应该知道,如果要自己定义数据输入模型的格式则需要自定义Dataloader创建时的collate_fn函数,此处的collator_fn函数定义在PinSAGECollator中:

class PinSAGECollator(object):
    def __init__(self, sampler, g, ntype, textset):
        self.sampler = sampler
        self.ntype = ntype
        self.g = g
        self.textset = textset

    def collate_train(self, batches):
        heads, tails, neg_tails = batches[0]
        # Construct multilayer neighborhood via PinSAGE...
        pos_graph, neg_graph, blocks = self.sampler.sample_from_item_pairs(
            heads, tails, neg_tails
        )
        assign_features_to_blocks(blocks, self.g, self.textset, self.ntype)

        return pos_graph, neg_graph, blocks

    def collate_test(self, samples):
        batch = torch.LongTensor(samples)
        blocks = self.sampler.sample_blocks(batch)
        assign_features_to_blocks(blocks, self.g, self.textset, self.ntype)
        return blocks

可以看到PinSAGECollator定义了两个将来用于构建Dataloader的函数collate_train和collate_test。collate_train每调用一次将会返回一个batch的pos_graph和neg_graph、blocks用于模型训练。

dataloader的构建如下:

collator = sampler_module.PinSAGECollator(
        neighbor_sampler, g, item_ntype, textset
    )
    dataloader = DataLoader(
        batch_sampler,
        collate_fn=collator.collate_train,
        num_workers=args.num_workers,
    )

通过以下过程获取dataloader一个batch内的数据并送入模型:

dataloader_it = iter(dataloader)
    for epoch_id in range(args.num_epochs):
        model.train()
        for batch_id in tqdm.trange(args.batches_per_epoch):
            pos_graph, neg_graph, blocks = next(dataloader_it)
            # Copy to GPU
            for i in range(len(blocks)):
                blocks[i] = blocks[i].to(device)
            pos_graph = pos_graph.to(device)
            neg_graph = neg_graph.to(device)
            # 将正样本图、负样本图以及blocks送入模型
            loss_tmp = model(pos_graph, neg_graph, blocks)
            loss = loss_tmp.mean()
            opt.zero_grad()
            loss.backward()
            opt.step()

模型定义

PinSAGE模型的构建主要涉及到两个文件model.py和layers.py两个文件。首先看PinSAGE的主要结构:

class PinSAGEModel(nn.Module):
    def __init__(self, full_graph, ntype, textsets, hidden_dims, n_layers):
        super().__init__()
        self.proj = layers.LinearProjector(
            full_graph, ntype, textsets, hidden_dims
        )
        self.sage = layers.SAGENet(hidden_dims, n_layers)
        self.scorer = layers.ItemToItemScorer(full_graph, ntype)
    def forward(self, pos_graph, neg_graph, blocks):
        h_item = self.get_repr(blocks)
        pos_score = self.scorer(pos_graph, h_item)
        neg_score = self.scorer(neg_graph, h_item)
        # pos_score和neg_score对应的是一个节点和正样本对之间的相似性和该节点与负样本之间的相似性
        # 优化的目标是neg_score - pos_score的值要小于-1
        return (neg_score - pos_score + 1).clamp(min=0)
    def get_repr(self, blocks):
        # 卷积层数决定了采样层数,决定了有多少个block,block[0]中的源节点是初始节点,block[-1]中的目标节点是最终的输出节点
        # 每个block中的源节点都会包含该block的输出节点
        # 对所有节点的数据先做embedding
        h_item = self.proj(blocks[0].srcdata)
        h_item_dst = self.proj(blocks[-1].dstdata)
        # 卷积后的值加上之前的值,类似于跳跃连接
        tmp = h_item_dst + self.sage(blocks, h_item)
        return tmp

PinSAGE模型的线性层——self.proj

这个层的主要作用是对movie节点的id,genere,title等属性进行Embedding,将每个节点映射为一个向量,这块没什么好讲的,就不展开介绍了。

PinSAGE模型的节点信息传递、聚合、更新层(卷积层)——self.sage

这个层主要由layers.py中的SAGENet类构成,SAGENet中由调用了同样在layers中的WeightedSAGEConv类,而这个类的核心就是负责信息的传递、更新、聚合,所以我个人将这个层也理解为卷积层。先看一下SAGENet的代码:

class SAGENet(nn.Module):
    def __init__(self, hidden_dims, n_layers):
        """
        g : DGLGraph
            The user-item interaction graph.
            This is only for finding the range of categorical variables.
        item_textsets : torchtext.data.Dataset
            The textual features of each item node.
        """
        super().__init__()
        self.convs = nn.ModuleList()
        for _ in range(n_layers):
            self.convs.append(
                WeightedSAGEConv(hidden_dims, hidden_dims, hidden_dims)
            )
    def forward(self, blocks, h):
        for layer, block in zip(self.convs, blocks):
            # 因为每个block中的目标节点都包含在源节点的最前面
            print(block.num_nodes("DST/" + block.ntypes[0]))
            h_dst = h[: block.num_nodes("DST/" + block.ntypes[0])]
            h = layer(block, (h, h_dst), block.edata["weights"])
        return h

可以看到其初始化方法里面定义了一个nn.ModuleList(),这就是一个可以将模型里的各个层或者模块都加入到里面的数组,相当于是一次封装,之后数据直接过这个nn.ModuleList()就可以。nn.ModuleList()里会加入n_layers个 WeightedSAGEConv模块。

PinSAGEModel在执行前向传播时,先调用了其本身的get_repr方法,将blocks中的第一个block中的源节点数据和第二个block中的目标节点数据过一下线性层,获得节点的向量表示,之后执行tmp = h_item_dst + self.sage(blocks, h_item)这个过程,在这个过程中就进入了SAGENet的前向传播过程。

在SAGENet前向传播时,先获得一个WeightedSAGEConv层和一个block,然后做一次卷积,我们可以看到给卷积层传的参数是第一个block的目标节点(因为block的源节点会包含目标节点,所以可以直接通过源节点获取目标节点,这在之前采样那篇文章里讲block时已经讲过了,不懂的点“在工业界落地的PinSAGE图卷积算法原理及源码学习(二)采样”),之后将这个block以及其中的源节点,目标节点,源节点到目标节点的边的权重传给了WeightedSAGEConv层。

接下来分析WeightedSAGEConv类的主要组成:

class WeightedSAGEConv(nn.Module):
    def __init__(self, input_dims, hidden_dims, output_dims, act=F.relu):
        super().__init__()
        self.act = act
        self.Q = nn.Linear(input_dims, hidden_dims)
        self.W = nn.Linear(input_dims + hidden_dims, output_dims)
        self.reset_parameters()
        self.dropout = nn.Dropout(0.5)
    def reset_parameters(self):
        gain = nn.init.calculate_gain("relu")
        nn.init.xavier_uniform_(self.Q.weight, gain=gain)
        nn.init.xavier_uniform_(self.W.weight, gain=gain)
        nn.init.constant_(self.Q.bias, 0)
        nn.init.constant_(self.W.bias, 0)
    def forward(self, g, h, weights):
        """
        g : graph
        h : node features
        weights : scalar edge weights
        """
        h_src, h_dst = h
        with g.local_scope():
            g.srcdata["n"] = self.act(self.Q(self.dropout(h_src)))
            print(g.srcdata["n"].shape)
            g.edata["w"] = weights.float()
            print(g.edata["w"].shape)
            g.update_all(fn.u_mul_e("n", "w", "m"), fn.sum("m", "n"))
            g.update_all(fn.copy_e("w", "m"), fn.sum("m", "ws"))
            n = g.dstdata["n"]
            ws = g.dstdata["ws"].unsqueeze(1).clamp(min=1)
            z = self.act(self.W(self.dropout(torch.cat([n / ws, h_dst], 1))))
            # 第一个参数表示求二范数,第二个参数代表求哪个维度的二范数
            z_norm = z.norm(2, 1, keepdim=True)
            z_norm = torch.where(
            # torch.tensor(1.0).to(z_norm) 将1.0这个tensor转成格式和z_norm一样的数据
            z_norm == 0, torch.tensor(1.0).to(z_norm), z_norm
            )
            z = z / z_norm
            return z

WeightedSAGEConv的初始化方法中主要定义了一些权重参数矩阵,这些都是将来节点上的信息做传递、聚合、更新时要用到的。

WeightedSAGEConv的前向传播forward主要负责每次卷积的工作,联系上文,SAGENet前向传播时每一次卷积向WeightedSAGEConv中传进来的是当前的block以及其中的源节点,目标节点,源节点到目标节点的边的权重,WeightedSAGEConv的前向传播中主要代码的含义为:

  • with g.local_scope():包裹着的内容其中的各种计算都不会改变节点原始信息,这样做只是为了方便计算,不会改变原始图。
  • g.srcdata["n"] = self.act(self.Q(self.dropout(h_src))):源节点传进来之后进行了dropout、乘上参数矩阵以及经过激活函数的操作,会将此时得到的信息存入到源节点的‘n’属性中。
  • g.update_all(fn.u_mul_e("n", "w", "m"), fn.sum("m", "n")):将源节点信息乘上源节点到目标节点的权重得到的值保存为‘m’,将‘m’与源节点‘n’属性中的值相加聚合到目标节点中并保存到目标节点的‘n’属性中。其中fn.u_mul_e是内置消息功能,如果特征形状相同,则通过在源节点和边(edge)特征之间执行逐元素乘法来计算边缘上的消息;否则,它将首先将特征广播到新形状并执行逐元素操作。因为该例中weights每条边上是单个的值,所以此处会进行广播的操作。
  • g.update_all(fn.copy_e("w", "m"), fn.sum("m", "ws")):先将边上的权重值复制保存为‘m’,再将‘m’的值相加聚合到目标节点中保存为‘ws’属性,个人感觉这个步骤举例说明的话类似:目标节点有两条边,这两条边的权重值分别为2,5,则将7这个值保存为该目标节点的‘ws’属性。
  • z = self.act(self.W(self.dropout(torch.cat([n / ws, h_dst], 1)))):这行代码中的n / ws其实是图卷积的一个经典操作,即降低权重较大的边传递过来的信息对目标节点的影响程度,同时拼接上目标节点原来的信息乘一个参数参数矩阵W

之后再经过一些归一化操作返回最终卷积后目标节点的值,这就是一次卷积的过程,第二次卷积过程和第一次类似。对于DGL中的消息传递函数和消息聚合函数不太了解的同学可以参考这篇文章[1]。

PinSAGE模型的节点相似性计算层——self.scorer

先看这个层的定义self.scorer = layers.ItemToItemScorer(full_graph, ntype),再看ItemToItemScorer这个类的定义:

class ItemToItemScorer(nn.Module):
    def __init__(self, full_graph, ntype):
        super().__init__()
        n_nodes = full_graph.num_nodes(ntype)
        self.bias = nn.Parameter(torch.zeros(n_nodes, 1))
    def _add_bias(self, edges):
        bias_src = self.bias[edges.src[dgl.NID]]
        bias_dst = self.bias[edges.dst[dgl.NID]]
        return {"s": edges.data["s"] + bias_src + bias_dst}
    def forward(self, item_item_graph, h):
        """
        item_item_graph : graph consists of edges connecting the pairs
        h : hidden state of every node
        """
        with item_item_graph.local_scope():
            # 为正样本图和负样本图上的节点赋予一个新属性,新属性是卷积后的节点的embedding
            item_item_graph.ndata["h"] = h
            # 正负样本图上有边的节点的embedding相乘(此处应该是向量点积),消息存储为s
            item_item_graph.apply_edges(fn.u_dot_v("h", "h", "s"))
            item_item_graph.apply_edges(self._add_bias)
            pair_score = item_item_graph.edata["s"]
        return pair_score

其中主要的就是前向传播的过程,通过之前的卷积已经得到了pos_graph和neg_graph中所有节点的向量表示,这个向量表示是聚合其采样得到的两层邻居节点的信息得到的。将这些向量表示对应的存储到pos_graph和neg_graph中的节点的‘h’属性上:item_item_graph.ndata["h"] = h。之后将有边相连的节点上的‘h’信息两两相乘并保存到边的‘s’属性上,返回这个属性的值。这个值其实就是正样本对之间的相似性评分和负样本对之间的相似性评分。

PinSAGE模型前向传播过程对这块的调用为:

pos_score = self.scorer(pos_graph, h_item)
neg_score = self.scorer(neg_graph, h_item)

得到正负样本对相似性评分之后,返回模型之后要优化的目标:

return (neg_score - pos_score + 1).clamp(min=0)

.clamp(min=0)会让neg_score - pos_score + 1得到的值里所有小于0的值变成0,那模型优化时便会将neg_score - pos_score的值朝着大于-1的目标优化,这个其实是有点类似max margin的感觉,感兴趣的同学可以自行了解一下这个损失函数的相关知识,说白了就是最小化负样本对相似性评分与正样本对相似性评分的差值,即负样本对相似性越小越好,正样本对相似性越大越好。

之后就是模型训练的常规操作了,model.py中的:

loss = loss_tmp.mean()
opt.zero_grad()
loss.backward()
opt.step()

至此PinSAGE模型原理及源码分析就结束了,在这个系列中我基本上将DGL中实现PinSAGE模型的这个example从头到尾的捋了一遍,整个过程加深了自己对空域图卷积算法的理解,之前一直在看论文了解算法原理什么的,但有时候论文中的公式和一些叙述真的让人看得丈二和尚摸不着头脑。如果不是详细的捋一遍代码,可能看完这个模型我只知道是有这么一回事,其中算法的一些具体细节根本没弄清楚。其实从开始了解这个算法到写完这个系列的文章中间花了很长的时间,其中碰到了很多困难,比如:

  • 完全不了解DGL,刚开始甚至连利用DGL创建图都不会,更别提后面DGL中的采样器,block等的原理是什么了。
  • 不了解随机游走采样和PinSAGE采样的目的到底是为了什么?
  • 采样得到的pos_graph和neg_graph究竟怎样和之后的block联系起来?
  • ......

所幸详细学习完这份代码之后,以上的疑惑基本上都得到了解决,在这个过程中付出的时间与精力我觉得是值得的。如果我这个系列的文章也能对此刻正在学习图卷积或者PinSAGE的您能提供一点思路,节省一点时间,那就让我更开心了!

不积跬步无以至千里,不积小流无以成江海。

祝我们都学有所成,Best Wishes!

鉴于本人水平及精力时间有限,文章中难免有错误的地方,如果对您造成疑惑,欢迎在评论区指出或与我私信交流,看到会第一时间回复。或添加我的微信公众号进行交流。

微信公众号:琛锡的算法笔记。

参考

[1] DGL官方教程--API--dgl.function

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
卷积神经网络(Convolutional Neural Network,CNN)是一种深度学习模型,广泛应用于像识别、语音识别、自然语言处理等领域。在裂纹识别领域,CNN也具有良好的表现。CNN主要包含卷积层、池化层和全连接层等组成部分,下面对这些组成部分进行简要阐述。 卷积层是CNN的核心组成部分之一,它可以从原始数据中提取出特征信息。卷积层的输入是一张像和一组卷积核,卷积核会按照一定的规律在像上滑动,并对每个位置的像素点进行卷积运算。卷积运算可以理解为是两个函数之间的乘积积分,它可以将像中的每个像素点与卷积核进行相乘并求和,得到一个新的值,然后将这个新的值作为输出。通过卷积核的不同设置,可以提取出像中不同的特征,比如边缘、纹理等。 池化层是CNN中的另一个重要组成部分,它可以对卷积层的输出进行降维处理。池化层的常见操作有最大池化和平均池化。最大池化会在卷积层的输出中找到每个区域内的最大值,用这个最大值来代表这个区域的特征,从而减少了特征维度。平均池化则是计算每个区域内的平均值,同样也可以达到降维的效果。 全连接层是CNN的最后一层,它将经过卷积和池化处理之后的特征像转化为分类结果。全连接层的输出会对应不同的分类结果,比如正常像和裂纹像。 在裂纹识别领域,CNN的应用可以分为两个阶段。首先,需要使用一些已知的裂纹像作为训练样本,训练一个CNN模型训练过程中,CNN会自动学习到裂纹像中的特征信息,从而提高对裂纹的识别能力。其次,在实际应用中,将待检测的像输入到训练好的CNN模型中,即可自动判断该像是否存在裂纹。 总之,卷积神经网络在裂纹识别领域具有广泛的应用前景,可以准确地识别裂纹像,提高工业生产的安全性和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值