Paper AlexNet 详解
AlexNet (ImageNet Classification with Deep Convolutional Neural Networks, Alex Krizhevsky, Ilya Sutskever, Geoffrey E. Hinton) 是 2012 年由 Hinton 老爷子和其学生 Alex 等提出的第 1 个大型卷积神经网络,因其在图像识别竞赛 ILSVRC-2012 上的成绩大幅度领先第 2 名,让人们认识到了神经网络的超优异性能,引发了深度学习研究和应用的新热潮,本文详细的来解读一下这篇经典之作。
首先简单的交代一下背景。在此之前,深度学习(神经网络)的发展处于低潮,而传统机器学习算法,如支持向量机、随机森林和多层感知器(层数较少的前馈神经网络)等,则一片欣欣向荣。对于简单的识别任务,这些传统方法已经可以取得非常好的效果,比如,在 MNIST 数据集上的错误率已经低于 0.3%,甚至超过了人类。但是,现实世界的物体是变化无常的,要准确识别出它们就需要更大的训练数据集。
基于这种认识,2009 年李飞飞等人推出了著名的数据集 ImageNet,该数据集有超过 1500 万的高分辨率图片,以及近乎 22000 个标注类别。从 2010 开始,从 ImageNet 数据集里挑出了 1000 个类别,每个类别差不多 1000 张图片,组成含有大概 120 万张图片的训练集, 来进行大规模的视觉挑战赛(ILSVRC)。
要从百万级别的图片里面学习目标,就需要设计超强学习容量的模型。但尽管有像 ImageNet 这么大的数据集,如果没有大量的先验知识,也是不能解决像目标识别这种具有极大复杂度的任务的。卷积神经网络是参考了先验知识而专门设计的一类模型,它们对图像的本质作了很强而且很大程度上是正确的假设:统计的稳定性及局部像素的依赖性(即卷积网络的参数共享及局部感受野)。它们的容量可以通过调节网络的宽度和深度来控制,而且相比于前馈神经网络具有更少的连接和参数,在理论上不会大幅度变差的情况下更容易训练。
结合大数据集以及 GPU 的使用,作者们提出了一个具有 5 个卷积层、3 个全连接层的大型卷积神经网络,即现在简写成的网络:AlexNet。
一、网络结构
如下图所示,网络有 8 个可训练层:
AlexNet 网络结构前 5 个是卷积层,后 3 个为全连接层。第 1 个卷积层接收分辨率为 224x224 的 3 通道图像的输入,卷积核大小为 11x11,卷积步幅为 4, 输出通道数为 96,输出特征映射的分辨率变为 54x54 (54 = [(224 - 11) / 4] + 1)。因为当时的 GPU 显存比较小,需要把这些输出放在两个不同的 GPU 上,因此网络分成了两条分支。第 2 个卷积层的卷积核大小为 5x5,输出通道数为 256(每个分支 128),执行卷积时上下左右各填充 2 个像素,因此输出分辨率保持不变,在做了整流线性单元 (ReLU)的激活之后,这里需要引入一个额外的标准化过程,即对激活函数的输出做局部响应标准化,然后做步幅为 2 的最大池化,将特征映射的分辨率减小一半到 27x27。余下的卷积层,卷积核大小都为 3x3,步幅为 1,填充大小为 1(上下左右各 1 个像素)。然后接 2 个结点数为 4096 的全连接层(这两个全连接都有 0.5 的 dropout 操作),最后输出 1000 个分类的概率。除了最后的输出层的激活函数为 softmax 之外,其它层的激活函数都是整流线性单元。另外,作为本文的一个技巧,网络中的所有池化层的步幅为 2(因此分辨率下降一半),但池化核大小为 3(因此各结点的池化区域会重叠)。
AlexNet 的网络结构虽然现在看起来很简单,但在当时能训练成功且取得超高的准确率却不是一件容易的事情,根据当时的算力,在 ImageNet 上它训练了 6 天。除了模型本身的多 GPU 串联结构之外,为了加速网络的收敛以及避免过拟合,也采取了如下的额外策略:
1. 整流线性单元
在此之前的网络采用的激活函数大都是双曲正切(tanh)函数或者 S 型(Sigmoid)函数。这两个函数在变量趋于正负无穷大时都收敛到有界常数,这种函数值饱和的情况在进行梯度下降训练时,会使得网络的收敛速度特别慢(因为函数饱和时的梯度接近 0)。而使用整流线性单元作为激活函数则会成倍的减少训练时间(只要某个样本在通过 ReLU 时为正,训练就一直可以进行),如下图所示,
整流线性单元作为激活函数能大幅度加速收敛作者以数据集 CIFAR-10 为例,实验表明:让一个 4 层卷积网络达到 25% 的错误率,使用整流线性单元比双曲正切函数作为激活函数时收敛速度要快 6 倍。
2. 多 GPU 训练
受限于当时的 GPU(GTX 580)只有 3GB 的显存,要在 120 万的训练数据上训练一个大型的网络是不可能的,因此作者们采用了多 GPU 并行的策略,这使得 AlexNet 的网络结构看起来比较怪异(实际上,如果现在要实现这个网络是不需要分叉为两个分支的)。双分支的网络,相比于在单个 GPU 上训练一个卷积层的通道数都减半的相同结构的网络来说,top-1 和 top-5 错误率分别下降了 1.7% 和 1.2%。
3. 局部响应标准化
整流线性单元的使用已经不需要对输入做额外的标准化来避免饱和化了,但作者们实验发现在经过激活函数之后,对输出再做如下的局部响应标准化(只作用于网络的第 2 个卷积层),可以进一步的提升泛化性能。记 为第 i 个通道上 (x, y) 位置处的激活值(即作用过整流线性单元之后的输出值),则它的响应标准化激活为:
局部响应标准化公式其中和式对所有 i 临近的通道 j 求和,N 为通道数。超参数根据验证集取值,论文中为:
因为是用临近通道的值来调整当前通道的值,可以看成是对输出做了某种亮度标准化。响应标准化减小的 top-1 和 top-5 错误率分别是 1.4% 和 1.2%。它的有效性也在 CIFAR-10 数据集上得到了验证,使用一个 4 层的网络,使用响应标准化可以将错误率从 13% 下降到 11%。
4. 重叠区域池化
具体的来说,池化操作是对每一个通道,每隔 s 步,将 zxz 领域内的所有像素综合(取平均或最大等运算),因此输出的分辨率会下降 s 倍。传统池化的时候,一般都会令 s=z,因此各个神经元的池化区域是不重叠的。论文的 AlexNet 中,取 s=2, z=3,因此池化区域会相互重叠。相比于不重叠的情况 s=2, z=2,重叠的设置可以将 top-1 和 top-5 错误率分别下降 0.4% 和 0.3%。
以上几个技巧,除了整流线性单元成为了卷积神经网络的标配之外,其它都基本被淘汰了。因为 GPU 显存不断增大,当前多 GPU 训练时,一般不再是将模型并行(即将单个模型拆分成几个部分,每个部分单独放在各个 GPU 上),而是将数据并行(模型共用,将一个批量的数据拆分成几个小批量,每个小批量数据放在各个 GPU 上)。局部响应标准化一般也被万能的批标准化替代了。而池化操作一般都是采用步长为 2 的 2x2 领域,其它模式几乎凤毛麟角。值得一提的是,网络第一个卷积层卷积核高达 11 的情况在后续的卷积网络中也越来越少见。
二、训练技巧
AlexNet 有将近 6000 万的参数,尽管训练数据集有 120 万张图像,但这仍不足以让网络不出现显著的过拟合,因此仍然需要对抗过拟合的措施。
1. 数据增强
最简单和最常见的减小过拟合的方法是人为的对数据做保持标签不变的变换。论文中做了两个不同形式的数据增强操作。第 1 个是随机裁剪和随机水平翻转。ImageNet 中的图像都具有不同的分辨率,因此首先需要将每张图像按照保持宽高比不变的方式将短边缩放到 256,然后对长边进行居中的裁剪出 256 个像素,得到一张 256x256 的固定大小的图像。此时,再随机的从中裁剪出 224x224 大小的图像,再以 0.5 的概率随机的水平翻转,就得到网路的输入(实际上,输入网络之前需要减去数据集的平均像素值)。这个操作将数据量扩大了 32 x 32 x 2 = 2048 倍。如果没有这个增强操作,网络将遭遇严重的过拟合,这就会迫使作者们设计更小的网络。测试的时候,从 256x256 的图像的上下左右四个角和中间裁剪出 5 个大小为 224x224 大小的图像,加上图像的水平翻转,就得到 10 个预测输出,最后对所有的 softmax 输出取平均即得到网络最后的预测。第 2 个数据增强操作是改变训练图像的 RGB 通道的强度。因为这个操作在后续的研究中都没有被采用,因此略过,感兴趣的读者可以自行阅读原文。
2. Dropout
模型集成是一个减小测试错误率的非常成功的方法,但训练多个不同的大模型(且每个模型都要耗时几天)也是一件代价十分昂贵的事情。一个代价很小且非常高效的技巧是 dropout,它对隐藏结点以 0.5 的概率来决定是否对该结点的输出置零。如果置零,那么当前结点被丢弃,对网络的前向传播没有任何贡献,因而也不参与网络的反向传播。因此,每次一个输入来的时候,网络都采样出不同的结构(即丢弃掉不同的结点),但所有这些结构都共享相同的参数。这个技巧减小了神经元之间复杂的相互作用,因为每个神经元是否出现(即不被置零)不依赖其它任何神经元。同时也会迫使网络学会更鲁棒的特征,这些特征对神经元的不用的随机子集都有用。测试的时候,对每个神经元的输出都乘以 0.5,这是一个由指数个 dropout 网络的预测输出的几何平均的一种合理的近似。Dropout 操作只限于前两个全连接层使用,虽然它使得网络收敛的迭代步数翻倍,但却极大的抑制了过拟合。
AlexNet 在 ILSVRC 上的预测结果为:
ILSVRC-2010 数据集上的结果对比 ILSVRC-2012 数据集上的结果对比显然,对比其它的模型,测试错误率都显著的降低了。特别是在 ILSVRC-2012 竞赛上以低于第 2 名 10.9% 的错误率夺冠,彻底引爆了深度学习的热度,让人工智能的概念深入千家万户。
AlexNet 如此高的准确率是因为抓住了图像的本质特征了吗?实验发现,网络确实在某种程度上能掌握到各个类别的本质特征,至少网络能够比较好的分辨出各个类别的特征区别。如下图右侧所示,
feature-vector.png右侧第 1 列是 5 张测试集中的图片,每张图像通过 AlexNet 之后到达第 2 个全连接层得到一个特征向量,通过这个特征向量在测试集中进行检索,选出与特征向量之间的欧式距离最近的 5 张图像,结果显示这些图像都来自同一类别,而且同类图像之间姿态样式各异,说明网络并不是仅仅记住了图像之间像素级别的相似性,而是提取出了更深层次的图像特征。这为神经网络的迁移学习提供了依据,能做特征迁移是神经网络大放异彩的一个重要原因。
三、实现
AlexNet 掀起了神经网络新的发展热潮,从此之后,性能优异的网络如雨后春笋般涌现,曾经的经典之作已逐渐无用武之地。当前主流的 AlexNet 的实现相比原文已经做了多处改动,以 TensorFlow 官方开源的 AlexNet 实现为例:
# Copyright 2016 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Contains a model definition for AlexNet.
This work was first described in:
ImageNet Classification with Deep Convolutional Neural Networks
Alex Krizhevsky, Ilya Sutskever and Geoffrey E. Hinton
and later refined in:
One weird trick for parallelizing convolutional neural networks
Alex Krizhevsky, 2014
Here we provide the implementation proposed in "One weird trick" and not
"ImageNet Classification", as per the paper, the LRN layers have been removed.
Usage:
with slim.arg_scope(alexnet.alexnet_v2_arg_scope()):
outputs, end_points = alexnet.alexnet_v2(inputs)
@@alexnet_v2
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import tensorflow as tf
from tensorflow.contrib import slim as contrib_slim
slim = contrib_slim
# pylint: disable=g-long-lambda
trunc_normal = lambda stddev: tf.compat.v1.truncated_normal_initializer(
0.0, stddev)
def alexnet_v2_arg_scope(weight_decay=0.0005):
with slim.arg_scope([slim.conv2d, slim.fully_connected],
activation_fn=tf.nn.relu,
biases_initializer=tf.compat.v1.constant_initializer(0.1),
weights_regularizer=slim.l2_regularizer(weight_decay)):
with slim.arg_scope([slim.conv2d], padding='SAME'):
with slim.arg_scope([slim.max_pool2d], padding='VALID') as arg_sc:
return arg_sc
def alexnet_v2(inputs,
num_classes=1000,
is_training=True,
dropout_keep_prob=0.5,
spatial_squeeze=True,
scope='alexnet_v2',
global_pool=False):
"""AlexNet version 2.
Described in: http://arxiv.org/pdf/1404.5997v2.pdf
Parameters from:
github.com/akrizhevsky/cuda-convnet2/blob/master/layers/
layers-imagenet-1gpu.cfg
Note: All the fully_connected layers have been transformed to conv2d layers.
To use in classification mode, resize input to 224x224 or set
global_pool=True. To use in fully convolutional mode, set
spatial_squeeze to false.
The LRN layers have been removed and change the initializers from
random_normal_initializer to xavier_initializer.
Args:
inputs: a tensor of size [batch_size, height, width, channels].
num_classes: the number of predicted classes. If 0 or None, the logits layer
is omitted and the input features to the logits layer are returned instead.
is_training: whether or not the model is being trained.
dropout_keep_prob: the probability that activations are kept in the dropout
layers during training.
spatial_squeeze: whether or not should squeeze the spatial dimensions of the
logits. Useful to remove unnecessary dimensions for classification.
scope: Optional scope for the variables.
global_pool: Optional boolean flag. If True, the input to the classification
layer is avgpooled to size 1x1, for any input size. (This is not part
of the original AlexNet.)
Returns:
net: the output of the logits layer (if num_classes is a non-zero integer),
or the non-dropped-out input to the logits layer (if num_classes is 0
or None).
end_points: a dict of tensors with intermediate activations.
"""
with tf.compat.v1.variable_scope(scope, 'alexnet_v2', [inputs]) as sc:
end_points_collection = sc.original_name_scope + '_end_points'
# Collect outputs for conv2d, fully_connected and max_pool2d.
with slim.arg_scope([slim.conv2d, slim.fully_connected, slim.max_pool2d],
outputs_collections=[end_points_collection]):
net = slim.conv2d(inputs, 64, [11, 11], 4, padding='VALID',
scope='conv1')
net = slim.max_pool2d(net, [3, 3], 2, scope='pool1')
net = slim.conv2d(net, 192, [5, 5], scope='conv2')
net = slim.max_pool2d(net, [3, 3], 2, scope='pool2')
net = slim.conv2d(net, 384, [3, 3], scope='conv3')
net = slim.conv2d(net, 384, [3, 3], scope='conv4')
net = slim.conv2d(net, 256, [3, 3], scope='conv5')
net = slim.max_pool2d(net, [3, 3], 2, scope='pool5')
# Use conv2d instead of fully_connected layers.
with slim.arg_scope(
[slim.conv2d],
weights_initializer=trunc_normal(0.005),
biases_initializer=tf.compat.v1.constant_initializer(0.1)):
net = slim.conv2d(net, 4096, [5, 5], padding='VALID',
scope='fc6')
net = slim.dropout(net, dropout_keep_prob, is_training=is_training,
scope='dropout6')
net = slim.conv2d(net, 4096, [1, 1], scope='fc7')
# Convert end_points_collection into a end_point dict.
end_points = slim.utils.convert_collection_to_dict(
end_points_collection)
if global_pool:
net = tf.reduce_mean(
input_tensor=net, axis=[1, 2], keepdims=True, name='global_pool')
end_points['global_pool'] = net
if num_classes:
net = slim.dropout(net, dropout_keep_prob, is_training=is_training,
scope='dropout7')
net = slim.conv2d(
net,
num_classes, [1, 1],
activation_fn=None,
normalizer_fn=None,
biases_initializer=tf.compat.v1.zeros_initializer(),
scope='fc8')
if spatial_squeeze:
net = tf.squeeze(net, [1, 2], name='fc8/squeezed')
end_points[sc.name + '/fc8'] = net
return net, end_points
alexnet_v2.default_image_size = 224
首先去掉了模型并行的双分支结构,其次抛弃了局部响应标准化的操作,最后网络的第 2 个卷积层的通道数改成了 64 而不是原文的 96,而且整个网络中的全连接层都被卷积层替代,成为了全卷积网络。
AlexNet 的深入阅读还可以参考这篇文章 One weird trick for parallelizing convolutional neural networks, Alex Krizhevsky, 2014。
广告时间,手机阅读可关注公众号: