我们提出的论文针对了三个问题。
根据[1]中的描述,对于采用的GTEA、50Salads、Breakfast这几个数据集都是通过[2]中提供的双流膨胀三维卷积(Two-Stream Inflated 3D ConvNet)提取得到。
该文研究了当前对于视频分析的主流网络,并提出了自己的模型。
常用的视频分析架构的区别在于
2DCNN从每一帧中提取特征,之后通过LSTM来构建时间结构。
该网络的输入从25FPS的视频中采集视频段,每隔5帧采集一帧作为输入。
训练的时候输入的视频段时长为25帧,测试的时候,从一个视频截取2个视频段,预测结果取平均。
是一种自然的视频建模方法,和标准的卷积网络一样,但是带有spatio-temporal filters。
[2]中指出了他们对这一卷积的特征发现:这一类模型能直接创建时空数据的分层表示。但是问题是,相比2DConvNets需要更多的参数,因此难以训练。
训练时输入的视频段长度为16帧,测试的时候,一个视频分为25段,预测结果为所有视频段的平均。
第一种结构中的LSTM能够对高层的变化进行建模,但是却忽略了在很多情况下至关重要的低层特征。
解决思路是对视频的短时间快照进行建模,具体而言是将单独的RGB帧和十个光流帧的预测结果进行平均。这些预测结果是各自通过ConvNet的输出。
训练的时候appearance流的输入为随机采样的一帧,motion流为10帧堆叠的光流。测试的时候视频分为25段,预测结果为所有视频段结果的平均。
还有一个改进版本是将两个Conv的输出通过卷积融合。
训练的时候,网络的输入为5帧RGB视频帧和50帧光流。测试的时候是一个视频分为5段,预测结果为所有视频段结果的平均。
3D ConvNets可以直接从RGB流中学习时间模式,但是通过包含光流,性能可以得到很大的提升。
一个网络针对RGB输入进行训练,另一个针对携带优化的平滑流信息进行训练。分别训练,并对预测进行平均。
训练的时候是64帧RGB,64帧光流,测试时扩展为250帧,结果为所有视频段的平均。
总结来看,模型的输出有直接输出、平均融合输出以及卷积融合输出。
训练时,都是定长的小片段。测试时,都是切分视频,逐段预测,平均输出。
我从这位大哥的博客中代码分析了解到了这一模型的输入和输出。
输入是(8,64,224,224,3),分别代表着batch_size,帧数,图像大小。
输出是(8,N),N是输出的类别数。
在输出之前,数据的shape为(8,3,1,1,1024)。
但是观察视频和数据,发现长度一致,说明应该是从I3D模型中直接提取了全部帧的特征,而没有将帧进行压缩(64->3)。
我们的数据的维度为2048,前1024为从RGB流中得到的特征,后1024为从光流中得到的特征。
我们观察到数据中,帧的变化十分缓慢,我们称为interframe stickness,这样会造成一定程度的过拟合(这里有详细的产生过拟合的原因)。
过拟合的表现为训练效果好,测试效果差,这实际上是一种泛化能力的缺陷。
本质上是因为模型根据数据学到的判别式存在缺陷。可能是因为模型(模型过于复杂)的原因,也可能是因为数据(存在噪声、数据有限,总之就是不能反映真实分布)的原因。
有些动作快,有些动作慢,interframe stickness会放缓帧与帧之间的动作,由此产生两种帧:
这就像一个界,快的动作被放慢后能得到更充分地分析,慢的动作很容易越界,从而产生多帧类似的帧,可以理解为有效延缓和无效延缓。
简而言之,就是有些动作就已经够慢了或者没必要,interframe stickness凭空又给多了些对performance没帮助的帧过来,反而削弱了动作的变化。
分出这两种帧,是基于[3]、[4]这两篇文章。
Clipbert[3]中认为假设稀疏片段已经捕捉到了视频中的关键视觉或语义信息,因为连续的片段通常包含来自连续场景的相似语义,所以,少量的片段就足矣得到好的结果。
我们认为本身少数的关键帧就能得到好的分类结果,所以类比着,选取其中的关键帧就足矣。
SCSampler(显著剪辑采样器)[4]中提及视频通常包含变化很少但持续时间很长的片段,这些片段没什么意义。模型大量地识别这些内容,会导致次优的识别准确性。
[4]给出的思路是通过SCSampler识别关键帧,把不重要的帧给删掉再给动作分类器。
无效帧就像噪声一样,混乱的标签、大量无意义的帧,会导致真实分布产生偏差,从而导致过拟合。
因为TAS任务要求输入输出等长,所以删掉帧是比较困难的。
我给出的思路是删去SSTCN的前几层,这样初始膨胀因子比较大,能让关键帧更多地、更快地充斥在逐层的特征当中。
尤其是对于无效帧,可以通过远离边界的有确定语义的帧来辅助识别。
当前视频分割领域存在一些基于边界进行后处理的模型,比如ASRF、BCN,都有边界回归分支。边界模糊是这些模型中的一大缺陷。
interframe stickness 造成了帧之间的相似性,因为模型只采三帧,在底层往往容易陷入相似帧的包围,即使是高层,对于长片段而言,这点感受野也是不够的。我们希望能从全视频的角度来区分边界。
随着膨胀因子的增大,很容易丢失一些细节信息,这时膨胀卷积的固有缺陷。同时每一帧只通过三帧得到,如此少的信息量对于细节的追求更遥不可及。
我们这里只介绍GTAM和LTAM。在介绍这个之前,先介绍Transformer。
关于Transformer的介绍,我们来看这篇文章。
Attention机制既有并行性又能保证对所有信息的吸收。
因为对每个词是并行处理的,所以需要一个模块来衡量word位置有关的信息。
作者给了两种思路:
公式为:
P E ( p o s , 2 i ) = sin p o s 1000 0 2 i d m o d e l PE_{(pos,2i)}=\sin\frac{pos}{10000^{\frac{2i}{d_{model}}}} PE(pos,2i)=sin10000dmodel2ipos
P E ( p o s , 2 i + 1 ) = cos p o s 1000 0 2 i d m o d e l PE_{(pos,2i+1)}=\cos\frac{pos}{10000^{\frac{2i}{d_{model}}}} PE(pos,2i+1)=cos10000dmodel2ipos
p o s pos pos表示位置, i , d m o d e l i,d_{model} i,dmodel表示维度。
两种选择的效果是一致的。但是后者更能关注相对位置信息,因为位置之间的关系可以表示成线性的,越远差越大。
另外这种编码的好处还有:
Encoder由很多Encoder Layers串联而成,每个Layer分为:
论文中是8个Self Attention组成多头自注意力模块。
关于这一部分的详细计算可以参考这里。
输入X通过与不同的、可学习的参数矩阵W得到Q(query)、K(key)、V(value),他们的维度是(batch,L,d_model);输出的结果也是这样。
每个帧既可以看作借款者(k)也可以看作贷款者(q),当他身为贷款者(q)时,跑去和每一个借款者问问愿意给自己借多少( q k T qk^T qkT),然后从他们手中借出这些钱( q k T v qk^Tv qkTv)。
多头的意思就是将embedding之后的x按维度512切分成8个,对每个分别作self- attention,然后再拼到一起。
公式为 L a y e r N o r m ( x + S u b l a y e r ( x ) ) LayerNorm(x+Sublayer(x)) LayerNorm(x+Sublayer(x))。Sublayer就是多头注意力模块和FFN。
分为两层,第一层是一个线性激活函数,第二层是一个ReLU。
公式为 F F N ( x ) = R e L U ( x W 1 + b 1 ) W 2 + b 2 FFN(x)=ReLU(xW_1+b_1)W_2+b2 FFN(x)=ReLU(xW1+b1)W2+b2
Decoder部分不做介绍。
class PositionalEncoding(nn.Module):
"Implement the PE function."
def __init__(self, d_model, dropout, max_len=10000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
# Compute the positional encodings once in log space.
pe = torch.zeros(max_len, d_model)
position = torch.arange(0., max_len).unsqueeze(1)
div_term = torch.exp(torch.arange(0., d_model, 2) *
-(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.t()
pe = pe.unsqueeze(0)
self.register_buffer('pe', pe)
def forward(self, x):
x = x + Variable(self.pe[:,:, :x.size(2)],
requires_grad=False)
return self.dropout(x)
class MultiStageTCN(nn.Module):
def __init__(self, in_channel, n_classes, stages,
n_features=64, dilated_n_layers=10, kernel_size=15):
super().__init__()
self.fstage = stages[0]
if stages[0] in ['att' , 'local']:
self.pe = PositionalEncoding(n_features,0.5)
self.pe1 = PositionalEncoding(in_channel,0.5)
self.conv_in = nn.Conv1d(in_channel,n_features,1)
self.conv_out = nn.Conv1d(n_features,n_classes,1)
self.local_att = SelfLocalAttention(n_features,151)
layers = [
DilatedResidualLayer(2**i, n_features, n_features) for i in range(dilated_n_layers)]
self.layers = nn.ModuleList(layers)
self.activation = nn.Softmax(dim=1)
elif stages[0] == 'dilated': # 起始层数为2
self.stage1 = SingleStageTCN1(in_channel, n_features, n_classes, dilated_n_layers)
elif stages[0] == 'ed': # EDTCN
self.stage1 = EDTCN(in_channel, n_classes)
elif stages[0] == 'dual': # MSTCN++
mstcn2pglayer = 11
self.stage1 = MS_SingleStageTCN(in_channel,n_features,n_classes,mstcn2pglayer)
else:
print("Invalid values as stages in Mixed Multi Stage TCN")
sys.exit(1)
if len(stages) == 1:
self.stages = None
else:
self.stages = []
for stage in stages[1:]:
if stage == 'dilated': # 不膨胀
self.stages.append(SingleStageTCN(
n_classes, n_features, n_classes, dilated_n_layers))
elif stage == 'ed':
self.stages.append(
EDTCN(n_classes, n_classes, kernel_size=kernel_size))
else:
print("Invalid values as stages in Mixed Multi Stage TCN")
sys.exit(1)
self.stages = nn.ModuleList(self.stages)
self.dropout = nn.Dropout(0.5)
def forward(self, x):
if self.training:
x = self.dropout(x)
# # for training
outputs = []
if self.fstage in ['dilated' , 'dual'] :
out = self.stage1(x)
elif self.fstage == 'att':
out = self.conv_in(x)
pe1 = self.pe1(x)
q = self.conv_in(x+pe1)
k = self.conv_in(x+pe1)
v = self.conv_in(x+pe1)
q = q.squeeze()
k = k.squeeze()
v = v.squeeze()
attn = torch.matmul(q.t(),k)
attn = attn/64
attn = self.activation(attn)
att_output = torch.matmul(attn, v.t())
att_output = att_output.t()
att_output = torch.unsqueeze(att_output,0)
for layer in self.layers:
out = layer(out)
out = self.conv_out(out + att_output)
elif self.fstage == 'local':
out = self.conv_in(x)
pe = self.pe(out)
local_att = self.local_att(out+pe)
for layer in self.layers:
out = layer(out)
out = self.conv_out(out + local_att)
outputs.append(out)
if self.stages is not None:
for stage in self.stages:
out = stage(F.softmax(out, dim=1))
outputs.append(out)
return outputs
else:
# for evaluation
if self.fstage in ['dilated' , 'dual'] :
out,attMatrix = self.stage1(x)
elif self.fstage == 'att':
out = self.conv_in(x)
pe1 = self.pe1(x)
q = self.conv_in(x+pe1)
k = self.conv_in(x+pe1)
v = self.conv_in(x+pe1)
q = q.squeeze()
k = k.squeeze()
v = v.squeeze()
attn = torch.matmul(q.t(),k)
attn = attn/64
attn = self.activation(attn)
att_output = torch.matmul(attn, v.t())
att_output = att_output.t()
att_output = torch.unsqueeze(att_output,0)
for layer in self.layers:
out = layer(out)
out = self.conv_out(out + att_output)
elif self.fstage == 'local':
out = self.conv_in(x)
pe = self.pe(out)
for layer in self.layers:
out = layer(out)
local_att = self.local_att(out+pe)
out = self.conv_out(out + local_att)
if self.stages is not None:
for stage in self.stages:
out = stage(F.softmax(out, dim=1))
return out,attMatrix
GTAM和LTAM的输入都是从Sparse Sampling得到。
应用于BCN、ASRF。
elif self.fstage == 'att':
out = self.conv_in(x)
pe1 = self.pe1(x)
q = self.conv_in(x+pe1) # 把编码和输入直接相加
k = self.conv_in(x+pe1)
v = self.conv_in(x+pe1)
q = q.squeeze() # (1,C,T) -> (C,T)
k = k.squeeze()
v = v.squeeze()
attn = torch.matmul(q.t(),k) # Q^T · K -> (T,T)
attn = attn/64 # 归一化
attn = self.activation(attn) # softmax
att_output = torch.matmul(attn, v.t()) # A · V^T -> (T,C)
att_output = att_output.t() # (C,T)
att_output = torch.unsqueeze(att_output,0) # (1,T,C)
for layer in self.layers: # 这是SparseSampling后单独的分支
out = layer(out)
out = self.conv_out(out + att_output) # 将两个分支的结果相加输出
这块代码很正常。
应用于MSTCN、MSTCN++、BCN、ASRF。
elif self.fstage == 'local':
out = self.conv_in(x)
pe = self.pe(out)
for layer in self.layers:
out = layer(out)
local_att = self.local_att(out+pe) # 我们把目光聚焦于SelfLocalAttention
out = self.conv_out(out + local_att) # 还是两个分支相加
class SelfLocalAttention(nn.Module):
def __init__(
self,
n_features: int,
win_size: int,
) -> None:
super().__init__()
self.win_size = win_size
self.n_features = n_features
self.unfold = UnfoldTemporalWindows(self.win_size,1,1)
self.activation = nn.Softmax(dim = 0)
# self.B=nn.Parameter(torch.randn((self.win_size,1),requires_grad=True))
def forward(self, out: torch.Tensor):
# with torch.no_grad():
q = out # 编码好的结果,都没有过一遍卷积
k = self.unfold(out)
v = k # (1,C,T,win_size)
q = q.view(-1,1) # (C*T,1)
k = k.view(-1,self.win_size) # (C*T,win_size)
att = torch.matmul(k.t(),q) # (win_size,1)
att = att/(self.win_size*64)
att = self.activation(att)
att = torch.matmul(v,att) # (1,C,T,1)
att = att.view(1,self.n_features,-1)
return 0.5*(att + out)
class UnfoldTemporalWindows(nn.Module):
'''
Unfold: catch local temporal attention
'''
def __init__(self, window_size, window_stride, window_dilation=1):
super().__init__()
self.window_size = window_size
self.window_stride = window_stride
self.window_dilation = window_dilation
self.padding = (window_size + (window_size-1) * (window_dilation-1) - 1) // 2
self.unfold = nn.Unfold(kernel_size=(self.window_size, 1),
dilation=(self.window_dilation, 1),
stride=(self.window_stride, 1),
padding=(self.padding, 0))
def forward(self, x):
x = torch.unsqueeze(x,3)
# Input shape: (N,C,T,V), out: (N,C,T,V*window_size)
N, C, T, V = x.shape
x = self.unfold(x)
# Permute extra channels from window size to the graph dimension; -1 for number of windows
x = x.view(N, C, self.window_size, -1, V).permute(0,1,3,2,4).contiguous()
x = x.view(N, C, -1, self.window_size * V)
return x
详细内容可以参考这里。
每一帧,都是一个C*Window_size的切片。
Unfold函数输入(N,C,H,W)格式的数据,输出(N,C*H*W,L)格式的数据。
这里我们将一维数据视为(C,1,T)格式的数据,得到了(N,C*window_size,T)格式的输出。
k最后成为一个(C*T,window_size)的数据和shape为(C*T,1)的q相乘得到(1,window_size)大小的注意力矩阵。
v的大小为(1,C,T,window_size),可以想象为一个立方体,相乘的结果为(1,C,T,1),相当于宽度维度做了一个压缩。
[1] Yazan Abu Farha and Jurgen Gall, “Ms-tcn: Multi-stage temporal convolutional network for action segmentation,” in Proceedings of the IEEE/CVF Conference on Computer Vision and
Pattern Recognition, 2019, pp. 3575–3584.
[2] Joao Carreira and Andrew Zisserman. Quo vadis, action
recognition? A new model and the kinetics dataset. In
IEEE Conference on Computer Vision and Pattern Recognition (CVPR), pages 4724–4733, 2017.
[3] Lei J, Li L, Zhou L, et al. Less is more: Clipbert for video-and-language learning via sparse sampling[C]//Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2021: 7331-7341.
[4] Bruno Korbar, Du Tran, and Lorenzo Torresani, “Scsampler:
Sampling salient clips from video for efficient action recognition,” in Proceedings of the IEEE/CVF International Conference on Computer Vision, 2019, pp. 6232–6242.