第一版
这是对End-to-End Instance Segmentation with Recurrent Attention这篇paper的初步理解,由于实习跟课程设计的关系,时间有点赶,可能有些理解不到位的地方,以后有时间会持续更新。
引用部分是笔者对论文重要内容的英文摘录和翻译(意译),正文部分是笔者的理解及相关知识的补充说明。
这篇paper中作者提出了一个端对端的递归注意力网络(Recurrent Attention)架构,类似于人脑计数机制的原理,用于解决目标分割的问题。这个模型能检测出每个区域中最显著的目标并实现分割
这里的翻译可能不太妥当,“human-like counting process”(直译类人脑计数机制),但递归注意力网络在很大程度上可以说比其他网络架构更符合人脑工作原理。
- CNN的机制在于逐行逐像素扫描,符合我们的观察习惯;
- 而RNN则认为人脑解决问题是有时间序列的,前面的记忆对后面的判断有重要作用,更贴近人脑思维过程
- 笔者认为递归注意力网络可以说是RNN的加强版(内部主要组成也是RNN),提到了一个重要的概念:注意力转移,跟LSTM有同样逻辑,保留有用的信息,去除冗余的垃圾,但还有一个优势在于可以通过转移注意力使得特征对当前某一区域更为敏感,抽取更鲁棒的特征,更符合人脑认知习惯
文章中还提到解决图片分割最基本的问题就是要先计算有多少类目标(classes),更进一步需要计算每类目标的数量(numbers)。这其实就是语义分割(semantic segmentation)跟实例分割(instance segmentation)的区别:
(c)图描述的是“语义分割”,下面5只绵羊被归为一类“绵羊”
(d)图描述的是“实例分割”,5只绵羊分别用5种颜色加以标记
标准的基于像素级别的图像语义分割回答了“每个像素属于什么类别”的问题,但无法回答“照片中每一类目标的数量是多少”。显然,实例级别的图像分割要比语义级别的分割更难,前者需要区分邻近(nearby)和被遮挡(occluded)的实例(instances)*
对于实例分割的作用有个很直观的理解,在自动驾驶这一领域,系统需要解决的是当前驾驶方向都有些什么物体(人、车、交通标志……),每一类都有多少数量,都处于什么位置,这样子才能做出正确的驾驶判断,而不是单纯知道前方有“人”就可以了。
解决“实例分割”问题,一个看起来可行的方法是看作是一个结构化输出问题。
既然在“语义分割”中已经可以解决“每个像素点属于什么类别”这一问题,按照同样的处理方式,可以建立关于每个像素点的模型,输出其跟周边所有像素点的关系(刻画当前像素与周边像素是否属于同一个物体),但这样的处理方式最大的问题在于这个结构化输出的维度,达到了“全部像素点数X全部目标的全部个数”。
这里需要再解释一下Fully Convolutional Networks (全卷积神经网络,FCN),其是实现“语义分割”的经典模型。
与经典的CNN在卷积层之后使用全连接层得到固定长度的特征向量进行分类不同,FCN可以接受任意尺寸的输入图像,采用反卷积层对最后一个卷积层的feature map进行上采样, 使它恢复到输入图像相同的尺寸,从而可以对每个像素都产生了一个预测, 同时也保留了原始输入图像中的空间信息, 最后在上采样的特征图上进行逐像素分类。逐像素计算softmax分类的损失, 相当于每一个像素对应一个训练样本。
想要实现“实例分割”,最大的两个问题是:
- One of the main challenges in instance segmentation, as in many other computer vision tasks such as object detection,is occlusion. Many approaches to handle occlusion utilize a form of non-maximal suppression (NMS), which is typically difficult to tune. In cluttered scenes, NMS may suppress the detection results for a heavily occluded object because it has too much overlap with foreground objects.One motivation of this work is to introduce an iterative procedure to perform dynamic NMS, reasoning about occlusion in a top-down manner.
在一般的机器视觉任务中,采用“非极大抑制算法”能取得一个不错的结果,但其非常难调整。如果采用动态非极大抑制(dynamic NMS),可以自上而下地解决遮挡问题。
- A related problem of interest entails counting the instances of an object class in an image.
图像计数问题。
*Our system addresses the dimensionality issue by using a temporal chain that outputs a single instance at a time. It also performs dynamic NMS, using an object that is already segmented to aid in the discovery of an occluded object later in the sequence.
使用时间序列解决维度问题,每次输出一个实例。它还执行动态的非极大抑制算法,使用一个已经被分割的对象来帮助在序列中发现一个可能被遮挡的对象。
*Using an RNN to segment one instance at a time is also inspired by human-like iterative and attentive counting processes. For real-world cluttered scenes, iterative counting with attention will likely perform better than a regression model that operates on the global image level. *
同时使用RNN对一个实例进行分段,也受到类似于人类的迭代和计数过程的启发。对于真实世界的混乱场景,与注意力的迭代计数可能比在全局图像级别上运行的回归模型更好。
Recurrent attention model
原始图片X0(h,w,3)经过预处理(Input pre-processing)成为X(h,w,9),示意图如下:
然后将X输入到模型中,训练模型分为四大部分:
Our proposed model has four major components:
A) an external memory that tracks the state of the segmented objects;
B) a box proposal network responsible for localizing objects of interest;
C) a segmentation network for segmenting image pixels within the box;
D) a scoring network that determines if an object instance has been found, and also decides when to stop.
Part A: External memory
外部存储器,记录了物体的边界信息以及根据已经分割出来的目标决定下一目标的位置,从上方的模型结构图中也能看出C)Seg. net部分跟A) Ext. Mem部分是有回路
- 下标(t)表示当前的timespan,对于CVPPP数据集,timespan固定值15,表示一张图片中最多不超过15片叶子,所以整个模型最多循环15次。当然,模型在Part D) Score net部分还设置了“终止”机制(Termination condition),当得分低于阈值0.5时也会被强制终止,这部分的详细内容在稍后解释。
-
max函数
采用tf.maximum函数,将当前的ct(canvas)跟Part C) Seg. net部分的预测值yt取较大值
这样的处理主要是基于这样的假设,“提供已分割的目标的信息给模型有助于推理出遮挡的目标以及下一个感兴趣的区域”
从直观上也很容易理解,如果ct的初始值为0,如果每次迭代都从0开始,经过同样的层、同样的操作,但进行Part C) 部分的预测时却告诉模型不一样的GT(真实标签,用来判断是否分割正确),这会使得模型长期处于“不学习状态”。
Part B: Box network
这一部分主要包括Conv和LSTM操作以及patch的提取,Conv作为特征提取器,将提取的特征输入到LSTM中进行边框(坐标)预测,选取感兴趣区域。但作者认为直接对整张图片进行多层卷积激活太复杂且低效,而且单纯的pooling是无法保存空间结构信息的。
为了解决这个问题,作者引入了一个概念,“soft-atttention(dynamic pooling)”,柔性注意力(动态池化),通过权重att(可以理解为注意力转移)来提取有用的空间结构信息。
将Conv的输出按L(feature dimension)的维度方向乘以对应的权重a输入LSTM中,具体计算公式如下:
- 其中第一个下标(t)是当前迭代的timespan(默认最大值15),第二个下标t表示的当前LSTM的第t次glimpse(默认最大值5),这两个都应该是属于超参数,是人为设定的
- a在各个位置上的初始权值都是一样的,随着迭代循环次数(glimpses)的增加,会有某一些位置的权值趋近于1,一些趋近于0,从而完成“位置相对重要程度”的划分,再通过这样的a与CNN的输出值进行点乘操作,使得位置重要的响应值得以保留,噪声得以消除
- 取LSTM最终输出的隐含层Zt,end,通过一个线性层处理得到预测的边框的中心坐标和尺寸,具体计算公式如下:
Zt,end是LSTM隐含层状态,shape为(1,49),进行矩阵运算过后,得到的是一个shape为(1,9)的输出,输出的每一维都有特地含义,例如前两维分别储存中心点(x,y)的坐标,第7维存放换算系数(scaling factor)……
Q:这些特地维度存放什么特征是怎么决定的?有什么高深的数学原理?
A:没有,乱来的
当然,这只是笔者的理解,如果有不对的地方希望能被指出。
笔者数学基础不算好,但接触到的机器学习模型貌似都有一个共同的特点,不管中间层是什么,提取了怎样的特征,只需要精心设计一个权重矩阵,通过矩阵运算就能改变输出的shape,然后强行回归学习
而一个好的模型意味着将这样的权重矩阵训练好,用于feature identify
-
Extracting a sub-region 这一部分的工作主要根据DRAW: A Recurrent Neural Network For Image Generation
来实现,下面是原理示意图及计算公式:
利用高斯插值内核将原图X0与Part A) 部分的输出值dt串联起来组成Xt
如上图所示,(gx,gy)表示patch的中心点坐标;stride表示步长,也可以理解为“视野”,控制了这个patch的空间大小(zoom),当步长越大,这个attention patch所覆盖的原图区域就会更多,但分辨率也会更低。
而中心点和步长采用下面的公式也就决定了这个patch在(i,j)位置的位置平均值:
其中Fx和Fy函数采用的是高斯函数,描述的是对应点在原图像位置(a,b)对这个点在patch中(i,j)位置的贡献值,即通过给定位置(a,b),能够计算出这个点在patch中的位置(i,j),从而画出边框。
等等,这样看起来貌似有作弊的嫌疑,一开始给定正确边框某一点(a,b),拿来生成预测边框中对应点(i,j),这跟我明明知道“1+1=2”,偏偏还要跟别人说“1+1+1-1=2”有什么区别?
所以真正的模型是有技巧的,需要单独先预训练Part B) box network,作者在代码中有很好的说明,此时的(a,b)是真实边框的某一点,计算loss用于回归学习。
然后利用预训练好的box net训练整个模型时,这时的(a,b)其实不过是Part B) 部分前馈计算的值(并非真实值),进行联合训练
Part C: Segmentation network
这一部分主要是利用Part B)中输出的Pt作为输入,进行卷积操作提取特征,再进行反卷积(D-CNN)进行原尺寸图像恢复,从而完成在原图像上的物体分割。
反卷积(deconvolution)其实是一个糟糕的名字,正确的叫法应该是“转置卷积(transposed convolution)”
反卷积的数学含义(可逆运算),通过反卷积可以将通过卷积的输出信号,完全还原输入信号。
但在一般的深度卷积神经网络模型中,转置卷积只能还原shape大小(转置运算),目的并不是完全还原value,可以理解为一种温和的upsampling。下面是conv与d-conv的示意图:
在此我们简单对比一下FCN图像分割与GAN图像生成中deconvolution的不同
- FCN中需要deconvolution恢复原图像尺寸,从而完成在原图像上的语义分割/实例分割
- GAN图像生成中,需要利用deconvolution近似恢复原来的value,减低loss,这里面有deconvolution network的learning,inference,optimize等过程(注:deconvolution network 跟 deconvolution 是完全不同的两个东西)
Q:为什么FCN不关心deconvolution 生成的values?这样的values直接输出肯定无法成为一个有正常颜色的物体。
A:FCN用于物体分割,只需要给出分割的边界,并不需要考虑重构图片这个问题。响应值(value)的大小并不决定这个像素点的颜色值,而是决定了其是否属于目标的一部分(利用激活函数很容易就能实现这一点),描述的是位置分布
Part D: Scoring network
这一部分主要是一个sigmod函数的线性激活层,利用Part B) 和 Part C) 部分的输出值,计算图片中有多少个目标,并且给出模型的终止条件(Termination condition):一旦输出分数<0.5,则终止
阈值0.5这个指标怎么确定的?跟loss function有关?
曾一度认为Part D)这一模块的设计是多余的。
后面loss function部分会对Part D)做进一步的描述
Loss functions
总的loss函数包括三部分:
- 匹配损失Ly(matching IoU loss)
- 边框损失Lb(box IoU loss)
- 得分损失Ls<(score cross-entropy loss)
前面两部分其实都是IoU指标值的计算,为方便理解下面的计算公式,这里先补充一下IoU的相关知识:
在目标检测的评价体系中,有一个参数叫做 IoU ,简单来讲就是模型产生的目标窗口和原来标记窗口的交叠率。具体我们可以简单的理解为: 即检测结果(DetectionResult)与 Ground Truth 的交集比上它们的并集,即为检测的准确率 IoU
Matching IoU loss (mIOU)
############################
# Box loss
############################
if fixed_order:
# [B, T] for fixed order.
iou_soft_box = modellib.f_iou(attn_box, attn_box_gt, pairwise=False)
else:
if use_knob:
# [B, T, T] for matching.
iou_soft_box = tf.concat(
1, [tf.expand_dims(iou_soft_box[tt], 1) for tt in range(timespan)])
else:
iou_soft_box = modellib.f_iou(
attn_box, attn_box_gt, timespan, pairwise=True)
# iou_soft_box = modellib.f_iou_pair_new(attn_box, attn_box_gt)
identity_match = modellib.get_identity_match(num_ex, timespan, s_gt)
if fixed_order:
match_box = identity_match
else:
match_box = modellib.f_segm_match(iou_soft_box, s_gt)
model['match_box'] = match_box
match_sum_box = tf.reduce_sum(match_box, reduction_indices=[2])
match_count_box = tf.reduce_sum(match_sum_box, reduction_indices=[1])
match_count_box = tf.maximum(1.0, match_count_box)
# [B] if fixed order, [B, T] if matching.
if fixed_order:
iou_soft_box_mask = iou_soft_box
else:
iou_soft_box_mask = tf.reduce_sum(iou_soft_box * match_box, [1])
iou_soft_box = tf.reduce_sum(iou_soft_box_mask, [1])
iou_soft_box = tf.reduce_sum(iou_soft_box / match_count_box) / num_ex_f
if box_loss_fn == 'mse':
box_loss = modellib.f_match_loss(
attn_params,
attn_params_gt,
match_box,
timespan,
modellib.f_squared_err,
model=model)
elif box_loss_fn == 'huber':
box_loss = modellib.f_match_loss(attn_params, attn_params_gt, match_box,
timespan, modellib.f_huber)
elif box_loss_fn == 'iou':
box_loss = -iou_soft_box
elif box_loss_fn == 'wt_cov':
box_loss = -modellib.f_weighted_coverage(iou_soft_box, attn_box_gt)
elif box_loss_fn == 'bce':
box_loss_fn = modellib.f_match_loss(y_out, y_gt, match_box, timespan, f_bce)
else:
raise Exception('Unknown box_loss_fn: {}'.format(box_loss_fn))
model['box_loss'] = box_loss
box_loss_coeff = tf.constant(1.0)
model['box_loss_coeff'] = box_loss_coeff
tf.add_to_collection('losses', box_loss_coeff * box_loss)
Soft box IoU loss
##############################
# Segmentation loss
##############################
# IoU (soft)
iou_soft_pairwise = modellib.f_iou(y_out, y_gt, timespan, pairwise=True)
real_match = modellib.f_segm_match(iou_soft_pairwise, s_gt)
if fixed_order:
iou_soft = modellib.f_iou(y_out, y_gt, pairwise=False)
match = identity_match
else:
iou_soft = iou_soft_pairwise
match = real_match
model['match'] = match
match_sum = tf.reduce_sum(match, reduction_indices=[2])
match_count = tf.reduce_sum(match_sum, reduction_indices=[1])
match_count = tf.maximum(1.0, match_count)
# Weighted coverage (soft)
wt_cov_soft = modellib.f_weighted_coverage(iou_soft_pairwise, y_gt)
model['wt_cov_soft'] = wt_cov_soft
unwt_cov_soft = modellib.f_unweighted_coverage(iou_soft_pairwise, match_count)
model['unwt_cov_soft'] = unwt_cov_soft
# [B] if fixed order, [B, T] if matching.
if fixed_order:
iou_soft_mask = iou_soft
else:
iou_soft_mask = tf.reduce_sum(iou_soft * match, [1])
iou_soft = tf.reduce_sum(iou_soft_mask, [1])
iou_soft = tf.reduce_sum(iou_soft / match_count) / num_ex_f
model['iou_soft'] = iou_soft
if segm_loss_fn == 'iou':
segm_loss = -iou_soft
elif segm_loss_fn == 'wt_cov':
segm_loss = -wt_cov_soft
elif segm_loss_fn == 'bce':
segm_loss = f_match_bce(y_out, y_gt, match, timespan)
else:
raise Exception('Unknown segm_loss_fn: {}'.format(segm_loss_fn))
model['segm_loss'] = segm_loss
segm_loss_coeff = tf.constant(1.0)
tf.add_to_collection('losses', segm_loss_coeff * segm_loss)
Monotonic score loss
####################
# Score loss
####################
conf_loss = modellib.f_conf_loss(s_out, match, timespan, use_cum_min=True)
model['conf_loss'] = conf_loss
tf.add_to_collection('losses', loss_mix_ratio * conf_loss)
以上是笔者对这篇paper的一些粗略理解,有些地方也还不是很理解,后续会继续补充。有任何不对的地方,欢迎在文章下方留言指出,也可直接电邮1643206826@qq.com,欢迎讨论改正,谢谢。