作者: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)
![](https://i-blog.csdnimg.cn/blog_migrate/43ba4acd25b9bf33e49221a6363f34e8.jpeg)
运动激励模块受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
![](https://i-blog.csdnimg.cn/blog_migrate/9daa811c229c46f20d4f239f0e04fc11.jpeg)
在ResNet上,我们将Action模块插入到每个残差块起始处,其他时候张量以(N*T, C, H, W)的形式在网络流动。
实验对比
ActionNet在各数据集上均有不错的结果(也希望能补充Kinetics数据集上的表现)
![](https://i-blog.csdnimg.cn/blog_migrate/c6b01c6467a991e9918f48b4e8489a54.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/a206500d631c7a517d812ed62df62236.jpeg)
☆ END ☆
如果看到这里,说明你喜欢这篇文章,请转发、点赞。微信搜索「uncle_pn」,欢迎添加小编微信「 mthler」,每日朋友圈更新一篇高质量博文。
↓扫描二维码添加小编↓