Laboratórios Práticos Language Understanding com Redes Recorrentes
Note que este tutorial requer a versão principal 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ção de Viagens Aéreas (ATIS) de marcação de slots e classificação de intenções. Começaremos com uma incorporação direta seguida de um LSTM recorrente. Vamos então alargá-lo para incluir as palavras dos vizinhos e correr bidirecionalmente. Por último, vamos transformar este sistema num classificador de intenção.
As técnicas que vai praticar incluem:
- descrição do modelo compondo blocos de camadas em vez de escrever fórmulas
- criando o seu próprio bloco de camadas
- variáveis com diferentes comprimentos de sequência na mesma rede
- formação paralela
Assumimos que está familiarizado com os fundamentos da aprendizagem profunda, e com estes conceitos específicos:
- redes recorrentes (página da Wikipédia)
- incorporação de texto (página da Wikipédia)
Pré-requisitos
Assumimos que já instalou a CNTK e pode executar o comando CNTK. Este tutorial foi realizado na KDD 2016 e requer uma construção recente, por favor consulte aqui para instruções de configuração. Basta seguir as instruções para descarregar um pacote de instalação binário a partir dessa página.
Em seguida, descarregue um arquivo ZIP (cerca de 12 MB): Clique neste link e, em seguida, no botão Descarregar.
O arquivo contém os ficheiros deste tutorial. Por favor, o arquivo e desemote o seu diretório de trabalho para SLUHandsOn
.
Os ficheiros com os que vai trabalhar são:
SLUHandsOn.cntk
: O ficheiro de configuração CNTK que iremos introduzir abaixo e trabalharemos com.slu.forward.nobn.cmf
,slu.forward.cmf
eslu.forward.lookahead.cmf
slu.forward.backward.cmf
: Modelos pré-treinados que são o resultado das respetivas configurações que estamos desenvolvendo ao longo deste tutorial.atis.train.ctf
eatis.test.ctf
: O corpo de treino e teste, já convertido em Formato de Texto CNTK (CTF).
Por último, recomendamos vivamente que o corram numa máquina com uma GPU compatível com a CUDA capaz. Aprender profundamente sem GPUs não é divertido.
Estrutura de Tarefas e Modelos
A tarefa que queremos abordar neste tutorial é a marcação de slot. Usamos o corpus ATIS. O ATIS contém consultas de computador humano do domínio dos Serviços de Informação de Viagens Aéreas, e a nossa tarefa será anotar (etiqueta) cada palavra de uma consulta se pertence a um item específico de informação (slot), e qual.
Os dados da sua pasta de trabalho já foram convertidos no "Formato de Texto CNTK". Vejamos um exemplo do ficheiro atis.test.ctf
test-set:
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
Este ficheiro tem 7 colunas:
- um id de sequência (19). Há 11 entradas com este id de sequência. Isto significa que a sequência 19 é composta por 11 fichas;
- coluna
S0
, que contém índices de palavras numéricas; - uma coluna de comentários denotada por
#
, para permitir que um leitor humano saiba o que significa o índice de palavra numérica; As colunas de comentários são ignoradas pelo sistema.BOS
eEOS
são palavras especiais para denotar início e fim da frase, respectivamente; - coluna
S1
é um rótulo de intenção, que só usaremos na última parte do tutorial; - outra coluna de comentários que mostre o rótulo legível pelo homem do índice de intenções numéricas;
- coluna
S2
é a etiqueta de slot, representada como um índice numérico; - outra coluna de comentários que mostra o rótulo legível pelo homem do índice de rótulo numérico.
A tarefa da rede neural é olhar para a consulta (coluna S0
) e prever a etiqueta de ranhura (coluna S2
).
Como pode ver, cada palavra na entrada é atribuída a uma etiqueta O
vazia ou a uma etiqueta de ranhura que começa com B-
a primeira palavra, e com I-
qualquer palavra consecutiva adicional que pertença à mesma ranhura.
O modelo que vamos usar é um modelo recorrente que consiste numa camada incorporada, 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 da rede CNTK. Por favor, dê uma olhada rápida e combine com a descrição acima:
model = Sequential (
EmbeddingLayer {150} :
RecurrentLSTMLayer {300} :
DenseLayer {labelDim}
)
As descrições destas funções podem ser encontradas em: Sequential()
, EmbeddingLayer{}
, e RecurrentLSTMLayer{}
DenseLayer{}
Configuração CNTK
Arquivo Config
Para treinar e testar um modelo em CNTK, precisamos fornecer um ficheiro de configuração que diga à CNTK quais as operações que pretende executar (command
variável) e uma secção de parâmetros para cada comando.
Para o comando de treino, a CNTK precisa de ser avisada:
- como ler os dados (
reader
secção) - a função do modelo e as suas entradas e saídas no gráfico de cálculo (
BrainScriptNetworkBuilder
secção) - hiper-parâmetros para o aluno (
SGD
secção)
Para o comando de avaliação, a CNTK precisa de saber ler os dados do teste (reader
secção).
Segue-se o ficheiro de configuração com que começaremos. Como pode ver, um ficheiro de configuração CNTK é um ficheiro de texto que consiste em definições de parâmetros, que são organizados numa hierarquia de registos. Também pode ver como a CNTK suporta a substituição básica dos parâmetros utilizando a $parameterName$
sintaxe. O ficheiro real contém apenas mais alguns parâmetros do que mencionados acima, mas por favor digitalize-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" }
}
}
}
Um breve olhar sobre dados e leitura de dados
Já vimos os dados.
Mas como gera este formato?
Para ler texto, este tutorial utiliza o CNTKTextFormatReader
. Espera que os dados de entrada sejam de um formato específico, que é descrito aqui.
Para este tutorial, criámos o corpora por dois passos:
converter os dados brutos num ficheiro de texto simples que contenha colunas separadas pelo TAB de texto separado do 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
Isto deve ser compatível com a saída do
paste
comando.convertê-lo em Formato de Texto CNTK (CTF) 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
onde os três
.wl
ficheiros dão o vocabulário como ficheiros de texto simples, uma linha por palavra.
Nestes ficheiros CTFG, as nossas colunas estão rotuladas S0
, S1
e S2
.
Estas estão ligadas à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
Pode encontrar o ficheiro de configuração acima no nome SLUHandsOn.cntk
da pasta de trabalho.
Para executá-lo, execute a configuração acima por este comando:
cntk configFile=SLUHandsOn.cntk
Isto irá executar a nossa configuração, começando com a formação de modelos como definido na secção que nomeámos TrainTagger
.
Depois de uma saída inicial um pouco tagarela, em breve verá isto:
Training 721479 parameters in 6 parameter tensors.
seguido por uma produção 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
Isto mostra como a aprendizagem prossegue ao longo das épocas (passa através dos dados).
Por exemplo, após duas épocas, o critério de cross-entropy, que tínhamos nomeado ce
no ficheiro de configuração, atingiu 0,27 como medido nas 36001 amostras desta época, e que a taxa de erro é de 5,883% nas mesmas amostras de treino 36016.
O 36001 vem do facto de a nossa configuração definir o tamanho da época como 36000. O tamanho da época é o número de amostras-- contadas como símbolos de palavra, não frases -- para processar entre os pontos de verificação dos modelos. Uma vez que as frases têm um comprimento variado e não resumem necessariamente múltiplos de precisamente 36000 palavras, você verá alguma pequena variação.
Uma vez concluído o treino (pouco menos de 2 minutos num Titan-X ou num Surface Book), a 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, no nosso conjunto de testes, as etiquetas de slot foram previstas com uma taxa de erro de 2,9%. Nada mau, para um sistema tão simples!
Numa máquina só com CPU, pode ser 4 ou mais vezes mais lento. Para se certificar de que o sistema está a progredir, pode permitir rastrear 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 não quiser esperar até que isto termine, pode executar um modelo intermédio, 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 testar também o nosso modelo pré-treinado, que 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
Modificar o Modelo
No seguinte, serão-lhe dadas tarefas para praticar a modificação das configurações da CNTK. As soluções são dadas no final deste documento... mas, por favor, tente sem!
Uma palavra sobre Sequential()
Antes de saltar 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 cólon (:
) é a sintaxe do BrainScript de expressões de matrizes. Por exemplo, (F:G:H)
é uma matriz com três elementos, F
G
e H
.
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 resumo, permite expressar compactamente uma situação muito comum em redes neurais onde uma entrada é processada propagando-a através de uma progressão de camadas.
Sequential()
toma uma série de funções como argumento, e devolve uma nova função que invoca estas funções em ordem, cada vez que passa 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)))
Isto é conhecido como "composição de função", e é especialmente conveniente para expressar redes neurais, que muitas vezes têm esta forma:
+-------+ +-------+ +-------+
x -->| F |-->| G |-->| H |--> y
+-------+ +-------+ +-------+
Voltando ao nosso modelo à mão, a expressão simplesmente diz que o Sequential
nosso modelo tem esta forma:
+-----------+ +----------------+ +------------+
x -->| Embedding |-->| Recurrent LSTM |-->| DenseLayer |--> y
+-----------+ +----------------+ +------------+
Tarefa 1: Adicionar normalização do lote
Queremos agora adicionar novas camadas ao modelo, especificamente a normalização do lote.
A normalização do lote é uma técnica popular para acelerar a convergência. É frequentemente usado para configurações de processamento de imagem, por exemplo, o nosso outro laboratório prático no reconhecimento de imagem. Mas também pode funcionar para modelos recorrentes?
Assim, a sua tarefa será inserir camadas de normalização de lote antes e depois da recorrente camada LSTM. Se tiver completado os laboratórios práticos sobre o processamento de imagem, deve lembrar-se que a camada de normalização do lote tem esta forma:
BatchNormalizationLayer{}
Então, por favor, vá em frente e modifique a configuração e veja o que acontece.
Se tudo correr bem, notará não só uma melhor velocidade de convergência (ce
e errs
) em comparação com a configuração anterior, mas também uma melhor taxa de erro 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 não quiser esperar que o treino esteja concluído, pode encontrar o modelo resultante com o nome slu.forward.cmf
.)
Por favor, veja a solução aqui.
Tarefa 2: Adicionar um Lookahead
O nosso modelo recorrente sofre de um défice estrutural: uma vez que a recorrência vai da esquerda para a direita, a decisão de um rótulo de slot não tem informação sobre as próximas palavras. O modelo está um pouco desequilibrado. A sua tarefa será modificar o modelo de modo a que a entrada para a recorrência consista não só na palavra atual, mas também na próxima (lookahead).
A sua solução deve estar no estilo de composição de funções. Por isso, terá de escrever uma função BrainScript que faz o seguinte:
- aceitar um argumento de entrada;
- calcular o "valor futuro" imediato desta entrada utilizando a
FutureValue()
função (utilizar este formulário específico:FutureValue (0, input, defaultHiddenActivation=0)
); e - concatenate os dois em um vetor de duas vezes a dimensão incorporada usando
Splice()
(use este formulário: Splice(x:y)
)
e, em seguida, insira esta função entre Sequence()
a incorporação e a camada recorrente.
Se tudo correr bem, 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
Isto funcionou! Saber qual é a próxima palavra permite que o slot tagger reduza a sua taxa de erro de 2,0% para 1,84%.
(Se não quiser esperar que o treino esteja concluído, pode encontrar o modelo resultante com o nome slu.forward.lookahead.cmf
.)
Por favor, veja a solução aqui.
Tarefa 3: Modelo bidirecional recorrente
Aha, o conhecimento de palavras futuras ajuda. Então, em vez de um olhar de uma palavra, por que não olhar para a frente até ao fim da frase, através de uma recorrência retrógrada? Vamos criar um modelo bidirecional!
A sua tarefa é implementar uma nova camada que execute uma recursão para a frente e para trás sobre os dados, e concatena os vetores de saída.
Note-se, no entanto, que isto difere da tarefa anterior, na medida em que a camada bidirecional contém parâmetros de modelo aprecáveis. 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 é simultaneamente uma função e um objeto. O que não significa mais nada que contenha dados ainda pode ser invocado como se fosse uma função.
Por exemplo, LinearLayer{outDim}
é uma função de fábrica que devolve um objeto de função que contém uma matriz de W
peso, um viés b
, e outra função a calcular W * input + b
.
Por exemplo, dizer LinearLayer{1024}
que irá criar este objeto de função, que pode ser utilizado como qualquer outra função, também imediatamente: LinearLayer{1024}(x)
.
Confuso? Tomemos um exemplo: Vamos implementar uma nova camada que combina uma camada linear com uma normalização subsequente do lote. Para permitir a composição da função, a camada precisa ser realizada como uma função de fábrica, que pode ser assim:
LinearLayerWithBN {outDim} = {
F = LinearLayer {outDim}
G = BatchNormalization {normalizationTimeConstant=2048}
apply (x) = G(F(x))
}.apply
Invocar esta função de fábrica criará primeiro um registo (indicado por {...}
) com três membros: F
, G
e apply
. Neste exemplo, F
e G
são os próprios objetos de função, e apply
é a função a aplicar aos dados.
Appending .apply
a esta expressão significa o que .x
significa sempre no BrainScript, aceder a um membro de gravação. Assim, por exemplo, a chamada LinearLayerWithBN{1024}
criará um objeto que contenha um objeto de função de camada linear chamado F
, um objeto G
de função de normalização de lote , e apply
que é a função que implementa o funcionamento real desta camada utilizando F
e G
. Em seguida, regressará apply
. Para o exterior, apply()
parece e comporta-se como uma função. Sob o capot, no entanto, apply()
manterá o registo a que pertence, mantendo assim o acesso às suas instâncias específicas de F
e G
.
Agora, de volta à nossa tarefa em mãos. Agora terá de criar uma função de fábrica, muito parecida com o exemplo acima.
Deve criar uma função de fábrica que crie duas instâncias de camada recorrentes (uma para a frente, outra para trás) e, em seguida, definir uma função apply (x)
que aplica ambas as instâncias de camada ao mesmo x
e concatenate os dois resultados.
Tudo bem, tente! Para saber como realizar uma recursão retrógrada na CNTK, por favor, dê uma pista de como a recursão dianteira é feita. Por favor, faça também o seguinte:
- remover o lookahead de uma palavra que adicionou na tarefa anterior, que pretendemos substituir; e
- alterar o
hiddenDim
parâmetro de 300 para 150, para manter o número total de parâmetros do modelo limitado.
A execução deste modelo produzirá com sucesso 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 encanto! Este modelo atinge 1,83%, um pouco melhor do que o modelo de lookahead acima. O modelo bidirecional tem 40% menos parâmetros do que o da cabeça do lookahead. No entanto, se voltar atrás e olhar atentamente para a saída completa do registo (não mostrada nesta página web), poderá descobrir que o lookahead um treinou cerca de 30% mais rápido. Isto porque o modelo lookahead tem tanto menos dependências horizontais (uma em vez de duas recorrências) e produtos de matriz maiores, podendo assim alcançar um paralelismo mais elevado.
Por favor, veja a solução aqui.
Tarefa 4: Classificação de Intenções
Acontece que o modelo que construímos até agora pode facilmente ser transformado num classificador de intenção.
Lembre-se que o nosso ficheiro 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 numa única etiqueta chama-se classificação de sequência. O nosso classificador de sequência será implementado como um LSTM recorrente (já temos isso) do qual damos o seu estado oculto do seu último passo. Isto dá-nos um único vetor para cada sequência. Este vetor é então alimentado em uma camada densa para classificação softmax.
A CNTK tem uma operação para extrair o último estado de uma sequência chamada BS.Sequences.Last()
.
Esta operação honra o facto de que a mesma minibatch pode conter sequências de comprimentos muito diferentes, e que são dispostas na memória num formato embalado.
Da mesma forma, para recursão retrógrada, podemos usar BS.Sequences.First()
.
A sua tarefa é modificar a rede bidirecional da Tarefa 3 de modo a que o último quadro seja extraído da recursão dianteira, e o primeiro quadro é extraído da recursão retrógrada, e os dois vetores são concatenados. O vetor concatenated (às vezes chamado de vetor de pensamento) deve então ser a entrada da camada densa.
Além disso, deve alterar a etiqueta da ranhura para a etiqueta de intenção: Basta mudar o nome da variável de entrada (slotLabels
) para, em vez disso, corresponder ao nome que é usado na secção do leitor para as etiquetas de intenção, e também corresponder à dimensão.
Por favor, tente a modificação. Se o fizeres bem, serás, no entanto, confrontado com uma mensagem de erro irritante, 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?
"Estás a usar diferentes comprimentos de sequência?" Oh sim! A consulta e a etiqueta de intenção fazem- o rótulo de intenção é apenas um único símbolo por consulta. É uma sequência de 1 elemento! Então, como resolver isto?
A CNTK permite que diferentes variáveis na rede tenham diferentes comprimentos de sequência. Pode pensar no comprimento da sequência como uma dimensão adicional e simbólica do tensor. Variáveis do mesmo comprimento partilham a mesma dimensão simbólica do comprimento. Se duas variáveis tiverem comprimentos diferentes, esta deve ser explicitamente declarada, caso contrário a CNTK assumirá que todas as variáveis partilham o mesmo comprimento simbólico.
Isto é feito criando um novo objeto de eixo dinâmico e associando-o a uma das entradas, da seguinte forma:
n = DynamicAxis()
query = Input {inputDim, dynamicAxis=n}
A CNTK tem um eixo predefinido. Como pode adivinhar pela exceção acima, o seu nome é "*".
Por isso, basta declarar um novo eixo; a outra Entrada (intentLabels
) continuará a utilizar o eixo predefinido.
Agora devemos ser bons para correr, 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, conseguimos uma taxa de erro de 4,1%. Muito bom para um primeiro tiro (embora não seja bem topo de gama nesta tarefa, que está nos 3%).
No entanto, pode notar uma coisa: o número de amostras por época é agora de cerca de 2700. Isto porque se trata do número de amostras de rótulos, das quais temos agora apenas uma por frase. Temos um número muito reduzido de sinais de supervisão nesta tarefa. Isto deve encorajar-nos a tentar aumentar o tamanho das minibatchs. 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
Este sistema aprende muito melhor! (Note-se, no entanto, que esta diferença é provavelmente um artefacto causado pelo nosso fsAdagrad
esquema de normalização de gradiente, e normalmente desaparecerá em breve quando utilizar conjuntos de dados maiores.)
A taxa de erro resultante é maior, no entanto:
Final Results: Minibatch[1-1]: errs = 4.479% * 893; ce = 0.31638223 * 893; perplexity = 1.37215463
mas esta diferença corresponde, na verdade, a 3 erros, o que não é significativo.
Por favor, veja a solução aqui.
Tarefa 5: Formação Paralela
Por último, se tiver várias GPUs, a CNTK permite-lhe paralelizar o treino utilizando o MPI (Interface de passagem de mensagens). Este modelo é demasiado pequeno para esperar qualquer aceleração; paralelamente a um modelo tão pequeno irá subutilizar severamente as GPUs disponíveis. No entanto, passemos pelas moções, para que saiba como fazê-lo quando passar para a carga de trabalho no mundo real.
Por favor, adicione as seguintes linhas ao SGD
bloco:
SGD = {
...
parallelTrain = {
parallelizationMethod = "DataParallelSGD"
parallelizationStartEpoch = 1
distributedMBReading = true
dataParallelSGD = { gradientBits = 2 }
}
}
e, em seguida, executar este comando:
mpiexec -np 4 cntk configFile=SLUHandsOn_Solution4.cntk stderr=Models/log parallelTrain=true command=TrainTagger
Isto irá executar o treino em 4 GPUs usando o algoritmo SGD de 1 bit (SGD de 2 bits neste caso, na verdade).
A sua aproximação não prejudicou a precisão: A taxa de erro é de 4,367%, mais dois erros (por favor, execute a TestTagger
ação separadamente numa única GPU).
Conclusão
Este tutorial introduziu o estilo de composição de função como um meio compacto de representação de redes. Muitos tipos de rede neural são adequados para representá-los desta forma, que é uma tradução mais direta e menos propensa a erros de um gráfico para uma descrição de rede.
Este tutorial praticou para ter uma configuração existente no estilo de composição de funções, e modificá-lo de formas específicas:
- adicionando uma camada (da nossa galeria de camadas predefinidas)
- definição e utilização de uma função
- definição e utilização de uma função de fábrica de camadas
O tutorial também discutiu o manuseamento de múltiplas dimensões do tempo, e vimos como paralelizar o treino.
Soluções
Abaixo estão as soluções para as tarefas acima. Nada de batota!
Solução 1: Adicionar normalização do lote
A função de modelo modificado tem esta forma:
model = Sequential (
EmbeddingLayer {embDim} : # embedding
BatchNormalizationLayer {} : ##### added
RecurrentLSTMLayer {hiddenDim, goBackwards=false} : # LSTM
BatchNormalizationLayer {} : ##### added
DenseLayer {labelDim} # output layer
)
Solução 2: Adicionar um Lookahead
A sua função de cabeça-de-cartaz pode ser definida assim:
OneWordLookahead (x) = Splice (x : DelayLayer {T=-1} (x))
e seria inserido no modelo como este:
model = Sequential (
EmbeddingLayer {embDim} :
OneWordLookahead : ##### added
BatchNormalizationLayer {} :
RecurrentLSTMLayer {hiddenDim, goBackwards=false} :
BatchNormalizationLayer {} :
DenseLayer {labelDim}
)
Solução 3: Modelo bidirecional recorrente
A camada bidirecional recorrente pode ser escrita assim:
BiRecurrentLSTMLayer {outDim} = {
F = RecurrentLSTMLayer {outDim, goBackwards=false}
G = RecurrentLSTMLayer {outDim, goBackwards=true}
apply (x) = Splice (F(x):G(x))
}.apply
e depois 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ções
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 ^^^
Alterar a entrada do rótulo da ranhura 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)
Utilize um novo eixo dinâmico:
n = DynamicAxis() ##### added
query = Input {inputDim, dynamicAxis=n} ##### use dynamic axis
Reconhecimento
Gostaríamos de agradecer a Derek Liu por ter preparado a base deste tutorial.