Double Attention Network Based On Sparse Sampling

问题及解决方案

我们提出的论文针对了三个问题。

缺乏考虑帧间相似性

回顾I3D

根据[1]中的描述,对于采用的GTEA、50Salads、Breakfast这几个数据集都是通过[2]中提供的双流膨胀三维卷积(Two-Stream Inflated 3D ConvNet)提取得到。

该文研究了当前对于视频分析的主流网络,并提出了自己的模型。

常用的视频分析架构的区别在于

  1. 卷积是基于2D的还是3D的?
  2. 输入是RGB视频还是预先计算好的光流。

请添加图片描述

ConvNet+LSTM

2DCNN从每一帧中提取特征,之后通过LSTM来构建时间结构。

该网络的输入从25FPS的视频中采集视频段,每隔5帧采集一帧作为输入。

训练的时候输入的视频段时长为25帧,测试的时候,从一个视频截取2个视频段,预测结果取平均。

3D ConvNets

是一种自然的视频建模方法,和标准的卷积网络一样,但是带有spatio-temporal filters。

[2]中指出了他们对这一卷积的特征发现:这一类模型能直接创建时空数据的分层表示。但是问题是,相比2DConvNets需要更多的参数,因此难以训练。

训练时输入的视频段长度为16帧,测试的时候,一个视频分为25段,预测结果为所有视频段的平均。

Two-Stream Network

第一种结构中的LSTM能够对高层的变化进行建模,但是却忽略了在很多情况下至关重要的低层特征。

解决思路是对视频的短时间快照进行建模,具体而言是将单独的RGB帧和十个光流帧的预测结果进行平均。这些预测结果是各自通过ConvNet的输出。

训练的时候appearance流的输入为随机采样的一帧,motion流为10帧堆叠的光流。测试的时候视频分为25段,预测结果为所有视频段结果的平均。

还有一个改进版本是将两个Conv的输出通过卷积融合。

训练的时候,网络的输入为5帧RGB视频帧和50帧光流。测试的时候是一个视频分为5段,预测结果为所有视频段结果的平均。

Two-Stream Inflated 3D ConvNets

3D ConvNets可以直接从RGB流中学习时间模式,但是通过包含光流,性能可以得到很大的提升。

一个网络针对RGB输入进行训练,另一个针对携带优化的平滑流信息进行训练。分别训练,并对预测进行平均。

训练的时候是64帧RGB,64帧光流,测试时扩展为250帧,结果为所有视频段的平均。

预测方式

请添加图片描述
总结来看,模型的输出有直接输出、平均融合输出以及卷积融合输出。

训练时,都是定长的小片段。测试时,都是切分视频,逐段预测,平均输出。

所谓的I3D特征是什么?

我从这位大哥的博客中代码分析了解到了这一模型的输入和输出。

输入是(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会放缓帧与帧之间的动作,由此产生两种帧:

  1. 冗余帧:肉眼看到的动作缓慢片段,在数据中实际更加缓慢;
  2. 无效帧:从最后一帧有效动作到第一帧有效动作之间的缓慢过度时没有意义的,同时也造成了标签存在噪声。

这就像一个界,快的动作被放慢后能得到更充分地分析,慢的动作很容易越界,从而产生多帧类似的帧,可以理解为有效延缓和无效延缓。

简而言之,就是有些动作就已经够慢了或者没必要,interframe stickness凭空又给多了些对performance没帮助的帧过来,反而削弱了动作的变化。

提出的理由

分出这两种帧,是基于[3]、[4]这两篇文章。

冗余帧

Clipbert[3]中认为假设稀疏片段已经捕捉到了视频中的关键视觉或语义信息,因为连续的片段通常包含来自连续场景的相似语义,所以,少量的片段就足矣得到好的结果。

我们认为本身少数的关键帧就能得到好的分类结果,所以类比着,选取其中的关键帧就足矣。

无效帧

SCSampler(显著剪辑采样器)[4]中提及视频通常包含变化很少但持续时间很长的片段,这些片段没什么意义。模型大量地识别这些内容,会导致次优的识别准确性。

[4]给出的思路是通过SCSampler识别关键帧,把不重要的帧给删掉再给动作分类器。

请添加图片描述
无效帧就像噪声一样,混乱的标签、大量无意义的帧,会导致真实分布产生偏差,从而导致过拟合。

我们的解决方案

因为TAS任务要求输入输出等长,所以删掉帧是比较困难的。

我给出的思路是删去SSTCN的前几层,这样初始膨胀因子比较大,能让关键帧更多地、更快地充斥在逐层的特征当中。

尤其是对于无效帧,可以通过远离边界的有确定语义的帧来辅助识别。

缺乏全局感受野

当前视频分割领域存在一些基于边界进行后处理的模型,比如ASRF、BCN,都有边界回归分支。边界模糊是这些模型中的一大缺陷。

interframe stickness 造成了帧之间的相似性,因为模型只采三帧,在底层往往容易陷入相似帧的包围,即使是高层,对于长片段而言,这点感受野也是不够的。我们希望能从全视频的角度来区分边界。

高层对于有效信息的缺失

随着膨胀因子的增大,很容易丢失一些细节信息,这时膨胀卷积的固有缺陷。同时每一帧只通过三帧得到,如此少的信息量对于细节的追求更遥不可及。

Double Attention Network

请添加图片描述

我们这里只介绍GTAM和LTAM。在介绍这个之前,先介绍Transformer。

Transformer

关于Transformer的介绍,我们来看这篇文章

思想

  1. RNN系列的计算是顺序的,所以说限制了模型的并行能力;
  2. 顺序计算的过程中信息会丢失,LSTM的门结构缓解但并不解决。

Attention机制既有并行性又能保证对所有信息的吸收。

结构

在这里插入图片描述

位置编码

因为对每个词是并行处理的,所以需要一个模块来衡量word位置有关的信息。

作者给了两种思路:

  1. 通过模型来学习;
  2. 通过公式来计算。

公式为:
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} idmodel表示维度。

两种选择的效果是一致的。但是后者更能关注相对位置信息,因为位置之间的关系可以表示成线性的,越远差越大。

另外这种编码的好处还有:

  1. 每个时间步输出独一无二的编码;
  2. 不同长度句子之间,任意两个时间步之间的距离应该一致;
  3. 值是有界的;
  4. 能泛化到更长的句子;
  5. 具有确定性。

Encoder

Encoder由很多Encoder Layers串联而成,每个Layer分为:

  1. Multi-Head Self Attention
  2. Feed-Forward Network
Multi-Head Self Attention

论文中是8个Self Attention组成多头自注意力模块。

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,然后再拼到一起。

Add&Norm

公式为 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。

  1. 进模块;
  2. 残差连接;
  3. 归一化。
Feed Forward Network

分为两层,第一层是一个线性激活函数,第二层是一个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部分不做介绍。

Positional Encoding

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)

DASS

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得到。

GTAM

应用于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) # 将两个分支的结果相加输出

这块代码很正常。

LTAM

应用于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

Unfold函数

详细内容可以参考这里

每一帧,都是一个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.

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

右边是我女神

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值