En el notebook de autograd definimos diferentes operaciones comunes junto con el calculo de sus gradientes, en este notebook vemos de donde salen esos calculos de gradientes.
Obs: La notacion matemática para representar operaciones entre tensores puede no estar correcta. Otra gran ayuda para saber los signos en latex es Detexify
import numpy as np
class Tensor(object):
def __init__(self, data,
autograd=False,
creators=None,
creation_op=None,
id=None):
'''
Inicializa un tensor utilizando numpy
@data: una lista de numeros
@creators: lista de tensores que participarion en la creacion de un nuevo tensor
@creators_op: la operacion utilizada para combinar los tensores en el nuevo tensor
'''
self.data = np.array(data)
self.creation_op = creation_op
self.creators = creators
self.grad = None
self.autograd = autograd
self.children = {}
# se asigna un id al tensor
if (id is None):
id = np.random.randint(0, 100000)
self.id = id
# se hace un seguimiento de cuantos hijos tiene un tensor
# si los creadores no es none
if (creators is not None):
# para cada tensor padre
for c in creators:
# se verifica si el tensor padre posee el id del tensor hijo
# en caso de no estar, agrega el id del tensor hijo al tensor padre
if (self.id not in c.children):
c.children[self.id] = 1
# si el tensor ya se encuentra entre los hijos del padre
# y vuelve a aparece, se incrementa en uno
# la cantidad de apariciones del tensor hijo
else:
c.children[self.id] += 1
def all_children_grads_accounted_for(self):
'''
Verifica si un tensor ha recibido la cantidad
correcta de gradientes por cada uno de sus hijos
'''
# print('tensor id:', self.id)
for id, cnt in self.children.items():
if (cnt != 0):
return False
return True
def backward(self, grad, grad_origin=None):
'''
Funcion que propaga recursivamente el gradiente a los creators o padres del tensor
@grad: gradiente
@grad_orign
'''
# tab=tab
if (self.autograd):
if (grad_origin is not None):
# Verifica para asegurar si se puede hacer retropropagacion
if (self.children[grad_origin.id] == 0):
raise Exception("No se puede retropropagar mas de una vez")
# o si se está esperando un gradiente, en dicho caso se decrementa
else:
# el contador para ese hijo
self.children[grad_origin.id] -= 1
# acumula el gradiente de multiples hijos
if (self.grad is None):
self.grad = grad
else:
self.grad += grad
if (self.creators is not None and
(self.all_children_grads_accounted_for() or grad_origin is None)):
if (self.creation_op == 'neg'):
self.creators[0].backward(self.grad.__neg__())
if (self.creation_op == 'add'):
# al recibir self.grad, empieza a realizar backprop
self.creators[0].backward(self.grad, grad_origin=self)
self.creators[1].backward(self.grad, grad_origin=self)
if(self.creation_op == "sub"):
self.creators[0].backward(Tensor(self.grad.data), self)
self.creators[1].backward(Tensor(self.grad.__neg__().data), self)
if(self.creation_op == "mul"):
new = self.grad * self.creators[1]
self.creators[0].backward(new , self)
new = self.grad * self.creators[0]
self.creators[1].backward(new, self)
if(self.creation_op == "mm"):
layer = self.creators[0] # activaciones => layer
weights = self.creators[1] # pesos = weights
# c0 = self.creators[0] # activaciones => layer
# c1 = self.creators[1] # pesos = weights
# new = self.grad.mm(c1.transpose()) # grad = delta => delta x weights.T
new = Tensor.mm(self.grad, weights.transpose()) # grad = delta => delta x weights.T
layer.backward(new)
# c0.backward(new)
# new = self.grad.transpose().mm(c0).transpose() # (delta.T x layer).T = layer.T x delta
new = Tensor.mm(layer.transpose(), self.grad) # layer.T x delta
weights.backward(new)
# c1.backward(new)
if(self.creation_op == "transpose"):
self.creators[0].backward(self.grad.transpose())
if("sum" in self.creation_op):
dim = int(self.creation_op.split("_")[1])
self.creators[0].backward(self.grad.expand(dim, self.creators[0].data.shape[dim]))
if("expand" in self.creation_op):
dim = int(self.creation_op.split("_")[1])
self.creators[0].backward(self.grad.sum(dim))
def __neg__(self):
if(self.autograd):
return Tensor(self.data * -1,
autograd=True,
creators=[self],
creation_op='neg')
return Tensor(self.data * -1)
def __add__(self, other):
'''
@other: un Tensor
'''
if (self.autograd and other.autograd):
return Tensor(self.data + other.data,
autograd=True,
creators=[self, other],
creation_op='add')
return Tensor(self.data + other.data)
def __sub__(self, other):
'''
@other: un Tensor
'''
if (self.autograd and other.autograd):
return Tensor(self.data - other.data,
autograd=True,
creators=[self, other],
creation_op='sub')
return Tensor(self.data - other.data)
def __mul__(self, other):
'''
@other: un Tensor
'''
if(self.autograd and other.autograd):
return Tensor(self.data * other.data,
autograd=True,
creators=[self,other],
creation_op="mul")
return Tensor(self.data * other.data)
def sum(self, dim):
'''
Suma atravez de dimensiones, si tenemos una matriz 2x3 y
aplicamos sum(0) sumara todos los valores de las filas
dando como resultado un vector 1x3, en cambio si se aplica
sum(1) el resultado es un vector 2x1
@dim: dimension para la suma
'''
if(self.autograd):
return Tensor(self.data.sum(dim),
autograd=True,
creators=[self],
creation_op="sum_"+str(dim))
return Tensor(self.data.sum(dim))
def expand(self, dim, copies):
'''
Se utiliza para retropropagar a traves de una suma sum().
Copia datos a lo largo de una dimension
'''
trans_cmd = list(range(0,len(self.data.shape)))
trans_cmd.insert(dim,len(self.data.shape))
new_data = self.data.repeat(copies).reshape(list(self.data.shape) + [copies]).transpose(trans_cmd)
if(self.autograd):
return Tensor(new_data,
autograd=True,
creators=[self],
creation_op="expand_"+str(dim))
return Tensor(new_data)
def transpose(self):
if(self.autograd):
return Tensor(self.data.transpose(),
autograd=True,
creators=[self],
creation_op="transpose")
return Tensor(self.data.transpose())
def mm(self, x):
if(self.autograd):
return Tensor(self.data.dot(x.data),
autograd=True,
creators=[self,x],
creation_op="mm")
return Tensor(self.data.dot(x.data))
def __repr__(self):
return str(self.data.__repr__())
def __str__(self):
return str(self.data.__str__())
def __sub__(self, other):
'''
@other: un Tensor
'''
if (self.autograd and other.autograd):
return Tensor(self.data - other.data,
autograd=True,
creators=[self, other],
creation_op='sub')
return Tensor(self.data - other.data)
def zero_grads(tensor: Tensor):
for c in tensor.creators:
c.grad.data *= 0
x1 = Tensor([3,3,3], autograd=True)
x2 = Tensor([2,2,2], autograd=True)
Resta o sub
¶
Calculamos la resta entre los tensores x1 y x2
y = x1-x2
y
Ahora queremos calcular el gradiente que deberá ser pasado tanto a x1 como a x2 a partir de esta operacion. Sabemos que:
- x1 es el
minuendo
- x2 es el
sustraendo
Esto se puede verificar facilmente mirando quienes fueron los creadores de y
print(y.creators[0].id == x1.id)
print(y.creators[1].id == x2.id)
print(y.grad)
En el caso de una resta, el gradiente que debemos pasar a los creadores del tensor y debe ser:
- Para el minuendo, el gradiente del hijo
- Para el sustraendo, el la negacion ( todo por -1) del gradiente del hijo
y_grad = Tensor(np.ones_like(y.data))
x1.grad = y_grad
x2.grad = y_grad.__neg__()
print('x1:',x1.id,'\n', x1.grad)
print('x2: ',x2.id,'\n', x2.grad)
zero_grads(y)
Implementando esto dentro de la funcion backward()
y.backward(Tensor(np.ones_like(y.data)))
Verificamos que esto se aplique correctamente
print('y.creators[0]: ',y.creators[0].id,'\n', y.creators[0].grad)
print('y.creators[1]: ',y.creators[1].id,'\n', y.creators[1].grad)
Multiplicacion elemento por elemento o mul
¶
y = x1*x2
y
Ahora queremos calcular el gradiente que deberá ser pasado tanto a $x_1$ como a $x_2$ a partir de esta operacion.
Como los gradientes se optienen a partir de derivadas parciales
debemos calcular la derivada parcial de $x_1*x_2$ con respecto a $x_1$ y con respecto a $x_2$ por el gradiente que viene del tensor hijo ($y$) y pasarle este nuevo valor a los tensores padres como gradientes
A modo de ejemplo consideremos solo $x_1$:
$$f(x_1,x_2) = x_1 * x_2$$
Al calcular la derivada parcial con respecto a $x_1$, $x_2$ queda como constante, por tanto:
$${d f(x_1,x_2)\over dx_1} = x_2 $$
A todo esto debemos multiplicarle el gradiente que viene del tensor hijo por lo que queda de la siguiente manera:
Como el gradiente debe ser pasado a ambos tensores, para el caso de $x_2$ quedaría de la siguiente manera: $$f(x_1,x_2) = x_1 * x_2$$ $${d f(x_1,x_2)\over dx_2} = x_1 $$ A todo esto debemos multiplicarle el gradiente que viene del tensor hijo por lo que queda de la siguiente manera: $$grad_x = grad_y * x_1$$
Por tanto, para realizar retropropagación de una multiplicacion elemento a elemento, debemos crear un metodo que pase estos gradientes a los tensores padres
y_grad = Tensor(np.ones_like(y.data))
x1.grad = y_grad * x2
x2.grad = y_grad * x1
print('x1:',x1.id,'\n', x1.grad)
print('x2: ',x2.id,'\n', x2.grad)
zero_grads(y)
Implementando esto dentro de la funcion backward()
y.backward(Tensor(np.ones_like(y.data)))
print('y.creators[0]: ',y.creators[0].id,'\n', y.creators[0].grad)
print('y.creators[1]: ',y.creators[1].id,'\n', y.creators[1].grad)
Multiplicacion de Matrices o mm
¶
Esta operacion es clave a la hora de multiplicar entradas o ìnputs
con los pesos de una capa de una red neuronal y permite obtener la salida de una capa. Se trata de realizar filas por columnas.
Consideremos como ejemplo un tensor $x_1$ de dimensiones $(2,2)$ y un tensor $x_2$ de dimensiones $(2,1)$.
x1 = Tensor([[2,2,2],
[2,2,2]], autograd=True)
x2 = Tensor([[3],
[3],
[3]], autograd=True)
La regla para multiplicar matrices entre sí es que el numero de filas de la primera matriz debe ser igual a la segunda y como resultado se obtiene una matriz con el mismo numero de filas que la primera columna y el mismo numero de columnas que la segunda matriz
y = Tensor.mm(x1,x2)
print(y.data.shape)
print(y.data)
y_grad = Tensor(np.ones_like(y.data))
Para entender de una forma más intuitiva la multiplicacion de matrices podemos observar un ejemplo en el siguiente link (reemplazar los valores de la matriz por los mismos de este notebook).
Ahora es necesario pasar el gradiente desde $Y$ hasta sus tensores padres
$X_1$ y $X_2$. Tenemos los siguientes tensores:
Como el gradiente debe ser pasado a los dos tensores que generaron $Y$, es necesario generar, utilizando el gradiente, tensores con las mismas dimensiones para cada tensor "padre", es decir:
- Si $X_1$ es un tensor $(2,3)$, debemos pasar como gradiente un tensor con dimension $(2,3)$
- Si $X_2$ es un tensor $(3,1)$, debemos pasar como gradiente un tensor con dimension $(3,1)$
- Sabemos tambien que tanto $y$ como $y_{grad}$ son de dimensiones $(2,1)$
En el caso de $X_1$ podemos regenerar un tensor $(2,3)$ utilizando ${grad_Y}$ y la transpuesta de $X_2$
$$grad_{X_1} = grad_Y * {X_2^T} = \begin{pmatrix} 1\\ 1\\ \end{pmatrix} \times \begin{pmatrix} 3& 3& 3\\ \end{pmatrix} = \begin{pmatrix} 3& 3& 3\\ 3& 3& 3\\ \end{pmatrix} $$En el caso de $X_2$ podemos regenerar un tensor $(3,1)$ utilizando la transpuesta de $X_1$ y ${grad_Y}$
$$grad_{X_1} = grad_Y * {X_2^T} = \begin{pmatrix} 2& 2\\ 2& 2\\ 2& 2\\ \end{pmatrix} \times \begin{pmatrix} 1\\ 1\\ \end{pmatrix} = \begin{pmatrix} 4\\ 4\\ 4\\ \end{pmatrix} $$Podemos decir que básicamente el gradiente de una multiplicacion de matrices es el producto de la multiplicacion de matrices por la transpuesta
x1.grad = y_grad.mm(x2.transpose())
x2.grad = x1.transpose().mm(y_grad)
print('x1:',x1.id,'\n', x1.grad)
print('x2: ',x2.id,'\n', x2.grad)
zero_grads(y)
y.backward(Tensor(np.ones_like(y.data)))
print('y.creators[0]: ',y.creators[0].id,'\n', y.creators[0].grad)
print('y.creators[1]: ',y.creators[1].id,'\n', y.creators[1].grad)
Transpuesta de Matrices o transpose
¶
Hacer backward de la funcion transpuesta es sencillo
- Si tenemos una matriz $X$ de dimensiones $(a,b)$ la transpuesta $Y=X'$ tendrá las dimensiones $(b,a)$
- Como tenemos un grafo computacional, debemos pasar el gradiente de $Y$ al tensor padre, en este caso $X$
- Como el gradiente $grad_Y$ en $Y$ es de dimension $(b,a)$ al pasar este gradiente a su tensor padre, debemos pasarlo de manera que tenga la misma forma que el tensor padre $X$, por tanto aplicando nuevamente la transpuesta a $grad_Y$, tenemos un tensor con las mismas dimensiones que $X$
x = Tensor([[2,2,2],
[2,2,2]], autograd=True)
x.data.shape
y = Tensor.transpose(x)
y.data.shape
y_grad = Tensor(np.ones_like(y.data))
y_grad.data.shape
x.grad = y_grad.transpose()
print('x: ',x.id,'\n', x.grad.data.shape, '\n', x.grad.data)
zero_grads(y)
y.backward(np.ones_like(y.data))
y.grad
x.grad
Suma y Expancion¶
Estas operaciones siguen una lógica similar entre sí. Se utilizan para sumar las filas o columnas de una matriz, o para expandir n veces (copiar) las columnas de un vector columna o expandir n veces las filas de un vector fila
Partamos de un tensor $X$ de dimension $(2,3)$
x = Tensor([[2,2,2],
[2,2,2]], autograd=True)
En numpy
y por consiguiente en lightdlf
se guardan las dimensiones en atributo llamado shape
el cual es una tupla
x.data.shape
Por lo que si queremos saber cuantas filas o cuantas columnas tiene el tensor debemos seguir el orden en el que aparecen en la tupla shape
, el valor en el indice 0
se correspondera a la cantidad de filas y el valor en el indice 1
al numero de columnas. Por ejemplo:
print('filas:', x.data.shape[0])
print('columntas:', x.data.shape[1])
Si queremos sumar todas las filas entre sí, es decir tomar todos los elementos de una columna y sumarlos para obtener un solo valor por columna, obtendremos un vector fila. Para referenciar a las filas debemos usar el indice 0
de la tupla shape
para que la funcion sepa que queremos sumar las filasA
x_0 = x.sum(0)
x_0
Lo mismo si queremos sumar las columnas, con la diferencia de que ahora queremos sumar las columnas, es decir, sumar todos los valores de una fila en un solo valor. En este caso el indice de la tupla shape
que determina las columnas es el indice 1
x_1 = x.sum(1)
x_1
Ahora digamos que queremos pasar gradientes a $X$ desde $X_0$ o $X_1$, para esto usamos la operacion de expanción
y_0 = x_0.expand(dim=0,copies=x.data.shape[0])
y_0
y_1 = x_1.expand(dim=1, copies=x.data.shape[1])
y_1
En el caso inverso, en el que partamos de un vector columna o un vector fila, y lo expandimos a una matriz, para pasar el gradiente al vector padre debemos realizar una suma, usando el eje 0 en caso de ser un vector fila y el eje 1 en caso de ser un vector columna