PROGRAMANDO CON LA API DE BAJO NIVEL DE TENSORFLOW (II)

Ya estamos aqu铆 para continuar el relato donde lo dejamos en el art铆culo anterior de esta serie. La trama qued贸 en suspense despu茅s de mostrar que un programa de Tensorflow se divide en 2 fases. La primera consiste en la construcci贸n del grafo que alberga por un lado las operaciones a realizar y por otro los tensores que contienen los valores que alimentan a dichas operaciones. La segunda etapa se basa en la creaci贸n de un objeto de sesi贸n que permitir谩 ejecutar el grafo sobre dispositivos locales o remotos. Sigamos con este recorrido por los principales componentes del Core de Tensorflow.

Sesi贸n

La clase tf.Session permite conectar un programa cliente, que act煤a como frontenden alguno de los lenguajes disponibles como Python, gracias al 鈥淭ensorflow Distributed Execution Engine鈥 de C++. Un objeto de esta clase tf.Session proporciona el entorno para ejecutar objetos tf.Operation y evaluar objetos tf.Tensor. Esto lo hace tanto en dispositivos (como CPUs, GPUs, TPUs) de la m谩quina local como de m谩quinas remotas. Tambi茅n proporciona el entorno para cachear el grafo que tenga asociada dicha sesi贸n.

Como se ha comentado, en DIA4RA vamos a emplear la API de alto nivel Estimator que, entre otros beneficios, crear谩 y gestionar谩 el objeto tf.Session por nosotros. Adem谩s, nos permitir谩 abstraernos de escribir el c贸digo de bajo nivel necesario para poder realizar un entrenamiento distribuido entre m煤ltiples m谩quinas. Por ello, aunque no vayamos a manejar sesiones de forma expl铆cita en nuestro proyecto, vamos a mostrar una peque帽a introducci贸n en las siguientes l铆neas.

El constructor de esta clase recibe de forma opcional 3 argumentos:

  • target: El valor por defecto de este argumento provoca que la sesi贸n solo emplear谩 dispositivos de la m谩quina local. Sin embargo, se puede especificar una URL 鈥済rpc://鈥 para indicar la direcci贸n de un objeto train.Server que contiene un conjunto de dispositivos locales y de conexiones a otros objetos tf.train.Server que pueden ser usados para realizar c谩lculos distribuidos.
  • graph: Si no se especifica ning煤n valor en este par谩metro, la sesi贸n se asociar谩 al grafo actual por defecto. En caso de que se est茅n empleando varios grafos, se puede indicar mediante este par谩metro a cu谩l de esos grafos se desea emparejar la sesi贸n.
  • config: Permite indicar un objeto ConfigProto聽que sirve para establecer el comportamiento de la sesi贸n.

Una pr谩ctica habitual a la hora de crear una sesi贸n es la de hacerlo mediante un bloque 鈥渨ith鈥 de Python. De esta forma, la sesi贸n estar谩 activa dentro del 谩mbito de dicho bloque y al salir se cerrar谩 autom谩ticamente y se liberar谩n sus recursos. En caso de no emplearse un bloque 鈥渨ith鈥 ser谩 necesario cerrar expl铆citamente esa sesi贸n mediante tf.Session.close para que se liberen sus recursos.

Para realizar la ejecuci贸n propiamente dicha se emplea el m茅todo tf.Session.run聽que recibe una lista con los tf.Operation o/y tf.Tensor que se pretenden ejecutar y que deben estar presentes en el grafo asociado a dicha sesi贸n.

En la siguiente captura de pantalla se muestra la creaci贸n del grafo por defecto que contendr谩 la operaci贸n creada mediante tf.add que trabajar谩 sobre los arrays de numpy 鈥渁鈥 y 鈥渂鈥 (cuyo valor se obtiene inmediatamente al no ser tensores de Tensorflow). A continuaci贸n, se genera mediante un bloque 鈥渨ith鈥 la sesi贸n 鈥渟ess鈥 y dentro de su 谩mbito se ejecuta empleando sess.run la operaci贸n de suma. Al finalizar el 谩mbito del bloque 鈥渨ith鈥 se cerrar谩 autom谩ticamente la sesi贸n.

Programando con la API de Bajo Nivel de Tensorflow

A continuaci贸n, se puede ver otra imagen en la que se ha creado el grafo 鈥済1鈥 al que se le ha a帽adido la operaci贸n creada mediante tf.multiply que trabajar谩 sobre los arrays de numpy 鈥渄鈥 y 鈥渆鈥. Una vez hecho eso, se crea la sesi贸n 鈥渟ess1鈥 que operar谩 sobre el contenido del grafo 鈥済1鈥 煤nicamente. Mediante sess1.run se ejecuta la multiplicaci贸n presente en 鈥済1鈥 y como la sesi贸n no se ha creado mediante un bloque 鈥渨ith鈥, como buena pr谩ctica cuando ya no se va a emplear m谩s dicha sesi贸n, se cierra utilizando sess1.close.

Programando con la API de Bajo Nivel de Tensorflow

En estos ejemplos se han utilizado sesiones que trabajan sobre recursos presentes en la m谩quina local. La API de Bajo Nivel de Tensorflow permite que la sesi贸n pueda acceder a dispositivos presentes en otras m谩quinas que trabajen conjuntamente formando un cl煤ster permitiendo un c谩lculo distribuido. En DIA4RA emplearemos la abstracci贸n de ejecuci贸n distribuida que ofrece la Estimator API para realizar entrenamientos en paralelo entre m煤ltiples m谩quinas presentes en el Cloud.

Los cl煤ster distribuidos de Tensorflow

Un cl煤ster distribuido de Tensorflow estar谩 formado por uno o m谩s jobs, de forma que cada job puede contener una o m谩s tasks. Las tasks聽de un job colaboran en una ejecuci贸n distribuida de un grafo. Normalmente cada task se ejecuta en una m谩quina diferente pero tambi茅n es posible ejecutar varias tasks en la misma m谩quina, por ejemplo, empleando un dispositivo diferente presente en dicha m谩quina (si dispone de varias GPUs). Cada tarea estar谩 asociada a un tf.train.Server聽que estar谩 formado por un master para crear sesiones y por un worker para ejecutar operaciones del grafo.

Una t茅cnica muy utilizada cuando se realiza un entrenamiento distribuido consiste en emplear procesos que actuar谩n como Parameter Servers. Las variables ser谩n repartidas entre los parameter servers disponibles en funci贸n de su tama帽o para balancear la carga.聽 Cuando un proceso worker necesite un variable almacenada en un parameter server, har谩 referencia directamente a ella. Cuando un worker calcule un gradiente, ser谩 enviado al parameter server que almacene esa variable concreta.

Para crear un cl煤ster ser谩 necesario por un lado crear un objeto de la clase tf.train.ClusterSpec y uno o varios tf.train.Server.

La clase tf.train.ClusterSpec permite indicar todas las tasks del cl煤ster. Su constructor recibir谩 un diccionario mapeando cada nombre de job a una lista de direcciones de red de las tasks de ese job o a otro diccionario en el que cada clave ser谩 un 铆ndice de task y su correspondiente valor ser谩 la direcci贸n de red de dicha task.

El siguiente fragmento de c贸digo crea la configuraci贸n de un cl煤ster formado 2 workers y 1 parameter server.

cluster = tf.train.ClusterSpec({鈥渨orker鈥: [鈥192.168.1.120:3333鈥, 鈥192.168.1.121:3335鈥漖,
                                            鈥減s鈥: [鈥192.168.1.122:3335鈥漖
                                            })

A continuaci贸n, lo que habr谩 que hacer es instanciar聽 los servers correspondientes a los 2 workers y al parameter server.

server0 = tf.train.Server(cluster, job_name=鈥渨orker鈥, task_index=0)
server1 = tf.train.Server(cluster, job_name=鈥渨orker鈥, task_index=1)
serverPs = tf.train.Server(cluster, job_name=鈥減s鈥, task_index=0)

El siguiente paso ser谩 emplear la funci贸n tf.device para especificar en qu茅 proceso se colocar谩 cada operaci贸n:

with tf.device(鈥/job:ps/task:0鈥):
    var0 = tf.Variable( 鈥 )
    var1 = tf.Variable( 鈥 )
    var2 = tf.Variable( 鈥 )
with tf.device(鈥/job:worker/task:0鈥):
    res0 = tf.matmul(var0, var1)  
with tf.device(鈥/job:worker/task:0鈥):
    res1 = tf.matmul(var1, var2)

En este punto, se crear谩n las sesiones necesarias y cada una de ellas se asociar谩 con un server (2 workers y 1 parameter server).

sessPs = tf.Session(target=serverPs.target)
sess0 = tf.Session(target=server0.target)
sess1 = tf.Session(target=server1.target)

Dichas sesiones se emplear谩n para ejecutar las operaciones asociadas a esos servers (tasks). En un futuro art铆culo se presentar谩 de forma pr谩ctica un entrenamiento distribuido empleando la API de Bajo Nivel de Tensorflow. Pero antes, vamos a continuar con el recorrido por sus componentes b谩sicos.

Asignaci贸n de dispositivos mediante tf.device()

En Tensorflow los dispositivos disponibles para ejecutar operaciones son CPUs y GPUs. La notaci贸n que se emplea para referenciarlos es la siguiente:

  • 鈥/cpu:0鈥: Se corresponde con la CPU de la m谩quina.
  • 鈥/gpu:0鈥: Es la primera GPU de la m谩quina.
  • 鈥/gpu:1鈥: Indica la segunda GPU de la m谩quina en caso de que exista.

La secuencia seguir铆a aument谩ndose el 铆ndice de la GPU si existiesen m谩s de estos dispositivos. Por defecto, si una operaci贸n est谩 implementada para poder ejecutarse tanto en CPU como en GPU, los dispositivos GPU tendr谩n prioridad a la hora de recibir esa operaci贸n.

En primer lugar, para visualizar los dispositivos disponibles en la m谩quina se podr谩 ejecutar el siguiente c贸digo:

from tensorflow.python.client import device_lib
device_lib.list_local_devices()

Programando con la API de Bajo Nivel de Tensorflow

Si se desea colocar una o varias operaciones en un dispositivo en concreto la forma de hacerlo ser谩 empleando un bloque 鈥渨ith鈥 con la funci贸n tf.device() que recibir谩 el nombre de dicho dispositivo.

with tf.device(鈥/gpu:0鈥):
    a = tf.constant([2, 7, 3])

Constantes

Se trata de un tensor que tiene un valor constante que se crea mediante la funci贸n tf.constant() y dispone de los siguientes argumentos:

  • value: Un valor o lista de valores constantes de tipo dtype.
  • dtype: Indica el tipo de los elementos del tensor.
  • shape: Indica de forma opcional el n煤mero de elementos de cada dimensi贸n del tensor con valor constante.
  • name: Este par谩metro permite establecer de forma opcional un nombre personalizado para el tensor.
  • verify_shape: Par谩metro booleano que permite la verificaci贸n de un shape de valores.

Programando con la API de Bajo Nivel de Tensorflow

Variables

Una variable de Tensorflow permite almacenar un tensor cuyo valor puede ser cambiado ejecutando una serie de operaciones sobre ella. Esas modificaciones son visibles desde m煤ltiples objetos tf.Session lo que permite que diferentes workers puedan acceder al valor de una misma variable.

A bajo nivel la clase que se encarga de su implementaci贸n se trata de tf.Variable. En cuanto a la creaci贸n de variables la forma recomendada es mediante la funci贸n tf.get_variable, que recibe el nombre de dicha variable y su shape entre otros argumentos opcionales como su valor inicial.

var1 = tf.get_variable(鈥渢est鈥, [2, 1, 4], dtype=tf.int64)

Ese comando crear谩 una variable llamada 鈥渢est鈥 de 3 dimensiones y shape [2, 1, 4], cuyos valores ser谩n de tipo tf.int64 (en caso de no indicarse el par谩metro dtype, el tipo por defecto ser谩 tf.float32) y su valor inicial se establece por defecto a partir a partir de la distribuci贸n aleatoria determinada por tf.glorot_uniform_initializer.

Esta funci贸n (tambi茅n llamada inicializaci贸n Xavier) generar谩 muestras aleatorias entre [-a, a], donde:

  • a = sqrt(6 / (fan_in + fan_out))
  • fan_in = n煤mero de unidades de entrada en el tensor de pesos
  • fan_out = n煤mero de unidades de salida en el tensor de pesos.

Si se desea indicar un initializer en concreto se puede hacer de la siguiente forma:

var2 = tf.get_variable(鈥渢est2鈥, [5, 2, 4, 1], dtype=tf.int32, initializer=tf.zeros_initializer)

En caso de que el initializer sea un tf.Tensor entonces no habr谩 que indicar el shape en el constructor de tf.get_variable ya que lo inferir谩 a partir del shape de ese initializer ([2, 3]):

var4 = tf.get_variable(鈥渢est4鈥, initializer=tf.constant([2,3]))

Las 鈥collections鈥 de Tensorflow son listas con nombre de tensores u otros objetos como variables. Cuando se crea una variable se a帽adir谩 por defecto en las colecciones tf.GraphKeys.TRAINABLE_VARIABLES (son las variables para las que Tensorflow calcular谩 sus gradientes) y tf.GraphKeys.GLOBAL_VARIABLES (variables que pueden ser compartidas entre m煤ltiples dispositivos).

Para hacer que una variable no sea tenida en cuenta a la hora de calcular los gradientes habr铆a que pasar el par谩metro 鈥渢rainable=False鈥 a la hora de su creaci贸n.

var5 = tf.get_variable(鈥渢est5鈥, [4, 2], trainable=False)

Se pueden crear collections personalizadas utilizando cualquier cadena como nombre. Para a帽adir un objeto ya creado a una colecci贸n se emplear谩 el m茅todo tf.add_to_collection.

tf.add_to_collection(鈥渃ol1鈥, var4)

La forma de conocer todos los objetos de una collection es mediante la siguiente funci贸n:

tf.get_collection(鈥渃ol1鈥)

Hasta este punto hemos visto c贸mo crear variables y c贸mo unirlas a collections. Pero antes de poder utilizar una variable hay que a帽adir una operaci贸n al grafo para que la inicialice expl铆citamente, lo que permite evitar que se vuelvan a ejecutar initializers que pueden consumir un tiempo importante, por ejemplo, al volver a cargar un modelo desde un 鈥渃heckpoint鈥.

La funci贸n que emplearemos para inicializar una 煤nica variable ser谩:

sess = tf.Session()
sess.run(var1.initializer)

Pero como ir inicializando variable por variable puede ser muy tedioso, hay una funci贸n que viene a nuestro rescate, tf.global_variables_initializer(). Se encarga de a帽adir una 煤nica operaci贸n al grafo que, cuando sea ejecutada, inicializar谩 todas las variables presentes en la collection聽 tf.GraphKeys.GLOBAL_VARIABLES.

init = tf.global_variables_initializer()
sess1 = tf.Session()
sess1.run(init)

Esta funci贸n no asegura el orden en que ser谩n inicializadas las variables. De forma que si hay variables que dependen de otras y no estamos seguros de si ya han sido inicializadas, una forma de programar de forma segura es empleando en la variable dependiente la funci贸n var_de_la_que_se_depende.initialized_value().

a = tf.get_variable(鈥渁鈥, [1,2], initializer=tf.zeros_initializer())
b = tf.get_varible(鈥渂鈥, initializer=a.initialized_value() * 2)

Si se quiere asignar un nuevo valor a una variable se podr谩n emplear la familia de funciones assign, assign_add.

sum = a.assign(a + 7.0)
sum2 = a.assign_add(8.0)

La instrucci贸n tf.get_variable puede encontrarse en un bloque de c贸digo (una funci贸n) que puede ser ejecutado repetidas veces. No quedar铆a claro si lo que se pretende es volver a crear la misma variable o reutilizarla. Una forma de solucionar esto es creando una variable dentro de un bloque 鈥渨ith鈥 con tf.variable_scope (eso a帽adir谩 el nombre del variable_scope al principio del nombre de la variable) y cuando se quiera reutilizar dicha variable, la instrucci贸n tf.get_variable se incluir谩 en el interior de聽 un tf.variable_scope con el mismo nombre que cuando se cre贸 pero que adem谩s recibir谩 el par谩metro “reuse=True”.

En caso de no utilizar el flag “reuse=True”, si la variable ya hab铆a sido previamente creada mediante una llamada a get_variable, al volver a intentar crearla se obtendr谩 una excepci贸n o si no se cre贸 mediante una llamada a get_variable. Esto permite evitar reutilizar variables por error. Una vez establecido reuse a True ya no se puede poner a False dentro del bloque. Tambi茅n se puede fijar el flag reuse a True dentro del bloque llamando a scope.reuse_variables().

A continuaci贸n, vamos a ver un ejemplo de utilizaci贸n de la funci贸n get_variable para crear una variable compartida si no existe o reutilizarla en caso de que exista. Esto se controla dentro del scope “relu”. Si se definen otros variable scopes dentro del actual, heredar谩n el flag “reuse=True”.

Las variables creadas usando get_variable siempre se nombran utilizando el nombre de su variable scope como prefijo. Por ejemplo, relu/threshold. Si se crea un scope con un nombre que ya hab铆a sido utilizado por otro scope, se a帽adir谩 un sufijo 铆ndice” para que el nombre sea 煤nico.

Se definir谩 una funci贸n “relu()” que reutilizar谩 una variable relu/threashold. Dicha variable ser谩 creada fuera de la funci贸n inicializ谩ndola a 0.0. A continuaci贸n se construir谩n 5 relus llamando 5 veces a la funci贸n聽“relu()”.

tf.reset_default_graph()

def relu(X):
    with tf.variable_scope("relu", reuse=True): # Reutilizaci贸n de la variable threshold
        threshold = tf.get_variable("threshold", shape=(), initializer=tf.constant_initializer(0.0))
        w_shape = int(X.get_shape()[1]), 1
        w = tf.Variable(tf.random_normal(w_shape), name="weights")
        b = tf.Variable(0.0, name="bias")
        linear = tf.add(tf.matmul(X, w), b, name="linear")
        return tf.maximum(linear, threshold, name="max")

X = tf.placeholder(tf.float32, shape=(None, n_features), name="X")
with tf.variable_scope("relu"): # Creaci贸n de la variable threshold
    threshold = tf.get_variable("threshold", shape=(), initializer=tf.constant_initializer(0.0))
relus = [relu(X) for i in range(5)]
output = tf.add_n(relus, name="output")

Placeholders

Un tf.placeholder es una estructura que almacenar谩 datos pero dichos datos ser谩n cargados en tiempo de ejecuci贸n. Por tanto, nos permitir谩 definir un almac茅n de informaci贸n que podr谩 ser a帽adido al grafo sin tener que alimentarlo hasta que se cree una sesi贸n y se desee ejecutar operaciones que dependan de este placeholder.

Para ello, el m茅todo sess.run deber谩 recibir como par谩metro adem谩s de las operaciones a ejecutar, un argumento llamado feed_dict que ser谩 un diccionario donde cada clave ser谩 el nombre de un placeholder y su correspondiente valor ser谩 justamente el valor que queramos que reciba esa estructura.

import tensorflow as tf

a = tf.placeholder(鈥渇loat鈥, None)
b = a + 7

with tf.Session() as sess:
    r = sess.run(b, feed_dict={a:[6,3]})

Como se ha podido apreciar, no es necesario definir est谩ticamente un tama帽o para los datos que albergar谩 un tf.placeholder. Tambi茅n puede tener cualquier n煤mero de dimensiones y cuando alguna de sus dimensiones sea 鈥None鈥, significa que puede tener cualquier n煤mero de elementos en ella.

import tensorflow as tf

a = tf.placeholder(鈥渇loat鈥, [None, 2])
b = a + 7

with tf.Session() as sess:
    r = sess.run(b, feed_dict={a:[[1, 4], [3, 5], [7, 9]]})

Hasta aqu铆 este breve repaso a los componentes b谩sicos del Core de Tensorflow. En pr贸ximos art铆culos continuaremos viendo APIs de m谩s alto nivel de este potente framework de Google.

dia4ra cdtiEl proyecto empresarial de DATAHACK CONSULTING SL., denominado 鈥淒ESARROLLO DE INTELIGENCIA ARTIFICIAL EN ROBOTS APLICADOS AL TRATAMIENTO DEL ALZHEIMER Y LA DEMENCIA鈥 y n煤mero de expediente聽00104725 / SNEO-20171211 ha sido subvencionado por聽el CENTRO PARA EL DESARROLLO TECNOL脫GICO INDUSTRIAL (CDTI)

M脕STER EXPERTO EN BIG DATA & ANALYTICS

Gracias al Master en Big Data Analytics 100% Online tendr谩s amplios conocimientos sobre las herramientas y t茅cnicas anal铆ticas necesarias para la modelizaci贸n de los principales retos de negocio, con el fin de mejorar la toma de decisiones a trav茅s de los datos y el conocimiento.

Deja un comentario

Datahack logo