1.前言
好久不见各位,前段时间因为专注个人工作所以很少有时间完成一些文字性工作。这个周末起的比较早,来更新一下系列的文章。在第一章节中,我们讲到了什么是深度学习中的数据,以及常见的一些数据预处理方式。其实对于实际的研究和现实工作来讲,第一章节所讲的内容只能说是帮助大家树立一个意识。在实际的工业应用和研究中,我们面临的数据总是多种多样,数据质量参差不齐,因此具体怎么处理仍要看工作的实际需求和任务的实际实现i情况。通过这两点来进行不断地调试,才能获得最佳的结果。
当我们准备好了数据,那么下一步的工作就是,构建自己的神经网络,训练自己的网络模型。作者在这里选用的数据库是初学者最常见的也是最喜欢的两个标准数据集,MNIST和CIFAR10作为样例来帮助大家了解在一个神经网络中都有哪些潜在的机制可以被应用。作者这里先使用Keras作为Demo展示的框架,因为这个框架的代码相对而言方便初学者理解。
2. 训练神经网络的基本环节
对于初学者而言,非常困难的一点是树立起一个清晰的框架来了解到底怎么样才算是完整地构建了一个神经网络,完整地完成了一个深度学习任务。那么,我们就自然而然会想到,训练一个基本的神经网络,到底有哪些基本的环节?通常情况下,一个深度学习工程应该包括以下几个基本环节:
由图1可知,深度学习的基本环节包括:数据加载器,数据预处理,神经网络的构建,模型优化器,损失函数及验证指标的设定和超参数及参数的调整等。这几个环节在不同的框架中实现的方式稍有不同,但是整体上没有很明显的区别。那么拿一个具体的例子来帮助读者感受一下,这几个流程在一个深度学习工程中究竟是怎样对应实现的。这里为了方便读者理解,降低编程难度,笔者采用Keras框架对MNIST数据集和CIFAR10数据集进行分类任务。
3. 数据加载器
数据加载器在深度学习框架中是一个必不可缺的环节,为了方便调用数据库,很多框架都预先设定了一些基本数据库的标准调用方式。如Keras中,你可以调用keras.datasets下的几个标准数据集,包括MNIST,FashionMNIST,IMDB 等小型数据库。在pytorch中,除了可以调用这些小型数据库,你也可以通过torchvision.datasets 调用许多大型数据库,如COCO,ImageNet,CelebA等数据库,十分的便捷而且标准化,极大化方便了编程人员进行导入和测试,但是笔者最近测试有一些数据库可能在下载时候有问题,尤其是在国内的朋友在下载的时候可能会遇到网络链接不稳定,这个时候就需要我们离线进行数据下载,否则一旦下载的数据不完整且下载的流中断了,这个数据库就会乱成一锅粥,继续下载就变得很不方便,有时候会造成数据缺失。
当然,编程人员也可以自己进行数据库的构建,将数据重构为神经网络需要的input shape即可。这里我们举最简单的例子,即使用Keras调用MNIST。
from keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
print(x_train.shape)
print(y_train.shape)
print(x_test.shape)
print(y_test.shape)
通过这段代码,我们就已经可以加载mnist数据集了,可以看出这种方式真的非常快捷方便。加载后的变量shape是(60000,28,28),其中60000是数据集中图像的数量,28是图像的高和宽。因为MNIST是二值图像,所以没有颜色通道。这里我们对比一个CIFAR10加载结果。CIFAR10中的图像都是彩色图像,相比MNIST,可以看到shape中多出一个彩色通道3,这意味着rgb三个通道,图像的shape是32,且数据集图像的数量是50000。
由图2可知,送入神经网络前的数据应该被reshape或者构建成这样的数据,可以看到这里的数据已经被分好了训练集和测试集,这个在数据集预处理时候我们就已经说过了,这里就不赘述了。
那么假设我们有一个文件夹里全都是自己的图片,我们改如何构建这样的数据集呢?其实很简单,通过系统操作读取文件夹下的图片名字列表,后遍历并将图片依次读入计算机。此时,如果是有监督学习,那么还要关注它的label。这个label可以是文字可以是图像,但是必须要符合你的网络结构。通过python的append操作,我们即可将图像重组成这样的一个数据结构,并送入神经网络进行训练。
这里要提醒大家的一点是,我们在这里使用到的方法是一次性将整个数据库读入计算机内存中,这样做的好处是可以加速训练,由于是一次性加载完成, 节约了数据读取的时间。但是缺点是一次性读入大量的数据,可能会导致内存溢出(OOM)问题。为解决这一问题,keras中包含了fit_generator的特殊功能。其主要设计动机时每次从大量数据库中随机抽取一部分图像作为这一个batch的输入并进行训练,在下一个batch训练的时候,再次随机读取一批数据进入到计算机中。具体的解释和参数机制请读者仔细阅读keras的Document文件,类似的,这一机制同样适用于自己构建的数据集。
4. 神经网络的构建
Keras中的模型构建有两种方式,一种是使用sequential(),另外一种是使用Model构建的,需要使用Model函数对每个层进行封装。在下面的代码中,我们展示了两种不同方法对于同一个神经网络的构建。
# model construction approach 1
model = Sequential()
model.add(Conv2D(filters=32, kernel_size=(5,5), padding='same', activation='relu', input_shape=(28, 28, 1)))
model.add(MaxPool2D(strides=2))
model.add(Conv2D(filters=48, kernel_size=(5,5), padding='valid', activation='relu'))
model.add(MaxPool2D(strides=2))
model.add(Flatten())
model.add(Dense(256, activation='relu'))
model.add(Dense(84, activation='relu'))
model.add(Dense(10, activation='softmax'))
model.build()
model.summary()
# model construction approach 2
input = Input((28, 28, 1))
x = Conv2D(filters=32, kernel_size=(5,5), padding='same', activation='relu')(input)
x = MaxPool2D(strides=2)(x)
x = Conv2D(filters=48, kernel_size=(5,5), padding='valid', activation='relu')(x)
x = MaxPool2D(strides=2)(x)
x = Flatten()(x)
x = Dense(256, activation='relu')(x)
x = Dense(84, activation='relu')(x)
x = Dense(10, activation='softmax')(x)
model_app2 = Model(input, x)
model_app2.build()
model_app2.summary()
可以看出,方法1中的模型构建方法只适用于单向单分支网络,而方法2中的网络构建方式则适用于多向,多分枝网络。明显可以看出第二种方法的应用灵活性更高,同时能够更好地对层之间的连接进行清晰的定义。通过这样的方式,我们就已经构建出一个经典的LeNet5网络,并可以被用于分类任务。我们现在简单对网络结构进行分析:
【1】网络需要定义输入的shape,实现的方式可能直接使用conv2d内置的input_shape,也可以使用Input layer
【2】conv2d是为了提取图像的2d卷积特征,并对图像的尺寸等参数进行适当调整。
【3】Pooling的操作是为了筛选特征,maxpooling是为了直接快速提取主要的特征。
【4】Flatten是因为conv2d的输出是4D,而Dense的输入是2D,因此需要将对应的维度进行拉伸展开
【5】Dense层是fully-connected layer,也即全连接层,负责调整最终分类任务的权重
到此,一个神经网络的构建便已完成。感兴趣的读者建议多阅读对应的Document来了解具体层的参数设置,因为在官方文档中已经解释的比较通俗易懂,这里我们就不赘述了。
5. 模型编译
keras框架中对于模型的编译需要注意三个地方,即模型优化器,损失函数,和metric。
【1】模型优化器,通常被称作optimizer,是神经网络更新梯度参数最重要的设置,一个好的optimizer能够帮助模型找到最合适的解,同时也能加速模型的收敛速度。那么究竟有哪些optimizer?这些optimizer有哪些特点?在什么情况下选用何种optimizer?是一个非常重要但是仍未受到广泛初学者关注的问题。作为参考,笔者在这里推荐几篇文章[1], [2],这几篇文章对优化器一节的内容有较为清晰的解释。由于篇幅问题,我们将这一节的内容放在后面的博文中进行讲解。这里我们仅选用最常见的Adam优化器对网络进行参数优化,来帮助大家快速梳理概念。
import keras
optimizer = keras.optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=None, decay=0.0, amsgrad=False)
【2】损失函数与metric:在keras框架中,优化器需要在model.compile中被指定,和优化器同样需要被指定的是损失函数。损失函数主要是用于衡量预测值和标签值之间的距离,并将距离作为依据进行梯度更新。metric则是用来监控训练的指标。如果是分类任务,则通常是categorical_crossentropy,也即分类交叉熵。需要注意的是,model在compile之后,便相当于完成了一次权重初始化,也只有完成初始化的model才可以训练。
模型训练
keras提供了非常简洁的命令来训练神经网络。我们可以将编译好的模型和准备好的数据,使用fit函数进行训练。
model.fit(x_train, y_train,
batch_size=batch_size,
epochs=epochs,
verbose=1,
validation_data=(x_test, y_test))
可以看到这里的几个输入参数,x_train和y_train分别对应了训练输入和对应的标签值,然后需要确定每一个batch有多少数据的batchsize,也要确定究竟让模型在数据上走几趟的epoch,还有验证数据validation data用于评测训练模型的表现和超参数的调整。如果verbose选择1,则在训练过程中动态显示训练过程;如果是0则只显示最终测评结果和损失函数。这个参数对训练本身没有任何影响,笔者可以对其按需修改。到此,一个完整的数据加载和模型训练就完成了。当训练结束,我们就已经获取一个可以进行手写体分类的模型了。如果需要对模型进行保存,笔者这里推荐直接model.save(),可以省略很多麻烦。
完整代码
'''Trains a simple convnet on the MNIST dataset.
Gets to 99.25% test accuracy after 12 epochs
(there is still a lot of margin for parameter tuning).
16 seconds per epoch on a GRID K520 GPU.
'''
from __future__ import print_function
import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras import backend as K
batch_size = 128
num_classes = 10
epochs = 12
# input image dimensions
img_rows, img_cols = 28, 28
# the data, split between train and test sets
(x_train, y_train), (x_test, y_test) = mnist.load_data()
if K.image_data_format() == 'channels_first':
x_train = x_train.reshape(x_train.shape[0], 1, img_rows, img_cols)
x_test = x_test.reshape(x_test.shape[0], 1, img_rows, img_cols)
input_shape = (1, img_rows, img_cols)
else:
x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1)
x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1)
input_shape = (img_rows, img_cols, 1)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255
print('x_train shape:', x_train.shape)
print(x_train.shape[0], 'train samples')
print(x_test.shape[0], 'test samples')
# convert class vectors to binary class matrices
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3),
activation='relu',
input_shape=input_shape))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))
model.compile(loss=keras.losses.categorical_crossentropy,
optimizer=keras.optimizers.Adam(),
metrics=['accuracy'])
model.fit(x_train, y_train,
batch_size=batch_size,
epochs=epochs,
verbose=1,
validation_data=(x_test, y_test))
score = model.evaluate(x_test, y_test, verbose=0)
print('Test loss:', score[0])
print('Test accuracy:', score[1])
Reference
[1] https://blog.csdn.net/weixin_40170902/article/details/80092628