CNTK - 卷积神经网络


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

介绍

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

如果我们回想一下普通神经网络的工作原理,每个神经元都会接收一个或多个输入,进行加权和,然后通过激活函数产生最终输出。这里,问题出现了:如果 CNN 和普通 NN 有如此多的相似之处,那么是什么让这两个网络彼此不同呢?

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

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

这些层后面是一个或多个全连接层,如标准多层神经网络中一样。因此,我们可以将 CNN 视为全连接网络的特例。

卷积神经网络 (CNN) 架构

CNN 的架构基本上是将 3 维(即图像体积的宽度、高度和深度)转换为 3 维输出体积的层列表。这里需要注意的一个要点是,当前层中的每个神经元都连接到前一层输出的一小部分,这就像在输入图像上叠加一个 N*N 滤波器。

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

  • 输入- 顾名思义,该层保存原始像素值。原始像素值是指图像的原始数据。例如,INPUT [64×64×3] 是宽度为 64、高度为 64、深度为 3 的 3 通道 RGB 图像。

  • CONV - 该层是 CNN 的构建块之一,因为大部分计算都是在该层中完成的。示例 - 如果我们在上述输入 [64×64×3] 上使用 6 个滤波器,则可能会产生体积 [64×64×6]。

  • RELU -也称为修正线性单元层,它将激活函数应用于前一层的输出。以其他方式,可以通过 RELU 将非线性添加到网络中。

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

  • FC - 它被称为全连接层或更具体地说是输出层。它用于计算输出类别分数,结果输出是大小为 1*1* L的体积 ,其中 L 是与类别分数对应的数字。

下图代表了 CNN 的典型架构:

美国有线电视新闻网架构

创建 CNN 结构

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

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

因此,首先让我们创建一个 CNN 结构。一般来说,当我们构建一个 CNN 来识别图像中的模式时,我们会执行以下操作:

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

  • 网络末端有一个或多个隐藏层。

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

在以下步骤的帮助下,我们可以构建网络结构 -

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

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

步骤 2 - 接下来,我们需要导入 CNN 的激活函数。

from cntk.ops import log_softmax, relu

步骤 3 - 之后,为了稍后初始化卷积层,我们需要导入glorot_uniform_initializer ,如下所示 -

from cntk.initializer import glorot_uniform

步骤 4 - 接下来,要创建输入变量,请导入input_variable函数。并导入default_option函数,使NN的配置更容易一些。

from cntk import input_variable, default_options

步骤 5 - 现在要存储输入图像,创建一个新的input_variable。它将包含三个通道,即红色、绿色和蓝色。它的大小为 28 x 28 像素。

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

步骤 6 - 接下来,我们需要创建另一个input_variable来存储要预测的标签。

labels = input_variable(10)

步骤 7 - 现在,我们需要为神经网络创建default_option 。并且,我们需要使用glorot_uniform作为初始化函数。

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

步骤 8 - 接下来,为了设置神经网络的结构,我们需要创建一个新的顺序层集。

步骤 9 - 现在我们需要在Sequential层集中添加一个Filter_shape为 5 且步长设置为1的Convolutional2D层。另外,启用填充,以便填充图像以保留原始尺寸。

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

步骤 10 - 现在是时候添加一个MaxPooling层,filter_shape为 2,步幅设置为 2,将图像压缩一半。

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

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

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

步骤 12 - 现在,正如我们在步骤 10 中所做的那样,添加另一个MaxPooling层,其filter_shape为 3,步幅设置为 3,以将图像缩小到三分之一。

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

步骤 13 - 最后,添加一个包含 10 个神经元的密集层,用于网络可以预测的 10 个可能的类别。为了将网络转变为分类模型,请使用log_siftmax激活函数。

Dense(10, activation=log_softmax)
])

创建 CNN 结构的完整示例

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)

用图像训练 CNN

当我们创建了网络结构后,就该训练网络了。但在开始训练网络之前,我们需要设置小批量源,因为训练处理图像的神经网络需要比大多数计算机更多的内存。

我们已经在前面的部分中创建了小批量源。以下是设置两个小批量源的 Python 代码 -

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

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])

完整的实施示例

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])

输出

-------------------------------------------------------------------
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
[………]

图像转换

正如我们所看到的,训练用于图像识别的神经网络很困难,而且还需要大量数据来训练。另一个问题是,它们往往会过度拟合训练期间使用的图像。让我们看一个例子,当我们有直立位置的脸部照片时,我们的模型将很难识别沿另一个方向旋转的脸部。

为了克服这个问题,在为图像创建小批量源时,我们可以使用图像增强,并且 CNTK 支持特定的变换。我们可以使用以下几种变换 -

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

  • 我们也可以使用比例和颜色。

让我们在以下 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)

在上面代码的帮助下,我们可以增强该函数以包含一组图像变换,这样,当我们训练时,我们可以随机裁剪图像,从而获得更多的图像变化。