最近需要测试这个网络的flops以及params,在此对网络的部分结构以及代码做简单介绍,并介绍如何测试。
论文作者在简介中表示,视频理解算法在深度学习的加持下已经获得了长足的进步,但是很多算法仅注重较短时间内的动作检测。如果我们想要进一步理解较长尺度动作的意图,这种短时间的内容就不够了。同时,一些3D卷积的算法在计算量上也十分庞杂,以至于只能对部分视频进行处理。这些算法通常有一个特点-他们都在“事后”对不同时间的信息进行整合。
所以,作者直接设计了一种端到端的架构来解决这些问题。作者意识到,既然单张图片已经可以对动作内容做出一定分类,那大量的邻近帧就是冗余的,所以在一个时间段内只取一帧来处理就已经够了。此外,作者认为,与其把各个时间段的分数整合起来得到最终分数,不如整合各个时间段中的特征信息。我们可以隔一段距离取一帧,把这些图片放到3D卷积里处理,这样不仅能更好地处理时间信息,还可以处理更长时间段内的信息。
话不多说上网络结构:
上图是ECO Lite的网络结构:视频被分成了N个同样时长的区段,在每个区段中随机抽取一帧,先把这些图片通过常规2D卷积网络进行处理得到特征图,再把特征图堆叠输入3D卷积网络。3D网络基于时间信息对动作进行分类。
这里有一个关键设计--随机取样。在每个区段中随机取样有利于提高采样的多样性,让一个动作中的更多变化进入网络。另外,虽然时间区段的划分上也可以进一步研究,但是出于推理时的简便考虑,作者没有采取更加复杂的动态划分方法。
考虑到不同用户需求,作者把之前的设计命名为了轻量版(Lite,代码中即为ECO,而Full在代码中为ECOfully),由此升级得到全尺寸版(Full)的设计。两版大体相同,区别在于轻量版(Lite)主要着力于通过3D网络榨取长时间信息,因而短时间内特征可能被忽略。全尺寸版(Full)增加了一个2D网络来进一步利用2D部分的信息。新设计如下图所示:
新加的2D网络和原3D网络输出的特征图在进入分类器以前会被连在一起。
2D网络: 采用了BN-Inception结构,取到Inception-3c截止。选择原因是高效。输出是96个的特征图。
3D网络: 采用了改良版3D-Resnet18。这个结构也是被广为使用。
2D网络s: 也就是全尺寸网络新加的2D网络。同样使用了BN-Inception, 从Inception-4a层开始,截至最后一个池化层。输出是长1024,共N个特征向量。
具体如何训练详见论文,接下来介绍ECO网络的部分代码:
import torch
from torch import nn
from .layer_factory import get_basic_layer, parse_expr
import torch.utils.model_zoo as model_zoo
import yaml
class ECO(nn.Module):
def __init__(self, model_path='tf_model_zoo/ECO/ECO.yaml', num_classes=101,
num_segments=4, pretrained_parts='both'):
super(ECO, self).__init__()
self.num_segments = num_segments
self.pretrained_parts = pretrained_parts
manifest = yaml.safe_load(open(model_path))
layers = manifest['layers']
self._channel_dict = dict()
self._op_list = list()
for l in layers:
out_var, op, in_var = parse_expr(l['expr'])
if op != 'Concat' and op != 'Eltwise':
id, out_name, module, out_channel, in_name = get_basic_layer(l,
3 if len(self._channel_dict) == 0 else self._channel_dict[in_var[0]],
conv_bias=True if op == 'Conv3d' else True, num_segments=num_segments)
self._channel_dict[out_name] = out_channel
setattr(self, id, module)
self._op_list.append((id, op, out_name, in_name))
elif op == 'Concat':
self._op_list.append((id, op, out_var[0], in_var))
channel = sum([self._channel_dict[x] for x in in_var])
self._channel_dict[out_var[0]] = channel
else:
self._op_list.append((id, op, out_var[0], in_var))
channel = self._channel_dict[in_var[0]]
self._channel_dict[out_var[0]] = channel
def forward(self, input):
data_dict = dict()
data_dict[self._op_list[0][-1]] = input
def get_hook(name):
def hook(m, grad_in, grad_out):
print(name, grad_out[0].data.abs().mean())
return hook
for op in self._op_list:
if op[1] != 'Concat' and op[1] != 'InnerProduct' and op[1] != 'Eltwise':
# first 3d conv layer judge, the last 2d conv layer's output must be transpose from 4d to 5d
if op[0] == 'res3a_2':
inception_3c_output = data_dict['inception_3c_double_3x3_1_bn']
inception_3c_transpose_output = torch.transpose(inception_3c_output.view((-1, self.num_segments) + inception_3c_output.size()[1:]), 1, 2)
data_dict[op[2]] = getattr(self, op[0])(inception_3c_transpose_output)
else:
data_dict[op[2]] = getattr(self, op[0])(data_dict[op[-1]])
# getattr(self, op[0]).register_backward_hook(get_hook(op[0]))
elif op[1] == 'InnerProduct':
x = data_dict[op[-1]]
data_dict[op[2]] = getattr(self, op[0])(x.view(x.size(0), -1))
elif op[1] == 'Eltwise':
try:
data_dict[op[2]] = torch.add(data_dict[op[-1][0]], 1, data_dict[op[-1][1]])
except:
for x in op[-1]:
print(x,data_dict[x].size())
raise
# x = data_dict[op[-1]]
# data_dict[op[2]] = getattr(self, op[0])(x.view(x.size(0), -1))
else:
try:
data_dict[op[2]] = torch.cat(tuple(data_dict[x] for x in op[-1]), 1)
except:
for x in op[-1]:
print(x,data_dict[x].size())
raise
# print output data size in each layers
# for k in data_dict.keys():
# print(k,": ",data_dict[k].size())
# exit()
# "self._op_list[-1][2]" represents: last layer's name(e.g. fc_action)
return data_dict[self._op_list[-1][2]]
以上代码利用layer_factory.py中的部分函数,实现对ECO模型定义文件ECO.yaml的加载,并建立正向传播,返回正向传播最后得到的数据。
ECO在TSN的基础上通过更改base_model实现对网络模型的定义和初始化,TSN可参见TSN网络代码或本文文末代码。
以下测试网络的flops和params,另外要说明的是,ECO_en表示输入帧数为16,20,24,32的ensemble,修改配置文件:
import argparse
parser = argparse.ArgumentParser(description="PyTorch implementation of ECO")
parser.add_argument('--dataset', type=str, default="kinetics", choices=['ucf101', 'hmdb51', 'kinetics', 'something','jhmdb'])
parser.add_argument('--modality', type=str, default="RGB", choices=['RGB', 'Flow', 'RGBDiff'])
parser.add_argument('--train_list', default="", type=str)
parser.add_argument('--val_list', default="", type=str)
parser.add_argument('--net_model', type=str, default=None)
parser.add_argument('--net_model2D', type=str, default=None)
parser.add_argument('--net_modelECO', type=str, default=None)
parser.add_argument('--net_model3D', type=str, default=None)
# ========================= Model Configs ==========================
parser.add_argument('--arch', type=str, default="ECO")
parser.add_argument('--num_segments', type=int, default=16)
parser.add_argument('--consensus_type', type=str, default='avg',
choices=['avg', 'max', 'topk', 'identity', 'rnn', 'cnn'])
parser.add_argument('--pretrained_parts', type=str, default='both',
choices=['scratch', '2D', '3D', 'both','finetune'])
parser.add_argument('--k', type=int, default=3)
parser.add_argument('--dropout', '--do', default=0.5, type=float,
metavar='DO', help='dropout ratio (default: 0.5)')
parser.add_argument('--loss_type', type=str, default="nll",
choices=['nll'])
# ========================= Learning Configs ==========================
parser.add_argument('--epochs', default=45, type=int, metavar='N',
help='number of total epochs to run')
parser.add_argument('-b', '--batch-size', default=1, type=int,
metavar='N', help='mini-batch size (default: 256)')
parser.add_argument('-i', '--iter-size', default=1, type=int,
metavar='N', help='number of iterations before on update')
parser.add_argument('--lr', '--learning-rate', default=0.001, type=float,
metavar='LR', help='initial learning rate')
parser.add_argument('--lr_steps', default=[20, 40], type=float, nargs="+",
metavar='LRSteps', help='epochs to decay learning rate by 10')
parser.add_argument('--momentum', default=0.9, type=float, metavar='M',
help='momentum')
parser.add_argument('--weight-decay', '--wd', default=5e-4, type=float,
metavar='W', help='weight decay (default: 5e-4)')
parser.add_argument('--clip-gradient', '--gd', default=None, type=float,
metavar='W', help='gradient norm clipping (default: disabled)')
parser.add_argument('--no_partialbn', '--npb', default=False, action="store_true")
parser.add_argument('--nesterov', default=False)
parser.add_argument('--num_saturate', type=int, default=5,
help='if number of epochs that validation Prec@1 saturates, then decrease lr by 10 (default: 5)')
# ========================= Monitor Configs ==========================
parser.add_argument('--print-freq', '-p', default=20, type=int,
metavar='N', help='print frequency (default: 10)')
parser.add_argument('--eval-freq', '-ef', default=5, type=int,
metavar='N', help='evaluation frequency (default: 5)')
# ========================= Runtime Configs ==========================
parser.add_argument('-j', '--workers', default=4, type=int, metavar='N',
help='number of data loading workers (default: 4)')
parser.add_argument('--resume', default='', type=str, metavar='PATH',
help='path to latest checkpoint (default: none)')
parser.add_argument('-e', '--evaluate', dest='evaluate', action='store_true',
help='evaluate model on validation set')
parser.add_argument('--snapshot_pref', type=str, default="")
parser.add_argument('--start-epoch', default=0, type=int, metavar='N',
help='manual epoch number (useful on restarts)')
parser.add_argument('--gpus', nargs='+', type=int, default=None)
parser.add_argument('--flow_prefix', default="", type=str)
parser.add_argument('--rgb_prefix', default="", type=str)
此处batch_size设为1,输入时从每段中抽取一帧,测试代码如下:
import torch
from models import TSN
from opts import parser
args = parser.parse_args()
model = TSN(400, args.num_segments, args.pretrained_parts, args.modality,
base_model="ECO",
consensus_type=args.consensus_type, dropout=args.dropout, partial_bn=not args.no_partialbn)
from thop import profile
#input:(t, c, h, w), 注意:此处t = batch_size*segment_num, 根据TSN而定
input = torch.randn(16,3,224,224)
flops, params = profile(model, inputs=(input,))
print("FLOPs = %f G"%(flops/1e9))
print("params = %f M"%(params/1e6))
# print(model)
#FLOPs = 46.681547 G
#params = 37.503856 M
模型初始化如上,其他帧数测试时,需要更改segment_nums,同时,参数统计也可采用torchsummary里的summary。
本篇论文我并未细读,只是对网络进行简单的初始化并测试flops和params,更多的细节可以参照论文和源代码。
论文及源代码如下:
1804.09066.pdf (arxiv.org)https://arxiv.org/pdf/1804.09066.pdfmzolfaghari/ECO-pytorch: PyTorch implementation for "ECO: Efficient Convolutional Network for Online Video Understanding", ECCV 2018 (github.com)https://github.com/mzolfaghari/ECO-pytorch