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:
- redes recorrentes (página wikipédia)
- inserção de texto (página wikipédia)
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:
SLUHandsOn.cntk
: o arquivo de configuração do CNTK com o qual apresentaremos abaixo e trabalharemos.slu.forward.nobn.cmf
,slu.forward.cmf
eslu.forward.lookahead.cmf
slu.forward.backward.cmf
: modelos pré-treinados que são o resultado das respectivas configurações que estamos desenvolvendo ao longo deste tutorial.atis.train.ctf
eatis.test.ctf
: o corpo de treinamento e teste, já convertido em CTF (Formato de Texto CNTK).
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.ctf
de 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
eEOS
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 S0
e S1
S2
.
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, G
F
e 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 W
de peso, um viés b
e 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
, G
e 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 G
de 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.