3D卷積神經網絡

首先,我們先參考Tensorflow深度學習算法整理 卷積神經網絡回憶一下2D卷積。

3D卷積如上圖所示,3D卷積輸入多了深度C這個維度,輸入是高度H*寬度W*深度C的三維矩陣。3D卷積核的深度小於輸入層深度,這是3D卷積核跟2D卷積核最本質的區別。因此,3D 卷積核可以在所有三個方向(圖像的高度、寬度、通道)上移動,而2D卷積核只能在特徵圖的高、寬平面上移動。在每個位置,逐元素的乘法和加法都會提供一個數值。因爲3D卷積核是滑過一個 3D 空間,所以輸出數值也按 3D 空間排布。也就是說輸出是一個 3D 數據。3D卷積被普遍用在視頻分類,三維醫學圖像分割等場景中。

我們將時間維度看成是第三維,這裏是對連續的四幀圖像進行卷積操作,3D卷積是通過堆疊多個連續的幀組成一個立方體,然後在立方體中運用3D卷積核。在這個結構中,卷積層中每一個特徵map都會與上一層中多個鄰近的連續幀相連,因此捕捉運動信息。

3D卷積和多通道卷積的區別

多通道卷積屬於2D卷積範疇,它的卷積核一定是一個2D卷積核,無論輸入的feature map的有多少個通道,通過2D卷積核的輸出一定是一個單通道的結果,其計算方式爲將單通道的卷積核對所有通道同時進行卷積,將所有卷積結果再相加。如果需要輸出多個通道的結果只能夠通過增加捲積核來完成,輸出多少個通道就增加多少個卷積核。

3D卷積核本身就是多通道的,其本身的通道數一定是小於輸入的feature map的通道數的。3D卷積運算的時候不是對feature map的所有通道同時進行卷積,而是將feature map的所有通道作爲一個整體進行卷積,得到的也是一個多通道的輸出結果

視頻分類

雖然視頻本質上是連續幀的二維圖像,但是如果將一段視頻切片當做一個整體,將其數據升級到三維,三維卷積神經網絡在視頻方面應用最廣泛的就是進行視頻分類。與二維神經網絡相同,三維神經網絡也包括輸入層,卷積層,池化層,全連接層,損失函數層等網絡層。

光流(optical flow)

通過時序上相鄰幀計算像素移動的方向和速度。

通過計算視頻幀沿水平、豎直和時間方向的梯度進行推斷。在右圖中,不同顏色表示不同方向的運動,顏色深淺表示速度快慢。

input—>H1:

神經網絡的輸入爲7張大小爲60*40的連續幀(每幀都是單通道灰度圖),7張幀通過事先設定硬核(hardwired kernels,一種編碼方式,注意不是3D卷積核)獲得5種不同特徵:灰度、x方向梯度、y方向梯度、x方向光流、y方向光流,前面三個通道的信息可以直接對每幀分別操作獲取,後面的光流(x,y)則需要利用兩幀的信息才能提取,因此H1層的特徵maps數量:(7+7+7+6+6=33)[解釋:7個灰度(輸入是7個),7個x方向梯度,7個y方向梯度,6個x方向光流(因爲是兩幀作差得到的,所以7個,相互兩個作差就是6個),6個y方向光流],特徵maps的大小依然是60* 40。所以這裏纔是對網絡的初始輸入特徵圖爲33*60*40.

H1—>C2:

兩個7*7*3的3D卷積覈對5種特徵分別進行卷積,由原來的7+7+7+6+6變成了5+5+5+4+4=23,獲得兩個系列,每個系列5個通道(7*7表示空間維度,3表示時間維度,也就是每次操作3幀圖像),同時,爲了增加特徵maps的個數,在這一層採用了兩種不同的3D卷積核,因此C2層的特徵maps數量爲:(((7-3)+1)*3+((6-3)+1)*2)*2=23*2。這裏右乘的2表示兩種卷積核,這是時間上的卷積。而空間上的卷積特徵maps的大小爲:((60-7)+1)* ((40-7)+1)=54*34。然後爲卷積結果加上偏置套一個tanh函數進行輸出。(典型神經網。)

C2—>S3:

2x2的2D池化,下采樣。下采樣之後的特徵maps數量保持不變,因此S3層的特徵maps數量爲:23*2。特徵maps的空間大小爲:((54/2)*(34/2)=27*17

S3—>C4:

三個7*6*3的3D卷積核分別對各個系列各個通道進行卷積,由原來的5+5+5+4+4變成了3+3+3+2+2=13,獲得6個系列,每個系列依舊5個通道的大量maps。在這一層採用了三種不同的3D卷積核,因此C4層的特徵maps數量爲:(((5-3)+1)*3+((4-3)+1)*2)*6=13*6,這裏的6表示3種卷積覈對2組特徵變成了6組。而空間上的卷積特徵maps的大小爲:((27-7)+1)* ((17-6)+1)=21*12。然後爲卷積結果加上偏置套一個tanh函數進行輸出。

C4—>S5:

3x3的2D池化,下采樣。下采樣之後的特徵maps數量保持不變,因此S3層的特徵maps數量爲:13*6。特徵maps的空間大小爲:((21/3)*(12/3)=7*4

S5—>C6:

7x4的2D卷積,直接消除空間維度變成1*1。C6的特徵maps數量保持不變,依然爲13*6。再經過flatten之後變成128維的向量。加上偏置套一個tanh函數進行輸出。

C6—>output:

最後經過全連接層進行輸出。

這個是一個早期的3D卷積模型,只適合一些比較簡單的數據集。一般現在3D模型不會這麼去配置

這個是在TRECVID,KTH數據集上的實驗結果,我們可以看到它比2D卷積具有一定的優勢。

深度3DCNN模型

該模型包括了8個卷積層(3*3*3),5個最大池化層,2個全連接層,模型的輸入爲3*16*112*112(長112、寬112,3通道,16幀),pool1是一個2D池化,pool2到5都是3D池化。

  • 時間卷積核大小的比較

上圖中左圖使用的時間卷積核大小都是相同的,當時間卷積核大小爲1的時候,3D卷積就退化成了2D卷積,我們可以看到它的精度是最低的。而使用時間卷積核大小爲3的時候,模型精度是最高的。上圖中右圖中使用的是時間卷積核大小爲3與時間卷積核大小不斷遞增和遞減兩種情況的比較,我們可以看到模型的精度依然是時間卷積核大小爲3的時候最高。

PyTorch代碼

import torch
import torch.nn as nn

class C3D(nn.Module):

    def __init__(self, num_classes):
        super(C3D, self).__init__()
        self.conv1 = nn.Conv3d(3, 64, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.bn1 = nn.BatchNorm3d(64)
        self.pool1 = nn.MaxPool3d(kernel_size=(1, 2, 2), stride=(1, 2, 2))

        self.conv2 = nn.Conv3d(64, 128, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.bn2 = nn.BatchNorm3d(128)
        self.pool2 = nn.MaxPool3d(kernel_size=(2, 2, 2), stride=(2, 2, 2))

        self.conv3a = nn.Conv3d(128, 256, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.bn3a = nn.BatchNorm3d(256)
        self.conv3b = nn.Conv3d(256, 256, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.bn3b = nn.BatchNorm3d(256)
        self.pool3 = nn.MaxPool3d(kernel_size=(2, 2, 2), stride=(2, 2, 2))

        self.conv4a = nn.Conv3d(256, 512, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.bn4a = nn.BatchNorm3d(512)
        self.conv4b = nn.Conv3d(512, 512, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.bn4b = nn.BatchNorm3d(512)
        self.pool4 = nn.MaxPool3d(kernel_size=(2, 2, 2), stride=(2, 2, 2))

        self.conv5a = nn.Conv3d(512, 512, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.bn5a = nn.BatchNorm3d(512)
        self.conv5b = nn.Conv3d(512, 512, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.bn5b = nn.BatchNorm3d(512)
        self.pool5 = nn.MaxPool3d(kernel_size=(2, 2, 2), stride=(2, 2, 2), padding=(0, 1, 1))

        self.fc6 = nn.Linear(8192, 4096)
        self.fc7 = nn.Linear(4096, 4096)
        self.fc8 = nn.Linear(4096, num_classes)

        self.dropout = nn.Dropout(p=0.5)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.relu(self.bn1(self.conv1(x)))
        x = self.pool1(x)

        x = self.relu(self.bn2(self.conv2(x)))
        x = self.pool2(x)

        x = self.relu(self.bn3a(self.conv3a(x)))
        x = self.relu(self.bn3b(self.conv3b(x)))
        x = self.pool3(x)

        x = self.relu(self.bn4a(self.conv4a(x)))
        x = self.relu(self.bn4b(self.conv4b(x)))
        x = self.pool4(x)

        x = self.relu(self.bn5a(self.conv5a(x)))
        x = self.relu(self.bn5b(self.conv5b(x)))
        x = self.pool5(x)

        x = x.view(-1, 8192)
        x = self.relu(self.fc6(x))
        x = self.dropout(x)
        x = self.relu(self.fc7(x))
        x = self.dropout(x)

        x = self.fc8(x)
        return x

if __name__ == '__main__':

    inputs = torch.rand(1, 3, 16, 112, 112)
    net = C3D(num_classes=101)
    outputs = net(inputs)
    print(outputs.size())

運行結果

torch.Size([1, 101])

Tensorflow代碼

import tensorflow as tf
import numpy as np
from tensorflow.keras import layers, models

class C3D(models.Model):

    def __init__(self, num_classes):
        super(C3D, self).__init__()
        self.conv1 = layers.Conv3D(64, kernel_size=(3, 3, 3), padding='same')
        self.bn1 = layers.BatchNormalization()
        self.pool1 = layers.MaxPool3D(pool_size=(1, 2, 2), strides=(1, 2, 2), padding='valid')

        self.conv2 = layers.Conv3D(128, kernel_size=(3, 3, 3), padding='same')
        self.bn2 = layers.BatchNormalization()
        self.pool2 = layers.MaxPool3D(pool_size=(2, 2, 2), strides=(2, 2, 2), padding='valid')

        self.conv3a = layers.Conv3D(256, kernel_size=(3, 3, 3), padding='same')
        self.bn3a = layers.BatchNormalization()
        self.conv3b = layers.Conv3D(256, kernel_size=(3, 3, 3), padding='same')
        self.bn3b = layers.BatchNormalization()
        self.pool3 = layers.MaxPool3D(pool_size=(2, 2, 2), strides=(2, 2, 2), padding='valid')

        self.conv4a = layers.Conv3D(512, kernel_size=(3, 3, 3), padding='same')
        self.bn4a = layers.BatchNormalization()
        self.conv4b = layers.Conv3D(512, kernel_size=(3, 3, 3), padding='same')
        self.bn4b = layers.BatchNormalization()
        self.pool4 = layers.MaxPool3D(pool_size=(2, 2, 2), strides=(2, 2, 2), padding='valid')

        self.conv5a = layers.Conv3D(512, kernel_size=(3, 3, 3), padding='same')
        self.bn5a = layers.BatchNormalization()
        self.conv5b = layers.Conv3D(512, kernel_size=(3, 3, 3), padding='same')
        self.bn5b = layers.BatchNormalization()
        self.padding = layers.ZeroPadding3D(padding=(0, 1, 1))
        self.pool5 = layers.MaxPool3D(pool_size=(2, 2, 2), strides=(2, 2, 2), padding='valid')

        self.flatten = layers.Flatten()
        self.fc6 = layers.Dense(4096, activation='relu')
        self.fc7 = layers.Dense(4096, activation='relu')
        self.fc8 = layers.Dense(num_classes)

        self.relu = layers.ReLU()
        self.dropout = layers.Dropout(0.5)

    def call(self, x):
        x = self.relu(self.bn1(self.conv1(x)))
        x = self.pool1(x)
        x = self.relu(self.bn2(self.conv2(x)))
        x = self.pool2(x)

        x = self.relu(self.bn3a(self.conv3a(x)))
        x = self.relu(self.bn3b(self.conv3b(x)))
        x = self.pool3(x)

        x = self.relu(self.bn4a(self.conv4a(x)))
        x = self.relu(self.bn4b(self.conv4b(x)))
        x = self.pool4(x)

        x = self.relu(self.bn5a(self.conv5a(x)))
        x = self.relu(self.bn5b(self.conv5b(x)))
        x = self.padding(x)
        x = self.pool5(x)

        x = self.flatten(x)
        x = self.fc6(x)
        x = self.dropout(x)
        x = self.fc7(x)
        x = self.dropout(x)

        x = self.fc8(x)
        return x

if __name__ == '__main__':

    inputs = tf.constant(np.random.rand(1, 16, 112, 112, 3))
    net = C3D(num_classes=101)
    outputs = net(inputs)
    print(outputs.shape)

運行結果

(1, 101)

3D卷積模型分解

  • 卷積拆分與低秩近似原理

將高維卷積拆分成低維卷積,在上圖中左邊的矩形體是一個3D卷積(C是深度、Y是高度、X是寬度),而在右邊圖中,我們將其拆分爲三個一維卷積。

卷積拆分的有效性

卷積拆分可以減少參數量和計算量,上圖的3D卷積核爲DXY(寬X、高Y,深度爲D),輸出特徵大小爲THW(高H、寬W、時間爲T),那麼卷積核的參數量爲DXY相乘,計算量爲THW(DXY)相乘;我們將這個3D卷積核分解爲一個XY的2D卷積和一個時間維度爲D的1D卷積,這樣兩個卷積核的總的參數量爲XY+D,計算量爲THW(D+XY),我們可以看到一個是做乘法,一個是做加法,那麼自然做加法的量要小的多,參數量和計算量自然要小的多。

實際上我們早就接觸過了卷積的拆分,在Inception V3中

我們就是將一個n*n的2D卷積給拆分成了1*n和n*1的1D卷積,從而減少了參數量和計算量來達到縮減模型大小的作用,從而可以使用在更小的移動設備中。

  • 分解3D卷積

在上圖中是一個叫做,這裏我們假設3D卷積爲K,我們可以將其拆分成空間卷積和時間卷積這兩組卷積。在上圖的藍色長條塊就是空間卷積層,實際上就是我們經常用到的2D卷積,位於網絡的淺層,離輸入比較近,它是用來學習每幀圖像中的語義信息的,它不關心時序,所以它學習到的是表徵特徵,一般我們將圖像RGB語義信息稱爲表徵信息。

經過空間卷積層之後經過了兩個分支,上面的淡綠色的分支,它包含了轉換層,時序層和全連接層,總體上就是一個時間分支。它是學習時間相關的信息,就是運動相關的特徵。我們來看一下網絡的輸入

網絡的輸入分爲兩部分,一部分是RGB的幀,另外一部分是幀差(兩幀圖像的差值)。它有一個時間上的採樣的步長(stride),根據採樣的步長來進行採樣(每隔多少幀進行採樣)。我們將採樣的RGB的幀稱爲,幀差就爲,計算公式爲

這裏的就爲時間間隔。取相隔時序的幀,去計算它們倆的距離。包含了比較短的時序上的動作,而把所有的拼接起來又反映了一個長程的動作,稱爲。將拼接起來作爲整體的輸入。

  • 空間轉換+排列層

經過空間卷積提取完特徵之後,接下來要輸入到時間卷積層。但是格式需要轉換,因爲早期的框架不支持3D卷積,我們依然要使用2D卷積來實現時間上的卷積,將4D(通道、時間、高度、寬度)的tensor轉換成3D的tensor,通過reshape將高度、寬度合併,只保留時間維度和通道維度,再對其進行2D卷積,這樣就可以使用原有的框架。這裏還對通道的維度進行了學習,使用學習的p變換(初始爲高斯正態分佈)將通道f通過加權得到f'。當然現在無需這一步,因爲現在的框架早已經支持3D卷積。

  • 雙通道時間卷積,學習不同快慢的時間信息

經過上面的空間轉換,空間的維度已經壓縮成了1個,故使用2D卷積就可以來實現。在進入時間卷積後,我們看到它分成了兩個分支,一個是3*3的卷積,一個是5*5的卷積。現在用於去學習的一個維度是時間,卷積核的參數越大,可以看作是時間維度的感受野越大,時間維度的感受野越大可以看作是可以看到更長程的動作,更長程的動作可以理解成更慢的動作。而更快的動作只需要更小的感受野就可以了,故3*3卷積又稱爲fast分支,而5*5卷積稱爲slow分支。

經過空間卷積層之後經過了兩個分支,下面的深綠色的分支是隨機從T幀中採樣一幀學習圖像特徵,實際上這裏只是起到一個輔助模型學習的作用,並不是必須的。它的網絡的深度要更深一些,相比於與時間卷積層共用的空間卷積層,意味着可以學習更加抽象的視覺特徵。

  • 加權融合策略(Sparsity Concentration Index (SCI))

上圖中SCI(p)中的表示某一段視頻,將該視頻分爲第j類的概率,max表示取最高的概率,值越大越好,最好等於1。SCI(p)爲可靠性因子,反映了模型的可靠性,通過SCI()進行一個加權,它用到了多個圖像值來進行訓練的,C表示空間裁剪數量(9個位置+2種翻轉=18個圖像塊),每一個都可以預測出來一個概率向量,需要對這些概率向量進行融合,不同的圖像塊的可靠性SCI(p)不一樣。將不同圖像的可靠性SCI(p)計算出來之後進行融合,得到一個更好的。然後再計算有M個p,M表示切片的數量,切片就是之前說的按照一個步長進行採樣,採樣的數量就是切片的數量。基於每一個切片計算出來的,再在這麼多個中找出一個概率最大的當作最終的分類結果。

  • 不同策略的比較

上圖的左邊的表是該模型不同配置的不同數據集的精度。我們之前介紹的是最下面的SCI的融合策略,它還有其他的融合策略——倒數第三的只選擇單獨的圖像塊的策略、倒數第二的對所有圖像塊的平均融合、正數第一的僅使用空間卷積的策略和正數第二的只使用時間卷積的策略等。經過該表我們看到綜合了所有策略的SCI融合策略所取得的精度是最好的。

上圖的右邊的表是不同的主流模型的比較,比如倒數第四的雙流法,還有一些傳統方法,方法能取得更好的性能。

  • 特徵降維和可視化分析

作者將進行了降維,上圖的左邊的左上部分是空間維度的降維圖形,右上部分是時間維度的降維圖形,而下面的部分則是包含了空間維度和時間維度的降維圖形,我們可以看到下面的圖形具有更好的聚分辨識性。上圖的右邊是反捲積可視化方法,反捲積可視化方法可以看到什麼樣的特徵可以激活卷積核。

殘差3D卷積Pseudo-3D

相比於,還有更多的3D卷積模型效能比它要好。比如ResNet的3D版本。

上圖中的a圖就是一個標準的2D ResBlock,b圖就是3D版本的ResBlock,而c圖和d圖都是3D ResBlock的變形。在b圖中,1*3*3是空間卷積,3*1*1是時間卷積,它們是一種串行的方式;而在c圖中,它們是一種並行的方式;d圖是增加了分支,與b圖相比,b圖的輸出結果只與時間卷積有關係,而d圖是將時間卷積的結果加上空間卷積的殘差。這三種3D卷積各有特點,實際上作者最終採用的架構是將這三種單元進行交替的串聯使用。它們其實都是一種僞3D模型。

  • 不同架構比較,特徵降維與可視化

上圖中左邊的表第一行表示2D卷積的ResNet-50,P3D-A、P3D-B、P3D-C表示分別只使用A、B、C模塊,而P3D ResNet爲使用A、B、C串行的架構。它們的參數量相當,速度也相當。從結果來看,使用三種結構串行的P3D ResNet的精度最高。

上圖中右邊的圖爲降維可視化,各個顏色簇分的越開表示特徵越有效。

  • 與其他模型比較

上面的表分成3部分,第一部分是端到端模型;第二種是用3D卷積提取特徵,用SVM進行分類訓練;第三種是和一些方法進行融合。總的來說P3D ResNet取得了最好的效果。

Pytorch代碼

import torch
import torch.nn as nn
import collections
from itertools import repeat

class SpatioTemporalConv(nn.Module):
    # 時空卷積
    def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, bias=False):
        super(SpatioTemporalConv, self).__init__()
        _triple = self._ntuple(3)
        kernel_size = _triple(kernel_size)
        stride = _triple(stride)
        padding = _triple(padding)

        self.temporal_spatial_conv = nn.Conv3d(in_channels, out_channels, kernel_size,
                                               stride=stride, padding=padding, bias=bias)
        self.bn = nn.BatchNorm3d(out_channels)
        self.relu = nn.ReLU()

    def _ntuple(self, n):
        def parse(x):
            if isinstance(x, collections.abc.Iterable):
                return tuple(x)
            return tuple(repeat(x, n))

        return parse

    def forward(self, x):
        x = self.bn(self.temporal_spatial_conv(x))
        x = self.relu(x)
        return x

class SpatioTemporalResBlock(nn.Module):
    # 時空ResBlock
    def __init__(self, in_channels, out_channels, kernel_size, downsample=False):
        super(SpatioTemporalResBlock, self).__init__()
        # 是否下采樣
        self.downsample = downsample
        # same padding
        padding = kernel_size // 2
        if self.downsample:
            # 1*1*1卷積,並且進行降採樣
            self.downsampleconv = SpatioTemporalConv(in_channels, out_channels, 1, stride=2)
            self.downsamplebn = nn.BatchNorm3d(out_channels)
            self.conv1 = SpatioTemporalConv(in_channels, out_channels, kernel_size, padding=padding, stride=2)
        else:
            self.conv1 = SpatioTemporalConv(in_channels, out_channels, kernel_size, padding=padding)

        self.bn1 = nn.BatchNorm3d(out_channels)
        self.relu1 = nn.ReLU()

        # standard conv->batchnorm->ReLU
        self.conv2 = SpatioTemporalConv(out_channels, out_channels, kernel_size, padding=padding)
        self.bn2 = nn.BatchNorm3d(out_channels)
        self.outrelu = nn.ReLU()

    def forward(self, x):
        res = self.relu1(self.bn1(self.conv1(x)))
        res = self.bn2(self.conv2(res))

        if self.downsample:
            x = self.downsamplebn(self.downsampleconv(x))

        return self.outrelu(x + res)

class SpatioTemporalResLayer(nn.Module):
    # 時空ResLayer
    def __init__(self, in_channels, out_channels, kernel_size, layer_size, block_type=SpatioTemporalResBlock,
                 downsample=False):
        super(SpatioTemporalResLayer, self).__init__()
        # 第一個時空ResBlock
        self.block1 = block_type(in_channels, out_channels, kernel_size, downsample)
        # 後續的時空ResBlock
        self.blocks = nn.ModuleList([])
        for i in range(layer_size - 1):
            self.blocks += [block_type(out_channels, out_channels, kernel_size)]

    def forward(self, x):
        x = self.block1(x)
        for block in self.blocks:
            x = block(x)
        return x

class R3DNet(nn.Module):

    def __init__(self, layer_sizes, num_classes):
        super(R3DNet, self).__init__()
        # 普通3D卷積
        self.conv1 = SpatioTemporalConv(3, 64, [3, 7, 7], stride=[1, 2, 2], padding=[1, 3, 3])
        # Res3D卷積
        self.conv2 = SpatioTemporalResLayer(64, 64, 3, layer_sizes[0])
        # 帶降採樣的Res3D卷積
        self.conv3 = SpatioTemporalResLayer(64, 128, 3, layer_sizes[1], downsample=True)
        self.conv4 = SpatioTemporalResLayer(128, 256, 3, layer_sizes[2], downsample=True)
        self.conv5 = SpatioTemporalResLayer(256, 512, 3, layer_sizes[3], downsample=True)

        self.pool = nn.AdaptiveAvgPool3d(1)
        self.fc = nn.Linear(512, num_classes)

    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = self.conv3(x)
        x = self.conv4(x)
        x = self.conv5(x)

        x = self.pool(x)
        x = x.view(-1, 512)
        x = self.fc(x)
        return x

if __name__ == '__main__':

    inputs = torch.rand(1, 3, 16, 112, 112)
    net = R3DNet((2, 2, 2, 2), 101)
    outputs = net(inputs)
    print(outputs.size())

運行結果

torch.Size([1, 101])

Tensorflow代碼

import tensorflow as tf
import numpy as np
from tensorflow.keras import layers, models, Sequential
import collections
from itertools import repeat

class SpatioTemporalConv(layers.Layer):

    def __init__(self, out_channels, kernel_size, stride=1):
        super(SpatioTemporalConv, self).__init__()
        _triple = self._ntuple(3)
        kernel_size = _triple(kernel_size)
        stride = _triple(stride)
        self.temporal_spatial_conv = layers.Conv3D(out_channels, kernel_size, strides=stride, padding='same')
        self.bn = layers.BatchNormalization()
        self.relu = layers.ReLU()

    def _ntuple(self, n):
        def parse(x):
            if isinstance(x, collections.abc.Iterable):
                return tuple(x)
            return tuple(repeat(x, n))
        return parse

    def call(self, x):
        x = self.relu(self.bn(self.temporal_spatial_conv(x)))
        return x

class SpatioTemporalResBlock(layers.Layer):

    def __init__(self, out_channels, kernel_size, downsample=False):
        super(SpatioTemporalResBlock, self).__init__()
        self.downsample = downsample
        if self.downsample:
            self.downsampleconv = SpatioTemporalConv(out_channels, 1, stride=2)
            self.downsamplebn = layers.BatchNormalization()
            self.conv1 = SpatioTemporalConv(out_channels, kernel_size, stride=2)
        else:
            self.conv1 = SpatioTemporalConv(out_channels, kernel_size)

        self.bn1 = layers.BatchNormalization()
        self.relu1 = layers.ReLU()

        self.conv2 = SpatioTemporalConv(out_channels, kernel_size)
        self.bn2 = layers.BatchNormalization()
        self.outrelu = layers.ReLU()

    def call(self, x):
        res = self.relu1(self.bn1(self.conv1(x)))
        res = self.bn2(self.conv2(res))

        if self.downsample:
            x = self.downsamplebn(self.downsampleconv(x))

        return self.outrelu(x + res)

class SpatioTemporalResLayer(layers.Layer):

    def __init__(self, out_channels, kernel_size, layer_size, block_type=SpatioTemporalResBlock,
                 downsample=False):
        super(SpatioTemporalResLayer, self).__init__()
        self.block1 = block_type(out_channels, kernel_size, downsample)
        self.blocks = Sequential([])
        for i in range(layer_size - 1):
            self.blocks.add(block_type(out_channels, kernel_size))

    def call(self, x):
        x = self.block1(x)
        x = self.blocks(x)
        return x

class R3DNet(models.Model):

    def __init__(self, layer_sizes, num_classes):
        super(R3DNet, self).__init__()
        self.padding = layers.ZeroPadding3D(padding=(1, 3, 3))
        self.conv1 = SpatioTemporalConv(64, [3, 7, 7], stride=[1, 2, 2])
        self.conv2 = SpatioTemporalResLayer(64, 3, layer_sizes[0])
        self.conv3 = SpatioTemporalResLayer(128, 3, layer_sizes[1], downsample=True)
        self.conv4 = SpatioTemporalResLayer(256, 3, layer_sizes[2], downsample=True)
        self.conv5 = SpatioTemporalResLayer(512, 3, layer_sizes[3], downsample=True)
        self.pool = layers.GlobalAveragePooling3D()
        self.flatten = layers.Flatten()
        self.fc = layers.Dense(num_classes)

    def call(self, x):
        x = self.padding(x)
        x = self.conv1(x)
        x = self.conv2(x)
        x = self.conv3(x)
        x = self.conv4(x)
        x = self.conv5(x)

        x = self.pool(x)
        x = self.flatten(x)
        x = self.fc(x)
        return x

if __name__ == '__main__':

    inputs = tf.constant(np.random.rand(1, 16, 112, 112, 3))
    net = R3DNet((2, 2, 2, 2), 101)
    outputs = net(inputs)
    print(outputs.shape)

運行結果

(1, 101)

R(2+1)D

將不同的卷積進行堆疊有這麼一些常見的策略,上圖中a表示的是全部用2D卷積進行特徵學習,然後在頂層進行特徵的融合,這種策略跟3D沒有太大的關係。b、c、d、e都是3D模型,對於b來說是把3D模塊放在底層,c是把3D模塊放在頂層,d全部都是3D模塊,e全部都是(2+1)D模塊(3D模塊的分解,空間維度2D,時間維度1D)。

  • 不同子結構比較(Kinetics validation set)

上圖是不同結構的比較,在輸入上還用到了8幀和16幀。MC2、MC3、MC4、MC5表示底層用到的3D卷積的數量。綜合來看R(2+1)D表現了最好的性能,它和R3D保持了相當的參數量。右邊的圖的橫軸表示參數量,縱軸爲精度來進行可視化。

  • 訓練難度比較,不同訓練策略

R(2+1)D相比R3D更加容易訓練,體現在上圖中就是R(2+1)D訓練的損失值和驗證的損失值比R3D有更低的值。在相同的網絡深度的情況下,有更低的損失值就表明模型更加容易訓練。右邊的表展示了不同的訓練加finetune(就是用別人訓練好的模型,加上我們自己的數據,來訓練新的模型。finetune相當於使用別人的模型的前幾層,來提取淺層特徵,然後在最後再落入我們自己的分類中)的策略。train表示訓練用的長度,finetune表示使用預訓練模型的長度。作者發現在長的序列上進行訓練不適用finetune需要花費很長的時間,而在短的序列上訓練,在長的序列上進行finetune具有好的性價比,比如最下面的一行,使用訓練好的8序列的模型對32序列進行finetune比不finetune,訓練的時間少了將近2/3。而模型精度只損失了大概1%。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章