目录
参考资料
论文:
Beyond Short Snippets: Deep Networks for Video Classification
博客:
【论文阅读】Beyond Short Snippets: Deep Networks for Video Classification
Beyond Short Snippets: Deep Networks for Video Classification
第1章 引言
视频为识别任务提供了更多可利用的信息,但计算要求也要高得多,因为每个视频可能包含数百到数千帧,并非所有的帧都有用。一直最直观的方法就是视频帧视为静止图像,并应用CNN识别每个帧,并在视频级别对预测进行平均。但是这种方法将使用不完整的信息,因此很容易混淆类,尤其是当两个行为仅存在微小差别时。因此,我们假设学习视频时间演化的全局描述对于准确的视频分类非常重要。
作者说必须用固定数量的参数来建模可变长度的视频,评估了两种能够满足这一要求的方法:特征池化
和递归神经网络
。通过时间共享参数,这两种架构都能够在捕获视频时间演化的全局描述的同时保持恒定数量的参数。
特征池网络
使用CNN独立处理每一帧,然后使用不同的池层组合帧级信息。LSTM网络
也在帧级CNN激活上运行,并且可以学习如何随着时间的推移集成信息。
文章的贡献如下:
- 探索了各种卷积时间特征池架构,并证明使用越来越多的帧可以显著提高分类性能;
- 提出了
CNN+LSTM
的方法,并证实了光流图像对视频分类有很大的好处; - 通过时间共享参数,无论是在特征池化还是LSTM架构中,参数的数量都与视频长度成函数关系。
第2章 主要方法
2.1 CNN+时间池化
时间池化的方法一般包含 平均池化
、加权平均池化
、最大池化
。但是本文通过实验发现还是最大池化的效果比较好,所以本文中所有池化的方法都是最大池化。本文设计了五种CNN+池化的网络结构,每一种都有不同的特点,特征池网络使用CNN独立处理每一帧,然后使用不同的池层组合帧级信息。图中 红色是卷积层
, 紫色是最大池化层
,绿色是时域的最大池化
,黄色全连接层
,橘色softmax层
。
2.1.1 Conv Pooling
第一种结构称为 卷积-池化
结构,其在卷积层的输出增加一个最大池化层,然后再连接两个全连接层,这种网络结构的特点是:因为卷积网络输出的卷积特征图都会保持其输入的空间结构,即卷积特征图的响应值与其感受野是相互对应的,所以通过最大池化可以沿着时间保留其空间信息。
因为是用CNN单独处理了每一帧,而这种方式是相当于把每一帧所提取到的空间特征进行最大池化,也就是把这N帧图像提取到的Feature Map进行最大池化,最后得到的还是一个N帧的Feature Map,然后再reshape经过线性层,最后再分类。
2.1.2 Late Pooling
第二种结构称为 滞后池化
结构,这种结构首先将每一帧计算的卷积特征图通过两个全连接层,然后对全连接层的输出沿着时间最大池化,这种网络结构与Conv Pooling结构相比,不会保留池化前特征的空间结构,而是直接组合高维抽象特征的时间动态。
2.1.3 Slow Pooling
第三种结构称为 慢速池化结构
,这种结构使用一个时间窗口分级地组合输入中的时间信息,对于卷积网络的输出,首先使用滑动窗口最大池化组合输入的局部时间信息,然后对每个局部时间组合连接全连接层,再对全连接层的输出结果通过最大池化组合全部的局部运动信息,最后通过全连接层获得最终的结果。这种网络结构的特点是在组合高维抽象的特征之前,可以先组合局部的时间运动信息。
2.1.4 Local Pooling
第四种结构称为 局部池化结构
,这种结构和Slow Pooling很像,对于卷积网络的输出,首先使用滑动窗口最大池化组合输入的局部时间信息,然后对每个局部运动特征连接两个全连接层直接获得抽象局部运动特征。这种网络结构与Slow Pooling的不同点在于该网络结构不会组合输入的全局运动信息,从而减少了时间信息的丢失(因为每一次时间池化都会丢失一定的时间信息)。
2.1.5 Time-Domain Convolution
第五种结构称为 时域卷积结构
,这种结构与前边的四种结构相比多了一层时间卷积层,相当于在Conv Pooling结构的最大池化层之前增加一层时间卷积层。这种结构的特点是因为增加了时间卷积层,所以可以在最大池化层之前组合更短时局部的时间信息。
2.2 CNN+LSTM
2.2.1 RNN
循环神经网络RNN是用来对时间序列数据进行建模的,该网络结构会对前面的输入信息进行记忆,并结合当前的输入,计算出当前的输出。其非展开结构与展开后的结构如下图所示:
根据上图可以得到如下公式:
{
o
t
=
σ
(
v
⋅
s
t
)
s
t
=
σ
(
u
⋅
x
t
+
w
⋅
s
t
−
1
)
\begin{equation} \begin{cases} o_t = σ(v· s_t)\\ s_t = σ(u· x_t+w· s_{t-1}) \end{cases} \end{equation}
{ot=σ(v⋅st)st=σ(u⋅xt+w⋅st−1)
上式按照时间展开之后可以发现,当前的输出会受到前面时刻输入的影响。
o
t
=
σ
(
v
⋅
σ
(
u
⋅
x
t
+
w
⋅
σ
(
u
⋅
x
t
−
1
+
.
.
.
)
)
)
o_t=σ(v·σ(u·x_t+w·σ(u·x_{t-1}+...)))
ot=σ(v⋅σ(u⋅xt+w⋅σ(u⋅xt−1+...)))
2.2.2 LSTM
LSTM主要通过3个门来控制信息流, i t i_t it是输入门, f t f_t ft是遗忘门, o t o_t ot是输出门, c t c_t ct是用来保存历史信息的cell, h t h_t ht 是短期记忆单元,LSTM的结构图如下图所示:
2.2.3 CNN+LSTM网络结构
文章中的CNN+LSTM结构中使用的是 5 层的LSTM网络,每一层包含 512 个记忆单元,最顶层连接softmax层用于分类,其结构如下图所示:
2.3 训练细节
CNN为 AlexNet
和 GoogLeNet
,在ImageNet上提前预训练。CNN和池化或者LSTM的训练是分开的,先使用单帧训练CNN,LSTM的每一个时刻的输出都计算损失,同时也加入光流作为输入,光流在15fps的视频中计算,首先筛选出数值在[-40,40]的光流,然后将其归一化到[0,255],第三个通道全部设置为0。
第3章 Pytorch实现代码
参考代码:
MRzzm/action-recognition-models-pytorch
具体实现如下:
import torch
import torch.nn as nn
import torchvision
from torchsummary import summary
# 逐帧卷积池化
class conv_pooling(nn.Module):
# Input size: BatchxChannelx120x224x224
# The CNN structure is first trained from single frame, then the FC layers are fine-tuned from scratch.
def __init__(self, num_class):
super(conv_pooling, self).__init__()
# self.children()存储网络结构的子层模块,参考:https://blog.csdn.net/dss_dssssd/article/details/83958518
# 最后两层不要
self.conv = nn.Sequential(*list(torchvision.models.resnet101().children())[:-2])
# 时域池化
self.time_pooling = nn.MaxPool3d(kernel_size=(120, 1, 1))
# 空域池化
self.average_pool = nn.AvgPool3d(kernel_size=(1, 7, 7))
# 线性层
self.linear1 = nn.Linear(2048, 2048)
self.linear2 = nn.Linear(2048, num_class)
def forward(self, x):
# 输入维度为(batch_size,channel,time,width,height)
t_len = x.size(2) # 取出帧数这一维度
# 保存逐帧卷积的结果
conv_out_list = []
for i in range(t_len):
# 把每一帧拿出来进行卷积
conv_out_list.append(self.conv(torch.squeeze(x[:, :, i, :, :])))
# 把张量拼接起来,参考:https://blog.csdn.net/xinjieyuan/article/details/105205326
# 按时间维度拼接,如果原来是120帧,拼接之后的维度为(batch,channel,120,w,h)
conv_out = self.time_pooling(torch.stack(conv_out_list, 2))
conv_out = self.average_pool(conv_out)
conv_out = self.linear1(conv_out.view(conv_out.size(0), -1))
conv_out = self.linear2(conv_out)
return conv_out
class cnn_lstm(nn.Module):
# Input size: BatchxChannelx10x224x224
# The CNN structure is first trained from single frame, then the lstm is fine-tuned from scratch.
def __init__(self, num_class):
super(cnn_lstm, self).__init__()
# 舍弃最后一层线性层
self.conv = nn.Sequential(*list(torchvision.models.resnet101().children())[:-1])
# 5层的LSTM,隐层的维度为512
self.lstm = nn.LSTM(2048, hidden_size=512, num_layers=5, batch_first=True)
self.fc = nn.Linear(512, num_class)
def forward(self, x):
t_len = x.size(2)
conv_out_list = []
for i in range(t_len):
# 维度为(batch,channel,1,1,1),一共有10帧
conv_out_list.append(self.conv(torch.squeeze(x[:, :, i, :, :])))
# 按照channel维度拼接 -> (batch,10,channel,1,1)
conv_out = torch.stack(conv_out_list, 1)
print(conv_out.shape)
# 输入被reshape为了(batch,10,channel)的大小,然后送入LSTM
# conv_out的维度应该为(batch,10,512),也就是(batch,seqlen,hidden_size),因为设置了batch_first
conv_out, (hidden, cell) = self.lstm(conv_out.view(conv_out.size(0), conv_out.size(1), -1))
print(conv_out.shape)
print(hidden.shape)
print(cell.shape)
# 按照seqlen的维度,把每一个输出拿出来过线性层
lstm_out = []
for j in range(conv_out.size(1)):
lstm_out.append(self.fc(torch.squeeze(conv_out[:, j, :])))
return torch.stack(lstm_out, 1), (hidden, cell)
def test_conv_pooling():
net = conv_pooling(num_class=100)
#创建模型,部署gpu
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
net.to(device)
summary(net, (3, 120, 224, 224))
def test_cnn_lstm():
net = cnn_lstm(num_class=100)
inputs = torch.rand(2, 3, 10, 224, 224)
outputs = net.forward(inputs)
print(outputs)
# summary(net, (3, 10, 224, 224))
if __name__ == '__main__':
# test_conv_pooling()
test_cnn_lstm()