論文原址:MobileNets v1
TensorFlow實現:mobilenet_v1.py
TensorFlow預訓練模型:mobilenet_v1.md
一、深度可分離卷積
標准的卷積過程可以看上圖,一個2×2的卷積核在卷積時,對應圖像區域中的所有通道均被同時考慮,問題在於,為什么一定要同時考慮圖像區域和通道?我們為什么不能把通道和空間區域分開考慮?
深度可分離卷積提出了一種新的思路:對於不同的輸入channel采取不同的卷積核進行卷積,它將普通的卷積操作分解為兩個過程。
卷積過程
假設有 的輸入,同時有
個
的卷積。如果設置
且
,那么普通卷積輸出為
。
Depthwise 過程
Depthwise是指將 的輸入分為
組,然后每一組做
卷積。這樣相當於收集了每個Channel的空間特征,即Depthwise特征。
Pointwise 過程
Pointwise是指對 的輸入做
個普通的
卷積。這樣相當於收集了每個點的特征,即Pointwise特征。Depthwise+Pointwise最終輸出也是
。
二、優勢與創新
Depthwise+Pointwise可以近似看作一個卷積層:
- 普通卷積:3x3 Conv+BN+ReLU
- Mobilenet卷積:3x3 Depthwise Conv+BN+ReLU 和 1x1 Pointwise Conv+BN+ReLU
計算加速
參數量降低
假設輸入通道數為3,要求輸出通道數為256,兩種做法:
1.直接接一個3×3×256的卷積核,參數量為:3×3×3×256 = 6,912
2.DW操作,分兩步完成,參數量為:3×3×3 + 3×1×1×256 = 795(3個特征層*(3*3的卷積核)),卷積深度參數通常取為1
乘法運算次數降低
對比一下不同卷積的乘法次數:
- 普通卷積計算量為:
- Depthwise計算量為:
- Pointwise計算量為:
通過Depthwise+Pointwise的拆分,相當於將普通卷積的計算量壓縮為:
通道區域分離
深度可分離卷積將以往普通卷積操作同時考慮通道和區域改變(卷積先只考慮區域,然后再考慮通道),實現了通道和區域的分離。
三、Mobilenet v1
Mobilenet v1利用深度可分離卷積進行加速,其架構如下,
還可以對所有卷積層 數量統一乘以縮小因子
(其中
)以壓縮網絡。這樣Depthwise+Pointwise總計算量可以進一降低為:
當然,壓縮網絡計算量肯定是有代價的。下圖展示了 不同時Mobilenet v1在ImageNet上的性能。可以看到即使
時Mobilenet v1在ImageNet上依然有63.7%的准確度。
下圖展示Mobilenet v1 與GoogleNet和VGG16的在輸入分辨率
情況下,准確度差距非常小,但是計算量和參數量都小很多。同時原文也給出了以Mobilenet v1提取特征的SSD/Faster R-CNN在COCO數據集上的性能。
結構實現一探
在實現代碼中(鏈接見本文開頭),作者使用具名元組存儲了網絡結構信息,
Conv = namedtuple('Conv', ['kernel', 'stride', 'depth']) DepthSepConv = namedtuple('DepthSepConv', ['kernel', 'stride', 'depth']) # MOBILENETV1_CONV_DEFS specifies the MobileNet body MOBILENETV1_CONV_DEFS = [ Conv(kernel=[3, 3], stride=2, depth=32), DepthSepConv(kernel=[3, 3], stride=1, depth=64), DepthSepConv(kernel=[3, 3], stride=2, depth=128), DepthSepConv(kernel=[3, 3], stride=1, depth=128), DepthSepConv(kernel=[3, 3], stride=2, depth=256), DepthSepConv(kernel=[3, 3], stride=1, depth=256), DepthSepConv(kernel=[3, 3], stride=2, depth=512), DepthSepConv(kernel=[3, 3], stride=1, depth=512), DepthSepConv(kernel=[3, 3], stride=1, depth=512), DepthSepConv(kernel=[3, 3], stride=1, depth=512), DepthSepConv(kernel=[3, 3], stride=1, depth=512), DepthSepConv(kernel=[3, 3], stride=1, depth=512), DepthSepConv(kernel=[3, 3], stride=2, depth=1024), DepthSepConv(kernel=[3, 3], stride=1, depth=1024) ]
然后,在生成結構中迭代這個具名元組列表,根據信息生成網路結構,這僅僅給出深度可分離層的實現部分,
elif isinstance(conv_def, DepthSepConv): end_point = end_point_base + '_depthwise' # By passing filters=None # separable_conv2d produces only a depthwise convolution layer if use_explicit_padding: net = _fixed_padding(net, conv_def.kernel, layer_rate) net = slim.separable_conv2d(net, None, conv_def.kernel, # <---Depthwise depth_multiplier=1, stride=layer_stride, rate=layer_rate, scope=end_point) end_points[end_point] = net if end_point == final_endpoint: return net, end_points end_point = end_point_base + '_pointwise' net = slim.conv2d(net, depth(conv_def.depth), [1, 1], # <---Pointwise stride=1, scope=end_point)
四、相關框架實現
TensorFlow 分步執行
順便一提,tf的實現可以接收rate參數,即可以采用空洞卷積的方式進行操作。
1、depthwise_conv2d 分離卷積部分
我們定義一張4*4的雙通道圖片
import tensorflow as tf img1 = tf.constant(value=[[[[1],[2],[3],[4]], [[1],[2],[3],[4]], [[1],[2],[3],[4]], [[1],[2],[3],[4]]]],dtype=tf.float32) img2 = tf.constant(value=[[[[1],[1],[1],[1]], [[1],[1],[1],[1]], [[1],[1],[1],[1]], [[1],[1],[1],[1]]]],dtype=tf.float32) img = tf.concat(values=[img1,img2],axis=3) img
<tf.Tensor 'concat_1:0' shape=(1, 4, 4, 2) dtype=float32>
使用3*3的卷積核,輸入channel為2,輸出channel為2(卷積核數目為2),
filter1 = tf.constant(value=0, shape=[3,3,1,1],dtype=tf.float32) filter2 = tf.constant(value=1, shape=[3,3,1,1],dtype=tf.float32) filter3 = tf.constant(value=2, shape=[3,3,1,1],dtype=tf.float32) filter4 = tf.constant(value=3, shape=[3,3,1,1],dtype=tf.float32) filter_out1 = tf.concat(values=[filter1,filter2],axis=2) filter_out2 = tf.concat(values=[filter3,filter4],axis=2) filter = tf.concat(values=[filter_out1,filter_out2],axis=3) filter
<tf.Tensor 'concat_4:0' shape=(3, 3, 2, 2) dtype=float32>
同時執行卷積操作,和深度可分離卷積操作,
out_img_conv = tf.nn.conv2d(input=img, filter=filter, strides=[1,1,1,1], padding='VALID') out_img_depthwise = tf.nn.depthwise_conv2d(input=img, filter=filter, strides=[1,1,1,1], rate=[1,1], padding='VALID') with tf.Session() as sess: res1 = sess.run(out_img_conv) res2 = sess.run(out_img_depthwise) print(res1, '\n', res1.shape) print(res2, '\n', res2.shape)
[[[[ 9. 63.] [ 9. 81.]] [[ 9. 63.] [ 9. 81.]]]] (1, 2, 2, 2) # 《----------
[[[[ 0. 36. 9. 27.] [ 0. 54. 9. 27.]] [[ 0. 36. 9. 27.] [ 0. 54. 9. 27.]]]] (1, 2, 2, 4)# 《----------
對比輸出shape,depthwise_conv2d輸出的channel數目為in_channel * 卷積核數目,每一個卷積核對應通道都會對對應的channel進行一次卷積,所以輸出通道數更多,
看到這里大家可能會誤解深度可分離卷積的輸出通道數大於普通卷積,其實這只是“分離”部分,后面還有組合的步驟,而普通卷積只不過直接完成了組合:通過對應點相加,將四個卷積中間結果合並為卷積核個數(這里是2)
2、合並特征
合並過程如下,可分離卷積中的合並過程變成可學習的了,使用一個1*1的普通卷積進行特征合並,
point_filter = tf.constant(value=1, shape=[1,1,4,4],dtype=tf.float32) out_img_s = tf.nn.conv2d(input=out_img_depthwise, filter=point_filter, strides=[1,1,1,1], padding='VALID') with tf.Session() as sess: res3 = sess.run(out_img_s) print(res3, '\n', res3.shape)
TensorFlow 一步執行
out_img_se = tf.nn.separable_conv2d(input=img, depthwise_filter=filter, pointwise_filter=point_filter, strides=[1,1,1,1], rate=[1,1], padding='VALID') with tf.Session() as sess: print(sess.run(out_img_se))
[[[[ 72. 72. 72. 72.]
[ 90. 90. 90. 90.]]
[[ 72. 72. 72. 72.]
[ 90. 90. 90. 90.]]]]
(1, 2, 2, 4)
slim 庫API介紹
def separable_convolution2d( inputs, num_outputs, kernel_size, depth_multiplier=1, stride=1, padding='SAME', data_format=DATA_FORMAT_NHWC, rate=1, activation_fn=nn.relu, normalizer_fn=None, normalizer_params=None, weights_initializer=initializers.xavier_initializer(), pointwise_initializer=None, weights_regularizer=None, biases_initializer=init_ops.zeros_initializer(), biases_regularizer=None, reuse=None, variables_collections=None, outputs_collections=None, trainable=True, scope=None): """一個2維的可分離卷積,可以選擇是否增加BN層。 這個操作首先執行逐通道的卷積(每個通道分別執行卷積),創建一個稱為depthwise_weights的變量。如果num_outputs 不為空,它將增加一個pointwise的卷積(混合通道間的信息),創建一個稱為pointwise_weights的變量。如果 normalizer_fn為空,它將給結果加上一個偏置,並且創建一個為biases的變量,如果不為空,那么歸一化函數將被調用。 最后再調用一個激活函數然后得到最終的結果。 Args: inputs: 一個形狀為[batch_size, height, width, channels]的tensor num_outputs: pointwise 卷積的卷積核個數,如果為空,將跳過pointwise卷積的步驟. kernel_size: 卷積核的尺寸:[kernel_height, kernel_width],如果兩個的值相同,則可以為一個整數。 depth_multiplier: 卷積乘子,即每個輸入通道經過卷積后的輸出通道數。總共的輸出通道數將為: num_filters_in * depth_multiplier。 stride:卷積步長,[stride_height, stride_width],如果兩個值相同的話,為一個整數值。 padding: 填充方式,'VALID' 或者 'SAME'. data_format:數據格式, `NHWC` (默認) 和 `NCHW` rate: 空洞卷積的膨脹率:[rate_height, rate_width],如果兩個值相同的話,可以為整數值。如果這兩個值 任意一個大於1,那么stride的值必須為1. activation_fn: 激活函數,默認為ReLU。如果設置為None,將跳過。 normalizer_fn: 歸一化函數,用來替代biase。如果歸一化函數不為空,那么biases_initializer 和biases_regularizer將被忽略。 biases將不會被創建。如果設為None,將不會有歸一化。 normalizer_params: 歸一化函數的參數。 weights_initializer: depthwise卷積的權重初始化器 pointwise_initializer: pointwise卷積的權重初始化器。如果設為None,將使用weights_initializer。 weights_regularizer: (可選)權重正則化器。 biases_initializer: 偏置初始化器,如果為None,將跳過偏置。 biases_regularizer: (可選)偏置正則化器。 reuse: 網絡層和它的變量是否可以被重用,為了重用,網絡層的scope必須被提供。 variables_collections: (可選)所有變量的collection列表,或者是一個關鍵字為變量值為collection的字典。 outputs_collections: 輸出被添加的collection. trainable: 變量是否可以被訓練 scope: (可選)變量的命名空間。 Returns: 代表這個操作的輸出的一個tensor"""