前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从零开始手把手带你了解并复现AlexNet

从零开始手把手带你了解并复现AlexNet

作者头像
CristianoC
发布2020-06-02 11:19:51
1.1K0
发布2020-06-02 11:19:51
举报

学深度学习也有一年多了,学的东西较为多且杂,最近打算从一些经典论文阅读与复现入手,一方面是给自己留个笔记,另一方面也给一些学深度学习的小白或者一些光学了理论不知道如何实践的朋友一个指引。本篇文章由于是面向小白的,所以后面代码讲解得会比较详细,希望初学者看完之后能理解并自己复现。

目录

  • 论文概述
  • 代码详解

论文概述

概述论文之前笔者啰嗦多几句,一些初学者可能喜欢上网看一些网上大牛讲解的一些网络结构进行学习,不是说这样不好,一些大牛会把一些晦涩的理论知识讲解的比较生动,但一些论文的细节可能就不会讲解到,笔者认为学习深度学习还是配合论文学习,要自己咬着牙去看论文,能收获到很多看别人讲解收获不到的知识,以下是英语不好的我我第一次看这篇文章配合欧路字典的惨烈结果,当然最后还是坚持啃了几遍,然后把这篇论文给复现了。

AlexNet的论文的创新点有以下几点:

  1. 首次提出ReLU不饱和非线性单元(Rectified Linear Units)
  2. 多GPU并行训练。
  3. 提出LRN局部响应归一化(Local Response Normalization)
  4. 提出重叠池化(Overlapping Pooling)

接下来我们就这些创新点一点点说,再说一下AlexNet的网络结构,其他论文提到的细节的比如采取随机失活和数据增强策略来防止过拟合等我就不一一赘述了。

ReLU激活函数

在此之前,一般选择tanh作为激活函数,当使用梯度下降法训练的时候,这类饱和非线性激活函数,相对于非饱和非线性激活函数,会耗费更长的训练时间。(因为输入值处于饱和区时,梯度几乎为0,收敛极慢)

由图可知:采用ReLU的四层CNN(实线)在CIFAR-10数据集达到25%训练错误率是采用tanh的CNN(虚线)的六倍。每个网络的学习了都是独立选取以使其训练速度最大化,且都没有经过正则化处理。

多GPU并行训练

单个GTX580 GPU 只有3GB显存,限制了能训练的神经网络的最大规模,于是作者将神经网络搭建于两个GPU上,使他们能够直接互相读取和写入显存而不需要经过主机内存,而且作者设置了一个机制,即两个GPU只在特定的层级中互相“交流”,就是某层接受前一层所有神经元的输出,而另一层只接受搭载于同一GPU上神经元的输出结果。具体的连接模式可以通过交叉验证来调整,但这一机制使得我们可以精准地调整连接数,使得其占总计算量的比例达到我们可以接受的程度。

LRN局部响应归一化

有一种生物学的现象叫lateral inhibition,指的是相近的神经元彼此之间发生的抑制作用,即在某个神经元受刺激而产生兴奋的时候,再刺激相近的神经元,则后者所发生的兴奋对前者产生的抑制作用,LRN就是利用了这一性质,其实LRN核心思想就是用相邻的数据进行归一化,公式如下:

其中:ax,yi表示第i个卷积核,作用于位置(x,y),N表示卷积核总数目,其他参数都是超参数,通过验证集确定具体的值。 LRN使得Top1和Top5错误率分别降低了1.4%和1.2%.但是现在LRN逐渐都被Batch Normalization取代了。

Overlapping Pooling 重叠池化

在普通的池化中,池化窗口z与滑动步长s相等。而重叠池化指的就是s<z的池化,此时相邻的滑动窗口之间就会有重叠,本文使用z=3,s=2,使得Top-1和Top-5误差率分别降低0.4%和0.3%,作者也发现使用这种形式可以降低过拟合。

AlexNet网络结构

AlexNet的网络结构如下图:

如上图所示,AlexNet包含8个层,前五层为卷积层,后面两层是全连接层,最后接上一个1000-way的softmax层进行最后的分类。

模型中第二、四和五卷积层的卷积核只接收位于同一GPU上前一层的卷积核输出结果。而第三卷积层的卷积核接收前一层所有的卷积核(两个GPU)。全连接层的神经元也是接收前一层所有的输出结果。第一和第二卷积层后面各附带一个响应归一层。前面介绍的重叠池化层位于每一个响应归一层之后以及第五个卷积层之后。每一个卷积层和全连接层的输出结果都会经过ReLU非线性化处理。

AlexNet各个层的参数和结构如下: 输入层:227x227x3 C1:96x11x11x3 (卷积核个数/高/宽/深度) C2:256x5x5x48(卷积核个数/高/宽/深度) C3:384x3x3x256(卷积核个数/高/宽/深度) C4:384x3x3x192(卷积核个数/高/宽/深度) C5:256x3x3x192(卷积核个数/高/宽/深度)

  • 这里有一点需要进行说明,在论文中作者的输入是224x224x3。在数据增强部分作者有提到:作者是从原本大小256x256的图象中提取所有大小为224x224的子图像(以及他们的水平翻转图象)作为输入,但是我们可以从生成的卷积特征图倒推回去,生成的卷积特征图单元数为55x55x48x2,每个特征图尺度为55x55,由此可知输入图像尺度为227x227(55(单向尺度)x4(步长) + (11(卷积核尺度)-4(步长)) = 227)。因此输入图像单元数为227x227x3x1

代码详解

我们定义AlexNet的主结构之前,先定义一下各层。

  • 这里主要是要注意一下这里卷积的操作(笔者的电脑只有一块GPU,所以只能模拟两块GPU并行计算):我们在定义卷积核的维度的时候加上一个参数,channel/groups,如果某层需要分开卷积再合并则传groups=2参数,将w和b用split进行切分,即把各自卷积核[Kheight, Kwidth, chanel/groups, num_filter]的第四个维度分为两半,分别进行卷积计算,然后使用tf.concat进行合并,合并之后加上偏置再激活。
def maxPoolLayer(x, kHeight, kWidth, strideX, strideY, name, padding = "SAME"):
    '''max-pooling Layer'''
    return tf.nn.max_pool(x, ksize=[1, kHeight, kWidth, 1],
                          strides=[1, strideX, strideY, 1], padding=padding, name=name)

def dropout(x, keepPro, name = None):
    '''dropout'''
    return tf.nn.dropout(x, keepPro, name)

def LRN(x, R, alpha, beta, name = None, bias = 1.0):
    '''LRN'''
    return tf.nn.local_response_normalization(x, depth_radius= R, alpha = alpha,
                                              beta = beta, bias = bias, name = name)

def fcLayer(x, inputD, outputD, reluFlage, name):
    '''fully-connect Layer'''
    with tf.variable_scope(name) as scoup:
        w = tf.get_variable("w", shape = [inputD,outputD], dtype = "float")
        b = tf.get_variable("b", shape = [outputD], dtype = "float")
        out = tf.nn.xw_plus_b(x, w, b, name = scoup.name)
        if reluFlage:
            return tf.nn.relu(out)
        else:
            return out

def convLayer(x, kHeight, kWidth, strideX, strideY,featureNum, name,
              padding = "SAME", groups = 1):
    '''convolution Layer '''
    channel = int(x.get_shape()[-1])
    conv = lambda a,b: tf.nn.conv2d(a, b, strides = [1, strideX, strideY, 1], padding = padding)
    with tf.variable_scope(name) as scope :
        w = tf.get_variable("w", shape=[kHeight, kWidth, channel / groups, featureNum])
        b = tf.get_variable("b", shape = [featureNum])

        xNew = tf.split(value = x, num_or_size_splits = groups, axis = 3)
        wNew = tf.split(value = w, num_or_size_splits = groups, axis = 3)

        featureMap = [conv(t1,t2) for t1, t2 in zip(xNew, wNew)]

        mergeFeatureMap = tf.concat(axis = 3, values = featureMap)

        out = tf.nn.bias_add(mergeFeatureMap, b)
        return tf.nn.relu(tf.reshape(out, mergeFeatureMap.get_shape().as_list(), name = scope.name))

定义好每一层后,我们就可以来定义AlexNet的主结构了。

  1. 参数文件bvlc_alexnet.npy在http://www.cs.toronto.edu/~guerzhoy/tf_alexnet/这里下载
  2. skip参数用来筛选冻结的参数层,便于finetune操作
  3. 最后一层使用self.fc8为了后面主循环调用
class alexNet(object):
    '''alexNet model'''
    def __init__(self, x, keepPro, classNum, skip, modelPath = "bvlc_alexnet.npy"):
        self.X = x
        self.KEEPPRO = keepPro
        self.CLASSNUM = classNum
        self.SKIP = skip
        self.MODELPATH = modelPath
        self.bulidNet()

    def bulidNet(self):
        '''bulid model'''
        conv1 = convLayer(self.X, 11, 11, 4, 4, 96, "conv1", "VALID")
        lrn1 = LRN(conv1, 2, 2e-05, 0.75, "norm1")
        pool1 = maxPoolLayer(lrn1, 3, 3, 2, 2, "pool", "VALID")

        conv2 = convLayer(pool1, 5, 5, 1, 1, 256, "conv2", groups= 2)
        lrn2 = LRN(conv2, 2, 2e-05, 0.75, "lrn2")
        pool2 = maxPoolLayer(lrn2, 3, 3, 2, 2, "pool2", "VALID")

        conv3 = convLayer(pool2, 3, 3, 1, 1, 384, "conv3")

        conv4 = convLayer(conv3, 3, 3, 1, 1, 384, "conv4", groups = 2)

        conv5 = convLayer(conv4, 3, 3, 1, 1, 256, "conv5", groups = 2)
        pool5 = maxPoolLayer(conv5, 3, 3, 2, 2, "pool5", "VALID")


        fcIn = tf.reshape(pool5, [-1,256 * 6 * 6])
        fc1 = fcLayer(fcIn, 256 * 6 * 6, 4096, True, "fc6")
        dropout1 = dropout(fc1, self.KEEPPRO)

        fc2 = fcLayer(dropout1, 4096, 4096, True, "fc7")
        dropout2 = dropout(fc2, self.KEEPPRO)

        self.fc3 = fcLayer(dropout2, 4096, self.CLASSNUM, True, "fc8")

接着我们来定义读入参数文件的函数:

  1. 我们读入参数字典后,先循环字典的keys,将我们需要读入冻结的参数读入
  2. 通过维度的大小判断读入的是w还是b
  3. 最后用assign函数进行赋值
    def loadModel(self,sess):
        '''load model'''
        wDict = np.load(self.MODELPATH, encoding = "bytes").item()
        for name in wDict:
            if name not in self.SKIP:
                with tf.variable_scope(name, reuse = True):
                    for p in wDict[name]:
                        if len(p.shape) == 1:
                            sess.run(tf.get_variable("b", trainable = False).assign(p))
                        else:
                            sess.run(tf.get_variable("w", trainable = False).assign(p))

定义好这些后我们就可以来定义主循环了:

  1. 使用argpase.ArgumentParser()构建输入参数,参数分为图片本地途径和网络url地址。
  2. 判断图片地址来源:如果是本地文件路径,用lamda构建路径后,确定路径里确实存在文件,使用字典将读取结果进行组合;如果是url地址,用urllib.request.urlopen()打开图片路径后,将读取图片转换为二进制,再转换为uint8类型的彩色图片。
  3. 设置一些网络参数。
  4. 实例化模型后得到score得分值,通过softmax层获得概率值,传入预处理后的图片(减去均值)。
  5. 获得最大概率的索引值,再去caffe_classes文件中获取类别名,最后将图片显示出来,标题改为类别名。
'''add some argument'''
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter,
                                 description='Classify some images.',
                                 epilog= "And that's the category")
parser.add_argument('-m', '--mode', choices=['folder', 'url'], default='folder')
parser.add_argument('-p', '--path', help='Specify a path [e.g. testModel]', default = 'testModel')
args = parser.parse_args(sys.argv[1:])

if args.mode == 'folder':
    '''get image from folder'''
    withPath = lambda f: '{}/{}'.format(args.path,f)
    testImg = dict((f,cv2.imread(withPath(f))) for f in os.listdir(args.path) if os.path.isfile(withPath(f)))
elif args.mode == 'url':
    def url2img(url):
        '''get image from url'''
        resp = urllib.request.urlopen(url)
        image = np.asarray(bytearray(resp.read()), dtype="uint8")
        image = cv2.imdecode(image, cv2.IMREAD_COLOR)
        return image
    testImg = {args.path:url2img(args.path)}

if testImg.values():
    #define some alexNet's params
    dropoutPro = 1
    classNum = 1000
    skip = []

    imgMean = np.array([104, 117, 124], np.float)
    x = tf.placeholder("float", [1, 227, 227, 3])

    model = alexnet.alexNet(x, dropoutPro, classNum, skip)
    score = model.fc3
    softmax = tf.nn.softmax(score)

    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        model.loadModel(sess)

        for key,img in testImg.items():
            '''img preprocess'''
            resized = cv2.resize(img.astype(np.float), (227, 227)) - imgMean
            maxx = np.argmax(sess.run(softmax, feed_dict = {x: resized.reshape((1, 227, 227, 3))}))
            res = caffe_classes.class_names[maxx]

            print("{}: {}\n----".format(key,res))
            cv2.imshow("{}".format(res), img)
            cv2.waitKey(0)

我们在终端输入python3 test.py -m folder -p testModel,就可以看到结果(espresso:浓缩咖啡):

paper:http://papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks.pdf

完整代码已上传到github:https://github.com/cristianoc20/AlexNet

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-10-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 计算机视觉漫谈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目录
  • 论文概述
  • ReLU激活函数
  • 多GPU并行训练
  • LRN局部响应归一化
  • Overlapping Pooling 重叠池化
  • AlexNet网络结构
    • 代码详解
    相关产品与服务
    GPU 云服务器
    GPU 云服务器(Cloud GPU Service,GPU)是提供 GPU 算力的弹性计算服务,具有超强的并行计算能力,作为 IaaS 层的尖兵利器,服务于深度学习训练、科学计算、图形图像处理、视频编解码等场景。腾讯云随时提供触手可得的算力,有效缓解您的计算压力,提升业务效率与竞争力。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档