視覺(vision)、自然語言處理(Nature Language Processing, NLP)、語音(Speech)是深度學習研究的三大方向。三大領域各自都誕生了若干經典的模組,用來建模該領域資料所蘊含的不同特性的模式。上一篇文章介紹了 PaddleFluid 和 TensorFlow 的設計和核心概念,這一篇我們從影像任務開始,使用 PaddleFluid 和 TensorFlow 來寫一個完全相同的網路,通過這種方式瞭解我們的使用經驗如何在不同平臺之間遷移,從而幫助我們選擇便利的工具,集中於機器學習任務本身。
這一篇我們使用影像分類中的 SE_ResNeXt [1][2] 作為實踐任務。
SE 全稱 Sequeeze-and-Excitation,SE block 並不是一個完整的網路結構,而是一個子結構,可以嵌到其他分類或檢測模型中。SENet block 和 ResNeXt 的結合在 ILSVRC 2017 的分類專案中取得 了第一名的成績。在 ImageNet 資料集上將 top-5 錯誤率從原先的最好成績 2.991% 降低到 2.251%。
如何使用程式碼
本篇文章配套有完整可執行的程式碼,程式碼包括以下幾個檔案:
在終端執行下面的命令便可以使用 PaddleFluid 進行模型的訓練,訓練過程如果未在當前工作目錄下的 cifar-10-batches-py 目錄中檢測到訓練資料(無需手動建立 cifar-10-batches-py 資料夾),會自動從網路中下載訓練及測試資料。
python SE_ResNeXt_fluid.py
在命令列執行下面的命令便可以使用 TensorFlow 進行模型的訓練,訓練中模型驗證以及模型儲存:
python SE_ResNeXt_tensorflow.py
注:本系列的最新程式碼,可以在 github 上的 TF2Fluid [3] 中隨時 獲取。
背景介紹
卷積神經網路在影像類任務上取得了巨大的突破。卷積核作為卷積神經網路的核心,通常被看做是在區域性感受野上將空間上(spatial)的資訊和通道(channel)上的資訊進行聚合的資訊聚合體。 通常,一個卷積 Block 由卷積層、非線性層和下采樣層(Pooling 層)構成,一個卷積神經網路則由一系列堆疊的卷積 block 構成。隨著卷積 Block 的層數的加深,感受野也在不斷擴大,最終達到從全域性感受野上捕獲影像的特徵來進行影像的描述的目標。
Squeeze-and-Excitation (SE)模組
影像領域很多研究工作從不同角度出發研究如何構建更加強有力的網路學習影像特徵。例如,如 Inception 結構中嵌入了多尺度資訊:使用多個不同卷積核,聚合多種不同感受野上的特徵來獲得效能增益;將 Attention 機制引入到空間(spatial)維度上等,都獲得了相當不錯的成果。
除了上角度,很自然會想到,是否可以從考慮特徵通道(channel)之間的關係出發,增強網路效能?這正是 Squeeze-and-Excitation 模組的設計動機。
SE block 由兩個非常關鍵的操作,Squeeze 和 Excitation 構成, 其核心思想是:顯式地建模特徵通道之間的相互依賴關係,通過自適應學習的方式來獲取每個特徵通道的重要程度,然後依照這個重要程度去提升有用的特徵(有用 feature map 權重增大 )並抑制對當前任務用處不大的特徵(無效或效果小的 feature map 權重減小),達到更好的模型效果。
下圖是 SE Block 示意圖,來自 參考文獻 [1] 中的 Figure 1。
▲ 圖 1. SE block示意圖
Squeeze 操作,圖 1 中的:
通過一個全域性的 pooling 操作,沿著空間維度進行特徵壓縮,將每個二維的特徵通道(channel)變成一個實數。這個實數一定程度上具有全域性的感受野。Squeeze 操作的輸出維度和輸入 feature map 的特徵通道數完全相等,表徵著在特徵通道維度上響應的全域性分佈,Squeeze 使得靠近輸入的層也可以獲得全域性的感受野,這在很多工中都是非常有用。
Excitation 操作,圖 1 中的:
類似與門機制。通過引數WW的作用,為每個特徵通道學習一個權重,其中 可學習引數WW用來顯式地建模特徵通道間的相關性。
Scale 加權操作:
最後一步,將 Excitation 的輸出的權重看做是經過特徵選擇後每個特徵通道的重要性,然後通過 scale 逐通道對原始特徵進行 reweight,完成在通道維度上的對原始特徵的重標定(recalibration)。
當 SE block 嵌入原有一些分類網路中時,不可避免會增加需要學習的引數和 引入計算量,但是 SE block 帶來的計算量較低,效果面改善依然非常可觀,對大多數任務還是可以接受。
SE模組和Residual Connection疊加
圖 2 是 SE 模組和 ResNet 中的 Residual Connection 進行疊加的原理示意圖,來自參考文獻[1] 中的 Figure 3。
▲ 圖 2. SE 模組與Residual Connection疊加
SE 模組可以嵌入到含有 Residual Connection 的 ResNet 的模型中,圖 2 右是 SE 疊加 Residual Connection 的示意圖。論文中建議在 Residual Connection 的跨層連線進行 Addition 操作前,而不是 Addition 操作之後疊加 SE 模組:前對分支上 Residual 的特徵進行了特徵重標定(recalibration),是由於如果主幹上存在 0 ~ 1 的 scale 操作(extraction 步驟),在網路較深時候,反向傳播演算法進行到網路靠近輸入層時,容易出現梯度消散的情況,導致模型難以優化。
ResNeXt 模型結構
深度學習方法想要提高模型的準確率,通常會選擇加深或加寬網路。但是隨著網路深度或者寬度的增加,網路引數(例如:特徵通道數,filter size 等)量也顯著增加,計算開銷增大。ResNeXt 網路結構設計的初衷是希望在不增加引數複雜度的前提下,提高網路的效能,同時還減少了超引數的數量。
在引出 ResNeXt 網路之前,我們首先回顧影像分類領域兩個非常重要的工作:VGG 和 Inception 網路(這兩個網路的設計細節 PaddleBook 的影像分類 [4] 一節有詳細的介紹)。VGG 網路通過堆疊相同形狀的網路模組,這一簡單策略來構建深度網路,之後的 ResNet 也同樣使用了這一策略。這個策略降低了超引數的選擇,在許多不同的資料集和任務上依然 有效工作,展示了很好的魯棒性。
Inception 的網路結果經過精心設計,遵循:split-transform-merge 策略。在一個 Inception 模型中:(1)split:輸入通過 1×1卷積被切分到幾個低維 feature map;(2)transform:split 步驟的輸出經過一組(多個)filter size 不同的特定濾波器(filters)對映變換;(3)merge:通過 concatenation 將 transform 步驟的結果融合到一起。
Inception 模型期望通過較低的計算量來近似大的密集的濾波器的表達能力。儘管 Inception 模型的精度很好,但是每個 Inception 模組要量身定製濾波器數量、尺寸,模組在每一階段都要改變,特別是當 Inception 模組用於新的資料或者任務時如何修改沒有特別通用的方法。
ResNeXt 綜合了 VGG 和 Inception 各自的優點,提出了一個簡單架構:採用 VGG/ResNets 重複相同網路層的策略,以一種簡單可擴充套件的方式延續 split-transform-merge 策略,整個網路的 building block 都是一樣的,不用在每個 stage 裡對每個 building block 的超引數進行調整,只用一個結構相同的 building block,重複堆疊即可形成整個網路。
實圖 3 來自參考文獻 [2] 中的Figure 1,展示了 ResNeXt 網路的基本 building block。
▲ 圖 3. ResNeXt網路的基本building block
解釋圖 3 之前要先介紹一個名詞 cardinality,是指 building block 中變換集合的大小(the size of the set of transformation)。圖 3 中 cardinality=32,將左邊的 64 個卷積核分成了右邊 32 條不同 path,最後將 32 個 path 的輸出向量的所有通道對應位置相加,再與 Residual connection 相加。
圖 3 再結合上一節中介紹的“SE 模組和 Residual Connection 疊加”,便構成了最終的 SE_ResNeXt 網路的 building block,重複堆疊即可形成整個 ResNeXt。
CIFAR-10 資料集介紹
至此,介紹完 ResNeXt 模型的模型原理和基本結構,我們準備開始分別使用 PaddleFluid 和 TensorFlow 構建訓練任務。
這一篇我們使用 cifar10 資料集 [5][6] 作為實驗資料。cifar-10 資料集包含 60000 個 32*32 的彩色影像,共有 10 類,圖 4 是 cifar10 資料集的 10 個類別。圖 4 是 cifar-10 資料集的 10 個類別示意圖。
▲ 圖 4. cifar10資料集
cifar10 資料集有 50000 個訓練影像和 10000 個測試影像,被劃分為 5 個訓練塊和 1 個測試塊,每個塊有 10000 個影像。測試塊包含從每類隨機選擇的 1000 個影像。
下載資料
執行訓練程式時, 如果在當前執行目錄下沒有 cifar-10-batches-py 目錄,或是 cifar-10-batches-py 目錄下沒有已經下載好的資料,PaddleFluid 和 TensorFlow 的資料讀取模組會呼叫 data_utils [7] 中的 download_data 方法自動 從網站上下載 cifar-10 資料集,無需手動下載。
載入cifar-10資料集
PaddleFluid
1. 定義網路的輸入層:
上一篇基本使用概念中介紹過 PaddleFluid 模型通過 fluid.layers.data 來接收輸入資料。影像分類網路以圖片以及圖片對應的類別標籤作為網路的輸入:
IMG_SHAPE = [3, 32, 32]
images = fluid.layers.data(name="image", shape=IMG_SHAPE, dtype="float32")
labels = fluid.layers.data(name="label", shape=[1], dtype="int64")
在上面的程式碼片段中, fluid.layers.data 中指定的 shape 無需顯示地指定第 0 維 batch size,框架會自動補充第 0 維,並在執行時填充正確的 batch size。
一副圖片是一個 3-D Tensor。PaddleFluid 中卷積操作使用 channel-first 的資料輸入格式。因此在接收 原始影像 資料時,shape 的三個維度其含義分別是:channel、圖片的寬度以及圖片的高度。
2. 使用 python 語言編寫的 data reader 函式
PaddleFluid 中通過 DataFeeder 介面來為對應的 fluid.data.layersfeed 資料,呼叫方式如下:
train_reader = paddle.batch(train_data(), batch_size=conf.batch_size)
feeder = fluid.DataFeeder(place=place, feed_list=[images, labels])
在上面的程式碼片段中:
1. 僅需要使用者編寫 train_data() 這樣一個 python 的 generator。這個函式 是 PaddleFluid 要求提供的 data reader 介面,函式名字不限。
2. 實現這個 data reader 介面時只需要考慮:如何從原始資料檔案中讀取資料,返回一條以 numpy ndarrary 格式的訓練資料。
3. 呼叫 paddle.batch(train_data(), batch_size=conf.batch_size) 介面會將資料首先讀入一個 pool 中,進行 shuffle,然後從 pool 中依次取一個一個的 mini-batch。
完整程式碼請參考 train_data() [8],這裡不再直接貼上程式碼片段。
TensorFlow
1. 定義 placeholder
在 TensorFlow 中,通過定義 placeholder 這樣一個特殊的 Tensor 來接受輸入資料。
IMG_SHAPE = [3, 32, 32]
LBL_COUNT = 10
images = tf.placeholder(
tf.float32, shape=[None, IMG_SHAPE[1], IMG_SHAPE[2], IMG_SHAPE[0]])
labels = tf.placeholder(tf.float32, shape=[None, LBL_COUNT])
需要注意的是:
TensorFlow 中的卷積操作預設使用 channel-last 資料格式( 也可以在呼叫卷積的介面中使用 channel-first 的資料格式),images 這個 placeholder 的 shape 和 PaddleFluid 中 images 形狀不同。
在 placeholder 中,batch size 這樣執行時才可以確定具體數值的維度使用 None 代替。
2. 通過 feeding 字典為placeholder 提供資料
TensorFlow 通過 session 管理執行一個計算圖,在 呼叫 session 的 run 方法時,提供一個 feeding 字典,將 mini-batch 資料 feed 給 placeholder。
下面的程式碼片實現了 載入 cifar-10 資料進行訓練。
image_train, label_train = train_data()
image_test, label_test = train_data()
total_train_sample = len(image_train)
for epoch in range(1, conf.total_epochs + 1):
for batch_id, start_index in enumerate(
range(0, total_train_sample, conf.batch_size)):
end_index = min(start_index + conf.batch_size,
total_train_sample)
batch_image = image_train[start_index:end_index]
batch_label = label_train[start_index:end_index]
train_feed_dict = {
images: batch_image,
labels: batch_label,
......
}
上面程式碼片段中的 train_data() [7] 完成了原始資料的讀取。與 PaddleFluid 中只需考慮讀取一條資料由框架完成組 batch 不同,train_data 讀取所有資料,由使用者程式控制 shuffle 和組 batch。
構建網路結構
使用不同深度學習框架的核心就是使用框架提供的運算元構建神經網路模型結構。PaddleFluid 和 TensorFlow 各自提供的基本運算元的詳細說明,可以在各自官網獲取。
這一篇提供了程式碼檔案 SE_ResNeXt_fluid.py [9] 和 SE_ResNeXt_tensorflow.py [10] 分別是用 PaddleFluid 和 TensorFlow 寫成的 ResNeXt 網路,兩個檔案中的網路結構完全相同,並且程式碼結構也完全相同。核心都是 SE_ResNeXt 這個類,如下面程式碼片段所示:
class SE_ResNeXt(object):
def __init__(self, x, num_block, depth, out_dims, cardinality,
reduction_ratio, is_training):
...
def transform_layer(self, x, stride, depth):
...
def split_layer(self, input_x, stride, depth, layer_name, cardinality):
...
def transition_layer(self, x, out_dim):
...
def squeeze_excitation_layer(self, input_x, out_dim, reduction_ratio):
...
def residual_layer(self, input_x, out_dim, layer_num, cardinality, depth,
reduction_ratio, num_block):
...
def build_SEnet(self, input_x):
...
SE_ResNeXt_fluid.py [9] 和 SE_ResNeXt_tensorflow.py [10] 中 SE_ResNeXt 類有著完全相同的成員函式,可以通過對比在兩個平臺下實現同樣功能的程式碼,瞭解如何使用經驗如何在兩個平臺之間遷移。
2-D卷積層使用差異
2-D 卷積是影像任務中的一個重要操作,卷積核在 2 個軸向上平移,卷積核的每個元素與被卷積影像對應位置相乘,再求和。卷積核的不斷移動會輸出一個新的影像,這個影像完全由卷積核在各個位置時的乘積求和的結果組成。圖 5 是當輸入影像是 4-D Tensor 時(batch size 這裡固定是 1,也就是 4-D Tensor 的第一維固定為 1),2-D 卷積的視覺化計算過程。
▲ 圖 5. RGB 3通道影像輸入上的2-D卷積
作者 : Martin Görner / Twitter: @martin_gorner
卷積計算的一些細節在 PaddleFluid 和 TensorFlow 中略有不同,在這裡我們稍作解釋。
下面是 PaddleFluid 2-D 卷積呼叫介面:
fluid.layers.conv2d(
input,
num_filters,
filter_size,
stride=1,
padding=0,
dilation=1,
groups=None,
act=None,
...)
1. 在PaddleFluid卷積使用"channel-first"輸入資料格式,也就是常說的“NCHW”格式。
如果輸入影像是“channel-last”格式,可以在卷積操作之前加入 fluid.layers.transposeoperator,對輸入資料的各個軸進行換序。
2. PaddleFluid 中卷積計算的 padding 屬性使用者需要自己進行計算,最終輸出影像的 height 和 width 由下面公式計算得到:
padding 屬性 可以接受一個含有兩個元素的 python list,分別指定對影像 height 方向和width 方向填充。如果 padding 是一個整型數而不是一個 list 使,認為 height 和 width 方向填充相同多個 0。
這個邏輯同樣適用於 stride 和 dilation 引數。
下面是 TensorFlow 2-D 卷積呼叫介面:
tf.layers.conv2d(
inputs,
filters,
kernel_size,
strides=(1, 1),
padding='valid',
data_format='channels_last'
...)
1. 在 TensorFlow 卷積預設使用"channel-last"輸入資料格式,也就是常說的“NHWC”格式。
2. TensorFlow 中卷積計算的 padding 屬性可以指定兩種模式:“valid”:不填充;"same":卷積計算完畢輸出影像的寬度和高度與輸入影像相同。
正則項使用差異
L2 正則項作為預防過擬合的手段之一,在神經網路訓練中有著重要作用。PaddleFluid 平臺和 TensorFlow 中新增 L2 正則的 使用介面略有不同。
PaddleFluid
在 PaddleFluid 中使用 L2 正則這樣的標準正則項較為簡單,L2 正則作為 optimizer 的一個引數,直接傳遞正則項係數即可。
optimizer = fluid.optimizer.Momentum(
learning_rate=conf.learning_rate,
momentum=0.9,
regularization=fluid.regularizer.L2Decay(conf.weight_decay))
TensorFlow
在 TensorFlow 中,L2 正則作為損失函式的一部分,需要顯示地為網路中每一個需要新增 L2 正則項的可學習引數新增 L2 正則。
l2_loss = tf.add_n([tf.nn.l2_loss(var) for var in tf.trainable_variables()])
optimizer = tf.train.MomentumOptimizer(
learning_rate=learning_rate,
momentum=conf.momentum,
use_nesterov=conf.use_nesterov)
train = optimizer.minimize(cost + l2_loss * conf.weight_decay)
以上部分是 PaddleFluid 和 TensorFlow 使用中需要關注的一些差異,ResNeXt 模型在 兩個平臺訓練的其它介面呼叫細節,可以在程式碼 SE_ResNeXt_fluid.py 和 SE_ResNeXt_tensorflow.py 中找到,這裡不再貼上所有程式碼,整個過程都遵循:
定義網路結構;
載入訓練資料;
在一個 for 迴圈中讀取 一個一個 mini-batch 資料,呼叫網路的前向和反向計算,呼叫優化過程。
總結
這一篇我們從影像領域的影像分類問題入手,使用 PaddleFluid 和 TensorFlow 實現完全相同 ResNeXt 網路結構。 來介紹:
1. 在 PaddleFluid 和 TensorFlow 中如何讀取併為網路 feed 圖片資料;
2. 如何使用 PaddleFluid 和 TensorFlow 中的 2-d 卷積,2-d pooling,pad 等影像任務中常用計算單元;
3. 如何執行一個完整的訓練任務,在訓練中對測試集樣本進行測試。
可以看到 PaddleFluid 和 TensorFlow 的影像操作介面高度相似。PaddleFluid 能夠支援與 TensorFlow 同樣豐富的影像操作,這也是今天主流深度學習框架共同的選擇。作為使用者,我們的使用經驗可以在平臺之間 非常 簡單的進行遷移,只需要略微關注介面呼叫細節的一些差異即可。
在下面的篇章中,我們將進一步在 NLP 任務中對比如果使用 PaddleFluid 和 TensorFlow 中的迴圈神經網路單元處理序列輸入資料。並且逐步介紹多執行緒,多卡等主題。
參考文獻
[1]. Hu J, Shen L, Sun G. Squeeze-and-excitation networks[J]. arXiv preprint arXiv:1709.01507, 2017.
[2]. Xie S, Girshick R, Dollár P, et al. Aggregated residual transformations for deep neural networks[C]//Computer Vision and Pattern Recognition (CVPR), 2017 IEEE Conference on. IEEE, 2017: 5987-5995.
[3]. TF2Fluid: https://github.com/JohnRabbbit/TF2Fluid
[4]. PaddleBook影像分類
http://www.paddlepaddle.org/docs/develop/book/03.image_classification/index.cn.html
[5]. Learning Multiple Layers of Features from Tiny Images, Alex Krizhevsky, 2009.
https://www.cs.toronto.edu/~kriz/learning-features-2009-TR.pdf
[6]. The CIFAR-10 dataset: https://www.cs.toronto.edu/~kriz/cifar.html
[7]. data_utils
https://github.com/JohnRabbbit/TF2Fluid/blob/master/02_image_classification/data_utils.py#L35
[8]. train_data()
https://github.com/JohnRabbbit/TF2Fluid/blob/master/02_image_classification/cifar10_fluid.py#L43
[9]. SE_ResNeXt_fluid.py
https://github.com/JohnRabbbit/TF2Fluid/blob/master/02_image_classification/SE_ResNeXt_fluid.py
[10]. SE_ResNeXt_tensorflow.py
https://github.com/JohnRabbbit/TF2Fluid/blob/master/02_image_classification/SE_ResNeXt_tensorflow.py