En el notebook anterior se implemento autograd con la operación de suma
pero en un grafo computacional la suma es una de muchas otras operaciones que se pueden realizar con tensores. Por tanto es necesario definir estas operaciones con sus respectivos gradientes o derivadas.
Para la suma definimos el siguiente metodo:
def __add__(self, other):
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)
Y para la retropropagacion definimos la siguiente condicion dentro del método backward()
:
if (self.creation_op == 'add'):
self.creators[0].backward(self.grad, grad_origin=self)
self.creators[1].backward(self.grad, grad_origin=self)
Aparte de estas dos secciones de código, la retropropagación no se maneja en ninguna otra parte de la clase Tensor
. Toda la lógica de la retropropagación se abstrae para que todo lo necesario para la suma sea definido solamente en esas dos partes.
Hay que notar también de que la retropropagación o backprop
(a partir de ahora) no se realiza en el caso de que el tensor tenga el atributo autograd = False
, por eso la primera linea de __add__
contiene una condicion que verifica que los dos tensores participando de la suma tengan self.autograd == True
para que al instanciar un nuevo tensor, este también tenga self.autograd == True
Añadamos la soporte para la negacion
¶
La funcion de negacion se logra agregando las siguientes secciones de código a la clase:
Para realizar la negacion
:
def __neg__(self):
if(self.autograd):
return Tensor(self.data * -1,
autograd=True,
creators=[self],
creation_op='neg')
return Tensor(self.data * -1)
Y dentro de la funcion backward()
if (self.creation_op == 'neg'):
self.creators[0].backward(self.grad.__neg__())
Como la negacion involucra solo a un tensor, se hace backward solo para un solo creator
. Al retropropagar el gradiente de un tensor "negado", el signo de este gradiente también se invierte
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 == '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 == 'neg'):
self.creators[0].backward(self.grad.__neg__())
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 __neg__(self):
if(self.autograd):
return Tensor(self.data * -1,
autograd=True,
creators=[self],
creation_op='neg')
return Tensor(self.data * -1)
def __repr__(self):
return str(self.data.__repr__())
def __str__(self):
return str(self.data.__str__())
Ejemplo:
x = Tensor([1,1,1,1], autograd=True)
y = (-x) + (-x)
y.backward(Tensor([1,1,1,1]))
print(x.grad.data == np.array([-2,-2,-2,-2]))
Agregando soporte a más funciones¶
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__())
Como funciona sum
¶
x = Tensor([[1,2,3],[4,5,6]])
print(x)
x.sum(0)
x.sum(1)
Como funciona expand
¶
x.expand(0, 4)
x.expand(1, 4)
x.expand(2, 4)
x = Tensor([[1,1]], autograd=True)
y = Tensor([1], autograd=True)
weight_0_1 = Tensor([[1,1,1,1],[1,1,1,1]], autograd=True)
weight_1_2 = Tensor([[1],[1],[1],[1]], autograd=True)
print(x.data.shape, weight_0_1.data.shape, weight_1_2.data.shape)
layer_1 = x.mm(weight_0_1)
layer_2 = layer_1.mm(weight_1_2)
print(layer_1.data.shape, layer_2.data.shape)
backward pass
¶
layer_2_delta = layer_2 - y
print(layer_2, y, layer_2_delta)
grad = layer_2_delta
Calculo de delta o gradiente de la capa actual tomando el grad
de la capa anterior:
grad * weight.T
grad_1 = grad.mm(weight_1_2.transpose())
print(grad_1)
grad_1 = Tensor.mm(grad, weight_1_2.transpose())
print(grad_1)
Dos maneras de calcular la actualizacion de los pesos de una red neuronal, es decir layer.T x grad
weight_1_2_update = grad.transpose().mm(layer_1).transpose()
print(weight_1_2_update, weight_1_2_update.data.shape ,weight_1_2_update.autograd)
weight_1_2_update = Tensor.mm(layer_1.transpose(), grad)
print(weight_1_2_update, weight_1_2_update.data.shape ,weight_1_2_update.autograd)