Consigue programar tu primera red neuronal con el nuevo middleware MindSpore de Huawei, nuevo competidor de PyTorch (Facebook) y TensorFlow (Google).
Content
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:
- Configuración del entorno de ejecución
- Obtención y preparación de los datos
- Definición del modelo
- Configuración del proceso de aprendizaje
- Entrenamiento del modelo
- 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))
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]