Tensorflow es una librería de código abierto para el cálculo numérico basado en grafos, el cual fue desarrollado por el equipo de Google Brain. Tensorflow puede ser utilizado para efectuar, por ejemplo, sumas, multiplicaciones y diferenciaciones. Además, puede ser utilizado para el entrenamiento de modelos de Machine Learning.
Así, ésta puede ser utilizada para entrenar redes neuronales, decifrar y detectar correlaciones, entre otros.
Comenzaremos por indagar en el concepto de tensor. La idea de tensor es la generalización de vectores y matrices a dimensiones potencialmente altas. Por ejemplo, los números escalares son tensores de dimensión cero, los vectores son tensores de dimensión uno; las matrices son tensores de dos dimensiones; si consideramos la idea de una matriz tridimensional
tenemos que éstas son tensores de tres dimensiones. Y así sucesivamente, podemos escalar en el número de dimensiones.
Podemos definir tensores mediante tensorflow como sigue:
# Importacion necesaria
import tensorflow as tf
# Definimos un tensor de dimension 0 con solo la entrada numerica de 1
tensor_0 = tf.ones((1,))
print(tensor_0)
tf.Tensor([1.], shape=(1,), dtype=float32)
lo cual nos muestra el escalar [1.]
, la forma shape=(1,)
(lo cual es muy similar a la forma que se obtiene de los array NumPy) y obtenemos el tipo de dato de la entrada numérica, el cual es float32
.
Continuando
# Definimos un tensor de dimension 1 con solo la entrada numerica de 1
# El tensor tendra cuatro entradas
tensor_1 = tf.ones((4,))
print(tensor_1)
tf.Tensor([1. 1. 1. 1.], shape=(4,), dtype=float32)
el cual vemos que es un vector. Definimos una matriz de 3 filas por 4 columnas, es decir, definimos
# Un tensor de dimension 2 con solo la entrada numerica de 1
tensor_2 = tf.ones((3,4))
print(tensor_2)
tf.Tensor( [[1. 1. 1. 1.] [1. 1. 1. 1.] [1. 1. 1. 1.]], shape=(3, 4), dtype=float32)
# Definimos un tensor de dimension 3 con solo la entrada numerica de 1
# Seran tres matrices de 4 filas por 5 columnas
tensor_3 = tf.ones((3,4,5))
print(tensor_3)
tf.Tensor( [[[1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.]] [[1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.]] [[1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.]]], shape=(3, 4, 5), dtype=float32)
El cual representa la idea de una matriz tridimensional. Podemos imprimir el tensor de manera directa sin ver la demás información utilizando el método numpy()
como sigue
print(tensor_3.numpy())
[[[1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.]] [[1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.]] [[1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.]]]
Por otro lado, indagaremos en la categoría más simple de tensor en tensorflow: las constantes. Las constantes no cambian y no pueden ser entrenadas, sin embargo puede tener cualquier dimensión.
# Definimos una constante cuyas entradas son todas iguales a 3
# y que es de la forma: dos filas por tres columnas
a = tf.constant(3, shape=[2,3])
print(a)
print('*' * 50)
print(a.numpy())
tf.Tensor( [[3 3 3] [3 3 3]], shape=(2, 3), dtype=int32) ************************************************** [[3 3 3] [3 3 3]]
# Definimos una costante, un escalar o tensor de dimension 0
# a partir de una lista
a2 = tf.constant([1])
print(a2)
tf.Tensor([1], shape=(1,), dtype=int32)
Podemos definir una constante cuyas entradas sean distinta y no solo el mismo número. Por ejemplo
# Tensor cuyas entradas seran el 1,2,3,4 acomodados en una matriz
# de dos por dos
b = tf.constant([1, 2, 3, 4], shape=[2,2])
print(b)
print('*' * 50)
print(b.numpy())
tf.Tensor( [[1 2] [3 4]], shape=(2, 2), dtype=int32) ************************************************** [[1 2] [3 4]]
Recordemos que tf.ones
crea tensores cuyas entradas son todas iguales a 1. De manera similar tenemos:
tf.zeros
: Crea tensores cuyas entradas son todas iguales a cero.tf.fill
: Crear tensores a partir de cierta forma, y rellena todas las entradas con un número dado. Por ejemploc1 = tf.fill([3,3], 7)
c1.numpy()
array([[7, 7, 7], [7, 7, 7], [7, 7, 7]])
creamos una matriz de tres por tres (un tensor de dimensión dos) cuyas entradas son todas iguales a 7.
tf.zeros_like
: Copia la forma de un tensor dado. Las entradas que generará son todas iguales a cero# Copiamos la forma del tensor c1 pero cuyas entradas seran todas iguales
# a cero
c2 = tf.zeros_like(c1)
c2.numpy()
array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
tf.ones_like
: Análogo a tf.zeros_like()
.A diferencia de una constante, una variable puede cambiar durante el cálculo. El valor de una variable es compartido, persistente y modificable, sin embargo, su tipo de datos y forma son fijos. Construyamos una variable en tensorflow:
# Definimos dos variables
x0 = tf.Variable([1, 2, 3, 4, 5, 6], dtype=tf.float32)
x1 = tf.Variable([1, 2, 3, 4, 5, 6], dtype=tf.int16)
print(x0)
print(x1)
print('*' * 50)
print(x0.numpy())
print(x1.numpy())
<tf.Variable 'Variable:0' shape=(6,) dtype=float32, numpy=array([1., 2., 3., 4., 5., 6.], dtype=float32)> <tf.Variable 'Variable:0' shape=(6,) dtype=int16, numpy=array([1, 2, 3, 4, 5, 6], dtype=int16)> ************************************************** [1. 2. 3. 4. 5. 6.] [1 2 3 4 5 6]
las cuales hemos inicializado con el array [1, 2, 3, 4, 5, 6]
(tensor de dimensión 1) y que hemos asignado el tipo de dato de flotante de 32 bits para x0
, y de entero de 16 bits para x1
.
Definamos ahora la siguiente constante, la cual será un escalar (tensor de dimensión cero)
b = tf.constant(2, tf.float32)
b
<tf.Tensor: shape=(), dtype=float32, numpy=2.0>
Podemos multiplicar constantes por variables
# Manera 1:
c0 = tf.multiply(x0, b)
print(c0)
print('*' * 70)
# Manera 2:
c1 = x0 * b
print(c1)
tf.Tensor([ 2. 4. 6. 8. 10. 12.], shape=(6,), dtype=float32) ********************************************************************** tf.Tensor([ 2. 4. 6. 8. 10. 12.], shape=(6,), dtype=float32)
# Podemos tambiem definir variables escalares
x1 = tf.Variable(-1, dtype=tf.int16)
x1
<tf.Variable 'Variable:0' shape=() dtype=int16, numpy=-1>
Hasta ahora podemos notar, intuitivamente, que la manera de operar entre tensores es, a lo sumo, similar a la manera de operar entre los array NumPy.
El funcionamiento de tensorflow gira en torno a los grafos, donde los bordes son tensores y los nodos operaciones. Por ejemplo
en los nodos tenemos dos tensores constantes los cuales se suman y resultan, digamos, en una matriz. Luego
Las matrices resultantes de las sumas de las constantes después son multiplicadas mediante la operación de multiplicación matricial.
Por ejemplo
# Definimos dos constantes
a0 = tf.constant([1])
b0 = tf.constant([2])
# Sumamos los dos tensores de dimension 0 anteriores
# mediante tf.add()
print(tf.add(a0, b0))
tf.Tensor([3], shape=(1,), dtype=int32)
en lo cual se está sumando entrada a entrada. Otro ejemplo
# Definimos dos tensores de dimension 2
a1 = tf.constant([[1, 2],
[3, 4]])
b1 = tf.constant([[5, 6],
[7, 8]])
# Sumamos los tensores anteriores
print(tf.add(a1, b1))
tf.Tensor( [[ 6 8] [10 12]], shape=(2, 2), dtype=int32)
# Alternativa
print(a1 + b1)
tf.Tensor( [[ 6 8] [10 12]], shape=(2, 2), dtype=int32)
# Podemos efectuar la multiplicacion entrada a entrada
print(a1 * b1)
tf.Tensor( [[ 5 12] [21 32]], shape=(2, 2), dtype=int32)
Con base en lo anterior podemos decir que las operaciones entre tensores solo pueden llevarse a cabo si los tensores involucrados son de la misma forma.
Podemos también efectuar la multiplicación matricial, para lo cual utilizaremos matmul(A,B)
, donde el número de columnas de A
debe ser igual al número de filas de B
para la compatibilidad. Por ejemplo:
# Multiplicacion matricial
print(tf.matmul(a1, b1))
tf.Tensor( [[19 22] [43 50]], shape=(2, 2), dtype=int32)
Alternativamente podemos escribir a1 @ b1
para efectuar la multiplicación matricial.
reduce_sum(A)
) o solo una en particular (reduce_sum(A, i)
). Por ejemplo# Definimos un tensor de tres dimensiones cuyas entradas
# son todas iguales a uno, cuya forma es:
# dos matrices de tres filas por cuatro columnas
A = tf.ones((2,3,4))
A.numpy()
array([[[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]], [[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]]], dtype=float32)
Así, podemos sumar todas las entradas de la dimensión 0, correspondiente a las matrices, esto es, sumar sobre la dimensión 0 al tensor anterior es realizar la suma entrada a entrada de las dos matrices involucradas:
# Sumamos todos los elementos de A sobre la dimension 0
tf.reduce_sum(A, 0)
<tf.Tensor: shape=(3, 4), dtype=float32, numpy= array([[2., 2., 2., 2.], [2., 2., 2., 2.], [2., 2., 2., 2.]], dtype=float32)>
# Sumamos todos los elementos de A sobre la dimension 1
tf.reduce_sum(A, 1)
<tf.Tensor: shape=(2, 4), dtype=float32, numpy= array([[3., 3., 3., 3.], [3., 3., 3., 3.]], dtype=float32)>
Donde la suma fue efectuada por columnas para cada matriz. Es decir, de
array([[3., 3., 3., 3.],
[3., 3., 3., 3.]])
la primer componente [3., 3., 3., 3.]
representa las sumas de las entradas, de la primer matriz del tensor, mediante columnas; la segunda componente [3., 3., 3., 3.]
representa las sumas de las entradas por columnas de la segunda matriz. Finalmente, la suma sobre la dimensión 2, la cual corresponde a las filas, nos arroja
tf.reduce_sum(A, 2)
<tf.Tensor: shape=(2, 3), dtype=float32, numpy= array([[4., 4., 4.], [4., 4., 4.]], dtype=float32)>
la primer componente [4., 4., 4.
representa las sumas de las entradas, de la primer matriz del tensor, mediante filas. Podemos también sumar absolutamente todas las entrada del tensor de manera "recorrida":
tf.reduce_sum(A)
<tf.Tensor: shape=(), dtype=float32, numpy=24.0>
gradient
: Computa la pendiente de una función en un punto.reshape()
: Cambia la forma de un tensor.random()
: Genera un tensor aleatorio a partir de valores extraídos al azar de alguna distribución de probabilidad en específico.Notamos entonces que, en particular, gradient()
será de utilidad para el algoritmo del descenso de gradiente, el cual vimos en clases pasadas.
Utilicemos tensorflow para calcular el gradiente. Comenzamos por definir una variable inicializada en -1.0:
x = tf.Variable(-1.0)
x
<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=-1.0>
Lo cual es diferente a la variable
# Alternativamente
x_alter = tf.Variable([-1.0])
x_alter
<tf.Variable 'Variable:0' shape=(1,) dtype=float32, numpy=array([-1.], dtype=float32)>
Para poder realizar el cálculo del gradiente debemos abordar el siguiente subtema:
TensorFlow proporciona la API tf.GradientTape
para la diferenciación automática; es decir, calcular el gradiente de un cálculo con respecto a algunas entradas, generalmente variables del tipo tf.Variable
. TensorFlow "graba" operaciones relevantes ejecutadas dentro del contexto de un tf.GradientTape
en una "cinta". Luego, TensorFlow usa esa cinta para calcular los gradientes de un cálculo "grabado" mediante la diferenciación de modo inverso .
Aquí hay un ejemplo simple:
# Definimos una variable inicializada en 3.0
x = tf.Variable(3.0)
# Contexto en el cual se grabaran las operaciones
# relevantes para el calculo del gradiente
# en una "cinta"
with tf.GradientTape() as tape:
y = x ** 2
Una vez que haya registrado algunas operaciones, usamos GradientTape.gradient(target, sources)
para calcular el gradiente de algún objetivo en relación con alguna variable:
# dy = 2x * dx
# Evaluaremos el gradiente obtenido en el valor de la variable x
dy_dx = tape.gradient(y, x)
dy_dx.numpy()
6.0
En lo cual calculamos para $f(x)=x^{2}$:
$$ f'(3.0)=2\cdot (3.0)=6.0 $$De manera conjunta podemos escribir
# Contexto en el cual se grabaran las operaciones
# relevantes para el calculo del gradiente
# en una "cinta"
with tf.GradientTape() as tape:
# Funcion a la cual queremos calcular el gradiente
y = x ** 2
# Calculamos el gradiente de y evaluado en
# el valor de la tf.variable x
dy_dx = tape.gradient(y, x)
# veamos el resultado
dy_dx.numpy()
6.0
El ejemplo anterior usa escalares, pero tf.GradientTape
funciona con la misma facilidad en cualquier tensor.
Posteriormente, recordemos que reduce_sum(A)
realiza la suma de todas las entradas del tensor A
. De forma análoga, reduce_mean(A)
calcula el promedio de todos los elementos del tensor A
, por ejemplo