The Vanishing Gradient Problem
Problema
Para funciones de activación, cuya derivada se acerca a cero, conforme se agregan capas a la red neuronal este se vuelve más y más difícil de entrenar.
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('ggplot')
def sigma(z):
return 1 / (1 + np.exp(-z))
def derivada_sigma(z):
a = sigma(z)
return a * (1 - a)
def grafica_activación():
x = np.linspace(-4,4)
s = sigma(x)
ds = derivada_sigma(x)
t = np.tanh(x)
dt = 1 - t**2
r = np.maximum(0,x)
dr = np.heaviside(x, 0)
fig, axes = plt.subplots(1, 3, figsize=(12,4))
axes[0].plot(x, s, label="$\\sigma(x)$")
axes[0].plot(x, ds, label="$\\sigma'(x)$")
axes[0].set_title("Comportamiento de la logística")
axes[1].plot(x, t, label="$\\tanh(x)$")
axes[1].plot(x, dt, label="$\\tanh'(x)$")
axes[1].set_title("Comportamiento de la $\\tanh$")
axes[2].plot(x, r, label="$ReLU(x)$")
axes[2].plot(x, dr, label="$ReLU'(x)$")
axes[2].set_title("Comportamiento de ReLU")
for i in range(3):
axes[i].legend()
axes[i].set_xlabel("$x$")
axes[i].set_ylabel("$y$")
grafica_activación()
Los algoritmos de optimización basados en el gradiente comparten la idea escencial del descenso por el gradiente: \begin{align} W_{t+1} = W_t - \alpha \nabla_W J \end{align} donde $J$ es la función de error y $W$ los pesos.
Recordando que para calcular el gradiente con respecto a cada capa de pesos éste es proporcional a: \begin{align} \Delta^{(l-1)} &= \Delta^{(l)}\left(W^{(l-1)}\right)^T \circ g'(Z^{(l-1)}) \end{align}
tenemos que, entre más capas, más veces aparece el término $g'(Z^{(l-1)})$, de modo que para la primer capa:
\begin{align} \Delta^{(1)} &\propto g'(Z^{(0)}) \circ g'(Z^{(1)}) \circ ... \circ g'(Z^{(L-1)}) \end{align}siendo todos estos números $\ll 1$.
De este modo, al sustituir el valor de un gradiente muy pequeño en la fórmula de entrenamiento, los pesos prácticamente no cambian, independientemente de qué tan grande sea la magnitud del valor de entrada.
def grafica_ejemplo():
"""
Muestra la gráfica de la función que tratamos de aproximar
"""
x = np.arange(-1,1.02,0.02)
plt.plot(x, x/2 * (np.sin(20*x) - np.cos(20*x/3)))
plt.title('$f(x) = \\frac{x}{2} \\left( sin(20x) - cos(\\frac{20}{3}x) \\right)$')
plt.xlabel('x')
plt.ylabel('y')
grafica_ejemplo()
import torch
from torch import nn
X = torch.arange(-1.,1.02,0.02) # Tensor 1D
X = torch.reshape(X, (len(X),1))
def f(x):
"""
Función objetivo.
"""
return x/2 * (torch.sin(20*x) - torch.cos(20*x/3))
# Calculamos los valores de la función que queremos aproximar
# para las entradas X
with torch.no_grad():
Y = f(X)
class DeepNeuralNetwork(nn.Module):
def __init__(self, s, f_activacion=nn.Sigmoid):
"""
param s: Lista de número de neuronas en las capa ocultas
"""
super(DeepNeuralNetwork, self).__init__()
# Primer capa oculta:
componentes = [nn.Linear(1,s[0]), f_activacion()]
# Demás capas ocultas:
for i in range(1,len(s)):
componentes.append(nn.Linear(s[i-1],s[i]))
componentes.append(f_activacion())
# Capa de salida
componentes.append(nn.Linear(s[-1],1))
componentes.append(f_activacion())
# Arma perceptrón multicapa
self.layers_stack = nn.Sequential(*componentes)
def forward(self, x):
"""
Evaluación de la red sobre la entrada x
usando alimentación hacia adelante para los pesos
actuales.
"""
logits = self.layers_stack(x)
return logits
model = DeepNeuralNetwork([3] * 7)
# Calcularemos el gradiente de la diferencia al cuadrado
loss_fn = nn.MSELoss()
# Evaluamos la función de la red con encadenamiento hacia adelante
Y_ = model(X)
# Evaluamos el error
loss = loss_fn(Y, Y_)
# Limpiamos tensores para gradientes
model.zero_grad()
# Solicitamos el cálculo del gradiente del error
loss.backward()
# Parámetros entrenables de la red
params = list(model.parameters())
# Cada componente del gradiente, que acaba de ser
# calculado, con respecto
# a un parámetro se encuentra almacenado al
# lado del tensor que contiene al parámetro.
for t in params:
print("Parámetro: ")
print(t)
print("Gradiente: ")
print(t.grad)
print()
Parámetro: Parameter containing: tensor([[-0.3747], [ 0.3810], [ 0.8487]], requires_grad=True) Gradiente: tensor([[-2.9598e-08], [-4.4662e-09], [-1.0860e-08]]) Parámetro: Parameter containing: tensor([ 0.5580, -0.8845, -0.2833], requires_grad=True) Gradiente: tensor([-2.4753e-07, -3.0874e-08, -9.2564e-08]) Parámetro: Parameter containing: tensor([[ 0.5251, -0.1779, -0.2642], [-0.4621, -0.0953, -0.4137], [-0.5315, 0.3036, -0.3229]], requires_grad=True) Gradiente: tensor([[-5.3400e-07, -2.5634e-07, -3.8342e-07], [ 1.1432e-06, 5.4975e-07, 8.2295e-07], [-2.5947e-07, -1.2478e-07, -1.8678e-07]]) Parámetro: Parameter containing: tensor([-0.3422, 0.5498, 0.0545], requires_grad=True) Gradiente: tensor([-8.5147e-07, 1.8239e-06, -4.1399e-07]) Parámetro: Parameter containing: tensor([[-0.4503, -0.1407, 0.1792], [-0.2964, 0.4519, -0.1615], [ 0.0518, 0.3945, -0.0757]], requires_grad=True) Gradiente: tensor([[1.1630e-06, 1.3103e-06, 1.0703e-06], [4.1970e-06, 4.7286e-06, 3.8624e-06], [4.0043e-06, 4.5116e-06, 3.6852e-06]]) Parámetro: Parameter containing: tensor([-0.2431, -0.4966, 0.2690], requires_grad=True) Gradiente: tensor([2.5632e-06, 9.2500e-06, 8.8256e-06]) Parámetro: Parameter containing: tensor([[ 0.5196, -0.1359, -0.2528], [-0.3173, -0.3968, 0.0437], [-0.1837, 0.1314, -0.1131]], requires_grad=True) Gradiente: tensor([[-3.9296e-05, -3.8745e-05, -6.1722e-05], [-4.4144e-05, -4.3524e-05, -6.9336e-05], [-5.7807e-05, -5.6996e-05, -9.0797e-05]]) Parámetro: Parameter containing: tensor([-0.5750, -0.2630, 0.4972], requires_grad=True) Gradiente: tensor([-0.0001, -0.0001, -0.0001]) Parámetro: Parameter containing: tensor([[ 0.4874, -0.2084, 0.4445], [-0.4296, -0.4781, -0.5411], [ 0.1240, 0.4788, 0.3673]], requires_grad=True) Gradiente: tensor([[-8.3349e-05, -8.6929e-05, -1.3935e-04], [ 2.1898e-04, 2.2839e-04, 3.6611e-04], [-1.7935e-04, -1.8705e-04, -2.9985e-04]]) Parámetro: Parameter containing: tensor([-0.5213, 0.4927, -0.3021], requires_grad=True) Gradiente: tensor([-0.0002, 0.0006, -0.0005]) Parámetro: Parameter containing: tensor([[-0.0089, 0.3949, -0.4920], [-0.0530, 0.5379, 0.4794], [ 0.5185, 0.3558, 0.5124]], requires_grad=True) Gradiente: tensor([[ 0.0021, 0.0021, 0.0025], [ 0.0010, 0.0010, 0.0012], [-0.0007, -0.0007, -0.0008]]) Parámetro: Parameter containing: tensor([-0.5563, 0.1217, 0.1844], requires_grad=True) Gradiente: tensor([ 0.0046, 0.0022, -0.0015]) Parámetro: Parameter containing: tensor([[ 0.5241, 0.2613, -0.4219], [-0.5057, -0.0998, -0.0186], [-0.5765, -0.1275, 0.3985]], requires_grad=True) Gradiente: tensor([[ 0.0124, 0.0232, 0.0253], [-0.0085, -0.0160, -0.0174], [ 0.0065, 0.0122, 0.0133]]) Parámetro: Parameter containing: tensor([-0.3482, 0.0596, -0.4209], requires_grad=True) Gradiente: tensor([ 0.0360, -0.0248, 0.0189]) Parámetro: Parameter containing: tensor([[ 0.5198, -0.3534, 0.2791]], requires_grad=True) Gradiente: tensor([[0.1206, 0.1278, 0.1120]]) Parámetro: Parameter containing: tensor([0.1247], requires_grad=True) Gradiente: tensor([0.2827])
def linealiza_params(params):
params_list = []
grad_list = []
for t in params:
nparams = torch.reshape(t, (-1,)).detach().numpy()
for p in nparams:
params_list.append(p)
ngrads = torch.reshape(t.grad, (-1,)).detach().numpy()
for g in ngrads:
grad_list.append(g)
return params_list, grad_list
def plot_magnitudes(params, f_act_name="Sigmoide"):
"""
Graficamos el valor de gradiente para cada capa
"""
params_list, grad_list = linealiza_params(params)
x = np.arange(len(params_list))
fig, axes = plt.subplots(1, 2, figsize=(12,4))
axes[0].bar(x, params_list)
axes[0].set_title("Magnitudes de los pesos")
axes[0].set_xlabel("$w_i$")
axes[1].bar(x, grad_list)
axes[1].set_title(f"Componentes del gradiente \n{f_act_name}")
axes[1].set_xlabel("$\partial /\partial w_i$")
plot_magnitudes(params)
# Versión interactiva
from ipywidgets import interact_manual
import ipywidgets as widgets
modelo_experimento = None
fs_activacion = {"Tanh": nn.Tanh,
"LogSigmoid": nn.LogSigmoid,
"ReLU": nn.ReLU,
"LeakyReLU": nn.LeakyReLU,
"Sigmoid": nn.Sigmoid
}
@interact_manual(
activacion_nombre = widgets.SelectionSlider(
options = ["Tanh",
"LogSigmoid",
"ReLU",
"LeakyReLU",
"Sigmoid"
]
)
)
def experimenta(activacion_nombre):
global modelo_experimento
modelo_experimento = model = DeepNeuralNetwork([3] * 7, fs_activacion[activacion_nombre])
# Evaluamos la función de la red con encadenamiento hacia adelante
Y_ = model(X)
# Evaluamos el error
loss = loss_fn(Y, Y_)
# Limpiamos tensores para gradientes
model.zero_grad()
# Solicitamos el cálculo del gradiente del error
loss.backward()
plot_magnitudes(list(model.parameters()), activacion_nombre)
interactive(children=(SelectionSlider(description='activacion_nombre', options=('Tanh', 'LogSigmoid', 'ReLU', …
La siguiente celda permite entrenar el modelo y visualizar qué pasa con los pesos y los gradientes
import math
def train(X, Y, model, learning_rate, num_steps):
"""
Función que realiza el entrenamiento:
Intentará reducir la distancia entre los valores que salen
de la red y lo que deseamos modificando los pesos de la red.
"""
size = len(X)
loss_fn = nn.MSELoss()
# Misma alfa para todos los pesos:
#optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
# Utiliza momentos para actualizar alfa
# sobre parámetros individuales
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
step_report = 100
num_reports = int(math.ceil(num_steps/step_report))
e = np.zeros(num_reports)
for i in range(num_steps):
# Predicción y error
Y_hat = model(X)
loss = loss_fn(Y_hat, Y)
# Reduciendo el error
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Cómo vamos
if i % step_report == 0:
num_report = int(i/step_report)
e[num_report] = loss.item()
#print(f"{num_report} loss: {loss:>7f}")
#plt.plot(e)
return Y_hat, e
@interact_manual(
pasos = widgets.FloatLogSlider(value=1000,
base=10,
min=3, # min exponent of base
max=5, # min exponent of base
step=0.2), # exponent step,
)
def experimenta(pasos):
n_pasos = int(pasos)
global modelo_experimento
y_pred, e = train(X, Y, modelo_experimento, 0.01, n_pasos)
y_pred = y_pred.detach().numpy()
params_list, grad_list = linealiza_params(params)
fig, axes = plt.subplots(2, 2, figsize=(12,12))
axes[0][0].plot(np.arange(len(e))*100,e)
axes[0][0].set_title("Error vs pasos")
axes[0][0].set_xlabel('pasos')
axes[0][0].set_ylabel('error')
axes[0][1].plot(torch.reshape(X, (-1,)).detach().numpy(), y_pred)
axes[0][1].set_title("$\hat{y}(x)$")
axes[0][1].set_xlabel('$x$')
axes[0][1].set_ylabel('$y$')
x = np.arange(len(params_list))
axes[1][0].bar(x, params_list)
axes[1][0].set_title("Magnitudes de los pesos")
axes[1][0].set_xlabel("$w_i$")
axes[1][1].bar(x, grad_list)
axes[1][1].set_title(f"Componentes del gradiente")
axes[1][1].set_xlabel("$\partial /\partial w_i$")
interactive(children=(FloatLogSlider(value=1000.0, description='pasos', max=5.0, min=3.0, step=0.2), Button(de…