Como se vio al final del notebook anterior, si un tensor participa en la creacion de más de un tensor, su gradiente no se acumula, simplemente sobreescribe el gradiente con el ultimo gradiente recibido por el tensor.
Para que un tensor pueda participar en la creacion de más de un tensor y mantener correctamente su gradiente es necesario añadir una nueva funcion y actualizar otras tres.
Primero que nada los gradientes tienen que poder ser acumulables, permitiendo que si un tensor es usado más de una vez, pueda recibir el gradiente de todos sus hijos (tensores que se originan a partir de el).
Adicionalmente se debe crear un contador que permite saber el número de gradientes recibidos por cada uno de los hijos
o tensores creados a partir de los iniciales. Con este conteo también se previene retropropagar el gradiente del mismo hijo dos veces.
También el método all_children_accounted_for()
se utiiza para computar si un tensor recibió el gradiente de todos sus hijos en el grafo
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, tab='', print_call=True):
'''
Verifica si un tensor ha recibido la cantidad
correcta de gradientes por cada uno de sus hijos
'''
# print('tensor id:', self.id)
# print(tab+'all_children_grads_accounted_for({})'.format(self.id))
for id, cnt in self.children.items():
if (print_call) :
print(tab+'Tensor actual:', self.id, 'hijo:', id, 'count', cnt)
if(cnt != 0):
return False
return True
def backward(self, grad, grad_origin=None, tab=''):
'''
Funcion que propaga recursivamente el gradiente a los creators o padres del tensor
@grad: gradiente
@grad_orign
'''
# tab=tab
print(tab+'backward({}, {}, {})'.format(self.id, grad, grad_origin))
if(self.autograd):
if(grad_origin is not None):
print(tab+'El gradiente de',self.id,'proviene de (grad_origin):',grad_origin.id, 'count:', self.children[grad_origin.id])
# 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
print(tab+'por tanto el contador de',self.id,'se reduce a', self.children[grad_origin.id], 'para su hijo', grad_origin.id)
# acumula el gradiente de multiples hijos
if(self.grad is None):
self.grad = grad
else:
self.grad += grad
print(tab+'Tensor', self.id, 'has creators?', self.creators is not None,
'\n'+tab+'All children grads from', self.id,'accounted for is (cnt != 0)', self.all_children_grads_accounted_for(tab=tab, print_call=False),
'\n'+tab+'Has grad origin?', grad_origin is None,
'\n'+tab+'Has creators and (children grads accounted or grad no grad origin)',
'\n'+tab, self.creators is not None, 'and', '(',self.all_children_grads_accounted_for(print_call=False) ,'or',grad_origin is None,') =>',
self.creators is not None and (self.all_children_grads_accounted_for(print_call=False) or grad_origin is None)
)
if(self.creators is not None and
(self.all_children_grads_accounted_for(print_call=False) or grad_origin is None)):
if (self.creation_op == 'add'):
# al recibir self.grad, empieza a realizar backprop
print(tab + str(self.id), 'creators are:')
print(tab+'creator', self.creators[0].id, ':', self.creators[0],
'creator', self.creators[1].id, ':',self.creators[1])
print(tab+'\tbackward call from creator[0]:', self.creators[0].id)
self.creators[0].backward(self.grad, grad_origin=self, tab=tab+'\t')
print()
print(tab+'\tbackward call from creator[1]', self.creators[0].id)
self.creators[1].backward(self.grad, grad_origin=self, tab=tab+'\t')
def __add__(self, other):
'''
@other: un Tensor
'''
if(self.autograd and other.autograd):
new_tensor = Tensor(self.data + other.data,
autograd=True,
creators=[self, other],
creation_op='add')
print(' new tensor id is', new_tensor.id)
return new_tensor
return Tensor(self.data + other.data)
def __repr__(self):
return str(self.data.__repr__())
def __str__(self):
return str(self.data.__str__())
Visualizacion de las llamadas de backward¶
Se agrego al metodo backward una serie de print()
s que permiten ver las llamadas a medida que se propaga el gradiente por cada uno de los tensores que participaron en la creación del tensor final, es decir la salida.
x = Tensor([2,2,2,2], autograd=True)
print('y = x + x')
y = x + x
print('z = y + y')
z = y + y
print()
print('x id:',x.id)
for hijo, cnt in x.children.items():
print(' hijo:', hijo, 'count', cnt)
print('y id:', y.id)
for hijo, cnt in y.children.items():
print(' hijo:', hijo, 'count', cnt)
print('z id:', z.id, '\n')
z.backward(Tensor([1,1,1,1]))
print('\nx gradient data:',x.grad.data)
# z.backward(Tensor([1,1,1,1]))
a = Tensor([1,2,3,4,5], autograd=True)
b = Tensor([2,2,2,2,2], autograd=True)
c = Tensor([5,4,3,2,1], autograd=True)
print('d = a + b')
d = a + b
print('e = b + c')
e = b + c
print('f = d + e')
f = d + e
print()
print('a id:',a.id)
for hijo, cnt in a.children.items():
print('\thijo:', hijo, 'count', cnt)
print('b id:', b.id)
for hijo, cnt in b.children.items():
print('\thijo:', hijo, 'count', cnt)
print('c id:',c.id)
for hijo, cnt in c.children.items():
print('\thijo:', hijo, 'count', cnt)
print('d id:', d.id)
for hijo, cnt in d.children.items():
print('\thijo:', hijo, 'count', cnt)
print('e id:', e.id)
for hijo, cnt in e.children.items():
print('\thijo:', hijo, 'count', cnt)
print('f id:', f.id, '\n')
f.backward(Tensor(np.array([1,1,1,1,1])))
print(b.grad.data == np.array([2,2,2,2,2]))
x1 = Tensor([1,1,1,1], autograd=True)
x2 = Tensor([1,1,1,1], autograd=True)
print('x3 = x1 + x2')
x3 = x1 + x2
print('x4 = x1 + x2')
x4 = x1 + x2
print('x5 = x1+ x2 + x3 + x4')
x5 = x1+ x2 + x3 + x4
print()
print('x1 id:',x1.id)
for hijo, cnt in x1.children.items():
print('\thijo:', hijo, 'count', cnt)
print('x2 id:', x2.id)
for hijo, cnt in x2.children.items():
print('\thijo:', hijo, 'count', cnt)
print('x3 id:',x3.id)
for hijo, cnt in x3.children.items():
print('\thijo:', hijo, 'count', cnt)
print('x4 id:', x4.id)
for hijo, cnt in x4.children.items():
print('\thijo:', hijo, 'count', cnt)
print('x5 id:', x5.id, '\n')
x5.backward(Tensor([1,1,1,1]))
x = Tensor([2,2,2,2], autograd=True)
print('y = x + x + x + x')
y = x + x + x + x
print('z = y + y')
z = y + y
print()
print('x id:',x.id)
for hijo, cnt in x.children.items():
print(' hijo:', hijo, 'count', cnt)
print('y id:', y.id)
for hijo, cnt in y.children.items():
print(' hijo:', hijo, 'count', cnt)
print('z id:', z.id, '\n')
z.backward(Tensor([1,1,1,1]))
print('\nx gradient data:',x.grad.data)