Compartilhar via


Laboratórios práticos Reconhecimento vocal com redes recorrentes

Observe que este tutorial requer a versão mestra mais recente ou o próximo CNTK 1.7.1 que será lançado em breve.

Este laboratório prático mostra como implementar uma rede recorrente para processar texto para as tarefas dos Serviços de Informações de Viagens Aéreas (ATIS) de marcação de slot e classificação de intenção. Começaremos com uma inserção direta seguida de um LSTM recorrente. Em seguida, o estenderemos para incluir palavras vizinhas e executaremos bidirecionalmente. Por fim, transformaremos esse sistema em um classificador de intenção.

As técnicas que você praticará incluem:

  • descrição do modelo redigindo blocos de camada em vez de escrever fórmulas
  • criando seu próprio bloco de camada
  • variáveis com comprimentos de sequência diferentes na mesma rede
  • treinamento paralelo

Presumimos que você esteja familiarizado com os conceitos básicos de aprendizado profundo e esses conceitos específicos:

Pré-requisitos

Presumimos que você já instalou o CNTK e pode executar o comando CNTK. Este tutorial foi realizado no KDD 2016 e requer um build recente, consulte aqui para obter instruções de instalação. Basta seguir as instruções para baixar um pacote de instalação binária dessa página.

Em seguida, baixe um arquivo ZIP (cerca de 12 MB): clique neste link e, em seguida, no botão Baixar. O arquivo contém os arquivos deste tutorial. Aguarde o arquivo e defina o diretório de trabalho como SLUHandsOn. Os arquivos com os quais você trabalhará são:

Por fim, é altamente recomendável executar isso em um computador com uma GPU compatível com CUDA compatível com CUDA. O aprendizado profundo sem GPUs não é divertido.

Estrutura de tarefa e modelo

A tarefa que queremos abordar neste tutorial é a marcação de slot. Usamos o corpus do ATIS. A ATIS contém consultas de computador humano do domínio dos Serviços de Informações de Viagens Aéreas, e nossa tarefa será anotar (marca) cada palavra de uma consulta se ela pertence a um item específico de informações (slot) e qual delas.

Os dados em sua pasta de trabalho já foram convertidos no "Formato de Texto CNTK". Vamos examinar um exemplo do arquivo atis.test.ctfde conjunto de testes:

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

Esse arquivo tem 7 colunas:

  • uma ID de sequência (19). Há 11 entradas com essa ID de sequência. Isso significa que a sequência 19 consiste em 11 tokens;
  • coluna S0, que contém índices de palavras numéricas;
  • uma coluna de comentário indicada por #, para permitir que um leitor humano saiba o que significa o índice de palavras numéricas; As colunas de comentário são ignoradas pelo sistema. BOS e EOS são palavras especiais para indicar início e fim da frase, respectivamente;
  • column S1 is an intent label, which will only will use in the last part of the tutorial;
  • outra coluna de comentário que mostra o rótulo legível humano do índice de intenção numérica;
  • column S2 is the slot label, represented as a numeric index; and
  • outra coluna de comentário que mostra o rótulo legível humano do índice de rótulo numérico.

A tarefa da rede neural é examinar a consulta (coluna S0) e prever o rótulo do slot (coluna S2). Como você pode ver, cada palavra na entrada recebe um rótulo O vazio ou um rótulo de slot que começa para B- a primeira palavra e para I- qualquer palavra consecutiva adicional que pertence ao mesmo slot.

O modelo que usaremos é um modelo recorrente que consiste em uma camada de inserção, uma célula LSTM recorrente e uma camada densa para calcular as probabilidades posteriores:

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"

Ou, como uma descrição de rede CNTK. Dê uma olhada rápida e combine-a com a descrição acima:

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

Descrições dessas funções podem ser encontradas em: Sequential(), , EmbeddingLayer{}, RecurrentLSTMLayer{}e DenseLayer{}

Configuração do CNTK

Arquivo de configuração

Para treinar e testar um modelo no CNTK, precisamos fornecer um arquivo de configuração que informe ao CNTK quais operações você deseja executar (command variável) e uma seção de parâmetro para cada comando.

Para o comando de treinamento, o CNTK precisa ser informado:

  • como ler os dados (reader seção)
  • a função de modelo e suas entradas e saídas no grafo de computação (BrainScriptNetworkBuilder seção)
  • hipermetrâmetros para o aprendiz (SGD seção)

Para o comando de avaliação, o CNTK precisa saber como ler os dados de teste (reader seção).

Veja a seguir o arquivo de configuração com o qual começaremos. Como você vê, um arquivo de configuração CNTK é um arquivo de texto que consiste em definições de parâmetros, que são organizados em uma hierarquia de registros. Você também pode ver como o CNTK dá suporte à substituição de parâmetro básico usando a $parameterName$ sintaxe. O arquivo real contém apenas mais alguns parâmetros do que mencionado acima, mas verifique-o e localize os itens de configuração mencionados:

# 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" }
        }
    }
}

Uma breve olhada em dados e leitura de dados

Já examinamos os dados. Mas como você gera esse formato? Para ler texto, este tutorial usa o CNTKTextFormatReader. Ele espera que os dados de entrada sejam de um formato específico, que é descrito aqui.

Para este tutorial, criamos a corporação por duas etapas:

  • converta os dados brutos em um arquivo de texto sem formatação que contém colunas separadas por TAB de texto separado por espaço. Por exemplo:

    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
    

    Isso deve ser compatível com a saída do paste comando.

  • converta-o em CTF (Formato de Texto CNTK) com o seguinte comando:

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

    em que os três .wl arquivos dão o vocabulário como arquivos de texto sem formatação, uma linha por palavra.

Nesses arquivos CTFG, nossas colunas são rotuladas S0e S1S2. Eles são conectados às entradas de rede reais pelas linhas correspondentes na definição do leitor:

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

Executando-o

Você pode encontrar o arquivo de configuração acima sob o nome SLUHandsOn.cntk na pasta de trabalho. Para executá-lo, execute a configuração acima por este comando:

cntk  configFile=SLUHandsOn.cntk

Isso executará nossa configuração, começando com o treinamento de modelo conforme definido na seção que nomeamos TrainTagger. Depois de uma saída de log inicial um pouco tagarela, você verá isso em breve:

Training 721479 parameters in 6 parameter tensors.

seguido por uma saída como esta:

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

Isso mostra como o aprendizado prossegue em épocas (passa pelos dados). Por exemplo, após duas épocas, o critério de entropia cruzada, que tínhamos nomeado ce no arquivo de configuração, atingiu 0,27 conforme medido nos exemplos 36001 dessa época e que a taxa de erro é de 5,883% nesses mesmos exemplos de treinamento 36016.

O 36001 vem do fato de que nossa configuração definiu o tamanho da época como 36000. O tamanho da época é o número de exemplos , contados como tokens de palavra, não frases - para processar entre pontos de verificação de modelo. Como as frases têm comprimento variado e não necessariamente somam múltiplos de precisamente 36.000 palavras, você verá alguma pequena variação.

Depois que o treinamento for concluído (pouco menos de 2 minutos em um Titan-X ou um Surface Book), o CNTK prosseguirá com a ação EvalTagger

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

Ou seja, em nosso conjunto de testes, os rótulos de slot foram previstos com uma taxa de erro de 2,9%. Nada mal, para um sistema tão simples!

Em um computador somente CPU, ele pode ser 4 ou mais vezes mais lento. Para ter certeza antecipadamente de que o sistema está progredindo, você pode habilitar o rastreamento para ver resultados parciais, que devem aparecer razoavelmente 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 você não quiser esperar até que isso seja concluído, poderá executar um modelo intermediário, por exemplo.

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

ou teste nosso modelo pré-treinado também, que você pode encontrar na pasta de trabalho:

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

Modificando o modelo

No seguinte, você receberá tarefas para praticar a modificação das configurações do CNTK. As soluções são fornecidas no final deste documento... mas tente sem!

Uma palavra sobre Sequential()

Antes de ir para as tarefas, vamos dar uma olhada novamente no modelo que acabamos de executar. O modelo é descrito no que chamamos de estilo de composição de função.

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

onde o dois-pontos (:) é a sintaxe do BrainScript de expressar matrizes. Por exemplo, (F:G:H) é uma matriz com três elementos, GFe H.

Você pode estar familiarizado com a notação "sequencial" de outros kits de ferramentas de rede neural. Caso contrário, Sequential() é uma operação poderosa que, em poucas palavras, permite expressar compactamente uma situação muito comum em redes neurais em que uma entrada é processada propagando-a por meio de uma progressão de camadas. Sequential() usa uma matriz de funções como argumento e retorna uma nova função que invoca essa função em ordem, cada vez passando a saída de uma para a outra. Por exemplo,

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

significa o mesmo que

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

Isso é conhecido como "composição de função" e é especialmente conveniente para expressar redes neurais, que geralmente têm essa forma:

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

Voltando ao nosso modelo em questão, a Sequential expressão simplesmente diz que nosso modelo tem essa forma:

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

Tarefa 1: Adicionar normalização em lote

Agora queremos adicionar novas camadas ao modelo, especificamente a normalização em lote.

A normalização em lote é uma técnica popular para acelerar a convergência. Ele geralmente é usado para configurações de processamento de imagens, por exemplo, nosso outro laboratório prático no reconhecimento de imagem. Mas poderia funcionar para modelos recorrentes também?

Portanto, sua tarefa será inserir camadas de normalização em lote antes e depois da camada LSTM recorrente. Se você concluiu os laboratórios práticos sobre o processamento de imagens, pode lembrar que a camada de normalização em lote tem esse formulário:

BatchNormalizationLayer{}

Portanto, vá em frente e modifique a configuração e veja o que acontece.

Se tudo der certo, você observará não apenas a velocidade de convergência aprimorada (ce e errs) em comparação com a configuração anterior, mas também uma taxa de erro melhor de 2,0% (em comparação com 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 você não quiser aguardar a conclusão do treinamento, poderá encontrar o modelo resultante sob o nome slu.forward.cmf.)

Confira a solução aqui.

Tarefa 2: Adicionar um Lookahead

Nosso modelo recorrente sofre de um déficit estrutural: como a recorrência é executada da esquerda para a direita, a decisão de um rótulo de slot não tem informações sobre palavras futuras. O modelo é um pouco desequilibrado. Sua tarefa será modificar o modelo de modo que a entrada para a recorrência consista não apenas na palavra atual, mas também no próximo (lookahead).

Sua solução deve estar no estilo de composição de função. Portanto, você precisará escrever uma função BrainScript que faça o seguinte:

  • aceitar um argumento de entrada;
  • compute o "valor futuro" imediato dessa entrada usando a FutureValue() função (use este formulário específico: FutureValue (0, input, defaultHiddenActivation=0)); e
  • concatenar os dois em um vetor de duas vezes a dimensão de inserção usando Splice() (use este formulário: Splice (x:y))

e insira essa função entre a Sequence() inserção e a camada recorrente. Se tudo correr bem, você verá a seguinte saída:

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

Isso funcionou! Saber qual é a próxima palavra permite que o marcador de slot reduza sua taxa de erros de 2,0% para 1,84%.

(Se você não quiser aguardar a conclusão do treinamento, poderá encontrar o modelo resultante sob o nome slu.forward.lookahead.cmf.)

Confira a solução aqui.

Tarefa 3: Modelo Recorrente Bidirecional

Aha, conhecimento de palavras futuras ajuda. Então, em vez de um lookahead de uma palavra, por que não olhar para a frente até o final da frase, através de uma recorrência retrógrada? Vamos criar um modelo bidirecional!

Sua tarefa é implementar uma nova camada que executa uma recursão para frente e para trás sobre os dados e concatena os vetores de saída.

No entanto, observe que isso difere da tarefa anterior, na qual a camada bidirecional contém parâmetros de modelo aprendizes. No estilo de composição de função, o padrão para implementar uma camada com parâmetros de modelo é escrever uma função de fábrica que cria um objeto de função.

Um objeto de função, também conhecido como functor, é um objeto que é uma função e um objeto. O que significa que nada mais que ele contenha dados ainda pode ser invocado como se fosse uma função.

Por exemplo, LinearLayer{outDim} é uma função de fábrica que retorna um objeto de função que contém uma matriz Wde peso, um viés be outra função para computação W * input + b. Por exemplo, dizer LinearLayer{1024} criará esse objeto de função, que pode ser usado como qualquer outra função, também imediatamente: LinearLayer{1024}(x).

Confuso? Vamos dar um exemplo: vamos implementar uma nova camada que combina uma camada linear com uma normalização em lote subsequente. Para permitir a composição da função, a camada precisa ser realizada como uma função de fábrica, que pode ter esta aparência:

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

Invocar essa função de fábrica primeiro criará um registro (indicado por {...}) com três membros: F, Ge apply. Neste exemplo, F e G são objetos de função em si, e apply é a função a ser aplicada aos dados. Acrescentar .apply a essa expressão significa o que .x sempre significa no BrainScript acessar um membro de registro. Portanto, a chamada LinearLayerWithBN{1024} , por exemplo, criará um objeto que contém um objeto de função de camada linear chamado F, um objeto Gde função de normalização em lote e apply que é a função que implementa a operação real dessa camada usando F e G. Em seguida, ele retornará apply. Para o lado de fora, apply() parece e se comporta como uma função. No entanto, sob o capô, apply() manterá o registro ao qual pertence e, portanto, manterá o acesso a suas instâncias específicas de F e G.

Agora de volta à nossa tarefa em questão. Agora você precisará criar uma função de fábrica, muito semelhante ao exemplo acima. Você deve criar uma função de fábrica que cria duas instâncias de camada recorrentes (uma para frente, uma para trás) e define uma apply (x) função que aplica ambas as instâncias de camada ao mesmo x e concatena os dois resultados.

Tudo bem, tente! Para saber como realizar uma recursão para trás no CNTK, dê uma dica de como a recursão para a frente é feita. Faça o seguinte:

  • remova o lookahead de uma palavra que você adicionou na tarefa anterior, que pretendemos substituir; E
  • altere o hiddenDim parâmetro de 300 para 150, para manter o número total de parâmetros de modelo limitado.

Executar esse modelo com êxito produzirá a seguinte saída:

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

Funciona como um charme! Esse modelo alcança 1,83%, um pouco melhor do que o modelo lookahead acima. O modelo bidirecional tem 40% menos parâmetros do que o lookahead. No entanto, se você voltar e examinar de perto a saída de log completa (não mostrada nesta página da Web), poderá descobrir que o lookahead treinou cerca de 30% mais rápido. Isso ocorre porque o modelo lookahead tem dependências menos horizontais (uma em vez de duas recorrências) e produtos de matriz maiores e, portanto, pode alcançar maior paralelismo.

Confira a solução aqui.

Tarefa 4: Classificação de Intenção

Acontece que o modelo que construímos até agora pode facilmente ser transformado em um classificador de intenção. Lembre-se de que nosso arquivo de dados continha esta coluna adicional chamada S1. Esta coluna contém um único rótulo por frase, indicando a intenção da consulta de encontrar informações sobre tópicos como airport ou airfare.

A tarefa de classificar uma sequência inteira em um único rótulo é chamada de classificação de sequência. Nosso classificador de sequência será implementado como um LSTM recorrente (já temos isso) do qual tomamos seu estado oculto de sua etapa final. Isso nos dá um único vetor para cada sequência. Esse vetor é então alimentado em uma camada densa para classificação softmax.

O CNTK tem uma operação para extrair o último estado de uma sequência, chamada BS.Sequences.Last(). Esta operação honra o fato de que a mesma minibatch pode conter sequências de comprimentos muito diferentes e que elas são organizadas na memória em um formato empacotado. Da mesma forma, para recursão para trás, podemos usar BS.Sequences.First().

Sua tarefa é modificar a rede bidirecional da Tarefa 3 de modo que o último quadro seja extraído da recursão para a frente e o primeiro quadro seja extraído da recursão para trás e os dois vetores sejam concatenados. O vetor concatenado (às vezes chamado de vetor de pensamento) deve então ser a entrada da camada densa.

Além disso, você deve alterar o rótulo do slot para o rótulo de intenção: basta renomear a variável de entrada (slotLabels) para, em vez disso, corresponder ao nome usado na seção leitor para os rótulos de intenção e também corresponder à dimensão.

Tente a modificação. No entanto, se você fizer isso direito, será confrontado com uma mensagem de erro vexatório e uma longa:

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?

"Você está usando comprimentos de sequência diferentes?" Oh sim! A consulta e o rótulo de intenção fazem- o rótulo de intenção é apenas um único token por consulta. É uma sequência de 1 elemento! Então, como corrigir isso?

O CNTK permite que variáveis diferentes na rede tenham comprimentos de sequência diferentes. Você pode considerar o comprimento da sequência como uma dimensão tensor simbólica adicional. Variáveis do mesmo comprimento compartilham a mesma dimensão de comprimento simbólico. Se duas variáveis tiverem comprimentos diferentes, isso deverá ser declarado explicitamente, caso contrário, o CNTK assumirá que todas as variáveis compartilham o mesmo comprimento simbólico.

Isso é feito criando um novo objeto de eixo dinâmico e associando-o a uma das entradas, da seguinte maneira:

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

O CNTK tem um eixo padrão. Como você pode adivinhar da exceção acima, seu nome é '*'.
Portanto, você só precisa declarar um novo eixo; a outra entrada (intentLabels) continuará a usar o eixo padrão.

Agora, devemos ser bons para executar e ver a seguinte saída:

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

Sem muito esforço, alcançamos uma taxa de erro de 4,1%. Muito bom para um primeiro tiro (embora não seja bem o estado da arte nesta tarefa, que está em 3%).

No entanto, você pode observar uma coisa: o número de amostras por época agora é de cerca de 2700. Isso ocorre porque esse é o número de exemplos de rótulo, dos quais agora temos apenas um por frase. Temos um número muito reduzido de sinais de supervisão nesta tarefa. Isso deve nos encorajar a tentar aumentar o tamanho da minibatch. Vamos tentar 256 em vez de 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

Esse sistema aprende muito melhor! (Observe, porém, que essa diferença é provavelmente um artefato causado pelo nosso fsAdagrad esquema de normalização de gradiente e, normalmente, desaparecerá em breve ao usar conjuntos de dados maiores.)

No entanto, a taxa de erro resultante é maior:

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

mas essa diferença corresponde a três erros, o que não é significativo.

Confira a solução aqui.

Tarefa 5: Treinamento paralelo

Por fim, se você tiver várias GPUs, o CNTK permitirá que você paralelize o treinamento usando MPI (Interface de Passagem de Mensagens). Esse modelo é muito pequeno para esperar qualquer aceleração; paralelizar um modelo tão pequeno subutilizará severamente GPUs disponíveis. No entanto, vamos percorrer os movimentos, para que você saiba como fazê-lo depois de passar para cargas de trabalho do mundo real.

Adicione as seguintes linhas ao SGD bloco:

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

e execute este comando:

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

Isso executará o treinamento em 4 GPUs usando o algoritmo SGD de 1 bit (SGD de 2 bits nesse caso, na verdade). Sua aproximação não prejudicou a precisão: a taxa de erro é de 4,367%, dois erros a mais (execute a ação TestTagger separadamente em uma única GPU).

Conclusão

Este tutorial introduziu o estilo de composição de função como um meio compacto de representar redes. Muitos tipos de rede neural são adequados para representá-los dessa forma, o que é uma tradução mais direta e menos propensa a erros de um grafo em uma descrição de rede.

Este tutorial praticou para usar uma configuração existente no estilo de composição de função e modificá-la de maneiras específicas:

  • adicionando uma camada (de nossa galeria de camadas predefinidas)
  • definindo e usando uma função
  • definindo e usando uma função de fábrica de camadas

O tutorial também discutiu o tratamento de várias dimensões de tempo e vimos como paralelizar o treinamento.

Soluções

Abaixo estão as soluções para as tarefas acima. Ei, nada de traição!

Solução 1: Adicionar normalização em lote

A função de modelo modificada tem este formulário:

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

Solução 2: Adicionar um Lookahead

Sua função lookahead pode ser definida da seguinte maneira:

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

e ela seria inserida no modelo da seguinte maneira:

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

Solução 3: modelo recorrente bidirecional

A camada recorrente bidirecional pode ser escrita desta forma:

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

e, em seguida, usado assim:

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

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

Solução 4: Classificação de intenção

Reduza as sequências para o último/primeiro oculto da camada recorrente:

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

Altere a entrada do rótulo do slot para a intenção:

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

Use um novo eixo dinâmico:

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

Confirmação

Gostaríamos de agradecer a Derek Liu por preparar a base deste tutorial.