1、引言
卷积神经网络是当前最热门的技术,我想深入地学习这门技术,从他的发展历史开始,了解神经网络算法的兴衰起伏;同时了解他在发展过程中的**“里程碑式算法”**,能更好的把握神经网络发展的未来趋势,了解神经网络的特征。
之前的LeNet为以后的神经网络模型打下了一个基础的框架,真正让神经网络模型在外界广泛引起关注的还是AlexNet。
AlexNet文章地址
2、AlexNet提出背景
在LeNet之后,随着计算能力的提升,研究者不断改进模型的表达能力,最明显的是卷积层数的增加,每一层的内部通道数目也在增加。其中最具代表性的就是AlexNet模型。
AlexNet由Hinton和他的学生Alex Krizhevsky设计,模型名字来源于论文第一作者的姓名Alex。该模型以很大的优势获得了2012年ISLVRC竞赛的冠军网络,分类准确率由传统的 70%+提升到 80%+,自那年之后,深度学习开始迅速发展。
注:
ImageNet是一个在2009年创建的图像数据集,从2010年开始到2017年举办了七届的ImageNet 挑战赛——ImageNet Large Scale Visual Recognition ChallengeI (LSVRC),在这个挑战赛上诞生了AlexNet、ZFNet、OverFeat、VGG、Inception、ResNet、WideResNet、FractalNet、DenseNet、ResNeXt、DPN、SENet 等经典模型。
3、AlexNet的模型详解
从上图看,在网络设计上其实并非如上图所示,上图包含了GPU通信的部分。这是由当时GPU受限制引起的,作者使用两块GPU进行计算,因此分为上下两部分。但是以目前GPU的处理能力,单GPU足够了,因此其结构图可以如下所示:
从上述结构图才逐层分析LeNet的网络结构
- INPUT(输入层)
输入图像的尺寸为3×224×224,是三通道的RGB彩色图像。从输入层看比之前的LeNet147倍,可见输入上就复杂了许多。
- C1(卷积层)
C1的基本结构为:卷积–>ReLU–>池化
- 1.卷积:输入是(227 × 227× 3),96个大小为( 11× 11× 3)卷积核,padding=0,stride=4,所以经过变化(227-11+2× 0+4)/4=55,所以最终的输出为(55× 55× 96)。
- 2.激活函数:ReLU
- 3.池化:输入是(55× 55× 96),池化核大小为(3× 3),padding=0,stride=2,所以经过变化(55-3+2× 0+2)/2=27,所以最终的输出为(27×27× 96)。
- C2(卷积层)
-
1.卷积:输入是(27 × 27 × 96),256个大小为( 5 × 5 × 96)卷积核,padding=2,stride=1,所以经过变化(27-5+2 × 2+1)/1=27,所以最终的输出为(27 × 27× 256)。
-
2.激活函数:ReLU
-
3.池化:输入是(27 × 27 × 256),池化核大小为(3 × 3),padding=0,stride=2,所以经过变化(27-3+2 × 0+2)/2=13,所以最终的输出为(13 ×13 × 256)。
- C3(卷积层)
C3的基本结构为:卷积–>ReLU–>池化,注意一点:此层没有进行MaxPooling操作。
- 1.卷积:输入是(13 × 13 × 256),384个大小为( 3 × 3× 256)卷积核,padding=1,stride=1,所以经过变化(13-3+2 × 1+1)/1=13,所以最终的输出为(13 ×
13 × 384)。 - 2.激活函数:ReLU。
- C4(卷积层)
C4的基本结构为:卷积–>ReLU–>池化,注意一点:此层没有进行MaxPooling操作。
- 1.卷积:输入是(13 × 13 × 384),384个大小为( 3 × 3 × 384)卷积核,padding=1,stride=1,所以经过变化(13-3+2 × 1+1)/1=13,所以最终的输出为(13 ×
13 × 384)。(此处未将输出分到两个GPU中,若按照论文将分成两组,每组为13×13×192) - 2.激活函数:ReLU
- C5(卷积层)
C5的基本结构为:卷积–>ReLU–>池化,。
- 1.卷积:输入是(13 ×13 × 384),256个大小为( 3 × 3 × 834)卷积核,padding=1,stride=1,所以经过变化(13-3+2 × 1+1)/1=13,所以最终的输出为(13 × 13 × 256)。
- 2.激活函数:ReLU
- 3.池化:输入是(13 × 13 × 256),池化核大小为(3 × 3),padding=0,stride=2,所以经过变化(13-3+2 × 0+2)/2=6所以最终的输出为(6 × 6 × 256)。(此处未将输出分到两个GPU中,若按照论文将分成两组,每组为6×6×128)
- FC6(全连接层)
FC6的基本结构为:全连接–>ReLU–>Dropout
- 1.全连接:此层的全连接实际上是通过卷积进行的,输入(6 × 6 × 256),4096个大小为( 6 × 6 × 256)卷积核,padding=0,stride=1,所以经过变化(6-6-2 × 0+1)/1=1,所以最终的输出为(1 × 1× 4096)。
- 2.激活函数:ReLU
- 3.Dropout:全连接层中去掉了一些神经节点,达到防止过拟合,FC6输出为1×1×4096。
- FC7(全连接层)
FC7的基本结构为:全连接–>ReLU–>Dropout
- 1.全连接:输入(1 × 1× 4096)。
- 2.激活函数:ReLU
- 3.Dropout:全连接层中去掉了一些神经节点,达到防止过拟合,FC7输出为1×1×4096。
- FC8(全连接层)
FC8的基本结构为:全连接–>softmax
- 1.全连接:输入(1× 1× 4096)。
- 2.softmax:softmax为1000,FC8输出为1×1×1000;
在整个过程中,并没有将C1与C2中的Local Response Normalization(局部响应归一化)操作添加在其中,此操作就是将ReLU得到的结果进行归一化,读者可以查看一下原论文
最终,AlexNet的总结如下:
4、AlexNet的创新之处
4.1使用ReLU激活函数
传统的神经网络普遍使用 Sigmoid 或者 tanh 等非线性函数作为激励函数,然而它们容易出现梯度弥散或梯度饱和的情况。以 Sigmoid 函数为例,当输入的值非常大或者非常小的时候,这些神经元的梯度接近于 0(梯度饱和现象),如果输入的初始值很大的话,梯度在反向传播时因为需要乘上一个 Sigmoid 导数,会造成梯度越来越小,导致网络变的很难学习。(详见本公博客的文章:深度学习中常用的激励函数)。
在 AlexNet 中,使用了 ReLU (Rectified Linear Units)激励函数,该函数的公式为:f (x)=max (0,x),当输入信号 < 0 时,输出都是 0,当输入信号 > 0 时,输出等于输入,如下图所示:
使用 ReLU 替代 Sigmoid/tanh,由于 ReLU 是线性的,且导数始终为 1,计算量大大减少,收敛速度会比 Sigmoid/tanh 快很多,如下图所示:
4.2数据扩充
有一种观点认为神经网络是靠数据喂出来的,如果能够增加训练数据,提供海量数据进行训练,则能够有效提升算法的准确率,因为这样可以避免过拟合,从而可以进一步增大、加深网络结构。而当训练数据有限时,可以通过一些变换从已有的训练数据集中生成一些新的数据,以快速地扩充训练数据。
其中,最简单、通用的图像数据变形的方式:水平翻转图像,从原始图像中随机裁剪、平移变换,颜色、光照变换,如下图所示:
AlexNet 在训练时,在数据扩充(data augmentation)这样处理:
(1)随机裁剪,对 256×256 的图片进行随机裁剪到 224×224,然后进行水平翻转,相当于将样本数量增加了((256-224)^2)×2=2048 倍;
(2)测试的时候,对左上、右上、左下、右下、中间分别做了 5 次裁剪,然后翻转,共 10 个裁剪,之后对结果求平均。作者说,如果不做随机裁剪,大网络基本上都过拟合;
(3)对 RGB 空间做 PCA(主成分分析),然后对主成分做一个(0, 0.1)的高斯扰动,也就是对颜色、光照作变换,结果使错误率又下降了 1%。
4.3重叠池化
一般的池化(Pooling)是不重叠的,池化区域的窗口大小与步长相同,如下图所示:
在 AlexNet 中使用的池化(Pooling)却是可重叠的,也就是说,在池化的时候,每次移动的步长小于池化的窗口长度。AlexNet 池化的大小为 3×3 的正方形,每次池化移动步长为 2,这样就会出现重叠。重叠池化可以避免过拟合,这个策略贡献了 0.3% 的 Top-5 错误率。
4.4局部归一化
在神经生物学有一个概念叫做 “侧抑制”(lateral inhibitio),指的是被激活的神经元抑制相邻神经元。归一化(normalization)的目的是 “抑制”,局部归一化就是借鉴了 “侧抑制” 的思想来实现局部抑制,尤其当使用 ReLU 时这种 “侧抑制” 很管用,因为 ReLU 的响应结果是无界的(可以非常大),所以需要归一化。使用局部归一化的方案有助于增加泛化能力。
LRN 的公式如下,核心思想就是利用临近的数据做归一化,这个策略贡献了 1.2% 的 Top-5 错误率。
4.5Dropout
引入 Dropout 主要是为了防止过拟合。在神经网络中 Dropout 通过修改神经网络本身结构来实现,对于某一层的神经元,通过定义的概率将神经元置为 0,这个神经元就不参与前向和后向传播,就如同在网络中被删除了一样,同时保持输入层与输出层神经元的个数不变,然后按照神经网络的学习方法进行参数更新。在下一次迭代中,又重新随机删除一些神经元(置为 0),直至训练结束。
Dropout 应该算是 AlexNet 中一个很大的创新,以至于 “神经网络之父” Hinton 在后来很长一段时间里的演讲中都拿 Dropout 说事。Dropout 也可以看成是一种模型组合,每次生成的网络结构都不一样,通过组合多个模型的方式能够有效地减少过拟合,Dropout 只需要两倍的训练时间即可实现模型组合(类似取平均)的效果,非常高效。
如下图所示:
4.6多GPU训练
AlexNet 当时使用了 GTX580 的 GPU 进行训练,由于单个 GTX 580 GPU 只有 3GB 内存,这限制了在其上训练的网络的最大规模,因此他们在每个 GPU 中放置一半核(或神经元),将网络分布在两个 GPU 上进行并行计算,大大加快了 AlexNet 的训练速度。
5、AlexNet的代码实现
from keras.datasets import mnist
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn.metrics import confusion_matrix
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Input, Dropout, BatchNormalization
from keras.models import Model, Sequential
from tensorflow.python.keras.utils.np_utils import to_categorical
# from keras.utils import np_utils
"""
数据集获取
"""
def get_mnist_data():
(x_train_original, y_train_original), (x_test_original, y_test_original) = mnist.load_data()
# 从训练集中分配验证集
x_val = x_train_original[50000:]
y_val = y_train_original[50000:]
x_train = x_train_original[:50000]
y_train = y_train_original[:50000]
# 将图像转换为四维矩阵(nums,rows,cols,channels), 这里把数据从unint类型转化为float32类型, 提高训练精度。
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1).astype('float32')
x_val = x_val.reshape(x_val.shape[0], 28, 28, 1).astype('float32')
x_test = x_test_original.reshape(x_test_original.shape[0], 28, 28, 1).astype('float32')
# 原始图像的像素灰度值为0-255,为了提高模型的训练精度,通常将数值归一化映射到0-1。
x_train = x_train / 255
x_val = x_val / 255
x_test = x_test / 255
# 图像标签一共有10个类别即0-9,这里将其转化为独热编码(One-hot)向量
y_train = to_categorical(y_train)
y_val = to_categorical(y_val)
y_test = to_categorical(y_test_original)
return x_train, y_train, x_val, y_val, x_test, y_test
"""
定义alexnet网络模型
"""
def alexnet():
model = Sequential()
model.add(Conv2D(96, (11, 11), strides=(4, 4), input_shape=(28, 28, 1), padding='same', activation='relu',
kernel_initializer='uniform'))
model.add(MaxPooling2D(pool_size=(3, 3), strides=(2, 2)))
model.add(BatchNormalization())
model.add(Conv2D(256, (5, 5), strides=(1, 1), padding='same', activation='relu', kernel_initializer='uniform'))
model.add(MaxPooling2D(pool_size=(3, 3), strides=(2, 2)))
model.add(BatchNormalization())
model.add(Conv2D(384, (3, 3), strides=(1, 1), padding='same', activation='relu', kernel_initializer='uniform'))
model.add(Conv2D(384, (3, 3), strides=(1, 1), padding='same', activation='relu', kernel_initializer='uniform'))
model.add(Conv2D(256, (3, 3), strides=(1, 1), padding='same', activation='relu', kernel_initializer='uniform'))
model.add(Flatten())
model.add(Dense(4096, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(4096, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(10, activation='softmax'))
print(model.summary())
return model
"""
编译网络并训练
"""
x_train, y_train, x_val, y_val, x_test, y_test = get_mnist_data()
model = alexnet()
# 编译网络(定义损失函数、优化器、评估指标)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
# 开始网络训练(定义训练数据与验证数据、定义训练代数,定义训练批大小)
train_history = model.fit(x_train, y_train, validation_data=(x_val, y_val), epochs=10, batch_size=32, verbose=2)
# 模型保存
model.save('alexnet_mnist.h5')
# 定义训练过程可视化函数(训练集损失、验证集损失、训练集精度、验证集精度)
def show_train_history(train_history, train, validation):
plt.plot(train_history.history[train])
plt.plot(train_history.history[validation])
plt.title('Train History')
plt.ylabel(train)
plt.xlabel('Epoch')
plt.legend(['train', 'validation'], loc='best')
plt.show()
show_train_history(train_history, 'accuracy', 'val_accuracy')
show_train_history(train_history, 'loss', 'val_loss')
# 输出网络在测试集上的损失与精度
score = model.evaluate(x_test, y_test)
print('Test loss:', score[0])
print('Test accuracy:', score[1])
# 测试集结果预测
predictions = model.predict(x_test)
predictions = np.argmax(predictions, axis=1)
print('前20张图片预测结果:', predictions[:20])
# 预测结果图像可视化
(x_train_original, y_train_original), (x_test_original, y_test_original) = mnist.load_data()
def mnist_visualize_multiple_predict(start, end, length, width):
for i in range(start, end):
plt.subplot(length, width, 1 + i)
plt.imshow(x_test_original[i], cmap=plt.get_cmap('gray'))
title_true = 'true=' + str(y_test_original[i])
# title_prediction = ',' + 'prediction' + str(model.predict_classes(np.expand_dims(x_test[i], axis=0)))
title_prediction = ',' + 'prediction' + str(predictions[i])
title = title_true + title_prediction
plt.title(title)
plt.xticks([])
plt.yticks([])
plt.show()
mnist_visualize_multiple_predict(start=0, end=9, length=3, width=3)
# 混淆矩阵
cm = confusion_matrix(y_test_original, predictions)
cm = pd.DataFrame(cm)
class_names = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
def plot_confusion_matrix(cm):
plt.figure(figsize=(10, 10))
sns.heatmap(cm, cmap='Oranges', linecolor='black', linewidth=1, annot=True, fmt='', xticklabels=class_names, yticklabels=class_names)
plt.xlabel("Predicted")
plt.ylabel("Actual")
plt.title("Confusion Matrix")
plt.show()
plot_confusion_matrix(cm)
6、总结
AlexNet的横空出世使得CNN名声大噪,他的出现有很多前提条件,首先计算机的计算能力得到了长足的提高,所以输入不再是简单的灰度图像,而是100多倍的RGB图像;在运算时不再是简单的CPU,而是2个GPU并行运算;在处理图像时,他对数据图像做了增强;在网络结构上在LeNet的基础上,增加了网络的层数,提升了有效参数的数目,使得网络的功能更加强大;然后AlexNet也做出了有效的创新,激活函数使用ReLu,提升了函数运行效率,用最大池化替代平均池化;步长小于池化核的尺寸,提升了特征的丰富性。提出了RLN层,增强了模型的泛化能力,同时在模型中加入了2层全连接层,每个全连接层都使用了dropout,避免了过拟合。从后来的视角看,这些都是开创性的创新,他让神经网络算法再度伟大。