Laboratorios prácticos Language Understanding con redes recurrentes
Tenga en cuenta que este tutorial requiere la versión maestra más reciente o la próxima CNTK 1.7.1 que se publicará próximamente.
En este laboratorio práctico se muestra cómo implementar una red recurrente para procesar texto, para las tareas de Air Travel Information Services (ATIS) de etiquetado de ranuras y clasificación de intenciones. Comenzaremos con una inserción directa seguida de un LSTM recurrente. A continuación, se ampliará para incluir palabras vecinas y ejecutarse bidireccionalmente. Por último, convertiremos este sistema en un clasificador de intenciones.
Entre las técnicas que practicará se incluyen:
- descripción del modelo mediante la composición de bloques de capas en lugar de escribir fórmulas
- crear su propio bloque de capas
- variables con longitudes de secuencia diferentes en la misma red
- entrenamiento paralelo
Se supone que está familiarizado con los conceptos básicos del aprendizaje profundo y estos conceptos específicos:
- redes recurrentes (página wikipedia)
- inserción de texto (página wikipedia)
Requisitos previos
Se supone que ya ha instalado CNTK y puede ejecutar el comando CNTK. Este tutorial se realizó en KDD 2016 y requiere una compilación reciente. Consulte aquí para obtener instrucciones de configuración. Solo puede seguir las instrucciones para descargar un paquete de instalación binario desde esa página.
A continuación, descargue un archivo ZIP (aproximadamente 12 MB): haga clic en este vínculo y, a continuación, en el botón Descargar.
El archivo contiene los archivos de este tutorial. Archive y establezca el directorio de trabajo en SLUHandsOn
.
Los archivos con los que va a trabajar son:
SLUHandsOn.cntk
: El archivo de configuración de CNTK con el que presentaremos a continuación y trabajaremos con él.slu.forward.nobn.cmf
,slu.forward.cmf
,slu.forward.lookahead.cmf
yslu.forward.backward.cmf
: modelos entrenados previamente que son el resultado de las configuraciones respectivas que estamos desarrollando en este tutorial.atis.train.ctf
yatis.test.ctf
: el corpus de entrenamiento y prueba, ya convertido al formato de texto CNTK (CTF).
Por último, se recomienda encarecidamente ejecutar esto en una máquina con una GPU compatible con CUDA compatible. El aprendizaje profundo sin GPU no es divertido.
Estructura de tareas y modelos
La tarea que queremos abordar en este tutorial es el etiquetado de ranuras. Usamos el corpus de ATIS. ATIS contiene consultas de equipo humano del dominio de Air Travel Information Services, y nuestra tarea será anotar (etiquetar) cada palabra de una consulta si pertenece a un elemento específico de información (ranura) y cuál.
Los datos de la carpeta de trabajo ya se han convertido en el "Formato de texto CNTK". Veamos un ejemplo del archivo 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 archivo tiene 7 columnas:
- un identificador de secuencia (19). Hay 11 entradas con este identificador de secuencia. Esto significa que la secuencia 19 consta de 11 tokens;
- columna
S0
, que contiene índices numéricos de palabras; - una columna de comentario indicada por
#
, para permitir que un lector humano sepa lo que significa el índice de palabras numéricas; El sistema omite las columnas de comentario.BOS
yEOS
son palabras especiales para indicar el principio y el final de la oración, respectivamente; - column
S1
es una etiqueta de intención, que solo usaremos en la última parte del tutorial; - otra columna de comentario que muestra la etiqueta legible del índice de intención numérica;
- column
S2
es la etiqueta de ranura, representada como un índice numérico; y - otra columna de comentario que muestra la etiqueta legible del índice de etiquetas numéricas.
La tarea de la red neuronal es examinar la consulta (columna S0
) y predecir la etiqueta de ranura (columna S2
).
Como puede ver, a cada palabra de la entrada se le asigna una etiqueta O
vacía o una etiqueta de ranura que comienza por B-
para la primera palabra y con I-
para cualquier palabra consecutiva adicional que pertenezca a la misma ranura.
El modelo que usaremos es un modelo recurrente que consta de una capa de inserción, una celda LSTM recurrente y una capa densa para calcular las 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"
O bien, como descripción de red de CNTK. Tenga un aspecto rápido y haga coincidirlo con la descripción anterior:
model = Sequential (
EmbeddingLayer {150} :
RecurrentLSTMLayer {300} :
DenseLayer {labelDim}
)
Las descripciones de estas funciones se pueden encontrar en: Sequential()
, EmbeddingLayer{}
, RecurrentLSTMLayer{}
y DenseLayer{}
Configuración de CNTK
Archivo de configuración
Para entrenar y probar un modelo en CNTK, es necesario proporcionar un archivo de configuración que indique a CNTK qué operaciones desea ejecutar (command
variable) y una sección de parámetros para cada comando.
Para el comando de entrenamiento, se debe indicar a CNTK:
- cómo leer los datos (
reader
sección) - la función del modelo y sus entradas y salidas en el gráfico de cálculo (
BrainScriptNetworkBuilder
sección) - hiperparámetres para el aprendiz (
SGD
sección)
Para el comando de evaluación, CNTK debe saber cómo leer los datos de prueba (reader
sección).
A continuación se muestra el archivo de configuración con el que comenzaremos. Como puede ver, un archivo de configuración de CNTK es un archivo de texto que consta de definiciones de parámetros, que se organizan en una jerarquía de registros. También puede ver cómo CNTK admite el reemplazo básico de parámetros mediante la $parameterName$
sintaxis . El archivo real contiene solo algunos parámetros más de los mencionados anteriormente, pero escanee y busque los elementos de configuración que acaba de mencionar:
# 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" }
}
}
}
Un breve vistazo a la lectura de datos y datos
Ya hemos visto los datos.
¿Pero cómo se genera este formato?
Para leer texto, en este tutorial se usa CNTKTextFormatReader
. Espera que los datos de entrada sean de un formato específico, que se describe aquí.
Para este tutorial, creamos el corpora en dos pasos:
convierta los datos sin procesar en un archivo de texto sin formato que contenga columnas separadas por TABULAciones de texto separado por espacios. Por ejemplo:
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
Esto está pensado para ser compatible con la salida del
paste
comando.conviértalo en formato de texto CNTK (CTF) con el siguiente comando:
python Scripts/txt2ctf.py --map query.wl intent.wl slots.wl --annotated True --input atis.test.txt --output atis.test.ctf
donde los tres
.wl
archivos proporcionan el vocabulario como archivos de texto sin formato, una línea por palabra.
En estos archivos CTFG, nuestras columnas están etiquetadas S0
como , S1
y S2
.
Estas se conectan a las entradas de red reales mediante las líneas correspondientes de la definición del lector:
input = {
query = { alias = "S0" ; dim = $vocabSize$ ; format = "sparse" }
intentLabels = { alias = "S1" ; dim = $numIntents$ ; format = "sparse" }
slotLabels = { alias = "S2" ; dim = $numLabels$ ; format = "sparse" }
}
Ejecutándolo
Puede encontrar el archivo de configuración anterior bajo el nombre SLUHandsOn.cntk
en la carpeta de trabajo.
Para ejecutarlo, ejecute la configuración anterior mediante este comando:
cntk configFile=SLUHandsOn.cntk
Esto ejecutará nuestra configuración, empezando por el entrenamiento del modelo tal y como se define en la sección denominada TrainTagger
.
Después de una salida de registro inicial algo chatty, pronto verá esto:
Training 721479 parameters in 6 parameter tensors.
seguido de una salida similar a la siguiente:
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
Esto muestra cómo el aprendizaje continúa a lo largo de las épocas (pasa por los datos).
Por ejemplo, después de dos épocas, el criterio de entropía cruzada, que habíamos nombrado ce
en el archivo de configuración, ha alcanzado 0,27 como se mide en las muestras 36001 de esta época, y que la tasa de errores es del 5,883 % en esas mismas muestras de entrenamiento de 36016.
El 36001 procede del hecho de que nuestra configuración definió el tamaño de época como 36000. El tamaño de época es el número de muestras que se cuentan como tokens de palabra, no como oraciones, para procesar entre puntos de control del modelo. Dado que las oraciones tienen una longitud variable y no necesariamente suman hasta múltiplos de exactamente 36000 palabras, verá una pequeña variación.
Una vez completado el entrenamiento (un poco menos de 2 minutos en un Titan-X o un Surface Book), CNTK continuará con la EvalTagger
acción.
Final Results: Minibatch[1-1]: errs = 2.922% * 10984; ce = 0.14306181 * 10984; perplexity = 1.15380111
Es decir, en nuestro conjunto de pruebas, las etiquetas de ranura se han previsto con una tasa de errores del 2,9 %. ¡No en absoluto malo, para un sistema tan simple!
En una máquina solo con CPU, puede ser 4 o más veces más lenta. Para asegurarse de que el sistema está progresando, puede habilitar el seguimiento para ver resultados parciales, que deben aparecer razonablemente rápidamente:
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
...
Si no desea esperar hasta que finalice, puede ejecutar un modelo intermedio, por ejemplo.
cntk configFile=SLUHandsOn.cntk command=TestTagger modelPath=Models/slu.cmf.4
Final Results: Minibatch[1-1]: errs = 3.851% * 10984; ce = 0.18932937 * 10984; perplexity = 1.20843890
o pruebe también nuestro modelo entrenado previamente, que puede encontrar en la carpeta de trabajo:
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
Modificación del modelo
A continuación, se le proporcionarán tareas para practicar la modificación de las configuraciones de CNTK. Las soluciones se proporcionan al final de este documento... pero por favor pruébelo sin!
Una palabra sobre Sequential()
Antes de saltar a las tareas, echemos un vistazo de nuevo al modelo que acabamos de ejecutar. El modelo se describe en lo que llamamos estilo de composición de funciones.
model = Sequential (
EmbeddingLayer {embDim} : # embedding
RecurrentLSTMLayer {hiddenDim, goBackwards=false} : # LSTM
DenseLayer {labelDim, initValueScale=7} # output layer
)
donde los dos puntos (:
) son la sintaxis de BrainScript de expresar matrices. Por ejemplo, (F:G:H)
es una matriz con tres elementos, F
, G
y H
.
Es posible que esté familiarizado con la notación "secuencial" de otros kits de herramientas de red neuronal.
Si no es así, es una operación eficaz que, Sequential()
en pocas palabras, permite expresar de forma compacta una situación muy común en las redes neuronales en las que se procesa una entrada mediante la propagación a través de una progresión de capas.
Sequential()
toma una matriz de funciones como argumento y devuelve una nueva función que invoca esta función en orden, cada vez que pasa la salida de una a la siguiente.
Por ejemplo,
FGH = Sequential (F:G:H)
y = FGH (x)
es lo mismo que .
y = H(G(F(x)))
Esto se conoce como "composición de funciones" y es especialmente conveniente para expresar redes neuronales, que a menudo tienen esta forma:
+-------+ +-------+ +-------+
x -->| F |-->| G |-->| H |--> y
+-------+ +-------+ +-------+
Volviendo a nuestro modelo a mano, la Sequential
expresión simplemente dice que nuestro modelo tiene esta forma:
+-----------+ +----------------+ +------------+
x -->| Embedding |-->| Recurrent LSTM |-->| DenseLayer |--> y
+-----------+ +----------------+ +------------+
Tarea 1: Agregar normalización por lotes
Ahora queremos agregar nuevas capas al modelo, específicamente la normalización por lotes.
La normalización por lotes es una técnica popular para acelerar la convergencia. A menudo se usa para las configuraciones de procesamiento de imágenes, por ejemplo, nuestro otro laboratorio práctico sobre el reconocimiento de imágenes. ¿Pero también podría funcionar para modelos recurrentes?
Por lo tanto, la tarea será insertar capas de normalización por lotes antes y después de la capa LSTM recurrente. Si ha completado los laboratorios prácticos en el procesamiento de imágenes, puede recordar que la capa de normalización por lotes tiene este formato:
BatchNormalizationLayer{}
Por lo tanto, continúe y modifique la configuración y vea lo que sucede.
Si todo salió bien, observará que no solo se ha mejorado la velocidad de convergencia (ce
y errs
) en comparación con la configuración anterior, sino también una mejor tasa de errores del 2,0 % (en comparación con el 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
(Si no desea esperar a que se complete el entrenamiento, puede encontrar el modelo resultante bajo el nombre slu.forward.cmf
).
Consulte la solución aquí.
Tarea 2: Agregar un lookahead
Nuestro modelo recurrente sufre de un déficit estructural: dado que la periodicidad se ejecuta de izquierda a derecha, la decisión de una etiqueta de ranura no tiene información sobre las próximas palabras. El modelo está un poco desapredado. La tarea será modificar el modelo de forma que la entrada a la periodicidad consta no solo de la palabra actual, sino también de la siguiente (lookahead).
La solución debe estar en el estilo de composición de funciones. Por lo tanto, deberá escribir una función BrainScript que haga lo siguiente:
- aceptar un argumento de entrada;
- calcular el "valor futuro" inmediato de esta entrada mediante la
FutureValue()
función (use esta forma específica:FutureValue (0, input, defaultHiddenActivation=0)
); y - concatenar los dos en un vector de dos veces la dimensión de inserción mediante
Splice()
(use esta forma: Splice(x:y)
)
y, a continuación, inserte esta función en el Sequence()
entre la inserción y la capa recurrente.
Si todo va bien, verá la siguiente salida:
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
¡Esto funcionó! Saber cuál es la siguiente palabra permite que el etiquetador de ranura reduzca su tasa de errores del 2,0 % al 1,84 %.
(Si no desea esperar a que se complete el entrenamiento, puede encontrar el modelo resultante bajo el nombre slu.forward.lookahead.cmf
).
Consulte la solución aquí.
Tarea 3: Modelo recurrente bidireccional
Aha, conocimiento de las palabras futuras ayuda. Entonces, en lugar de un vistazo de una sola palabra, ¿por qué no mirar hacia adelante hasta el final de la frase, a través de una periodicidad hacia atrás? Vamos a crear un modelo bidireccional.
La tarea consiste en implementar una nueva capa que realiza una recursividad hacia delante y hacia atrás sobre los datos y concatena los vectores de salida.
Sin embargo, tenga en cuenta que esto difiere de la tarea anterior en que la capa bidireccional contiene parámetros de modelo aprendibles. En el estilo de composición de funciones, el patrón para implementar una capa con parámetros de modelo es escribir una función de fábrica que crea un objeto de función.
Un objeto de función, también conocido como functor, es un objeto que es una función y un objeto . Esto significa que todavía no se puede invocar nada más que contiene datos como si fuera una función.
Por ejemplo, LinearLayer{outDim}
es una función de fábrica que devuelve un objeto de función que contiene una matriz W
de peso, un sesgo b
y otra función para calcular W * input + b
.
Por ejemplo, diciendo LinearLayer{1024}
que creará este objeto de función, que luego se puede usar como cualquier otra función, también inmediatamente: LinearLayer{1024}(x)
.
¿Confundido? Veamos un ejemplo: Vamos a implementar una nueva capa que combina una capa lineal con una normalización por lotes posterior. Para permitir la composición de funciones, la capa debe realizarse como una función de fábrica, que podría tener este aspecto:
LinearLayerWithBN {outDim} = {
F = LinearLayer {outDim}
G = BatchNormalization {normalizationTimeConstant=2048}
apply (x) = G(F(x))
}.apply
Al invocar esta función de fábrica, primero se creará un registro (indicado por {...}
) con tres miembros: F
, G
y apply
. En este ejemplo, F
y G
son objetos de función, y apply
es la función que se va a aplicar a los datos.
Anexar .apply
a esta expresión significa lo que .x
siempre significa en BrainScript, para acceder a un miembro de registro. Por lo tanto, por ejemplo, llamar LinearLayerWithBN{1024}
a creará un objeto que contiene un objeto de función de capa lineal denominado F
, un objeto G
de función de normalización por lotes , y apply
que es la función que implementa la operación real de esta capa mediante F
y G
. A continuación, devolverá apply
. Para el exterior, apply()
parece y se comporta como una función. Sin embargo, en segundo plano, se mantendrá en el registro al que pertenece y, por tanto, apply()
conservará el acceso a sus instancias específicas de F
y G
.
Ahora vuelve a nuestra tarea a mano. Ahora tendrá que crear una función de fábrica, muy similar al ejemplo anterior.
Debe crear una función de fábrica que cree dos instancias de capa recurrentes (una hacia delante, una hacia atrás) y, a continuación, definirá una apply (x)
función que aplique ambas instancias de capa a la misma x
y concatene los dos resultados.
¡Muy bien, pruébelo! Para saber cómo realizar una recursividad hacia atrás en CNTK, tome una sugerencia de cómo se realiza la recursividad hacia delante. También haga lo siguiente:
- quite el encabezado lookahead de una palabra que agregó en la tarea anterior, que pretendemos reemplazar; Y
- cambie el
hiddenDim
parámetro de 300 a 150 para mantener limitado el número total de parámetros del modelo.
Al ejecutar este modelo correctamente, se producirá la siguiente salida:
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 un encanto! Este modelo logra un 1,83 %, un poco mejor que el modelo de aspecto anterior. El modelo bidireccional tiene un 40 % menos parámetros que el de lookahead uno. Sin embargo, si vuelve y examina detenidamente la salida de registro completa (no se muestra en esta página web), es posible que encuentre que el lookahead uno entrenado aproximadamente un 30 % más rápido. Esto se debe a que el modelo lookahead tiene tanto menos dependencias horizontales (una en lugar de dos periodicidades) como productos de matriz más grandes y, por tanto, pueden lograr un paralelismo mayor.
Consulte la solución aquí.
Tarea 4: Clasificación de intenciones
Resulta que el modelo que creamos hasta ahora se puede convertir fácilmente en un clasificador de intenciones.
Recuerde que nuestro archivo de datos contenía esta columna adicional denominada S1
.
Esta columna contiene una sola etiqueta por frase, que indica la intención de la consulta de encontrar información sobre temas como airport
o airfare
.
La tarea de clasificar una secuencia completa en una sola etiqueta se denomina clasificación de secuencia. Nuestro clasificador de secuencias se implementará como un LSTM recurrente (ya lo tenemos) del cual llevamos su estado oculto de su paso final. Esto nos da un único vector para cada secuencia. A continuación, este vector se introduce en una capa densa para la clasificación softmax.
CNTK tiene una operación para extraer el último estado de una secuencia, denominada BS.Sequences.Last()
.
Esta operación respeta el hecho de que el mismo minibatch puede contener secuencias de longitudes muy diferentes, y que se organizan en memoria en un formato empaquetado.
Del mismo modo, para la recursividad hacia atrás, podemos usar BS.Sequences.First()
.
La tarea consiste en modificar la red bidireccional de la tarea 3 de modo que el último fotograma se extraiga de la recursividad hacia delante y el primer fotograma se extraiga de la recursividad hacia atrás y los dos vectores se concatenan. El vector concatenado (a veces denominado vector de pensamiento) será la entrada de la capa densa.
Además, debe cambiar la etiqueta de la ranura a la etiqueta de intención: simplemente cambie el nombre de la variable de entrada (slotLabels
) para que coincida con el nombre que se usa en la sección lector para las etiquetas de intención y también coincida con la dimensión.
Pruebe la modificación. Sin embargo, si lo hace, se enfrenta a un mensaje de error vexing y un largo en eso:
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?
"¿Usa diferentes longitudes de secuencia?" ¡Venga, sí! La consulta y la etiqueta de intención hacen: la etiqueta de intención es solo un único token por consulta. Es una secuencia de 1 elemento! ¿Cómo arreglar esto?
CNTK permite que diferentes variables de la red tengan diferentes longitudes de secuencia. Puede considerar la longitud de la secuencia como una dimensión de tensor simbólica adicional. Las variables de la misma longitud comparten la misma dimensión de longitud simbólica. Si dos variables tienen longitudes diferentes, se debe declarar explícitamente; de lo contrario, CNTK supone que todas las variables comparten la misma longitud simbólica.
Para ello, se crea un nuevo objeto de eje dinámico y se asocia con una de las entradas, como se indica a continuación:
n = DynamicAxis()
query = Input {inputDim, dynamicAxis=n}
CNTK tiene un eje predeterminado. Como puede adivinar de la excepción anterior, su nombre es "*".
Por lo tanto, solo tiene que declarar un nuevo eje; la otra entrada (intentLabels
) seguirá usando el eje predeterminado.
Ahora deberíamos estar listos para ejecutarse y ver la siguiente salida:
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
Sin mucho esfuerzo, hemos logrado una tasa de errores del 4,1 %. Muy agradable para un primer disparo (aunque no bastante de última generación en esta tarea, que es a 3%).
Sin embargo, puede observar una cosa: el número de muestras por época es ahora alrededor de 2700. Esto se debe a que este es el número de muestras de etiqueta, de las cuales ahora solo tenemos una por frase. Tenemos un número considerablemente reducido de señales de supervisión en esta tarea. Esto debería animarnos a intentar aumentar el tamaño del minibatch. Vamos a probar 256 en lugar 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 mucho mejor. (Sin embargo, tenga en cuenta que esta diferencia es probable que sea un artefacto causado por nuestro fsAdagrad
esquema de normalización de degradado y normalmente desaparecerá pronto cuando se usen conjuntos de datos más grandes).
Sin embargo, la tasa de errores resultante es mayor:
Final Results: Minibatch[1-1]: errs = 4.479% * 893; ce = 0.31638223 * 893; perplexity = 1.37215463
pero esta diferencia corresponde realmente a 3 errores, que no son significativos.
Consulte la solución aquí.
Tarea 5: Entrenamiento paralelo
Por último, si tiene varias GPU, CNTK le permite paralelizar el entrenamiento mediante MPI (interfaz de paso de mensajes). Este modelo es demasiado pequeño para esperar cualquier aceleración; La paralelización de este modelo pequeño infrautilizará gravemente las GPU disponibles. Sin embargo, pasemos por los movimientos, de modo que sepa cómo hacerlo una vez que pase a cargas de trabajo reales.
Agregue las líneas siguientes al SGD
bloque :
SGD = {
...
parallelTrain = {
parallelizationMethod = "DataParallelSGD"
parallelizationStartEpoch = 1
distributedMBReading = true
dataParallelSGD = { gradientBits = 2 }
}
}
y, a continuación, ejecute este comando:
mpiexec -np 4 cntk configFile=SLUHandsOn_Solution4.cntk stderr=Models/log parallelTrain=true command=TrainTagger
Esto ejecutará el entrenamiento en 4 GPU con el algoritmo SGD de 1 bits (SGD de 2 bits en este caso, en realidad).
Su aproximación no hizo daño a la precisión: la tasa de errores es del 4,367 %, dos errores más (ejecute la TestTagger
acción por separado en una sola GPU).
Conclusión
En este tutorial se ha introducido el estilo de composición de funciones como un medio compacto para representar redes. Muchos tipos de red neuronal son adecuados para representarlos de esta manera, que es una traducción más directa y menos propensa a errores de un grafo en una descripción de red.
En este tutorial se ha práctico tomar una configuración existente en el estilo de composición de funciones y modificarla de maneras específicas:
- agregar una capa (desde nuestra galería de capas predefinidas)
- definición y uso de una función
- definición y uso de una función de generador de capas
En el tutorial también se explicó el control de varias dimensiones de tiempo y hemos visto cómo paralelizar el entrenamiento.
Soluciones
A continuación se muestran las soluciones a las tareas anteriores. ¡Oye, no engaña!
Solución 1: Agregar normalización por lotes
La función del modelo modificado tiene este formato:
model = Sequential (
EmbeddingLayer {embDim} : # embedding
BatchNormalizationLayer {} : ##### added
RecurrentLSTMLayer {hiddenDim, goBackwards=false} : # LSTM
BatchNormalizationLayer {} : ##### added
DenseLayer {labelDim} # output layer
)
Solución 2: Agregar un lookahead
La función lookahead-function podría definirse de la siguiente manera:
OneWordLookahead (x) = Splice (x : DelayLayer {T=-1} (x))
y se insertaría en el modelo de la siguiente manera:
model = Sequential (
EmbeddingLayer {embDim} :
OneWordLookahead : ##### added
BatchNormalizationLayer {} :
RecurrentLSTMLayer {hiddenDim, goBackwards=false} :
BatchNormalizationLayer {} :
DenseLayer {labelDim}
)
Solución 3: Modelo recurrente bidireccional
La capa recurrente bidireccional podría escribirse de la siguiente manera:
BiRecurrentLSTMLayer {outDim} = {
F = RecurrentLSTMLayer {outDim, goBackwards=false}
G = RecurrentLSTMLayer {outDim, goBackwards=true}
apply (x) = Splice (F(x):G(x))
}.apply
y, a continuación, se usan de esta manera:
hiddenDim = 150 ##### changed from 300 to 150
model = Sequential (
EmbeddingLayer {embDim} :
###OneWordLookahead : ##### removed
BatchNormalizationLayer {} :
BiRecurrentLSTMLayer {hiddenDim} :
BatchNormalizationLayer {} :
DenseLayer {labelDim}
)
Solución 4: Clasificación de intenciones
Reduzca las secuencias a la última o primera oculta de la capa recurrente:
apply (x) = Splice (BS.Sequences.Last(F(x)):BS.Sequences.First(G(x)))
##### added Last() and First() calls ^^^
Cambie la entrada de etiqueta de ranura a intención:
intentDim = $numIntents$ ###### name change
...
DenseLayer {intentDim} ##### different dimension
...
intentLabels = Input {intentDim}
...
ce = CrossEntropyWithSoftmax (intentLabels, z)
errs = ErrorPrediction (intentLabels, z)
...
labelNodes = (intentLabels)
Use un nuevo eje dinámico:
n = DynamicAxis() ##### added
query = Input {inputDim, dynamicAxis=n} ##### use dynamic axis
Acknowledgement (Confirmación)
Nos gustaría agradecer a Derek Liu por preparar la base de este tutorial.