本次百度论文复现挑战赛题目27 PointRend: Image Segmentation as Rendering(CVPR2020)”的复现得到了百度飞浆平台和工作人员的大力支持,非常感谢!
该论文是计算机视觉顶级会议CVPR2020的会议论文,论文提出了一种新的方法,可以对物体和场景进行有效的高质量图像分割。具体来讲,核心idea采用了将图像分割作为渲染问题的独特视角。从这个角度出发,论文提出了PointRend(基于点的渲染)神经网络模块:该模块基于迭代细分算法在自适应选择的位置执行基于点的分割预测。通过在现有最新模型的基础上构建,PointRend可以灵活地应用于实例和语义分割任务。 定性地结果分析表明,PointRend在先前方法过度平滑的区域中输出清晰的对象边界。定量来看,无论是实例还是语义分割,PointRend都在COCO和Cityscapes上产生了有效的性能提升。
从原始论文的数据来看,该论文提出的PointRend模块作为可附加的模块提升现有网络的分割结果。同时论文提供了开源的代码,是一篇值得复现且很有参考价值的论文。
- 复现结果:
采用80000 iter,batch_size=16 for 4 GPUs(4 imgs for per gpu),base_lr=0.01 warmup+poly的学习率策略(原论文8块GPU,batchsize 32),SemanticFPN+PointRend with ResNet101模型在Cityscaps VAL数据集上达到了78.78(论文78.5)的mIOU。。
在复现的过程中,也遇到了一些比较问题。现我将其整理如下:
原始的torch代码:
refined_seg_logits = refined_seg_logits.reshape(batch_size, channels, height * width)
refined_seg_logits = refined_seg_logits.scatter_(2, point_indices, point_logits)
这里的scatter_的第一个形参是维度,也就是将point_logits按point_indices在refined_seg_logits的哪个维度上进行赋值。这里显然是在height * width这个维度。
而paddle的scatter函数:
paddle.scatter(x, index, updates, overwrite=True, name=None)
显然并没有提供这个在维度上按索引赋值的功能,而只有三个输入,一个是x也就是我们上面的待修改的refined_seg_logits,index自然是对应point_indices,updates对应point_logits。
为了解决这个问题,不得不自己想办法。这里的解决办法是对point_indices进行重新构造。也就是将待改的refined_seg_logits和对应的point_indices、point_logits都进行展平操作,变成符合paddle.scatter输入的形式。
最后再reshape回原来的样子。具体代码在pointrendseg.py中的scatter_paddle函数。
def scatter_paddle(refined_seg_logits, point_indices, point_logits):
"""
scatter paddle version: equal to pytorch version scatter(2,point_indices,point_logits)
:param refined_seg_logits:shape=[batch_size, channels, height * width]
:param point_indices:
:param point_logits:
:return:
"""
original_shape = refined_seg_logits.shape # [batch_size, channels, height * width]
new_refined_seg_logits = refined_seg_logits.flatten(0, 1) # [N*C,H*W]
offsets = (
paddle.arange(new_refined_seg_logits.shape[0]) * new_refined_seg_logits.shape[1]
).unsqueeze(-1) # [N*C,1]
point_indices = point_indices.flatten(0, 1) # [N*C,H*W]
new_point_indices = (point_indices + offsets).flatten()
point_logits = point_logits.flatten() # [N*C*H*W]
refined_seg_logits = paddle.scatter(
refined_seg_logits.flatten(),
new_point_indices,
point_logits,
overwrite=True)
return refined_seg_logits.reshape(shape=original_shape)
2、loss计算问题:一个是point标签从seg_label构造;一个是paddle中提供的cross_entropy_loss默认只支持[b,c,h,w]形式的loss计算。对于文中采用的用于计算point[b,h*w,2]的loss需要对原有loss进行改造。
这部分改起来其实也比较简单: 主要对cross_entropy_loss前面的代码进修改就行,如下:
# 原始cross_entropy_loss
channel_axis = 1 if self.data_format == 'NCHW' else -1
if self.weight is not None and logit.shape[channel_axis] != len(
self.weight):
raise ValueError(
'The number of weights = {} must be the same as the number of classes = {}.'
.format(len(self.weight), logit.shape[1]))
logit = paddle.transpose(logit, [0, 2, 3, 1])
# for point cross_entropy_loss
logit, points = logits # [N, C, point_num],[N, point_num, 2]
label = label.unsqueeze(1) # [N,1,H,W] #seg_label
label = point_sample( #TODO
label.astype('float32'),
points,
mode='nearest',
align_corners=self.align_corners) # [N, 1, point_num]
label = paddle.squeeze(label,axis=1).astype('int64') # [N, 2048]
channel_axis = 1 if self.data_format == 'NCHW' else -1
if self.weight is not None and logit.shape[channel_axis] != len(
self.weight):
raise ValueError(
'The number of weights = {} must be the same as the number of classes = {}.'
.format(len(self.weight), logit.shape[1]))
logit = paddle.transpose(logit, [0, 2, 1])
#torch
idx += shift[:, None]
#paddle
idx += shift.unsqueeze([-1]) #idx.shape=[xxx,1]
# torch:
point_coords = point_coords[idx, :] # point_coords.shape=[xxx,2],idx.shape=[xx]
#paddle
point_coords = paddle.index_select(point_coords, idx, axis=0)
以上主要是一些印象比较深刻的问题和对应解决方案。其他问题就基本上是靠查API和重构代码结构就能解决的问题了。
总结下来: 主要关键还是原文采用的框架下的模型和loss是耦合的,也就是写在模型里面的。但是为了更加清晰的梳理模型和利用paddleseg工具,这里的复现对其进行了拆分。
Reference:
author info : xbchen(email:joyful_chen@163.com) date: 2021-08-28