Banner de MindSpore de mi post

Programa tu primera red neuronal con MindSpore de Huawei

Consigue programar tu primera red neuronal con el nuevo middleware MindSpore de Huawei, nuevo competidor de PyTorch (Facebook) y TensorFlow (Google).

En un anterior post explicaba que Huawei acaba de anunciar que su framework MindSpore pasa a ser open source y daba un visión general de cómo era.

En este post voy a resumir mi experiencia programando mi primera red neuronal (una LeNet que usa el dataset MNIST) con MindSpore y compartir toda la información para que lo pueda hacer el lector o lectora por su cuenta.

He escogido este ejemplo porque en muchos sentidos clasificar los digítos MNIST con una red neuronal LeNet es el equivalente «Hello World» de Deep Learning para la clasificación de imágenes. A continuación presentamos uno a uno los pasos que se deben realizar para programar una red neuronal en MindSpore, los mismos que realizamos en TensorFlow y PyTorch.  ¡Vamos a por ello!

 

Entorno de trabajo

En este apartado presento cómo se puede habilitar un entorno de ejecución para que el lector o lectora programe sobre el middleware MindSpore evitándose los tediosos pasos que realicé para preparar mi entorno.  Si el propósito de leer este post es simplemente hacerse una idea de cómo es  la API de  MindSpore, se puede saltar este apartado. Pero yo no recomiendo salarse este paso, mis alumnos y alumnas saben que soy un gran defensor del «learn by doing» usando el teclado: ¡Animaros  tocar tecla mientras leéis!

Instalación de MindSpore

Cómo comentaba en mi anterior post, he podido instalar el docker que Huawei ofrece en su Docker Hub siguiendo las indicaciones de esta página web de issues en Gitee  en un MacBook Pro (Intel Core i7).

Ya que programar directamente en el interprete de Python desde un terminal resulta bastante incómodo, he decidido usar iPython notebooks instalando un Jupyter notebook en el contenedor de manera que se pueda acceder al panel de edición de Jupyter desde la comodidad de un navegador. Una vez finalizado lo he añadido a mi Docker Hub.

Para ello el lector o lectora puede descargarse la imagen  del docker que he usado simplemente con el siguiente comando:

docker pull torresai/mindspore

A continuación debemos lanzar un contenedor con esta imagen, que lo podemos hacer con este comando:

docker run -it -p 8888:8888 --name mindspore-test torresai/mindspore /bin/bash

En este punto ya tenemos un docker que tiene instalado todos los paquetes necesarios para poder usar el middleware MindSpore y además tiene un Jupyter notebook al que nos podemos conectar desde nuestro navegador simplemente accediendo a la URL http://127.0.0.1:8888. ¡Manos a la obra!

Ejecución del middleware en el Cloud

Si el lector o lectora no tiene instalado Dockers en su ordenador, puede optar por ejecutarlo en algún proveedor de Cloud. Para el primer código que presentamos en este post en detalle, el servicio gratuito de labs.play-with-docker.com ofrece suficientes recursos para poder ejecutar esta imagen de Docker que he preparado.

Si se opta por esta opción, una vez registrado el usuario (para darse de alta en el servicio sólo se requiere una cuenta de email) y entrado en el portal, se debe pulsar «+ADD NEW INSTANCE» y nos aparecerá un terminal en el que debemos ejecutar los dos comandos indicados en el anterior subapartado.

Una vez ejecutados, puede tardar un par de minutos, en la parte superior de la pantalla aparece el boto «OPEN PORT» indicando el puerto 8888. Si se pulsa en «8888» debería aparecerles el panel de Jupyter desde el que podrán empezar a programar cómodamente.

NOTA: Mientras he estado probando este servicio de play-with-Docker, en alguna ocasión se ha requerido reintentar alguno de los pasos, puedo suponer que dada la gratuidad del servicio en algún momento puede no haber suficientes recursos disponibles.

Descarga del código del GitHub

Con este docker, y un poco de tiempo, he podido programar mi primera red neuronal.  Concretamente una LeNet para clasificar dígitos MNIST basándome  en un ejemplo de código que ofrece el tutorial de MindSpore.

Este código lo comparto en mi GitHub  en el repositorio MindSpore desde donde el lector o lectora se lo pueda descargar en su disco local en forma de notebook .ipnb.

Una vez descargado localmente, este se puede cargar en Jupyter mediante la pestaña «upload».

 

Programación de una red neuronal

En este apartado  presentaré en detalle un ejemplo de código para mostrar que en realidad la API de MindSpore toma prestado  mucha sintaxis de la API de PyTorch y de la API de Keras de TensorFlow.  Por tanto, resulta fácil iniciarse con esta nueva API para cualquiera que esté activo actualmente en la programación de redes neuronales . ¡Vamos allá!

Pasos en la programación de una red neuronal

A continuación presentamos uno a uno los pasos que se deben realizar para programar una red neuronal en MindSpore, los mismos que realizamos en TensorFlow y PyTorch.  En general podríamos resumir los pasos en:

  1. Configuración del entorno de ejecución
  2. Obtención y preparación de los datos
  3. Definición del modelo
  4. Configuración del proceso de aprendizaje
  5. Entrenamiento del modelo
  6. Evaluación del modelo

Pasemos a describir con un poco más de detalle cada uno de ellos.

Configuración del entorno de ejecución

El primer paso es configurar el entorno de ejecución actual, y para ello usaremos la API minspore.context que nos permitirá configurar el modo de ejecución y el backend de ejecución entre otras características. En nuestro ejemplo sencillo nos limitaremos a indicar que usaremos una sola CPU mediante la siguiente línea de código que usa context.set_context():

from mindspore import context

context.set_context(mode=context.GRAPH_MODE, device_target='CPU', enable_mem_reuse=False)

Obtención y preparación de los datos

Descarga de los datos

A continuación debemos descargar los datos directamente de la web de Yann Lecun donde se encuentran almacenados los datos que usaremos , los descompactaremos y los almacenaremos en un directorio local de nuestro contenedor. En concreto hemos decidido los subdirectorios ./MNIST_Data/train para los datos de entrenamiento y el directorio ./MNIST_Data/test para los de test. Las siguientes dos funciones realizan este trabajo.

import os
import urllib.request
from urllib.parse import urlparse
import gzip

def unzipfile(gzip_path):
    open_file = open(gzip_path.replace('.gz',''), 'wb')
    gz_file = gzip.GzipFile(gzip_path)
    open_file.write(gz_file.read())
    gz_file.close()

def download_dataset():
    train_path = "./MNIST_Data/train/"
    test_path = "./MNIST_Data/test/"
    train_path_check = os.path.exists("./MNIST_Data/train/")
    test_path_check = os.path.exists("./MNIST_Data/test/")
    if train_path_check == False and test_path_check ==False:
        os.makedirs(train_path)
        os.makedirs(test_path)
    train_url = {"http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz", 
                 "http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz"}
    test_url = {"http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz", 
                "http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz"}
    for url in train_url:
        url_parse = urlparse(url)
        file_name = os.path.join(train_path,url_parse.path.split('/')[-1])
        if not os.path.exists(file_name.replace('.gz','')):
            file = urllib.request.urlretrieve(url, file_name)
            unzipfile(file_name)
            os.remove(file_name)
    for url in test_url:
        url_parse = urlparse(url)
        file_name = os.path.join(test_path,url_parse.path.split('/')[-1])
        if not os.path.exists(file_name.replace('.gz','')):
            file = urllib.request.urlretrieve(url, file_name)
            unzipfile(file_name)
            os.remove(file_name)

 

download_dataset()

El lector o lectora puede observar que es un código Python «normal» (nada específico de la librería MindSpore)  por ello creo que no hace falta comentarlo.

Preparación de los datos

Una vez descargados los datos, igual que nos ocurre con las librerías TensorFlow y Pytorch, los datos deben ser «preparados» antes de ser usados por una red neuronal. MindSpore usa la API mindspore.dataset  para este paso. Este módulo proporciona APIs para cargar y procesar los datasets habituales como MNIST, CIFAR-10, CIFAR-100, VOC, ImageNet, CelebA, etc. También soporta datasets en formatos específicos, incluyendo mindrecord, tfrecord, manifest.  Veamos el código:

from mindspore.dataset.transforms.vision import Inter
from mindspore.common import dtype as mstype
import mindspore.dataset as ds

def processing_data(data_path, batch_size=32):

    mnist_ds = ds.MnistDataset(data_path)

    resize_height, resize_width = 32, 32
    rescale = 1.0 / 255.0
    shift = 0.0
    rescale_nml = 1 / 0.3081
    shift_nml = -1 * 0.1307 / 0.3081

    resize_op =      CV.Resize((resize_height, resize_width), interpolation=Inter.LINEAR)  
    rescale_nml_op = CV.Rescale(rescale_nml, shift_nml) 
    rescale_op =     CV.Rescale(rescale, shift)
    hwc2chw_op =     CV.HWC2CHW() 
    type_cast_op =   C.TypeCast(mstype.int32)

    mnist_ds = mnist_ds.map(input_columns="label", operations=type_cast_op)
    mnist_ds = mnist_ds.map(input_columns="image", operations=resize_op)
    mnist_ds = mnist_ds.map(input_columns="image", operations=rescale_op)
    mnist_ds = mnist_ds.map(input_columns="image", operations=rescale_nml_op)
    mnist_ds = mnist_ds.map(input_columns="image", operations=hwc2chw_op)

    mnist_ds = mnist_ds.shuffle(buffer_size=10000)
    mnist_ds = mnist_ds.batch(batch_size, drop_remainder=True)
    return mnist_ds

En mnist_ds = ds.MnistDataset(data_path)  definimos el dataset para MNIST.  A continuación se concretan los parámetros para las operaciones de transformación que realizaremos con .map().  En MindSpore realizaremos en esta etapa el habitual shuffle de los datos de entrenamiento y la creación de los lotes (batchs), y que en este ejemplo usaremos un  batch_size de 32. MindSpore soporta varios preprocesados de datos que se pueden consultar en la sección Data Processing and Augmentation del tutorial de MindSpore.

Finalmente con las siguientes dos líneas de código creamos los dataset para entrenamiento y test que usaremos más adelante:

ds_train = processing_data("./MNIST_Data/train", 32)
ds_eval  = processing_data("./MNIST_Data/test")

Verificar los datos de entrada

Aunque este no es un paso requerido para entrenar la red neuronal, sí que lo encuentro útil en este ejemplo con propósito pedagógico para que el lector o lectora vaya siguiendo que contienen las estructuras de datos que vamos manipulando. El siguiente código, usando la librería matplotlib, nos permite visualizar cómo son los datos que tenemos en ds_train preparados para ser consumidos por la red neuronal. En concreto este código obtiene el primer lote (batch) de datos de 32 elementos, compuesto cada uno de ellos por una imagen y la etiqueta correspondiente, siendo mostrado por pantalla los primeros 20 de ellos.

import numpy as np
import matplotlib.pyplot as plt

iterator = ds_train.create_dict_iterator()
item = next (iterator)

image = np.squeeze(item["image"],1)
label = item["label"]

fig = plt.figure(figsize=(8, 8))
for idx in np.arange(20):
    ax = fig.add_subplot(4, 10/2, idx+1, xticks=[], yticks=[])
    ax.imshow(image[idx,:,:], cmap=plt.cm.binary)
    ax.set_title(str(label[idx]))

Definición del modelo

La red neuronal LeNet-5 elegida para este ejemplo incluye, además de la capa de entrada, 2 capas convolucionales, 2 capas de pooling, y finalmente  un clasificador compuesto por 3 capas densamente conectadas. A pesar que esta red es simple, nos es perfecta para ver como se definen las redes neuronales en MindSpore.

Una red neuronal en MindSpore hereda mindspore.nn.Cell. Cell es la clase base de todas las redes neuronales que define cada capa de la red neuronal en el método init() , y luego define el método construct() para completar la red neuronal. De acuerdo con la estructura de la red LeNet-5, la definiremos de la siguiente manera:

from mindspore.common.initializer import TruncatedNormal

def weight_variable():
    """Weight initial."""
    return TruncatedNormal(0.02)

class LeNet5(nn.Cell):

    def __init__(self):
        super(LeNet5, self).__init__()
        self.batch_size = 32
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5,
                               weight_init=weight_variable(),has_bias=False,pad_mode="valid")
        self.conv2 = nn.Conv2d(6, 16, kernel_size=5,
                               weight_init=weight_variable(),has_bias=False,pad_mode="valid")
        self.fc1 =  nn.Dense(in_channels= 16 * 5 * 5, out_channels= 120,
                               weight_init= weight_variable(),bias_init=weight_variable())
        self.fc2 =  nn.Dense(in_channels= 120,  out_channels= 84,
                               weight_init=weight_variable(),bias_init=weight_variable())
        self.fc3 =  nn.Dense(in_channels=  84, out_channels= 10,
                               weight_init=weight_variable(),bias_init=weight_variable())
        self.relu = nn.ReLU()
        self.max_pool2d = nn.MaxPool2d(kernel_size=2, stride=2)
        self.reshape = P.Reshape()

    def construct(self, x):
        x = self.conv1(x)
        x = self.relu(x)
        x = self.max_pool2d(x)
        x = self.conv2(x)
        x = self.relu(x)
        x = self.max_pool2d(x)
        x = self.reshape(x, (self.batch_size, -1))
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        x = self.relu(x)
        x = self.fc3(x)
        return x

network = LeNet5()

Cómo podemos observar, la similitud con la manera de especificar una red neuronal usando la API de PyTorch es muy elevada. Por ejemplo la clase que nos proporciona las capas predefinidas, mindspore.nn, usa la notación « nn» exactamente igual que en Pytorch. O que el método construct() es equivalente al de forward() en PyTorch.

Igual que en PyTorch, MindSpore dispone de una serie de redes predefinidas. En este ejemplo podríamos haber usado las dos siguientes lineas de código para ahorrarnos la definición de la red:

from mindspore.model_zoo.lenet import LeNet5
network = LeNet5(10)

 

Configuración del proceso de aprendizaje

Al igual que en cualquier middleware, para poder entrenar la red neuronal nos hace falta definir la función de pérdida y el optimizador.

En estos momentos las funciones de pérdida (Loss functions) soportadas por MindSpore incluyen  SoftmaxCrossEntropyWithLogits, L1Loss y MSELoss según puedo leer en la documentación. En este ejemplo usaremos la función de pérdida  SoftmaxCrossEntropyWithLogits (que como vemos incluye el Softmax que no se ha definido en la red en el paso anterior):

from mindspore.nn.loss import SoftmaxCrossEntropyWithLogits
criterion = SoftmaxCrossEntropyWithLogits(is_grad=False, sparse=True, reduction='mean')

Según la documentación de MidSpore los optimizadores soportados en estos momentos incluyen Adam, AdamWeightDecay y Momentum. Pero en mis pruebas solo me ha funcionado el optimizador Momentum:

from mindspore.nn import Momentum
optimizer = Momentum(params=network.trainable_params(), learning_rate=0.01, momentum=0.9)

Entrenamiento del modelo

En cuanto a forma de especificar el entrenamiento en la API de MindSpore se acerca más a la forma de Keras (que a la de PyTorch) con una llamada al método train() del modelo, en cierta manera equivalente al método fit() que usamos en Keras (mientras que en PyTorch el programador especifica el bucle de entrenamiento).

Además, MindSpore, como en Keras, proporciona el mecanismo de callback  para poder personalizar el entrenamiento en una iteración o epoch. Por ejemplo con el callbacks ModelCheckpoint se puede obtener los parámetros del modelo (el parámetro  save_checkpoint_steps indica la frecuencia con la que ese salva y el parámetro keep_checkpoint_max indica el máximo número de ficheros de checkpoint que pueden ser salvados). Con el callback LossMonitor se puede monitorizar los cambios del valor de la loss durante el entrenamiento:

from mindspore.train.callback import ModelCheckpoint, CheckpointConfig, LossMonitor
from mindspore.nn.metrics import Accuracy 
from mindspore.train import Model
epochs = 1
config_ck = CheckpointConfig(save_checkpoint_steps=1875, keep_checkpoint_max=10)
ckpoint_cb = ModelCheckpoint(prefix="checkpoint_lenet", config=config_ck)

model = Model(network, criterion, optimizer, metrics={"Accuracy": Accuracy()})
model.train(epochs, ds_train, callbacks=[ckpoint_cb, LossMonitor()], dataset_sink_mode=False)

Evaluación del modelo

Finalmente queda evaluar el modelo, o bien usarlo para inferencia. Mediante load_checkpoint se pueden cargar los parámetros del modelo que se encuentran salvados en un fichero (que ha generado el método train()) del modelo).  El método load_checkpoint retorna un diccionario con los parámetros y con el método load_param_into_net los cargamos al modelo.

Nuevamente, para la evaluación, MindSpore se decanta por un estilo parecido a Keras, mediante el uso del método eval() del modelo (recordemos que en ds_eval hemos cargado los datos de test):

from mindspore.train.serialization import load_checkpoint, load_param_into_net

param_dict = load_checkpoint("checkpoint_lenet_1-1_1875.ckpt")
load_param_into_net(network, param_dict)
acc = model.eval(ds_eval, dataset_sink_mode=False)
print("{}".format(acc))
{'Accuracy': 0.9658453525641025}

A Bonus Network: AlexNet con CIFAR-10

Durante las pruebas he programado también una red de mayor tamaño,  una AlexNet, usando el dataset CIFAR-10 que  recordaran que consta de 60 000 imágenes en color de 32 × 32 píxeles clasificadas en 10 clases, con 6000 imágenes por clase. Hay 50 000 imágenes de entrenamiento y 10 000 imágenes de prueba.

En mi GitHub se puede encontrar el código que comparto para que lo pruebe quién quiera.  Este notebook lo he ejecutado en un docker con la imagen que les he compartido (aumentando la memoria asignada al docker a 12 GBytes).

Cómo verán, la precisión obtenida por el modelo no es muy alta, un 54%, pero esto es debido a que he parado el entrenamiento a una sola epoch que ha requerido 90 minutos y he pensado que a efectos de comprobar el código (no nos preocupa ahora la precisión) es suficiente.

 

[versión en inglés en Medium]