CVPR2021-ActionNet

作者:zzzk

链接:https://zhuanlan.zhihu.com/p/374014267

本文转载自知乎,作者已授权,未经许可请勿二次转载

前言

对于视频理解任务来说,时空性(Spatial-temporal),特征(channel-wise),运动模式(motion pattern)这三方面信息是至关重要的。传统的2D CNN计算代价很低,但是不能捕捉时序关系。而3D CNN虽然能捕捉时序关系,但是计算量较大。

基于上述三个性质,我们分别提出了时空激励(Spatial-Temporal Excitation)模块,通道激励(Channel Excitation)模块,和运动激励模块(Motion Excitation)。最后相加到一起,实现Action 模块。

我们将Action模块移入到2D CNN中,得到ActionNet,在各视频数据集上均有不错的表现。

为什么2D CNN解决不了视频问题?

在视频理解任务中,一些复杂的动作具有时间依赖性。视频不仅在每一帧在空间域上有其信息,在一段时间内也包含时间信息。

以一对很类似的动作,如打开箱子,关闭箱子。这两个动作在空间域上的信息是类似的(比如视频帧会出现箱子,人手等),但在时间信息上,这两个动作是完全相反的。

因此,在视频理解任务中也衍生出了3D CNN模型,3D卷积的特性能让他捕捉时空特性,但是其计算量过大,模型容易过拟合,模型收敛缓慢。

类似的,也有将光流信息引入到2D CNN中,做一个双流结构,但是其计算量也是很大

ActionNet的设计

为了方便讲解,使用以下简记。N对应批次,T对应视频帧数目,C对应通道数,H,W分别对应特征图高,宽

时空激励模块(STE)

时空激励模块主要是使用3D卷积核针对时间,空间这两个维度的信息进行建模,其做法如下:

  • 对通道维度进行平均池化

  • 使用Reshape操作,张量转换成(N, 1, T, H, W)的形式

  • 使用3x3x3的3D卷积核进行处理

  • Reshape回(N, T, 1, H, W)的形式

  • 经过sigmoid函数运算,与原始输入相乘,并加上残差连接

我们看下对应的源代码实现

def __init__(...): 
    ...
    self.sigmoid = nn.Sigmoid()
    # # spatial temporal excitation
    self.action_p1_conv1 = nn.Conv3d(1, 1, kernel_size=(3, 3, 3), 
                                    stride=(1, 1 ,1), bias=False, padding=(1, 1, 1))
def forward(...): 
    ...
    # 3D convolution: c*T*h*w, spatial temporal excitation
    nt, c, h, w = x_shift.size()
    x_p1 = x_shift.view(n_batch, self.n_segment, c, h, w).transpose(2,1).contiguous()
    x_p1 = x_p1.mean(1, keepdim=True)
    x_p1 = self.action_p1_conv1(x_p1)
    x_p1 = x_p1.transpose(2,1).contiguous().view(nt, 1, h, w)
    x_p1 = self.sigmoid(x_p1)
    x_p1 = x_shift * x_p1 + x_shift

通道激励模块(CE)

这个模块设计与SEBlock类似,不过作者在两个全连接层中,插入了一个一维卷积,以表征时间信息上的通道特征。具体做法如下:

  • 对H, W维度进行平均池化

  • 使用1x1卷积将通道数减少为原来的 1/16

  • 使用大小为3的一维卷积进行运算

  • 使用1x1卷积恢复原始通道数

  • 经过sigmoid运算,与原始输入相乘,并加上残差连接

相关源代码如下:

def __init__(...): 
    self.avg_pool = nn.AdaptiveAvgPool2d(1)
    self.relu = nn.ReLU(inplace=True)
    # # channel excitation
    self.action_p2_squeeze = nn.Conv2d(self.in_channels, self.reduced_channels, kernel_size=(1, 1), stride=(1 ,1), bias=False, padding=(0, 0))
    self.action_p2_conv1 = nn.Conv1d(self.reduced_channels, self.reduced_channels, kernel_size=3, stride=1, bias=False, padding=1, 
                                       groups=1)
    self.action_p2_expand = nn.Conv2d(self.reduced_channels, self.in_channels, kernel_size=(1, 1), stride=(1 ,1), bias=False, padding=(0, 0))
    
def forward(...): 
    # 2D convolution: c*T*1*1, channel excitation
    x_p2 = self.avg_pool(x_shift)
    x_p2 = self.action_p2_squeeze(x_p2)
    nt, c, h, w = x_p2.size()
    x_p2 = x_p2.view(n_batch, self.n_segment, c, 1, 1).squeeze(-1).squeeze(-1).transpose(2,1).contiguous()
    x_p2 = self.action_p2_conv1(x_p2)
    x_p2 = self.relu(x_p2)
    x_p2 = x_p2.transpose(2,1).contiguous().view(-1, c, 1, 1)
    x_p2 = self.action_p2_expand(x_p2)
    x_p2 = self.sigmoid(x_p2)
    x_p2 = x_shift * x_p2 + x_shift

运动激励模块(ME)

运动激励模块受TEA中的Motion Excitation模块的启发,利用相邻帧的特征信息来模拟运动信息。

下图是本文提出的运动激励模块,它在SEBlock基础上,加入相邻帧做差的处理:

  • 使用1x1卷积将通道数降为原始的 1/16

  • 在T维度上将五维张量分离为T个四维张量(即每一个视频帧对应的四维张量NCHW),其中,后面T-1个张量经过一个共享2D卷积核,然后将后一帧与前一帧相减,并在T维度上进行拼接(这种做法会导致用于拼接的张量是T-1个,这里作者采取补0操作)

  • 池化H, W维度

  • 经过1x1卷积恢复原始通道数

  • 经过sigmoid运算,与原始输入相乘,并加上残差连接

相关代码如下:

def __init__(...): 
    # motion excitation
    self.pad = (0,0,0,0,0,0,0,1)
    self.action_p3_squeeze = nn.Conv2d(self.in_channels, self.reduced_channels, kernel_size=(1, 1), stride=(1 ,1), bias=False, padding=(0, 0))
    self.action_p3_bn1 = nn.BatchNorm2d(self.reduced_channels)
    self.action_p3_conv1 = nn.Conv2d(self.reduced_channels, self.reduced_channels, kernel_size=(3, 3), 
                                    stride=(1 ,1), bias=False, padding=(1, 1), groups=self.reduced_channels)
    self.action_p3_expand = nn.Conv2d(self.reduced_channels, self.in_channels, kernel_size=(1, 1), stride=(1 ,1), bias=False, padding=(0, 0))
    
def forward(...): 
    # # 2D convolution: motion excitation
    x3 = self.action_p3_squeeze(x_shift) # 对通道降维
    x3 = self.action_p3_bn1(x3)
    nt, c, h, w = x3.size()
    x3_plus0, _ = x3.view(n_batch, self.n_segment, c, h, w).split([self.n_segment-1, 1], dim=1) # 将X3 split成(N, nsegment-1, C, H, W) 和 (N, 1, C, H, W)
    x3_plus1 = self.action_p3_conv1(x3) # 这里的做法是对X3都进行卷积

    _ , x3_plus1 = x3_plus1.view(n_batch, self.n_segment, c, h, w).split([1, self.n_segment-1], dim=1) # 将x3_plus1 split成(N, 1, C, H, W)和(N, nsegment-1, C, H, W)
    x_p3 = x3_plus1 - x3_plus0 # 对前面的nsegment-1个帧相减
    x_p3 = F.pad(x_p3, self.pad, mode="constant", value=0) # 补0,对齐到nsegment长度
    x_p3 = self.avg_pool(x_p3.view(nt, c, h, w))
    x_p3 = self.action_p3_expand(x_p3)
    x_p3 = self.sigmoid(x_p3)
    x_p3 = x_shift * x_p3 + x_shift

在ResNet上,我们将Action模块插入到每个残差块起始处,其他时候张量以(N*T, C, H, W)的形式在网络流动。

实验对比

ActionNet在各数据集上均有不错的结果(也希望能补充Kinetics数据集上的表现)

☆ END ☆

如果看到这里,说明你喜欢这篇文章,请转发、点赞。微信搜索「uncle_pn」,欢迎添加小编微信「 mthler」,每日朋友圈更新一篇高质量博文。

扫描二维码添加小编↓

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值