深度学习框架之Keras感知:快速搭建各种经典卷积神经网络(LeNet、AlexNet、VGG16)玩转手写数字识别

1. 写在前面

如果是刚入深度学习的新手小白,可能有着只学习了一点深度学习的理论,也见识到了各种神经网络的强大而不能立马实现的烦恼,想学习TensorFlow,pytorch等出色强大的深度学习框架,又看到那代码晦涩难懂而有些想知难而退,这时候,我觉得有必要掌握一下Keras了,这是个啥? Keras是高级神经网络API,因为Keras短小精悍,非常适合快速原型制作和神经网络的搭建。在很短的时间内,就能够建立一个模型,以实现出色的结果,让神经网络的搭建像积木一样简单,更重要的一点学习深度学习网络,能快速实现,有满满的成就感,不仅可以增加对知识的理解,更可以给自己提供源源不断的学习动力。

基于这些,想把自己学习Keras的经历整理一下,因为我也是小白学起,正好边学边整理。 最近又正好看了《将夜》,发现昊天世界的修炼等级名称比较有趣(初识,感知,不惑,洞玄,知命),所以为了增加趣味性,把Keras学习系列的名字和修炼级别关联起来了,因为我们学习本身就是一场修行。

深度学习框架经过更新很快,TensorFlow,Pytorch等传播盛行,但短小精悍的Keras在未来仍会占据一席之地,并长期占据下去

如果学习了深度学习框架之Keras初识:像搭积木般的玩转神经网络,那么恭喜你进入了感知境, 学习了知识当然是用, 在这一个境界,我们首先拿号称深度学习界的Hello World级别的手写数字识别来练练手, 看看Keras是如何快速的搭建卷积神经网络完成这个任务的。在这个任务中,会认识到3个经典的网络架构,并且能实现它们。

知识大纲:

  • 先用Keras搭建LeNet5网络进行手写数字识别的热身
  • 用AlexNet进行手写数字识别
  • 使用VGG16 fine-tuning实现手写数字识别

OK, Let’s go!

2. Keras搭建LeNet5网络进行手写数字识别的热身

首先,介绍一下LeNet5网络,LeNet 提出于 1986 年,是最早用于数字识别的 CNN 网络,输入尺寸是 32 * 32。它输入的是灰度的图像,整个的网络结构样子如下(实际实现改了点地方):
在这里插入图片描述

输入层→C1 卷积层→S2 池化层→C3 卷积层→S4 池化层→C5 卷积层→F6 全连接层→Output 全连接层,对应的 Output 输出类别数为 10。

我们下面开始操作。

2.1 导入包和数据集

"""导入包"""
import keras
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Flatten, Dense
from keras.datasets import mnist

"""导入包"""
"""导入数据集"""

# 使用程序下载数据集,有时候太慢,并且可能网络发生错误
#(train_x, train_y), (test_x, test_y) = mnist.load_data()

# 所有直接去网站下载下来https://s3.amazonaws.com/img-datasets/mnist.npz
path = './dataset/mnist.npz'
f = np.load(path)
train_x, train_y = f['x_train'], f['y_train']
test_x, test_y = f['x_test'], f['y_test']
f.close()

# 查看一下形状:
print(train_x.shape)
print(train_y.shape)
print(test_x.shape)
print(test_y.shape)

# 结果:
(60000, 28, 28)
(60000,)
(10000, 28, 28)
(10000,)

2.2 数据预处理一下

我们需要把train_x, test_x进行维度变化一下, 加上通道个数, 然后归一化数据, 另外对于train_y和test_y我们需要进行独热编码一下:

"""重置形状"""
train_x = train_x.reshape(train_x.shape[0], 28, 28, 1)
test_x = test_x.reshape(test_x.shape[0], 28, 28, 1)

train_x = train_x / 255
test_x = test_x / 255

train_y = keras.utils.to_categorical(train_y, 10)
test_y = keras.utils.to_categorical(test_y, 10)

# 在看一下形状
print(train_x.shape)
print(train_y.shape)
print(test_x.shape)
print(test_y.shape)

# 结果:
(60000, 28, 28, 1)
(60000, 10)
(10000, 28, 28, 1)
(10000, 10)

2.3 建立模型LeNet5并完成相关配置

这里这个模型和上面的图稍微有些不一样,因为加了最大池化了,如果你是破镜过来的,那么我相信我只要描述一下这个结构,你应该立马就搭建完成。

我先描述一下这里使用的架构长什么样子:

我们从输入开始, 输入的形状是(28, 28, 1)
然后到达第一层卷积, 卷积核个数6, 大小5 * 5, 激活函数relu
然后第二层,最大池化层,窗口大小(2,2)
然后第三层卷积层,大小5 * 5, 卷积核16个,激活函数relu
第四层,最大池化层,窗口大小(2,2)
第五层,扁平化层,然后跟一个全连接层,神经元个数120
第六层,全连接层,神经元个数84
第七层,输出层,神经元个数10, 激活函数softmax

建立完函数之后,损失函数用交叉熵损失,优化器用Adam,评估用[accuracy]

你可以根据我描述的先自己建一个这个结构,然后配置一下子。
这里可以尝试用序列化的方式建立模型。

"""建立模型 LeNet5
输入层→C1 卷积层→S2 池化层→C3 卷积层→S4 池化层→C5 卷积层→F6 全连接层→Output 全连接层,
对应的 Output 输出类别数为 10
"""
lenet5 = Sequential()
# 第一层 卷积层 6个卷积核,大小是5*5, 激活函数relu
lenet5.add(Conv2D(6, kernel_size=(5, 5), activation='relu', input_shape=(28, 28, 1)))
# 第二层,最大池化层, 池化窗口大小2*2
lenet5.add(MaxPooling2D(pool_size=(2, 2)))
# 第三层, 卷积层16个卷积核, 大小是5*5, 激活函数rule
lenet5.add(Conv2D(16, kernel_size=(5, 5), activation='relu'))
# 第四层, 最大池化层
lenet5.add(MaxPooling2D(pool_size=(2, 2)))
# 第五层扁平化层
lenet5.add(Flatten())
lenet5.add(Dense(120, activation='relu'))
# 全连接层, 输出节点个数84
lenet5.add(Dense(84, activation='relu'))
# 输出层 用softmax计算分类概率
lenet5.add(Dense(10, activation='softmax'))
# 设置损失函数和优化器
lenet5.compile(loss=keras.metrics.categorical_crossentropy, optimizer=keras.optimizers.Adam(), metrics=['accuracy'])

可以使用lenet5.summary()查看一下结构:
在这里插入图片描述

2.4 模型训练和预测结果

"""训练"""
lenet5.fit(train_x, train_y, batch_size=128, epochs=20, verbose=1, validation_data=(test_x, test_y))

"""对结果进行评估"""
score = lenet5.evaluate(test_x, test_y)
print('误差: %.4lf' % score[0])
print('准确率: ', score[1])

## 结果:
10000/10000 [==============================] - 1s 57us/step
误差: 0.0443
准确率:  0.9888

可以看到,经历了20次训练之后,LeNet5的准确率能达到0.9888,还是不错的。 好了,热身完成。

下面我们再来点挑战性的任务,那就是搭建一个更加复杂的模型AlexNet来完成这个任务,看看这个模型的效果。如果这个模型的效果好,那么我们就保存这个模型,以后测试的时候,可以加载出来直接用的。

3. AlexNet模型实现手写数字识别

AlexNet 在 LeNet 的基础上做了改进,提出了更深的 CNN 网络模型,输入尺寸是 227 * 227 * 3,可以输入 RGB 三通道的图像,整个网络的结构是:
在这里插入图片描述

输入层→(C1 卷积层→池化层)→(C2 卷积层→池化层)→C3 卷积层→C4 卷积层→(C5 池化层→池化层)→全连接层→全连接层→Output 全连接层。
在这里插入图片描述

这个网络是不是比上一个复杂了很多啊,这一次我们尝试用建立模型的第二种方式Model,来建立这个模型,但是在这之前,依然需要导入。

3.1 导入包和数据

"""导入包"""
import numpy as np
import matplotlib.pyplot as plt
import keras

from keras.models import Sequential
from keras.layers.core import Dense, Dropout, Activation, Flatten
from keras.layers.convolutional import Conv2D, MaxPooling2D, AveragePooling2D,ZeroPadding2D
from keras.layers.normalization import BatchNormalization
from keras.callbacks import ModelCheckpoint

"""导入数据"""
data_path = './dataset/mnist.npz'

f = np.load(data_path)

train_x, train_y, test_x, test_y = f['x_train'], f['y_train'], f['x_test'], f['y_test']

"""可视化看看效果"""
for i in range(9):
    plt.subplot(3, 3, i+1)
    plt.imshow(train_x[i], cmap='gray', interpolation='none')
    plt.title("Class{}".format(train_y[i]))

可视化了前9张图片,看看效果:
在这里插入图片描述

3.2 数据预处理

"""改变形状,然后归一化,然后独热编码"""
train_x = train_x.reshape(train_x.shape[0], 28, 28, 1)
test_x = test_x.reshape(test_x.shape[0], 28, 28, 1)

train_x = train_x / 255
test_x = test_x / 255

train_y = keras.utils.to_categorical(train_y, 10)
test_y = keras.utils.to_categorical(test_y, 10)

3.3 建立AlexNet模型,并完成相关配置

这里你能根据上面的图片搭建这个模型吗?这里我们搭建模型用Model方式

"""建立模型Alexnet
输入层→(C1 卷积层→池化层)→(C2 卷积层→池化层)→C3 卷积层→C4 卷积层→(C5 池化层→池化层)→
全连接层→全连接层→Output 全连接层。
"""

inputs = keras.Input(shape = [28, 28, 1])
# 第一层卷积层: 卷积核64个,大小11*11, 步长1,1 激活函数relu, padding same, 输入形状(28, 28, 1) 用偏置
conv1 = keras.layers.Conv2D(filters= 64, kernel_size= [11, 11], strides= [1, 1], activation= keras.activations.relu, use_bias= True, padding= 'same')(inputs)
pooling1 = keras.layers.AveragePooling2D(pool_size= [2, 2], strides= [2, 2], padding= 'valid')(conv1)
stand1 = keras.layers.BatchNormalization(axis= 1)(pooling1)
 
# 第二层卷积层 卷积核192, 大小5*5, 激活函数relu
conv2 = keras.layers.Conv2D(filters= 192, kernel_size= [5, 5], strides= [1, 1], activation= keras.activations.relu, use_bias= True, padding= 'same')(stand1)
pooling2 = keras.layers.AveragePooling2D(pool_size= [2, 2], strides= [2, 2], padding= 'valid')(conv2)
stand2 = keras.layers.BatchNormalization(axis= 1)(pooling2)
 
# 第三层卷积层 卷积核384, 大小3*3, 激活函数relu
conv3 = keras.layers.Conv2D(filters= 384, kernel_size= [3, 3], strides= [1, 1], activation= keras.activations.relu, use_bias= True, padding= 'same')(stand2)
stand3 = keras.layers.BatchNormalization(axis=1)(conv3)
 
# 第四层卷积层 卷积核384, 大小3*3, 激活函数relu
conv4 = keras.layers.Conv2D(filters= 384, kernel_size= [3, 3], strides= [1, 1], activation= keras.activations.relu, use_bias= True, padding= 'same')(stand3)
stand4 = keras.layers.BatchNormalization(axis=1)(conv4)
 
# 第五层卷积层
conv5 = keras.layers.Conv2D(filters= 256, kernel_size= [3, 3], strides= [1, 1], activation= keras.activations.relu, use_bias= True, padding= 'same')(stand4)
pooling5 = keras.layers.AveragePooling2D(pool_size= [2, 2], strides= [2, 2], padding= 'valid')(conv5)
stand5 = keras.layers.BatchNormalization(axis=1)(pooling5)
 
# 第六层 flatten
flatten = keras.layers.Flatten()(stand5)
fc1 = keras.layers.Dense(4096, activation= keras.activations.relu, use_bias= True)(flatten)
drop1 = keras.layers.Dropout(0.5)(fc1)
 
fc2 = keras.layers.Dense(4096, activation= keras.activations.relu, use_bias= True)(drop1)
drop2 = keras.layers.Dropout(0.5)(fc2)
 
fc3 = keras.layers.Dense(10, activation= keras.activations.softmax, use_bias= True)(drop2)


# 基于Model方法构建模型
alexnet = keras.Model(inputs= inputs, outputs = fc3)
# 编译模型
alexnet.compile(optimizer= tf.train.AdamOptimizer(0.001),
              loss= keras.losses.categorical_crossentropy,
              metrics= ['accuracy'])

看一下模型结构:
在这里插入图片描述

3.3 AlexNet模型训练和预测

# 配置参数
batch_size = 64
num_classes = 10
epochs = 10

alexnet.fit(train_x, train_y, batch_size=batch_size, epochs=epochs, verbose=1, validation_data=(test_x, test_y))

"""对结果进行评估"""
score = alexnet.evaluate(test_x, test_y)
print('误差: %.4lf' % score[0])
print('准确率: ', score[1])

## 结果
10000/10000 [==============================] - 3s 332us/step
误差: 0.0381
准确率:  0.9909

可以发现,AlexNet迭代了10次,准确率达到了0.9909, 比LeNet5的效果要好很多,所以我们要保存起这个模型来。

"""模型的保存"""
alexnet.save('./model/alexnet.h5')

3.4模型的导入与再使用

我们已经保存好了我们的alexnet模型,如果我们这时候有一个测试样本,我们应该怎么导入模型进行预测呢?

"""导入模型进行预测"""

# 随机选择一个样本
index = np.random.randint(0, test_x.shape[0])
x = test_x[index]
y = test_y[index]

# 显示该数字
plt.imshow(x, cmap='gray_r')
plt.title("original{}".format(y))
plt.show()

# 加载模型
model = keras.models.load_model('./model/alexnet.h5')

# 预测
x = x.reshape(1,28, 28, 1)
predict = np.argmax(model.predict(x))
print('index', index)
print('original', y)
print('predict', predict)

## 结果
index 5958
original 6
predict 6

显示数字:
在这里插入图片描述
好了,到了这里相信你应该认识到Keras的强大了吧。分分钟就可以实现经典网络进行手写数字识别。

当然,这些网络架构已经有大牛实现并训练好了,我们只需要站在人家的肩膀上去眺望远方就可以了。

下面我们就看看如何使用训练好的VGG16,进行简单的微调,用到手写数字的任务上来,毕竟这个结构16层,参数更多更多。 总不能一点点的敲一遍吧。

4. 使用VGG16进行fine-tuning进行手写数字识别

VGG16长下面这样:

看到这个是不是参数多的吓人,复杂度也加大了很多,但是好在我们这里不实现这个,我们对训练好的进行微调就可以为我们的任务所用。 那么怎么微调呢?

  • 首先,VGG16需要的输入维度至少是48,并且通道要求是3通道,所以对于手写数字识别数据集来说,需要改变维度和形状, 这一个在数据预处理块完成,用OpenCV里面的函数即可。
  • 其次,建立模型的时候,我们要先导入进这个训练好的VGG16,但是输入参数要改成我们这里的,然后不用原来顶端的网络块,也就是我们的输出得自己定义
  • 然后,把前15层的参数冻结住,最后的卷积网络块可训练
  • 然后,自定义输出的那块地方,用一个新的序列模型即可。把输出改成我们想要的
  • 把这个基础的VGG16模型和我们自定义的模型连起来完成

具体看操作:

4.1 导入包和数据集

import keras
import cv2
from keras import optimizers
from keras import applications
from keras.models import Model
from keras.models import Sequential
from keras.callbacks import ModelCheckpoint
from keras.layers import Dropout, Flatten, Dense

"""导入数据"""
data_path = './dataset/mnist.npz'
f = np.load(data_path)
train_x, train_y, test_x, test_y = f['x_train'], f['y_train'], f['x_test'], f['y_test']

4.2 数据预处理(这里要改变维度)

"""重置形状  VGG16要求大小不能低于48, 且必须似乎3通道,所以需要使用OpenCV包进行转换"""
x_train = [cv2.cvtColor(cv2.resize(i, (48, 48)), cv2.COLOR_GRAY2BGR) for i in train_x]
x_train = np.concatenate([arr[np.newaxis] for arr in x_train]).astype('float32')
x_test = [cv2.cvtColor(cv2.resize(i, (48, 48)), cv2.COLOR_GRAY2BGR) for i in test_x]
x_test = np.concatenate([arr[np.newaxis] for arr in x_test]).astype('float32')

train_x = x_train / 255
test_x = x_test / 255

train_y = keras.utils.to_categorical(train_y, 10)
test_y = keras.utils.to_categorical(test_y, 10)

## 看一下维度
(60000, 48, 48, 3)
(60000, 10)
(10000, 48, 48, 3)
(10000, 10)

4.3 模型的定义

基础模型我们使用训练好的VGG16,改一下输入部分,然后冻结住前15层的参数

base_model = applications.VGG16(weights='imagenet', include_top=False, input_shape=(48, 48, 3)

for layer in base_model.layers[:15]:
	layer.trainable = False   # 冻结

#输出的部分我们自己写
top_model = Sequential()
top_model.add(Flatten(input_shape=base_model.output_shape[1:]))
top_model.add(Dense(32, activation='relu')
top_model.add(Dropout(0.5))
top_model.add(Dense(10, activation='softmax'))

# 然后基础的和输出的连起来就是我们最后的模型 
# 新网络=预训练网络+自定义网络
model = Model(inputs=base.model.input, outputs=top_model(base_model.output))

model.compile(
    loss='categorical_crossentropy',
    optimizer=optimizers.SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True),
    metrics=['accuracy'])  # 损失函数为分类交叉熵,优化器为SGD

看下网络结构:
在这里插入图片描述

4.3 模型训练与评估

# 训练
model.fit(train_x, train_y, batch_size=128, epochs=10, validation_data=(test_x, test_y))

"""对结果进行评估"""
score = model.evaluate(test_x, test_y)
print('误差: %.4lf' % score[0])
print('准确率: ', score[1])

## 结果:
10000/10000 [==============================] - 9s 853us/step
误差: 0.0561
准确率:  0.9884

这个效果竟然是最差的,我认为,这个迭代的次数少,10次太少,参数多,这样模型有可能处在了欠拟合的情况,我电脑跑这个挺费时间了,就没敢加迭代次数。 主要目的也是看到底好不好,而是看看这个微调是怎么使用的。

5. 总结

好了,玩转手写数字识别的游戏就到这儿吧,相信通过今天的学习,应该能搞定深度学习界的hello world了吧。简单的梳理一下: 首先是用Keras实现了LeNet5网络进行手写数字识别,然后又实现了AlexNet,最后对VGG16进行了微调完成这些任务。

这样一圈下来,是不是对这三个网络不太陌生了啊,当然这些流程也可以实现别的网络完成这个任务或者其他的任务。 Keras是不是很强大啊?

当然,感知境仅凭这一个小项目是过不去的,都这么简单破镜,那不五个项目就到了知命。 感知境肯定会再有别的项目,比如实现LSTM网络啊,ResNet网络啊等等吧。

每次破镜,应该是对Keras又有了新的认识并实现了项目之后才能破镜,目前先逗留在感知吧。先去学习一些新的知识,再来考虑破镜。

参考:

发布了85 篇原创文章 · 获赞 107 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/wuzhongqiang/article/details/104433590