1. 程式人生 > >CNTK API文件翻譯(12)——CNTK進階

CNTK API文件翻譯(12)——CNTK進階

這篇教程展示了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