Julio de 2018

Volumen 33, número 7

Serie de pruebas: introducción a la clasificación de imágenes DNN con CNTK

Por James McCaffrey

James McCaffreyClasificación de imágenes consiste en determinar qué categoría pertenece una imagen de entrada para, por ejemplo que identifica una fotografía como uno que contiene "manzanas" o "oranges" o "banana". Los dos métodos más comunes para la clasificación de imágenes utiliza una red neuronal profunda estándar (DNN) o mediante una red neuronal convolucional (CNN). En este artículo, explicaré el enfoque DNN, mediante la biblioteca de CNTK.

Eche un vistazo a la Figura 1 para conocer la orientación de este artículo. El programa de demostración crea un modelo de clasificación de imágenes para un subconjunto del conjunto de datos modificado Instituto nacional de normas y tecnología (MNIST). El conjunto de datos de entrenamiento de demostración está formado por 1000 imágenes de dígitos manuscritos. Cada imagen es 28 alto por 28 píxeles de ancho (784 píxeles) y representa un dígito, del 0 al 9.

Clasificación de imágenes con una DNN con CNTK
Figura 1 clasificación de imágenes mediante un DNN con CNTK

El programa de demostración crea una red neuronal estándar con 784 nodos de entrada (uno para cada píxel), dos capas de procesamiento ocultos (cada uno con 400 nodos) y 10 nodos de salida (uno para cada dígito posible). El modelo se entrena con 10 000 iteraciones. La pérdida (también conocido como error de entrenamiento) disminuye lentamente y la precisión de predicción aumenta lentamente, que indica el aprendizaje está trabajando.

Una vez completado el entrenamiento, la demostración aplica el modelo entrenado a un conjunto de datos de prueba de 100 elementos. La exactitud del modelo es 84.00%, por lo que 84 de las imágenes de 100 pruebas se han clasificado correctamente.

En este artículo se da por supuesto que tiene intermedios o altos de programación conocimientos con un lenguaje de familia C, pero no se supone que sepa gran cosa sobre CNTK o las redes neuronales. La demostración se programó con Python, pero, incluso si no lo conoce, debería poder seguirla sin dificultad. En este artículo se presenta todo el código del programa de demostración. Los datos de dos archivos que se utilizan están disponibles en la descarga que acompaña este artículo.

Descripción de los datos

El conjunto de datos MNIST completo consta de 60 000 imágenes de entrenamiento y 10 000 imágenes para realizar pruebas. Un poco inusualmente, el conjunto de entrenamiento se encuentra en dos archivos, uno que contiene todos los valores de píxel y otro que contiene los valores de etiqueta asociada (0-9). Las imágenes de prueba también se encuentran en dos archivos.

Además, los archivos de cuatro origen se almacenan en un formato binario. Cuando se trabaja con redes neuronales profundas, obtener los datos a un formato utilizable es casi siempre un proceso lento y difícil. Figura 2 se muestra el contenido de la primera imagen de entrenamiento. El punto clave es que cada imagen tiene 784 píxeles, y cada píxel es un valor entre 00h (0 decimal) y FFh (255 decimal).

Una imagen MNIST
Figura 2 una imagen MNIST

Antes de escribir el programa de demostración, escribí un programa de utilidad para leer los archivos de origen binarios y escribir un subconjunto de su contenido en archivos de texto que pueden utilizarse fácilmente en un objeto de lector CNTK. El archivo mnist_train_1000_cntk.txt es similar al siguiente:

|digit 0 0 0 0 0 1 0 0 0 0 |pixels 0 .. 170 52 .. 0
|digit 0 1 0 0 0 0 0 0 0 0 |pixels 0 .. 254 66 .. 0
etc.

Introducción a los datos binarios sin procesar de MNIST en formato CNTK no es trivial. El código fuente de mi programa de utilidad puede encontrarse en: bit.ly/2ErcCbw.

Hay 1000 líneas de datos y cada uno representa una imagen. Las etiquetas "| dígitos" y "| píxeles" indicar el inicio de los valores de predictor y el valor de predicción. La etiqueta de dígitos es "one-hot" con codificación en la posición del bit 1 indica el dígito. Por lo tanto, en el código anterior, las dos primeras imágenes representan un "5" y "1". Cada línea de datos tiene 784 valores de píxel, cada uno de ellos está entre 0 y 255. Archivo mnist_test_100_cntk.txt tiene 100 imágenes y usa el mismo formato compatible con CNTK.

En los problemas de red neuronales más, va a normalizar los valores de predictor. En lugar de directamente la normalización de los valores de píxel de los archivos de datos, el programa de demostración normaliza los datos sobre la marcha, como verá en breve.

El programa de demostración

El programa de demostración completo, con ediciones menores para ahorrar espacio, se presenta en la Figura 3. Se ha quitado una comprobación de error normal. Se aplica una sangría con dos caracteres de espacio en lugar de los cuatro habituales para ahorrar espacio. Tenga en cuenta que Python usa el carácter "\" para la continuación de línea.

Figura 3 de demostración completo listado de programas

# mnist_dnn.py
# MNIST using a 2-hidden layer DNN (not a CNN)
# Anaconda 4.1.1 (Python 3.5.2), CNTK 2.4
import numpy as np
import cntk as C
def create_reader(path, input_dim, output_dim, rnd_order, m_swps):
  x_strm = C.io.StreamDef(field='pixels', shape=input_dim,
    is_sparse=False)
  y_strm = C.io.StreamDef(field='digit', shape=output_dim,
    is_sparse=False)
  streams = C.io.StreamDefs(x_src=x_strm, y_src=y_strm)
  deserial = C.io.CTFDeserializer(path, streams)
  mb_src = C.io.MinibatchSource(deserial, randomize=rnd_order,
    max_sweeps=m_swps)
  return mb_src
# ===================================================================
def main():
  print("\nBegin MNIST classification using a DNN \n")
  train_file = ".\\Data\\mnist_train_1000_cntk.txt"
  test_file  = ".\\Data\\mnist_test_100_cntk.txt"
  C.cntk_py.set_fixed_random_seed(1)
  input_dim = 784  # 28 x 28 pixels
  hidden_dim = 400
  output_dim = 10  # 0 to 9
  X = C.ops.input_variable(input_dim, dtype=np.float32)
  Y = C.ops.input_variable(output_dim)  # float32 is default
  print("Creating a 784-(400-400)-10 ReLU classifier")
  with C.layers.default_options(init=\
    C.initializer.uniform(scale=0.01)):
    h_layer1 = C.layers.Dense(hidden_dim, activation=C.ops.relu,
      name='hidLayer1')(X/255) 
    h_layer2 = C.layers.Dense(hidden_dim, activation=C.ops.relu,
      name='hidLayer2')(h_layer1)
    o_layer = C.layers.Dense(output_dim, activation=None,
      name='outLayer')(h_layer2)
  dnn = o_layer               # train this
  model = C.ops.softmax(dnn)  # use for prediction
  tr_loss = C.cross_entropy_with_softmax(dnn, Y)
  tr_eror = C.classification_error(dnn, Y)
  max_iter = 10000   # num batches, not epochs
  batch_size = 50   
  learn_rate = 0.01
  learner = C.sgd(dnn.parameters, learn_rate)
  trainer = C.Trainer(dnn, (tr_loss, tr_eror), [learner]) 
  # 3. create reader for train data
  rdr = create_reader(train_file, input_dim, output_dim,
    rnd_order=True, m_swps=C.io.INFINITELY_REPEAT)
  mnist_input_map = {
    X : rdr.streams.x_src,
    Y : rdr.streams.y_src
  } 
  # 4. train
  print("\nStarting training \n")
  for i in range(0, max_iter):
    curr_batch = rdr.next_minibatch(batch_size, \
      input_map=mnist_input_map)
    trainer.train_minibatch(curr_batch)
    if i % int(max_iter/10) == 0:
      mcee = trainer.previous_minibatch_loss_average
      macc = (1.0 - trainer.previous_minibatch_evaluation_average) \
        * 100
      print("batch %4d: mean loss = %0.4f, accuracy = %0.2f%% " \
        % (i, mcee, macc))
  print("\nTraining complete \n")
  # 5. evaluate model on test data
  rdr = create_reader(test_file, input_dim, output_dim,
    rnd_order=False, m_swps=1)
  mnist_input_map = {
    X : rdr.streams.x_src,
    Y : rdr.streams.y_src
  }
  num_test = 100
  test_mb = rdr.next_minibatch(num_test, input_map=mnist_input_map)
  test_acc = (1.0 - trainer.test_minibatch(test_mb)) * 100
  print("Model accuracy on the %d test items = %0.2f%%" \
    % (num_test,test_acc)) 
  print("\nEnd MNIST classification using a DNN \n")
if __name__ == "__main__":
  main()

La demostración de mnist_dnn.py tiene una función auxiliar, create_reader. Toda la lógica de control está en la función principal única. Dado que CNTK es joven y está en continuo desarrollo, es una buena idea agregar un comentario que detalla qué versión se está usando (2.4 en este caso).

Instalar CNTK puede resultar un poco complicado si está familiarizado con el mundo de Python. En primer lugar instale una distribución de Anaconda de Python, que contiene el intérprete de Python necesario, los paquetes necesarios, como NumPy y SciPy y herramientas útiles como pip. He usado Anaconda3 4.1.1 de 64 bits, que incluye Python 3.5. Después de instalar Anaconda, instalar CNTK como paquete de Python, no es un sistema independiente, mediante la utilidad pip. Desde un shell ordinario, el comando que usé fue:

>pip install https://cntk.ai/PythonWheel/CPU-Only/cntk-2.4-cp35-cp35m-win_amd64.whl

Tenga en cuenta es "cp35" en el archivo de ruedas que indica el archivo para su uso con Python 3.5. Tenga cuidado; casi todos los errores de instalación de CNTK que he visto han sido debido a incompatibilidades entre versiones de Anaconda y CNTK.

La firma de la función lector es create_reader (ruta de acceso, input_dim, output_dim, rnd_order, m_swps). El parámetro de ruta de acceso apunta a un archivo de prueba o entrenamiento que se encuentra en formato CNTK. El parámetro rnd_order es una marca booleana que se establecerá en True para los datos de entrenamiento porque desea procesar los datos de entrenamiento en orden aleatorio para evitar oscilantes sin realizar el curso de entrenamiento. El parámetro se establecerá en False al leer los datos de prueba para evaluar la precisión del modelo porque no es importante el orden, a continuación. El parámetro m_swps ("barridos máximo") se establecerá en la constante INFINITELY_REPEAT para datos de entrenamiento (por lo que se pueden procesar varias veces) y establezca en 1 para la evaluación de datos de prueba.

Creación del modelo

La demostración prepara una red neuronal profunda con:

train_file = ".\\Data\\mnist_train_1000_cntk.txt"
test_file  = ".\\Data\\mnist_test_100_cntk.txt"
C.cntk_py.set_fixed_random_seed(1)
input_dim = 784
hidden_dim = 400
output_dim = 10
X = C.ops.input_variable(input_dim, dtype=np.float32)
Y = C.ops.input_variable(output_dim)  # 32 is default

Suele ser una buena idea establecer explícitamente CNTK global número valor de inicialización aleatorio para que los resultados sea reproducibles. El número de nodos de entrada y salida viene determinado por los datos, pero el número de nodos de procesamiento ocultos es un parámetro libre que debe determinarse mediante prueba y error. Uso de variables de 32 bits es el valor predeterminado de CNTK y se cobra típica para las redes neuronales porque la precisión obtenida al usar 64 bits no vale la pena la disminución del rendimiento.

La red se crea así:

with C.layers.default_options(init=
  C.initializer.uniform(scale=0.01)):
  h_layer1 = C.layers.Dense(hidden_dim,
    activation=C.ops.relu, name='hidLayer1')(X/255) 
  h_layer2 = C.layers.Dense(hidden_dim,
  activation=C.ops.relu, name='hidLayer2')(h_layer1)
  o_layer = C.layers.Dense(output_dim, activation=None,
    name='outLayer')(h_layer2)
dnn = o_layer               # train this
model = C.ops.softmax(dnn)  # use for prediction

Python con una instrucción es un acceso directo sintáctico para aplicar un conjunto de argumentos comunes a varias funciones. Aquí se utiliza para inicializar todos los pesos de red a los valores aleatorios entre -0.01 y +0.01. El objeto X contiene los valores de entrada 784 para una imagen. Tenga en cuenta que cada valor se normaliza dividiendo por 255, por lo que los valores de entrada reales estará en el intervalo [0.0, 1.0].

 El acto de los valores de entrada normalizados como entrada para la primera capa oculta. Los resultados de la primera capa oculta actúan como entradas para la segunda capa oculta. A continuación, se envían los resultados de la segunda capa oculta a la capa de salida. Las dos capas ocultas usar activación ReLU (unidades lineales rectificadas), que, para la clasificación de imágenes, a menudo funciona mejor que la activación tanh estándar.

Tenga en cuenta que no se aplica ninguna activación a los nodos de salida. Esto es una peculiaridad de CNTK, ya que la función de entrenamiento de CNTK espera valores sin procesar y sin activar. El objeto de dnn es simplemente un alias cómodo. El objeto de modelo tiene activación softmax, por lo que se puede usar después del entrenamiento para hacer predicciones. Dado que Python asigna por referencia, el objeto de la dnn de formación, también entrena el objeto de modelo.

Entrenamiento de la red neuronal

La red neuronal está preparada para entrenarse con:

tr_loss = C.cross_entropy_with_softmax(dnn, Y)
tr_eror = C.classification_error(dnn, Y)
max_iter = 10000 
batch_size = 50   
learn_rate = 0.01
learner = C.sgd(dnn.parameters, learn_rate)
trainer = C.Trainer(dnn, (tr_loss, tr_eror), [learner])

El objeto de entrenamiento pérdida (tr_loss) indica a CNTK cómo medir el error al entrenar. El error de entropía cruzada suele ser la mejor opción para los problemas de clasificación. El objeto de error (tr_eror) de la clasificación de entrenamiento se puede usar para calcular automáticamente el porcentaje de predicciones incorrectas durante el entrenamiento o después del entrenamiento. Es necesario especificar una función de pérdida, pero es opcional especificar una función de error de clasificación.

Los valores para el número máximo de iteraciones de entrenamiento, el número de elementos en un lote para entrenar en un momento y la velocidad de aprendizaje es gratis todos los parámetros que deben determinarse mediante prueba y error. Se puede considerar el objeto learner como un algoritmo y el objeto que entrena como el objeto que utiliza el factor para buscar los valores válidos para la red neuronal valores de ponderaciones y sesgos. El factor de descenso de gradiente estocástico (sgd) es el algoritmo más primitivo, pero funciona bien para problemas sencillos. Las alternativas incluyen la propagación de la raíz cuadrada Media (rmsprop) y la estimación de momento adaptable (adam).

Se crea un objeto de lector para los datos de entrenamiento con estas instrucciones:

rdr = create_reader(train_file, input_dim, output_dim,
  rnd_order=True, m_swps=C.io.INFINITELY_REPEAT)
mnist_input_map = {
  X : rdr.streams.x_src,
  Y : rdr.streams.y_src
}

Si examina el código de create_reader de figura 3, verá que especifica los nombres de etiqueta ("píxeles" y "digit") usados en el archivo de datos. Puede considerar create_reader y el código para crear un objeto lector como código reutilizable para problemas de clasificación de imagen DNN. Lo único que debe cambiar es que los nombres de etiqueta y el nombre del diccionario de asignación (mnist_input_map).

Una vez preparado todo, entrenamiento se realiza como se muestra en figura 4.

Figura 4 entrenamiento

print("\nStarting training \n")
for i in range(0, max_iter):
  curr_batch = rdr.next_minibatch(batch_size, \
    input_map=mnist_input_map)
  trainer.train_minibatch(curr_batch)
  if i % int(max_iter/10) == 0:
    mcee = trainer.previous_minibatch_loss_average
    macc = (1.0 - \
      trainer.previous_minibatch_evaluation_average) \
        * 100
    print("batch %4d: mean loss = %0.4f, accuracy = \
      %0.2f%% " % (i, mcee, macc))

El programa de demostración está diseñado para que cada iteración procesa un lote de elementos de aprendizaje. Muchas bibliotecas de red neuronal usan el término "epoch" para hacer referencia a un paso a través de todos los elementos de entrenamiento. En este ejemplo, dado que hay 1.000 elementos de entrenamiento y el tamaño del lote se establece en 50, una época sería 20 iteraciones.

Una alternativa al entrenamiento con un número fijo de iteraciones es terminar el entrenamiento cuando pérdida/error cae por debajo de un umbral. Es importante mostrar la pérdida o error durante el entrenamiento porque los errores en el entrenamiento son una regla, más que una excepción. Error de entropía cruzada es difícil de interpretar directamente, pero desea ver los valores que tienden a reducirse. En lugar de mostrar el error de clasificación Media ("25 por ciento mal"), la demostración calcula e imprime la precisión de clasificación Media ("75 por ciento corregir"), que es una métrica más natural en mi opinión.

Evaluar y usar el modelo

Una vez entrenado un clasificador de imágenes, normalmente desea evaluar el modelo entrenado en los datos de prueba que se han reservado. La demostración calcula la precisión de clasificación como se muestra en figura 5.

Precisión de clasificación de informática de la figura 5

rdr = create_reader(test_file, input_dim, output_dim,
  rnd_order=False, m_swps=1)
mnist_input_map = {
  X : rdr.streams.x_src,
  Y : rdr.streams.y_src
}
num_test = 100
test_mb = rdr.next_minibatch(num_test,
  input_map=mnist_input_map)
test_acc = (1.0 - trainer.test_minibatch(test_mb)) * 100
print("Model accuracy on the %d test items = %0.2f%%" \
  % (num_test,test_acc)))

Se crea un nuevo lector de datos. Tenga en cuenta que, al contrario de lo que sucede con el lector usado para el entrenamiento, el nuevo lector no recorre los datos en orden aleatorio y el número de barridos está definido en 1. Se vuelve a crear el objeto de diccionario mnist_input_map. Un error común es intentar usar el sistema de lectura original, pero el objeto rdr ha cambiado por lo que deberá volver a crear la asignación. La función test_minibatch devuelve el error promedio de clasificación para su argumento de minilote, que en este caso es el conjunto de pruebas de 100 elementos completa.

Durante el entrenamiento o después de este, normalmente, querrá guardar el modelo. En CNTK, ahorro tendría el siguiente aspecto:

mdl_name = ".\\Models\\mnist_dnn.model"
model.save(mdl_name)

Se guardarían utilizando el formato CNTK v2 predeterminado. Una alternativa es usar el formato de intercambio de red neuronal abierta (ONNX). Tenga en cuenta que por lo general deseará guardar el objeto de modelo (con activación softmax) en lugar del objeto de la dnn (sin activación de salida). Desde otro programa, un modelo guardado se puede cargar en la memoria junto con las líneas de:

mdl_name = ".\\Models\\mnist_dnn.model"
model = C.ops.functions.Function.load(mdl_name)

Después de cargarlo, el modelo se puede usar como si se acabara de entrenar. El programa de demostración no usa el modelo entrenado para hacer una predicción. Código de predicción podría parecerse a esto:

input_list = [0.55] * 784  # [0.55, 0.55, . . 0.55]
input_vec = np.array(input_list, dtype=np.float32)
pred_probs = model.eval(input_vec)
pred_digit = np.argmax(pred_probs)
print(pred_digit)

El input_list tiene una entrada ficticia de 784 valores de píxel, cada uno con valor 0,55 (Recuerde que el modelo se entrenó en datos normalizados, por lo que debe suministrar los datos normalizados). Los valores de píxel se copian en una matriz de NumPy. La llamada a la función eval devolvería una matriz de 10 valores que suman 1,0 y puede interpretarse aproximadamente como probabilidades. La función argmax devuelve el índice (de 0 a 9) del valor más grande, que equivale a cómodamente el dígito previsto. ¡Fantástico!

Resumen

Uso de una red neuronal profunda solía ser el enfoque más común para la clasificación de imágenes simple. Sin embargo, las Dnn tienen al menos dos limitaciones más importantes. En primer lugar, las Dnn no se escalan bien a las imágenes que tienen un gran número de píxeles. En segundo lugar, las Dnn explícitamente no tienen en cuenta la geometría de píxeles de imagen. Por ejemplo, en una imagen MNIST, un píxel que está directamente debajo de un segundo píxel es 28 posiciones a primer píxel en el archivo de entrada.

Debido a estas limitaciones y por otras razones, también el uso de una red neuronal convolucional (CNN) ahora es más común para la clasificación de imágenes. Es decir, para la clasificación de imagen básica, tareas, mediante un DNN es más fácil y a menudo, al igual que en (o incluso más) eficaz que el uso de un CNN.


El Dr. James McCaffrey trabaja para Microsoft Research en Redmond, Washington. Ha colaborado en el desarrollo de varios productos de Microsoft como, por ejemplo, Internet Explorer y Bing. Puede ponerse en contacto con el Dr. McCaffrey en jamccaff@microsoft.com.

Gracias a los siguientes expertos técnicos de Microsoft por revisar este artículo: Chris Lee, Ricky Loynd, Ken Tran


Discuta sobre este artículo en el foro de MSDN Magazine