论文链接:https://arxiv.org/abs/2009.07641
论文代码:https://github.com/xxcheng0708/BSNPlusPlus-boundary-sensitive-network(非官方!)
原作者Haisheng Su解析:https://zhuanlan.zhihu.com/p/344065976
注意:论文没有官方的源码开源,下文中的代码都是摘录至非官方实现版本,精度与原文还是有不少差距。代码可能存在出入,仅供参考!!!
前情回顾和引入
问题定义: 在Temporal Action Proposal Generation(时序动作提名生成)任务中,对于一段未剪辑的视频 ,其中 代表视频 的第t个RGB帧, 是视频 的总帧数,有时间动作实例集合 作为注释,其中 和 分别代表动作实例 的开始和结束时间。我们希望预测得到的动作提名集合 能够尽可能地覆盖真实集合 。
BSN++文如其名,是对ECCV 2018上的BSN方法进行的改进。值得一提的是,BSN++的作者Haisheng Su也是BSN的原作者之一。此外,文章虽说名为BSN++,但是也能看见不少BMN的影子。BMN和BSN++这两篇文章也算是BSN的“正统”续作了。无论从网络的整体流程,数据的处理细节,还是从论文的写作结构上来看,三篇文章都有很强的相似性。可以对照着来阅读,效果更佳。
现有的时序动作提名生成方法可以大致分为两大类:
Top-down(自上而下):基于滑动窗口或者预先定义的anchors来生成proposals,再使用二分类器去评估proposal的置信度。以这种方式产生的proposal在持续时间范围上缺乏灵活性,并会产生不精确的边界。
Bottom-Up(自下而上):以BSN为代表的方法,为每个时间位置生成动作概率,通过动作概率的组合生成proposals,并通过提名评估模块为每个proposal生成置信度分数。最终能够得到具有灵活持续时间和可靠置信度得分的动作提名。
BSN作为Bottom-Up流派中的开创性工作,也存在着一些问题。其中部分问题已经在BMN中得到了解决:
BSN中提名特征构建和置信度评估过程无法并行地对Proposals处理:BMN引入了边界匹配机制来评估密集分布的提名的置信度。
BSN中构造的Proposal特征过于简单,无法很好地捕捉时间上下文:BMN中的BM feature map包含了所有可能的proposal的特征,可以利用卷积更好地聚合相邻提名信息 。
BSN是多阶段而非统一的框架:BMN利用Base Module作为共享主干,在TEM和PEM两个分支中同时生成时间边界概率序列和BM置信图,进行端到端的联合训练。
但是,还有一些问题依然没有得到关注和解决:
BSN只使用了边界附近的局部细节来生成边界概率,而没有利用整个视频序列中丰富的时间上下文。
BSN在置信度评估中没有考虑提名与提名之间的关系。
BSN中忽略了proposals中的存在的正负样本和持续时间分布不均衡问题。
BSN++针对上述问题提出了相应的解决方案:
提出了一个互补边界生成器,以利用“局部和全局”、“过去和未来”上下文进行精确的时间边界预测。
提出了一个用于提名可信度评估的关系模块,采用两个自注意力模块从两个互补的方面对提名关系进行建模。
提出了一个两阶段重采样方案,包括 IoU-balanced (positive-negative)采样scale-balanced采样。
Video
Feature Encoding
图1.Video Feature Encoding
数据集设置和特征编码方面,BSN++沿用了BMN的设置。前面BMN的解读中有详细的说明,这里不再赘述。
BSN++
图2.BSN++
BSN++由三个模块组成:
1.Base Net: 与BMN中的Base Module一致:增大时间感受野,提供共享主干特征。
2.Complementary Boundary Generator: 使用嵌套的U形编码器-解码器结构,从正向和反向两个角度处理输入视频特征以生成边界概率序列。
3.Proposal Relation Block: 通过两个负责互补依赖关系的自注意模块对提名关系进行建模,生成每个提名对应的置信度分数。
接下来分别对这三部分进行介绍,并从整体上对训练和推断过程进行分析。
Base Net
实现同BMN,Base Module通过两个核为3的1D卷积为后续两个分支(互补边界生成器和提名关系模块)提供共享特征。对于每一个window,其输出大小为 的特征。
# feat_dim: 400
# hidden_dim_1d: 128
self.x_1d_b = nn.Sequential(
nn.Conv1d(self.feat_dim, self.hidden_dim_1d,
kernel_size=3, padding=1, groups=4),
nn.BatchNorm1d(self.hidden_dim_1d),
nn.ReLU(inplace=True),
nn.Conv1d(self.hidden_dim_1d, self.hidden_dim_1d,
kernel_size=3, padding=1, groups=4),
nn.BatchNorm1d(self.hidden_dim_1d),
nn.ReLU(inplace=True)
)
经过BaseNet后,分为两个分支,分别为Complementary Boundary Generator和Proposal Relation Block。
Complementary Boundary Generator
图3.CBG模块
无论是BSN还是BMN中,都只使用了简单的卷积串联来生成边界概率,这样只能关注到边界附近的局部细节。为了能够更好地捕获高级的全局上下文和低级的局部细节,作者从U-Net结构中得到启发,将边界生成器设计为NestedUNet编码器-解码器网络,如上图左下所示。具体而言,每个圆圈表示具有512个filter且步长1,内核大小3的1D卷积,以及相应的BN和ReLU层。
通过两个下采样层来扩展感受野,然后使用相同数量的上采样层来恢复原始时间分辨率。圆圈之间的虚线表示skip connection,用于缩小编码器和解码器间的语义gap。其中下采样通过nn.MaxPool1d实现,上采样通过nn.Upsample实现。最后 的输出大小为 。通过Sigmoid将范围限制在[0, 1],得到两个大小为T的概率序列,分别对应动作起始概率和动作结束概率。
NestedUNet代码如下:
class ConvUnit(nn.Module):
"""
BSN++中CBG模块的UNet中的每个单元unit
"""
def __init__(self, in_ch, out_ch, is_output=False):
super(ConvUnit, self).__init__()
# out_ch: 2
module_list = [nn.Conv1d(in_ch, out_ch, kernel_size=3,
stride=1, padding=1, bias=True)]
if is_output is False:
module_list.append(nn.BatchNorm1d(out_ch))
module_list.append(nn.ReLU(inplace=True))
self.conv = nn.Sequential(*module_list)
def forward(self, x):
x = self.conv(x)
return x
class NestedUNet(nn.Module):
"""
UNet - Basic Implementation
Paper : https://arxiv.org/abs/1505.04597
"""
def __init__(self, in_ch=400, out_ch=2):
super(NestedUNet, self).__init__()
self.pool = nn.MaxPool1d(kernel_size=2, stride=2)
self.up = nn.Upsample(scale_factor=2)
n1 = 512
filters = [n1, n1 * 2, n1 * 3]
# UNet的第一列
self.conv0_0 = ConvUnit(in_ch, filters[0], is_output=False)
self.conv1_0 = ConvUnit(filters[0], filters[0], is_output=False)
self.conv2_0 = ConvUnit(filters[0], filters[0], is_output=False)
# UNet的第二列
self.conv0_1 = ConvUnit(filters[1], filters[0], is_output=False)
self.conv1_1 = ConvUnit(filters[1], filters[0], is_output=False)
# UNet的第三列
self.conv0_2 = ConvUnit(filters[2], filters[0], is_output=False)
# 输出,红点位置
self.final = nn.Conv1d(filters[0] * 3, out_ch, kernel_size=1)
# self.final = ConvUnit(filters[0] * 3, out_ch, is_output=True)
self.out = nn.Sigmoid()
def forward(self, x):
# x: (B, C, T)
x0_0 = self.conv0_0(x)
x1_0 = self.conv1_0(self.pool(x0_0))
x0_1 = self.conv0_1(torch.cat([x0_0, self.up(x1_0)], 1))
x2_0 = self.conv2_0(self.pool(x1_0))
x1_1 = self.conv1_1(torch.cat([x1_0, self.up(x2_0)], 1))
x0_2 = self.conv0_2(torch.cat([x0_0, x0_1, self.up(x1_1)], 1))
# 同步输出正向和反向的中间特征,用于计算MSELoss
out_feature = torch.cat([x0_0, x0_1, x0_2], 1)
final_feature = self.final(out_feature)
out = self.out(final_feature)
return out, out_feature
除此之外,作者认为可以进一步利用视频的时序信息来加强边界概率生成器:在以往的工作中,通常只使用顺序的序列特征来分别生成动作开始和结束概率。其中,起始分类器可以检测到背景到动作的突变,结束分类器可以检测到动作到背景的突变。作者敏锐地察觉到,若将视频序列顺序倒置,动作开始和结束边界就发生了互换,那么原来的起始分类器也可以作为“伪结束分类器”,提供了互补的预测结果。
如上图左上所示,forward和backward编码器-解码器结构的权重共享。为了更好的进行优化,在正向和反向的中间特征之间使用MSELoss,进行一致性约束。视频序列顺序翻转可以通过以下代码实现:
# input data: (B, C, T)
input_data_backward = torch.flip(input_data, dims=(2,))
推断时,forward编码器-解码器结构会产生顺序序列的起始热图 和结束热图 , 代表序列长度。同理,backward编码器-解码器结构会产生倒序序列的起始热图 和结束热图 。将对应项进行匹配得到最终的热图(大小皆为 ):
得到每个时间位置的起始和结束概率之后,可以将每个开始和结束位置的概率进行两两相乘,类似于BMN中的做法,构建出密集的边界图 ,如上图右下所示。横轴代表每个开始位置,最大取值为T;纵轴代表持续时间,最大取值为D;灰色区域为结束位置超出设定时间长度范围的部分。具体而言,边界图中的每一点 包含了对应proposal 的信息:
总结:Complementary Boundary Generator解决了最开始提到的BSN的第一个缺陷。采用了类似UNet的结构,捕获更加丰富的局部和全局上下文信息。此外,作者巧妙地使用逆序输入构建互补边界概率生成器,使生成的概率更加可靠。
Proposal Relation Block
图4.PRB模块
介绍完BSN++的左边分支,再来看看右边分支的“提名关系模块“。其目的是为每个proposal生成置信度分数。首先,同BMN中的右边分支一样,经过BM-Layer处理。其利用Base Net的输出特征 ,与预先生成的采样权重矩阵 相乘,得到proposal均匀采样后的BM feature map
,即上图中的黄色方块所示。其中通道数目C为128,采样数目N为32。
从整体结构上看,首先BM-layer的输出经过一个核为(N, 1, 1),步长为(N, 1, 1),filter数目为512的3D卷积,综合每个proposal所有N个采样点的信息,得到 。接着分为两个分支,分别执行空间感知注意力和通道感知注意力。在进入block前,两个分支利用核为(1, 1)的2D卷积,将通道数目减小到128。经过注意力机制处理后,两个block的输出都通过2D卷积进一步表达。除了单独的分支输出外,还将两个分支的输出相加,经过一个额外的2D卷积。最终,得到三个不同位置输出的Confidence map。类似于BMN,每个Confidence map又包含两个大小为 的map,分别使用回归损失和分类损失进行训练。
Proposal Relation Block的整体实现代码:
def conv_block(in_ch, out_ch, kernel_size=3, stride=1, bn_layer=False, activate=False):
module_list = [nn.Conv2d(in_ch, out_ch, kernel_size, stride, padding=1)]
if bn_layer:
module_list.append(nn.BatchNorm2d(out_ch))
module_list.append(nn.ReLU(inplace=True))
if activate:
module_list.append(nn.Sigmoid())
conv = nn.Sequential(*module_list)
return conv
class ProposalRelationBlock(nn.Module):
def __init__(self, in_channels, inter_channles=128, sub_sample=False):
super(ProposalRelationBlock, self).__init__()
# Position-Aware Attention Module
self.p_net = PositionAwareAttentionModule(in_channels, inter_channels=
inter_channles, sub_sample=sub_sample, dimension=2)
# Channel-Aware Attention Module
self.c_net = ChannelAwareAttentionModule(in_channels, inter_channels=
inter_channles, dimension=2)
self.conv0_0 = conv_block(in_channels, in_channels, 3, 1,
bn_layer=True, activate=False)
self.conv0_1 = conv_block(in_channels, in_channels, 3, 1,
bn_layer=True, activate=False)
self.conv1 = conv_block(in_channels, in_channels, 3, 1,
bn_layer=True, activate=False)
self.conv2 = conv_block(in_channels, 2, 3, 1, bn_layer=False, activate=True)
self.conv3 = conv_block(in_channels, 2, 3, 1, bn_layer=False, activate=True)
self.conv4 = conv_block(in_channels, in_channels, 3, 1,
bn_layer=True, activate=False)
self.conv5 = conv_block(in_channels, 2, 3, 1, bn_layer=False, activate=True)
def forward(self, x):
# x: (B, C, D, T)
x_p = self.conv0_0(x)
x_c = self.conv0_1(x)
x_p = self.p_net(x_p)
x_c = self.c_net(x_c)
x_p_0 = self.conv1(x_p)
x_p_1 = self.conv2(x_p_0)
x_c_0 = self.conv4(x_c)
x_c_1 = self.conv5(x_c_0)
x_p_c = self.conv3(x_p_0 + x_c_0)
# x_out = (x_p_1 + x_c_1 + x_p_c) / 3
return x_p_1, x_c_1, x_p_c # 对应上图中的三个蓝色圆点处的输出
接下来再分别看看两个block的实现细节。
Position-Aware Attention Module:类似于Non-local中的做法,构建一个HW维度的self-attention。首先将大小为 的A矩阵和大小为 的B矩阵相乘得到大小为 的position-aware attention,其中 , 表示第i个位置在第j个位置上的注意力大小:
经过Softmax处理后,将得到的权重矩阵 与输入 相乘,实现对应位置特征加权求和。最终得到大小为 的输出,通过Skip Connection与输入特征求和。
Channel-Aware Attention Module:类似于Position-Aware的做法,只不过将维度交换一下,对C维度执行注意力操作,以捕获不同的依赖关系。
空间注意力和通道注意力模块实现代码:
class PositionAwareAttentionModule(nn.Module):
def __init__(self, in_channels, inter_channels=None, sub_sample=None, dimension=2):
super(PositionAwareAttentionModule, self).__init__()
self.sub_sample = sub_sample
self.in_channels = in_channels
self.inter_channels = inter_channels
self.dimension = dimension
if self.inter_channels is None:
self.inter_channels = in_channels // 2
if self.inter_channels == 0:
self.inter_channels = 1
if self.dimension == 2:
conv_nd = nn.Conv2d
max_pool_layer = nn.MaxPool2d(kernel_size=(2, 2))
bn = nn.BatchNorm2d
else:
conv_nd = nn.Conv1d
max_pool_layer = nn.MaxPool1d(kernel_size=(2,))
bn = nn.BatchNorm1d
self.g = nn.Sequential(
conv_nd(in_channels=self.in_channels, out_channels=self.inter_channels,
kernel_size=1, stride=1, padding=0),
bn(self.inter_channels),
nn.ReLU(inplace=True)
)
self.theta = nn.Sequential(
conv_nd(in_channels=self.in_channels, out_channels=self.inter_channels,
kernel_size=1, stride=1, padding=0),
bn(self.inter_channels),
nn.ReLU(inplace=True)
)
self.phi = nn.Sequential(
conv_nd(in_channels=self.in_channels, out_channels=self.inter_channels,
kernel_size=1, stride=1, padding=0),
bn(self.inter_channels),
nn.ReLU(inplace=True)
)
self.W = nn.Sequential(
conv_nd(in_channels=self.inter_channels, out_channels=self.in_channels,
kernel_size=1, stride=1, padding=0),
bn(self.in_channels)
)
if self.sub_sample:
self.g = nn.Sequential(self.g, max_pool_layer)
self.phi = nn.Sequential(self.phi, max_pool_layer)
def forward(self, x):
# x: (B, C, D, T)
batch_size = x.size(0)
# value
g_x = self.g(x).view(batch_size, self.inter_channels, -1)
g_x = g_x.permute(0, 2, 1) # (B, D*T, C)
# query
theta_x = self.theta(x).view(batch_size, self.inter_channels, -1)
theta_x = theta_x.permute(0, 2, 1) # (B, D*T, C)
# key
phi_x = self.phi(x).view(batch_size, self.inter_channels, -1) # (B, C, D*T)
f = torch.matmul(theta_x, phi_x)
f = F.softmax(f, dim=2) # (B, D*T, D*T)
y = torch.matmul(f, g_x) # (B, D*T, C)
y = y.permute(0, 2, 1).contiguous() # (B, C, D*T)
y = y.view(batch_size, self.inter_channels, *x.size()[2:]) # (B, C, D, T)
y = self.W(y)
z = y + x
return z
class ChannelAwareAttentionModule(nn.Module):
def __init__(self, in_channels, inter_channels=None, dimension=2):
super(ChannelAwareAttentionModule, self).__init__()
self.in_channels = in_channels
self.inter_channels = inter_channels
self.dimension = dimension
if self.inter_channels is None:
self.inter_channels = in_channels // 2
if self.inter_channels == 0:
self.inter_channels = 1
if self.dimension == 2:
conv_nd = nn.Conv2d
max_pool_layer = nn.MaxPool2d(kernel_size=(2, 2))
bn = nn.BatchNorm2d
else:
conv_nd = nn.Conv1d
max_pool_layer = nn.MaxPool1d(kernel_size=(2,))
bn = nn.BatchNorm1d
self.g = nn.Sequential(
conv_nd(in_channels=self.in_channels, out_channels=self.inter_channels,
kernel_size=1, stride=1, padding=0),
bn(self.inter_channels),
nn.ReLU(inplace=True)
)
self.theta = nn.Sequential(
conv_nd(in_channels=self.in_channels, out_channels=self.inter_channels,
kernel_size=1, stride=1, padding=0),
bn(self.inter_channels),
nn.ReLU(inplace=True)
)
self.phi = nn.Sequential(
conv_nd(in_channels=self.in_channels, out_channels=self.inter_channels,
kernel_size=1, stride=1, padding=0),
bn(self.inter_channels),
nn.ReLU(inplace=True)
)
self.W = nn.Sequential(
conv_nd(in_channels=self.inter_channels, out_channels=self.in_channels,
kernel_size=1, stride=1, padding=0),
bn(self.in_channels)
)
def forward(self, x):
# x: (B, C, D, T)
batch_size = x.size(0)
g_x = self.g(x).view(batch_size, self.inter_channels, -1) # (B, C, D*T)
theta_x = self.theta(x).view(batch_size, self.inter_channels, -1) # (B, C, D*T)
phi_x = self.phi(x).view(batch_size, self.inter_channels, -1)
phi_x = phi_x.permute(0, 2, 1) # (B, D*T, C)
f = torch.matmul(theta_x, phi_x)
f = F.softmax(f, dim=2) # (B, C, C)
y = torch.matmul(f, g_x) # (B, C, D*T)
# y = y.permute(0, 2, 1).contiguous() # (B, D*T, C)
y = y.view(batch_size, self.inter_channels, *x.size()[2:]) # (B, C, D, T)
y = self.W(y)
z = y + x
return z
总结:Proposal Relation Block解决了BSN的第二个缺陷,对提名与提名之间的关系进行了建模。相比于BMN中使用简单的卷积来捕获关系,BSN++中进一步采用了更为强大的注意力模块,分别对D*T维度和C维度进行处理,从两个角度提供互补信息。
Re-sampling
图5.样本不均衡问题
IoU-balanced-sampling:置信度预测的训练过程中,存在样本不均衡的问题。在以往的工作中,通常考虑的是正负样本数目的不均衡问题,如上图中的黄色和蓝色曲线所示。为了缓解该问题,往往会设计一个IoU-balanced-sampler。即基于proposal与ground truth的IoU大小,对正负样本进行采样,实现数目平衡。但如上图(b)中的所示,即使经过了IoU-balanced-sampling处理,在proposals尺度的分布上仍然存在着明显的不均衡现象。这就需要进行第二阶段的尺度平衡重采样。
Scale-balanced re-sampling:以正样本的处理为例,先定义归一化后的尺度区间 ,如[0-0.3, 0.3-0.7, 0.7-1.0]。令 为区间 内的正样本数目, 为区间数目,则每个区间的正样本比率 可以记为:
我们期望正样本比率小的区间能够采样更多的样本。因此,对 进行重新计算:
超参数 设置为0.15。即但原始正样本比率小于0.15时进行放大,大于0.15时保持不变。观察该指数函数,当 时,采样比率下限为 ;当 时,采样比率上限为 。负样本的操作同上。重采样后的效果如上图(c)中所示。
总结:经过两阶段的采样,不仅实现了正负样本的平衡,也实现了proposals持续时间范围的平衡。
Training
and Inference
Training
从整体上看,训练损失可以分为三部分:
为左边分支互补边界生成器的损失, 为右边分支提名关系模块的损失, 为正则项。 和 分别取值为10和0.0001。
CBG模块的标签生成方式同BSN的TEM模块:取每个位置各自的region与该window内action instance的开始和结束区域计算重叠程度,并进行阈值筛选,分为正负样本。开始和结束标签可以表示为 。CBG模块的损失可以写作:
其中 同BSN,表示二元逻辑回归损失。前两项表示互补边界生成器的正向输出损失,第三四项表示反向输出损失,最后一项表示正向特征和反向特征间的Mean-Squared Loss。
代码如下:
def bi_loss(pred_score, gt_label):
"""
二元逻辑回归损失
"""
pred_score = pred_score.view(-1)
gt_label = gt_label.view(-1)
pmask = (gt_label > 0.5).float()
num_entries = len(pmask)
num_positive = torch.sum(pmask)
ratio = num_entries / num_positive
coef_0 = 0.5 * ratio / (ratio - 1)
coef_1 = 0.5 * ratio
epsilon = 0.000001
loss_pos = coef_1 * torch.log(pred_score + epsilon) * pmask
loss_neg = coef_0 * torch.log(1.0 - pred_score + epsilon) * (1.0 - pmask)
loss = -1 * torch.mean(loss_pos + loss_neg)
return loss
def cbg_loss_func(pred_start, pred_end, gt_start, gt_end):
"""
CBG模块计算开始和结束边界的概率损失
:param pred_start:
:param pred_end:
:param gt_start:
:param gt_end:
:return:
"""
loss_start = bi_loss(pred_start, gt_start)
loss_end = bi_loss(pred_end, gt_end)
loss = loss_start + loss_end
return loss
def cbg_feature_loss(forward_feature, backward_feature):
"""
计算CBG模块对于视频特征序列前向和反向计算得到的中间特征的MSE损失
:param forward_feature:
:param backward_feature:
:return:
"""
loss = F.mse_loss(forward_feature, backward_feature, reduction="mean")
return loss
# loss
cbg_loss_forward = cbg_loss_func(pred_start, pred_end, gt_start, gt_end)
cbg_loss_backward = cbg_loss_func(torch.flip(pred_end_backward, dims=(1,)),
torch.flip(pred_start_backward, dims=(1,)),
gt_start, gt_end)
inter_feature_loss = cbg_feature_weight * cbg_feature_loss(feature_forward,
torch.flip(feature_backward, dims=(2,)))
cbg_loss = cbg_loss_forward + cbg_loss_backward + inter_feature_loss
PRB模块的标签生成方式同BMN的PEM模块:将BM map中每个位置对应的proposal与所有真实proposals计算IoU,并取其中最大值作为该proposal的置信度标签。可以表示为 。同BMN中的做法,对生成的两个回归和分类置信度分别计算损失,PRB模块的损失可以写作:
回归损失使用smooth-L1,分类损失使用二分类损失,需经过前面所述的两阶段采样策略处理。
代码:
# p, c, p_c分别对应PRB中HW分支,C分支和两个分支融合后的输出
# regression loss
prb_reg_loss_p = bsnpp_pem_reg_loss_func(pred_bm_p[:, 0].contiguous(),
gt_iou_map, bm_mask)
prb_reg_loss_c = bsnpp_pem_reg_loss_func(pred_bm_c[:, 0].contiguous(),
gt_iou_map, bm_mask)
prb_reg_loss_p_c = bsnpp_pem_reg_loss_func(pred_bm_p_c[:, 0].contiguous(),
gt_iou_map, bm_mask)
prb_reg_loss = prb_reg_weight * (prb_reg_loss_p + prb_reg_loss_c + prb_reg_loss_p_c)
# classification loss
prb_cls_loss_p = bsnpp_pem_cls_loss_func(pred_bm_p[:, 1].contiguous(),
gt_iou_map, bm_mask)
prb_cls_loss_c = bsnpp_pem_cls_loss_func(pred_bm_c[:, 1].contiguous(),
gt_iou_map, bm_mask)
prb_cls_loss_p_c = bsnpp_pem_cls_loss_func(pred_bm_p_c[:, 1].contiguous(),
gt_iou_map, bm_mask)
prb_cls_loss = prb_cls_loss_p + prb_cls_loss_c + prb_cls_loss_p_c
# PRB loss
prb_loss = prb_weight_forward * (prb_reg_loss + prb_cls_loss)
Inference
在推理过程中,左边CBG分支会分别生成起始和结束边界概率 和 ,边界位置概率两两相乘后可以得到boundary map 。注意,不同于BMN中TEM分支的做法,BSN++的CBG分支直接保留了所有 个proposal,而不利用概率筛选开始位置和结束位置。右边PRB分支会生成回归和分类confidence map 和 ,最后将两个分支的结果进行融合:
CBG:
PRB:
Fusion:
代码:
for idx, input_data in tqdm.tqdm(test_loader, total=len(test_loader)):
video_name = test_loader.dataset.video_list[idx[0]]
input_data = input_data.cuda()
confidence_map_p, confidence_map_c, confidence_map_p_c, \
start_forward, end_forward, _ = model(input_data)
confidence_map = (confidence_map_p + confidence_map_c + confidence_map_p_c) / 3
input_data_backward = torch.flip(input_data, dims=(2,))
_, _, _, start_backward, end_backward, _ = model(input_data_backward)
# Hs
start = torch.sqrt(start_forward * torch.flip(end_backward, dims=(1,)))
# He
end = torch.sqrt(end_forward * torch.flip(start_backward, dims=(1,)))
start_scores = start[0].detach().cpu().numpy()
end_scores = end[0].detach().cpu().numpy()
# Mcc
clr_confidence = (confidence_map[0][1]).detach().cpu().numpy()
# Mcr
reg_confidence = (confidence_map[0][0]).detach().cpu().numpy()
# 遍历起始分界点与结束分界点的组合
new_props = []
for idx in range(tscale):
for jdx in range(tscale):
start_index = idx
end_index = jdx + 1
if start_index < end_index and end_index < tscale:
xmin = start_index / tscale
xmax = end_index / tscale
xmin_score = start_scores[start_index]
xmax_score = end_scores[end_index]
clr_score = clr_confidence[idx, jdx]
reg_score = reg_confidence[idx, jdx]
score = xmin_score * xmax_score * clr_score * reg_score
new_props.append([xmin, xmax, xmin_score, xmax_score,
clr_score, reg_score, score])
new_props = np.stack(new_props)
SoftNMS: 同BSN和BMN,Fusion得到的候选提名集合 ,经过SoftNMS筛选后生成 。其中 表示衰减后的分数。
实验
与SOTA比较
ActivityNet-1.3上的AR@AN和AUC结果:如表1,相比BSN的结果有较大提升,也优于BMN。
表1.ActivityNet-1.3结果
THUMOS14上的AR@AN结果:如表2,BSN++在不同的输入特征类型和不同的AR@AN指标下,皆明显优于之前的结果。
表2.THUMOS14结果
消融实验
BSN++中模块的有效性和效率以及与BSN的比较:消融结果如下表所示。BBM,SBS,PAM和CAM分别代表:双向匹配,尺度均衡采样,HW自注意力和C自注意力。可以看出,即使不使用双向匹配机制的CBG模块也能获得比BSN和BMN中TEM模块更好的效果。这说明编码器-解码器结构能够有效地学习“局部和全局”上下文以进行精确的边界预测。双向匹配机制进一步验证了未来信息在辅助边界判断中的重要性。同时,SBS方法和自注意力机制能够帮助实现更好的置信度预测,end-to-end结构也能够实现效果的提升。最后,相比于BSN和BMN,BSN+进一步提升了推断速度。
表3.模块消融结果
提名的泛化性:同BSN和BMN,本文也在ActivityNet-1.3上做了相同的泛化性实验,如下表。相比BMN,BSN++具有更好的泛化性。
表4.ActivityNet-1.3上的泛化性研究
使用BSN++的提名进行动作检测
分别在ActivityNet-1.3和THUMOS14上进行了实验,结果如表5和表6所示。在实现相同分类器的前提下,BSN++取得了比之前更好的mAP结果。
表5.ActivityNet-1.3上的时序动作检测结果
表6.THUMOS14上的时序动作检测结果
总结
BSN++虽然名为BSN的改进版,但总览全文,其更像是”BMN++“。整体上,沿用了BMN的“共享主干-双分支”结构,同时生成边界图和置信度图。不同的是,BSN++进一步对两个分支和采样方法进行了改进。
对于生成边界概率的TEM分支,BSN++用CBG模块替代。其使用具有类似U-Net结构的编码器-解码器网络取代简单的卷积,提取更加丰富的全局和局部信息。同时,在原单向特征基础上,扩展出双向边界匹配机制,进一步提升性能。
对于产生提名置信度分数的PEM分支,BSN++用PRB模块替代。相比于BMN中的PEM模块,PRB采用了更为有效的自注意力机制进行全局关系建模。
对于原有的正负样本均衡采样,BSN++进一步提出了尺度均衡采样。尺度均衡采样能够考虑到生成proposals的持续时间不均衡问题,这是在以往的工作中都被忽略掉的。
该方法获得了CVPR'2019 ActivityNet Challenge时序动作定位任务冠军。
来源:知乎
作者:Yaaaaaa
|深延科技|
深延科技成立于2018年1月,中关村高新技术企业,是拥有全球领先人工智能技术的企业AI服务专家。以计算机视觉、自然语言处理和数据挖掘核心技术为基础,公司推出四款平台产品——深延智能数据标注平台、深延AI开发平台、深延自动化机器学习平台、深延AI开放平台,为企业提供数据处理、模型构建和训练、隐私计算、行业算法和解决方案等一站式AI平台服务。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.