PaddlePaddle飞桨论文复现营——3D Residual Networks for Action Recognition学习笔记
1 背景知识
1.1 C3D
C3D是一种3D卷积提取视频特征的方法,从水平(X)、垂直(Y)和时序(T)三个方向三个方面同时提取视频的时空特征,其提取到的特征比常规的2D卷积更自然[1]。缺点就是,因为多了一维“时序(T)”,卷积后会引起参数剧增,对计算机GPU算力要求较大。
P.S: C3D源代码和预训练模型可参考:http://vlg.cs.dartmouth.edu/c3d/
通过上图,不难发现,相比VGG16或VGG19,C3D的卷积层相对较少,只有8层。此外,3D卷积核相比2D卷积核所多出的大量参数,在数据集不充足的情况下,网络易趋于过拟合。
1.2 Kinetics数据集
2017年5月22日,Deepmind团队发布了最具有影响力的视频分类数据集之一,目前,其包含大约650000个高质量视频链接,涵盖700个人类动作类,包括人与对象之间的交互(如演奏乐器)以及人与人之间的互动(如握手和拥抱)。每个动作类至少有600个视频剪辑。每个剪辑都是人工注释与单个动作类,并持续约10秒。
P.S: Kinetics地址:https://deepmind.com/research/open-source/kinetics
1.3 ResNet
更深的神经网络意味着更难的训练。因此,微软研究院何恺明等人在《Deep Residual Learning for Image Recognition》提出了一种减轻网络训练负担的残差学习框架(ResNet),这种网络比以前使用过的网络本质上层次更深。可理解为其显式地将层重新配置为参考层输入学习剩余函数,而不是学习未参考函数。他们通过提供了全面的经验证据,证明了这些残差网络更易于优化,并且可以通过深度的增加而获得准确性的提升。在ImageNet数据集上,深度最大为152层的残差网络-比VGG19网络还要深8倍,但复杂度仍然较低。这些残留网络的整体在ImageNet测试仪上实现了3.57%的误差。该结果在ILSVRC2015分类任务中获得第一名[2]。其框架如下图所示:
下图是ResNet在CIFAR-10测试集上的分类错误情况
2 研究方法
2.1 3D ResNets简述
在当时的研究背景下,最新的大型视频数据集(如,Kinetics),虽然能大幅度地改善过拟合的情况,但是,相对于优秀的2D网络(如,ResNet)而言,C3D的网络缺乏深度。因此,作者的团队就基于ResNet提出了3D ResNets这个网络架构。
2.2 3D ResNet网络架构
2.2.1 Residual block——残差块
ResNets引入了捷径连接(shortcut connections),可以绕过一层到另一层的标记。这些连接通过网络的梯度流从较后层到较早层,并简化了非常深层网络的训练。上图(Figure 1)展示了残差块的结构,它是ResNets的一个元素。连接从块的顶部到尾部绕过标记。ResNets由多个残差块组成。
2.2.2 Network Architecture——网络架构
通过Table1的3D ResNets网络架构图不难发现,3D ResNets与原始ResNets的区别在于卷积核(convolutional kernels)和池化层(pooling)的维数。3DResNet执行3D卷积和3D池化。卷积核的大小为“ 3 × 3 × 3”,并且conv1的时间步长(stride)为1,类似于C3D。网络使用16帧RGB剪辑作为输入。输入剪辑的大小为“3 × 16 × 112 × 112”。残差块显示在Table1的括号中。 每个卷积层之后是Batch Normalization(BN,对每批数据进行归一化处理)和relu(激活函数)。 输入的下采样由步长(stride)为2的conv3_1,conv4_1,conv5_1执行。当特征图(feature maps)的数量增加时,作者采用零填充的身份快捷方式来避免增加参数数量。框架的最后一层是为Kinetics数据集(400个类别)设置的最后一个完全连接层,其输出函数是softmax(将输出值限定在0~1之间的一个概率值)。
2.2.3 Training——训练
作者使用带有动量的随机梯度下降(SGD)来训练3D ResNet的网络,通过从训练数据中的视频中随机生成训练样本以执行数据增强。主要内容如下:
- 通过均匀采样选择每个样本的时间位置(temporal positions)。
- 在选定的时间位置(temporal positions)周围生成16帧剪辑。如若视频少于16帧,则将对视频进行必要的多次循环。
- 从4个角或中心随机选择空间位置。
- 对每个样本的空间尺度进行多尺度裁剪,尺度选自 { 1 , 1 2 1 / 4 , 1 2 , 1 2 1 / 4 , 1 2 } \left\{1, \frac{1}{2^{1 / 4}}, \frac{1}{\sqrt{2}}, \frac{1}{2^{1 / 4}}, \frac{1}{2}\right\} { 1,21/41,21,21/41,21},其中1为最大尺度。裁剪框的长宽比为1。生成的样本水平翻转的概率为50%。
- 对每个样本进行均值减法运算。所有生成的样本均与其原始视频具有相同的类别标签。
作者在训练时,学习率现(lr)先是设定为0.1,然后,当学习率降到0.0001后,验证损失达到饱和。较大的lr和batch对于实现良好的识别性能尤为重要。
2.2.4 Recognition——识别
首先,使用训练好的模型来识别视频中的动作。训练过程中,每个剪辑(每个视频被分成不重叠的16帧剪辑。)都以最大比例围绕中心位置进行裁剪。使用经过训练的模型估算每个剪辑的类别概率,并将它们平均化到视频的所有剪辑中,以识别视频中的动作。
2.2.5 Dataset——数据集
此实验中,作者使用了ActivityNet(v1.3)和Kinetics数据集。
ActivityNet数据集提供了200个人类动作类别的样本,每个类别平均有137个未修剪的视频,每个视频的Activity Instances(活动实例)为1.41。视频总时长为849小时,Activity Instances(活动实例)的总数为28108。数据集随机分为三个子集:训练,验证和测试,其中50%用于训练,25%用于验证和25%用于测试。
2017年,Kinetics数据集刚发布时,其提供了400个人类动作类别的样本,并且每个类别包含400个(或更多)的视频。视频在时间上进行了修剪,因此它们不包含非动作帧,并且持续约10秒钟。视频总数为300,000或更多。训练,验证和测试集的数量分别约为240,000、20,000和40,000。Kinetics的Activity Instances(活动实例)数量是ActivityNet的Activity Instances(活动实例)数量的10倍,而两个数据集的总视频长度却很接近。
对于这两个数据集,作者都其将视频的大小调整为360像素高度,而未更改其宽高比,并将其存储。
3 研究成果
3.1 基于ActivityNet数据集的初步实验结果
此实验的目的是在相对较小的数据集上探索3D ResNet的训练效果。在此实验中,作者的团队训练了Table1中所述的18层的3D ResNet和Sports-1M预先训练的C3D。观察Figure2,可以发现18层的3D ResNet出现了过拟合,因此其验证精度明显低于训练精度。相比之下,经过Sports-1M预训练的C3D没有出现过拟合,并且获得了更好的识别精度。
3.2 基于Kinetics数据集的实验结果
此实验中,作者的团队训练了34层的3D ResNet而不是18层的3D ResNet,因为Kinetics的活动实例数量明显大于ActivityNet的活动实例数量。Figure3显示了34层的3D ResNet不会过拟合并获得良好的性能。如Figure1(b)所示,Sports-1M预训练的C3D也实现了良好的验证准确性,但是,它的训练精度明显低于验证精度。
Table2显示了34层的3D ResNet和同时期较新技术的准确性。34层的3D ResNet的精度高于Sports-1M预先训练的C3D和C3D,并且具有从头开始训练的Batch Normalization(BN,对每批数据进行归一化处理)。该结果证明了3DResNet的有效性。但是,深度数小于34层的3D ResNet的RGB-I3D达到了最佳性能却在此实验中表现优异,其原因可能是,训练RGB-I3D时,使用了32 个 GPU,而训练34层的3D ResNet,只使用了4个256批处理大小的GPU。由于 GPU 内存限制,3D ResNet 的大小为“3 × 16 × 112 × 112”,而 RGB-I3D 的大小为“3 × 64 × 224 × 224”。高空间分辨率和较长的持续时间可提高识别精度。因此,使用大量GPU并增加批处理大小,空间分辨率和时间持续时间可能会进一步实现3D ResNets 的改进。
4 Conclusion——结论
作者及其团队创新性地提出了3D卷积内核及3D池化层的概念,并据此结合一系列实验来探索论证了ResNets在视频分类领域的有效性(尤其是在大数据集的环境下)[3]。
5 源码简析
源码参考地址:https://github.com/kenshohara/3D-ResNets
5.1 training.py
import torch # 通过paddlepaddle实现时,此处需修改为对应的paddlepaddle包
import time
import os
import sys
import torch # 通过paddlepaddle实现时,此处需修改为对应的paddlepaddle包
import torch.distributed as dist # 通过paddlepaddle实现时,此处需修改为对应的paddlepaddle包
from utils import AverageMeter, calculate_accuracy
def train_epoch(epoch, # 训练轮次
data_loader,
model,
criterion,
optimizer,
device,
current_lr,
epoch_logger,
batch_logger,
tb_writer=None,
distributed=False):
print('train at epoch {}'.format(epoch))
model.train()
batch_time = AverageMeter()
data_time = AverageMeter()
losses = AverageMeter()
accuracies = AverageMeter()
end_time = time.time()
for i, (inputs, targets) in enumerate(data_loader):
data_time.update(time.time() - end_time)
targets = targets.to(device, non_blocking=True)
outputs = model(inputs)
loss = criterion(outputs, targets) # 损失值计算
acc = calculate_accuracy(outputs, targets) # 准确率计算
losses.update(loss.item(), inputs.size(0)) # 损失值更新
accuracies.update(acc, inputs.size(0)) # 准确率更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
batch_time.update(time.time() - end_time)
end_time = time.time()
if batch_logger is not None:
batch_logger.log({
'epoch': epoch,
'batch': i + 1,
'iter': (epoch - 1) * len(data_loader) + (i + 1),
'loss': losses.val,
'acc': accuracies.val,
'lr': current_lr
})
print('Epoch: [{0}][{1}/{2}]\t' # 打印运行日志
'Time {batch_time.val:.3f} ({batch_time.avg:.3f})\t'
'Data {data_time.val:.3f} ({data_time.avg:.3f})\t'
'Loss {loss.val:.4f} ({loss.avg:.4f})\t'
'Acc {acc.val:.3f} ({acc.avg:.3f})'.format(epoch,
i + 1,
len(data_loader),
batch_time=batch_time,
data_time=data_time,
loss=losses,
acc=accuracies))
if distributed:
loss_sum = torch.tensor([losses.sum],
dtype=torch.float32,
device=device)
loss_count = torch.tensor([losses.count],
dtype=torch.float32,
device=device)</