NIN原理和实现


论文笔记

1.解决了什么

改进CNN。

2.提出的模型

提出mlpconv,引入了1x1卷积和global average pooling,提出Network In Network(NIN),整个模型未使用全连接。

3.实验结果

在CIFAR-10和CIFAR-100上面获得SOTA结果。

4.待解决的问题

模型不够深,迁移能力有待加强。


卷积神经网络(CNN)

    卷积神经网络一般由卷积层、池化层和全连接层组成,如下图:

3.jpg

    卷积层的工作原理:卷积核与局部感受野(local receptive field)点积得到特征图(feature map),举例如下:

1.png

2.gif


模型提出

    传统卷积层的滤波器/卷积核是局部感受野的广义线性模型(GLM),作者认为GLM的抽象层级很低,使用更有效的非线性函数逼近器(nonlinear function approximator)可以提高卷积层的抽象能力。当隐含概念(latent concepts)的样本是线性可分时,即概念的变体都位于GLM定义的分离平面的一侧时,GLM可以实现很好的特征抽象。所以CNN隐含的假设是latent concepts是线性可分的。

    但是实际上相同概念的数据通常存在于非线性流形(nonlinear manifold)上,需要通过高度非线性函数去捕捉这些概念表示。在NIN中,使用微型网络(micro network)结构替代GLM,micro network是一个广义非线性函数逼近器,这里使用多层感知器(MLP)作为micro network。下图是线性卷积层和多层感知器卷积层(MlpConv Layer):

3.png

    线性卷积层和mlpconv都是将局部感受野(local receptive field)映射到输出特征向量。Mlpconv核使用带非线性激活函数的MLP,跟传统的CNN一样,MLP在各个局部感受野中共享参数的,滑动MLP核可以最终得到输出特征图。NIN通过多个mlpconv的堆叠得到。

    在NIN中,卷积层后不接全连接层(FC),而是将最后一个的mlpconv的输出特征图全局平均池化(global average pooling,GAP),而后softmax。

    使用GAP有个好处是增加模型的可解释性。传统的CNN中,因为FC层是一个黑盒,所以很难知道类别信息(category level information)是如何传到前面的卷积层的。而GAP强制要求特征图对应类别信息,因此GAP的可解释性更强。此外,相比于FC,GAP的泛化性能更好,更不容易过拟合。


线性卷积层

    传统的线性卷积层的计算公式如下:

2.png

    其中i,j是输出特征图的索引,x_ij是输入图片的局部感受野的索引,k是输出特征图的通道索引,wk是第k个卷积核,max(A,0)代表使用relu激活函数。

    前面说过,当隐含概念是线性可分时,线性卷积层对于抽象是足够的。线性卷积层需要通过一组over-complete的卷积核来覆盖所有隐含概念的变体,一个滤波器检测同一概念的变体。然而,一个概念有太多的卷积核会给下一层卷积层带来额外的负担,因为它需要考虑来自上一层的概念所有变化组合。在CNN中,后面的卷积层滤波器对应原始输入的较大区域,滤波器通过联合上一层输出的较低层的特征来生成更高层次的特征。由此可见,每一层抽象程度更高是有利的。


跨通道池化(CCP)

    这个概念在论文Maout Networks中有解释。假设你有一个线性模块,该模型有50个输出通道,但是你想要的是5个输出通道。就可以使用cross-channel pooling来对channel进行下采样(可以使用最大池化)。具体说CCP输出特征图中的每个点是10个原始特征图中对应点的最大值。


MLP卷积层

    需要用一个通用函数逼近器来抽取局部感受野的特征,最常用的有径向基网络多层感知器。选择MLP的理由是:1.MLP与卷积神经网络兼容,都使用BP训练;2. 多层感知器本身是一个深度模型,这与特征重用的思想是一致。使用MLP代替原来卷积层的GLM,新的层叫做mlpconv。mlpconv的计算如下:

4.png

    这里的n是mlp的层数,mlp使用relu作为激活函数。

    从跨通道池化的观点看,上式等价于在普通卷积层上级联跨通道参数池化层(cascaded cross channel parametric pooling,CCCP),每一个池化层对输入特征图进行加权线性重组,然后通过一个非线性激活函数。直白地说CCCP就是1x1卷积。这种结构允许复杂和可学习的跨通道信息交互。


全局平均池化(GAP)

    传统的CNN模型先是使用堆叠的卷积层提取特征,输入全连接层(FC)进行分类。这种结构沿袭自LeNet5,把卷积层作为特征抽取器,全连接层作为分类器。但是FC层参数数量太多,很容易过拟合,会影响模型的泛化性能。因此需要用Dropout增加模型的泛化性。

    这里提出GAP代替传统的FC层。主要思想是零每个分类对应最后一层mlpconv的输出特征图。对每个特征图取平均,后将得到的池化后的向量softmax得到分类概率。GAP的优点:

  1. 加强了特征映射和类别之间的对应,更适合卷积神经网络,特征图可以被解释类别置信度。

  2. GAP层不用优化参数,可以避免过拟合。

  3. GAP对空间信息进行汇总,因此对输入数据的空间变换有更好的鲁棒性。

    可以将GAP看做一个结构正则化器,显性地强制特征图映射为概念置信度。


Network In Network(NIN)

    NIN是由mlpconv堆叠而成,顶部是GAP层,在mlpconv层之间加入下采样层。下图是有三层mlpconv层的NIN,每个mlpconv层里有3层的mlp。

5.png

    NIN是基于AlexNet 改进而来,因此卷积层的其它参数设置和alexnet类似。


NIN训练

    使用四个基准数据集评估NIN模型:CIFAR-10 CIFAR-100 SVHN MNIST。

    模型架构:三层mlpconv堆叠而成,每个mlpconv层后接最大池化层,size=2,stride=2。最后接GAP。

    Dropout:除了最后的mlpconv层外,其他层均使用dropout。

    正则化:所有权重应用L2正则化。

    数据集预处理和及其它训练过程与AlexNet类似。


NIN对比实验

    1. CIFAR-10数据集

    对这个数据集,作者应用了相同全局对比度归一化和ZCA白化进行处理,这和Maxout论文里的类似。训练时先通过验证集调整local receptive field size和weight decay,当确定好这两个超参数后,重新训练网络。

    首先测试有无dropout的影响,结果如下:

6.png

    从上图可以看到,在mlpconv层之间引入dropout可以减少20%以上的测试误差,因此这里所有测试的模型都加入了dropout。NIN无数据增强的情况下最终得到10.41%的测试错误率,加入数据增强后达到8.81%。如下图:

7.png


    2. CIFAR-100数据集

8.png



    3. Street View House Number数据集

9.png


    4. MNIST数据集

10.png


GAP对比实验

    GAP和FC类似,都是对向量化的特征映射进行线性转换,不同的地方是变换矩阵的不同。GAP的变换矩阵is prefixed and it is non-zero only on block diagonal elements which share the same value.(没理解)。FC有稠密的变换矩阵,变换矩阵的值由BP优化。FC和GAP对比如下:

11.png

    作者还探索了GAP在传统的CNN模型上是否也有更好的效果。比如将LeNet5改造,使其最后一个卷积层输出10通道特征图,然后用GAP替换FC+Dropout。在CIFAR-10上测试。

    结果对比:仅仅FC的测试错误率为17.56%,FC+Dropout的测试错误率为15.99%,GAP的测试错误率为16.46%。由此可知GAP具有正则化的作用。之所以比FC+Dropout差,作者说是因为GAP对线性卷积层的要求高,需要带ReLU的线性滤波器去建模类别置信度映射。


NIN可视化

    对CIFAR-10上的模型的特征图进行可视化。如下图:

12.png

    很明显最大激活的的特征图对应ground truth。可视化再次证明了NIN的有效性,它通过mlpconv层进行更强的局部接受域建模,全球平均池加强了类别级特征映射的学习。



代码实现

    使用tensorflow.slim,基于CIFAR-10数据集训练NIN网络。

  1. CIFAR-10数据集预处理

import numpy as np
from keras.datasets import cifar10
import keras

num_classes=10

(x_train, y_train), (x_test, y_test) = cifar10.load_data()
y_train = keras.utils.to_categorical(y_train, num_classes) # one-hot 编码
y_test = keras.utils.to_categorical(y_test, num_classes) # one-hot 编码

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
# 数据集减去均值,除以标准差
x_train, x_test = color_preprocessing(x_train, x_test)


    2.模型定义

import tensorflow as tf
from tensorflow.contrib.layers import xavier_initializer
from tensorflow import name_scope as namespace

slim = tf.contrib.slim

REGULARIZER=0.0005

def NIN(inputs):
    with slim.arg_scope([slim.conv2d], 
                        stride=1,
                        activation_fn=tf.nn.relu,
                        padding='SAME',
                        weights_initializer=xavier_initializer(),
                        weights_regularizer=slim.l2_regularizer(REGULARIZER),
                        biases_regularizer=slim.l2_regularizer(REGULARIZER),
                        biases_initializer=tf.zeros_initializer()):
        net=inputs
        with namespace('mlpconv_1'):
            net = slim.conv2d(net, kernel_size=5,num_outputs=192,scope='mlpconv_1_conv')
            net = slim.conv2d(net, kernel_size=1,num_outputs=160,scope='mlpconv_1_conv1x1_1')
            net = slim.conv2d(net, kernel_size=1,num_outputs=96,scope='mlpconv_1_conv1x1_2')
        net=slim.max_pool2d(net,[3,3],2,padding='SAME',scope='maxpooling1')
        net = slim.dropout(net, 0.5, scope='dropout1')
        
        with namespace('mlpconv_2'):
            net = slim.conv2d(net, kernel_size=5,num_outputs=192,scope='mlpconv_2_conv')
            net = slim.conv2d(net, kernel_size=1,num_outputs=192,scope='mlpconv_2_conv1x1_1')
            net = slim.conv2d(net, kernel_size=1,num_outputs=192,scope='mlpconv_2_conv1x1_2')
        net=slim.max_pool2d(net,[3,3],2,padding='SAME',scope='maxpooling2')
        net = slim.dropout(net, 0.5, scope='dropout2')
        
        with namespace('mlpconv_3'):
            net = slim.conv2d(net, kernel_size=3,num_outputs=192,scope='mlpconv_3_conv')
            net = slim.conv2d(net, kernel_size=1,num_outputs=192,scope='mlpconv_3_conv1x1_1')
            net = slim.conv2d(net, kernel_size=1,num_outputs=10,scope='mlpconv_3_conv1x1_2')
        
        #GAP
        net= tf.reduce_mean(net, [1, 2], name='GAP', keep_dims=True)
        outputs=tf.squeeze(net,axis=[1,2],name='reshape',)
        return outputs


    3.模型配置

from tensorflow import name_scope as namespace

BATCH_SIZE=128
DATA_LEN=x_train.shape[0]

x = tf.placeholder(tf.float32, shape=[None, 32, 32, 3], name='input')
y_ = tf.placeholder(tf.float32, [None, 10], name='labels')
global_step=tf.Variable(0,trainable=False)

y=NIN(x)

with namespace('loss'):
    #softmax并计算交叉熵
    #print(y.get_shape().as_list() )
    ce_loss = slim.losses.softmax_cross_entropy(y, y_) #交叉熵损失
    regularization_loss = tf.add_n(slim.losses.get_regularization_losses())#正则损失
    loss=ce_loss+regularization_loss
    
with namespace('train'):
    #使用指数衰减学习率
    learning_rate=tf.train.exponential_decay(
        0.01,#初始学习率
        global_step,
        DATA_LEN/BATCH_SIZE,#多少次更新一次学习率
        0.99,#学习率衰减率
        staircase=True#学习率阶梯下降
    )
    train_step=tf.train.MomentumOptimizer(learning_rate,0.9,#动量系数
           ).minimize(loss,global_step=global_step)
    
with namespace('acc'):
    correct_prediction=tf.equal(tf.argmax(y,1),tf.argmax(y_,1))
    accuracy=tf.reduce_mean(tf.cast(correct_prediction,tf.float32))  
    
    
tf.summary.scalar('loss',loss)
tf.summary.scalar('accuracy',accuracy)
merged=tf.summary.merge_all();


    3.模型训练,keras的数据生成器太好用了。

#定义数据生成器
from keras.preprocessing import image
import os
import time

epochs=30

TEST_STEP=int(x_test.shape[0]/BATCH_SIZE)-1
datagen = image.ImageDataGenerator(
    horizontal_flip=True,
    width_shift_range=0.125,
    height_shift_range=0.125,
    fill_mode='constant',
    cval=0.)
datagen.fit(x_train)
tg=datagen.flow(x_train, y_train, batch_size=BATCH_SIZE)

with tf.Session() as sess:
    init_op=tf.global_variables_initializer()
    sess.run(init_op)
    writer=tf.summary.FileWriter('D:/Jupyter/cv/NIN_log',sess.graph)
    saver=tf.train.Saver()
    
    for i in range(epochs):
        start = time.clock()
        for j in range(int(DATA_LEN/BATCH_SIZE)):
            next_data,next_label=next(tg)
            summary,_,step=sess.run([merged,train_step,global_step],feed_dict={x:next_data,y_:next_label})
            writer.add_summary(summary,step)
            
        loss_sum=0.0
        acc_sum=0.0
        for k in range(TEST_STEP):
            loss_value,acc=sess.run([loss,accuracy],
                                    feed_dict={x:x_test[k*BATCH_SIZE:(k+1)*BATCH_SIZE],
                                               y_:y_test[k*BATCH_SIZE:(k+1)*BATCH_SIZE]}
                                   )
            loss_sum+=loss_value
            acc_sum+=acc
        elapsed = (time.clock() - start)
        print('epochs:%d loss:%f acc:%f time:%f'%(i,loss_sum/TEST_STEP,acc_sum/TEST_STEP,elapsed))
        
        model_save_path = os.path.join(r'D:/Jupyter/cv/NIN_log', 'model.ckpt')
        saver.save(sess, save_path=model_save_path, global_step=i+1)
    writer.close()

    训练30轮的结果:

r.png




参考文献

[1]Min Lin, Qiang Chen, Shuicheng Yan.Network In Network.

[2] bryant_meng.【Keras-NIN】CIFAR-10. https://blog.csdn.net/bryant_meng/article/details/80979211. 2018-11-23

首页 所有文章 机器人 计算机视觉 自然语言处理 机器学习 编程随笔 关于