翻译:张睿毅
校对:吴金笛
本文约9300字,建议阅读20分钟。
本文章逐步介绍了卷积神经网络的建模过程,最终实现了MNIST手写数字分类。
MNIST手写数字分类问题是计算机视觉和深度学习中使用的标准数据集。
尽管数据集得到了有效的解决,但它可以作为学习和实践如何开发、评估和使用卷积深度学习神经网络从头开始进行图像分类的基础。这包括如何开发一个用于评估模型性能的强大测试工具,如何探索模型的改进,以及如何保存模型,然后加载它以对新数据进行预测。
在本教程中,您将了解如何从头开始开发用于手写数字分类的卷积神经网络。
完成本教程后,您将了解:
如何开发测试工具以开发稳健的模型评估并建立分类任务性能的基准线
如何在基准模型上拓展以改进学习及模型容量
如何开发最终模型,评估最终模型性能,并用它对于新图像进行预测
让我们开始吧!
Photo by Richard Allaway, some rights reserved
图片来源:https://www.flickr.com/photos/geographyalltheway_photos/2918451763/
教程的五个部分分别是:
1. MNIST手写数字分类数据集
2. 模型评估方法
3. 如何建立基准模型
4. 如何建立改进模型
5. 如何完成模型建立并进行预测
MNIST数据集是修改后的国家标准与技术研究所数据集的缩写。
它是一个由60000个28×28像素的小正方形灰度图像组成的数据集,这些图像的手写单位数介于0和9之间。
任务是将手写数字的给定图像分类为10个类中的一个,这些类表示0到9之间的整数值(包括0和9)。
它是一个广泛使用和深入理解的数据集,并且在大多数情况下是“已解决”的。性能最好的模型是深度学习卷积神经网络,其分类精度达到99%以上,保留测试数据集的错误率在0.4%到0.2%之间。
下面的示例使用keras API加载MNIST数据集,并创建训练数据集中前九个图像的绘图。
# example of loading the mnist dataset
from keras.datasets import mnist
from matplotlib import pyplot
# load dataset
(trainX, trainy), (testX, testy) = mnist.load_data()
# summarize loaded dataset
print('Train: X=%s, y=%s' % (trainX.shape, trainy.shape))
print('Test: X=%s, y=%s' % (testX.shape, testy.shape))
# plot first few images
for i in range(9):
# define subplot
pyplot.subplot(330 + 1 + i)
# plot raw pixel data
pyplot.imshow(trainX[i], cmap=pyplot.get_cmap('gray'))
# show the figure
pyplot.show()
运行该示例将加载MNIST训练和测试数据集并打印它们的形状。
我们可以看到训练数据集中有60000个例子,测试数据集中有10000个例子,图像确实是28×28像素的正方形。
Train: X=(60000, 28, 28), y=(60000,)Test: X=(10000, 28, 28), y=(10000,)
同时,我们创建了数据集中前九个图像的绘图,显示待分类图像的自然手写性质。
从MNIST数据集中选出的子集
尽管MNIST数据集得到了有效的解决,但使用卷积神经网络解决图像分类任务的方法可以作为开发和实践的一个有用的起点。
我们可以从头开始开发一个新的模型,而不是回顾数据集上性能良好的模型的文献。
数据集已经有了一个明确定义的训练和测试数据集,我们可以使用它。
为了估计给定训练运行模型的性能,我们可以进一步将训练集划分为训练和验证数据集。然后,可以绘制每次运行的训练和验证数据集的性能,以提供学习曲线,并洞察模型学习问题的程度。
keras API通过在训练模型时向 model.fit() 函数指定 “validation_data” 参数来支持这一点,该参数将返回一个对象,该对象描述了每个训练阶段所选损失和指标的模型性能。
# record model performance on a validation dataset during training
history = model.fit(..., validation_data=(valX, valY))
为了估计一个模型在一般问题上的性能,我们可以使用k倍交叉验证,或者5倍交叉验证。这将在训练和测试数据集的差异以及学习算法的随机性方面,给出一些模型的方差。考虑到标准差,模型的性能可以作为k-折叠的平均性能,如果需要,可以用它来估计置信区间。
我们可以使用scikit Learn API中的Kfold类来实现给定神经网络模型的k重交叉验证评估。虽然我们可以选择一种灵活的方法,其中kfold类只用于指定每个spit所用的行索引,但实现这一点的方法有很多种。
我们将保留实际的测试数据集,并将其用作最终模型的评估。
第一步是建立一个基准模型。
这一点很关键,因为它即涉及到为测试工具开发基础设施,以便我们设计的任何模型都可以在数据集上进行评估,并且它在模型性能方面建立了一个基线,通过这个基线可以比较所有的改进。
测试工具的设计是模块化的,我们可以为每个部件开发单独的功能。如果我们愿意的话,这允许对测试工具的某个特定方面进行修改或相互更改,独立于其他部分。
我们可以用五个关键要素开发这个测试工具。它们是数据集的加载、数据集的准备、模型的定义、模型的评估和结果的表示。
我们对数据集已经有一些了解。
例如,我们知道图像都是预先对齐的(例如,每个图像只包含一个手绘数字),所有图像都具有相同的28×28像素的正方形大小,并且图像是灰度的。
因此,我们可以加载图像并将数据数组整形为具有单一颜色通道。
# load dataset (trainX, trainY), (testX, testY) = mnist.load_data() # reshape dataset to have a single channel trainX = trainX.reshape((trainX.shape[0], 28, 28, 1)) testX = testX.reshape((testX.shape[0], 28, 28, 1)) |
我们还知道有10个类,这些类被表示为唯一的整数。
因此,我们可以对每个样本的类元素使用一个热编码,将整数转换为一个10元素的二进制向量,其中1表示类值的索引,0表示所有其他类的值。我们可以使用to_categorial()实际程序函数来实现这一点。
# one hot encode target values
trainY = to_categorical(trainY)
testY = to_categorical(testY)
load_dataset()函数实现这些行为,并可以被用作加载数据集。
# load train and test dataset
def load_dataset():
# load dataset
(trainX, trainY), (testX, testY) = mnist.load_data()
# reshape dataset to have a single channel
trainX = trainX.reshape((trainX.shape[0], 28, 28, 1))
testX = testX.reshape((testX.shape[0], 28, 28, 1))
# one hot encode target values
trainY = to_categorical(trainY)
testY = to_categorical(testY)
return trainX, trainY, testX, testY
我们知道,数据集中每个图像的像素值都是介于黑白或0到255之间的无符号整数。
我们不知道缩放用于建模的像素值的最佳方法,但我们知道需要进行一些缩放。
一个好的起点是规范化灰度图像的像素值,例如将其重新调整到范围[0,1]。这涉及到首先将数据类型从无符号整数转换为浮点数,然后将像素值除以最大值。
# convert from integers to floats
train_norm = train.astype('float32')
test_norm = test.astype('float32')
# normalize to range 0-1
train_norm = train_norm / 255.0
test_norm = test_norm / 255.0
以下的prep_pixels()函数实现这些行为,我们提供这些从训练和测试数据集中需要被测量的像素值给这个函数。
# scale pixels
def prep_pixels(train, test):
# convert from integers to floats
train_norm = train.astype('float32')
test_norm = test.astype('float32')
# normalize to range 0-1
train_norm = train_norm / 255.0
test_norm = test_norm / 255.0
# return normalized images
return train_norm, test_norm
这个函数必须被调用以在任何模型之前准备好像素值。
接下来,我们需要为问题定义一个基线卷积神经网络模型。
该模型主要有两个部分:前端特征提取由卷积层和池化层组成,后端分类器进行预测。
对于卷积前端,我们可以从单个卷积层开始,该卷积层具有较小的过滤器大小(3,3)和少量的过滤器(32),然后是最大池化层。然后可以展平过滤器映射,为分类器提供特性。
考虑到该问题是一个多类分类任务,我们知道我们需要一个具有10个节点的输出层来预测属于这10个类中每个类的图像的概率分布。这还需要使用SoftMax激活功能。在特性提取器和输出层之间,我们可以添加一个全连接层来解释特性,在本例中是100个节点。
所有层都将使用relu激活函数和He 权重初始化方案,这两个都是最佳方法。
我们将对学习率为0.01,动量为0.9的随机梯度下降优化器使用保守配置。分类交叉熵损失函数将得到优化,适用于多类分类,我们将监测分类精度指标,这是适当的,因为我们在10个类中的每一类都有相同数量的例子。
下面的define_model()函数将定义并返回此模型。
# define cnn model
def define_model():
model = Sequential()
model.add(Conv2D(32, (3, 3), activation='relu', kernel_initializer='he_uniform', input_shape=(28, 28, 1)))
model.add(MaxPooling2D((2, 2)))
model.add(Flatten())
model.add(Dense(100, activation='relu', kernel_initializer='he_uniform'))
model.add(Dense(10, activation='softmax'))
# compile model
opt = SGD(lr=0.01, momentum=0.9)
model.compile(optimizer=opt, loss='categorical_crossentropy', metrics=['accuracy'])
return model
模型定义后,我们需要对其进行评估。
模型将通过五重交叉验证进行评估。选择k=5的值为重复评估提供基线,并且不需要太长的运行时间。每个测试集将是训练数据集的20%,或大约12000个示例,接近此问题的实际测试集大小。
训练数据集在分割前进行洗牌,每次都进行样本洗牌,这样我们评估的任何模型在每个折叠中都将具有相同的训练和测试数据集,从而提供模型之间的逐个比较。
我们将为一个适度的10个训练阶段培训基线模型,默认批量大小为32个示例。每个阶段的测试集将用于评估模型在训练运行的每个阶段,以便我们可以稍后创建学习曲线,并在运行结束时,以便我们可以评估模型的性能。因此,我们将跟踪每次运行的结果历史,以及折叠的分类精度。
下面的evaluate_model()函数实现了这些行为,将定义的模型和培训数据集作为参数,并返回一个精度分数和训练历史的列表,这些列表可以稍后进行总结。
# evaluate a model using k-fold cross-validation
def evaluate_model(model, dataX, dataY, n_folds=5):
scores, histories = list(), list()
# prepare cross validation
kfold = KFold(n_folds, shuffle=True, random_state=1)
# enumerate splits
for train_ix, test_ix in kfold.split(dataX):
# select rows for train and test
trainX, trainY, testX, testY = dataX[train_ix], dataY[train_ix], dataX[test_ix], dataY[test_ix]
# fit model
history = model.fit(trainX, trainY, epochs=10, batch_size=32, validation_data=(testX, testY), verbose=0)
# evaluate model
_, acc = model.evaluate(testX, testY, verbose=0)
print('> %.3f' % (acc * 100.0))
# stores scores
scores.append(acc)
histories.append(history)
return scores, histories
一旦对模型进行了评估,我们就可以给出结果。
有两个关键的方面要呈现:训练期间模型学习行为的记录和模型性能的评估。这些可以使用单独的函数来实现。
首先,记录包括创建一个折线图,显示在K-折叠交叉验证的每个折叠期间训练集和测试集的模型性能。这些图对于了解模型是否过度拟合、欠拟合,还是是否对数据集有良好的拟合是很有价值的。
我们将创建一个包含两个子图的单个图,一个子图用于损失,一个子图用于准确性。
蓝线表示训练数据集上的模型性能,橙色线表示预留测试数据集上的性能。下面的summary_diagnostics()函数根据收集的训练历史创建并显示此图。
# plot diagnostic learning curves
def summarize_diagnostics(histories):
for i in range(len(histories)):
# plot loss
pyplot.subplot(211)
pyplot.title('Cross Entropy Loss')
pyplot.plot(histories[i].history['loss'], color='blue', label='train')
pyplot.plot(histories[i].history['val_loss'], color='orange', label='test')
# plot accuracy
pyplot.subplot(212)
pyplot.title('Classification Accuracy')
pyplot.plot(histories[i].history['acc'], color='blue', label='train')
pyplot.plot(histories[i].history['val_acc'], color='orange', label='test')
pyplot.show()
其次,通过计算平均值和标准差,可以总结出各折叠过程中收集的分类准确度得分。这提供了在这个数据集上训练的模型的平均预期性能的估计,以及平均方差的估计。我们还将通过创建和显示箱型图和须状图来总结分数分布。
下面的summary_performance()函数为模型评估期间收集的给定分数列表实现此功能。
# summarize model performance
def summarize_performance(scores):
# print summary
print('Accuracy: mean=%.3f std=%.3f, n=%d' % (mean(scores)*100, std(scores)*100, len(scores)))
# box and whisker plots of results
pyplot.boxplot(scores)
pyplot.show()
我们需要一个驱动测试工具的函数。
这涉及到调用所有的定义的函数。
# run the test harness for evaluating a model
def run_test_harness():
# load dataset
trainX, trainY, testX, testY = load_dataset()
# prepare pixel data
trainX, testX = prep_pixels(trainX, testX)
# define model
model = define_model()
# evaluate model
scores, histories = evaluate_model(model, trainX, trainY)
# learning curves
summarize_diagnostics(histories)
# summarize estimated performance
summarize_performance(scores)
我们现在拥有了所需的一切;下面列出了MNIST数据集上基线卷积神经网络模型的完整代码示例。
# baseline cnn model for mnist
from numpy import mean
from numpy import std
from matplotlib import pyplot
from sklearn.model_selection import KFold
from keras.datasets import mnist
from keras.utils import to_categorical
from keras.models import Sequential
from keras.layers import Conv2D
from keras.layers import MaxPooling2D
from keras.layers import Dense
from keras.layers import Flatten
from keras.optimizers import SGD
# load train and test dataset
def load_dataset():
# load dataset
(trainX, trainY), (testX, testY) = mnist.load_data()
# reshape dataset to have a single channel
trainX = trainX.reshape((trainX.shape[0], 28, 28, 1))
testX = testX.reshape((testX.shape[0], 28, 28, 1))
# one hot encode target values
trainY = to_categorical(trainY)
testY = to_categorical(testY)
return trainX, trainY, testX, testY
# scale pixels
def prep_pixels(train, test):
# convert from integers to floats
train_norm = train.astype('float32')
test_norm = test.astype('float32')
# normalize to range 0-1
train_norm = train_norm / 255.0
test_norm = test_norm / 255.0
# return normalized images
return train_norm, test_norm
# define cnn model
def define_model():
model = Sequential()
model.add(Conv2D(32, (3, 3), activation='relu', kernel_initializer='he_uniform', input_shape=(28, 28, 1)))
model.add(MaxPooling2D((2, 2)))
model.add(Flatten())
model.add(Dense(100, activation='relu', kernel_initializer='he_uniform'))
model.add(Dense(10, activation='softmax'))
# compile model
opt = SGD(lr=0.01, momentum=0.9)
model.compile(optimizer=opt, loss='categorical_crossentropy', metrics=['accuracy'])
return model
# evaluate a model using k-fold cross-validation
def evaluate_model(model, dataX, dataY, n_folds=5):
scores, histories = list(), list()
# prepare cross validation
kfold = KFold(n_folds, shuffle=True, random_state=1)
# enumerate splits
for train_ix, test_ix in kfold.split(dataX):
# select rows for train and test
trainX, trainY, testX, testY = dataX[train_ix], dataY[train_ix], dataX[test_ix], dataY[test_ix]
# fit model
history = model.fit(trainX, trainY, epochs=10, batch_size=32, validation_data=(testX, testY), verbose=0)
# evaluate model
_, acc = model.evaluate(testX, testY, verbose=0)
print('> %.3f' % (acc * 100.0))
# stores scores
scores.append(acc)
histories.append(history)
return scores, histories
# plot diagnostic learning curves
def summarize_diagnostics(histories):
for i in range(len(histories)):
# plot loss
pyplot.subplot(211)
pyplot.title('Cross Entropy Loss')
pyplot.plot(histories[i].history['loss'], color='blue', label='train')
pyplot.plot(histories[i].history['val_loss'], color='orange', label='test')
# plot accuracy
pyplot.subplot(212)
pyplot.title('Classification Accuracy')
pyplot.plot(histories[i].history['acc'], color='blue', label='train')
pyplot.plot(histories[i].history['val_acc'], color='orange', label='test')
pyplot.show()
# summarize model performance
def summarize_performance(scores):
# print summary
print('Accuracy: mean=%.3f std=%.3f, n=%d' % (mean(scores)*100, std(scores)*100, len(scores)))
# box and whisker plots of results
pyplot.boxplot(scores)
pyplot.show()
# run the test harness for evaluating a model
def run_test_harness():
# load dataset
trainX, trainY, testX, testY = load_dataset()
# prepare pixel data
trainX, testX = prep_pixels(trainX, testX)
# define model
model = define_model()
# evaluate model
scores, histories = evaluate_model(model, trainX, trainY)
# learning curves
summarize_diagnostics(histories)
# summarize estimated performance
summarize_performance(scores)
# entry point, run the test harness
run_test_harness()
运行这个示例可以打印交叉验证过程中每个阶段的分类精度。这有助于了解模型评估正在进行。
我们可以看到两种情况下,模型达到完美的技能,在一种情况下,它实现低于99%的准确性。这些都是很好的结果。
> 98.558
> 99.842
> 99.992
> 100.000
> 100.000
接下来,显示一个记录图,深入了解每个折叠的模型的学习行为。
在这种情况下,我们可以看到,该模型总体上实现了良好的拟合,即训练和测试学习曲线收敛。没有明显的过度或不足的迹象。
接下来,计算模型性能的粗略值。我们可以看到,在这种情况下,该模型的估计能力约为99.6%,这令人印象深刻,尽管它的标准偏差高到约为0.5%。
Accuracy: mean=99.678 std=0.563, n=5
最后,建立了一个方格和须状图,总结了精度分数的分布情况。
使用k倍交叉验证评估的基线模型准确度分数的盒状和胡须图
正如我们所期望的,分布是紧密的,超过99.8%的准确性,有一个异常值结果。
我们现在有了一个强大的测试工具和一个性能良好的基线模型。
我们可以通过许多方法来探索对基线模型的改进。
我们将研究通常会导致改进的模型配置领域,即所谓的易实现的目标。第一个是学习算法的改变,第二个是模型深度的增加。
学习算法有许多方面可以探索改进。
也许最大的杠杆作用是学习率,例如评估学习率的较小或较大值可能产生的影响,以及在训练期间改变学习率的时间表。
另一种可以快速加速模型学习并导致性能大幅度提高的方法是批处理规范化。我们将评估批处理规范化对基线模型的影响。
批处理规范化可以在卷积层和完全连接层之后使用。它的作用是改变层的输出分布,特别是通过标准化输出。这有助于稳定和加速学习过程。
对于基线模型的卷积层和密集层,我们可以在激活函数之后更新模型定义以使用批处理规范化。下面列出了使用批处理规范化的define_model()函数的更新版本。
# define cnn model
def define_model():
model = Sequential()
model.add(Conv2D(32, (3, 3), activation='relu', kernel_initializer='he_uniform', input_shape=(28, 28, 1)))
model.add(MaxPooling2D((2, 2)))
model.add(Conv2D(64, (3, 3), activation='relu', kernel_initializer='he_uniform'))
model.add(Conv2D(64, (3, 3), activation='relu', kernel_initializer='he_uniform'))
model.add(MaxPooling2D((2, 2)))
model.add(Flatten())
model.add(Dense(100, activation='relu', kernel_initializer='he_uniform'))
model.add(Dense(10, activation='softmax'))
# compile model
opt = SGD(lr=0.01, momentum=0.9)
model.compile(optimizer=opt, loss='categorical_crossentropy', metrics=['accuracy'])
return model
为完整展示起见,整段代码包括这个改变,如下所示。
# deeper cnn model for mnist
from numpy import mean
from numpy import std
from matplotlib import pyplot
from sklearn.model_selection import KFold
from keras.datasets import mnist
from keras.utils import to_categorical
from keras.models import Sequential
from keras.layers import Conv2D
from keras.layers import MaxPooling2D
from keras.layers import Dense
from keras.layers import Flatten
from keras.optimizers import SGD
# load train and test dataset
def load_dataset():
# load dataset
(trainX, trainY), (testX, testY) = mnist.load_data()
# reshape dataset to have a single channel
trainX = trainX.reshape((trainX.shape[0], 28, 28, 1))
testX = testX.reshape((testX.shape[0], 28, 28, 1))
# one hot encode target values
trainY = to_categorical(trainY)
testY = to_categorical(testY)
return trainX, trainY, testX, testY
# scale pixels
def prep_pixels(train, test):
# convert from integers to floats
train_norm = train.astype('float32')
test_norm = test.astype('float32')
# normalize to range 0-1
train_norm = train_norm / 255.0
test_norm = test_norm / 255.0
# return normalized images
return train_norm, test_norm
# define cnn model
def define_model():
model = Sequential()
model.add(Conv2D(32, (3, 3), activation='relu', kernel_initializer='he_uniform', input_shape=(28, 28, 1)))
model.add(MaxPooling2D((2, 2)))
model.add(Conv2D(64, (3, 3), activation='relu', kernel_initializer='he_uniform'))
model.add(Conv2D(64, (3, 3), activation='relu', kernel_initializer='he_uniform'))
model.add(MaxPooling2D((2, 2)))
model.add(Flatten())
model.add(Dense(100, activation='relu', kernel_initializer='he_uniform'))
model.add(Dense(10, activation='softmax'))
# compile model
opt = SGD(lr=0.01, momentum=0.9)
model.compile(optimizer=opt, loss='categorical_crossentropy', metrics=['accuracy'])
return model
# evaluate a model using k-fold cross-validation
def evaluate_model(model, dataX, dataY, n_folds=5):
scores, histories = list(), list()
# prepare cross validation
kfold = KFold(n_folds, shuffle=True, random_state=1)
# enumerate splits
for train_ix, test_ix in kfold.split(dataX):
# select rows for train and test
trainX, trainY, testX, testY = dataX[train_ix], dataY[train_ix], dataX[test_ix], dataY[test_ix]
# fit model
history = model.fit(trainX, trainY, epochs=10, batch_size=32, validation_data=(testX, testY), verbose=0)
# evaluate model
_, acc = model.evaluate(testX, testY, verbose=0)
print('> %.3f' % (acc * 100.0))
# stores scores
scores.append(acc)
histories.append(history)
return scores, histories
# plot diagnostic learning curves
def summarize_diagnostics(histories):
for i in range(len(histories)):
# plot loss
pyplot.subplot(211)
pyplot.title('Cross Entropy Loss')
pyplot.plot(histories[i].history['loss'], color='blue', label='train')
pyplot.plot(histories[i].history['val_loss'], color='orange', label='test')
# plot accuracy
pyplot.subplot(212)
pyplot.title('Classification Accuracy')
pyplot.plot(histories[i].history['acc'], color='blue', label='train')
pyplot.plot(histories[i].history['val_acc'], color='orange', label='test')
pyplot.show()
# summarize model performance
def summarize_performance(scores):
# print summary
print('Accuracy: mean=%.3f std=%.3f, n=%d' % (mean(scores)*100, std(scores)*100, len(scores)))
# box and whisker plots of results
pyplot.boxplot(scores)
pyplot.show()
# run the test harness for evaluating a model
def run_test_harness():
# load dataset
trainX, trainY, testX, testY = load_dataset()
# prepare pixel data
trainX, testX = prep_pixels(trainX, testX)
# define model
model = define_model()
# evaluate model
scores, histories = evaluate_model(model, trainX, trainY)
# learning curves
summarize_diagnostics(histories)
# summarize estimated performance
summarize_performance(scores)
# entry point, run the test harness
run_test_harness()
再次运行该示例将报告交叉验证过程的每个阶段的模型性能。
我们可以看到,与跨交叉验证折叠的基线相比,模型性能可能略有下降。
> 98.592
> 99.792
> 99.933
> 99.992
> 99.983
创建一个学习曲线图,在这种情况下,显示学习速度(在每一批训练中改进)似乎与基线模型没有不同。
这些图表明,至少在本例中实现的批处理规范化并没有带来任何好处。
K-折叠交叉验证过程中批量标准化模型的损失和精度学习曲线
接下来,给出了模型的估计性能,表明模型的平均精度略有下降:与基线模型的99.678相比,为99.658,但标准偏差可能略有下降。
Accuracy: mean=99.658 std=0.538, n=5
用k倍交叉验证评估的批量标准化模型的准确度分数的盒状和晶须图
有许多方法可以更改模型配置,以探索对基线模型的改进。
两种常用的方法包括改变模型特征提取部分的能力或改变模型分类器部分的能力或功能。也许影响最大的一点是对特性提取器的更改。
我们可以增加模型的特征抽取器部分的深度,遵循一个类似于VGG的模式,在增加过滤器数量的同时,添加更多的卷积和池化层。在这种情况下,我们将添加一个具有64个过滤器的双卷积层,然后是另一个最大池层。
下面列出了带有此更改的define_model()函数的更新版本。
# define cnn model
def define_model():
model = Sequential()
model.add(Conv2D(32, (3, 3), activation='relu', kernel_initializer='he_uniform', input_shape=(28, 28, 1)))
model.add(MaxPooling2D((2, 2)))
model.add(Conv2D(64, (3, 3), activation='relu', kernel_initializer='he_uniform'))
model.add(Conv2D(64, (3, 3), activation='relu', kernel_initializer='he_uniform'))
model.add(MaxPooling2D((2, 2)))
model.add(Flatten())
model.add(Dense(100, activation='relu', kernel_initializer='he_uniform'))
model.add(Dense(10, activation='softmax'))
# compile model
opt = SGD(lr=0.01, momentum=0.9)
model.compile(optimizer=opt, loss='categorical_crossentropy', metrics=['accuracy'])
return model
为完整展示起见,整段代码包括这个改变,如下所示。
# deeper cnn model for mnist
from numpy import mean
from numpy import std
from matplotlib import pyplot
from sklearn.model_selection import KFold
from keras.datasets import mnist
from keras.utils import to_categorical
from keras.models import Sequential
from keras.layers import Conv2D
from keras.layers import MaxPooling2D
from keras.layers import Dense
from keras.layers import Flatten
from keras.optimizers import SGD
# load train and test dataset
def load_dataset():
# load dataset
(trainX, trainY), (testX, testY) = mnist.load_data()
# reshape dataset to have a single channel
trainX = trainX.reshape((trainX.shape[0], 28, 28, 1))
testX = testX.reshape((testX.shape[0], 28, 28, 1))
# one hot encode target values
trainY = to_categorical(trainY)
testY = to_categorical(testY)
return trainX, trainY, testX, testY
# scale pixels
def prep_pixels(train, test):
# convert from integers to floats
train_norm = train.astype('float32')
test_norm = test.astype('float32')
# normalize to range 0-1
train_norm = train_norm / 255.0
test_norm = test_norm / 255.0
# return normalized images
return train_norm, test_norm
# define cnn model
def define_model():
model = Sequential()
model.add(Conv2D(32, (3, 3), activation='relu', kernel_initializer='he_uniform', input_shape=(28, 28, 1)))
model.add(MaxPooling2D((2, 2)))
model.add(Conv2D(64, (3, 3), activation='relu', kernel_initializer='he_uniform'))
model.add(Conv2D(64, (3, 3), activation='relu', kernel_initializer='he_uniform'))
model.add(MaxPooling2D((2, 2)))
model.add(Flatten())
model.add(Dense(100, activation='relu', kernel_initializer='he_uniform'))
model.add(Dense(10, activation='softmax'))
# compile model
opt = SGD(lr=0.01, momentum=0.9)
model.compile(optimizer=opt, loss='categorical_crossentropy', metrics=['accuracy'])
return model
# evaluate a model using k-fold cross-validation
def evaluate_model(model, dataX, dataY, n_folds=5):
scores, histories = list(), list()
# prepare cross validation
kfold = KFold(n_folds, shuffle=True, random_state=1)
# enumerate splits
for train_ix, test_ix in kfold.split(dataX):
# select rows for train and test
trainX, trainY, testX, testY = dataX[train_ix], dataY[train_ix], dataX[test_ix], dataY[test_ix]
# fit model
history = model.fit(trainX, trainY, epochs=10, batch_size=32, validation_data=(testX, testY), verbose=0)
# evaluate model
_, acc = model.evaluate(testX, testY, verbose=0)
print('> %.3f' % (acc * 100.0))
# stores scores
scores.append(acc)
histories.append(history)
return scores, histories
# plot diagnostic learning curves
def summarize_diagnostics(histories):
for i in range(len(histories)):
# plot loss
pyplot.subplot(211)
pyplot.title('Cross Entropy Loss')
pyplot.plot(histories[i].history['loss'], color='blue', label='train')
pyplot.plot(histories[i].history['val_loss'], color='orange', label='test')
# plot accuracy
pyplot.subplot(212)
pyplot.title('Classification Accuracy')
pyplot.plot(histories[i].history['acc'], color='blue', label='train')
pyplot.plot(histories[i].history['val_acc'], color='orange', label='test')
pyplot.show()
# summarize model performance
def summarize_performance(scores):
# print summary
print('Accuracy: mean=%.3f std=%.3f, n=%d' % (mean(scores)*100, std(scores)*100, len(scores)))
# box and whisker plots of results
pyplot.boxplot(scores)
pyplot.show()
# run the test harness for evaluating a model
def run_test_harness():
# load dataset
trainX, trainY, testX, testY = load_dataset()
# prepare pixel data
trainX, testX = prep_pixels(trainX, testX)
# define model
model = define_model()
# evaluate model
scores, histories = evaluate_model(model, trainX, trainY)
# learning curves
summarize_diagnostics(histories)
# summarize estimated performance
summarize_performance(scores)
# entry point, run the test harness
run_test_harness()
运行这个示例可以为交叉验证过程的每个阶段报告模型性能。
每个折叠的分数可能表现比基线有所改善。
> 98.925
> 99.867
> 99.983
> 99.992
> 100.000
创建了一个学习曲线图,在这种情况下,显示模型仍然对问题有很好的拟合,没有明显的过度拟合迹象。这些情节甚至表明,进一步的训练阶段可能会有所帮助。
k次交叉验证过程中深层模型的损失和精度学习曲线
接下来,给出了模型的估计性能,与基线99.678到99.753相比,性能略有改善,标准偏差也略有下降。
Accuracy: mean=99.753 std=0.417, n=5
只要我们有想法,有时间和资源来测试它们,模型改进的过程可能会持续下去。
在某种程度上,必须选择并采用最终的模型配置。在这种情况下,我们将选择更深的模型作为最终模型。
首先,我们将最终确定我们的模型,但要在整个培训数据集上拟合模型,并将模型保存到文件中以供以后使用。然后,我们将加载模型,并在保留测试数据集上评估其性能,以了解所选模型在实践中的实际执行情况。最后,我们将使用保存的模型对单个图像进行预测。
最终模型通常适用于所有可用数据,例如所有列车和测试数据集的组合。
在本教程中,我们有意保留一个测试数据集,以便我们可以估计最终模型的性能,这在实践中是一个好主意。因此,我们将只在训练数据集中拟合我们的模型。
# fit model
model.fit(trainX, trainY, epochs=10, batch_size=32, verbose=0)
一旦拟合,我们可以通过调用模型上的save()函数并传入所选的文件名,将最终模型保存到H5文件中。
# save model
model.save('final_model.h5')
注意,保存和加载keras模型需要在工作站上安装h5py库。
下面列出了在训练数据集上拟合最终深度模型并将其保存到文件中的完整示例。
# save the final model to file
from keras.datasets import mnist
from keras.utils import to_categorical
from keras.models import Sequential
from keras.layers import Conv2D
from keras.layers import MaxPooling2D
from keras.layers import Dense
from keras.layers import Flatten
from keras.optimizers import SGD
# load train and test dataset
def load_dataset():
# load dataset
(trainX, trainY), (testX, testY) = mnist.load_data()
# reshape dataset to have a single channel
trainX = trainX.reshape((trainX.shape[0], 28, 28, 1))
testX = testX.reshape((testX.shape[0], 28, 28, 1))
# one hot encode target values
trainY = to_categorical(trainY)
testY = to_categorical(testY)
return trainX, trainY, testX, testY
# scale pixels
def prep_pixels(train, test):
# convert from integers to floats
train_norm = train.astype('float32')
test_norm = test.astype('float32')
# normalize to range 0-1
train_norm = train_norm / 255.0
test_norm = test_norm / 255.0
# return normalized images
return train_norm, test_norm
# define cnn model
def define_model():
model = Sequential()
model.add(Conv2D(32, (3, 3), activation='relu', kernel_initializer='he_uniform', input_shape=(28, 28, 1)))
model.add(MaxPooling2D((2, 2)))
model.add(Conv2D(64, (3, 3), activation='relu', kernel_initializer='he_uniform'))
model.add(Conv2D(64, (3, 3), activation='relu', kernel_initializer='he_uniform'))
model.add(MaxPooling2D((2, 2)))
model.add(Flatten())
model.add(Dense(100, activation='relu', kernel_initializer='he_uniform'))
model.add(Dense(10, activation='softmax'))
# compile model
opt = SGD(lr=0.01, momentum=0.9)
model.compile(optimizer=opt, loss='categorical_crossentropy', metrics=['accuracy'])
return model
# run the test harness for evaluating a model
def run_test_harness():
# load dataset
trainX, trainY, testX, testY = load_dataset()
# prepare pixel data
trainX, testX = prep_pixels(trainX, testX)
# define model
model = define_model()
# fit model
model.fit(trainX, trainY, epochs=10, batch_size=32, verbose=0)
# save model
model.save('final_model.h5')
# entry point, run the test harness
run_test_harness()
运行此示例后,当前工作目录中将有一个名为“final_model.h5”的1.2兆字节文件。
我们现在可以加载最终模型并在保留的测试数据集上对其进行评估。
如果我们对向项目利益相关者展示所选模型的性能感兴趣,那么我们可能会这样做。
模型可以通过load_model()函数加载。
下面列出了加载保存的模型并在测试数据集中对其进行评估的完整示例。
# evaluate the deep model on the test dataset
from keras.datasets import mnist
from keras.models import load_model
from keras.utils import to_categorical
# load train and test dataset
def load_dataset():
# load dataset
(trainX, trainY), (testX, testY) = mnist.load_data()
# reshape dataset to have a single channel
trainX = trainX.reshape((trainX.shape[0], 28, 28, 1))
testX = testX.reshape((testX.shape[0], 28, 28, 1))
# one hot encode target values
trainY = to_categorical(trainY)
testY = to_categorical(testY)
return trainX, trainY, testX, testY
# scale pixels
def prep_pixels(train, test):
# convert from integers to floats
train_norm = train.astype('float32')
test_norm = test.astype('float32')
# normalize to range 0-1
train_norm = train_norm / 255.0
test_norm = test_norm / 255.0
# return normalized images
return train_norm, test_norm
# run the test harness for evaluating a model
def run_test_harness():
# load dataset
trainX, trainY, testX, testY = load_dataset()
# prepare pixel data
trainX, testX = prep_pixels(trainX, testX)
# load model
model = load_model('final_model.h5')
# evaluate model on test dataset
_, acc = model.evaluate(testX, testY, verbose=0)
print('> %.3f' % (acc * 100.0))
# entry point, run the test harness
run_test_harness()
运行该示例将加载保存的模型,并在保留测试数据集上评估模型。
计算并打印测试数据集上模型的分类精度。在这种情况下,我们可以看到,模型的精确度达到了99.090%,或者仅仅低于1%,这一点也不差,并且相当接近于估计的99.753%,标准偏差约为0.5%(例如,分数的99%)。
> 99.090
进行预测
我们可以使用我们保存的模型对新图像进行预测。
该模型假定新图像是灰度图像,它们已经对齐,因此一个图像包含一个居中的手写数字,并且图像的大小与大小为28×28像素的正方形。
下面是从MNIST测试数据集中提取的图像。可以将其保存在当前工作目录中,文件名为“sample_image.png”。
手写数字样例
我们将假装这是一个全新的、看不见的图像,以所需的方式进行准备,并了解如何使用我们保存的模型来预测图像所代表的整数(例如,我们期望“7”)。
首先,我们可以加载图像,强制它为灰度格式,并强制大小为28×28像素。然后可以调整加载图像的大小,使其具有单个通道,并在数据集中表示单个样本。load_image()函数实现了这一点,并将返回已加载的图像,以便进行分类。
重要的是,像素值的准备方式与在拟合最终模型时为训练数据集准备的像素值相同,在这种情况下,是标准化的。
# load and prepare the image
def load_image(filename):
# load the image
img = load_img(filename, grayscale=True, target_size=(28, 28))
# convert to array
img = img_to_array(img)
# reshape into a single sample with 1 channel
img = img.reshape(1, 28, 28, 1)
# prepare pixel data
img = img.astype('float32')
img = img / 255.0
return img
接下来,我们可以像前一节一样加载模型,并调用predict_classes()函数来预测图像所代表的数字。
# predict the class
digit = model.predict_classes(img)
整段例子列在下面。
# make a prediction for a new image.
from keras.preprocessing.image import load_img
from keras.preprocessing.image import img_to_array
from keras.models import load_model
# load and prepare the image
def load_image(filename):
# load the image
img = load_img(filename, grayscale=True, target_size=(28, 28))
# convert to array
img = img_to_array(img)
# reshape into a single sample with 1 channel
img = img.reshape(1, 28, 28, 1)
# prepare pixel data
img = img.astype('float32')
img = img / 255.0
return img
# load an image and predict the class
def run_example():
# load the image
img = load_image('sample_image.png')
# load model
model = load_model('final_model.h5')
# predict the class
digit = model.predict_classes(img)
print(digit[0])
# entry point, run the example
run_example()
运行该示例首先加载并准备图像,加载模型,然后正确预测加载的图像代表数字“7”。
7
延伸
本节列出了一些扩展您可能希望探索的教程的想法。
调整像素比例。探索与基线模型相比,替代像素缩放方法如何影响模型性能,包括居中和标准化。
调整学习速度。与基线模型(如0.001和0.0001)相比,探索不同的学习率如何影响模型性能。
调整模型深度。探索与基线模型相比,向模型中添加更多层是如何影响模型性能的,例如,在模型的分类器部分中添加另一个卷积和池层块或另一个密集层。
总结
在这个教程中,您学会了如何从头开始为手写数字分类开发卷积神经网络。
具体来说,你学到了:
如何开发测试工具以开发对模型的稳健评估并为分类任务建立性能基线。
如何探索基线模型的扩展,以提高学习和模型容量。
如何开发最终模型,评估最终模型的性能,并使用它来预测新图像。
原文题目:
How to Develop a Convolutional Neural Network From Scratch for MNIST Handwritten Digit Classification
原文地址:
https://machinelearningmastery.com/how-to-develop-a-convolutional-neural-network-from-scratch-for-mnist-handwritten-digit-classification/
编辑:王菁
校对:林亦霖
译者简介
张睿毅,北京邮电大学大二物联网在读。我是一个爱自由的人。在邮电大学读第一年书我就四处跑去蹭课,折腾整一年惊觉,与其在当下焦虑,不如在前辈中沉淀。于是在大二以来,坚持读书,不敢稍歇。资本主义国家的科学观不断刷新我的认知框架,同时因为出国考试很早出分,也更早地感受到自己才是那个一直被束缚着的人。太多真英雄在社会上各自闪耀着光芒。这才开始,立志终身向遇到的每一个人学习。做一个纯粹的计算机科学里面的小学生。
翻译组招募信息
工作内容:将选取好的外文前沿文章准确地翻译成流畅的中文。如果你是数据科学/统计学/计算机专业的留学生,或在海外从事相关工作,或对自己外语水平有信心的朋友,数据派翻译组欢迎你们加入!
你能得到:提高对于数据科学前沿的认知,提高对外文新闻来源渠道的认知,海外的朋友可以和国内技术应用发展保持联系,数据派团队产学研的背景为志愿者带来好的发展机遇。
其他福利:和来自于名企的数据科学工作者,北大清华以及海外等名校学生共同合作、交流。
点击文末“阅读原文”加入数据派团队~
转载须知
如需转载,请在开篇显著位置注明作者和出处(转自:数据派THU ID:DatapiTHU),并在文章结尾放置数据派醒目二维码。有原创标识文章,请发送【文章名称-待授权公众号名称及ID】至联系邮箱,申请白名单授权并按要求编辑。
发布后请将链接反馈至联系邮箱(见下方)。未经许可的转载以及改编者,我们将依法追究其法律责任。
点击“阅读原文”拥抱组织