TensorFlow
Tutorial TensorFlow: Implementación de una red neuronal con NumPy
TensorFlow: Introducción a NumPy para Deep Learning, Cálculos Eficientes con Redes Neuronales
Aprende TensorFlow GRATIS y certifícateIntroducción a NumPy para Deep Learning
En el ámbito del Deep Learning, es fundamental manejar eficientemente los cálculos numéricos y las operaciones matemáticas que subyacen en las redes neuronales. NumPy es una biblioteca esencial en Python que proporciona soporte para arreglos y matrices multidimensionales, conocidas como arrays y tensores, así como operaciones matemáticas de alto rendimiento sobre ellos.
Los arrays de NumPy permiten representar datos y parámetros de modelos de Deep Learning, como pesos y biases, de manera estructurada y eficiente. Por ejemplo, una imagen en escala de grises puede ser representada como un array bidimensional, mientras que una capa de neuronas puede ser modelada mediante una matriz de pesos.
Una de las características clave de NumPy es su capacidad para realizar operaciones vectorizadas, lo que significa que las operaciones se aplican simultáneamente a todos los elementos de un array sin necesidad de bucles explícitos en Python. Esto no solo simplifica el código sino que también mejora significativamente el rendimiento computacional.
Considera el siguiente ejemplo donde se inicializan los pesos y se calcula la salida de una neurona simple:
import numpy as np
# Inicialización de un generador aleatorio con una semilla para reproducibilidad
rng = np.random.default_rng(seed=42)
# Inicialización de pesos y entrada
pesos = np.array([0.2, -0.5, 0.1])
entrada = np.array([1.0, 2.0, 3.0])
# Cálculo de la suma ponderada
suma_ponderada = np.dot(entrada, pesos)
# Aplicación de una función de activación (por ejemplo, sigmoide)
salida = 1 / (1 + np.exp(-suma_ponderada))
print(f"La salida de la neurona es: {salida}")
En este ejemplo, se utiliza np.dot
para calcular el producto escalar entre la entrada y los pesos, y luego se aplica la función de activación sigmoide de forma eficiente gracias a las operaciones vectorizadas de NumPy.
Además, NumPy proporciona funciones para generar datos aleatorios, lo cual es útil para la inicialización aleatoria de pesos en una red neuronal:
# Inicialización aleatoria de pesos para una capa con 4 entradas y 3 salidas
pesos_capa = rng.standard_normal((4, 3)) * 0.01
La comprensión de las operaciones matriciales, como la multiplicación de matrices y la transposición, es esencial en Deep Learning. Por ejemplo, durante el forward pass en una red neuronal de múltiples capas, se realizan cálculos como:
# Supongamos una entrada x y matrices de pesos w1 y w2
x = np.array([[1.0], [2.0], [3.0]])
w1 = rng.standard_normal((4, 3))
w2 = rng.standard_normal((1, 4))
# Cálculo de activaciones intermedias y salida
z1 = np.dot(w1, x)
a1 = np.maximum(0, z1) # Aplicación de ReLU
z2 = np.dot(w2, a1)
salida = 1 / (1 + np.exp(-z2)) # Aplicación de sigmoide
print(f"La salida final es: {salida}")
En este contexto, np.maximum
se utiliza para implementar la función de activación ReLU, mientras que las operaciones np.dot
manejan las multiplicaciones matriciales necesarias para calcular las activaciones en cada capa.
El uso de NumPy también es crucial para implementar algoritmos de backpropagation, donde se requieren cálculos de derivadas y actualización de pesos. NumPy facilita estas operaciones al permitir manipular eficientemente los arrays y aplicar operaciones matemáticas avanzadas.
Por último, es importante mencionar que NumPy se integra bien con otras bibliotecas del ecosistema científico de Python, lo que permite crear entornos de experimentación y prototipado robustos para modelos de Deep Learning desarrollados desde cero.
Construcción de capas y neuronas desde cero
Para comprender en profundidad el funcionamiento de las redes neuronales, es fundamental saber cómo construir capas y neuronas desde cero utilizando NumPy. Este enfoque permite apreciar los detalles internos de una red neuronal y sentar las bases para modelos más complejos.
Una neurona en una red neuronal se inspira en el funcionamiento de las neuronas biológicas. Matemáticamente, una neurona toma una serie de entradas y las combina linealmente utilizando pesos y biases (sesgos), aplicando luego una función de activación para producir una salida no lineal.
La operación matemática básica de una neurona puede expresarse como:
$$
y = f(\mathbf{w}^\top \mathbf{x} + b)
$$
Donde:
- $\mathbf{x}$ es el vector de entradas.
- $\mathbf{w}$ es el vector de pesos.
- $b$ es el bias.
- $f$ es la función de activación.
- $y$ es la salida de la neurona.
En código Python con NumPy, una neurona simple se puede implementar de la siguiente manera:
import numpy as np
# Inicialización de un generador aleatorio con una semilla para reproducibilidad
rng = np.random.default_rng(seed=42)
def neurona(entrada, pesos, bias):
suma = np.dot(entrada, pesos) + bias
salida = funcion_activacion(suma)
return salida
def funcion_activacion(x):
# Por ejemplo, función sigmoide
return 1 / (1 + np.exp(-x))
# Datos de ejemplo
entrada = np.array([0.5, -1.2, 3.3])
pesos = np.array([0.8, -0.5, 0.1])
bias = 0.2
salida_neurona = neurona(entrada, pesos, bias)
print(f"La salida de la neurona es: {salida_neurona}")
En este ejemplo, la función neurona
calcula la salida de una neurona aplicando la función de activación después de la suma ponderada de las entradas y los pesos.
Una capa consiste en varias neuronas que operan en paralelo. Para construir una capa desde cero, podemos extender el código anterior para manejar múltiples neuronas. En lugar de vectores, utilizaremos matrices para representar los pesos y biases de toda la capa.
La operación para una capa completa se puede expresar como:
$$
\mathbf{y} = f(\mathbf{X} \mathbf{W}^\top + \mathbf{b})
$$
Donde:
- $\mathbf{X}$ es el vector de entradas.
- $\mathbf{W}$ es la matriz de pesos de la capa.
- $\mathbf{b}$ es el vector de biases.
- $\mathbf{y}$ es el vector de salidas de la capa.
Implementemos una capa con NumPy:
def capa(entrada, pesos, biases):
suma = np.dot(entrada, pesos.T) + biases
salida = funcion_activacion(suma)
return salida
def funcion_activacion(x):
# Función ReLU
return np.maximum(0, x)
# Datos de ejemplo
entrada = np.array([0.5, -1.2, 3.3])
pesos = np.array([
[0.8, -0.5, 0.1],
[0.2, 0.9, -0.3],
[-0.7, 0.4, 0.6]
])
biases = np.array([0.2, -0.1, 0.5])
salida_capa = capa(entrada, pesos, biases)
print(f"La salida de la capa es: {salida_capa}")
En este código, pesos
es una matriz donde cada fila corresponde a los pesos de una neurona, y biases
es un vector con los biases de cada neurona en la capa. La función capa
realiza el cálculo para todas las neuronas simultáneamente utilizando operaciones vectorizadas, lo que es altamente eficiente.
Es importante inicializar correctamente los pesos y biases. Una práctica común es inicializarlos aleatoriamente para romper la simetría y permitir que la red aprenda características útiles. Por ejemplo:
num_entradas = 3
num_neuronas = 4
pesos_inicializados = rng.standard_normal((num_neuronas, num_entradas)) * 0.01
biases = np.zeros(num_neuronas)
Aquí, los pesos se inicializan usando una distribución normal con una pequeña desviación estándar, y los biases se inicializan a cero. Esto es una práctica recomendada para estabilizar el entrenamiento de la red.
Para construir una red neuronal más profunda, se pueden encadenar varias capas. Cada capa toma la salida de la capa anterior como su entrada:
def red_neuronal(entrada):
salida1 = capa(entrada, pesos1, biases1)
salida2 = capa(salida1, pesos2, biases2)
return salida2
# Inicialización de pesos y biases para dos capas
num_entradas = 3
num_neuronas_capa1 = 5
num_neuronas_capa2 = 2
pesos1 = rng.standard_normal((num_neuronas_capa1, num_entradas)) * 0.01
biases1 = np.zeros(num_neuronas_capa1)
pesos2 = rng.standard_normal((num_neuronas_capa2, num_neuronas_capa1)) * 0.01
biases2 = np.zeros(num_neuronas_capa2)
# Datos de entrada
entrada = np.array([0.5, -1.2, 3.3])
# Obtener la salida de la red
salida_red = red_neuronal(entrada)
print(f"La salida de la red neuronal es: {salida_red}")
En este ejemplo, red_neuronal
define una red con dos capas. La primera capa transforma la entrada original, y la segunda capa procesa la salida de la primera. De esta manera, las redes pueden aprender representaciones más complejas y abstractas de los datos.
Las funciones de activación juegan un papel crucial en la introducción de no linealidad en la red. Sin funciones de activación no lineales, la red sería equivalente a una transformación lineal, incapaz de resolver problemas complejos.
Algunas funciones de activación comunes son:
- Sigmoide: útil para problemas de clasificación binaria.
- ReLU (Rectified Linear Unit): se utiliza ampliamente en capas ocultas por su simplicidad y eficiencia.
- Tanh: similar a sigmoide pero centrada en cero.
Es fundamental al construir capas y neuronas desde cero manejar correctamente las dimensiones de los arrays. Las operaciones matriciales deben ser compatibles, por lo que es recomendable verificar las formas de los arrays durante la implementación.
Por ejemplo, si entrada
es un vector de forma (n_entradas,)
y pesos
es de forma (n_neuronas, n_entradas)
, entonces al calcular np.dot(entrada, pesos.T)
, el resultado será un vector de tamaño (n_neuronas,)
, que corresponde a las salidas antes de la función de activación.
Al construir modelos desde cero con NumPy, se adquiere una visión profunda de los mecanismos internos de las redes neuronales, lo que es invaluable para comprender y solucionar potenciales problemas en modelos más complejos y para optimizar su desempeño.
Implementación del algoritmo de forward pass
El algoritmo de forward pass es el proceso por el cual una red neuronal calcula la salida a partir de una entrada, propagando los datos a través de sus capas. En esta sección, implementaremos este algoritmo utilizando NumPy, aprovechando las operaciones vectorizadas para optimizar el rendimiento.
Comenzamos definiendo una función que realiza el forward pass para una sola muestra de entrada:
import numpy as np
def forward_pass(entrada, pesos, biases, funciones_activacion):
activacion = entrada
for i in range(len(pesos)):
z = np.dot(activacion, pesos[i].T) + biases[i]
activacion = funciones_activacion[i](z)
return activacion
En este código:
entrada
es un array de NumPy que representa la entrada al modelo.pesos
es una lista de matrices que contienen los pesos de cada capa.biases
es una lista de vectores que contienen los biases o sesgos.funciones_activacion
es una lista de funciones que aplican la activación en cada capa.
Las funciones de activación introducen no linealidad en el modelo. Definimos algunas funciones comunes:
def relu(x):
return np.maximum(0, x)
def sigmoid(x):
return 1 / (1 + np.exp(-x))
Procedemos a inicializar los pesos y biases de la red neuronal:
# Inicialización de un generador aleatorio con una semilla para reproducibilidad
rng = np.random.default_rng(seed=42)
dimensiones = [4, 5, 3, 1] # Dimensiones de las capas
funciones_activacion = [relu, relu, sigmoid]
# Inicialización de pesos y biases
pesos = []
biases = []
for i in range(len(dimensiones) - 1):
peso = rng.standard_normal((dimensiones[i+1], dimensiones[i])) * 0.01
bias = np.zeros((dimensiones[i+1],))
pesos.append(peso)
biases.append(bias)
En este ejemplo:
- La red tiene una entrada de dimensión 4, dos capas ocultas de dimensiones 5 y 3, y una capa de salida de dimensión 1.
- Los pesos se inicializan con valores aleatorios pequeños.
- Los biases se inicializan a cero.
Ahora, podemos realizar el forward pass con una muestra de entrada:
# Muestra de entrada
x = np.array([0.5, -1.2, 0.3, 2.1])
# Cálculo de la salida
salida = forward_pass(x, pesos, biases, funciones_activacion)
print(f"Salida de la red neuronal: {salida}")
El forward pass consiste en propagar la entrada a través de cada capa, aplicando las operaciones lineales y las funciones de activación correspondientes. Gracias a las operaciones vectorizadas de NumPy, este proceso es eficiente y escalable.
Para procesar múltiples muestras simultáneamente (mini-batch), adaptamos la función:
def forward_pass_batch(entradas, pesos, biases, funciones_activacion):
activaciones = entradas
for i in range(len(pesos)):
z = np.dot(activaciones, pesos[i].T) + biases[i]
activaciones = funciones_activacion[i](z)
return activaciones
Utilizamos esta función con un conjunto de datos:
# Conjunto de entradas (mini-batch)
X = np.array([
[0.5, -1.2, 0.3, 2.1],
[1.0, 0.0, -0.5, 0.8],
[-0.3, 1.5, 0.7, -1.2]
])
# Cálculo de las salidas
salidas = forward_pass_batch(X, pesos, biases, funciones_activacion)
print("Salidas de la red neuronal:")
print(salidas)
En este caso, X
es una matriz donde cada fila representa una muestra. El uso de mini-batches mejora la eficiencia computacional y es una práctica común en el entrenamiento de redes neuronales.
Nota sobre dimensiones: Es crucial que las dimensiones de los pesos y las entradas sean compatibles. Si activaciones
tiene forma (n_muestras, n_características)
y pesos[i]
tiene forma (n_unidades_capa, n_características)
, entonces z
tendrá forma (n_muestras, n_unidades_capa)
.
Para organizar mejor el código, podemos encapsular la lógica de la red en una clase:
class RedNeuronal:
def __init__(self, dimensiones, funciones_activacion):
self.funciones_activacion = funciones_activacion
self.pesos = []
self.biases = []
for i in range(len(dimensiones) - 1):
peso = rng.standard_normal((dimensiones[i+1], dimensiones[i])) * 0.01
bias = np.zeros((dimensiones[i+1],))
self.pesos.append(peso)
self.biases.append(bias)
def forward(self, entrada):
activacion = entrada
for i in range(len(self.pesos)):
z = np.dot(activacion, self.pesos[i].T) + self.biases[i]
activacion = self.funciones_activacion[i](z)
return activacion
Utilizamos la clase de la siguiente manera:
# Instanciar la red neuronal
red = RedNeuronal(dimensiones, funciones_activacion)
# Realizar el forward pass
salida = red.forward(x)
print(f"Salida de la red neuronal: {salida}")
La encapsulación en una clase facilita la gestión de los parámetros y la extensión del modelo para incluir métodos adicionales, como el entrenamiento y la evaluación.
Al implementar el algoritmo de forward pass, es esencial tener en cuenta la eficiencia y la correcta vectorización de las operaciones. NumPy permite aprovechar al máximo los recursos computacionales al evitar bucles innecesarios y utilizar operaciones optimizadas.
Además, es importante manejar correctamente las funciones de activación, ya que influyen en la capacidad de la red para aprender representaciones complejas. La elección de la función de activación puede afectar significativamente el rendimiento del modelo.
Por último, aunque aquí hemos implementado una red sencilla, los mismos principios se aplican a arquitecturas más complejas. La comprensión detallada del forward pass es fundamental para el desarrollo y la optimización de modelos de Deep Learning más avanzados.
Implementación del algoritmo de backpropagation
El algoritmo de backpropagation es fundamental para entrenar redes neuronales, permitiendo ajustar los pesos y biases en función del error obtenido en la salida. Este proceso implica calcular las derivadas parciales de la función de pérdida respecto a cada parámetro, aplicando la regla de la cadena para propagar el error desde la salida hacia las capas anteriores.
En primer lugar, es necesario definir una función de pérdida que cuantifique la diferencia entre la salida real de la red y la esperada. Una de las funciones de pérdida más comunes para problemas de regresión es el Error Cuadrático Medio (MSE), que se define como:
$$
L = \frac{1}{n} \sum_{i=1}^{n} (y^{(i)} - \hat{y}^{(i)})^2
$$
Donde $y^{(i)}$ es el valor esperado y $\hat{y}^{(i)}$ es la salida predicha por la red para la muestra $i$.
Para implementar el backpropagation, se sigue el siguiente flujo:
- Forward pass: calcular la salida de la red para la entrada dada, utilizando los pesos actuales.
- Cálculo del error: evaluar la función de pérdida utilizando la salida predicha y la etiqueta real.
- Backward pass: calcular los gradientes de la función de pérdida respecto a cada parámetro de la red.
- Actualización de parámetros: ajustar los pesos y biases utilizando los gradientes calculados y una tasa de aprendizaje $\alpha$.
El cálculo de gradientes se realiza aplicando la derivada de la función de activación y propagando el error hacia atrás a través de las capas. A continuación, implementamos este proceso en código utilizando NumPy.
Supongamos que tenemos una red neuronal con dos capas ocultas y una capa de salida. Utilizaremos la función de activación ReLU en las capas ocultas y sigmoide en la capa de salida. Definimos, además, las derivadas de estas funciones de activación:
def derivada_relu(x):
return np.where(x > 0, 1, 0)
def derivada_sigmoid(x):
s = 1 / (1 + np.exp(-x))
return s * (1 - s)
En el forward pass, almacenamos las activaciones y los valores intermedios (z) necesarios para el cálculo del backpropagation:
def forward_pass_con_cache(entrada, pesos, biases, funciones_activacion):
activaciones = [entrada]
zs = []
a = entrada
for i in range(len(pesos)):
z = np.dot(a, pesos[i].T) + biases[i]
zs.append(z)
a = funciones_activacion[i](z)
activaciones.append(a)
return activaciones, zs
Ahora, implementamos el backward pass, calculando los gradientes de la función de pérdida respecto a cada peso y bias:
def backward_pass(y_real, activaciones, zs, pesos, funciones_derivada):
grad_pesos = [np.zeros_like(p) for p in pesos]
grad_biases = [np.zeros_like(b) for b in biases]
# Error en la capa de salida
delta = (activaciones[-1] - y_real) * funciones_derivada[-1](zs[-1])
# Gradiente para los pesos y biases de la última capa
grad_pesos[-1] = np.outer(delta, activaciones[-2])
grad_biases[-1] = delta
# Propagación hacia atrás
for l in range(2, len(pesos)+1):
z = zs[-l]
sp = funciones_derivada[-l](z)
delta = np.dot(delta, pesos[-l+1]) * sp
grad_pesos[-l] = np.outer(delta, activaciones[-l-1])
grad_biases[-l] = delta
return grad_pesos, grad_biases
En este código:
y_real
es el valor esperado para la muestra de entrada.activaciones
es una lista de las activaciones en cada capa durante el forward pass.zs
es una lista de los valores intermedios antes de aplicar la función de activación.funciones_derivada
es una lista con las derivadas de las funciones de activación utilizadas en cada capa.
Actualizamos los pesos y biases utilizando los gradientes y una tasa de aprendizaje:
def actualizacion_parametros(pesos, biases, grad_pesos, grad_biases, alpha):
nuevos_pesos = []
nuevos_biases = []
for p, b, gp, gb in zip(pesos, biases, grad_pesos, grad_biases):
nuevos_pesos.append(p - alpha * gp)
nuevos_biases.append(b - alpha * gb)
return nuevos_pesos, nuevos_biases
Definimos las funciones de activación y sus derivadas:
funciones_activacion = [relu, relu, sigmoid]
funciones_derivada = [derivada_relu, derivada_relu, derivada_sigmoid]
Combinamos todo en un ciclo de entrenamiento para una muestra:
# Datos de ejemplo
x = np.array([0.5, -1.2, 0.3, 2.1])
y_real = np.array([1]) # Etiqueta esperada
alpha = 0.01 # Tasa de aprendizaje
# Forward pass con almacenamiento de cache
activaciones, zs = forward_pass_con_cache(x, pesos, biases, funciones_activacion)
# Cálculo de gradientes mediante backward pass
grad_pesos, grad_biases = backward_pass(y_real, activaciones, zs, pesos, funciones_derivada)
# Actualización de parámetros
pesos, biases = actualizacion_parametros(pesos, biases, grad_pesos, grad_biases, alpha)
Este proceso constituye un paso de entrenamiento para una muestra individual. Para entrenar con múltiples muestras, se debe iterar sobre el conjunto de datos o utilizar mini-lotes (mini-batches). Para optimizar el rendimiento, es recomendable vectorizar las operaciones y aprovechar las capacidades de NumPy.
Implementar el backpropagation desde cero permite comprender en detalle cómo los cambios en los pesos afectan la salida de la red y cómo el algoritmo ajusta los parámetros para minimizar la función de pérdida. Además, este conocimiento es crucial para depurar y mejorar modelos más complejos.
Algunos puntos importantes a considerar:
- La correcta inicialización de los pesos es esencial para un buen rendimiento.
- El uso de una tasa de aprendizaje adecuada influye en la rapidez y estabilidad del entrenamiento.
- Es fundamental manejar correctamente las dimensiones de los arrays para evitar errores en las operaciones matriciales.
Para finalizar, se puede encapsular todo el proceso en una clase que maneje el entrenamiento de la red:
class RedNeuronal:
def __init__(self, dimensiones, funciones_activacion, funciones_derivada):
self.funciones_activacion = funciones_activacion
self.funciones_derivada = funciones_derivada
self.pesos = []
self.biases = []
for i in range(len(dimensiones) -1):
peso = rng.standard_normal((dimensiones[i+1], dimensiones[i])) * 0.01
bias = np.zeros((dimensiones[i+1],))
self.pesos.append(peso)
self.biases.append(bias)
def forward_pass(self, entrada):
activaciones = [entrada]
zs = []
a = entrada
for i in range(len(self.pesos)):
z = np.dot(a, self.pesos[i].T) + self.biases[i]
zs.append(z)
a = self.funciones_activacion[i](z)
activaciones.append(a)
return activaciones, zs
def backward_pass(self, y_real, activaciones, zs):
grad_pesos = [np.zeros_like(p) for p in self.pesos]
grad_biases = [np.zeros_like(b) for b in self.biases]
delta = (activaciones[-1] - y_real) * self.funciones_derivada[-1](zs[-1])
grad_pesos[-1] = np.outer(delta, activaciones[-2])
grad_biases[-1] = delta
for l in range(2, len(self.pesos)+1):
z = zs[-l]
sp = self.funciones_derivada[-l](z)
delta = np.dot(delta, self.pesos[-l+1]) * sp
grad_pesos[-l] = np.outer(delta, activaciones[-l-1])
grad_biases[-l] = delta
return grad_pesos, grad_biases
def actualizacion_parametros(self, grad_pesos, grad_biases, alpha):
for i in range(len(self.pesos)):
self.pesos[i] -= alpha * grad_pesos[i]
self.biases[i] -= alpha * grad_biases[i]
def entrenamiento(self, x, y, alpha):
activaciones, zs = self.forward_pass(x)
grad_pesos, grad_biases = self.backward_pass(y, activaciones, zs)
self.actualizacion_parametros(grad_pesos, grad_biases, alpha)
Con esta clase, el proceso de entrenamiento se simplifica:
# Inicializar la red
red = RedNeuronal(dimensiones, funciones_activacion, funciones_derivada)
# Entrenar con una muestra
red.entrenamiento(x, y_real, alpha)
Este enfoque modular facilita la extensión del modelo para incluir características adicionales, como regularización, diferentes funciones de costo o métodos de optimización más avanzados.
En conclusión, la implementación del algoritmo de backpropagation utilizando NumPy permite una comprensión profunda de los mecanismos internos del entrenamiento de las redes neuronales. Mediante el cálculo de los gradientes y la actualización iterativa de los parámetros, la red es capaz de aprender de los datos y mejorar su rendimiento en tareas de predicción.
Entrenamiento y evaluación del modelo
Para entrenar y evaluar nuestro modelo de red neuronal implementado con NumPy, debemos integrar el algoritmo de backpropagation en un ciclo de entrenamiento y aplicar métricas para medir su rendimiento. En esta sección, crearemos un loop de entrenamiento que ajustará los pesos de la red mediante aprendizaje supervisado y evaluaremos su desempeño en un conjunto de datos.
Antes de comenzar, definimos una función de pérdida apropiada para nuestro problema. Si estamos trabajando en un problema de regresión, podemos utilizar el Error Cuadrático Medio (MSE), mientras que para problemas de clasificación binaria, es común utilizar la Entropía Cruzada Binaria. Para este ejemplo, consideraremos un problema de clasificación binaria.
Preparación de los datos
Supongamos que tenemos un conjunto de datos de entrada X
y etiquetas asociadas y
. Para simplificar, generaremos datos sintéticos:
import numpy as np
# Generación de datos sintéticos
rng = np.random.default_rng(seed=42)
n_muestras = 1000
X = rng.standard_normal((n_muestras, 4))
y = (np.sum(X, axis=1) > 0).astype(int) # Etiquetas binarizadas
En este ejemplo, las etiquetas y
son 1 si la suma de las características es positiva y 0 en caso contrario. Nuestro objetivo es entrenar la red neuronal para que aprenda esta regla.
Función de pérdida y métricas
Implementamos la función de pérdida Entropía Cruzada Binaria y una métrica para evaluar el desempeño de la red:
def perdida_entropia_cruzada(y_real, y_pred):
epsilon = 1e-15 # Pequeño valor para estabilidad numérica
y_pred = np.clip(y_pred, epsilon, 1 - epsilon) # Evitar log(0)
perdida = - (y_real * np.log(y_pred) + (1 - y_real) * np.log(1 - y_pred))
return np.mean(perdida)
def precision(y_real, y_pred):
y_pred_clasificado = (y_pred > 0.5).astype(int)
exactitud = np.mean(y_pred_clasificado == y_real)
return exactitud
En la función de pérdida, utilizamos np.clip
para evitar problemas numéricos al calcular el logaritmo de cero. La métrica de precisión nos indicará el porcentaje de predicciones correctas.
Ciclo de entrenamiento
Ahora, incorporamos el ciclo de entrenamiento que actualizará los pesos de la red utilizando el algoritmo de backpropagation:
def entrenar_modelo(red, x_train, y_train, epocas, tasa_aprendizaje, tamano_batch):
n_muestras = x_train.shape[0]
historial_perdida = []
historial_precision = []
for epoca in range(epocas):
indices = rng.permutation(n_muestras)
x_shuffled = x_train[indices]
y_shuffled = y_train[indices]
for inicio in range(0, n_muestras, tamano_batch):
fin = inicio + tamano_batch
x_batch = x_shuffled[inicio:fin]
y_batch = y_shuffled[inicio:fin]
# Forward pass
activaciones, zs = red.forward_pass(x_batch)
# Backward pass y actualización de parámetros
grad_pesos, grad_biases = red.backward_pass(y_batch.reshape(-1, 1), activaciones, zs)
red.actualizacion_parametros(grad_pesos, grad_biases, tasa_aprendizaje)
# Evaluación al final de cada época
y_pred = red.forward_pass(x_train)[0][-1]
perdida = perdida_entropia_cruzada(y_train, y_pred.flatten())
acc = precision(y_train, y_pred.flatten())
historial_perdida.append(perdida)
historial_precision.append(acc)
if (epoca + 1) % 10 == 0 or epoca == 0:
print(f"Época {epoca+1}/{epocas} - Pérdida: {perdida:.4f} - Precisión: {acc:.4f}")
return historial_perdida, historial_precision
En esta función:
- Se realizan múltiples épocas de entrenamiento, donde en cada época se procesan todos los datos.
- Los datos se mezclan antes de cada época para garantizar que los mini-batches sean variados.
- El entrenamiento se realiza en mini-batches, lo que mejora la eficiencia y la convergencia.
- Después de cada época, se calcula la pérdida y la precisión en todo el conjunto de entrenamiento para monitorear el progreso.
Adaptación del modelo para procesamiento por lotes
Modificamos la clase RedNeuronal
para soportar entradas con múltiples muestras:
class RedNeuronal:
def __init__(self, dimensiones, funciones_activacion, funciones_derivada):
self.funciones_activacion = funciones_activacion
self.funciones_derivada = funciones_derivada
self.pesos = []
self.biases = []
for i in range(len(dimensiones) - 1):
peso = rng.standard_normal((dimensiones[i+1], dimensiones[i])) * 0.01
bias = np.zeros((dimensiones[i+1],))
self.pesos.append(peso)
self.biases.append(bias)
def forward_pass(self, entrada):
activaciones = [entrada]
zs = []
a = entrada
for i in range(len(self.pesos)):
z = np.dot(a, self.pesos[i].T) + self.biases[i]
zs.append(z)
a = self.funciones_activacion[i](z)
activaciones.append(a)
return activaciones, zs
def backward_pass(self, y_real, activaciones, zs):
m = y_real.shape[0] # Número de muestras
grad_pesos = [np.zeros_like(p) for p in self.pesos]
grad_biases = [np.zeros_like(b) for b in self.biases]
# Error en la capa de salida
delta = (activaciones[-1] - y_real) * self.funciones_derivada[-1](zs[-1])
grad_pesos[-1] = np.dot(delta.T, activaciones[-2]) / m
grad_biases[-1] = np.mean(delta, axis=0)
# Propagación hacia atrás
for l in range(2, len(self.pesos)+1):
z = zs[-l]
sp = self.funciones_derivada[-l](z)
delta = np.dot(delta, self.pesos[-l+1]) * sp
grad_pesos[-l] = np.dot(delta.T, activaciones[-l-1]) / m
grad_biases[-l] = np.mean(delta, axis=0)
return grad_pesos, grad_biases
def actualizacion_parametros(self, grad_pesos, grad_biases, alpha):
for i in range(len(self.pesos)):
self.pesos[i] -= alpha * grad_pesos[i]
self.biases[i] -= alpha * grad_biases[i]
Los cambios principales son:
- En
forward_pass
, las operaciones trabajan con matrices donde cada fila representa una muestra. - En
backward_pass
, los gradientes se calculan considerando todas las muestras del batch y se promedian.
Entrenamiento del modelo
Configuramos los hiperparámetros y entrenamos la red neuronal:
# Definición de funciones de activación y sus derivadas
def relu(x):
"""Función de activación ReLU."""
return np.maximum(0, x)
def derivada_relu(x):
"""Derivada de la función de activación ReLU."""
return (x > 0).astype(float)
def sigmoid(x):
"""Función de activación Sigmoide."""
return 1 / (1 + np.exp(-x))
def derivada_sigmoid(x):
"""Derivada de la función de activación Sigmoide."""
s = sigmoid(x)
return s * (1 - s)
# Definición de las dimensiones y funciones
dimensiones = [4, 5, 3, 1]
funciones_activacion = [relu, relu, sigmoid]
funciones_derivada = [derivada_relu, derivada_relu, derivada_sigmoid]
# Inicialización del modelo
red = RedNeuronal(dimensiones, funciones_activacion, funciones_derivada)
# Hiperparámetros de entrenamiento
epocas = 100
tasa_aprendizaje = 0.1
tamano_batch = 32
# Entrenamiento
historial_perdida, historial_precision = entrenar_modelo(red, X, y, epocas, tasa_aprendizaje, tamano_batch)
Durante el entrenamiento, el modelo ajusta sus pesos para minimizar la pérdida y mejorar la precisión. La función entrenar_modelo
muestra el progreso cada 10 épocas.
Evaluación del modelo
Después del entrenamiento, evaluamos el modelo en un conjunto de prueba para medir su capacidad de generalización:
# Generación de datos de prueba
n_muestras_prueba = 200
X_prueba = rng.standard_normal((n_muestras_prueba, 4))
y_prueba = (np.sum(X_prueba, axis=1) > 0).astype(int)
# Predicción en datos de prueba
y_pred_prueba = red.forward_pass(X_prueba)[0][-1]
perdida_prueba = perdida_entropia_cruzada(y_prueba, y_pred_prueba.flatten())
precision_prueba = precision(y_prueba, y_pred_prueba.flatten())
print(f"Pérdida en prueba: {perdida_prueba:.4f} - Precisión en prueba: {precision_prueba:.4f}")
La ejecución de este código proporciona una estimación del rendimiento del modelo en nuevos datos no vistos durante el entrenamiento.
Visualización de resultados
Para analizar el proceso de entrenamiento, es útil visualizar la evolución de la pérdida y la precisión:
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(historial_perdida, label='Pérdida de entrenamiento')
plt.xlabel('Épocas')
plt.ylabel('Pérdida')
plt.legend()
plt.subplot(1, 2, 2)
plt.plot(historial_precision, label='Precisión de entrenamiento')
plt.xlabel('Épocas')
plt.ylabel('Precisión')
plt.legend()
plt.show()
La gráfica resultante mostrará si el modelo está convergiendo adecuadamente o si ocurre algún problema como sobreajuste o subajuste.
Consideraciones finales
Al entrenar y evaluar modelos de redes neuronales con NumPy, es importante tener en cuenta:
- Tasa de aprendizaje: un valor demasiado alto puede provocar inestabilidad, mientras que uno muy bajo puede hacer que el entrenamiento sea lento.
- Inicialización de pesos: inicializaciones adecuadas ayudan a una convergencia más rápida.
- Tamaño de batch: balancea entre eficiencia computacional y estabilidad del gradiente.
- Regularización: aunque no se incluyó en este ejemplo, técnicas como dropout o penalizaciones L2 pueden mejorar la generalización.
Este enfoque manual brinda una comprensión profunda de los mecanismos del entrenamiento de redes neuronales y sienta las bases para trabajar con librerías más sofisticadas como TensorFlow.
Todas las lecciones de TensorFlow
Accede a todas las lecciones de TensorFlow y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción Al Deep Learning Y Redes Neuronales
Introducción Y Entorno
Introducción A Tensorflow
Introducción Y Entorno
Introducción A Keras
Introducción Y Entorno
Redes Neuronales De Múltiples Capas
Introducción Y Entorno
Algoritmo De Backpropagation
Introducción Y Entorno
Implementación De Una Red Neuronal Con Numpy
Introducción Y Entorno
Modelo Con Api Secuencial
Construcción De Modelos Con Keras
Modelo Con Api Funcional
Construcción De Modelos Con Keras
Subclases De Modelos
Construcción De Modelos Con Keras
Capas En Keras
Construcción De Modelos Con Keras
Funciones De Activación
Construcción De Modelos Con Keras
Redes Neuronales Densas De Regresión
Construcción De Modelos Con Keras
Redes Neuronales Densas De Clasificación Binaria
Construcción De Modelos Con Keras
Redes Neuronales Densas De Clasificación Multiclase
Construcción De Modelos Con Keras
Redes Convolucionales Cnn
Construcción De Modelos Con Keras
Redes Recurrentes Rnn
Construcción De Modelos Con Keras
Redes Neuronales Mixtas
Construcción De Modelos Con Keras
Api Dataset
Procesamiento De Datos
Manejo De Valores Faltantes
Procesamiento De Datos
Encoding De Valores Categóricos En Continuos
Procesamiento De Datos
Preprocesados De Escalado, Normalización Y Estandarización
Procesamiento De Datos
Generación De Nuevas Características
Procesamiento De Datos
Algoritmos De Optimización
Entrenamiento Y Evaluación De Modelos
Técnicas De Validación
Entrenamiento Y Evaluación De Modelos
Monitorización De Entrenamiento
Entrenamiento Y Evaluación De Modelos
Redes Generativas Adversariales Gans
Técnicas Avanzadas
Transformers
Técnicas Avanzadas
Autoencoders
Técnicas Avanzadas
Carga De Capas Ya Hechas
Técnicas Avanzadas
Regularización De Modelos
Herramientas Y Optimización
Hiperparámetros Con Keras Tuner
Herramientas Y Optimización
Tensorboard
Herramientas Y Optimización
Uso De Tensorflow Keras En Gpu
Herramientas Y Optimización
Objetivos de aprendizaje de esta lección
- Comprender cómo utilizar arrays y matrices de NumPy para representar datos y parámetros de redes neuronales.
- Implementar cálculos vectorizados para operaciones comunes como suma ponderada y funciones de activación.
- Usar NumPy para inicializar pesos y biases de redes neuronales.
- Construir y operar neuronas y capas simples utilizando operaciones matriciales.
- Integrar NumPy en el ciclo completo de forward pass y backpropagation.