DIA4RA para programadores: Programando con la API de Bajo Nivel de Tensorflow (I)

Programando con la API de Bajo Nivel de Tensorflow

Tras una breve introducción en la que os mostramos el objetivo de DIA4RA y nuestra intención de hacer un recorrido por las principales herramientas que estamos empleando, ha llegado el momento de ponernos manos a la obra. Y qué mejor manera de comenzar que hacerlo por el tejado, la librería que vamos a utilizar para implementar los algoritmos que dotarán de inteligencia al robot. Como habréis adivinado por el título, se trata de Tensorflow.

Tensorflow y el cálculo de gradientes

A diferencia de lo que muchos podrían pensar, esta librería desarrollada por Google no es únicamente aplicable a casos de uso de Inteligencia Artificial. En realidad, es un framework de computación numérica que, entre muchas otras posibilidades, permite el cálculo de gradientes de forma automática.

Muchos algoritmos de Deep Learning emplean como base de aprendizaje la técnica de “Gradient Descent” que, a muy grandes rasgos, consiste en intentar encontrar el valor mínimo global de la función de error del problema que se pretende aprender a solucionar. Para ir avanzando en la búsqueda de ese valor mínimo, en cada paso de dicho proceso de aprendizaje se calcula el gradiente de la función de error en ese punto, con el objetivo de conocer la dirección hacia donde hay que dirigirse para ir minimizando el error cometido.

Como Tensorflow facilita este cálculo y tiene a un gigante como Google manteniendo este proyecto, se ha popularizado a la hora de aplicarse en el ámbito de las Redes Neuronales Artificiales.

Cómo funciona Tensorflow

Tensorflow es multi-plataforma y para ello, proporciona un motor denominado “Tensorflow Distributed Execution Engine” cuyo “core” está implementado en C++. Su objetivo es permitir la ejecución en múltiples arquitecturas como dispositivos móviles, sistemas embebidos,  CPUs, GPUs… Incluso TPUs (Tensor Processing Units), que es un HW desarrollado por Google optimizado para realizar de forma muy eficiente operaciones con “tensores”.

Por encima de esta capa se asientan “frontends” en distintos lenguajes de programación como Python, C++, Java, Julia, Go, Javascript, etc. Pero, sin duda, la versión más utilizada es la de Python que además ofrece APIs con diversos niveles de abstracción.

En este artículo vamos a hacer un breve recorrido por la API de Python de Bajo Nivel de Tensorflow y en los siguientes nos centraremos en una de las APIs de Alto Nivel que más se está impulsando desde el equipo de desarrolladores de Tensorflow. Esta coincide con la que estamos empleando para codificar los algoritmos en DIA4RA: la Estimator API.

La principal razón de haber tomado esta elección se debe a que permite emplear el mismo código tanto para entrenar modelos y hacer inferencia en local como para subir el código al Cloud y realizar un entrenamiento distribuido y posteriormente exportar dicho modelo ya entrenado y emplearlo (servirlo) para poder realizar predicciones desde la nube.

Otra API de Alto Nivel de Tensorflow bastante conocida por su simplicidad es Keras, que también se utilizará en este proyecto pero siempre combinada con la estructura de programación de la Estimator API para mantener un mismo estilo entre diferentes modelos.

API de Python de Bajo Nivel de Tensorflow

Como ya tendréis ganas de empezar a cacharrear con Tensorflow, vamos a comenzar mostrando algunos de los componentes esenciales de su API de Bajo Nivel.

Tensores

El elemento fundamental del que toma su nombre Tensorflow es el tensor y se implementa mediante un nodo tf.Tensor. Puede definirse como un array n-dimensional  y se corresponde con su estructura básica de almacenamiento de datos.

Cada tensor tiene un nombre, un rank, un shape y un tipo.

  • El nombre identifica de forma única al tensor.
  • El rank es el número de dimensiones de un tensor. De forma que:
    • un tensor de 0 dimensiones (rank = 0) es un escalar (por ejemplo, tensor = 2)
    • otro tensor de 1 dimensión (rank = 1) es un vector (por ejemplo, tensor = [2, 7])
    • un tensor de 2 dimensiones (rank = 2) se trata de una matrix de n filas y m columnas (por ejemplo, tensor = [[2, 7], [3,5], [6, 1]]), etc.
  • El shape es el número de elementos en cada dimensión.
    • El tensor = 3 tiene rank = 0 y shape = ( ).
    • Un tensor = [2, 7] tiene rank = 1 y shape = (2).
    • El tensor = [[2, 7], [3, 5], [6, 1]] tiene rank = 2 y shape = (3, 2).
      Se puede dar el caso en que el rank sea conocido pero se desconozca el tamaño de 1 ó más dimensiones. Un ejemplo de esta situación se da cuando se realiza un entrenamiento y se conoce el shape del vector de features pero se deja sin fijar hasta el instante de la ejecución el tamaño de cada batch de ejemplos con los que se entrenará. Suponiendo que el vector de features de la entrada es de 32x32x1, como se pretende dejar sin establecer el shape del tamaño de batch, se utilizará la palabra reservada “None” para especificar su shape, quedando como resultado un tensor de shape (None, 32, 32, 1).
  • Por último, cada tensor también dispone de un tipo de datos, como por ejemplo, tf.float32, tf.int64, tf.string, etc.

Operaciones

Los datos contenidos en los tensores se corresponden con las entradas de operaciones que realizan cálculos con dicha información. Cuando se consulta documentación de Tensorflow, a menudo se verá que se hace referencia a las operaciones empleando el término ‘ops’ y formalmente genera un nodo tf.Operation.

Las operaciones son nodos que toman cero o más tensores como entrada, realizan cálculos a partir de esos datos y devuelven cero o más tensores como salida. Para instanciar una op hay que llamar a su constructor, que recibirá los tensores necesarios. Por ejemplo, una operación como tf.multiply puede tomar 2 entradas y cuando sea ejecutada devolverá el resultado de su producto.

import tensorflow as tf

import numpy as np

# Inicialización de arrays de numpy que actuarán como los tensores de entrada de la op

a = np.array([2, 3], dtype=np.int32)

b = np.array([4, 5], dtype=np.int32)

# Creación de la op de multiplicación

resultado = tf.multiply(a, b)

Si se desea añadir una nueva op que no esté presente entre las ofrecidas por Tensorflow, será necesario seguir los pasos indicados en esta url de la documentación oficial.

Resumiendo, el proceso conlleva realizar las siguientes acciones:

  1. Registrar la nueva operación en un fichero C++.
  2. Implementar la op en C++.
  3. De forma opcional, crear un wrapper en Python.
  4. También opcionalmente, escribir una función que calcule el gradiente de esa op.
  5. Probar dicha op.

Grafos

Un grafo está formado por un conjunto de objetos:

  • tf.Operation: representan las unidades de cómputo
  • tf.Tensor: se corresponden con las estructuras de datos que fluyen entre operaciones almacenando sus entradas y salidas.

Los programas de Tensorflow que no emplean el modo Eager Execution (en esta serie de artículos veremos en qué consiste) se componen de 2 fases diferenciadas:

Primera fase: construcción del grafo

La primera de ellas es la construcción del grafo de operaciones. Automáticamente, Tensorflow crea un grafo por defecto. Cada vez que se declara una operación, será añadida también de forma automática al grafo por defecto (con la excepción, de la que hablaremos a continuación, de que otro grafo se haya fijado momentáneamente como el grafo por defecto).

Si se define la operación “resultado” y se consulta si pertenece al grafo por defecto, la respuesta será “True”.Programando con la API de Bajo Nivel de Tensorflow

Pero también existe la posibilidad de crear otros grafos mediante la instrucción tf.Graph(). Además, como se comentó anteriormente, se puede establecer temporalmente uno de esos grafos (“g”) como el grafo por defecto mediante un bloque “with”. Si definimos operaciones dentro del ámbito de dicho bloque “with”, esas operaciones serán añadidas al grafo “g”.

En la siguiente captura, se ha creado un grafo “g” y se ha establecido mediante un bloque “with” como el grafo por defecto. Además se ha declarado una operación “resultado2” que será añadida al grafo “g”. Cuando se consulte si “resultado2” pertenece al grafo por defecto se obtendrá un “False”. Pero cuando se compruebe si pertenece al grafo “g” la respuesta será “True”.

Programando con la API de Bajo Nivel de Tensorflow

Un aspecto fundamental del funcionamiento de Tensorflow es que durante la fase de construcción del grafo ninguna operación es evaluada. Solo se añaden dichas operaciones al grafo o grafos. Por lo que si se intenta obtener el valor de la op “resultado” lo único que se obtendrá es la estructura de ese tensor, como puede observarse en la siguiente imagen:

Programando con la API de Bajo Nivel de Tensorflow

Segunda fase: ejecución de operaciones del grafo

La segunda de las fases de las que se compone un programa de Tensorflow es la de ejecución de las operaciones del grafo. Para llevar a cabo esta etapa será necesario crear un objeto tf.Session().

Como AIDA reclama nuestra atención, en los próximos artículos de esta serie continuaremos mostrando la forma de crear tanto sesiones como el resto de los componentes de lo que se puede considerar el Core de Tensorflow. ¡Os esperamos por aquí!

Continúa leyendo sobre la API de Python de Bajo nivel de Tensorflow

 Más entradas del diario


Rubén Martínez, Data engineer en datahack

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *