基於飛槳復現 CVPR2018 Relation Net的全程解析

飛槳開發者說】佟興宇,北京航空航天大學碩士,機器視覺算法工程師。php

Relation Net 是 CVPR2018的一篇論文,論文連接:git

https://arxiv.org/pdf/1711.06025.pdfgithub

論文做者發現,在視覺識別任務中,訓練模型時須要大量標註過的圖片,並迭代屢次去訓練參數。每當新增物體類別,都須要花費大量時間去標註,可是有一些新興物體類別和稀有物體類別可能不存在大量標註過的圖片,從而影響模型訓練效果。反觀人類,只要不多的認知學習就可實現小樣本(FSL)和無樣本學習(ZSL)。算法

好比:小孩子只要在一張圖片或一本書裏認識了斑馬,或者只是聽到描述斑馬是一種」條紋馬」,就能夠毫無困難的識別出斑馬這種動物。爲了解決深度學習中模型樣本少致使的分類效果差的問題,同時又受到人類的小樣本和無樣本學習能力帶來的啓發,小樣本學習又恢復了一些熱度。網絡

深度學習中的Fine-tune技術能夠用於一些樣本比較少的狀況,可是在只有一個或者幾個樣本的狀況下,即便使用了數據加強和正則化技術,仍然會有過擬合的問題。目前其餘的小樣本學習的推理機制又比較複雜,因此論文做者提出了一個能夠端到端訓練,而且結構簡單的模型Relation Net。框架

在 FSL 任務中,通常將數據集分爲 Training set 、Support set 、Testing set。Support set和 Testing set有共同的標籤;Training set裏面不包涵 Support set和 Testing set的標籤;在 Support set 中有 K 個標註過的數據和C個不一樣的類別,則稱做爲 C-way K-shot。在訓練的過程當中從 Training set 中選取 sample set /query set 對應Support set / Testing set,具體方法在文中的訓練策略裏會詳細說明。ide

Relation Network由 embedding model 和 relation model 組成。Relation Network 的核心思想是:首先經過embedding model分別提取 support set 和 testing set中圖像的特徵圖,而後將特徵圖中表明通道數的維度進行拼接,獲得一個新的特徵圖。而後把新的特徵圖送入 relation model 進行運算獲得 relation score,這個值表明了兩張圖的類似度。函數

下圖爲5-way 1-shot 的狀況下接受1個樣本的網絡結構與流程。5張sample set 中的圖片與1張 query set 中的圖片會分別的經過 embedding model 提取特徵並拼接,獲得5個新的特徵圖,而後送入 Relation Net 進行計算 relation score,最後會獲得一個 one-shot 的向量,分數最高的表明對應的類別。性能

訓練使用的損失函數也比較簡單,使用均方偏差做爲損失函數。公式中 ri,j表明圖片 i與 j 的類似度。yi 與 yj表明圖片的真實標籤。學習

下載安裝命令

## CPU版本安裝命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle

## GPU版本安裝命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu

基於飛槳復現

Relation Network

下面我將復現的技術細節與各位開發者分享,Relation Network 模型結構定義請查看:

https://github.com/txyugood/paddle_RN_FSL/blob/master/RelationNet.py

1. 搭建 Relation Network 網絡

模型由embedding model 和 relation model 兩部分組成,兩個網絡都主要由 【Conv+BN+Relu】 模塊組成。所以先定義一個 BaseNet類,並在其中實現conv_bn_layer方法,代碼以下:

class BaseNet:
    def conv_bn_layer(self,
                      input,
                      num_filters,
                      filter_size,
                      stride=1,
                      groups=1,
                      padding=0,
                      act=None,
                      name=None,
                      data_format='NCHW'):
        n = filter_size * filter_size * num_filters
        conv = fluid.layers.conv2d(
            input=input,
            num_filters=num_filters,
            filter_size=filter_size,
            stride=stride,
            padding=padding,
            groups=groups,
            act=None,
            param_attr=ParamAttr(name=name + "_weights", initializer=fluid.initializer.Normal(0,math.sqrt(2. / n))),
            bias_attr=ParamAttr(name=name + "_bias",
                                initializer=fluid.initializer.Constant(0.0)),
            name=name + '.conv2d.output.1',
            data_format=data_format)

        bn_name = "bn_" + name

        return fluid.layers.batch_norm(
            input=conv,
            act=act,
            momentum=1,
            name=bn_name + '.output.1',
            param_attr=ParamAttr(name=bn_name + '_scale',
                                 initializer=fluid.initializer.Constant(1)),
            bias_attr=ParamAttr(bn_name + '_offset',
                                initializer=fluid.initializer.Constant(0)),
            moving_mean_name=bn_name + '_mean',
            moving_variance_name=bn_name + '_variance',
            data_layout=data_format)

飛槳支持靜態圖和動態圖兩種網絡定義模式,這裏我選用的靜態圖。以上代碼定義了一個卷積神經網絡中最常常出現的 conv_bn 層,但要注意的是 batch_norm 層的 momentum 設置爲1,實現的效果就是不記錄全局均值和方差。

具體參數含義以下:

  • Input:傳入待卷積處理的張量對象;

  • num_filter:卷積核數量(輸出的卷積結果的通道數);

  • filter_size:卷積核尺寸;

  • stride:卷積步長;

  • groups:分組卷積的組數量;

  • padding:填充大小,這裏設置爲0,表明卷積後不填充;

  • act:接在 BN 層後的激活函數,若是爲 None,則不使用激活函數;

  • name:在運算圖中的對象名稱。

接着咱們定義 Relation Network 中的 embedding model 部分。

class EmbeddingNet(BaseNet):
    def net(self,input):
        conv = self.conv_bn_layer(
            input=input,
            num_filters=64,
            filter_size=3,
            padding=0,
            act='relu',
            name='embed_conv1')
        conv = fluid.layers.pool2d(
            input=conv,
            pool_size=2,
            pool_stride=2,
            pool_type='max')
        conv = self.conv_bn_layer(
            input=conv,
            num_filters=64,
            filter_size=3,
            padding=0,
            act='relu',
            name='embed_conv2')
        conv = fluid.layers.pool2d(
            input=conv,
            pool_size=2,
            pool_stride=2,
            pool_type='max')
        conv = self.conv_bn_layer(
            input=conv,
            num_filters=64,
            filter_size=3,
            padding=1,
            act='relu',
            name='embed_conv3')
        conv = self.conv_bn_layer(
            input=conv,
            num_filters=64,
            filter_size=3,
            padding=1,
            act='relu',
            name='embed_conv4')
        return conv

在上述代碼中建立一個EmbeddingNet類,繼承BaseNet類,它就繼承了conv_bn_layer方法。在EmbeddingNet中定義net方法,參數 input 表明輸入的圖像張量,這個方法用來建立網絡的靜態圖。

輸入的 input 先通過一個【Conv+BN+relu】的模塊獲得特徵圖embed_conv1;而後進行了一次最大值池化操做,池化的做用的是在保留重要特徵的前提下縮小特徵圖,後面的卷積和池化操做做用與此相同;最後embed_conv4輸出的特徵圖形狀是[-1,64,19,19]一共4個維度,第1個緯度表明了 batch_size,由於 batch_size 在建立靜態網絡時是不肯定的,因此用-1來表示能夠是任意值。第2個緯度表明了特徵的圖的通道數,通過 embedding model後,特徵圖的通道數爲64。最後第3和第4個維度表明了特徵圖的寬度和高度,這裏是19x19。

Relation model 代碼部分以下:

class RelationNet(BaseNet):
    def net(self, input, hidden_size):
        conv = self.conv_bn_layer(
            input=input,
            num_filters=64,
            filter_size=3,
            padding=0,
            act='relu',
            name='rn_conv1')
        conv = fluid.layers.pool2d(
            input=conv,
            pool_size=2,
            pool_stride=2,
            pool_type='max')
        conv = self.conv_bn_layer(
            input=conv,
            num_filters=64,
            filter_size=3,
            padding=0,
            act='relu',
            name='rn_conv2')
        conv = fluid.layers.pool2d(
            input=conv,
            pool_size=2,
            pool_stride=2,
            pool_type='max')
        fc = fluid.layers.fc(conv,size=hidden_size,act='relu',
                             param_attr=ParamAttr(name='fc1_weights',
                                                  initializer=fluid.initializer.Normal(0,0.01)),
                             bias_attr=ParamAttr(name='fc1_bias',
                                                 initializer=fluid.initializer.Constant(1)),
                             )
        fc = fluid.layers.fc(fc, size=1,act='sigmoid',
                             param_attr=ParamAttr(name='fc2_weights',
                                                  initializer=fluid.initializer.Normal(0,0.01)),
                             bias_attr=ParamAttr(name='fc2_bias',
                                                 initializer=fluid.initializer.Constant(1)),
                             )
        return fc

建立一個RelationNet類,它一樣繼承於 BaseNet 類,繼承了conv_bn_layer方法。在 net 方法中,模型的前面幾層與 embeding model 中相似使用【Conv+BN+Relu】模塊進行特徵提取,在最後使用兩層全鏈接層,將特徵值映射爲一個標量relation score,表明了兩個圖片的類似度。

在訓練過程當中,sample set 中圖片和 query set 的圖片通過 embedding model後都獲得了形狀爲[-1,64,19,19]的特徵圖,在送入 relation model 以前須要進行拼接,這段代碼略有些複雜,下面我分段解釋一下。

sample_image = fluid.layers.data('sample_image', shape=[38484], dtype='float32')
query_image = fluid.layers.data('query_image', shape=[38484], dtype='float32')

sample_query_image = fluid.layers.concat([sample_image, query_image], axis=0)
sample_query_feature = embed_model.net(sample_query_image)

這部分代碼是將 sample image和 query image的張量在batch_size 的緯度上拼接獲得張量sample_query_image,一塊兒送到 embedding model 中去提取特徵,獲得sample_query_feature。

sample_batch_size = fluid.layers.shape(sample_image)[0]
query_batch_size = fluid.layers.shape(query_image)[0]

這部分代碼取 image 張量的0維度做爲 batch_size。

sample_feature = fluid.layers.slice(
                sample_query_feature,
                axes=[0],
                starts=[0],
                ends=[sample_batch_size])
if k_shot > 1:
# few_shot
      sample_feature = fluid.layers.reshape(sample_feature, shape=[c_way, k_shot, 641919])
      sample_feature = fluid.layers.reduce_sum(sample_feature, dim=1)
query_feature = fluid.layers.slice(
      sample_query_feature,
      axes=[0],
      starts=[sample_batch_size],
      ends=[sample_batch_size + query_batch_size])

因爲以前圖片進行了拼接,因此在特徵以後,一樣須要在sample_query_feature的 batch_size 對應的0維度上進行切片,分別獲得sample_feature 和query_feature。這裏若是 K-shot 大於1時,須要對 sample_feature改變形狀,而後在 K-shot 對應的1維度上對 K-shot 個張量求和並刪除該維度,這時 sample_feature的形狀就變成爲[C-way,64,19,19]。這時 sample_batch_size 的值應該爲 C-way。

sample_feature_ext = fluid.layers.unsqueeze(sample_feature, axes=0)
query_shape = fluid.layers.concat(
       [query_batch_size, fluid.layers.assign(np.array([111,1]).astype('int32'))])
sample_feature_ext = fluid.layers.expand(sample_feature_ext, query_shape)

由於 sample set 中的每一張圖片特徵都須要與 C 個類型的圖片特徵進行拼接,因此這裏經過unsqueeze新增一個維度。根據 expand 接口的參數要求,這裏新建一個 query_shape 張量實現複製 sample_feature 張量query_batch_size 次獲得一個形狀爲[query_batch_size, sample_batch_size, 64, 19, 19]的張量。

query_feature_ext = fluid.layers.unsqueeze(query_feature, axes=0)
if k_shot > 1:
sample_batch_size = sample_batch_size / float(k_shot)
sample_shape = fluid.layers.concat(
      [sample_batch_size, fluid.layers.assign(np.array([1111]).astype('int32'))])
query_feature_ext = fluid.layers.expand(query_feature_ext, sample_shape)

同上面的操做同樣,query set 的特徵也須要新增一維度,這裏須要複製 sample_batch_size 次。值得注意的是,若是 k-shot 大於1的狀況下,由於以前已經作過 reduce_mean 操做,因此要使sample_batch_size除以 k-shot獲得新的sample_batch_size。最後經過複製獲得一個[sample_batch_size, query_batch_size, 64, 19, 19]的張量。

query_feature_ext = fluid.layers.transpose(query_feature_ext, [1, 0, 2, 3, 4])
relation_pairs = fluid.layers.concat([sample_feature_ext, query_feature_ext], axis=2)
relation_pairs = fluid.layers.reshape(relation_pairs, shape=[-1, 128, 19, 19])

最後經過transpose方法進行轉置使sample_feature_ext和query_feature_ext形狀一致,最後對兩個特徵進行拼接和修改形狀獲得一個形狀爲[query_batch_size x sample_batch_size, 128, 19, 19]的張量relation_pairs。

relation = RN_model.net(relation_pairs, hidden_size=8)
relation = fluid.layers.reshape(relation, shape=[-1, c_way])    

最後將以前拼接的特徵送入 relation model 模塊,首先會獲得一個query_batch_size x sample_batch_size長度的向量,而後改變形狀獲得[query_batch_size, sample_batch_size]的張量(sample_batch_size 實際上等於 C-way), sample_batch_size長度的向量以 one-hot 的形式表示出每個 query image 的類別。

損失函數的代碼以下:

one_hot_label = fluid.layers.one_hot(query_label, depth=c_way)
loss = fluid.layers.square_error_cost(relation, one_hot_label)
loss = fluid.layers.reduce_mean(loss)

首先將 query image 的標籤 query_label 轉換爲 one-hot 的形式,以前獲得的relation也是 one-hot的形式, 而後計算relation和one_hot_label的MSE獲得損失函數。

2. 訓練策略

在 FSL 任務中,若是隻使用 Support set 去訓練,也能夠對 Testing set 進行推理預測,可是因爲Support set 中樣本數量比較少,致使分類器的性能通常很差。所以通常使用Training set進行訓練,這樣分類器會有一個比較好的性能。這裏有一個有效的方法,叫作 episode based training。

episode based training的實現步驟以下:

  1. 訓練須要循環迭代 N 個 episode,每1個 episode 會在 training set 中隨機選取 C 個類別的中的 K 個數據,組成1個sample set數據集。C和 K 對應 support set 中的 C-way K-shot,一共有 C x K個樣本。

  2. 在 C 個類別中剩餘的樣本中隨機選取幾個樣本做爲 query set, 進行訓練。

對於 5-way 1-shot學習,sample set 的 batch_size 選擇的是5,query set 的 batch_size 選擇的是15。對於5-way 5-shot學習,sample set 的 batch_size 選擇的是25(每一個類別5張圖),query set 的 batch_size 選擇的10。

對於訓練的優化器,選擇的是 Adam優化,學習率設置爲0.001。對於數據增廣,在數據讀取時對 sample set 和 query set 的圖像都使用了 AutoAugment 的方法來增長數據的多樣性。

3. 驗證模型復現效果

驗證時的數據集只使用了論文中實驗用的 minImageNet,共有100個分類,每一個分類600張圖片。這個100個分類分別劃分爲 training/validation/testing 三個數據集,數量分別爲6四、16和20。

文章中提到模型在minImageNet的testing 數據集上準確率以下:

在5-way 1-shot 和 5-way 5-shot 分別達到了50.44和65.32左右的準確率。一樣使用基於飛槳實現的 Relation Net 在minImageNet的testing 數據集上的

5-way 1-shot 準確率:

5-way 5-shot 準確率:

結果與論文中的準確率一致,模型復現完成。

代碼地址:

https://github.com/txyugood/paddle_RN_FSL

如在使用過程當中有問題,可加入飛槳官方QQ羣進行交流:1108045677。

若是您想詳細瞭解更多飛槳的相關內容,請參閱如下文檔。

下載安裝命令

## CPU版本安裝命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle

## GPU版本安裝命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu

官網地址:

https://www.paddlepaddle.org.cn

飛槳開源框架項目地址:

GitHub: 

https://github.com/PaddlePaddle/Paddle

Gitee:  

https://gitee.com/paddlepaddle/Paddle

END

相關文章
相關標籤/搜索