利用边缘特征

https://zhuanlan.zhihu.com/p/432923190

1. Backgrounds

图表示学习近年来取得巨大进展,以GCNs为代表的一系列图神经网络模型在节点分类、图分类、链接预测等图领域任务取得亮眼的成果。其中大部分模型基于message-passing方式构建,即“聚合邻居信息,更新节点自身状态”,在此范式中,节点特征得到充分的学习。而现实的许多图中,边上存在丰富的信息,它们在当前大多模型中未被充分利用。

Edge Features同样描述着网络,学习edge features能强化图神经网络的表达能力。

以下图为例:

社交网络中,edge features更具体地描述着用户(nodes)间关系。

2. Recently Works

当前图神经网络对边信息主要有如下几种利用方式:

2.1 Implicit Utilization

每个节点只aggregate其邻居的信息,这一聚合方式本身就基于节点间的边实现。此情况下只视作各个边为binary feature,只有“有边/无边”区别。

2.2 Naive Utilization

对于边上特征为scalar的情况,最简单直接的方式是使用带权的邻接矩阵描述,与之对应的,使用支持edge weight的模型学习即可。

2.3 Aggregate from Different Types of Edge

在许多场景中,边上特征为类别标签,如社交网络中,边上可以标注两人为工作关系、家人等。

对于存在多种类型边的图(边异构),常见处理方法是依照边的类型分别聚合信息

如早期工作Relational GCN[2],

其只在GCN

hl+1i=∑j∈Ni1normW(l)hj(l)+W(l)hi(l)

的基础上,增加了 ∑r∈R .

其他模型也是类似思路,仅在聚合方式上做进一步细化。 如下图:

2.4 Multi-dimensional Edge Features

上述3个方式并不能较好地处理边上多维特征。面对多维边特征,常见手段也是在aggregation阶段将边特征、邻居节点特征通过某种function结合在一起,再传给目标节点。

General Idea 如下图:

相关工作有PNAConv[3],Crystal Graph Conv[4]。

2.5 Learn Edge Embeddings

与2.4区别在于,下述方法以多维边特征为输入,并在模型每层更新,类似学习node embedding一般,同时学习edge embeddings。其实现方式多为创建某种辅助图,在该图中将边也视作节点,再用现有GAT等模型学习边和节点的表示。

EGNN [5]

Xl=σ[∏p=1P(α⋯pl(Xl−1,E⋯pl−1)gl(Xl−1))]

$P$ 为边特征维度数。

在GAT基础上,单独处理每一维的特征。聚合函数中加入节点特征,并为每一维特征单独学一组注意力权重,最后将各维输出concate。本文的edge embeddings,为每层所学的边多维特征注意力权重。

2. CensNet [6]

使用line graph(原始图中节点变为line graph中的边,边变为节点)构建辅助图,在original graph和line graph上训练模型,交替更新node, edge embeddings。

3. NENN [7]

以GAT为基础,提出Node-level Attention Layer, Edge-level Attention Layer。

每个layer区别主要在于输入的图的观察角度。

如下图中两矩形方框部分,分别以node、edge为视角,重新定义“邻居”,将边/节点视作新图中的节点,在新图中学习边和节点的embeddings。

4. EGAT [8]

CensNet类似,使用line graph+GAT学习节点和边的表示。

3. Discussion

2.5中多用GAT编码边特征信息,带来较大的计算开销,能否更轻量且优雅的编码边特征?

2.5中使用诸如line graph等构建辅助图,把原图中的边变换为辅助图中的节点,从而可以利用已有GNN进行边嵌入的学习。但是,对于“边的邻居边”,是否同样满足节点与其邻居相近的假设?

如何评估边特征与节点的关系,边特征如何切实的帮助图表示学习?


Reference

https://www.youtube.com/watch?v=mdWQYYapvR8

Schlichtkrull M, Kipf T N, Bloem P, et al. Modeling relational data with graph convolutional networks[C]//European semantic web conference. Springer, Cham, 2018: 593-607.

Corso G, Cavalleri L, Beaini D, et al. Principal neighbourhood aggregation for graph nets[J]. arXiv preprint arXiv:2004.05718, 2020.

Xie T, Grossman J C. Crystal graph convolutional neural networks for an accurate and interpretable prediction of material properties[J]. Physical review letters, 2018, 120(14): 145301.

Gong L, Cheng Q. Exploiting edge features for graph neural networks[C]//Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2019: 9211-9219.

Jiang X, Ji P, Li S. CensNet: Convolution with Edge-Node Switching in Graph Neural Networks[C]//IJCAI. 2019: 2656-2662.

Yang Y, Li D. Nenn: Incorporate node and edge features in graph neural networks[C]//Asian Conference on Machine Learning. PMLR, 2020: 593-608.

Chen J, Chen H. Edge-Featured Graph Attention Network[J]. arXiv preprint arXiv:2101.07671, 2021.

——————————————————————————

Gated-SCNN: GatedShape CNNs for Semantic Seg

https://zhuanlan.zhihu.com/p/182959105

写在前面

本篇论文《Gated-SCNN: Gated Shape CNNs for Semantic Segmentation》是作者在NVIDIA工作期间(现在在Google)的一篇将门控卷积和seg结合的paper,文章发表在2019ICCV.

论文地址:https://openaccess.thecvf.com/content_ICCV_2019/papers/Takikawa_Gated-SCNN_Gated_Shape_CNNs_for_Semantic_Segmentation_ICCV_2019_paper.pdf

代码地址(tensorflow):https://github.com/ben-davidson-6/Gated-SCNN

代码地址(pytorch):https://github.com/nv-tlabs/GSCNN(官方)

数据集:Cityscape(开源)

分析问题

现在的deep CNN能提取图像中很多feature map,比如图像的纹理、颜色、形状特征,但对于分割任务来说这并不是我们全部需要的,分割任务最理想的是根据边界和形状信息进行识别,如果信息流中包含了很多颜色、纹理可能会导致识别问题。

解决方法

针对上述问题,作者提出了双流CNN(two-stream CNN)结构,也就是将shape stream单独分离出来,与常规的CNN进行并行操作,最后将两者学习到的特征通过ASPP进行融合处理,从而提高了语义分割的性能。(这是我在查对小目标识别问题的时候搜到的,作者也提出此结构尤其在smaller和thinner object上有很好的识别结果)

整体网络结构

以下是作者提到的整体的网络结构,整体上它由三部分1.常规流(Regular Stream)2.形状流(Shape Stream)3.Fusion Module组成

Regular Stream

作者实验的backbone是Resnet101 & WideResnet,也可以用VGG或者其他的Resnet来代替,输入是N*3*H*W,输出N*3*H/m*W/m,其中m表示步长。

Shape Stream

这是本结构的重点部分,我们来详细看一下,结构如下图所示。

论文里面写GCL1的输入是图像的梯度信息(可以用Canny算子提取得到)+Regular Stream的第一层卷积输出作为输入(但是上面结构图似乎只能看出是Regular Stream第一层输出)。整个Shape Stream的输出是N*1*H*W边缘map。因为我们可以获得groundtrtuth二进制边缘语义分割label(比如用Canny算子就能简单地提取边缘信息),我们使用经典的BCEloss对ShapeStream部分进行有监督训练。

以下是它输入到Fusion Module的可视化结果(boundary feature):

GCL(Gated Convolution Layer)

GCL是网络的最核心的组件,它帮助ShapeStream只处理相关信息(比如形状、纹理)而过滤掉其他无关的特征信息。以下是我基于代码理解画的GCL流程图。

其中alpha_t(N*1*H*W)完全可以看成是PixelAttention map,结构可以看作是res block.以GCL1为例,它有两个输入,来自ShapeStream的r1(N*C*H*W)和经过conv降维的r2(N*1*H*W),输出得到N*C*H*W的featuremap作为下一个GCL2的输入。GCL2的输入来自GCL1res后的结果(N*C*H*W)和降维后的r3。三次GCL操作最后经过一次conv降维输出C*1*H*W的边缘结果图。公式如下图所示(个人觉得这里的alpah_t和spatial attention一样的 omega_t和channel attention是一样的).

Fusion Module

此模块将ShapeStream的输出S(N*1*H*W)(可视为boundary feature)和RegularStream的输出R(N*C*H*W)(可视为region feature)通过ASPP融合(ASPP通过不同rate的dilation卷积获得multi-scale的多尺度特征。用空洞卷积替换pooling可以减少信息损失)

损失函数设计

本次是多任务学习(边缘检测指导语义分割),total function由四部分组成:两个任务的loss function和两个任务的正则化项。

loss function

前半部分是ShapeStream的损失函数,因为边缘分割相当于是二分类问题(是/不是边缘),所以此任务的损失函数用BCEloss,后半部分是RegularStream的损失函数

regularizer

(组会上跟师兄导师讨论了一下正则化项和loss function的关系,开始他们告诉我它们没有明显的界限,后来导师说深度学习中不太常提正则化项的概念,让我可以把loss function理解为描述output和ground truth之间的直接关系, 而正则化项单纯是对output的一种约束)

前半部分正则化项是边缘分割的计算公式如下:

其中:

G表示高斯滤波,我们希望确保边界像素和GT边界不匹配时,避免无边界像素主导损失函数。类似地,我们可以使用形状的边界预测流确保二进制边界预测和语义预测的一致性。这就是后半部分的正则化项的作用。

实验细节

数据集:CityScapes数据,包含27个城市。总共2975训练集,500验证集和1525测试集。

评估指标:

IoU:用于评估是否精确的预测区域

F-score:用于评价边界的预测

Distance-base:通过对预测结果crop来评价小目标上的性能

试验细节:

batch_size=16,

lr=0.01多项式衰减

损失参数:20,1,1,1

resolution:800x800

实验结果

在整体结果的mIoU上,GSCNN要优于Deeplabv3,尤其是在一些较小目标比如下面的t-light,t-sign,pole即交通信号灯、交通信号标志、电线杆等识别上比起Deeplabv3+有较大的提升。

消融实验证明了GCL和Gradient的有效性(这个Gradient作者没有太多着墨,就在消融实验中提了一下在fusion module之前对图像进行了image gradient具体是canny算子实现的)

以及error map,可以看到我红框里标出的部分,Deeplabv3+没有识别到电线杆和栅栏,但GSCNN识别出来了。

代码(稍微解析一下关键代码,先滑到后面的forward)

import torch
import torch.nn.functional as F
from torch import nn
from network import SEresnext
from network import Resnet
from network.wider_resnet import wider_resnet38_a2
from config import cfg
from network.mynn import initialize_weights, Norm2d
from torch.autograd import Variable

from my_functionals import GatedSpatialConv as gsc

import cv2
import numpy as np

class Crop(nn.Module):
    def __init__(self, axis, offset):
        super(Crop, self).__init__()
        self.axis = axis
        self.offset = offset

    def forward(self, x, ref):
        """
        :param x: input layer
        :param ref: reference usually data in
        :return:
        """
        for axis in range(self.axis, x.dim()):
            ref_size = ref.size(axis)
            indices = torch.arange(self.offset, self.offset + ref_size).long()
            indices = x.data.new().resize_(indices.size()).copy_(indices).long()
            x = x.index_select(axis, Variable(indices))
        return x


class MyIdentity(nn.Module):
    def __init__(self, axis, offset):
        super(MyIdentity, self).__init__()
        self.axis = axis
        self.offset = offset

    def forward(self, x, ref):
        """
        :param x: input layer
        :param ref: reference usually data in
        :return:
        """
        return x

class SideOutputCrop(nn.Module):
    """
    This is the original implementation ConvTranspose2d (fixed) and crops
    """

    def __init__(self, num_output, kernel_sz=None, stride=None, upconv_pad=0, do_crops=True):
        super(SideOutputCrop, self).__init__()
        self._do_crops = do_crops
        self.conv = nn.Conv2d(num_output, out_channels=1, kernel_size=1, stride=1, padding=0, bias=True)

        if kernel_sz is not None:
            self.upsample = True
            self.upsampled = nn.ConvTranspose2d(1, out_channels=1, kernel_size=kernel_sz, stride=stride,
                                                padding=upconv_pad,
                                                bias=False)
            ##doing crops
            if self._do_crops:
                self.crops = Crop(2, offset=kernel_sz // 4)
            else:
                self.crops = MyIdentity(None, None)
        else:
            self.upsample = False

    def forward(self, res, reference=None):
        side_output = self.conv(res)
        if self.upsample:
            side_output = self.upsampled(side_output)
            side_output = self.crops(side_output, reference)

        return side_output


class _AtrousSpatialPyramidPoolingModule(nn.Module):
    '''
    operations performed:
      1x1 x depth
      3x3 x depth dilation 6
      3x3 x depth dilation 12
      3x3 x depth dilation 18
      image pooling
      concatenate all together
      Final 1x1 conv
    '''

    def __init__(self, in_dim, reduction_dim=256, output_stride=16, rates=[6, 12, 18]):
        super(_AtrousSpatialPyramidPoolingModule, self).__init__()

        # Check if we are using distributed BN and use the nn from encoding.nn
        # library rather than using standard pytorch.nn

        if output_stride == 8:
            rates = [2 * r for r in rates]
        elif output_stride == 16:
            pass
        else:
            raise 'output stride of {} not supported'.format(output_stride)

        self.features = []
        # 1x1
        self.features.append(
            nn.Sequential(nn.Conv2d(in_dim, reduction_dim, kernel_size=1, bias=False),
                          Norm2d(reduction_dim), nn.ReLU(inplace=True)))
        # other rates
        for r in rates:
            self.features.append(nn.Sequential(
                nn.Conv2d(in_dim, reduction_dim, kernel_size=3,
                          dilation=r, padding=r, bias=False),
                Norm2d(reduction_dim),
                nn.ReLU(inplace=True)
            ))
        self.features = torch.nn.ModuleList(self.features)

        # img level features
        self.img_pooling = nn.AdaptiveAvgPool2d(1)
        self.img_conv = nn.Sequential(
            nn.Conv2d(in_dim, reduction_dim, kernel_size=1, bias=False),
            Norm2d(reduction_dim), nn.ReLU(inplace=True))
        self.edge_conv = nn.Sequential(
            nn.Conv2d(1, reduction_dim, kernel_size=1, bias=False),
            Norm2d(reduction_dim), nn.ReLU(inplace=True))
         

    def forward(self, x, edge):
        x_size = x.size()

        img_features = self.img_pooling(x)
        img_features = self.img_conv(img_features)
        img_features = F.interpolate(img_features, x_size[2:],
                                     mode='bilinear',align_corners=True)
        out = img_features

        edge_features = F.interpolate(edge, x_size[2:],
                                      mode='bilinear',align_corners=True)
        edge_features = self.edge_conv(edge_features)
        out = torch.cat((out, edge_features), 1)

        for f in self.features:
            y = f(x)
            out = torch.cat((out, y), 1)
        return out

class GSCNN(nn.Module):
    '''
    Wide_resnet version of DeepLabV3
    mod1
    pool2
    mod2 str2
    pool3
    mod3-7
      structure: [3, 3, 6, 3, 1, 1]
      channels = [(128, 128), (256, 256), (512, 512), (512, 1024), (512, 1024, 2048),
                  (1024, 2048, 4096)]
    '''

    def __init__(self, num_classes, trunk=None, criterion=None):
        
        super(GSCNN, self).__init__()
        self.criterion = criterion
        self.num_classes = num_classes

        wide_resnet = wider_resnet38_a2(classes=1000, dilation=True)
        wide_resnet = torch.nn.DataParallel(wide_resnet)
        
        wide_resnet = wide_resnet.module
        self.mod1 = wide_resnet.mod1
        self.mod2 = wide_resnet.mod2
        self.mod3 = wide_resnet.mod3
        self.mod4 = wide_resnet.mod4
        self.mod5 = wide_resnet.mod5
        self.mod6 = wide_resnet.mod6
        self.mod7 = wide_resnet.mod7
        self.pool2 = wide_resnet.pool2
        self.pool3 = wide_resnet.pool3
        self.interpolate = F.interpolate
        del wide_resnet

        self.dsn1 = nn.Conv2d(64, 1, 1)
        self.dsn3 = nn.Conv2d(256, 1, 1)
        self.dsn4 = nn.Conv2d(512, 1, 1)
        self.dsn7 = nn.Conv2d(4096, 1, 1)

        self.res1 = Resnet.BasicBlock(64, 64, stride=1, downsample=None)
        self.d1 = nn.Conv2d(64, 32, 1)
        self.res2 = Resnet.BasicBlock(32, 32, stride=1, downsample=None)
        self.d2 = nn.Conv2d(32, 16, 1)
        self.res3 = Resnet.BasicBlock(16, 16, stride=1, downsample=None)
        self.d3 = nn.Conv2d(16, 8, 1)
        self.fuse = nn.Conv2d(8, 1, kernel_size=1, padding=0, bias=False)

        self.cw = nn.Conv2d(2, 1, kernel_size=1, padding=0, bias=False)

        self.gate1 = gsc.GatedSpatialConv2d(32, 32)
        self.gate2 = gsc.GatedSpatialConv2d(16, 16)
        self.gate3 = gsc.GatedSpatialConv2d(8, 8)
         
        self.aspp = _AtrousSpatialPyramidPoolingModule(4096, 256,
                                                       output_stride=8)

        self.bot_fine = nn.Conv2d(128, 48, kernel_size=1, bias=False)
        self.bot_aspp = nn.Conv2d(1280 + 256, 256, kernel_size=1, bias=False)

        self.final_seg = nn.Sequential(
            nn.Conv2d(256 + 48, 256, kernel_size=3, padding=1, bias=False),
            Norm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1, bias=False),
            Norm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, num_classes, kernel_size=1, bias=False))

        self.sigmoid = nn.Sigmoid()
        initialize_weights(self.final_seg)

    def forward(self, inp, gts=None):

        x_size = inp.size() 

        # res 1
        m1 = self.mod1(inp)

        # res 2
        m2 = self.mod2(self.pool2(m1))

        # res 3
        m3 = self.mod3(self.pool3(m2))

        # res 4-7
        m4 = self.mod4(m3)
        m5 = self.mod5(m4)
        m6 = self.mod6(m5)
        m7 = self.mod7(m6) 

        s3 = F.interpolate(self.dsn3(m3), x_size[2:],
                            mode='bilinear', align_corners=True)
        s4 = F.interpolate(self.dsn4(m4), x_size[2:],
                            mode='bilinear', align_corners=True)
        s7 = F.interpolate(self.dsn7(m7), x_size[2:],
                            mode='bilinear', align_corners=True)
        
        m1f = F.interpolate(m1, x_size[2:], mode='bilinear', align_corners=True)

        im_arr = inp.cpu().numpy().transpose((0,2,3,1)).astype(np.uint8)
        canny = np.zeros((x_size[0], 1, x_size[2], x_size[3]))
        for i in range(x_size[0]):
            canny[i] = cv2.Canny(im_arr[i],10,100)
        canny = torch.from_numpy(canny).cuda().float()

        cs = self.res1(m1f)
        cs = F.interpolate(cs, x_size[2:],
                           mode='bilinear', align_corners=True)
        cs = self.d1(cs)
        cs = self.gate1(cs, s3)
        cs = self.res2(cs)
        cs = F.interpolate(cs, x_size[2:],
                           mode='bilinear', align_corners=True)
        cs = self.d2(cs)
        cs = self.gate2(cs, s4)
        cs = self.res3(cs)
        cs = F.interpolate(cs, x_size[2:],
                           mode='bilinear', align_corners=True)
        cs = self.d3(cs)
        cs = self.gate3(cs, s7)
        cs = self.fuse(cs)
        cs = F.interpolate(cs, x_size[2:],
                           mode='bilinear', align_corners=True)
        edge_out = self.sigmoid(cs)
        cat = torch.cat((edge_out, canny), dim=1)
        acts = self.cw(cat)
        acts = self.sigmoid(acts)

        # aspp
        x = self.aspp(m7, acts)
        dec0_up = self.bot_aspp(x)

        dec0_fine = self.bot_fine(m2)
        dec0_up = self.interpolate(dec0_up, m2.size()[2:], mode='bilinear',align_corners=True)
        dec0 = [dec0_fine, dec0_up]
        dec0 = torch.cat(dec0, 1)

        dec1 = self.final_seg(dec0)  
        seg_out = self.interpolate(dec1, x_size[2:], mode='bilinear')            
       
        if self.training:
            return self.criterion((seg_out, edge_out), gts)              
        else:
            return seg_out, edge_out

我还根据网络画了一下这个结构图(左下角是那个Gate的结构,感觉自己像在画电路图哈哈哈)画完感觉还是挺清晰的,Regular Stream和Fusion Module都用绿色虚线框标出来啦, 剩下的都是Shape Stream部分:

写在后面

1.我总感觉这个GCL跟CBAM有相似(先是spatial_attention,就是那个alphat,再是channel attention,就是那个omega_t),我认为作者是把NLP里面的门卷积迁移到这里来了,但本质上还是两种attention结合。

2.这篇跟CE-Net有点相似,都是有个分支出来去学习边缘信息,再用边缘信息来指导语义分割。

CE-net原文:https://arxiv.org/abs/1903.02740

3.关于Canny算子的说明可以看看俺的这个:https://blog.csdn.net/weixin_40607008/article/details/107407484

4.既然可以搞专门学习边缘的GCL,那也可以搞学习颜色的、学习形状的等等,甚至也可以搞两个分支出来分别学习形状和边缘特征。

5.Github上说官方代码跑不通...(好多issue堆着...作者也不回

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值