这篇教程展示了CNTK中一些比较高级的特性,目标读者是完成了之前教程或者是使用过其他机器学习组件的人。如果你是完完全全的新手,请先看我们之前的十多期教程。

欢迎来到CNTK。深度神经网络正在重新定义计算机编程。在命令式编程、函数式变成和申明式变成之外,我们有有了一种完全不同的编程方式,这种方式是有效的从数据中学习程序。

CNTK是微软产品部门在所有产品中创建深度模型的首选工具,这些产品包含语音识别、机器翻译以及在必应搜索排序中使用的海量图片分类服务等。

本篇教程是CNTK的一个进阶向导,主要的读者是有过其他深度神经网络经验,但是没接触过CNTK的人。本文主要以示例来展示用CNTK进行深度学习的基本步骤。本教程不是一个完整的API说明文档,但文中的链接会指引读者到相关的文档和示例教程,以获取更多信息。

在训练深度模型时,你需要定义你的模型结构、准备你的数据,将它们传入CNTK、训练模型、评估精度最后使用他们。

本教程结构如下:

  • 定义模型结构
    • CNTK编程模型:用函数对象表示的神经网络
    • CNTK数据模型:张量和张量组
    • 第一个CNTK神经网络:逻辑回归
    • 第二个神经网络:MNIST数字识别
    • 图API:再次进行MNIST数字识别
  • 传入数据
    • 减小数据集以便适应内存容量:numpy/scipy数组。
    • 处理大数据:使用MinibatchSource
    • 填鸭式数据:自定义的取样包循环
  • 训练
    • 分布式训练
    • 记录日志
    • 基于交叉验证的训练组件
    • 最终评估
  • 使用模型
    • 用于Python
    • 用于C++和C#
    • 用于你自己的网络服务
    • 用于Azure(微软的云平台)网络服务
  • 总结

为了运行本教程的代码,你需要安装CNTK2.0,最理想的是显卡支持CUDA(没有显卡的深度学习一点都不好玩)。

先从引入我们需要的库开始。

from __future__ import print_function
import cntk
import numpy as np
import scipy.sparse
import cntk.tests.test_utils

 # (only needed for our build system)
cntk.tests.test_utils.set_device_from_pytest_env()
# fix the random seed so that LR examples are repeatable
cntk.cntk_py.set_fixed_random_seed(1) 

from IPython.display import Image
import matplotlib.pyplot

matplotlib.pyplot.rcParams['figure.figsize'] = (40,40)

定义模型结构

让我们开始。接下来我们会介绍CNTK的数据模型和编程模型——所有的神经网络都是函数对象(一个神经网络可以被叫做一个函数,但是他里面也有很多状态、权重和一些其他参数,在训练的时候去调整改变)。我们将在接下来使用CNTK基础API进行逻辑回归和MNIST数字识别的过程中用到这些。最后,CNTK还有一些底层图API,我们在一个例子中使用它。

CNTK编程模型:用函数对象表示的神经网络

在CNTK里面,一个神经网络就是一个函数对象。一方面,CNTK里面的一个神经网络就是一个函数,你可以把数据作为参数来调用他。另一方面,一个神经网络里面包含很多可以学习的参数,他们可以像对象的成员一样被访问。复杂的神经网络可以由简单的神经网络组合而成,就类似与简单的神经网络中的网络层一样。这种函数对象的方式是机器学习框架中通行的做法,其他框架(比如Keras, Chainer, Dynet, Pytorch和Sonnet等)也有类似的用法。

接下来使用伪代码描绘了函数对象,这段代码使用了全连接层(在CNTK中叫Dense)的例子。

# *Conceptual* numpy implementation of CNTK's Dense layer (simplified, e.g. no back-prop)
def Dense(out_dim, activation):
    # create the learnable parameters
    b = np.zeros(out_dim)
    # input dimension is unknown
    W = np.ndarray((0,out_dim)) 
    # define the function itself
    def dense(x):
    # first call: reshape and initialize W
        if len(W) == 0: 
            W.resize((x.shape[-1], W.shape[-1]), refcheck=False)
            W[:] = np.random.randn(*W.shape) * 0.05
        return activation(x.dot(W) + b)
    # return as function object: can be called & holds parameters as members
    dense.W = W
    dense.b = b
    return dense

# create the function object
d = Dense(5, np.tanh)  
# apply it like a function
y = d(np.array([1, 2]))  
# access member like an object
W = d.W                  
print('W =', d.W)

强调一遍,这只是伪代码。在实际使用时,CNTK里面的函数对象不是基于numpy数组的,相反,与其他的深度学习组件类似,其内部是使用C++写成的图结构,用于编码算法。在真实代码里面:

d = Dense(5, np.tanh)

这句仅仅构成了一个图,这句:

y = d(np.array([1, 2]))

是把数据传入图执行引擎。

图结构继承了Python的Function类,公开了必要的接口以便其他Python函数能够调用他以及访问他的成员(比如W和b)。

函数对象是CNTK通过约定俗成的一些规则将不同的神经网络计算操作进行了抽象,其操作包含以下内容:

  • 没有需要学习的参数的基本操作,比如sigmoid()
  • 神经网络层,比如Dense(), Embedding(), Convolution()等等。神经网络层将一系列的输入值映射到一些列的输出值,当然也伴随着需要学习的参数。
  • 递归函数,比如LSTM(), GRU(), RNNStep()。递归函数将之前的状态和一个新的输入数据映射到一个新的状态。
  • 成本函数和度量函数,比如cross_entropy_with_softmax(), binary_cross_entropy(), squared_error(), classification_error()等等。在CNTK里面,成本函数和量度函数就仅仅是个函数,与其他的CNTK函数有可以有一个或者多个输出值不同,成本函数和亮度函数只能有一个输出值。注意,成本函数不一定得输出一个标量:如果成本函数输出值不是标量,CNTK会自动的把矢量里面的所有值求和,当作成本只。当然这个操作在以后实际使用中是可以自己去重写的。
  • 模型。模型是用户定义的,他定义了我们需要预测或者打分的属性,也是最后我们拿来使用的东西。
  • 准则函数:准则函数将输入数据的属性、标签值隐射到成本函数和度量函数。训练器通过随机梯度下降算法优化成本只,记录下度量值。度量值在每轮训练可能会一样,但是成本只应该要变小。

高阶网络层将简单对象组合成更加复杂的对象,包括:

  • 网络层堆叠,比如Sequential(), For()
  • 循环,比如Recurrence(), Fold(), UnfoldFrom()等。

神经网络经常使用CNTK中定义好的函数(比如之前提到过的各种网络层)来定义,然后使用Sequential()方法来组合。当然,用户也可以使用Python表达式写他们自己的函数,只要他们是由CNTK的操作和CNTK的数据类型组成的。最终这些Python表达式会通过调用Function()转化成CNTK内部的表达方式。这些表达式可以通过使用@Function装饰器写成多行函数。

如果有些操作不能够使用原生CNTK实现,你也可以用Python或者C++写自己的网络层来扩展CNTK。这是一个比较高级的做法,我们现在不需要纠结这个,仅仅知道就可以了,以备以后要用。

最后,CNTK的函数对象允许参数共享。如果你在很多不同的地方调用相同的函数对象,则所有调用将自动的共享需要学习的参数。如果想避免参数共享,你只需要创建两个不同的函数对象就可以了。

总之,函数对象是CNTK为了方便的定义神经网络网络模型、实现参数共享和训练对象进行的一个抽象。

你也可以通过底层的图操作来直接定义神经网络,就和其他机器学习组件一样。当然你也可以自由的组合这两种风格,来定义自己的神经网络,这部分我们接下来会深入讨论。

CNTK数据模型:张量组

CNTK可以操作两种类型的数据:

  • 张量(N维数组),密集的或稀疏的
  • 张量组

他们的区别在于,张量的大小在运算的过程中是固定的,然而张量组的组长度与数据有关。在CNTK中我们使用轴来表示numpy数组的维度,比如一个大小是(7,10,6)的张量有三个轴或者说三个维度。张量的所有轴都是固定的,但是张量组有一个动态轴,也就是轴的长度是可变的。

分类数据通常用一位有效码的稀疏张量表示,比如所有的元素都是0,只有在代表所在类的那位元素的值是1。这可以让向量化和成本函数写成统一风格的矩阵产品。

将一个CNTK函数打印出来会输出类似如下格式的文字:
Operation(Sequence[Tensor[shape]], other arguments) -> Tensor[shape]

当一个操作是一个组合操作时,这个函数表示了这个操作下隐藏的整个图,但是打印出来仅会现实最后一个操作。这个图具有一定量的特定数据类型的输入。当你打印一个函数时,你可能会注意到不会显示取样包的大小,因为CNTK有意对用户隐藏了这部分数据,我们希望用户以张量和张量组的角度去思考我们的神经网络,把取样包这种细节留给CNTK。与其他机器学习组件不同,CNTK能够将不同长度的张量租打包成取样包,自动的解决其中需要处理的包装和填充问题,而不需要使用类似’bucketing’的技术。我们将动态轴从固定轴中分离出来的原因是因为通常只有很少量的操作会影响到动态轴。默认情况下,我们只会想要对取样包中的样本或者序列中的要素进行操作,只有极少数情况才会考虑到轴的事情。

第一个CNTK神经网络:简单逻辑回归

让我们通过一个简单的逻辑回归示例来理解上面说的内容。打个比方,我们创造一个模拟二维正态分布的点数据集,这些店需要被分成两类。注意CNTK需要输入数据的标签数据使用一位有效码。

# classify 2-dimensional data
input_dim_lr = 2    
# into one of two classes
num_classes_lr = 2  

# This example uses synthetic data from normal distributions,
# which we generate in the following.
#  X_lr[corpus_size,input_dim] - input data
#  Y_lr[corpus_size]           - labels (0 or 1), one-hot-encoded
np.random.seed(0)
def generate_synthetic_data(N):
    Y = np.random.randint(size=N, low=0, high=num_classes_lr)  # labels
    X = (np.random.randn(N, input_dim_lr)+3) * (Y[:,None]+1)   # data
    # Our model expects float32 features, and cross-entropy
    # expects one-hot encoded labels.
    Y = scipy.sparse.csr_matrix((np.ones(N,np.float32), (range(N), Y)), shape=(N, num_classes_lr))
    X = X.astype(np.float32)
    return X, Y
X_train_lr, Y_train_lr = generate_synthetic_data(20000)
X_test_lr,  Y_test_lr  = generate_synthetic_data(1024)
print('data =\n', X_train_lr[:4])
print('labels =\n', Y_train_lr[:4].todense())

我们现在定义模型函数。模型函数将输入的数据映射到预测出来的类别值,这也是我们的训练最终需要得到的结果。在本例中,我们使用最简单的模型:逻辑回归。

model_lr_factory = cntk.layers.Dense(num_classes_lr, activation=None)
x = cntk.input_variable(input_dim_lr)
y = cntk.input_variable(num_classes_lr, is_sparse=True)
model_lr = model_lr_factory(x)

接下来,我们定义准则函数。准这函数相当于是训练器在优化模型时的缰绳:他把输入数据和输入标签与成本和度量值映射起来。成本选用交叉熵成本函数,使用随机梯度下降算法进行优化。CNTK中还有一个cross_entropy_with_softmax()函数,将softmax()函数用于神经网络的输出值,因为交叉熵成本函数的输入值应该是概率。也因此我们再不需要将softmax()函数用于我们的模型了。最后我们计算分类的错误率来当作度量值。

我们使用Python代码定义准则函数,然后将其转换成Function对象。使用Function对象是可以使用如下的表达式Function(lambda x, y:expression of x and y),类似与Keras里面的Lambda()函数。为了避免多次评估模型,我们使用Python函数加上装饰器的方式来定义准则函数,使用装饰器@Function也是向CNTK声明输入数据类型的好方式。

@cntk.Function
def criterion_lr_factory(data, label_one_hot):
    # apply model. Computes a non-normalized log probability for every output class.
    z = model_lr_factory(data) 
    # applies softmax to z under the hood
    loss = cntk.cross_entropy_with_softmax(z, label_one_hot) 
    metric = cntk.classification_error(z, label_one_hot)
    return loss, metric
criterion_lr = criterion_lr_factory(x, y)
print('criterion_lr:', criterion_lr)

这个装饰器会将Python函数编译成CNTK内部图的表达方式。所以上述代码打印出来的结果不是一个Python函数,而是一个CNTK Function对象。

现在我们准备好训练我们的模型了。

learner = cntk.sgd(model_lr.parameters,
                   cntk.learning_rate_schedule(0.1, cntk.UnitType.minibatch))
progress_writer = cntk.logging.ProgressPrinter(0)

criterion_lr.train((X_train_lr, Y_train_lr), parameter_learners=[learner],
                   callbacks=[progress_writer])

print(model_lr.W.value) # peek at updated W

结果:

 average      since    average      since      examples
    loss       last     metric       last              
 ------------------------------------------------------
Learning rate per minibatch: 0.1
     3.58       3.58      0.562      0.562            32
     1.61      0.629      0.458      0.406            96
      1.1      0.715      0.464      0.469           224
     0.88      0.688      0.454      0.445           480
    0.734      0.598      0.427      0.402           992
    0.637      0.543      0.351      0.277          2016
    0.541      0.447      0.257      0.165          4064
     0.45      0.359      0.186      0.115          8160
    0.366      0.284      0.137     0.0876         16352
[[-1.25055134 -0.53687745]
 [-0.99188197 -0.30085728]]

学习器/训练器是让模型更新参数的对象。除了代码中使用的sgd()之外,我们还可以选择 momentum_sgd()和 adam()。progress_writer函数是一个内置的日志记录函数,他打印出来了我们结果中显示的那些数据,当然他也可以被自定义的日志记录函数或者内置的TensorBoardProgressWriter函数来用张量面板可视化训练过程。

train()函数将我们的数据(X_train_lr, Y_train_lr)以一个又一个取样包的形式传入模型,然后更新模型参数,这些数据和criterion_lr()函数的参数形式一致。

现在我们使用测试数据集来看我们之前的训练工作做得怎样:

test_metric_lr = criterion_lr.test((X_test_lr, Y_test_lr),
                                   callbacks=[progress_writer]).metric

最后,让我们运行几个样本看看我们的模型表现的怎样。因为虽然准则函数知道输入数据类型,但是模型并不知道,所以我们需要告诉他:

model_lr = model_lr_factory(x)
print('model_lr:', model_lr)

现在我们可以像调用Python函数一样调用模型了。

z = model_lr(X_test_lr[:20])
print("Label    :", [label.todense().argmax() for label in Y_test_lr[:20]])
print("Predicted:", [z[i,:].argmax() for i in range(len(z))])

第二个CNTK神经网络:MNIST数字识别

让我们使用真是的案例再来一遍,这个真实案例就是MNIST数字识别。(MNIST介绍内容略)我们可以使用CNTK里的功能更更简洁的实现MNIST数字识别。

input_shape_mn = (28, 28)  # MNIST digits are 28 x 28
num_classes_mn = 10        # classify as one of 10 digits

# Fetch the MNIST data. Best done with scikit-learn.
try:
    from sklearn import datasets, utils
    mnist = datasets.fetch_mldata("MNIST original")
    X, Y = mnist.data / 255.0, mnist.target
    X_train_mn, X_test_mn = X[:60000].reshape((-1,28,28)), X[60000:].reshape((-1,28,28))
    Y_train_mn, Y_test_mn = Y[:60000].astype(int), Y[60000:].astype(int)
except: 
    # workaround if scikit-learn is not present
    import requests, io, gzip
    X_train_mn, X_test_mn = (np.fromstring(gzip.GzipFile(fileobj=io.BytesIO(requests.get('http://yann.lecun.com/exdb/mnist/' + name + '-images-idx3-ubyte.gz').content)).read()[16:], dtype=np.uint8).reshape((-1,28,28)).astype(np.float32) / 255.0 for name in ('train', 't10k'))
    Y_train_mn, Y_test_mn = (np.fromstring(gzip.GzipFile(fileobj=io.BytesIO(requests.get('http://yann.lecun.com/exdb/mnist/' + name + '-labels-idx1-ubyte.gz').content)).read()[8:], dtype=np.uint8).astype(int) for name in ('train', 't10k'))

# Shuffle the training data.
np.random.seed(0) # always use the same reordering, for reproducability
idx = np.random.permutation(len(X_train_mn))
X_train_mn, Y_train_mn = X_train_mn[idx], Y_train_mn[idx]

# Further split off a cross-validation set
X_train_mn, X_cv_mn = X_train_mn[:54000], X_train_mn[54000:]
Y_train_mn, Y_cv_mn = Y_train_mn[:54000], Y_train_mn[54000:]

# Our model expects float32 features, and cross-entropy expects one-hot encoded labels.
Y_train_mn, Y_cv_mn, Y_test_mn = (scipy.sparse.csr_matrix((np.ones(len(Y),np.float32), (range(len(Y)), Y)), shape=(len(Y), 10)) for Y in (Y_train_mn, Y_cv_mn, Y_test_mn))
X_train_mn, X_cv_mn, X_test_mn = (X.astype(np.float32) for X in (X_train_mn, X_cv_mn, X_test_mn))

# Have a peek.
matplotlib.pyplot.rcParams['figure.figsize'] = (5, 0.5)
matplotlib.pyplot.axis('off')
_ = matplotlib.pyplot.imshow(np.concatenate(X_train_mn[0:10], axis=1), cmap="gray_r")

让我们定义CNTK模型函数,将长度为28×28图像映射到长度为10的结果向量。我把这些写进一个函数,之后我们可以很方便的重构他。如果你学习过之前的教程,你应该知道如何使用Layer库来构建、训练和测试较复杂的神经网络。

def create_model_mn_factory():
    with cntk.layers.default_options(activation=cntk.ops.relu, pad=False):
        return cntk.layers.Sequential([
            # reduction_rank=0 for B&W images
            cntk.layers.Convolution2D((5,5), num_filters=32, reduction_rank=0, pad=True), 
            cntk.layers.MaxPooling((3,3), strides=(2,2)),
            cntk.layers.Convolution2D((3,3), num_filters=48),
            cntk.layers.MaxPooling((3,3), strides=(2,2)),
            cntk.layers.Convolution2D((3,3), num_filters=64),
            cntk.layers.Dense(96),
            cntk.layers.Dropout(dropout_rate=0.5),
            # no activation in final layer (softmax is done in criterion)
            cntk.layers.Dense(num_classes_mn, activation=None) 
        ])
model_mn = create_model_mn_factory()

这个模型稍微有点复杂,他由好几个卷积-池化层和两个用于分类的全连接层组成,这展示了CNTK API的几个特性:

第一,我们使用CNTK的Layer库创建网络层。

第二,Sequential()函数用于将上面的层一个接一个的应用,这叫顺序函数组合。注意,这里和一些其他的机器学习组件有所不同,你不能够使用Add()函数来往已经定义好的层组后面在添加层。CNTK的Fuction对象除了里面需要学习的参数之外,都是不可改变的(如果要编辑一个Function对象,可以使用clone()函数)。如果你喜欢这种风格,创建你的网络层列表然后传入Sequential()函数即可。

第三,工作空间管理器函数default_options()允许我们给各个网络层设置选项参数,比如激活函数默认会使用ReLU,除非我们自己设置。

最后一点,注意我们设置的relu不是个字符串,而是一个真正的函数。我们可以设置任意函数作为我们的激活函数,甚至还可以直接传入一个Python的lambda表达式,比如relu就可以用lambda表达式表示成:activation=lambda x: cntk.ops.element_max(x, 0)

准则函数和我们之前的例子定义的一样,将输入数据和标签数据映射到成本只和度量值。

@cntk.Function
def criterion_mn_factory(data, label_one_hot):
    z = model_mn(data)
    loss = cntk.cross_entropy_with_softmax(z, label_one_hot)
    metric = cntk.classification_error(z, label_one_hot)
    return loss, metric
x = cntk.input_variable(input_shape_mn)
y = cntk.input_variable(num_classes_mn, is_sparse=True)
criterion_mn = criterion_mn_factory(x,y)

训练时,我们引入动量momentums:

N = len(X_train_mn)
lrs = cntk.learning_rate_schedule([0.001]*12 + [0.0005]*6 + [0.00025]*6 + [0.000125]*3 + [0.0000625]*3 + [0.00003125], cntk.learners.UnitType.sample, epoch_size=N)
momentums = cntk.learners.momentum_as_time_constant_schedule([0]*5 + [1024], epoch_size=N)
minibatch_sizes = cntk.minibatch_size_schedule([256]*6 + [512]*9 + [1024]*7 + [2048]*8 + [4096], epoch_size=N)

learner = cntk.learners.momentum_sgd(model_mn.parameters, lrs, momentums)

看起来好像跟之前的有点不同。首先,学习速率和训练周期设置成这么长一串(类似[0.001]×12 + [0.0005]×6 +…),这是在告诉CNTK在最初的12轮使用0.001,接下来的6轮使用0.005,以此类推。

第二,学习速率是针对每个样本的,动量则是根据时间变化的常数。这些数值直接确定了当前的weight值,也就是样本在训练模型时贡献的梯度,与取样包的大小无关。CNTK这个特性允许在不调整模型参数的情况下调整取样包的大小。上面的代码中我们将取样包的大小从256增长到4096,带来比三倍的速度增长(在Titan-X显卡上训练)。

好了,现在让我们开始训练,在Titan-X显卡上大概要运行一分钟。

progress_writer = cntk.logging.ProgressPrinter()
criterion_mn.train((X_train_mn, Y_train_mn), minibatch_size=minibatch_sizes,
                   max_epochs=40, parameter_learners=[learner], callbacks=[progress_writer])
test_metric_mn = criterion_mn.test((X_test_mn, Y_test_mn), callbacks=[progress_writer]).metric

图API示例:再次进行MNIST数字识别

CNTK也允许我们使用图级别的API来编写神经网络。使用图API会带来代码比较冗长,但是也更加灵活。下面的代码定义了与上面相同的模型和准则函数,也会有相同的结果。

images = cntk.input_variable(input_shape_mn, name='images')
with cntk.layers.default_options(activation=cntk.ops.relu, pad=False):
    r = cntk.layers.Convolution2D((5,5), num_filters=32, reduction_rank=0, pad=True)(images)
    r = cntk.layers.MaxPooling((3,3), strides=(2,2))(r)
    r = cntk.layers.Convolution2D((3,3), num_filters=48)(r)
    r = cntk.layers.MaxPooling((3,3), strides=(2,2))(r)
    r = cntk.layers.Convolution2D((3,3), num_filters=64)(r)
    r = cntk.layers.Dense(96)(r)
    r = cntk.layers.Dropout(dropout_rate=0.5)(r)
    model_mn = cntk.layers.Dense(num_classes_mn, activation=None)(r)

label_one_hot = cntk.input_variable(num_classes_mn, is_sparse=True, name='labels')
loss = cntk.cross_entropy_with_softmax(model_mn, label_one_hot)
metric = cntk.classification_error(model_mn, label_one_hot)
criterion_mn = cntk.combine([loss, metric])
print('criterion_mn:', criterion_mn)

传入数据

一旦你决定了你的模型结构并在代码中定义了他,你就会面临如何将训练数据传入他来进行训练的问题。

上面的的代码简单的将numpy/scipy数组传入模型,这仅仅是CNTK支持的三种数据传入方式中的一种,这三种分别是:

  1. 使用numpy数组或者scipy稀疏矩阵,适用于内存能够装下的少量数据。
  2. 使用CNTK的MinibatchSource类的实例,用于内存一次读不下的情况。
  3. 当上述两种方法都不好用时,可以自定义取样包循环。

1.通过Numpy/Scipy数组传入数据

(上面也做了示例,本部分略)

2.使用MinibatchSource类读取数据

工业级的训练数据通常都太大,一个取样包内存也装不下。为了应对这种情况,CNTK提供了MinibatchSource类,他提供如下功能:

  • 一个随机分块算法,只保存某个时间内存里面的数据。
  • 分布式读取,让每个工作的计算机读取不同的数据子集。
  • 一个图像和图像增强的转换器
  • 多数据类型整合
  • 异步加载数据以便在数据读取或者准备时运算设备不会等着。

目前,MinibatchSource类以解码器的形式实现了有限的集中数据类型:

  • 图像(ImageDeserializer)
  • 声音文件(HTKFeatureDeserializer, HTKMLFDeserializer)
  • CNTK标准文本格式(CTF),这种文件由特征通道集组成,每个样本包含一个稀疏或者密集矩阵。CTFDeserializer能够将文件中的特征通道和模型函数/准则函数的输入值对应起来。

下面的例子就是使用ImageDeserializer类来展示这种数据读取方法的一般形式。针对不同的输入文件格式,我们需要先阅读其文档。

image_width, image_height, num_channels = (32, 32, 3)
num_classes = 1000
def create_image_reader(map_file, is_training):
    transforms = []
    if is_training:  # train uses data augmentation (translation only)
        transforms += [
            cntk.io.transforms.crop(crop_type='randomside', side_ratio=0.8)  # random translation+crop
        ]
    transforms += [  # to fixed size
        cntk.io.transforms.scale(width=image_width, height=image_height, channels=num_channels, interpolations='linear'),
    ]
    # deserializer
    return cntk.io.MinibatchSource(cntk.io.ImageDeserializer(map_file, cntk.io.StreamDefs(
        features = cntk.io.StreamDef(field='image', transforms=transforms),
        labels   = cntk.io.StreamDef(field='label', shape=num_classes)
    )), randomize=is_training, max_sweeps = cntk.io.INFINITELY_REPEAT if is_training else 1)

3.使用自定义的取样包循环读取数据

不同于直接将所有的数据传入train()函数和test()函数让CNTK内部去实现取样包循环,我们也可以自己实现我们自己的取样包循环,然后使用底层API:train_minibatch()和test_minibatch()。这在我们的数据不适用上述两种方法时非常重要。train_minibatch()和test_minibatch()方法需要你实例化一个Trainer类的方法,来设置train()函数的参数。下面的代码实现了在逻辑回归中使用自定义的取样包循环。

# Recreate the model, so that we can start afresh. This is a direct copy from above.
model_lr = cntk.layers.Dense(num_classes_lr, activation=None)
@cntk.Function
def criterion_lr_factory(data, label_one_hot):
    # apply model. Computes a non-normalized log probability for every output class.
    z = model_lr(data) 
    # this applies softmax to z under the hood
    loss = cntk.cross_entropy_with_softmax(z, label_one_hot) 
    metric = cntk.classification_error(z, label_one_hot)
    return loss, metric

x = cntk.input_variable(input_dim_lr)
y = cntk.input_variable(num_classes_lr, is_sparse=True)
criterion_lr = criterion_lr_factory(x,y)

# Create the learner; same as above.
learner = cntk.sgd(model_lr.parameters, cntk.learning_rate_schedule(0.1, cntk.UnitType.minibatch))

# This time we must create a Trainer instance ourselves.
trainer = cntk.Trainer(None, criterion_lr, [learner], [cntk.logging.ProgressPrinter(50)])

# Train the model by spoon-feeding minibatch by minibatch.
minibatch_size = 32
# loop over minibatches
for i in range(0, len(X_train_lr), minibatch_size): 
    # get one minibatch worth of data
    x = X_train_lr[i:i+minibatch_size] 
    y = Y_train_lr[i:i+minibatch_size]
     # update model from one minibatch
    trainer.train_minibatch({criterion_lr.arguments[0]: x, criterion_lr.arguments[1]: y}) 
trainer.summarize_training_progress()

# Test error rate minibatch by minibatch
# metric is the second output of criterion_lr()
evaluator = cntk.Evaluator(criterion_lr.outputs[1], [progress_writer])
# loop over minibatches
for i in range(0, len(X_test_lr), minibatch_size):
    # get one minibatch worth of data
    x = X_test_lr[i:i+minibatch_size] 
    y = Y_test_lr[i:i+minibatch_size]
    # test one minibatch
    evaluator.test_minibatch({criterion_lr.arguments[0]: x, criterion_lr.arguments[1]: y}) 
evaluator.summarize_test_progress()

训练和评估

在之前的例子中,我们使用train()函数来训练,使用test()来测试评估。在这个部分,我们会给你展示train()的某些高级选项:

  1. 在多GPU上使用MPI进行分布式训练
  2. 使用回调函数进行进程跟踪、TensorBoard可视化、数据检查、基于交叉验证的训练控制以及最终模型测试。

1.分布式训练

CNTK让分布式训练非常容易实现。更棒的是,他支持三种分布式训练的方式。

  • 简单数据并行训练
  • 1比特随机梯度下降
  • BlockMomentum(不会翻译)

简单的数据并行训练将每个取样包分布在N个工作进程中,每个进程使用一个显卡。在每个取样包都运算之后,每个线程得到的梯度会在更新模型参数之前做一个汇总。这种方式通常用于卷积神经网络这种具有高运算通信比的神经网络。

一位随机梯度下降是一种使用能够提高数据并行运算是通信速度的技术的方法。这种方法对于那些通信成本为主要因素的神经网络有奇效,比如全连接神经网络或者由很多全连接层构成的神经网络。这种方法只有在加速良好时对精度的影响才比较小。

BlockMomentum技术是通过每N个取样包才交换一次梯度值来改善通信带宽。

训练开始后使用MPI进行通信,所以CNTK分布式训练可以在一台机器商工作,也能在多台机器上工作。你需要做的事情仅仅有:

  • 将你的训练器打包进一个distributed_learner对象
  • 使用mpiexec运行我们的Python脚本

2.回调

train()回调指定了一些train()运行过程中周期性执行的活动,这个周期通常是一个训练周期。回调是一组对象,对象的类型决定了具体的回调任务。

进程追踪器用来在处理N个取样包和完成一轮训练后打印出相关信息,当然也可以被设置成开始的每个取样包都打印。ProgressPrinter打印到标准输出设备上或者文件中,TensorBoardProgressWriter则吧事件输出到TensorBoard上进行可视化。你也可以编写你自己的进程追踪器。

接下来,CheckpointConfig类用来在每个训练周期记录一个数据检查文件,然后在检查通过后自动的开始训练。

CrossValidationConfig类告诉CNTK周期性的评估模型和数据集,然后调用一个用户自定义的回调函数来调整学习速率。

最后TestConfig让CNTK在完成训练之后使用给定的测试数据评估测试我们的模型。这和我们上面定义的test()功能一样。

实践:高端训练例子

让我们吧上述内容放在一个例子里面,下面的例子是在我们的MNIST中加入了日志、TensorBoard时间,数据检查,基于交叉验证的训练控制和最后的测试。

# Create model and criterion function.
x = cntk.input_variable(input_shape_mn)
y = cntk.input_variable(num_classes_mn, is_sparse=True)
model_mn = create_model_mn_factory()
@cntk.Function
def criterion_mn_factory(data, label_one_hot):
    z = model_mn(data)
    loss = cntk.cross_entropy_with_softmax(z, label_one_hot)
    metric = cntk.classification_error(z, label_one_hot)
    return loss, metric

criterion_mn = criterion_mn_factory(x, y)

# Create the learner.
learner = cntk.learners.momentum_sgd(model_mn.parameters, lrs, momentums)

# Create progress callbacks for logging to file and TensorBoard event log.
# Prints statistics for the first 10 minibatches, then for every 50th, to a log file.
progress_writer = cntk.logging.ProgressPrinter(50, first=10, log_to_file='my.log')
tensorboard_writer = cntk.logging.TensorBoardProgressWriter(50, log_dir='my_tensorboard_logdir',
                                                            model=criterion_mn)

# Create a checkpoint callback.
# Set restore=True to restart from available checkpoints.
epoch_size = len(X_train_mn)
checkpoint_callback_config = cntk.CheckpointConfig('model_mn.cmf', epoch_size, preserve_all=True, restore=False)

# Create a cross-validation based training control.
# This callback function halves the learning rate each time the cross-validation metric
# improved less than 5% relative, and stops after 6 adjustments.
prev_metric = 1 # metric from previous call to the callback. Error=100% at start.
def adjust_lr_callback(index, average_error, cv_num_samples, cv_num_minibatches):
    global prev_metric
    # did metric improve by at least 5% rel?
    if (prev_metric - average_error) / prev_metric < 0.05: 
        learner.reset_learning_rate(cntk.learning_rate_schedule(learner.learning_rate() / 2, cntk.learners.UnitType.sample))
        # we are done after the 6-th LR cut
        if learner.learning_rate() < lrs[0] / (2**7-0.1): 
            print("Learning rate {} too small. Training complete.".format(learner.learning_rate()))
            # means we are done
            return False 
        print("Improvement of metric from {:.3f} to {:.3f} insufficient. Halving learning rate to {}.".format(prev_metric, average_error, learner.learning_rate()))
    prev_metric = average_error
    # means continue
    return True 

cv_callback_config = cntk.CrossValidationConfig((X_cv_mn, Y_cv_mn), 3*epoch_size, minibatch_size=256,
                                                callback=adjust_lr_callback, criterion=criterion_mn)

# Callback for testing the final model.
test_callback_config = cntk.TestConfig((X_test_mn, Y_test_mn), criterion=criterion_mn)

# Train!
callbacks = [progress_writer, tensorboard_writer, checkpoint_callback_config, cv_callback_config, test_callback_config]
progress = criterion_mn.train((X_train_mn, Y_train_mn), minibatch_size=minibatch_sizes,
                              max_epochs=50, parameter_learners=[learner], callbacks=callbacks)

# Progress is available from return value
losses = [summ.loss for summ in progress.epoch_summaries]
print('loss progression =', ", ".join(["{:.3f}".format(loss) for loss in losses]))

使用你的模型

训练深度神经网络的最终目的是在你的项目或者程序中使用它。这涉及到Python之外的其他语言,我们这里给出一个简单的概览。

当你完成模型训练之后,你能在以下场景使用他:

  • 直接在Python程序中使用
  • 在CNTK支持的语言中使用,包括C++C#Java
  • 在你自己的网络服务中使用
  • 在微软的Azure网络服务中使用

所有场景的第一步是明确你的模型的输入类型,然后将训练好的模型保持到硬盘。

print(model_mn)
x = cntk.input_variable(input_shape_mn)
model = model_mn(x)
print(model)

model.save('mnist.cmf')

Python程序中使用它是非常简单的,因为我们的神经网络就是函数对象,像函数一样,是能够直接调用的。所以用起来就是:加载模型、使用输入数据调用他。

# At program start, load the model.
classify_digit = cntk.Function.load('mnist.cmf')

# To apply model, just call it.
# (pick a random test digit for illustration)
image_input = X_test_mn[8345]  
# call the model function with the input data
scores = classify_digit(image_input) 
 # find the highest-scoring class
image_class = scores.argmax()       

# And that's it. Let's have a peek at the result
print('Recognized as:', image_class)
matplotlib.pyplot.axis('off')
_ = matplotlib.pyplot.imshow(image_input, cmap="gray_r")

模型也可以直接被其他CNTK支持的语言调用,了解详情请看以下例子(点击左下角的阅读原文可以进入CNTK的例子集):

  • C++: Examples/Evaluation/CNTKLibraryCPPEvalCPUOnlyExamples/CNTKLibraryCPPEvalCPUOnlyExamples.cpp
  • C#: Examples/Evaluation/CNTKLibraryCSEvalCPUOnlyExamples/CNTKLibraryCSEvalExamples.cs

将模型用于网络服务,与上述方法相同,就看你的网络服务是使用什么语言编写的。

将模型用于Azure网络服务:Examples/Evaluation/CNTKAzureTutorial01

总结

本教程提供了使用CNTK创建和使用深度神经网络的五个主要任务:

(回顾略)


欢迎扫码关注我的微信公众号获取最新文章
image

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐