該論文提出了一種新穎的深度網絡結構,稱為“Network In Network”(NIN),以增強模型對感受野內local patches的辨別能力。與傳統的CNNs相比,NIN主要的創新點在於結構內使用的mlpconv layers(multiple layer perceptron convolution layers)和global average pooling。下面先介紹二者:
- MLP Convolution Layers
如Fig.1所示,傳統卷積網絡中的 linear convolution layer由linear filter+nonlinear activation構成,而mlpconv layer內部是一個micro network(在論文中作者選擇multilayer perceptron(MLP)作為 micro network)。作者之所以嘗試尋找一種新的layer代替 linear convolution layer,是因為傳統的卷積層存在着明顯的缺陷。一是CNN中的卷積核是 data patch 上的一個廣義線性模型(Generalized linear model,GLM),它的 abstraction 程度較低(這里的abstraction是指特征對同一概念(concept)的變體是不變的)。如果將GLM換成一個更有效的非線性函數逼近器就能夠增強 local model 的abstraction能力。二是當latent concepts的samples線性可分時,GLM才能達到很好的abstraction程度,比如concepts的變體全都在GLM定義的分界面一側,我們在使用傳統卷積時實際上是假設 the latent concepts是線性可分的。但是,相同concept的data往往是呈非線性流形(nonlinear manifold)分布的,因而對應那些concepts的表示(representations)通常是輸入的高度非線性函數。當latnet concepts的samples是線性可分時,linear convolution的abstraction能力是足夠的。線性不可分時,傳統的CNN會通過利用一系列完備的filters覆蓋latent concepts的所有變體來彌補線性划分的不足。也就是說,對於同一個concept,使用不同的linear filters來檢測不同的變體(variations)。但是,單個concept有太多filters的話下一層需要考慮到所有來自前面layers的combinations of variations,這會給下一層增加額外的負擔。正如CNN中那樣,來自higher layers的filters會在原始輸入上映射出更大的區域,這樣通過combinig來自低層的lower level concepts會產生一個higher level的concept。因此作者認為,在將lower level concepts combining成higher level concepts之前,在每一個local patch上進行更好的abstraction會很有意義。(patch:每次filter進行卷積時input或faeture maps參數計算的小區域;concept: 應該是希望檢測的objects的高級特征,如船舶、飛機等,論文后面說是categories;abstraction: 對同一concept的變體提取的特征不變的特征提取)
為此,作者將GLM替換為一個“micro network”結構,它是一個通用非線性函數逼近器(general nonlinear function approximator)。論文中作者選擇多層感知器(multilayer perceptron,MLP)作為micro network,原因有如下兩點:
1) 多層感知器與卷積神經網絡結構兼容,可以使用反向傳播算法進行訓練
2) 多層感知器本身可以是一個深度模型(Deep model),這與feature re-use的精神是一致的
mlpconv layer執行的計算如下:
式中,n是多層感知器層數,從max()也可以看出多層感知器中使用的激活函數是Rectified linear unit。該計算過程等同於在一個傳統的卷積層之后連接級聯的跨通道參數池化層。每一個池化層先在輸入的feature maps上進行加權線性重構,然后將結果輸入一個rectifier linear unit激活。跨通道池化得到feature maps接下來會在后面的layers中重復被跨通道池化。這種級聯的跨通道參數池化結構(cascaded cross channel parameteric pooling structure)允許復雜的可學習的跨通道信息交互。
跨通道參數池化層就相當於一個卷積核為1*1的 linear convolution layer(包括激活函數)。這種解釋有助於直觀理解NIN的結構。
此外,論文中還解釋了為何micro network不使用maxout network。如下:
maxout network通過對affine feature maps進行最大池化達到減少feature maps數量的目的(affine feature maps是 linear convolution 不帶activation function直接計算得到的結果)。最大池化線性函數的結果能夠得到一個可以逼近任何凸函數的分段線性逼近器,所以與進行線性划分的傳統卷積層相比,maxout network效果更好,因為它能夠對位於凸集(Convex set)內的concepts進行正確划分。
但是,maxout network默認在輸入空間中latent concepts的samples分布在一個凸集內,這一先驗知識卻不一定成立。考慮到distributions of the latent concepts的復雜性,我們需要的是一個更加通用的函數逼近器(more general function approximator),比如MLP
- Global Average Pooling
傳統的CNNs在網絡的lower layers執行卷積。在分類任務中,網絡最后一層卷積層的feature maps被向量化,並且隨后被送入全連接層與softmax logistic regression layer。這種結構結合了convolutional structure和傳統的neural network classifiers,將卷積層視為feature extractors,然后使用傳統的方法對得到的特征進行分類。但是,全連接層在訓練時容易overfitting,后來采用regularizer方法dropout大幅減輕了過擬合。
在該論文中,作者提出了另外一種叫做global average pooling的方法來代替傳統的全連接層。具體是:在最后一層mlpconv layer中為分類任務中的每一個類生成一個feature map;后面不接全連接網絡,而是取每個feature map的均值,這樣就得到一個長度為N的向量,與類別對應;添加softmax層,前面的向量直接傳入計算,這樣就得到了每個類別的概率。
與全連接層相比,全局平均池化有三個優點:
1) 全局平均池化強制建立feature maps和categories之間的對應關系,這使它更適用於卷積結構。因此feature maps可以很容易地被轉換成categories confidence maps
2)全局平均池化層中沒有需要優化的參數,因而能夠避免overfitting
3)全局平均池化對空間信息進行匯總,因此它對輸入的空間轉換更加魯棒
Global average pooling 可以被視為一個能夠將feature maps強制轉換為concepts(categories)的confidence maps的structural regularizer,因為mlpconv layers能比GLMs更好地逼近confidence maps
- Network In Network Structure
論文中有句概括性的話,“NIN is proposed from a more general perspective, the micro network is integrated into CNN structure in persuit of better abstractions for all levels of features”。翻譯過來就是,NIN是從一個更一般的角度(非線性划分)提出的,將micro network 整合到CNN結構中是為了更好地abstract所有levels的特征。
NIN的整個結構是由一系列mlpconv layers + global average pooling + objective cost layer 構成。除此之外,可以像在CNN和maxout network中那樣在mlpconv layers之間添加sub-sampling layers。
下面Fig.2是一個NIN結構,由3層mlpconv layers堆疊+1層global average pooling layer構成,每個mlpconv layer內包含一個3層的感知器。:
Fig.2很好理解,有點困惑的可能是mlpconv layers。前面說到過,在mlpconv layers中先進行一次傳統的卷積(filter尺寸隨意,比如3*3),然后結果經過MLP計算(1*1卷積)得到結果。下面根據下圖介紹具體過程:
1) 紅框部分是在進行傳統的卷積,可見卷積使用了多個filters。圖中豎排圓圈是卷積得到的feature maps在某個位置上所有通道的數值,而不是某個feature map。這也是為了簡單直
觀。
2) 圖中藍色框部分是MLP的前兩層,最后一層在mlpconv layer輸出的feature map上,只有一個節點,即藍色框后面的小立方體
3)MLP是在多組feature maps的同一位置建立的,而且feature maps每個通道內的元素前一組feature maps中對應元素連接的權重相同(想象1*1卷積)
4)MLP計算過程中得到的feature maps長寬一致。理解不了的話可以從1*1卷積的角度思考
下面是一幅帶參數的NIN,可以幫助我們理解mlpconv layers:
下圖是Fig.2中NIN結構的細節:
看到網上有人針對上圖中的結構寫了實現代碼就搬過來了。如下,dropout設置為0.5,weight decay設置為0.0001,使用data augmentation。在數據的預處理上采用減掉mean再除以std的方法:
import keras import numpy as np from keras.datasets import cifar10 from keras.preprocessing.image import ImageDataGenerator from keras.models import Sequential from keras.layers import Dense, Dropout, Activation, Flatten from keras.layers import Conv2D, MaxPooling2D, GlobalAveragePooling2D, AveragePooling2D from keras.initializers import RandomNormal from keras import optimizers from keras.callbacks import LearningRateScheduler, TensorBoard from keras.layers.normalization import BatchNormalization batch_size = 128 epochs = 164 iterations = 391 num_classes = 10 dropout = 0.5 log_filepath = './nin' def color_preprocessing(x_train,x_test): x_train = x_train.astype('float32') x_test = x_test.astype('float32') mean = [125.307, 122.95, 113.865] std = [62.9932, 62.0887, 66.7048] for i in range(3): x_train[:,:,:,i] = (x_train[:,:,:,i] - mean[i]) / std[i] x_test[:,:,:,i] = (x_test[:,:,:,i] - mean[i]) / std[i] return x_train, x_test def scheduler(epoch): learning_rate_init = 0.08 if epoch >= 81: learning_rate_init = 0.01 if epoch >= 122: learning_rate_init = 0.001 return learning_rate_init def build_model(): model = Sequential() model.add(Conv2D(192, (5, 5), padding='same', kernel_regularizer=keras.regularizers.l2(0.0001), kernel_initializer=RandomNormal(stddev = 0.01), input_shape=x_train.shape[1:])) model.add(Activation('relu')) model.add(Conv2D(160, (1, 1), padding='same', kernel_regularizer=keras.regularizers.l2(0.0001), kernel_initializer=RandomNormal(stddev = 0.05))) model.add(Activation('relu')) model.add(Conv2D(96, (1, 1), padding='same', kernel_regularizer=keras.regularizers.l2(0.0001), kernel_initializer=RandomNormal(stddev = 0.05))) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=(3, 3),strides=(2,2),padding = 'same')) model.add(Dropout(dropout)) model.add(Conv2D(192, (5, 5), padding='same', kernel_regularizer=keras.regularizers.l2(0.0001), kernel_initializer=RandomNormal(stddev = 0.05))) model.add(Activation('relu')) model.add(Conv2D(192, (1, 1),padding='same', kernel_regularizer=keras.regularizers.l2(0.0001), kernel_initializer=RandomNormal(stddev = 0.05))) model.add(Activation('relu')) model.add(Conv2D(192, (1, 1),padding='same', kernel_regularizer=keras.regularizers.l2(0.0001), kernel_initializer=RandomNormal(stddev = 0.05))) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=(3, 3),strides=(2,2),padding = 'same')) model.add(Dropout(dropout)) model.add(Conv2D(192, (3, 3), padding='same', kernel_regularizer=keras.regularizers.l2(0.0001), kernel_initializer=RandomNormal(stddev = 0.05))) model.add(Activation('relu')) model.add(Conv2D(192, (1, 1), padding='same', kernel_regularizer=keras.regularizers.l2(0.0001), kernel_initializer=RandomNormal(stddev = 0.05))) model.add(Activation('relu')) model.add(Conv2D(10, (1, 1), padding='same', kernel_regularizer=keras.regularizers.l2(0.0001), kernel_initializer=RandomNormal(stddev = 0.05))) model.add(Activation('relu')) model.add(GlobalAveragePooling2D()) model.add(Activation('softmax')) sgd = optimizers.SGD(lr=.1, momentum=0.9, nesterov=True) model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy']) return model if __name__ == '__main__': # load data (x_train, y_train), (x_test, y_test) = cifar10.load_data() y_train = keras.utils.to_categorical(y_train, num_classes) y_test = keras.utils.to_categorical(y_test, num_classes) x_train, x_test = color_preprocessing(x_train, x_test) # build network model = build_model() print(model.summary()) # set callback tb_cb = TensorBoard(log_dir=log_filepath, histogram_freq=0) change_lr = LearningRateScheduler(scheduler) cbks = [change_lr,tb_cb] # set data augmentation print('Using real-time data augmentation.') datagen = ImageDataGenerator(horizontal_flip=True,width_shift_range=0.125,height_shift_range=0.125,fill_mode='constant',cval=0.) datagen.fit(x_train) # start training model.fit_generator(datagen.flow(x_train, y_train,batch_size=batch_size),steps_per_epoch=iterations,epochs=epochs,callbacks=cbks,validation_data=(x_test, y_test)) model.save('nin.h5')
此外,作者又在這份代碼的基礎上加入了batch normalization改進該網絡,見此。具體是其它地方完全不動,只在conv和activation之間加入一個bn層,如下:
model.add(Conv2D(192, (5, 5), padding='same', kernel_regularizer=keras.regularizers.l2(0.0001), kernel_initializer=RandomNormal(stddev = 0.01), input_shape=x_train.shape[1:])) model.add(BatchNormalization()) model.add(Activation('relu'))
- Experiment
實驗內容見論文或下方的翻譯鏈接
- 參考文獻