Microsoft Cognitive Toolkit 简明教程

CNTK - Convolutional Neural Network

在本章中,让我们研究如何在 CNTK 中构建卷积神经网络 (CNN)。

Introduction

卷积神经网络 (CNN) 也由神经元构成,具有可学习的权重和偏差。这就是为什么以这种方式,它们类似于普通神经网络 (NN)。

如果我们对普通 NN 的工作原理进行回顾,每个神经元会接收一个或多个输入,并获取加权和,它会通过一个激活函数,以生成最终输出。在这里,会出现一个问题,如果 CNN 和普通 NN 具有如此多的相似性,那么是什么让这两个网络彼此不同?

它们的不同之处在于对输入数据和层类型的处理?输入数据的结构在普通神经网络中被忽略,并且在将其输入网络之前,所有数据都被转换成 1 维数组。

但是,卷积神经网络架构可以考虑图像的二维结构,对其进行处理并允许其提取特定于图像的属性。此外,CNN 具有一个或多个卷积层和池化层,这是 CNN 的主要构建块。

这些层后面跟着一个或多个完全连接的层,就像标准的多层神经网络中一样。因此,我们可以将 CNN 视为完全连接网络的一种特殊情况。

Convolutional Neural Network (CNN) architecture

CNN 的架构基本上是一系列层,将 3 维(即图像卷的宽度、高度和深度)转换为 3 维输出卷。这里需要注意的一个重要之处是,当前层中的每个神经元都连接到前一层输出的一个小块 patch,这就像在输入图像上覆盖一个 N*N 滤波器。

它使用 M 个滤波器,这些滤波器基本上是特征提取器,用于提取诸如边缘、角等特征。以下是用于构建卷积神经网络 (CNN) 的层* [INPUT-CONV-RELU-POOL-FC]*−

  1. * INPUT*−顾名思义,该层保存原始像素值。原始像素值是指图像的数据原样。例如,INPUT [64×64×3] 是一个宽度为 64、高度为 64、深度为 3 的 3 通道 RGB 图像。

  2. * CONV*−该层是 CNN 的构建块之一,因为大多数计算都是在此层中进行的。例如 - 如果我们在上面提到的 INPUT [64×64×3] 上使用 6 个滤波器,则可能会产生 [64×64×6] 的卷。

  3. RELU −也称为线性整流单元层,它对前一层的输出应用一个激活函数。换句话说,RELU 会给网络添加非线性。

  4. * POOL*−该层(即池化层)是 CNN 的另一个构建块。该层的主要任务是下采样,这意味着它独立于输入的每个切片进行操作,并在空间上调整其大小。

  5. FC −它被称为完全连接层,更具体地说是输出层。它用于计算输出类分数,并且输出结果是大小为 1*1*L 的卷,其中 L 是对应于类分数的数字。

下面的图表展示了 CNN 的典型架构−

cnns architecture

Creating CNN structure

我们已经了解了 CNN 的架构和基础知识,现在我们准备使用 CNTK 构建卷积网络。在这里,我们将首先了解如何构建 CNN 的结构,然后我们将了解如何训练它的参数。

最后我们将了解,如何通过使用各种不同的层设置来更改其结构来改进神经网络。我们将使用 MNIST 图像数据集。

因此,首先让我们创建一个 CNN 结构。通常,当我们构建一个用于识别图像中模式的 CNN 时,我们进行以下操作:

  1. 我们使用组合的卷积和池化层。

  2. 在网络的末尾有一个或多个隐藏层。

  3. 最后,我们使用 softmax 层完成网络,用于分类目的。

借助以下步骤,我们可以构建网络结构:

Step 1 - 首先,我们需要导入 CNN 所需的层。

from cntk.layers import Convolution2D, Sequential, Dense, MaxPooling

Step 2 − 接下里,我们需要导入 CNN 的激活函数。

from cntk.ops import log_softmax, relu

Step 3 − 为稍后初始化卷积层,我们需要按如下方式导入 glorot_uniform_initializer

from cntk.initializer import glorot_uniform

Step 4 − 接下里,为了创建输入变量,导入 input_variable 函数。并导入 default_option 函数,以使 NN 配置更加简单。

from cntk import input_variable, default_options

Step 5 − 现要存储输入图像,创建一个新的 input_variable 。它包含三个通道,分别是红色、绿色和蓝色。它的大小为 28 x 28 像素。

features = input_variable((3,28,28))

Step 6 − 接下里,我们需要创建另一个 input_variable ,以存储要预测的标签。

labels = input_variable(10)

Step 7 − 现在,我们需要为 NN 创建 default_option 。并需要将 glorot_uniform 用作初始化函数。

with default_options(initialization=glorot_uniform, activation=relu):

Step 8 − 接下里,为了设置 NN 的结构,我们需要创建一个新的 Sequential 层集。

Step 9 − 现在,我们需要在 Sequential 层集中添加一个 Convolutional2D 层,该层具有 filter_shape 为 5 和 strides 设置为 1 。此外,启用填充,以便对图像进行填充以保留原始尺寸。

model = Sequential([
Convolution2D(filter_shape=(5,5), strides=(1,1), num_filters=8, pad=True),

Step 10 − 现在是添加一个 MaxPooling 层的时候了,其中 filter_shape 为 2,而 strides 设置为 2,以将图像压缩一半。

MaxPooling(filter_shape=(2,2), strides=(2,2)),

Step 11 − 现在,如我们在步骤 9 中所做的那样,我们需要添加另一个 Convolutional2D 层,其 filter_shape 为 5 和 strides 设置为 1,使用 16 个滤波器。此外,启用填充,以便保留前一池化层产生的图像大小。

Convolution2D(filter_shape=(5,5), strides=(1,1), num_filters=16, pad=True),

Step 12 − 现在,如我们在步骤 10 中所做的那样,再添加一个 MaxPooling *layer with a *filter_shape 的 3 和 strides 设置为 3,以将图像缩小到三分之一。

MaxPooling(filter_shape=(3,3), strides=(3,3)),

Step 13 − 最后,添加一个密集层,该层具有十个神经元,以表示网络可以预测的 10 个可能的类别。为了将网络转换为分类模型,请使用 log_siftmax 激活函数。

Dense(10, activation=log_softmax)
])

Complete Example for creating CNN structure

from cntk.layers import Convolution2D, Sequential, Dense, MaxPooling
from cntk.ops import log_softmax, relu
from cntk.initializer import glorot_uniform
from cntk import input_variable, default_options
features = input_variable((3,28,28))
labels = input_variable(10)
with default_options(initialization=glorot_uniform, activation=relu):
model = Sequential([
   Convolution2D(filter_shape=(5,5), strides=(1,1), num_filters=8, pad=True),
MaxPooling(filter_shape=(2,2), strides=(2,2)),
   Convolution2D(filter_shape=(5,5), strides=(1,1), num_filters=16, pad=True),
MaxPooling(filter_shape=(3,3), strides=(3,3)),
Dense(10, activation=log_softmax)
])
z = model(features)

Training CNN with images

当我们创建了网络结构后,就该对该网络进行训练了。但是在开始训练我们的网络之前,我们需要设置最小批次源,这是因为使用图像的 NN 训练需要比大多数计算机拥有的更多的内存。

我们在前几节中已经创建了最小批次源。以下是设置两个最小批次源的 Python 代码:

当我们具有 create_datasource 函数时,我们现在可以创建两个独立的数据源(一个训练和一个测试)来训练模型。

train_datasource = create_datasource('mnist_train')
test_datasource = create_datasource('mnist_test', max_sweeps=1, train=False)

现在,当我们准备好了图像,我们就可以开始训练我们的 NN 了。正如我们在前几节中所做的那样,我们可以对损失函数使用训练方法来启动训练。以下是此代码:

from cntk import Function
from cntk.losses import cross_entropy_with_softmax
from cntk.metrics import classification_error
from cntk.learners import sgd
@Function
def criterion_factory(output, targets):
loss = cross_entropy_with_softmax(output, targets)
metric = classification_error(output, targets)
return loss, metric
loss = criterion_factory(z, labels)
learner = sgd(z.parameters, lr=0.2)

借助之前的代码,我们已经为 NN 设置了损失和学习者。以下代码将训练和验证 NN:

from cntk.logging import ProgressPrinter
from cntk.train import TestConfig
progress_writer = ProgressPrinter(0)
test_config = TestConfig(test_datasource)
input_map = {
   features: train_datasource.streams.features,
   labels: train_datasource.streams.labels
}
loss.train(train_datasource,
     max_epochs=10,
     minibatch_size=64,
     epoch_size=60000,
        parameter_learners=[learner],
     model_inputs_to_streams=input_map,
     callbacks=[progress_writer, test_config])

Complete Implementation Example

from cntk.layers import Convolution2D, Sequential, Dense, MaxPooling
from cntk.ops import log_softmax, relu
from cntk.initializer import glorot_uniform
from cntk import input_variable, default_options
features = input_variable((3,28,28))
labels = input_variable(10)
with default_options(initialization=glorot_uniform, activation=relu):
model = Sequential([
   Convolution2D(filter_shape=(5,5), strides=(1,1), num_filters=8, pad=True),
MaxPooling(filter_shape=(2,2), strides=(2,2)),
   Convolution2D(filter_shape=(5,5), strides=(1,1), num_filters=16, pad=True),
MaxPooling(filter_shape=(3,3), strides=(3,3)),
Dense(10, activation=log_softmax)
])
z = model(features)
import os
from cntk.io import MinibatchSource, StreamDef, StreamDefs, ImageDeserializer, INFINITELY_REPEAT
import cntk.io.transforms as xforms
def create_datasource(folder, train=True, max_sweeps=INFINITELY_REPEAT):
   mapping_file = os.path.join(folder, 'mapping.bin')
   image_transforms = []
   if train:
    image_transforms += [
     xforms.crop(crop_type='randomside', side_ratio=0.8),
     xforms.scale(width=28, height=28, channels=3, interpolations='linear')
]
   stream_definitions = StreamDefs(
   features=StreamDef(field='image', transforms=image_transforms),
    labels=StreamDef(field='label', shape=10)
)
   deserializer = ImageDeserializer(mapping_file, stream_definitions)
return MinibatchSource(deserializer, max_sweeps=max_sweeps)
train_datasource = create_datasource('mnist_train')
test_datasource = create_datasource('mnist_test', max_sweeps=1, train=False)
from cntk import Function
from cntk.losses import cross_entropy_with_softmax
from cntk.metrics import classification_error
from cntk.learners import sgd
@Function
def criterion_factory(output, targets):
   loss = cross_entropy_with_softmax(output, targets)
   metric = classification_error(output, targets)
return loss, metric
loss = criterion_factory(z, labels)
learner = sgd(z.parameters, lr=0.2)
from cntk.logging import ProgressPrinter
from cntk.train import TestConfig
progress_writer = ProgressPrinter(0)
test_config = TestConfig(test_datasource)
input_map = {
   features: train_datasource.streams.features,
   labels: train_datasource.streams.labels
}
loss.train(train_datasource,
     max_epochs=10,
     minibatch_size=64,
     epoch_size=60000,
        parameter_learners=[learner],
     model_inputs_to_streams=input_map,
     callbacks=[progress_writer, test_config])

Output

-------------------------------------------------------------------
average  since  average  since  examples
loss     last   metric   last
------------------------------------------------------
Learning rate per minibatch: 0.2
142      142      0.922   0.922    64
1.35e+06 1.51e+07 0.896   0.883    192
[………]

Image transformations

正如我们所看到的,训练用于图像识别的 NN 非常困难,并且它们还需要大量的数据进行训练。另一个问题是,它们往往在训练时使用的图像上过度拟合。让我们通过一个例子来说明,当我们有处于直立位置的面部照片时,我们的模型将难以识别向其他方向旋转的面部。

为了克服此类问题,我们可以使用图像增强,而 CNTK 在为图像创建最小批次源时支持特定变换。我们可以使用如下转换:

  1. 只需几行代码,我们就可以随机裁剪用于训练的图像。

  2. 我们还可以使用缩放和颜色。

让我们在以下 Python 代码的帮助下看看我们如何通过在用于创建小批量源的函数中包含裁剪变换来更改变换列表。

import os
from cntk.io import MinibatchSource, StreamDef, StreamDefs, ImageDeserializer, INFINITELY_REPEAT
import cntk.io.transforms as xforms
def create_datasource(folder, train=True, max_sweeps=INFINITELY_REPEAT):
   mapping_file = os.path.join(folder, 'mapping.bin')
   image_transforms = []
   if train:
   image_transforms += [
     xforms.crop(crop_type='randomside', side_ratio=0.8),
xforms.scale(width=28, height=28, channels=3, interpolations='linear')
]
   stream_definitions = StreamDefs(
   features=StreamDef(field='image', transforms=image_transforms),
labels=StreamDef(field='label', shape=10)
)
   deserializer = ImageDeserializer(mapping_file, stream_definitions)
return MinibatchSource(deserializer, max_sweeps=max_sweeps)

借助上面的代码,我们可以增强函数来包括一组图像变换,以便在训练时可以随机裁剪图像,这样我们可以获得更多图像变化。