1.概述
PySlowFast 是Facebook近期开源的一个视频理解项目,其中包含了数个优秀论文的实现,包括SlowFast、X3D、I3D等。项目的地址在这里,本人最近正在做用该项目作视频行为识别的相关内容,因此本文讲述在应用PySlowFast的一些问题,主要是展示代码的运行过程。在查看相关论文后,发现X3D是项目中无论是准确率还是速度比较优秀的一个,因此采取该方案来做行为识别。
2. PySlowFast解读
下边结合代码来解读PySlowFast框架的运行。因为项目工作时间有限,只对训练部分作了解,测试以及demo展示部分皆没有详细阅读。
解读过程是利用VSCode中的debug模型进行的,此法能够方便了解代码的运行过程,同时能够随时执行各种命令,查看变量的各种属性。
项目安装完成好后可以输入以下命令来运行
python tools/run_net.py --cfg {config file}
由于我主要训练的是X3D网络,因此将–cfg后的参数换成 configs/Kinetics/X3D_M.yaml即可。
以下是X3D_M.yaml文件
TRAIN:
ENABLE: True # default True
DATASET: kinetics
BATCH_SIZE: 8
#BATCH_SIZE: 16
EVAL_PERIOD: 1
CHECKPOINT_PERIOD: 1
AUTO_RESUME: False
CHECKPOINT_FILE_PATH: checkpoints/AVA_RWF_pretrain/checkpoint_epoch_00004.pyth
#CHECKPOINT_FILE_PATH: model/x3d_m.pyth
CHECKPOINT_EPOCH_RESET: True
X3D:
WIDTH_FACTOR: 2.0
DEPTH_FACTOR: 2.2
BOTTLENECK_FACTOR: 2.25
DIM_C5: 2048
DIM_C1: 12
TEST:
ENABLE: False
DATASET: kinetics
BATCH_SIZE: 64
# CHECKPOINT_FILE_PATH: 'x3d_m.pyth' # 76.21% top1 30-view accuracy to download from the model zoo (optional).
NUM_SPATIAL_CROPS: 1
#NUM_SPATIAL_CROPS: 3
CHECKPOINT_FILE_PATH: checkpoints/AVA_RWF_pretrain/checkpoint_epoch_00004.pyth
DATA:
PATH_TO_DATA_DIR:
NUM_FRAMES: 16
SAMPLING_RATE: 5
TRAIN_JITTER_SCALES: [256, 320]
TRAIN_CROP_SIZE: 224
# TEST_CROP_SIZE: 224 # use if TEST.NUM_SPATIAL_CROPS: 1
TEST_CROP_SIZE: 256 # use if TEST.NUM_SPATIAL_CROPS: 3
INPUT_CHANNEL_NUM: [3]
DECODING_BACKEND: pyav
#DECODING_BACKEND: torchvision
RESNET:
ZERO_INIT_FINAL_BN: True
TRANS_FUNC: x3d_transform
STRIDE_1X1: False
BN:
USE_PRECISE_STATS: True
NUM_BATCHES_PRECISE: 200
WEIGHT_DECAY: 0.0
SOLVER:
BASE_LR: 0.00002 # 1 machine
BASE_LR_SCALE_NUM_SHARDS: True
LR_POLICY: cosine
MAX_EPOCH: 10
WEIGHT_DECAY: 5e-5
#WARMUP_EPOCHS: 35.0
#WARMUP_START_LR: 0.01
#OPTIMIZING_METHOD: sgd
OPTIMIZING_METHOD: adam
MODEL:
NUM_CLASSES: 2
ARCH: x3d
MODEL_NAME: X3D
LOSS_FUNC: cross_entropy
DROPOUT_RATE: 0.5
DATA_LOADER:
NUM_WORKERS: 8
PIN_MEMORY: True
NUM_GPUS: 1
RNG_SEED: 0
OUTPUT_DIR: .
DEMO:
ENABLE: True
LABEL_FILE_PATH:# Path to json file providing class_name - id mapping.
INPUT_VIDEO:
OUTPUT_FILE:# Path to output video file to write results to.
# Leave an empty string if you would like to display results to a window.
THREAD_ENABLE: False # Run video reader/writer in the background with multi-threading.
NUM_VIS_INSTANCES: 1 # Number of CPU(s)/processes use to run video visualizer.
NUM_CLIPS_SKIP: 0 # Number of clips to skip prediction/visualization
# (mostly to smoothen/improve display quality with wecam input).
配置文件中包含运行代码所需的许多配置信息,包括超参数、数据路径、权重路径等。
配置文件中最重要的就是要设置好目前运行目的是训练、测试还是demo展示,分别对应的是TRAIN,TEST,DEMO中的ENABEL选项。选为True则运行该项,False则不运行,如果有多项设置为True,则会依次执行。例如TEST和DEMO中的ENABLE选项都设置为True的话,则会先进行测试,然后进行demo制作。
运行命令后程序将会执行
launch_job(cfg=cfg, init_method=args.init_method, func=train)
之后程序将会跳转到toos/train_net.py中的train函数中,此函数将包含训练模型所需要的所有命令。
def train(cfg):
du.init_distributed_training(cfg)
# Set random seed from configs.
np.random.seed(cfg.RNG_SEED)
torch.manual_seed(cfg.RNG_SEED)
# Setup logging format.
logging.setup_logging(cfg.OUTPUT_DIR)
# Init multigrid.
multigrid = None
if cfg.MULTIGRID.LONG_CYCLE or cfg.MULTIGRID.SHORT_CYCLE:
multigrid = MultigridSchedule()
cfg = multigrid.init_multigrid(cfg)
if cfg.MULTIGRID.LONG_CYCLE:
cfg, _ = multigrid.update_long_cycle(cfg, cur_epoch=0)
# Print config.
logger.info("Train with config:")
logger.info(pprint.pformat(cfg))
此部分代码主要是训练前的一些初始化准备,包括分布式训练初始化、随机数种子指定、日志记录初始化等。其中multigrid中在配置文件中默认是不执行的,因此并没有详细了解其作用。
model = build_model(cfg)
此行代码很明显是根据配置文件来创建模型,因此可以仔细查看一下其创建过程是怎么样的。
build_model函数是用cfg作为参数传入的,因此其他模型也可以通过此一行代码可以构建
def build_model(cfg, gpu_id=None):
"""someting"""
name = cfg.MODEL.MODEL_NAME
model = MODEL_REGISTRY.get(name)(cfg)
"""someting"""
此函数的核心代码就比较简单,就是在配置文件中读取函数名字,然后通过读取预先注册好的构建函数来构建模型。
下边进入这个函数,查看一下代码是如何构建模型的。
下一个进入的文件是slowfast/models/video_model_builder.py,查看发现其中包含很多模型的构建函数,包括slowfast,ResNet,X3D以及MVit。此项目其实还支持其他模型,包括I3D,在此处没有找到,可能用了其他方式进行声明。
2.1 X3D模型构建
查看video_model_builder.py中的X3D类可以发现,其主要部件都在__init__中进行初始化,forward函数只是简单地将每一个模块进行运算,因此我们只要看__init__函数就可以大致了解模型的构建了。
class X3D(nn.Module):
def __init__(self, cfg):
super(X3D, self).__init__()
self.norm_module = get_norm(cfg)
self.enable_detection = cfg.DETECTION.ENABLE
self.num_pathways = 1
exp_stage = 2.0
self.dim_c1 = cfg.X3D.DIM_C1
self.dim_res2 = (
round_width(self.dim_c1, exp_stage, divisor=8)
if cfg.X3D.SCALE_RES2
else self.dim_c1
)
self.dim_res3 = round_width(self.dim_res2, exp_stage, divisor=8)
self.dim_res4 = round_width(self.dim_res3, exp_stage, divisor=8)
self.dim_res5 = round_width(self.dim_res4, exp_stage, divisor=8)
self.block_basis = [
# blocks, c, stride
[1, self.dim_res2, 2],
[2, self.dim_res3, 2],
[5, self.dim_res4, 2],
[3, self.dim_res5, 2],
]
self._construct_network(cfg)
init_helper.init_weights(
self, cfg.MODEL.FC_INIT_STD, cfg.RESNET.ZERO_INIT_FINAL_BN
)
初始化函数主要是定义了一些变量,这些变量是构建模型的时候的各个块的超参数,如步长、通道数等等。定义完毕后代码执行_construct_network来构建模型,以及init_helper.init_weights来进行参数初始化,因此我们主要看前一个函数即可。
def _construct_network(self, cfg):
"""something"""
self.s1 = stem_helper.VideoModelStem(
dim_in=cfg.DATA.INPUT_CHANNEL_NUM,
dim_out=[dim_res1],
kernel=[temp_kernel[0][0] + [3, 3]],
stride=[[1, 2, 2]],
padding=[[temp_kernel[0][0][0] // 2, 1, 1]],
norm_module=self.norm_module,
stem_func_name="x3d_stem",
)
函数开始执行时同样是声明一些构建模型所需的变量。之后开始构建第一个模块
self.s1 = stem_helper.VideoModelStem()。由于我在训练时用的是X3D-M模块,因此可以参考论文中的结构图来了解其中的构建过程。
根据顺序可以看到stem_helper.VideoModelStem()函数构建的就是上图中的conv1模块。但是有些参数是不一样的。
class VideoModelStem(nn.Module):
def __init__(
self,
dim_in,
dim_out,
kernel,
stride,
padding,
inplace_relu=True,
eps=1e-5,
bn_mmt=0.1,
norm_module=nn.BatchNorm3d,
stem_func_name="basic_stem",
):
"""something"""
self._construct_stem(dim_in, dim_out, norm_module, stem_func_name)
此函数与上边类似,也是声明一些变量,然后用_construct_stem来构建模块。读完这些代码下来发现,这个项目的风格就是喜欢定义一些变量,然后用这些变量执行一个函数来生成模块。
def _construct_stem(self, dim_in, dim_out, norm_module, stem_func_name):
trans_func = get_stem_func(stem_func_name)
for pathway in range(len(dim_in)):
stem = trans_func(
dim_in[pathway],
dim_out[pathway],
self.kernel[pathway],
self.stride[pathway],
self.padding[pathway],
self.inplace_relu,
self.eps,
self.bn_mmt,
norm_module,
)
self.add_module("pathway{}_stem".format(pathway), stem)
此模块为了提高可复用性,利用get_stem_func()函数获取该调用的函数名字X3DStem,然后再执行。
此处执行时可以看到需要进入一个pathway循环。在X3D中,此处只执行一次。在SlowFast模块中,由于模型有两条前向模块,分别对应不同维度的输入,因此才需要这个循环。
class X3DStem(nn.Module):
def __init__(
self,
dim_in,
dim_out,
kernel,
stride,
padding,
inplace_relu=True,
eps=1e-5,
bn_mmt=0.1,
norm_module=nn.BatchNorm3d,
):
super(X3DStem, self).__init__()
self.kernel = kernel
self.stride = stride
self.padding = padding
self.inplace_relu = inplace_relu
self.eps = eps
self.bn_mmt = bn_mmt
# Construct the stem layer.
self._construct_stem(dim_in, dim_out, norm_module)
def _construct_stem(self, dim_in, dim_out, norm_module):
self.conv_xy = nn.Conv3d(
dim_in,
dim_out,
kernel_size=(1, self.kernel[1], self.kernel[2]),
stride=(1, self.stride[1], self.stride[2]),
padding=(0, self.padding[1], self.padding[2]),
bias=False,
)
self.conv = nn.Conv3d(
dim_out,
dim_out,
kernel_size=(self.kernel[0], 1, 1),
stride=(self.stride[0], 1, 1),
padding=(self.padding[0], 0, 0),
bias=False,
groups=dim_out,
)
self.bn = norm_module(
num_features=dim_out, eps=self.eps, momentum=self.bn_mmt
)
self.relu = nn.ReLU(self.inplace_relu)
def forward(self, x):
x = self.conv_xy(x)
x = self.conv(x)
x = self.bn(x)
x = self.relu(x)
return x
进入X3DStem模块,已经很清晰各个步骤了,模块首先执行conv_xy,然后是conv,最后是常规的batchnorm和relu。
其中conv_xy是空间维度的卷积,根据传入参数可以看到,kernel大小是1x3x3,stride是1x2x2,输入通道为3,输出通道为24。
conv模块是时间维度卷积,kernel大小是5x1x1,stride是1x1x1,输入和输出通道相等都是24。而文章中此处的kernel大小应为3x1x1,这是两者不同之处。
至此
X3DStem模块已经生成完毕,此模块对应的是论文结构图中的conv1模块。
返回到X3D模块中的_construct_network函数中去,现在第一个模块self.s1已经生成完毕。之后是进入一个for循环中,生成模型剩下的模块。
for stage, block in enumerate(self.block_basis):
s = resnet_helper.ResStage(
dim_in=[dim_in],
dim_out=[dim_out],
dim_inner=[dim_inner],
temp_kernel_sizes=temp_kernel[1],
stride=[block[2]],
num_blocks=[n_rep],
num_groups=[dim_inner]
if cfg.X3D.CHANNELWISE_3x3x3
else [num_groups],
num_block_temp_kernel=[n_rep],
nonlocal_inds=cfg.NONLOCAL.LOCATION[0],
nonlocal_group=cfg.NONLOCAL.GROUP[0],
nonlocal_pool=cfg.NONLOCAL.POOL[0],
instantiation=cfg.NONLOCAL.INSTANTIATION,
trans_func_name=cfg.RESNET.TRANS_FUNC,
stride_1x1=cfg.RESNET.STRIDE_1X1,
norm_module=self.norm_module,
dilation=cfg.RESNET.SPATIAL_DILATIONS[stage],
drop_connect_rate=cfg.MODEL.DROPCONNECT_RATE
* (stage + 2)
/ (len(self.block_basis) + 1),
)
此处输入参数十分多,看不过来,直接进入模块内部看看是怎么生成的。方便起见我就不一一列出代码了。
此函数进入的是resnet_helper.py中的ResStage类,在初始化函数中调用self._construct来生成模块
for pathway in range(self.num_pathways):
for i in range(self.num_blocks[pathway]):
# Retrieve the transformation function.
trans_func = get_trans_func(trans_func_name)
# Construct the block.
res_block = ResBlock(
dim_in[pathway] if i == 0 else dim_out[pathway],
dim_out[pathway],
self.temp_kernel_sizes[pathway][i],
stride[pathway] if i == 0 else 1,
trans_func,
dim_inner[pathway],
num_groups[pathway],
stride_1x1=stride_1x1,
inplace_relu=inplace_relu,
dilation=dilation[pathway],
norm_module=norm_module,
block_idx=i,
drop_connect_rate=self._drop_connect_rate,
)
以上是_construct函数的核心代码,可以看到函数进入了两个循环,第一个是对pathway进行循环,上文已经提及对于slowfast此处有两个pathway,而对于X3D这里只有1个。第二个循环是模块的重复次数,根据论文结构图,res2-5模块是分别循环3、5、11、17次,这里是第一个模块,因此循环次数为3。
class ResBlock(nn.Module):
def _construct():
self.branch1 = nn.Conv3d(
dim_in,
dim_out,
kernel_size=1,
stride=[1, stride, stride],
padding=0,
bias=False,
dilation=1,
)
self.branch1_bn = norm_module(
num_features=dim_out, eps=self._eps, momentum=self._bn_mmt
)
self.branch2 = trans_func(
dim_in,
dim_out,
temp_kernel_size,
stride,
dim_inner,
num_groups,
stride_1x1=stride_1x1,
inplace_relu=inplace_relu,
dilation=dilation,
norm_module=norm_module,
block_idx=block_idx,
)
self.relu = nn.ReLU(self._inplace_relu)
进入到ResBlock中的_construct函数中,可以看到主要定义了branch1和branch2,其中branch1的con3d,kernel大小是1x1x1,输入输出通道都是24,stride为1x2x2,因此可以明确此模块是用于下采样,缩小输入的H和W大小。
branch2是用X3DTransform函数生成的。
class X3DTransform():
def _construct():
self.a = nn.Conv3d(
dim_in,
dim_inner,
kernel_size=[1, 1, 1],
stride=[1, str1x1, str1x1],
padding=[0, 0, 0],
bias=False,
)
self.a_bn = norm_module(
num_features=dim_inner, eps=self._eps, momentum=self._bn_mmt
)
self.a_relu = nn.ReLU(inplace=self._inplace_relu)
# Tx3x3, BN, ReLU.
self.b = nn.Conv3d(
dim_inner,
dim_inner,
[self.temp_kernel_size, 3, 3],
stride=[1, str3x3, str3x3],
padding=[int(self.temp_kernel_size // 2), dilation, dilation],
groups=num_groups,
bias=False,
dilation=[1, dilation, dilation],
)
self.b_bn = norm_module(
num_features=dim_inner, eps=self._eps, momentum=self._bn_mmt
)
# Apply SE attention or not
use_se = True if (self._block_idx + 1) % 2 else False
if self._se_ratio > 0.0 and use_se:
self.se = SE(dim_inner, self._se_ratio)
if self._swish_inner:
self.b_relu = Swish()
else:
self.b_relu = nn.ReLU(inplace=self._inplace_relu)
# 1x1x1, BN.
self.c = nn.Conv3d(
dim_inner,
dim_out,
kernel_size=[1, 1, 1],
stride=[1, 1, 1],
padding=[0, 0, 0],
bias=False,
)
self.c_bn = norm_module(
num_features=dim_out, eps=self._eps, momentum=self._bn_mmt
)
self.c_bn.transform_final_bn = True
此模块较大,可以逐个看清楚。并且可以跟论文结构图对应起来
self.a,输入通道24,输出54,kernel大小1x1x1,stride1x1x1,单纯改变通道数用。
self.b,输入通道54,输出54,kernel大小3x3x3,stride1x2x2,缩小了空间大小,注意此模块将会循环生成3次,除了第一次之外,其余的stride均为1x1x1。
然后引入了SE模块,此为注意力模块,详情可以参考这篇文章。
之后还引入了Swish激活函数,此函数为relu和sigmoid的结合体,表现比relu的要好,详情可以看这篇文章。
self.c 输入通道54,输出24,kernel大小1x1x1,stride1x1x1,单纯改变通道数用。
至此X3DTransform模块的构建已经生成完毕,可以看到此类的重点输入参数有3个,dim_in对应输入通道,dim_out对应输出通道,dim_inner对应中间通道。ResBlock也对应的构建完毕。
返回到ResStage类中,可以看到现在代码在循环中,将重复生成3次ResBlock,组成论文结构图中的res2模块。
现在可以返回到X3D类了,可以看到代码现在在一个循环中
for stage, block in enumerate(self.block_basis):
dim_out = round_width(block[1], w_mul)
dim_inner = int(cfg.X3D.BOTTLENECK_FACTOR * dim_out)
n_rep = self._round_repeats(block[0], d_mul)
prefix = "s{}".format(
stage + 2
) # start w res2 to follow convention
s = resnet_helper.ResStage(
dim_in=[dim_in],
dim_out=[dim_out],
dim_inner=[dim_inner],
temp_kernel_sizes=temp_kernel[1],
stride=[block[2]],
num_blocks=[n_rep],
num_groups=[dim_inner]
if cfg.X3D.CHANNELWISE_3x3x3
else [num_groups],
num_block_temp_kernel=[n_rep],
nonlocal_inds=cfg.NONLOCAL.LOCATION[0],
nonlocal_group=cfg.NONLOCAL.GROUP[0],
nonlocal_pool=cfg.NONLOCAL.POOL[0],
instantiation=cfg.NONLOCAL.INSTANTIATION,
trans_func_name=cfg.RESNET.TRANS_FUNC,
stride_1x1=cfg.RESNET.STRIDE_1X1,
norm_module=self.norm_module,
dilation=cfg.RESNET.SPATIAL_DILATIONS[stage],
drop_connect_rate=cfg.MODEL.DROPCONNECT_RATE
* (stage + 2)
/ (len(self.block_basis) + 1),
)
dim_in = dim_out
self.add_module(prefix, s)
self.block_basis长度为4,分别对应生成论文结构图中的res2-5。在res2中,输入dim_in和输出通道dim_out都是24,中间通道dim_inner是54,重复num_blocks为3次。
在res3中,输入通道24,输出通道48,中间通道108,重复5次
在res4中,输入通道48,输出通道96,中间通道216,重复11次
在res5中,输入通道96,输出通道192,中间通道432,重复7次。
代码中的参数都能够与论文结构图对应起来。
在ResStage生成完毕后,代码最后构建了X3DHead,下边我们进入到这个函数中看看模块是如何生成的。
class X3DHead(nn.Module):
def _construct_head(self, dim_in, dim_inner, dim_out, norm_module):
self.conv_5 = nn.Conv3d(
dim_in,
dim_inner,
kernel_size=(1, 1, 1),
stride=(1, 1, 1),
padding=(0, 0, 0),
bias=False,
)
self.conv_5_bn = norm_module(
num_features=dim_inner, eps=self.eps, momentum=self.bn_mmt
)
self.conv_5_relu = nn.ReLU(self.inplace_relu)
if self.pool_size is None:
self.avg_pool = nn.AdaptiveAvgPool3d((1, 1, 1))
else:
self.avg_pool = nn.AvgPool3d(self.pool_size, stride=1)
self.lin_5 = nn.Conv3d(
dim_inner,
dim_out,
kernel_size=(1, 1, 1),
stride=(1, 1, 1),
padding=(0, 0, 0),
bias=False,
)
if self.bn_lin5_on:
self.lin_5_bn = norm_module(
num_features=dim_out, eps=self.eps, momentum=self.bn_mmt
)
self.lin_5_relu = nn.ReLU(self.inplace_relu)
if self.dropout_rate > 0.0:
self.dropout = nn.Dropout(self.dropout_rate)
# Perform FC in a fully convolutional manner. The FC layer will be
# initialized with a different std comparing to convolutional layers.
self.projection = nn.Linear(dim_out, self.num_classes, bias=True)
# Softmax for evaluation and testing.
if self.act_func == "softmax":
self.act = nn.Softmax(dim=4)
elif self.act_func == "sigmoid":
self.act = nn.Sigmoid()
else:
raise NotImplementedError(
"{} is not supported as an activation"
"function.".format(self.act_func)
)
首先是生成了self.conv_5,此模块输入通道是192,输出是432,对应的是论文结构图中的conv5。
接着是生成一个kernel大小为16x7x7的平均池化层,在conv5的输出对应的大小为16x7x7,因此这里是一个全局池化层。
接着生成的是self.lin_5,虽然此处声明的是conv3d,但是由于经过平均池化后特征图的大小已为1,因此就是一个全连接层,输入大小为432,输出大小为2048。
最后self.projection是将2048的特征向量运算输出长度为类别数的向量。至此模型构建完毕。
2.2 数据输入
模型构建完毕后我们下一个关心的就是数据输入的格式。行为识别的输入当然就是视频流了,但是输入的视频长度,以及训练时作了何种预处理,了解这些步骤有助于我们更加深入了解模型。
所以我们要看的就是dataset的构建以及读取数据的形式。dataloader的声明在train_net.py中的train函数中
train_loader = loader.construct_loader(cfg, "train")
进入此函数之后可以看到首先是构建了dataset,然后再构建dataloader并返回
def construct_loader(cfg, split, is_precise_bn=False):
"""something"""
dataset = build_dataset(dataset_name, cfg, split)
"""something"""
dataset生成完之后下边还有一堆代码用于创建dataloader时的选项,不过按照默认设置,collate_fn和worker_init_fn都是none,因此只是简单创建dataloader而已。因此读取数据的核心就在于dataset的构建与读取。
进入build_dataset后发现其构建的函数在kinetics.py的Kinetics类中。其中的__init__虽然较长,但是内容较简单,只是读取视频文件的路径及标签,保存在列表中。读取视频的核心代码在__getitem__函数中,因此我们先退出这个函数,进入到训练的模块中再来阅读这方面的代码。
返回到train_net中,容易发现train_epoch函数即为训练模块
train_epoch(
[train_loader,train_loader2],
model,
optimizer,
scaler,
train_meter,
cur_epoch,
cfg,
writer,
)
在train_epoch函数中,for循环读取trainloader中的数据即为读取模块,因此我们可以从这里进入到__getitem__函数中。
for cur_iter, (inputs, labels, _, meta) in enumerate(train_loader[0]):
def __getitem__(self, index):
short_cycle_idx = None
# When short cycle is used, input index is a tupple.
if isinstance(index, tuple):
index, short_cycle_idx = index
if self.mode in ["train", "val"]:
# -1 indicates random sampling.
temporal_sample_index = -1
spatial_sample_index = -1
min_scale = self.cfg.DATA.TRAIN_JITTER_SCALES[0]
max_scale = self.cfg.DATA.TRAIN_JITTER_SCALES[1]
crop_size = self.cfg.DATA.TRAIN_CROP_SIZE
if short_cycle_idx in [0, 1]:
crop_size = int(
round(
self.cfg.MULTIGRID.SHORT_CYCLE_FACTORS[short_cycle_idx]
* self.cfg.MULTIGRID.DEFAULT_S
)
)
if self.cfg.MULTIGRID.DEFAULT_S > 0:
# Decreasing the scale is equivalent to using a larger "span"
# in a sampling grid.
min_scale = int(
round(
float(min_scale)
* crop_size
/ self.cfg.MULTIGRID.DEFAULT_S
)
)
elif self.mode in ["test"]:
temporal_sample_index = (
self._spatial_temporal_idx[index]
// self.cfg.TEST.NUM_SPATIAL_CROPS
)
# spatial_sample_index is in [0, 1, 2]. Corresponding to left,
# center, or right if width is larger than height, and top, middle,
# or bottom if height is larger than width.
spatial_sample_index = (
(
self._spatial_temporal_idx[index]
% self.cfg.TEST.NUM_SPATIAL_CROPS
)
if self.cfg.TEST.NUM_SPATIAL_CROPS > 1
else 1
)
min_scale, max_scale, crop_size = (
[self.cfg.DATA.TEST_CROP_SIZE] * 3
if self.cfg.TEST.NUM_SPATIAL_CROPS > 1
else [self.cfg.DATA.TRAIN_JITTER_SCALES[0]] * 2
+ [self.cfg.DATA.TEST_CROP_SIZE]
)
# The testing is deterministic and no jitter should be performed.
# min_scale, max_scale, and crop_size are expect to be the same.
assert len({min_scale, max_scale}) == 1
else:
raise NotImplementedError(
"Does not support {} mode".format(self.mode)
)
sampling_rate = utils.get_random_sampling_rate(
self.cfg.MULTIGRID.LONG_CYCLE_SAMPLING_RATE,
self.cfg.DATA.SAMPLING_RATE,
)
函数的开头为通过配置文件声明一些变量,这些变量的意义等会随着代码的阅读将会逐渐明确。
for i_try in range(self._num_retries):
video_container = None
try:
video_container = container.get_video_container(
self._path_to_videos[index],
self.cfg.DATA_LOADER.ENABLE_MULTI_THREAD_DECODE,
self.cfg.DATA.DECODING_BACKEND,
)
except Exception as e:
logger.info(
"Failed to load video from {} with error {}".format(
self._path_to_videos[index], e
)
)
之后代码进入了一个for循环中循环10次,这是为了防止视频读取失败而设置的,如果读取视频成功则直接返回数据,失败超过10次则报错。
get_video_container函数是利用pyav来读取视频,并返回读取video_container对象,之后的操作都是基于这个对象。
下一步来到了decode函数
frames = decoder.decode(
video_container,
sampling_rate,
self.cfg.DATA.NUM_FRAMES,
temporal_sample_index,
self.cfg.TEST.NUM_ENSEMBLE_VIEWS,
video_meta=self._video_meta[index],
target_fps=self.cfg.DATA.TARGET_FPS,
backend=self.cfg.DATA.DECODING_BACKEND,
max_spatial_scale=min_scale,
use_offset=self.cfg.DATA.USE_OFFSET_SAMPLING,
)
此函数比较关键,因此必须进入看看做了什么操作。
decode函数首先执行的是pyav_decode函数
frames, fps, decode_all_video = pyav_decode(
container,
sampling_rate,
num_frames,
clip_idx,
num_clips,
target_fps,
use_offset=use_offset,
)
def pyav_decode(
container,
sampling_rate,
num_frames,
clip_idx,
num_clips=10,
target_fps=30,
use_offset=False,
):
fps = float(container.streams.video[0].average_rate)
frames_length = container.streams.video[0].frames
duration = container.streams.video[0].duration
if duration is None:
# If failed to fetch the decoding information, decode the entire video.
decode_all_video = True
video_start_pts, video_end_pts = 0, math.inf
else:
# Perform selective decoding.
decode_all_video = False
start_idx, end_idx = get_start_end_idx(
frames_length,
sampling_rate * num_frames / target_fps * fps,
clip_idx,
num_clips,
use_offset=use_offset,
)
timebase = duration / frames_length
video_start_pts = int(start_idx * timebase)
video_end_pts = int(end_idx * timebase)
frames = None
# If video stream was found, fetch video frames from the video.
if container.streams.video:
video_frames, max_pts = pyav_decode_stream(
container,
video_start_pts,
video_end_pts,
container.streams.video[0],
{"video": 0},
)
container.close()
frames = [frame.to_rgb().to_ndarray() for frame in video_frames]
frames = torch.as_tensor(np.stack(frames))
return frames, fps, decode_all_video
查看函数发现,其重要步骤在于,第一:利用get_start_end_idx函数定义了视频的开头和结尾的下标,由于输入的视频有可能过长,所以必须确定视频的开头和结尾。根据默认条件,视频帧的长度为80帧,片段是随机从视频中截取。第二:通过pyav_decode_stream利用开头和结尾下标来截取视频,从而生成frames变量,frame变量一般是一个长度为80的图片帧。
返回到decode函数中,在返回结果之前还要执行一个temporal_sampling函数,这个函数比较简单,就是将frame变量每隔5帧提取1帧,从而生成长度为16的图片帧,并返回。
decode函数已经执行完毕,下边返回到Kinetics类的__getitem__中,现在长度合适的frame变量已经生成完毕,但是看到之后还需要执行一个utils.spatial_sampling函数。
def spatial_sampling(
frames,
spatial_idx=-1,
min_scale=256,
max_scale=320,
crop_size=224,
random_horizontal_flip=True,
inverse_uniform_sampling=False,
aspect_ratio=None,
scale=None,
motion_shift=False,
):
assert spatial_idx in [-1, 0, 1, 2]
if spatial_idx == -1:
if aspect_ratio is None and scale is None:
#frames = F.interpolate(frames,crop_size)
frames, _ = transform.random_short_side_scale_jitter(
images=frames,
min_size=min_scale,
max_size=max_scale,
#min_size=224,
#max_size=224,
inverse_uniform_sampling=inverse_uniform_sampling,
)
frames, _ = transform.random_crop(frames, crop_size)
else:
transform_func = (
transform.random_resized_crop_with_shift
if motion_shift
else transform.random_resized_crop
)
frames = transform_func(
images=frames,
target_height=crop_size,
target_width=crop_size,
scale=scale,
ratio=aspect_ratio,
)
if random_horizontal_flip:
frames, _ = transform.horizontal_flip(0.5, frames)
else:
# The testing is deterministic and no jitter should be performed.
# min_scale, max_scale, and crop_size are expect to be the same.
assert len({min_scale, max_scale}) == 1
frames, _ = transform.random_short_side_scale_jitter(
frames, min_scale, max_scale
)
frames, _ = transform.uniform_crop(frames, crop_size, spatial_idx)
return frames
此函数主要是为了数据增强,随机裁切原视频。下边看一下它是怎样做的。
首先调用了random_short_side_scale_jitter函数,将视频的短边按比例随机缩放至min_size至max_size之间,这里默认的min_size和max_size分别是256和312。然后随机裁切到224x224大小。
最终返回的frame大小是3x16x224x224。至此,数据处理完毕,返回的frame即可用于模型输入。
3. 小结
X3D论文可以在这里查阅,解读论文的文章也有很多,我就不重复写了。小结一下就是,论文主要是通过坐标下降法,在baseline的基础上搜索各种变量对模型准确率和速度的影响,最终生成准确率高,计算量低的模型。我利用这个模型去做打架行为识别,利用几个公开的数据集去做,准确率可以达到90%以上,速度也很快,应该是可以直接使用了。经过我测试,忽略掉预处理的时间,模型在P40GPU上推理一段视频的时间大概是0.02秒,测量得比较粗糙可能并不准确,不过应该是这个数量级。为了加深论文的理解,也对代码作了较为深入的阅读,主要了解了模型是如何构建,以及预处理的过程。阅读的过程是利用VScode的debug模式,方便了解代码的执行过程以及变量的情况,否则的话光阅读是很难去消化的。当然还有很多部分没有来得及去详细了解,包括测试部分,以及demo制作部分,以后如果还有时间的话再继续补充