這篇教程展示了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()
from IPython.display import Image
import matplotlib.pyplot
matplotlib.pyplot.rcParams['figure.figsize'] = (,)
定義模型結構
讓我們開始。接下來我們會介紹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((,out_dim))
# define the function itself
def dense(x):
# first call: reshape and initialize W
if len(W) == :
W.resize((x.shape[-], W.shape[-]), refcheck=False)
W[:] = np.random.randn(*W.shape) *
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(, np.tanh)
# apply it like a function
y = d(np.array([, ]))
# access member like an object
W = d.W
print('W =', d.W)
強調一遍,這隻是僞代碼。在實際使用時,CNTK裡面的函數對象不是基于numpy數組的,相反,與其他的深度學習元件類似,其内部是使用C++寫成的圖結構,用于編碼算法。在真實代碼裡面:
d = Dense(, np.tanh)
這句僅僅構成了一個圖,這句:
y = d(np.array([, ]))
是把資料傳入圖執行引擎。
圖結構繼承了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 =
# into one of two classes
num_classes_lr =
# 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()
def generate_synthetic_data(N):
Y = np.random.randint(size=N, low=, high=num_classes_lr) # labels
X = (np.random.randn(N, input_dim_lr)+) * (Y[:,None]+) # 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()
X_test_lr, Y_test_lr = generate_synthetic_data()
print('data =\n', X_train_lr[:])
print('labels =\n', Y_train_lr[:].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(, cntk.UnitType.minibatch))
progress_writer = cntk.logging.ProgressPrinter()
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:
[[-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[:])
print("Label :", [label.todense().argmax() for label in Y_test_lr[:]])
print("Predicted:", [z[i,:].argmax() for i in range(len(z))])
第二個CNTK神經網絡:MNIST數字識别
讓我們使用真是的案例再來一遍,這個真實案例就是MNIST數字識别。(MNIST介紹内容略)我們可以使用CNTK裡的功能更更簡潔的實作MNIST數字識别。
input_shape_mn = (, ) # MNIST digits are 28 x 28
num_classes_mn = # 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 / , mnist.target
X_train_mn, X_test_mn = X[:].reshape((-,,)), X[:].reshape((-,,))
Y_train_mn, Y_test_mn = Y[:].astype(int), Y[:].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()[:], dtype=np.uint8).reshape((-,,)).astype(np.float32) / 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()[:], dtype=np.uint8).astype(int) for name in ('train', 't10k'))
# Shuffle the training data.
np.random.seed() # 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[:], X_train_mn[:]
Y_train_mn, Y_cv_mn = Y_train_mn[:], Y_train_mn[:]
# 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), )) 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'] = (, )
matplotlib.pyplot.axis('off')
_ = matplotlib.pyplot.imshow(np.concatenate(X_train_mn[:], axis=), 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((,), num_filters=, reduction_rank=, pad=True),
cntk.layers.MaxPooling((,), strides=(,)),
cntk.layers.Convolution2D((,), num_filters=),
cntk.layers.MaxPooling((,), strides=(,)),
cntk.layers.Convolution2D((,), num_filters=),
cntk.layers.Dense(),
cntk.layers.Dropout(dropout_rate=),
# 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([]* + []* + []* + []* + []* + [], cntk.learners.UnitType.sample, epoch_size=N)
momentums = cntk.learners.momentum_as_time_constant_schedule([]* + [], epoch_size=N)
minibatch_sizes = cntk.minibatch_size_schedule([]* + []* + []* + []* + [], 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=, 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((,), num_filters=, reduction_rank=, pad=True)(images)
r = cntk.layers.MaxPooling((,), strides=(,))(r)
r = cntk.layers.Convolution2D((,), num_filters=)(r)
r = cntk.layers.MaxPooling((,), strides=(,))(r)
r = cntk.layers.Convolution2D((,), num_filters=)(r)
r = cntk.layers.Dense()(r)
r = cntk.layers.Dropout(dropout_rate=)(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支援的三種資料傳入方式中的一種,這三種分别是:
- 使用numpy數組或者scipy稀疏矩陣,适用于記憶體能夠裝下的少量資料。
- 使用CNTK的MinibatchSource類的執行個體,用于記憶體一次讀不下的情況。
- 當上述兩種方法都不好用時,可以自定義取樣包循環。
1.通過Numpy/Scipy數組傳入資料
(上面也做了示例,本部分略)
2.使用MinibatchSource類讀取資料
工業級的訓練資料通常都太大,一個取樣包記憶體也裝不下。為了應對這種情況,CNTK提供了MinibatchSource類,他提供如下功能:
- 一個随機分塊算法,隻儲存某個時間記憶體裡面的資料。
- 分布式讀取,讓每個工作的計算機讀取不同的資料子集。
- 一個圖像和圖像增強的轉換器
- 多資料類型整合
- 異步加載資料以便在資料讀取或者準備時運算裝置不會等着。
目前,MinibatchSource類以解碼器的形式實作了有限的集中資料類型:
- 圖像(ImageDeserializer)
- 聲音檔案(HTKFeatureDeserializer, HTKMLFDeserializer)
- CNTK标準文本格式(CTF),這種檔案由特征通道集組成,每個樣本包含一個稀疏或者密集矩陣。CTFDeserializer能夠将檔案中的特征通道和模型函數/準則函數的輸入值對應起來。
下面的例子就是使用ImageDeserializer類來展示這種資料讀取方法的一般形式。針對不同的輸入檔案格式,我們需要先閱讀其文檔。
image_width, image_height, num_channels = (, , )
num_classes =
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=) # 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 )
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(, cntk.UnitType.minibatch))
# This time we must create a Trainer instance ourselves.
trainer = cntk.Trainer(None, criterion_lr, [learner], [cntk.logging.ProgressPrinter()])
# Train the model by spoon-feeding minibatch by minibatch.
minibatch_size =
# loop over minibatches
for i in range(, 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[]: x, criterion_lr.arguments[]: 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[], [progress_writer])
# loop over minibatches
for i in range(, 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[]: x, criterion_lr.arguments[]: y})
evaluator.summarize_test_progress()
訓練和評估
在之前的例子中,我們使用train()函數來訓練,使用test()來測試評估。在這個部分,我們會給你展示train()的某些進階選項:
- 在多GPU上使用MPI進行分布式訓練
- 使用回調函數進行程序跟蹤、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(, first=, log_to_file='my.log')
tensorboard_writer = cntk.logging.TensorBoardProgressWriter(, 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 = # 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 < :
learner.reset_learning_rate(cntk.learning_rate_schedule(learner.learning_rate() / , cntk.learners.UnitType.sample))
# we are done after the 6-th LR cut
if learner.learning_rate() < lrs[] / (**-):
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), *epoch_size, minibatch_size=,
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=, 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[]
# 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建立和使用深度神經網絡的五個主要任務:
(回顧略)
歡迎掃碼關注我的微信公衆号擷取最新文章
