1. 程式人生 > >CNTK API文件翻譯(18)——多對多神經網路處理文字資料(2)

CNTK API文件翻譯(18)——多對多神經網路處理文字資料(2)

(本期教程需要翻譯的內容實在是太多了,將其分割成兩期,上期主要講理論和模型建立,本期主要講訓練、測試、優化等)

訓練

在我們開始訓練之前,我們將定義訓練封裝器、貪婪解碼封裝器以及用於訓練模型的準則函式。首先是訓練封裝器。

def create_model_train(s2smodel):
    # model used in training (history is known from labels)
    # note: the labels must NOT contain the initial <s>
    @C.Function
    def model_train
(input, labels):
# (input*, labels*) --> (word_logp*) # The input to the decoder always starts with the special label sequence start token. # Then, use the previous value of the label sequence (for training) or the output (for execution). past_labels = C.layers.Delay(initial_state=sentence_start)(labels) return
s2smodel(past_labels, input) return model_train

上面我們又使用@Function裝飾器建立了一個CNTK函式物件model_train。這個函式的引數是輸入序列input和輸出序列labels。past_labels變數使用Delay層儲存了我們先前建立的模型的歷史記錄。這會返回之前單位時間的輸入labels。因此,如果我們將labels設定為[‘a’, ‘b’, ‘c’],past_labels的值將會是[‘’, ‘a’, ‘b’, ‘c’],然後返回呼叫past_labels和input的模型。

接著建立貪婪解碼模型封裝器:

def
create_model_greedy(s2smodel):
# model used in (greedy) decoding (history is decoder's own output) # (input*) --> (word_sequence*) @C.Function @C.layers.Signature(InputSequence[C.layers.Tensor[input_vocab_dim]]) def model_greedy(input): # Decoding is an unfold() operation starting from sentence_start. # We must transform s2smodel (history*, input* -> word_logp*) into a generator (history* -> output*) # which holds 'input' in its closure. unfold = C.layers.UnfoldFrom(lambda history: s2smodel(history, input) >> C.hardmax, # stop once sentence_end_index was max-scoring output until_predicate=lambda w: w[...,sentence_end_index], length_increase=length_increase) return unfold(initial_state=sentence_start, dynamic_axes_like=input) return model_greedy

上面我們建立了一個新的CNTK函式物件model_greedy,他只有一個引數。這當然是因為當我們將這個模型用於測試時,我們沒有任何標籤——建立標籤是模型的工作。在這種情況下,我們使用UnfoldFrom層用於使用當前的歷史資料執行模型,然後生成輸出hardmax。輸出的hardmax之後會成為歷史值的一部分,然後我們會繼續執行遞迴直到達到sentence_end_index。輸出序列的最大長度是length_increase的倍數。

在訓練之前的最後一件事前是為我們的模型建立準則函式

def create_criterion_function(model):
    @C.Function
    @C.layers.Signature(input=InputSequence[C.layers.Tensor[input_vocab_dim]], 
                        labels=LabelSequence[C.layers.Tensor[label_vocab_dim]])
    def criterion(input, labels):
        # criterion function must drop the <s> from the labels
        # <s> A B C </s> --> A B C </s>
        postprocessed_labels = C.sequence.slice(labels, 1, 0) 
        z = model(input, postprocessed_labels)
        ce = C.cross_entropy_with_softmax(z, postprocessed_labels)
        errs = C.classification_error(z, postprocessed_labels)
        return (ce, errs)

    return criterion

上面我們建立了準則函式,他會將標籤中的序列開始符號去掉,使用給定的input和labels執行模型,然後使用輸出值和標準比較。我們使用成本函式cross_entropy_with_softmax,計算lassification_error,表示每個單詞的誤差百分比,以此表徵我們的生成的精度。CNTK函式物件criterion以元組的形式返回上訴兩個值,python函式create_criterion_function返回上面的CNTK函式物件。

現在我們建立訓練迴圈

def train(train_reader, valid_reader, vocab, i2w, s2smodel, max_epochs, epoch_size):

    # create the training wrapper for the s2smodel, as well as the criterion function
    model_train = create_model_train(s2smodel)
    criterion = create_criterion_function(model_train)

    # also wire in a greedy decoder so that we can properly log progress on a validation example
    # This is not used for the actual training process.
    model_greedy = create_model_greedy(s2smodel)

    # Instantiate the trainer object to drive the model training
    minibatch_size = 72
    lr = 0.001 if use_attention else 0.005
    learner = C.fsadagrad(model_train.parameters,
                          lr = C.learning_rate_schedule([lr]*2+[lr/2]*3+[lr/4], C.UnitType.sample, epoch_size),
                          momentum = C.momentum_as_time_constant_schedule(1100),
                          gradient_clipping_threshold_per_sample=2.3,
                          gradient_clipping_with_truncation=True)
    trainer = C.Trainer(None, criterion, learner)

    # Get minibatches of sequences to train with and perform model training
    total_samples = 0
    mbs = 0
    eval_freq = 100

    # print out some useful training information
    C.logging.log_number_of_parameters(model_train) ; print()
    progress_printer = C.logging.ProgressPrinter(freq=30, tag='Training')    

    # a hack to allow us to print sparse vectors
    sparse_to_dense = create_sparse_to_dense(input_vocab_dim)

    for epoch in range(max_epochs):
        while total_samples < (epoch+1) * epoch_size:
            # get next minibatch of training data
            mb_train = train_reader.next_minibatch(minibatch_size)

            # do the training
            trainer.train_minibatch({criterion.arguments[0]: mb_train[train_reader.streams.features], 
                                     criterion.arguments[1]: mb_train[train_reader.streams.labels]})

            # log progress
            progress_printer.update_with_trainer(trainer, with_metric=True)

            # every N MBs evaluate on a test sequence to visually show how we're doing
            if mbs % eval_freq == 0:
                mb_valid = valid_reader.next_minibatch(1)

                # run an eval on the decoder output model (i.e. don't use the groundtruth)
                e = model_greedy(mb_valid[valid_reader.streams.features])
                print(format_sequences(sparse_to_dense(mb_valid[valid_reader.streams.features]), i2w))
                print("->")
                print(format_sequences(e, i2w))

                # visualizing attention window
                if use_attention:
                    debug_attention(model_greedy, mb_valid[valid_reader.streams.features])

            total_samples += mb_train[train_reader.streams.labels].num_samples
            mbs += 1

        # log a summary of the stats for the epoch
        progress_printer.epoch_summary(with_metric=True)

    # done: save the final model
    model_path = "model_%d.cmf" % epoch
    print("Saving final model to '%s'" % model_path)
    s2smodel.save(model_path)
    print("%d epochs complete." % max_epochs)

在上面的函式中,我們將模型用於訓練和評估。一般來是不需要評估的,我們做是因為我們需要週期性的檢視訓練過程中的樣本序列,以此來看我們的模型是如何收斂的。

然後我們設定了一些訓練中需要的標準變數。我們設定了minibatch_size(取樣包裡所有要素的數量),初始學習速率lr,我們使用adam_sgd演算法初始化了一個學習器,還設定了learning_rate_schedule,用於緩慢的減少學習速率。我們使用梯度削減來防止梯度膨脹,最終建立了一個訓練器物件trainer。

我們使用CNTK的ProgressPrinter類來監測每個取樣包/每輪的度量平均值,我們設定為每30個取樣包更新一次。最後在開始訓練之前,我們增加一個sparse_to_dense函式在驗證的時候列印輸入序列資料,因為驗證資料是洗漱的。函式如下

# dummy for printing the input sequence below. Currently needed because input is sparse.
def create_sparse_to_dense(input_vocab_dim):
    I = C.Constant(np.eye(input_vocab_dim))
    @C.Function
    @C.layers.Signature(InputSequence[C.layers.SparseTensor[input_vocab_dim]])
    def no_op(input):
        return C.times(input, I)
    return no_op

在訓練時,我們與訓練我們之前經歷過的其他CNTK神經網路一樣。我們請求下一個取樣包資料,然後執行我們的額訓練,再使用progress_printer把訓練過程打印出來。和普通神經網路不同的是我們運行了一個驗證版本的神經網路,用它執行一個序列”ABADI”來看他的預測值是怎樣的。

另外一個不同點是我們可以選擇使用注意力模型,並能在視窗中可視化出來。呼叫函式debug_attention可以顯示解碼器使用的每個編碼器每個輸出資訊的隱藏狀態的權重。這個函式需要另外一個format_sequences,用來將輸入輸出序列列印到螢幕上。函式如下

# Given a vocab and tensor, print the output
def format_sequences(sequences, i2w):
    return [" ".join([i2w[np.argmax(w)] for w in s]) for s in sequences]

# to help debug the attention window
def debug_attention(model, input):
    q = C.combine([model, model.attention_model.attention_weights])
    #words, p = q(input) # Python 3
    words_p = q(input)
    words = words_p[0]
    p     = words_p[1]
    seq_len = words[0].shape[attention_axis-1]
    #attention_span  #7 # test sentence is 7 tokens long
    span = 7
    # (batch, len, attention_span, 1, vector_dim)
    p_sq = np.squeeze(p[0][:seq_len,:span,0,:])
    opts = np.get_printoptions()
    np.set_printoptions(precision=5)
    print(p_sq)
    np.set_printoptions(**opts)

讓我們使用一個完整訓練週期的一小部分來訓練一下我們的神經網路。具體來說,我們將執行25000條資料(大概1個完整週期的的3%)

model = create_model()
train(train_reader, valid_reader, vocab, i2w, model, max_epochs=1, epoch_size=25000)

輸出的資料

['<s> A B A D I </s>']
->
['O O ~K ~K X X X X ~JH ~JH ~JH']
[[ 0.14327  0.14396  0.14337  0.14305  0.14248  0.1422   0.14166]
 [ 0.14327  0.14395  0.14337  0.14305  0.14248  0.1422   0.14166]
 [ 0.14327  0.14396  0.14337  0.14305  0.14248  0.1422   0.14166]
 [ 0.14328  0.14395  0.14337  0.14305  0.14248  0.1422   0.14166]
 [ 0.14327  0.14395  0.14337  0.14305  0.14248  0.1422   0.14166]
 [ 0.14327  0.14395  0.14337  0.14305  0.14248  0.1422   0.14166]
 [ 0.14327  0.14396  0.14337  0.14305  0.14248  0.1422   0.14166]
 [ 0.14327  0.14396  0.14337  0.14305  0.14248  0.1422   0.14166]
 [ 0.14327  0.14395  0.14337  0.14305  0.14248  0.1422   0.14166]
 [ 0.14327  0.14395  0.14337  0.14305  0.14248  0.1422   0.14166]
 [ 0.14327  0.14396  0.14337  0.14305  0.14248  0.1422   0.14166]]
 Minibatch[   1-  30]: loss = 4.145903 * 1601, metric = 87.32% * 1601;
 Minibatch[  31-  60]: loss = 3.648827 * 1601, metric = 86.45% * 1601;
 Minibatch[  61-  90]: loss = 3.320400 * 1548, metric = 88.44% * 1548;
['<s> A B A D I </s>']
->
['~N ~N </s>']
[[ 0.14276  0.14348  0.14298  0.1428   0.1425   0.14266  0.14281]
 [ 0.14276  0.14348  0.14298  0.14281  0.1425   0.14266  0.14281]
 [ 0.14276  0.14348  0.14298  0.14281  0.1425   0.14266  0.14281]]
 Minibatch[  91- 120]: loss = 3.231915 * 1567, metric = 86.02% * 1567;
 Minibatch[ 121- 150]: loss = 3.212445 * 1580, metric = 83.54% * 1580;
 Minibatch[ 151- 180]: loss = 3.214926 * 1544, metric = 84.26% * 1544;
['<s> A B A D I </s>']
->
['~R ~R ~AH ~AH ~AH </s>']
[[ 0.14293  0.14362  0.14306  0.14283  0.14246  0.14252  0.14259]
 [ 0.14293  0.14362  0.14306  0.14283  0.14246  0.14252  0.14259]
 [ 0.14293  0.14362  0.14306  0.14283  0.14246  0.14252  0.14259]
 [ 0.14293  0.14362  0.14306  0.14283  0.14246  0.14252  0.14259]
 [ 0.14293  0.14362  0.14306  0.14283  0.14246  0.14252  0.14259]
 [ 0.14293  0.14362  0.14306  0.14283  0.14246  0.14252  0.14259]]
 Minibatch[ 181- 210]: loss = 3.144272 * 1565, metric = 82.75% * 1565;
 Minibatch[ 211- 240]: loss = 3.185484 * 1583, metric = 83.20% * 1583;
 Minibatch[ 241- 270]: loss = 3.126284 * 1562, metric = 83.03% * 1562;
 Minibatch[ 271- 300]: loss = 3.150704 * 1551, metric = 83.56% * 1551;
['<s> A B A D I </s>']
->
['~R ~R ~R ~AH </s>']
[[ 0.14318  0.14385  0.14318  0.14286  0.14238  0.1423   0.14224]
 [ 0.14318  0.14385  0.14318  0.14286  0.14238  0.1423   0.14224]
 [ 0.14318  0.14385  0.14318  0.14287  0.14238  0.1423   0.14224]
 [ 0.14318  0.14385  0.14318  0.14287  0.14239  0.1423   0.14224]
 [ 0.14318  0.14385  0.14318  0.14287  0.14239  0.1423   0.14224]]
 Minibatch[ 301- 330]: loss = 3.131863 * 1575, metric = 82.41% * 1575;
 Minibatch[ 331- 360]: loss = 3.095721 * 1569, metric = 82.98% * 1569;
 Minibatch[ 361- 390]: loss = 3.098615 * 1567, metric = 82.32% * 1567;
['<s> A B A D I </s>']
->
['~K ~R ~R ~AH </s>']
[[ 0.14352  0.14416  0.14335  0.14292  0.1423   0.14201  0.14173]
 [ 0.1435   0.14414  0.14335  0.14293  0.14231  0.14202  0.14174]
 [ 0.14351  0.14415  0.14335  0.14293  0.1423   0.14202  0.14174]
 [ 0.14351  0.14415  0.14335  0.14293  0.1423   0.14202  0.14174]
 [ 0.14351  0.14415  0.14335  0.14293  0.1423   0.14202  0.14174]]
 Minibatch[ 391- 420]: loss = 3.115971 * 1601, metric = 81.70% * 1601;
Finished Epoch[1 of 300]: [Training] loss = 3.274279 * 22067, metric = 84.14% * 22067 64.263s (343.4 samples/s);
Saving final model to 'model_0.cmf'
1 epochs complete.

如我們在上面的輸出資料中縮減,成本值降低了不少,不過輸出的序列離我們的期望值還有點距離。取消下面程式碼的註釋,使用一個完整的週期進行訓練,在第一個週期完成之後都將看到一個非常好的從文字到語音的模型。

# Uncomment the line below to train the model for a full epoch
#train(train_reader, valid_reader, vocab, i2w, model, max_epochs=1, epoch_size=908241)

測試神經網路

現在我們訓練了一個用於文字和讀音轉換的sequence-to-sequence神經網路,我們要對此做兩件重要的事情。第一,我們需要使用留存資料測試他的經度。然後,我們需要將他放進一個互動式的環境,一遍我們能將其用進我們自己的輸入序列,來看看模型表現的怎麼樣。首先我們來看看模型的錯誤率。

在訓練結束時,我們使用s2smodel.save(model_path)儲存了模型。因此,要測試他,我們首先需要載入這個模型,然後使用它執行一些資料。我們先載入模型,然後建立一個可以用於讀取測試資料的讀取器。注意我們這次給create_reader函式傳入了False表示我們這是測試模式,所有的資料只需要使用一次。

# load the model for epoch 0
model_path = "model_0.cmf"
model = C.Function.load(model_path)

# create a reader pointing at our testing data
test_reader = create_reader(dataPath['testing'], False)

現在我們需要定義我們的測試函式。我們給他傳入資料讀取器reader,已經訓練好的s2smodel和詞彙表i2w,以便我們可以直接比較模型的預測值和測試資料集的標籤值。我們迴圈執行測試資料集,為了更快,取樣包大小設定為512,然後追蹤其錯誤率。注意下面我們是根據每個序列測試的,這表示在生成的序列中的每個資訊都應該和標籤序列中的對應資訊相匹配。

# This decodes the test set and counts the string error rate.
def evaluate_decoding(reader, s2smodel, i2w):

    # wrap the greedy decoder around the model
    model_decoding = create_model_greedy(s2smodel) 

    progress_printer = C.logging.ProgressPrinter(tag='Evaluation')

    sparse_to_dense = create_sparse_to_dense(input_vocab_dim)

    minibatch_size = 512
    num_total = 0
    num_wrong = 0
    while True:
        mb = reader.next_minibatch(minibatch_size)
        # finish when end of test set reached
        if not mb: 
            break
        e = model_decoding(mb[reader.streams.features])
        outputs = format_sequences(e, i2w)
        labels  = format_sequences(sparse_to_dense(mb[reader.streams.labels]), i2w)
        # prepend sentence start for comparison
        outputs = ["<s> " + output for output in outputs]

        num_total += len(outputs)
        num_wrong += sum([label != output for output, label in zip(outputs, labels)])

    rate = num_wrong / num_total
    print("string error rate of {:.1f}% in {} samples".format(100 * rate, num_total))
    return rate

現在我們將使用上面的函式評估解碼過程。如果你使用我們上面僅使用50000樣本訓練的模型,你將得到100%的錯誤率,因為我們不可能用這麼少量的訓練讓每一個輸出的訊號都正確。不過,如果我們取消上面的程式碼,使用完整的訓練週期,你將得到一個大大提升的模型,訓練的統計結果大概如下:

Finished Epoch[1 of 300]: [Training] loss = 0.878420 * 799303, metric = 26.23% * 799303 1755.985s (455.2 samples/s);

現在讓我們評估模型在測試資料集上的表現。

# print the string error rate
evaluate_decoding(test_reader, model, i2w)

如果你不是訓練了一個完整的週期,上面的輸出將會是1,表示100%的錯誤率。如果你取消了那部分程式碼的註釋,執行一個完整週期的訓練,你講得到一個0.569的輸出值。表示錯誤率是56.9%,這個資料對於每個資料只訓練一遍來說已經很不錯了。現在我們改變上面的evaluate_decoding函式,來輸出每一個讀音的錯誤率。這表示我們更精確的計算他的錯誤,也讓我們的工作看起來不那麼艱辛,因為之前在一個字串中即使有一個錯誤,其他都正確,錯誤率都是100%。下面是改變後的程式碼。

# This decodes the test set and counts the string error rate.
def evaluate_decoding(reader, s2smodel, i2w):

    # wrap the greedy decoder around the model
    model_decoding = create_model_greedy(s2smodel)

    progress_printer = C.logging.ProgressPrinter(tag='Evaluation')

    sparse_to_dense = create_sparse_to_dense(input_vocab_dim)

    minibatch_size = 512
    num_total = 0
    num_wrong = 0
    while True:
        mb = reader.next_minibatch(minibatch_size)
        # finish when end of test set reached
        if not mb:
            break
        e = model_decoding(mb[reader.streams.features])
        outputs = format_sequences(e, i2w)
        labels  = format_sequences(sparse_to_dense(mb[reader.streams.labels]), i2w)
        # prepend sentence start for comparison
        outputs = ["<s> " + output for output in outputs]

        for s in range(len(labels)):
            for w in range(len(labels[s])):
                num_total += 1
                # in case the prediction is longer than the label
                if w < len(outputs[s]):
                    if outputs[s][w] != labels[s][w]:
                        num_wrong += 1

    rate = num_wrong / num_total
    print("{:.1f}".format(100 * rate))
    return rate


# print the phoneme error rate
test_reader = create_reader(dataPath['testing'], False)
evaluate_decoding(test_reader, model, i2w)

如果你是用訓練了一個完整週期的模型,你得到的音符錯誤率大概在10%左右(如果使用不完整的模型,大概是45%),很不錯是不是。這表示在測試資料集中的383294個音符中,我們的模型預測正確了近90%。接下來讓我們在一個互動式的環境下使用模型,在這個環境下我們能輸入我們自己的序列,看模型如何預測他們的發音。另外我們將會把解碼器的注意力模型視覺化,來看看我們輸入的那個字母對生成讀音比較重要。注意下面的例子只有在你至少訓練了一個完整週期的情況下才會表現良好。

互動式環境

這裡我們將寫一個互動式函式,讓我們能夠比較方便的跟訓練好的模型互動,來試試輸入我們自己的輸入序列。注意如果我們的模型只訓練了不到一個完整週期,結果將非常難看。上面我們用到的模型訓練了一個週期,表現還不錯,如果你有時間和耐心訓練30個週期,他的表現將會非常棒。

我們會首先需要引入一些圖形模組用來展示注意力模型,隨後我們將定義一個translate函式,他接收numpy陣列作為輸入資料,然後執行模型。

# imports required for showing the attention weight heatmap
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd

def translate(tokens, model_decoding, vocab, i2w, show_attention=False):

    vdict = {v:i for i,v in enumerate(vocab)}
    try:
        w = [vdict["<s>"]] + [vdict[c] for c in tokens] + [vdict["</s>"]]
    except:
        print('Input contains an unexpected token.')
        return []

    # convert to one_hot
    query = C.Value.one_hot([w], len(vdict))
    pred = model_decoding(query)
    # first sequence (we only have one) -> [len, vocab size]
    pred = pred[0]
    if use_attention:
        # attention has extra dimensions
        pred = pred[:,0,0,:]

    # print out translation and stop at the sequence-end tag
    prediction = np.argmax(pred, axis=-1)
    translation = [i2w[i] for i in prediction]

    # show attention window (requires matplotlib, seaborn, and pandas)
    if use_attention and show_attention:    
        q = C.combine([model_decoding.attention_model.attention_weights])
        att_value = q(query)

        # get the attention data up to the length of the output (subset of the full window)
        # -> (len, span)
        att_value = att_value[0][0:len(prediction),0:len(w),0,0]

        # set up the actual words/letters for the heatmap axis labels
        columns = [i2w[ww] for ww in prediction]
        index = [i2w[ww] for ww in w]

        dframe = pd.DataFrame(data=np.fliplr(att_value.T), columns=columns, index=index)
        sns.heatmap(dframe)
        plt.show()

    return translation

上面的translate函式的引數有tokens(使用者輸入的字元列表),model_decoding(我們模型的貪婪解碼版本),vocab(詞彙表),i2w(vocab的索引對映),show_attention (決定是否顯示注意力向量)

我們先將我們的輸入字元轉換為以為有效嗎,使用model_decoding(query)將其在模型中執行一遍,現在每個預測值都是在詞彙表離的概率分佈,我們使用argmax獲取最有可能的輸出值。

為了視覺化注意力視窗,我們使用combine函式將attention_weights轉換成CNTK函式物件,用來儲存我們希望的輸入值。通過這種方式,當我們執行q函式是,輸出將會是attention_weights的值。我們做一些資料操作,將資料轉換為sns接受的格式,實現視覺化。

最後,我們需要些一個使用者互動迴圈,允許使用者輸入多個輸入序列。

def interactive_session(s2smodel, vocab, i2w, show_attention=False):

    # wrap the greedy decoder around the model
    model_decoding = create_model_greedy(s2smodel)

    import sys

    print('Enter one or more words to see their phonetic transcription.')
    while True:
        # Testing a prefilled text for routine testing
        if isTest():
            line = "psychology"
        else:    
            line = input("> ")
        if line.lower() == "quit":
            break
        # tokenize. Our task is letter to sound.
        out_line = []
        for word in line.split():
            in_tokens = [c.upper() for c in word]
            out_tokens = translate(in_tokens, model_decoding, vocab, i2w, show_attention=True)
            out_line.extend(out_tokens)
        out_line = [" " if tok == '</s>' else tok[1:] for tok in out_line]
        print("=", " ".join(out_line))
        sys.stdout.flush()
        #If test environment we will test the translation only once
        if isTest():
            break

上面的函式使用我們的模型簡單的建立了一個貪婪解碼器,然後不斷地請求使用者輸入,然後將輸入序列傳入我們的translate函式。輸入完成之後,就可以實現注意力資料的視覺化。

interactive_session(model, vocab, i2w, show_attention=True)

image

注意注意力權重展示輸入資料不同部分對於生成不同訊號的重要性。對於像機器翻譯這樣的任務,由於語言之間的語法差異,對應的單詞順序經常改變,有一首的是不過圖中我們看到注意力視窗離對角線越來越遠,這種現象在字元到語音的轉換中經常看到。


歡迎掃碼關注我的微信公眾號獲取最新文章
image