AlexNet原理和实现

    本文是读深度学习的开山之作《ImageNet Classification with Deep Convolutional Neural Networks》所作的笔记。


论文笔记

1.解决了什么问题

大规模图像分类的问题。

2.使用的方法

搭建深度卷积神经网络进行图像分类,被称为AlexNet

在GPU上实现了高度优化的卷积神经网络的训练过程

使用了ReLU激活函数,局部响应归一化,重叠池化等trick。

使用了数据增强、Dropout防止过拟合。

3.实验结果

该模型让深度学习成为最主流的机器学习算法

在ImageNet LSVRC-2010数据集上达到state of the art,模型的top5错误率为17.0%    

在ILSVRC-2012比赛中获得冠军,测试集top5错误率降低到15.3%

4.待解决的问题

由于当时计算资源的缺乏,该网络不够深,不够宽,因此准确率仍有很大的提升空间。


论文细节

AlexNet:这个神经网络拥有6000w个参数和65w的神经元,由5层卷积池化层组成,后面接3层全连接层,输出层有1000个神经元。

模型架构:

1.png

    如上图所示,模型由5个卷积层和3个全连接层组成,其中最后一个全连接层是softmax层,在ImageNet-1k数据集上训练,故输出1000个分类,使用分类交叉熵作为损失函数。

    可以看到模型分为两部分,因为该模型在两个GPU上训练,其中按照图像数据的通道切分为两部分,两部分在特定的层通信。卷积层的第二、第四、第五层直接和同一个GPU的前一层相连,第三层则与两个GPU上的第二层相连。全连接层都和上一层的所有神经元相连。

    卷积层的第一层使用96个卷积核,size=(11,11),stride=4。第二卷积层使用256个卷积核,size=(5,5)。第三卷积层使用3x3的384个卷积核,第四卷积层使用3x3的384个卷积核,第五卷积层使用3x3的256个卷积核。全连接有两层有4096个神经元。

    局部响应归一化应用在第一和第二卷积层之后,所有层的激活函数是ReLU,Dropout应用在全连接层。


在模型中用到的技术:

1.ReLU非线性激活函数

    这篇论文之前激活函数一般选择饱和非线性函数f(x)=tanh(x)或者sigmoid函数:f(x)=(1+e^(-x))^(-1),与之相比,非饱和非线性的ReLU f(x)=max(0,x)使用梯度下降训练的速度要快几倍。

2.在多GPUs上面训练

    Alex模型在两个GPU上并行训练,加快了训练速度,两部分只在特定的层通信。

3.局部响应归一化(LRN,Local Response Normalization)

    因为使用了ReLU,则不需要对输入进行标准化防止过拟合。对ReLU输出后的数据进行LRN的公式如下:

2.png

ai是像素坐标为(x,y),第i通道的输出值,bi是将a归一化后的值,N是通道的总数,k、alpha、beta、n都是参数。公式叠加通道方向的值,n表示叠加的范围,示意图如下:

3.png

    LRN的原理 :局部归一的动机:在神经生物学有-一个概念叫做侧抑制(lateral inhibitio) ,指的是被激活的神经元抑制相邻神经元。归一化(normalization)的目的是“抑制”,局部响应归一化就是借鉴侧抑制的思想来实现局部抑制,尤其当我们使用ReLU的时候这种“侧抑制”很管用。LRN的优点:增加范化能力。LRN层模仿生物神经系统的侧抑制机制,对局部神经元的活动创建竞争机制,使得响应比较大的值相对更大,提高模型范化能力。

    论文使用的超参数为k=2,n=5,alpha=10^-4,beta=0.75

4.重叠池化(Overlapping Pooling)

    重叠池化即池化步长<池化窗口,重叠池化可以减小过拟合。这里使用最大池化步长=2,池化窗口大小=3。


防止过拟合的方法

1.数据增强

    使用两种方式进行数据增强:

    第一种方法是图像平移和水平翻转和裁切。裁切过程先将图片的短边缩放至256,然后在中间裁切出256x256的图片。256x256的图片在四个角和中间裁切出5张224x224的图片,就是我们的训练数据。

    第二种方法是改变图片RGB通道的亮度。在ImageNet数据集上面执行PCA,对每个训练图片,将找到的主成分的倍数相加,与特征值成比例的模量乘以一个随机变量,随机变量来自均值为0标准差为0.1的高斯分布。对于每个像素Ixy=[Ixy_R, Ixy_G, Ixy_B],公式如下:

4.png

    pi是特征向量,lambdai是特征值,pi和lambdai都来源于RGB像素值的3x3的协方差矩阵的,alphai是随机变量。

2.使用Dropout

    在两个全连接层使用了0.5比率的dropout,这种技术减少了神经元之间复杂的相互适应。因为神经元不能依赖于其他神经元的存在,它被迫学习与其他神经元的许多不同随机子集结合使用的更健壮的特征。


模型训练

    AlexNet在ImageNet LSVRC-2010上面训练,该数据集有1000个分类,120w的数据。用GTX580 3GB GPU跑了5、6天。模型的输入为224x224大小,对于各种分辨率的图片,先将图片的短边缩放至224,再从中间进行裁剪中224x224的图片。对于图片的每个像素,减去训练集上的平均像素值。

    模型使用SGD训练,batch_size=128,动量是0.9,权重衰减因子为0.0005,权重更新的公式如下:

5.png

    v是动量变量。权重初始化采用高斯随机数,均值为0标准差为0.01,偏置初始化为0,学习是手动调整的,初始学习率为0.01当验证错误率不再提高时学习减小10倍。

    效果:在ImageNet LSVRC-2010数据集上达到state of the art,模型的top5错误率为17.0% ,ILSVRC-2012比赛中获得冠军,测试集top5错误率降低到15.3%。ps:在ImageNet竞赛中,通常用top-1和top-5错误率表示模型的性能,top-5错误率表示正确的标签不在最可能的5个标签的比率。


代码实现

  1. 数据集和数据预处理

    鉴于我的计算资源十分缺乏,因此我不打算在论文作者使用的数据集上跑一边。刚好我有一个mini的ImageNet数据集,包含100个分类总共6w个样本,勉强满足测试要求。数据集链接:https://pan.baidu.com/s/17mX1c_Xteaw7B9dr9elhng 提取码:qkmh 

    首先把训练图片缩放裁剪至224x224,并按分类保存到不同的文件夹便于调用Keras的数据增强API,代码如下:

import os
import cv2
import tqdm

IMG_SIZE=224

def ReadAndCutPicture(r_path,w_path):
    img = cv2.imread(r_path) #读到图片为BGR
    h=img.shape[0]
    w=img.shape[1]
    if(h<=w):
        new_h=IMG_SIZE
        new_w=int(w/h*IMG_SIZE)
        img=cv2.resize(img,(new_w,new_h))  #输出图片尺寸为(宽,高)
        cut_w=int((new_w-new_h)/2)
        img=img[:,cut_w:cut_w+IMG_SIZE,:]
    else:
        new_w=IMG_SIZE
        new_h=int(h/w*IMG_SIZE)
        img=cv2.resize(img,(new_w,new_h))
        cut_h=int((new_h-new_w)/2)
        img=img[cut_h:cut_h+IMG_SIZE,:,:]
    cv2.imwrite(w_path, img)

normal_path=r'F:\BaiduNetdiskDownload\mini-imagenet\images_normal' #裁剪后图片所在的位置
base_path=r'F:\BaiduNetdiskDownload\mini-imagenet\images' #数据集所在位置

for name in tqdm.tqdm(os.listdir(base_path)):
    r_path=os.path.join(base_path,name)
    w_dir=os.path.join(normal_path,name[:9])
    w_path=os.path.join(w_dir,name)
    
    if(os.path.exists(w_dir)==False):
        os.makedirs(w_dir)
    ReadAndCutPicture(r_path,w_path)


2.搭建网络

    Keras官方没有给出LRN层的实现,我在github上找了一个现成的LRN实现,但是放上去就报错了,这个代码是这样的:

from keras.layers.core import Layer
from keras import backend as K
import tensorflow as tf
import numpy as np
class LRN(Layer):
    def __init__(self, alpha=0.0001,k=1,beta=0.75,n=5, **kwargs):
        self.alpha = alpha
        self.k = k
        self.beta = beta
        self.n = n
        super(LRN, self).__init__(**kwargs)
    
    def call(self, x, mask=None):
        b, ch, r, c = x.shape
        half_n = self.n // 2 # half the local region
        # orig keras code
        #input_sqr = T.sqr(x)  # square the input
        input_sqr = K.square(x) # square the input
        # orig keras code
        #extra_channels = T.alloc(0., b, ch + 2 * half_n, r,c)  # make an empty tensor with zero pads along channel dimension
        #input_sqr = T.set_subtensor(extra_channels[:, half_n:half_n+ch, :, :],input_sqr) # set the center to be the squared input
        extra_channels = K.zeros((b, int(ch) + 2 * half_n, r, c))
        input_sqr = K.concatenate([extra_channels[:, :half_n, :, :],input_sqr, extra_channels[:, half_n + int(ch):, :, :]],axis = 1)
        scale = self.k # offset for the scale
        norm_alpha = self.alpha / self.n # normalized alpha
        for i in range(self.n):
            scale += norm_alpha * input_sqr[:, i:i+int(ch), :, :]
        scale = scale ** self.beta
        x = x / scale
        return x
    def get_config(self):
        config = {"alpha": self.alpha,
                  "k": self.k,
                  "beta": self.beta,
                  "n": self.n}
        base_config = super(LRN, self).get_config()
        return dict(list(base_config.items()) + list(config.items()))
class PoolHelper(Layer):
    
    def __init__(self, **kwargs):
        super(PoolHelper, self).__init__(**kwargs)
    
    def call(self, x, mask=None):
        return x[:,:,1:,1:]
    
    def get_config(self):
        config = {}
        base_config = super(PoolHelper, self).get_config()
        return dict(list(base_config.items()) + list(config.items()))

    由于使用的数据集跟原文的并不相同,因此这里对AlexNet做了微调,网络如下:

from keras.models import *
from keras.layers import *
from keras import models
from keras import initializers
from keras.utils import plot_model
normal_init=initializers.RandomNormal(mean=0.0, stddev=0.01, seed=None)
X=Input(shape=(224,224,3))
conv1=Conv2D(96,(11,11),
            strides=(4,4),
            padding='same',
            activation='relu',
            kernel_initializer=normal_init,
            bias_initializer='zeros',
            name='conv1')(X)
maxpooling1=MaxPooling2D(pool_size=(3,3),strides=2,name='maxpooling1')(conv1)
#lrn1=LRN(alpha=0.0001,k=2,beta=0.75,n=5)(maxpooling1)
conv2=Conv2D(256,(5,5),
            strides=(1,1),
            padding='same',
            activation='relu',
            kernel_initializer=normal_init,
            bias_initializer='zeros',
            name='conv2')(maxpooling1)
maxpooling2=MaxPooling2D(pool_size=(3,3),strides=2,name='maxpooling2')(conv2)
#lrn2=LRN(alpha=0.0001,k=2,beta=0.75,n=5)(maxpooling2)
conv3=Conv2D(384,(3,3),
            strides=(1,1),
            padding='same',
            activation='relu',
            kernel_initializer=normal_init,
            bias_initializer='zeros',
            name='conv3')(maxpooling2)
conv4=Conv2D(384,(3,3),
            strides=(1,1),
            padding='same',
            activation='relu',
            kernel_initializer=normal_init,
            bias_initializer='zeros',
            name='conv4')(conv3)
conv5=Conv2D(256,(3,3),
            strides=(1,1),
            padding='same',
            activation='relu',
            kernel_initializer=normal_init,
            bias_initializer='zeros',
            name='conv5')(conv4)
maxpooling3=MaxPooling2D(pool_size=(3,3),strides=2,name='maxpooling3')(conv5)
flatten=Flatten(name='flatten')(maxpooling3)
dense1=Dense(512,activation='relu',kernel_initializer=normal_init,name='dense1')(flatten)
dropout1=Dropout(0.5)(dense1)
dense2=Dense(512,activation='relu', kernel_initializer=normal_init,name='dense2')(dropout1)
dropout2=Dropout(0.5)(dense2)
outputs=Dense(100,activation='sigmoid',kernel_initializer=normal_init,name='output')(dropout2)
model = Model(X, outputs)
model.summary()
plot_model(model,to_file='AlexNet.png',show_shapes=True)

计算图:

AlexNet.png


3.训练网络

    这里使用Keras官方带数据增强的数据生成器,代码如下:

#定义数据生成器
from keras.preprocessing import image
from keras import optimizers
train_gen=image.ImageDataGenerator(
    featurewise_center=True,#输入数据数据减去数据集均值
    width_shift_range=0.2,#水平平移
    height_shift_range=0.2,#垂直平移
    horizontal_flip=True,#水平翻转
    brightness_range=[-0.1,0.1] #亮度变化范围
)
#编译模型
model.compile(loss='categorical_crossentropy',optimizer=optimizers.RMSprop(lr=1e-4),metrics=['acc'])
#训练图片路径
train_dir=r'F:\BaiduNetdiskDownload\mini-imagenet\images_normal'
tg=train_gen.flow_from_directory(
    train_dir,
    target_size=(224,224),
    batch_size=128,
    class_mode='categorical'
)
tg.class_indices

    开始训练。。。

history=model.fit_generator(
    tg,#训练数据生成器
    steps_per_epoch=10,#从生成器中抽取100个批次,
    epochs=5,#进行30轮训练
    shuffle=True,
)

因为是使用数据放在磁盘上,训练实在是太慢了,所以意思意思就得了。



参考文献

[1]Alex Krizhevsky,Ilya Sutskever,Geoffrey E. Hinton.ImageNet Classification with Deep Convolutional

Neural Networks.2012

[2] yangdashi888. 深度学习的局部响应归一化LRN(Local Response Normalization)理解. https://blog.csdn.net/yangdashi888/article/details/77918311. 2017-09-09

[3] Microstrong0305. 在AlexNet中LRN 局部响应归一化的理解. https://blog.csdn.net/program_developer/article/details/79430119. 2018-03-03

下一篇:

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