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.
代码地址(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堆着...作者也不回