26 Descenso por gradiente
Uno de los métodos más empleados para entrenar las redes neuronales debido a su efectividad y relativa eficiencia es el del descenso por gradiente. Imagina que estás en una montaña y tu objetivo es llegar al punto más bajo posible, pero está completamente oscuro y solo puedes sentir el terreno bajo tus pies. El algoritmo de descenso por gradiente es como tener un método para decidir en qué dirección dar el próximo paso basándote en la pendiente o inclinación del suelo que sientes con los pies. En vez de caminar al azar, decides moverte en la dirección en la que el terreno desciende más.
En este método la función objetivo está determinada por el error en las predicciones. \(E(\theta)\), donde \(\theta\) es un conjunto de parámetros. En la ecuación anterior no se especifica como parámetro las entradas de la red, \(x\), ni los valores objetivo, \(y\), porque durante el entrenamiento éstos se mantienen constantes.
26.1 Métricas de error
Existen diversas formas de medir el error, la más simple es usar el error residual de las predicciones:
$$
e = (y-)$$
, donde \(y\) es el valor objetivo y \(\hat{y}\) es la predicción. Este error es adecuado para una sola observación, sin embargo, no es fácil de generalizar a muchos valores dado que puede sufrir de cancelación o compensación de errores.
La suma de los cuadrados de los errores nos permite incorporar los errores de muchas predicciones en un solo valor escalar. La ecuación (#eq:sse) muestra el SSE, donde el super-índice indica el número de ejemplo y \(m\) el total de ejemplos.
$$
= _{i=1}^{m} (y^i - i)2 $$
El SSE contempla todos los errores sobre un conjunto de datos pero si los conjuntos de datos son muy grandes pueden causar un error de desbordamiento (overflow), que ocurre en computación cuando un cálculo produce un resultado que excede el rango máximo que puede ser representado con el número de bits asignados para ese tipo de datos en una computadora. Para solventar el problema se usa el error cuadrático promedio:
$$
= _{i=1}^{m} (y^i - i)2, $$
donde \(m\) es el total de ejemplos en el conjunto de datos.
En muchos casos se le denomina también a la función de error como función de pérdida (Loss function). En este libro haremos referencia de forma indistinta a error y pérdida.
26.2 Proceso general de descenso por gradiente
Regresando al método de descenso por gradiente, a éste lo podemos sintetizar en los siguientes pasos:
- Comenzamos con una conjetura inicial de los parámetros. $$
W = ()$$
- Determinamos la dirección del mayor decremento de la función mediante el gradiente. $$
_W E = $$
- Determinamos un cambio a partir de la dirección de decremento y un escalar (conocida como tasa de aprendizaje y escrita como \(\eta\)) que nos permite controlar que tanto se moverá la conjetura de los parámetros. Este cambio se suma a la conjetura previa de los pesos. $$
W = W - _W E
$$
- Repetimos hasta encontrar un mínimo local.
Si el lector requiere más información acerca del método de descenso por gradiente general, éste puede consultar la sección #sec:desc_gradiente_gral que describe el método e incluye ejemplos generales.

26.3 Uso en redes neuronales
La forma descrita previamente del descenso por gradiente a través de cuatro pasos es correcta, sin embargo, para poder llegar a una implementación práctica en las redes neuronales haremos algunas especificaciones y acomodos. Primero suponiendo el caso de una neurona simple y más tarde generalizándolo a neuronas con múltiples salidas.
En el primer paso del proceso, supondremos que el muestreo de los pesos se realiza de forma aleatoria bajo una distribución uniforme del espacio [-1, 1]. Más adelante abordaremos más formas de inicialización. Para el segundo paso, supondremos el caso de una neurona simple, es decir con una sola capa, y como función de error una versión modificada del MSE descrito en la ecuación #eq:mse: $$
E() = _{i=1}^{m} (y^i - i)2$$
Por lo tanto la derivada del error con respecto de cada peso, \(w_i\), se puede escribir como: $$
_{w_i} E = -(y-) f’(h) x_i $$
, donde \(f'(h)\) indica la derivada de la función de activación. Si aplicamos esto a la ecuación del descenso por gradiente, ecuación (#eq:descenso_general_w) obtenemos: $$
W = W - (-(y-) f’(h) x_i)
$$
Si bien, la ecuación (#eq:reemplazo_gradiente) ya se puede implementar directamente. En cálculos manuales puede ser complicado usarla. Por lo cual reacomodaremos desde la ecuación (#eq:gradiente_especifico) y haremos las siguientes definiciones.
Se compone del producto entre el error residual y la derivada de la función de activación, es decir: $$
= (y-)f’ (h)
$$
El término de error nos servirá como un indicativo de la dirección a la que nos tendremos que mover para minimizar la función de pérdida. Nótese que el término de error cambia el signo con respecto a la ecuación (#eq:gradiente_especifico). Ésto se debe a que por definición el gradiente apunta en la dirección del cambio ascendente y el movimiento que requerimos debe ser en sentido contrario.
Para el tercer paso, primero calcularemos un incremento de los pesos, donde incluímos un nuevo hiperparámetro, \(\eta\) (eta), que controlará la magnitud de cambio, a esta variable se le conoce también como tasa de aprendizaje (learning rate).
Se calcula a partir del producto entre la tasa de aprendizaje, el término de error y la entrada correspondiente. Es decir, $$
w_i= x_i
$$
Finalmente, para completar la definición del descenso por gradiente se realiza la actualización de los pesos.
El parámetro \(w_i\) se actualiza a partir del valor actual y el incremento del peso. Es decir, $$
w_i = w_i + w_i $$
El proceso de dividir el descenso por gradiente en tres partes, ecuaciones (#eq:termino_error), (#eq:incremento_del_peso) y (#eq:actualizacion_del_peso) es importante desde el punto de vista computacional, dado que permite reutilizar el cálculo y volver la ejecución más eficiente.
26.4 Derivadas de las funciones de activación
Como se observó en la sección anterior, la implementación por pasos del descenso por gradiente requiere que se utilice la derivada de la función de activación, indicada como \(f'(h)\). A continuación se proveen algunas derivadas comunes.
26.4.1 Sigmoide
$$
(x) = (x) (1 - (x))$$
def sigmoid_derivative(x):
s = 1 / (1 + np.exp(-x))
return s * (1 - s)26.4.2 Tangente hiperbólica
$$
(x) = 1 - ^2(x)$$
def tanh_derivative(x):
return 1 - np.tanh(x)**226.4.3 ReLU
$$
(x) = \[\begin{cases} 1 & \text{si } x > 0 \ 0 & \text{si } x \leq 0 \end{cases}\]i } x \end{cases}$$
def relu_derivative(x):
return np.where(x > 0, 1, 0)26.4.4 Leaky ReLU
$$
(x) = \[\begin{cases} 1 & \text{si } x > 0 \ \alpha & \text{si } x \leq 0 \end{cases}\]ext{si } x \end{cases}$$
def leaky_relu_derivative(x, alpha=0.01):
return np.where(x > 0, 1, alpha)Suponga la siguiente configuración de una neurona simple: 
donde \(W=[0.4, 0.5]\), \(b=-0.6\), \(f=\sigma(h)\) y \(X=[4, 2]\). Y sabiendo que la predicción actual de la red es \(\hat{y}=0.88\). Calcule los nuevos valores para los pesos a partir del descenso por gradiente a una época usando \(\eta = 1\) y tomando en cuenta que el valor esperado es \(y=0.5\).
SOLUCIÓN: Siguiendo los pasos propuestos en la sección 3.3, calculamos el error MSE, \(E\), con la ecuación #eq:mse:
$$
E()= _{i=1}^{m} (y^i - i)2\[ \]
E()= (0.5 - 0.88)^2 = 0.0722$$
Ahora calcularemos el término de error con la ecuación (#eq:termino_error), pero primero calcularemos la derivada, \(f'\), de nuestra función sigmoide:
$$
(h)=0.88\[ \]
f’(h) = (h)(1-(h)) = 0.06698$$
Entonces \(\delta\), será:
$$
= (y-)f’(h) = (0.5-0.88)(0.06698) = -0.0221$$
El siguiente paso es calcular el incremento del peso y para eso usaremos la ecuación (#eq:incremento_del_peso):
$$
w_i = x_i, = 1\[ \]
w_1 = 1(-0.0221)(4) = -0.0884\[ \]
w_2 = 1(-0.0221)(2) = -0.0442$$
Y para finalizar calcularemos los nuevos pesos con la ecuación (#eq:actualizacion_del_peso):
$$
w_1 = w_1 + w_1 = 0.4+(-0.0884)= 0.3116\[ \]
w_2 = w_2 + w_2 = 0.5+(-0.0442)= 0.4558$$
Siendo nuestro vector de pesos actualizados $$
W=[0.3116, 0.4558]$$
Continúe con el ejemplo #ejem:descenso_un_paso y calcule los valores de los pesos para tres épocas de tal forma que se complete la siguiente tabla.
SOLUCIÓN. Siguiendo el proceso de forma repetida obtenemos:
Época \(\hat{y}\) Residual \(\Delta_W\) Nuevo \(W\) ——- ———– ———- ——————- ——————
1 0.880 -0.330 \[-0.088,-0.044\] \[0.311,0.455\]
2 0.826 -0.326 \[-0.187,-0.093\] \[0.124,0.362\]
3 0.650 -0.150 \[-0.136,-0.068\] \[-0.012,0.243\]
4 0.459 0.040 \[0.04, 0.02\] \[0.027, 0.263\]
26.5 Actualización para múltiples ejemplos
En los últimos años, las redes neuronales han logrado avances impresionantes en una amplia gama de aplicaciones gracias a la disponibilidad de conjuntos de datos a gran escala. Estos conjuntos de datos proporcionan el “combustible” necesario para entrenar modelos profundos y complejos, lo que ha permitido avances significativos en áreas como visión por computadora, procesamiento del lenguaje natural, y el análisis de voz.
Entre los conjuntos de datos que se usan actualmente están lo siguientes. ImageNet (Deng et al. 2009): que se utiliza para tareas de clasificación de imágenes. Actualmente, contiene más de 14 millones de imágenes distribuidas en 1,000 categorías. COCO (Common Objects in Context) (Lin et al. 2014): un conjunto de datos ampliamente utilizado para tareas de reconocimiento de objetos y segmentación; contiene más de 330,000 imágenes, con más de 1.5 millones de instancias de objetos etiquetados. MassiveText (Rae et al. 2021): utilizado en el preentrenamiento de modelos de lenguaje, contiene múltiples petabytes de datos textuales extraídos de diversas fuentes, incluidos sitios web, repositorios de código, y literatura técnica. Entre otros.
Por lo anterior, es necesario que el método de entrenamiento contemple la contribución que tiene cada ejemplo a las predicciones de la red y en consecuencia a la pérdida. Esto se logra a partir de extender el incremento del peso definido en la ecuación (#eq:actualizacion_del_peso) a múltiples ejemplos.
Suponiendo un total de \(m\) tuplas de ejemplos, \((x^\mu, y^\mu) \in Data\), el incremento ahora se calcula considerando las contribuciones de cada ejemplo al error y promediando las contribuciones por el número de ejemplos. Es decir, para el peso \(w_i\) que conecta la entrada \(x_i\) su incremento se calcula como:
$$
w_i = _{}^{m} ^x_i, $$
donde \(\delta^\mu\) es el término de error para la \(\mu\)-ésima predicción: $$
^= (y-)f’(h^)$$
26.6 Implementación eficiente
Si bien las definiciones para el incremento y actualización de los pesos son correctas, la implementación de dichas ecuaciones se puede eficientar a través de reutilizar las cálculos. Por lo tanto, una posible implementación se presenta en el algoritmo #al:descenso_por_grad. Dicha implementación requiere el conjunto de datos, el número de registros, la tasa de aprendizaje y el número de épocas como entrada. Y entrega el conjunto de pesos actualizado. Como podemos observar en el algoritmo, líneas 6 y 8, el incremento de los pesos se hace reutilizando el término de error que es calculado para cada ejemplo. De forma similar, en la línea 8 no se incluye la tasa de aprendizaje ni el promedio (definidos en la ecuación (#eq:incremento_multiples_ejemplos)) debido a que mueven a la actualización en la linea 12. Esto tiene como consecuencia que solo se calcule una vez dicha multiplicación. En pocos ejemplos este cambio no tiene efecto, sin embargo, para miles o millones de ejemplos con miles de épocas de ejecución el ahorro será significativo.
# Descenso por gradiente para una neurona simple usando un conjunto de datos de longitud arbitraria.
[htb]
Entrada: Conjunto de datos ($Data$), número de registros ($m$), tasa de aprendizaje ($\eta$), número de épocas ($epochs$)
Salida: Pesos entrenados ($W$)
%$w_i = rand(-\frac{1}{\sqrt{n}}, \frac{1}{\sqrt{n}})$ # Inicializar pesos
$\texttt{Inicializar}(W)$# Inicializar pesos
\For{$e=1;epochs$}
%\tcc{Para cada registro en la tabla de datos}
$\text{Ceros}(\Delta W_i)$ \;
\ForEach{(x, y) $\in$ Data}
$\hat{y} = f(h(x,W))$ # Pase frontal
$\delta = (y-\hat{y})f'(h)$ # Término de error
\ForEach{$w_i \in W$}
$\Delta w_i = \Delta w_i + \delta x_i$
# Incremento
\ForEach{$w_i \in W$}
$w_i = w_i + \frac{\eta}{m} \Delta w_i $ # Actualización normalizada}
return $W$
Con respecto a la codificación del método en Python. El código del cuadro #cod:descenso_por_g implementa el entrenamiento de una red neuronal simple utilizando el método de descenso por gradiente. Comienza definiendo el número de épocas (\(epochs\)) y la tasa de aprendizaje (\(learnrate\)).
Luego, en un bucle for que recorre cada época de entrenamiento, se inicializa un incremento en los pesos (\(incremento_w\)) como una matriz de ceros del mismo tamaño que los pesos originales.
for e in range(epochs):
incremento_w = np.zeros(weights.shape)Dentro de este bucle principal, hay otro bucle que itera sobre cada par de características (\(features\)) y objetivos (\(targets\)). La función \(zip\) permite emparejar las características con los objetivos.
for x, y in zip(features.values, targets):
Para cada par, calcula la salida de la red neuronal (output) aplicando la función de activación sigmoidal a la suma ponderada de las características y los pesos actuales.
h = np.dot(x, weights)
output = sigmoid(h)Luego, se calcula el término de error en dos instrucciones: calcular el error residual y multiplicar por la derivada de la función de activación,
error = y - output
delta = error*sigmoid_prime(h)
A continuación se acumula el incremento. Hay que tener en cuenta que la entrada \(x\) es un vector y al usar el operador * se escala el vector por el término de error.
incremento_w += delta * x
Después de procesar todos los datos de entrenamiento, se normaliza el incremento de los pesos dividiendo por el número total de muestras (\(m\)), y luego actualiza los pesos sumándoles el producto del incremento en los pesos y la tasa de aprendizaje.
m = len(features.values)
weights += (learnrate/m) * incremento_w
En resumen, este código entrena una red neuronal durante un número específico de épocas, ajustando los pesos de acuerdo con el gradiente descendente para minimizar el error entre las predicciones del modelo y los valores objetivo.
def descenso_por_gradiente(ejemplos, valores_objetivo, tasa_de_aprendizaje, epocas):
# Variables varias
n_ejemplos, n_caracteristicas = ejemplos.shape
Historial_error = [] # Inicializacion de pesos
pesos = np.random.normal(scale=1 / n_caracteristicas**.5, size=n_caracteristicas) for e in range(epocas):
incremento_w = np.zeros(pesos.shape)
for x, y in zip(ejemplos.values, valores_objetivo):
# Inferencia
h = np.dot(x, pesos)
salida = sigmoid(h) # Calculo de error
error = y - salida
# Termino de error
delta = error*sigmoid_prime(h)
# Acumulacion del gradiente
incremento_w += delta * x
# Actualizacion de pesos
m = len(ejemplos.values)
pesos += (tasa_de_aprendizaje/m) * incremento_w
# Registro de error
out = sigmoid(np.dot(ejemplos, pesos))
error = np.mean((out - valores_objetivo) ** 2)
Historial_error.append(error)
if e % (epocas / 10) == 0:
print("Epoca:", e, " Error: {:.3f}".format(error))
return pesos, Historial_error26.7 Ejercicios
Muestre que la derivada del error cuadrático medio (MSE) con respecto de los pesos para la suma de los errores cuadráticos es igual al término de error por la entrada. Es decir, $$
( _{i=1}^{m} (y^i - i)2 ) = x_i$$
donde \(\delta\) se escribe de acuerdo a la ecuación (#eq:termino_error).
Suponiendo que la red del ejercicio #ej:rns_3_1-1 deba tener una salida objetivo \(y = 0.95\):
Determine el error residual, \(e\), de la red.
Determine el término de error, \(\delta\).
Determine el incremento de los pesos \(\Delta w_i\) usando una tasa de aprendizaje \(\eta = 10\).
Actualice el valor de cada \(w_i\).
Escriba un programa en python que resuelva el ejercicio #ej:descenso_1 para \(n\) épocas y que muestre la grafica de epocas vs MSE. La cantidad de épocas y la tasa de aprendizaje es introducida por el usuario.
Partiendo del ejercicio #ex:red_simple_ma1 calcule:
El vector de términos de error, \(\delta\)
La matriz de incrementos, \(\Delta W\)
Actualice los pesos.
Suponga una red neuronal simple con una entrada de tamaño tres y una salida de tamaño cuatro. Considerando: $$
X = [5, 2, 9],\[ $Y = [-1, 1, 1, -1],$ \]
W = \[\begin{bmatrix} 0.1 & -0.6 & 0.3 & -0.1\ 0.7 & -0.6 & 0.0 & -0.6 \ -0.9 & -0.8 & 0.7 & -0.5 \end{bmatrix}\]0.0 & -0.6
-0.9 & -0.8 & 0.7 & -0.5 \end{bmatrix},$$
una función de activación \(tanh\), una función de pérdida como error cuadrático medio, y \(\eta = 2\).
Determine,
La salida de la red, \(\hat{Y}\)
El vector término de error.
Los incrementos de los pesos.
La actualización de los pesos.