Tecnología que te entiende
Redes neuronales desde cero: teoría y código en Python
IA

Redes neuronales desde cero: teoría y código en Python

08 Apr 2026 5 min de lectura 828 vistas

Las librerías de alto nivel como TensorFlow y PyTorch ocultan la magia. Aquí construiremos una red neuronal desde cero usando solo NumPy para que entiendas realmente cómo aprende una máquina — sin cajas negras.

¿Qué es una red neuronal?

Una red neuronal artificial es un modelo matemático inspirado en el cerebro humano. Está compuesta por neuronas artificiales organizadas en capas que procesan información de forma encadenada. Cada neurona recibe entradas, las pondera, aplica una función de activación y pasa el resultado a la siguiente capa.

Los tres tipos de capas son:

  • Capa de entrada (input layer) — recibe los datos en bruto
  • Capas ocultas (hidden layers) — procesan y transforman la información
  • Capa de salida (output layer) — produce el resultado final (clasificación, predicción)

El Perceptrón: la neurona más simple

Todo empieza con el perceptrón, una sola neurona que hace una cosa: decidir si una entrada activa una salida o no.

import numpy as np

class Perceptron:
    def __init__(self, tasa_aprendizaje=0.01, epocas=1000):
        self.lr = tasa_aprendizaje
        self.epocas = epocas
        self.pesos = None
        self.sesgo = None

    def activacion(self, x):
        return 1 if x >= 0 else 0

    def entrenar(self, X, y):
        n_muestras, n_features = X.shape
        self.pesos = np.zeros(n_features)
        self.sesgo = 0

        for _ in range(self.epocas):
            for xi, yi in zip(X, y):
                prediccion = self.activacion(np.dot(xi, self.pesos) + self.sesgo)
                error = yi - prediccion
                self.pesos += self.lr * error * xi
                self.sesgo  += self.lr * error

    def predecir(self, X):
        return [self.activacion(np.dot(x, self.pesos) + self.sesgo) for x in X]

# Probar con compuerta lógica AND
X = np.array([[0,0],[0,1],[1,0],[1,1]])
y = np.array([0, 0, 0, 1])

p = Perceptron()
p.entrenar(X, y)
print(p.predecir(X))  # [0, 0, 0, 1] ✅

Funciones de activación

La función de activación determina si una neurona "se activa" y qué tan fuerte. Las más importantes:

import numpy as np

# Escalón (Perceptrón clásico) — solo 0 o 1
def escalon(x):
    return np.where(x >= 0, 1, 0)

# Sigmoide — salida entre 0 y 1, ideal para probabilidades
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

# ReLU — la más usada en redes profundas modernas
def relu(x):
    return np.maximum(0, x)

# Softmax — para clasificación multiclase (última capa)
def softmax(x):
    e_x = np.exp(x - np.max(x))
    return e_x / e_x.sum()
💡 ¿Cuándo usar cada una? ReLU en capas ocultas (rápida, evita el problema de gradiente desvaneciente). Sigmoid o Softmax en la capa de salida según sea clasificación binaria o multiclase respectivamente.

Red Neuronal completa con backpropagation

Aquí la implementación completa de una red de 2 capas desde cero — incluyendo la propagación hacia adelante (forward pass) y el algoritmo de retropropagación (backpropagation) que es donde ocurre el aprendizaje:

import numpy as np

class RedNeuronal:
    def __init__(self, capa_entrada, capa_oculta, capa_salida, lr=0.01):
        # Inicializar pesos con valores pequeños aleatorios
        self.W1 = np.random.randn(capa_entrada, capa_oculta) * 0.01
        self.b1 = np.zeros((1, capa_oculta))
        self.W2 = np.random.randn(capa_oculta, capa_salida) * 0.01
        self.b2 = np.zeros((1, capa_salida))
        self.lr = lr

    def sigmoid(self, x):
        return 1 / (1 + np.exp(-x))

    def sigmoid_deriv(self, x):
        s = self.sigmoid(x)
        return s * (1 - s)

    def forward(self, X):
        # Capa 1
        self.z1 = np.dot(X, self.W1) + self.b1
        self.a1 = self.sigmoid(self.z1)
        # Capa 2 (salida)
        self.z2 = np.dot(self.a1, self.W2) + self.b2
        self.a2 = self.sigmoid(self.z2)
        return self.a2

    def backward(self, X, y):
        m = X.shape[0]

        # Gradiente de la capa de salida
        dz2 = self.a2 - y
        dW2 = np.dot(self.a1.T, dz2) / m
        db2 = np.sum(dz2, axis=0, keepdims=True) / m

        # Gradiente de la capa oculta
        dz1 = np.dot(dz2, self.W2.T) * self.sigmoid_deriv(self.z1)
        dW1 = np.dot(X.T, dz1) / m
        db1 = np.sum(dz1, axis=0, keepdims=True) / m

        # Actualizar pesos (descenso de gradiente)
        self.W2 -= self.lr * dW2
        self.b2 -= self.lr * db2
        self.W1 -= self.lr * dW1
        self.b1 -= self.lr * db1

    def entrenar(self, X, y, epocas=10000):
        for epoca in range(epocas):
            self.forward(X)
            self.backward(X, y)
            if epoca % 1000 == 0:
                perdida = np.mean((self.a2 - y) ** 2)
                print(f"Época {epoca} — Pérdida: {perdida:.4f}")

    def predecir(self, X):
        return (self.forward(X) > 0.5).astype(int)

# Ejemplo: aprender la compuerta XOR (imposible con perceptrón simple)
X = np.array([[0,0],[0,1],[1,0],[1,1]])
y = np.array([[0],[1],[1],[0]])

red = RedNeuronal(capa_entrada=2, capa_oculta=4, capa_salida=1, lr=0.1)
red.entrenar(X, y, epocas=10000)
print(red.predecir(X))  # [[0],[1],[1],[0]] ✅

¿Qué acaba de pasar?

El algoritmo de backpropagation calcula cuánto contribuyó cada peso al error total usando la regla de la cadena del cálculo diferencial. Luego ajusta todos los pesos en la dirección que reduce ese error. Repitiendo este proceso miles de veces, la red "aprende" los patrones en los datos.

⚠️ Sobre la inicialización de pesos: Inicializar todos los pesos en cero hace que todas las neuronas aprendan lo mismo — la red nunca converge. Por eso inicializamos con valores aleatorios pequeños (* 0.01).

El siguiente paso: usar PyTorch

Una vez que entiendes los fundamentos, las librerías de alto nivel cobran sentido real. El mismo XOR en PyTorch:

import torch
import torch.nn as nn

X = torch.FloatTensor([[0,0],[0,1],[1,0],[1,1]])
y = torch.FloatTensor([[0],[1],[1],[0]])

modelo = nn.Sequential(
    nn.Linear(2, 4),
    nn.Sigmoid(),
    nn.Linear(4, 1),
    nn.Sigmoid()
)

optimizer = torch.optim.SGD(modelo.parameters(), lr=0.1)
criterio  = nn.MSELoss()

for epoca in range(5000):
    prediccion = modelo(X)
    perdida = criterio(prediccion, y)
    optimizer.zero_grad()
    perdida.backward()
    optimizer.step()
"Usar TensorFlow sin entender backpropagation es como manejar un auto sin saber qué es un motor. Puedes llegar a donde quieres, pero no sabrás qué hacer cuando algo falle."