Lab pratici Language Understanding con reti ricorrenti

Si noti che questa esercitazione richiede la versione master più recente o la prossima versione CNTK 1.7.1 che verrà rilasciata a breve.

Questo lab pratico illustra come implementare una rete ricorrente per elaborare il testo, per le attività di Air Travel Information Services (ATIS) di assegnazione di tag di slot e classificazione delle finalità. Inizieremo con un incorporamento semplice seguito da un LSTM ricorrente. Verrà quindi esteso in modo da includere le parole adiacenti ed eseguire in modo bidirezionale. Infine, questo sistema verrà trasformato in un classificatore finalità.

Le tecniche da praticare includono:

  • descrizione del modello componendo blocchi di livello invece di scrivere formule
  • creazione di un blocco di livello personalizzato
  • variabili con lunghezze di sequenza diverse nella stessa rete
  • training parallelo

Si presuppone che l'utente abbia familiarità con le nozioni di base di Deep Learning e questi concetti specifici:

Prerequisiti

Si supponga di aver già installato CNTK ed eseguire il comando CNTK. Questa esercitazione è stata tenuta a KDD 2016 e richiede una build recente. Per istruzioni sulla configurazione, vedere qui . È sufficiente seguire le istruzioni per scaricare un pacchetto di installazione binaria da tale pagina.

Scaricare quindi un archivio ZIP (circa 12 MB): fare clic su questo collegamento e quindi sul pulsante Scarica. L'archivio contiene i file per questa esercitazione. Archiviare e impostare la directory di lavoro su SLUHandsOn. I file con cui si sta lavorando sono:

Infine, è consigliabile eseguire questa operazione in un computer con una GPU compatibile con CUDA. L'apprendimento avanzato senza GPU non è divertente.

Attività e struttura del modello

L'attività che si vuole affrontare in questa esercitazione è l'assegnazione di tag agli slot. Usiamo il corpus ATIS. ATIS contiene query sul computer umano dal dominio di Air Travel Information Services e l'attività sarà annotare (tag) ogni parola di una query se appartiene a un elemento specifico di informazioni (slot) e a quale parola.

I dati nella cartella di lavoro sono già stati convertiti in "Formato testo CNTK". Di seguito è riportato un esempio del file atis.test.ctfdi set di test:

19  |S0 178:1 |# BOS      |S1 14:1 |# flight  |S2 128:1 |# O
19  |S0 770:1 |# show                         |S2 128:1 |# O
19  |S0 429:1 |# flights                      |S2 128:1 |# O
19  |S0 444:1 |# from                         |S2 128:1 |# O
19  |S0 272:1 |# burbank                      |S2 48:1  |# B-fromloc.city_name
19  |S0 851:1 |# to                           |S2 128:1 |# O
19  |S0 789:1 |# st.                          |S2 78:1  |# B-toloc.city_name
19  |S0 564:1 |# louis                        |S2 125:1 |# I-toloc.city_name
19  |S0 654:1 |# on                           |S2 128:1 |# O
19  |S0 601:1 |# monday                       |S2 26:1  |# B-depart_date.day_name
19  |S0 179:1 |# EOS                          |S2 128:1 |# O

Questo file ha 7 colonne:

  • id sequenza (19). Sono presenti 11 voci con questo ID sequenza. Ciò significa che la sequenza 19 è costituita da 11 token;
  • colonna S0, che contiene indici di parole numeriche;
  • una colonna di commento indicata da #, per consentire a un lettore umano di conoscere qual è l'indice numerico delle parole; Le colonne di commento vengono ignorate dal sistema. BOS e EOS sono parole speciali per indicare rispettivamente l'inizio e la fine della frase;
  • column S1 è un'etichetta di finalità, che verrà usata solo nell'ultima parte dell'esercitazione;
  • un'altra colonna di commento che mostra l'etichetta leggibile dell'indice finalità numerico;
  • colonna S2 è l'etichetta dello slot, rappresentata come indice numerico; e
  • un'altra colonna di commento che mostra l'etichetta leggibile dell'indice di etichetta numerica.

L'attività della rete neurale consiste nell'esaminare la query (colonna S0) e prevedere l'etichetta dello slot (colonna S2). Come si può notare, a ogni parola nell'input viene assegnata un'etichetta vuota o un'etichetta O slot che inizia con B- per la prima parola e con I- per qualsiasi parola consecutiva aggiuntiva appartenente allo stesso slot.

Il modello che verrà usato è un modello ricorrente costituito da un livello di incorporamento, una cella LSTM ricorrente e un livello denso per calcolare le probabilità posterior:

slot label   "O"        "O"        "O"        "O"  "B-fromloc.city_name"
              ^          ^          ^          ^          ^
              |          |          |          |          |
          +-------+  +-------+  +-------+  +-------+  +-------+
          | Dense |  | Dense |  | Dense |  | Dense |  | Dense |  ...
          +-------+  +-------+  +-------+  +-------+  +-------+
              ^          ^          ^          ^          ^
              |          |          |          |          |
          +------+   +------+   +------+   +------+   +------+   
     0 -->| LSTM |-->| LSTM |-->| LSTM |-->| LSTM |-->| LSTM |-->...
          +------+   +------+   +------+   +------+   +------+   
              ^          ^          ^          ^          ^
              |          |          |          |          |
          +-------+  +-------+  +-------+  +-------+  +-------+
          | Embed |  | Embed |  | Embed |  | Embed |  | Embed |  ...
          +-------+  +-------+  +-------+  +-------+  +-------+
              ^          ^          ^          ^          ^
              |          |          |          |          |
w      ------>+--------->+--------->+--------->+--------->+------... 
             BOS      "show"    "flights"    "from"   "burbank"

In alternativa, come descrizione di rete CNTK. Dare un'occhiata rapida e associarla alla descrizione precedente:

    model = Sequential (
        EmbeddingLayer {150} :
        RecurrentLSTMLayer {300} :
        DenseLayer {labelDim}
    )

Le descrizioni di queste funzioni sono disponibili in: Sequential(), EmbeddingLayer{}, RecurrentLSTMLayer{}e DenseLayer{}

Configurazione di CNTK

File di configurazione

Per eseguire il training e il test di un modello in CNTK, è necessario fornire un file di configurazione che indica a CNTK quali operazioni eseguire (command variabile) e una sezione di parametro per ogni comando.

Per il comando di training, CNTK deve essere indicato:

  • come leggere i dati (reader sezione)
  • la funzione del modello e i relativi input e output nel grafico di calcolo (BrainScriptNetworkBuilder sezione)
  • iper-parametri per lo learner (SGD sezione)

Per il comando di valutazione, CNTK deve sapere come leggere i dati di test (reader sezione).

Di seguito è riportato il file di configurazione con cui si inizierà. Come si può notare, un file di configurazione CNTK è un file di testo costituito da definizioni di parametri, organizzati in una gerarchia di record. È anche possibile vedere in che modo CNTK supporta la sostituzione dei parametri di base usando la $parameterName$ sintassi . Il file effettivo contiene solo alcuni altri parametri di quanto indicato in precedenza, ma analizzarlo e individuare gli elementi di configurazione appena menzionati:

# CNTK Configuration File for creating a slot tagger and an intent tagger.

command = TrainTagger:TestTagger

makeMode = false ; traceLevel = 0 ; deviceId = "auto"

rootDir = "." ; dataDir  = "$rootDir$" ; modelDir = "$rootDir$/Models"

modelPath = "$modelDir$/slu.cmf"

vocabSize = 943 ; numLabels = 129 ; numIntents = 26    # number of words in vocab, slot labels, and intent labels

# The command to train the LSTM model
TrainTagger = {
    action = "train"
    BrainScriptNetworkBuilder = {
        inputDim = $vocabSize$
        labelDim = $numLabels$
        embDim = 150
        hiddenDim = 300

        model = Sequential (
            EmbeddingLayer {embDim} :                            # embedding
            RecurrentLSTMLayer {hiddenDim, goBackwards=false} :  # LSTM
            DenseLayer {labelDim}                                # output layer
        )

        # features
        query      = Input {inputDim}
        slotLabels = Input {labelDim}

        # model application
        z = model (query)

        # loss and metric
        ce   = CrossEntropyWithSoftmax (slotLabels, z)
        errs = ClassificationError     (slotLabels, z)

        featureNodes    = (query)
        labelNodes      = (slotLabels)
        criterionNodes  = (ce)
        evaluationNodes = (errs)
        outputNodes     = (z)
    }

    SGD = {
        maxEpochs = 8 ; epochSize = 36000

        minibatchSize = 70

        learningRatesPerSample = 0.003*2:0.0015*12:0.0003
        gradUpdateType = "fsAdaGrad"
        gradientClippingWithTruncation = true ; clippingThresholdPerSample = 15.0

        firstMBsToShowResult = 10 ; numMBsToShowResult = 100
    }

    reader = {
        readerType = "CNTKTextFormatReader"
        file = "$DataDir$/atis.train.ctf"
        randomize = true
        input = {
            query        = { alias = "S0" ; dim = $vocabSize$ ;  format = "sparse" }
            intentLabels = { alias = "S1" ; dim = $numIntents$ ; format = "sparse" }
            slotLabels   = { alias = "S2" ; dim = $numLabels$ ;  format = "sparse" }
        }
    }
}

# Test the model's accuracy (as an error count)
TestTagger = {
    action = "eval"
    modelPath = $modelPath$
    reader = {
        readerType = "CNTKTextFormatReader"
        file = "$DataDir$/atis.test.ctf"
        randomize = false
        input = {
            query        = { alias = "S0" ; dim = $vocabSize$ ;  format = "sparse" }
            intentLabels = { alias = "S1" ; dim = $numIntents$ ; format = "sparse" }
            slotLabels   = { alias = "S2" ; dim = $numLabels$ ;  format = "sparse" }
        }
    }
}

Breve analisi dei dati e della lettura dei dati

I dati sono già stati esaminati. Ma come si genera questo formato? Per la lettura del testo, questa esercitazione usa .CNTKTextFormatReader Si prevede che i dati di input siano di un formato specifico, descritto qui.

Per questa esercitazione è stato creato il corpora in due passaggi:

  • convertire i dati non elaborati in un file di testo normale che contiene colonne separate da TAB di testo delimitato da spazi. Ad esempio:

    BOS show flights from burbank to st. louis on monday EOS (TAB) flight (TAB) O O O O B-fromloc.city_name O B-toloc.city_name I-toloc.city_name O B-depart_date.day_name O
    

    Questo è progettato per essere compatibile con l'output del paste comando.

  • convertirlo in CNTK Text Format (CTF) con il comando seguente:

    python Scripts/txt2ctf.py --map query.wl intent.wl slots.wl --annotated True --input atis.test.txt --output atis.test.ctf
    

    dove i tre .wl file danno il vocabolario come file di testo normale, una riga per parola.

In questi file CTFG le colonne sono etichettate S0, S1e S2. Questi elementi sono connessi agli input di rete effettivi dalle righe corrispondenti nella definizione del lettore:

input = {
    query        = { alias = "S0" ; dim = $vocabSize$ ;  format = "sparse" }
    intentLabels = { alias = "S1" ; dim = $numIntents$ ; format = "sparse" }
    slotLabels   = { alias = "S2" ; dim = $numLabels$ ;  format = "sparse" }
}

Esecuzione

È possibile trovare il file di configurazione precedente sotto il nome SLUHandsOn.cntk nella cartella di lavoro. Per eseguirlo, eseguire la configurazione precedente con questo comando:

cntk  configFile=SLUHandsOn.cntk

Verrà eseguita la configurazione, a partire dal training del modello, come definito nella sezione denominata TrainTagger. Dopo un output di log iniziale piuttosto chiacchiere, si noterà presto quanto segue:

Training 721479 parameters in 6 parameter tensors.

seguito dall'output simile al seguente:

Finished Epoch[ 1 of 8]: [Training] ce = 0.77274927 * 36007; errs = 15.344% * 36007
Finished Epoch[ 2 of 8]: [Training] ce = 0.27009664 * 36001; errs = 5.883% * 36001
Finished Epoch[ 3 of 8]: [Training] ce = 0.16390425 * 36005; errs = 3.688% * 36005
Finished Epoch[ 4 of 8]: [Training] ce = 0.13121604 * 35997; errs = 2.761% * 35997
Finished Epoch[ 5 of 8]: [Training] ce = 0.09308497 * 36000; errs = 2.028% * 36000
Finished Epoch[ 6 of 8]: [Training] ce = 0.08537533 * 35999; errs = 1.917% * 35999
Finished Epoch[ 7 of 8]: [Training] ce = 0.07477648 * 35997; errs = 1.686% * 35997
Finished Epoch[ 8 of 8]: [Training] ce = 0.06114417 * 36018; errs = 1.380% * 36018

Questo illustra come l'apprendimento procede attraverso i periodi (passa attraverso i dati). Ad esempio, dopo due periodi, il criterio di entropia incrociata, denominato ce nel file di configurazione, ha raggiunto 0,27 come misurato sui campioni 36001 di questo periodo e che la percentuale di errore è 5,883% sugli stessi campioni di training 36016.

Il 36001 deriva dal fatto che la configurazione definiva le dimensioni dell'epoca come 36000. La dimensione del periodo è il numero di campioni conteggiati come token di parola, non frasi da elaborare tra checkpoint del modello. Poiché le frasi hanno una lunghezza variabile e non necessariamente sommano a multipli di esattamente 36000 parole, si noterà una piccola variazione.

Una volta completato il training (poco meno di 2 minuti su Titan-X o un Surface Book), CNTK procederà con l'azione EvalTagger

Final Results: Minibatch[1-1]: errs = 2.922% * 10984; ce = 0.14306181 * 10984; perplexity = 1.15380111

Ad esempio, nel set di test, le etichette slot sono state stimate con una percentuale di errore del 2,9%. Non affatto male, per un sistema così semplice!

In un computer solo CPU può essere 4 o più volte più lento. Per assicurarsi che il sistema sia in corso, è possibile abilitare la traccia per visualizzare i risultati parziali, che dovrebbero apparire ragionevolmente rapidamente:

cntk  configFile=SLUHandsOn.cntk  traceLevel=1

Epoch[ 1 of 8]-Minibatch[   1-   1, 0.19%]: ce = 4.86535690 * 67; errs = 100.000% * 67 
Epoch[ 1 of 8]-Minibatch[   2-   2, 0.39%]: ce = 4.83886670 * 63; errs = 57.143% * 63
Epoch[ 1 of 8]-Minibatch[   3-   3, 0.58%]: ce = 4.78657442 * 68; errs = 36.765% * 68
...

Se non si vuole attendere il completamento di questa operazione, è possibile eseguire un modello intermedio, ad esempio

cntk  configFile=SLUHandsOn.cntk  command=TestTagger  modelPath=Models/slu.cmf.4
Final Results: Minibatch[1-1]: errs = 3.851% * 10984; ce = 0.18932937 * 10984; perplexity = 1.20843890

o testare anche il modello con training preliminare, che è possibile trovare nella cartella di lavoro:

cntk  configFile=SLUHandsOn.cntk  command=TestTagger  modelPath=slu.forward.nobn.cmf
Final Results: Minibatch[1-1]: errs = 2.922% * 10984; ce = 0.14306181 * 10984; perplexity = 1.15380111

Modifica del modello

Di seguito verranno fornite attività per modificare le configurazioni CNTK. Le soluzioni vengono fornite alla fine di questo documento... ma per favore prova senza!

Informazioni su word Sequential()

Prima di passare alle attività, si esaminerà di nuovo il modello appena eseguito. Il modello è descritto in quello che chiamiamo stile di composizione della funzione.

    model = Sequential (
        EmbeddingLayer {embDim} :                            # embedding
        RecurrentLSTMLayer {hiddenDim, goBackwards=false} :  # LSTM
        DenseLayer {labelDim, initValueScale=7}              # output layer
    )

dove i due punti (:) è la sintassi di BrainScript di esprimere matrici. Ad esempio, (F:G:H) è una matrice con tre elementi, F, Ge H.

È possibile avere familiarità con la notazione "sequenziale" di altri toolkit di rete neurale. In caso contrario, Sequential() è un'operazione potente che, in breve, consente di esprimere in modo compatto una situazione molto comune nelle reti neurali in cui un input viene elaborato propagandolo attraverso una progressione dei livelli. Sequential() accetta una matrice di funzioni come argomento e restituisce una nuova funzione che richiama questa funzione in ordine, ogni volta che passa l'output di uno al successivo. Ad esempio,

FGH = Sequential (F:G:H)
y = FGH (x)

significa uguale a

y = H(G(F(x))) 

Questo è noto come "composizione di funzioni" ed è particolarmente utile per esprimere le reti neurali, che spesso hanno questo formato:

     +-------+   +-------+   +-------+
x -->|   F   |-->|   G   |-->|   H   |--> y
     +-------+   +-------+   +-------+

Tornare al modello a portata di mano, l'espressione indica semplicemente che il Sequential modello ha questo formato:

     +-----------+   +----------------+   +------------+
x -->| Embedding |-->| Recurrent LSTM |-->| DenseLayer |--> y
     +-----------+   +----------------+   +------------+

Attività 1: Aggiungere la normalizzazione batch

Si vogliono ora aggiungere nuovi livelli al modello, in particolare la normalizzazione batch.

La normalizzazione batch è una tecnica comune per velocizzare la convergenza. Viene spesso usato per le configurazioni di elaborazione delle immagini, ad esempio l'altro lab pratico sul riconoscimento delle immagini. Ma potrebbe funzionare anche per i modelli ricorrenti?

L'attività sarà quindi quella di inserire livelli di normalizzazione batch prima e dopo il livello LSTM ricorrente. Se sono stati completati i lab pratici per l'elaborazione delle immagini, è possibile ricordare che il livello di normalizzazione batch ha questo formato:

BatchNormalizationLayer{}

Quindi, procedere e modificare la configurazione e vedere cosa accade.

Se tutto è andato bene, si noterà non solo una maggiore velocità di convergenza (ce e errs) rispetto alla configurazione precedente, ma anche una migliore percentuale di errore del 2,0% (rispetto al 2,9%):

Training 722379 parameters in 10 parameter tensors.

Finished Epoch[ 1 of 8]: [Training] ce = 0.29396894 * 36007; errs = 5.621% * 36007 
Finished Epoch[ 2 of 8]: [Training] ce = 0.10104186 * 36001; errs = 2.280% * 36001
Finished Epoch[ 3 of 8]: [Training] ce = 0.05012737 * 36005; errs = 1.258% * 36005
Finished Epoch[ 4 of 8]: [Training] ce = 0.04116407 * 35997; errs = 1.108% * 35997
Finished Epoch[ 5 of 8]: [Training] ce = 0.02602344 * 36000; errs = 0.756% * 36000
Finished Epoch[ 6 of 8]: [Training] ce = 0.02234042 * 35999; errs = 0.622% * 35999
Finished Epoch[ 7 of 8]: [Training] ce = 0.01931362 * 35997; errs = 0.667% * 35997
Finished Epoch[ 8 of 8]: [Training] ce = 0.01714253 * 36018; errs = 0.522% * 36018

Final Results: Minibatch[1-1]: errs = 2.039% * 10984; ce = 0.12888706 * 10984; perplexity = 1.13756164

Se non si vuole attendere il completamento del training, è possibile trovare il modello risultante sotto il nome slu.forward.cmf.

Vedere la soluzione qui.

Attività 2: Aggiungere un lookahead

Il nostro modello ricorrente soffre di un deficit strutturale: poiché la ricorrenza viene eseguita da sinistra a destra, la decisione per un'etichetta slot non ha informazioni sulle parole future. Il modello è leggermente inclinato. L'attività consisterà nel modificare il modello in modo che l'input alla ricorrenza sia costituito non solo dalla parola corrente, ma anche da quello successivo (lookahead).

La soluzione deve essere in stile di composizione della funzione. Di conseguenza, è necessario scrivere una funzione BrainScript che esegue le operazioni seguenti:

  • accettare un argomento di input;
  • calcolare il "valore futuro" immediato di questo input usando la FutureValue() funzione (usare questo formato specifico: FutureValue (0, input, defaultHiddenActivation=0)); e
  • concatenare i due in un vettore di due volte la dimensione di incorporamento usando Splice() (usare questo formato: Splice (x:y))

e quindi inserire questa funzione nell'oggetto tra l'incorporamento Sequence() e il livello ricorrente. Se tutto va bene, verrà visualizzato l'output seguente:

Training 902679 parameters in 10 parameter tensors.

Finished Epoch[ 1 of 8]: [Training] ce = 0.30500536 * 36007; errs = 5.904% * 36007
Finished Epoch[ 2 of 8]: [Training] ce = 0.09723847 * 36001; errs = 2.167% * 36001
Finished Epoch[ 3 of 8]: [Training] ce = 0.04082365 * 36005; errs = 1.047% * 36005
Finished Epoch[ 4 of 8]: [Training] ce = 0.03219930 * 35997; errs = 0.867% * 35997
Finished Epoch[ 5 of 8]: [Training] ce = 0.01524993 * 36000; errs = 0.414% * 36000
Finished Epoch[ 6 of 8]: [Training] ce = 0.01367533 * 35999; errs = 0.383% * 35999
Finished Epoch[ 7 of 8]: [Training] ce = 0.00937027 * 35997; errs = 0.278% * 35997
Finished Epoch[ 8 of 8]: [Training] ce = 0.00584430 * 36018; errs = 0.147% * 36018

Final Results: Minibatch[1-1]: errs = 1.839% * 10984; ce = 0.12023170 * 10984; perplexity = 1.12775812

Questo ha funzionato! Sapere qual è la parola successiva consente al tagger slot di ridurre il tasso di errore dal 2,0% al 1,84%.

Se non si vuole attendere il completamento del training, è possibile trovare il modello risultante sotto il nome slu.forward.lookahead.cmf.

Vedere la soluzione qui.

Attività 3: Modello ricorrente bidirezionale

Aha, conoscenza delle parole future aiutano. Quindi, invece di una sola parola lookahead, perché non guardare avanti fino alla fine della frase, attraverso una ricorrenza indietro? Creare un modello bidirezionale.

L'attività consiste nell'implementare un nuovo livello che esegue una ricorsione avanti e indietro sui dati e concatena i vettori di output.

Si noti, tuttavia, che questo comportamento differisce dall'attività precedente in quanto il livello bidirezionale contiene parametri del modello imparabili. Nello stile di composizione della funzione, il modello per implementare un livello con i parametri del modello consiste nel scrivere una funzione factory che crea un oggetto funzione.

Un oggetto funzione, noto anche come functor, è un oggetto che è sia una funzione che un oggetto . Ciò significa che non sono ancora presenti dati che possono essere richiamati come se fosse una funzione.

Ad esempio, LinearLayer{outDim} è una funzione factory che restituisce un oggetto funzione che contiene una matrice Wdi peso, una distorsione be un'altra funzione per calcolare W * input + b. Ad esempio, verrà LinearLayer{1024} creato questo oggetto funzione, che può quindi essere usato come qualsiasi altra funzione, anche immediatamente: LinearLayer{1024}(x).

Confuso? Di seguito viene illustrato come implementare un nuovo livello che combina un livello lineare con una normalizzazione batch successiva. Per consentire la composizione della funzione, il livello deve essere realizzato come funzione factory, che potrebbe essere simile al seguente:

LinearLayerWithBN {outDim} = {
    F = LinearLayer {outDim}
    G = BatchNormalization {normalizationTimeConstant=2048}
    apply (x) = G(F(x))
}.apply

Richiamare questa funzione factory creerà prima di tutto un record (indicato da {...}) con tre membri: F, Ge apply. In questo esempio F e G sono oggetti funzione stessi ed apply è la funzione da applicare ai dati. L'aggiunta .apply a questa espressione significa cosa .x significa sempre in BrainScript, per accedere a un membro del record. Ad esempio, la chiamata LinearLayerWithBN{1024} creerà un oggetto contenente un oggetto funzione a livello lineare denominato F, un oggetto Gfunzione di normalizzazione batch e apply che è la funzione che implementa l'operazione effettiva di questo livello usando F e G. Verrà quindi restituito apply. All'esterno, apply() sembra e si comporta come una funzione. Sotto le quinte, tuttavia, apply() manterrà il record a cui appartiene e quindi manterrà l'accesso alle istanze specifiche di F e G.

Ora torniamo al nostro compito a portata di mano. Sarà ora necessario creare una funzione factory, molto simile all'esempio precedente. Si creerà una funzione factory che crea due istanze ricorrenti del livello (una avanti, una all'indietro) e quindi definisce una apply (x) funzione che applica entrambe le istanze di livello alla stessa x e concatena i due risultati.

Va bene, prova! Per sapere come realizzare una ricorsione indietro in CNTK, prendere un suggerimento da come viene eseguita la ricorsione in avanti. Eseguire anche le operazioni seguenti:

  • rimuovere il lookahead di una parola aggiunto nell'attività precedente, che si mira a sostituire; E
  • modificare il hiddenDim parametro da 300 a 150 per mantenere limitato il numero totale di parametri del modello.

L'esecuzione di questo modello produrrà correttamente l'output seguente:

Training 542379 parameters in 13 parameter tensors.

Finished Epoch[ 1 of 8]: [Training] ce = 0.27651655 * 36007; errs = 5.288% * 36007
Finished Epoch[ 2 of 8]: [Training] ce = 0.08179804 * 36001; errs = 1.869% * 36001
Finished Epoch[ 3 of 8]: [Training] ce = 0.03528780 * 36005; errs = 0.828% * 36005
Finished Epoch[ 4 of 8]: [Training] ce = 0.02602517 * 35997; errs = 0.675% * 35997
Finished Epoch[ 5 of 8]: [Training] ce = 0.01310307 * 36000; errs = 0.386% * 36000
Finished Epoch[ 6 of 8]: [Training] ce = 0.01310714 * 35999; errs = 0.358% * 35999
Finished Epoch[ 7 of 8]: [Training] ce = 0.00900459 * 35997; errs = 0.300% * 35997
Finished Epoch[ 8 of 8]: [Training] ce = 0.00589050 * 36018; errs = 0.161% * 36018

Final Results: Minibatch[1-1]: errs = 1.830% * 10984; ce = 0.11924878 * 10984; perplexity = 1.12665017

Funziona come un fascino! Questo modello raggiunge il 1,83%, un bit leggermente migliore rispetto al modello lookahead precedente. Il modello bidirezionale ha meno parametri del 40% rispetto a quello lookahead. Tuttavia, se si torna indietro e si esamina attentamente l'output completo del log (non visualizzato in questa pagina Web), è possibile che lookahead uno sottoposto a training circa il 30% più velocemente. Ciò è dovuto al fatto che il modello lookahead ha sia dipendenze orizzontali meno orizzontali (una anziché due ricorrenze) che prodotti matrice più grandi e può quindi ottenere un parallelismo più elevato.

Vedere la soluzione qui.

Attività 4: Classificazione finalità

Si scopre che il modello costruito finora può essere facilmente trasformato in un classificatore finalità. Tenere presente che il file di dati contiene questa colonna aggiuntiva denominata S1. Questa colonna contiene una singola etichetta per frase, che indica la finalità della query di trovare informazioni su argomenti come airport o airfare.

L'attività di classificazione di un'intera sequenza in una singola etichetta è denominata classificazione sequenza. Il classificatore di sequenza verrà implementato come LSTM ricorrente (già presente) di cui prendiamo lo stato nascosto del passaggio finale. In questo modo viene specificato un singolo vettore per ogni sequenza. Questo vettore viene quindi inserito in un livello denso per la classificazione softmax.

CNTK ha un'operazione per estrarre l'ultimo stato da una sequenza, denominata BS.Sequences.Last(). Questa operazione rispetta il fatto che lo stesso minibatch può contenere sequenze di lunghezze molto diverse e che sono disposte in memoria in un formato compresso. Analogamente, per la ricorsione all'indietro, è possibile usare BS.Sequences.First().

L'attività consiste nel modificare la rete bidirezionale dall'attività 3 in modo che l'ultimo fotogramma venga estratto dalla ricorsione in avanti e il primo fotogramma venga estratto dalla ricorsione indietro e i due vettori siano concatenati. Il vettore concatenato (talvolta chiamato vettore di pensiero) sarà quindi l'input dello strato denso.

Inoltre, è necessario modificare l'etichetta dallo slot all'etichetta della finalità: rinominare semplicemente la variabile di input (slotLabels) in modo che corrisponda al nome usato nella sezione reader per le etichette delle finalità e che corrisponda anche alla dimensione.

Provare la modifica. Se lo fai bene, tuttavia, sarai confrontato con un messaggio di errore vexing, e un lungo a quel punto:

EXCEPTION occurred: Dynamic axis layout '*' is shared between inputs 'intentLabels'
and 'query', but layouts generated from the input data are incompatible on this axis.
Are you using different sequence lengths? Did you consider adding a DynamicAxis()
to the Input nodes?

"Si usano lunghezze di sequenza diverse?" Oh sì! La query e l'etichetta della finalità do-the intent label è un solo token per ogni query. È una sequenza di 1 elemento! Quindi, come risolvere questo problema?

CNTK consente a variabili diverse nella rete di avere lunghezze di sequenza diverse. È possibile considerare la lunghezza della sequenza come una dimensione di tensore simbolica aggiuntiva. Le variabili della stessa lunghezza condividono la stessa dimensione di lunghezza simbolica. Se due variabili hanno lunghezze diverse, questa deve essere dichiarata in modo esplicito. In caso contrario, CNTK presuppone che tutte le variabili condividono la stessa lunghezza simbolica.

Questa operazione viene eseguita creando un nuovo oggetto asse dinamico e associandolo a uno degli input, come indicato di seguito:

    n = DynamicAxis()
    query = Input {inputDim, dynamicAxis=n}

CNTK ha un asse predefinito. Come si può immaginare dall'eccezione precedente, il nome è '*'.
Pertanto, è sufficiente dichiarare un nuovo asse; l'altro input (intentLabels) continuerà a usare l'asse predefinito.

A questo momento è consigliabile eseguire e vedere l'output seguente:

Training 511376 parameters in 13 parameter tensors.

Finished Epoch[ 1 of 8]: [Training] ce = 1.17365003 * 2702; errs = 21.318% * 2702
Finished Epoch[ 2 of 8]: [Training] ce = 0.40112341 * 2677; errs = 9.189% * 2677
Finished Epoch[ 3 of 8]: [Training] ce = 0.17041608 * 2688; errs = 4.167% * 2688
Finished Epoch[ 4 of 8]: [Training] ce = 0.09521124 * 2702; errs = 2.739% * 2702
Finished Epoch[ 5 of 8]: [Training] ce = 0.08287138 * 2697; errs = 2.262% * 2697
Finished Epoch[ 6 of 8]: [Training] ce = 0.07138554 * 2707; errs = 2.032% * 2707
Finished Epoch[ 7 of 8]: [Training] ce = 0.06220047 * 2677; errs = 1.419% * 2677
Finished Epoch[ 8 of 8]: [Training] ce = 0.05072431 * 2686; errs = 1.340% * 2686

Final Results: Minibatch[1-1]: errs = 4.143% * 893; ce = 0.27832144 * 893; perplexity = 1.32091072

Senza molta fatica, abbiamo raggiunto un tasso di errore del 4,1%. Molto bello per un primo colpo (anche se non abbastanza stato dell'arte su questo compito, che è al 3%).

Si noterà però che il numero di campioni per periodo è ora di circa 2700. Questo perché questo è il numero di campioni di etichetta, di cui ora abbiamo solo una per frase. Abbiamo un numero notevolmente ridotto di segnali di supervisione in questa attività. Questo dovrebbe incoraggiarci a cercare di aumentare le dimensioni del minibatch. Provare 256 invece di 70:

Finished Epoch[ 1 of 8]: [Training] ce = 1.11500325 * 2702; errs = 19.282% * 2702
Finished Epoch[ 2 of 8]: [Training] ce = 0.29961089 * 2677; errs = 6.052% * 2677
Finished Epoch[ 3 of 8]: [Training] ce = 0.09018802 * 2688; errs = 2.418% * 2688
Finished Epoch[ 4 of 8]: [Training] ce = 0.04838102 * 2702; errs = 1.258% * 2702
Finished Epoch[ 5 of 8]: [Training] ce = 0.02996789 * 2697; errs = 0.704% * 2697
Finished Epoch[ 6 of 8]: [Training] ce = 0.02142932 * 2707; errs = 0.517% * 2707
Finished Epoch[ 7 of 8]: [Training] ce = 0.01220149 * 2677; errs = 0.299% * 2677
Finished Epoch[ 8 of 8]: [Training] ce = 0.01312233 * 2686; errs = 0.186% * 2686

Questo sistema impara molto meglio! Si noti, tuttavia, che questa differenza è probabilmente un artefatto causato dal nostro fsAdagrad schema di normalizzazione del gradiente e in genere scomparirà presto quando si usano set di dati di dimensioni maggiori.

La percentuale di errori risultante è superiore, tuttavia:

Final Results: Minibatch[1-1]: errs = 4.479% * 893; ce = 0.31638223 * 893; perplexity = 1.37215463

ma questa differenza corrisponde effettivamente a 3 errori, che non è significativo.

Vedere la soluzione qui.

Attività 5: Training parallelo

Infine, se sono presenti più GPU, CNTK consente di parallelizzare il training usando MPI (Message-Passing Interface). Questo modello è troppo piccolo per aspettarsi qualsiasi accelerazione; la parallelizzazione di un modello di piccole dimensioni sottoutilizzerà gravemente le GPU disponibili. Tuttavia, facciamoci passare attraverso i movimenti, in modo che tu sappia come farlo una volta che passi ai carichi di lavoro reali.

Aggiungere le righe seguenti al SGD blocco:

SGD = {
    ...
    parallelTrain = {
        parallelizationMethod = "DataParallelSGD"
        parallelizationStartEpoch = 1
        distributedMBReading = true
        dataParallelSGD = { gradientBits = 2 }
    }
}

e quindi eseguire questo comando:

mpiexec -np 4 cntk  configFile=SLUHandsOn_Solution4.cntk  stderr=Models/log  parallelTrain=true  command=TrainTagger

Verrà eseguito il training su 4 GPU usando l'algoritmo SGD a 1 bit (in questo caso SGD a 2 bit). L'approssimazione non ha danneggiato l'accuratezza: la frequenza di errore è 4,367%, due errori in più (eseguire l'azione TestTagger separatamente in una singola GPU).

Conclusioni

Questa esercitazione ha introdotto lo stile di composizione delle funzioni come mezzo compatto per rappresentare le reti. Molti tipi di rete neurale sono adatti per rappresentarli in questo modo, ovvero una traduzione più diretta e meno soggetta a errori di un grafico in una descrizione di rete.

Questa esercitazione ha fatto pratica per accettare una configurazione esistente nello stile di composizione delle funzioni e modificarla in modi specifici:

  • aggiunta di un livello (dalla raccolta di livelli predefiniti)
  • definizione e uso di una funzione
  • definizione e uso di una funzione factory di livello

L'esercitazione ha anche illustrato la gestione di più dimensioni temporali ed è stato illustrato come parallelizzare il training.

Soluzioni

Di seguito sono riportate le soluzioni alle attività precedenti. Ehi, no barare!

Soluzione 1: Aggiunta della normalizzazione batch

La funzione del modello modificata ha questo formato:

    model = Sequential (
        EmbeddingLayer {embDim} :                            # embedding
        BatchNormalizationLayer {} :           ##### added
        RecurrentLSTMLayer {hiddenDim, goBackwards=false} :  # LSTM
        BatchNormalizationLayer {} :           ##### added
        DenseLayer {labelDim}                                # output layer
    )

Soluzione 2: Aggiungere un lookahead

La funzione lookahead può essere definita come segue:

    OneWordLookahead (x) = Splice (x : DelayLayer {T=-1} (x))

e verrà inserito nel modello come segue:

    model = Sequential (
        EmbeddingLayer {embDim} :
        OneWordLookahead :                   ##### added
        BatchNormalizationLayer {} :
        RecurrentLSTMLayer {hiddenDim, goBackwards=false} :
        BatchNormalizationLayer {} :
        DenseLayer {labelDim}
    )

Soluzione 3: Modello ricorrente bidirezionale

Il livello ricorrente bidirezionale può essere scritto come segue:

    BiRecurrentLSTMLayer {outDim} = {
        F = RecurrentLSTMLayer {outDim, goBackwards=false}
        G = RecurrentLSTMLayer {outDim, goBackwards=true}
        apply (x) = Splice (F(x):G(x))
    }.apply

e quindi usato come segue:

    hiddenDim = 150      ##### changed from 300 to 150

    model = Sequential (
        EmbeddingLayer {embDim} :
        ###OneWordLookahead :                   ##### removed
        BatchNormalizationLayer {} :
        BiRecurrentLSTMLayer {hiddenDim} :
        BatchNormalizationLayer {} :
        DenseLayer {labelDim}
    )

Soluzione 4: Classificazione finalità

Ridurre le sequenze all'ultimo/primo nascosto del livello ricorrente:

        apply (x) = Splice (BS.Sequences.Last(F(x)):BS.Sequences.First(G(x)))
        ##### added Last() and First() calls ^^^

Modificare l'input dell'etichetta dallo slot alla finalità:

    intentDim = $numIntents$    ###### name change
    ...
        DenseLayer {intentDim}                      ##### different dimension
    ...
    intentLabels = Input {intentDim}
    ...
    ce   = CrossEntropyWithSoftmax (intentLabels, z)
    errs = ErrorPrediction         (intentLabels, z)
    ...
    labelNodes      = (intentLabels)

Usare un nuovo asse dinamico:

    n = DynamicAxis()                               ##### added
    query        = Input {inputDim, dynamicAxis=n}  ##### use dynamic axis

Acknowledgment (Riconoscimento)

Vorremmo ringraziare Derek Liu per aver preparato la base di questa esercitazione.