Phase 03 - Lesson 01

El Perceptron

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

El perceptron es el atomo de las redes neuronales. Abrelo y encontraras pesos, un sesgo y una decision.

Tipo: Build Lenguajes: Python Requisitos previos: Fase 1 (Intuicion de Algebra Lineal) Tiempo: ~60 minutos

Objetivos de Aprendizaje

  • Implementar un perceptron desde cero en Python, incluyendo la regla de actualizacion de pesos y la funcion de activacion escalon
  • Explicar por que un solo perceptron solo puede resolver problemas linealmente separables y demostrar el caso de fallo del XOR
  • Construir un perceptron multicapa componiendo compuertas OR, NAND y AND para resolver el XOR
  • Entrenar una red de dos capas con activacion sigmoide y retropropagacion para aprender el XOR automaticamente

El Problema

Conoces los vectores y los productos punto. Sabes que una matriz transforma entradas en salidas. Pero, como una maquina aprende que transformacion usar?

El perceptron responde a esto. Es la maquina de aprendizaje mas simple posible: toma algunas entradas, multiplicalas por pesos, suma un sesgo y toma una decision binaria. Luego ajusta. Eso es todo. Cada red neuronal jamas construida es capas de esta idea apiladas juntas.

Entender el perceptron significa entender lo que "aprender" realmente significa en codigo: ajustar numeros hasta que la salida coincida con la realidad.

El Concepto

Una Neurona, Una Decision

Un perceptron toma n entradas, multiplica cada una por un peso, las suma, agrega un sesgo y pasa el resultado por una funcion de activacion.

graph LR
    x1["x1"] -- "w1" --> sum["Σ(wi*xi) + b"]
    x2["x2"] -- "w2" --> sum
    x3["x3"] -- "w3" --> sum
    bias["sesgo"] --> sum
    sum --> step["step(z)"]
    step --> out["salida (0 o 1)"]

La funcion escalon es brutal: si la suma ponderada mas el sesgo es >= 0, la salida es 1. De lo contrario, la salida es 0.

step(z) = 1  if z >= 0
           0  if z < 0

Esto es un clasificador lineal. Los pesos y el sesgo definen una recta (o un hiperplano en dimensiones mayores) que divide el espacio de entrada en dos regiones.

La Frontera de Decision

Para dos entradas, el perceptron traza una recta a traves del espacio 2D:

  x2
  ┤
  │  Class 1        /
  │    (0)          /
  │                /
  │               / w1·x1 + w2·x2 + b = 0
  │              /
  │             /     Class 2
  │            /        (1)
  ┼───────────/──────────── x1

Todo lo que esta de un lado de la recta produce 0. Todo lo que esta del otro lado produce 1. El entrenamiento mueve esta recta hasta que separa correctamente las clases.

La Regla de Aprendizaje

La regla de aprendizaje del perceptron es simple:

For each training example (x, y_true):
    y_pred = predict(x)
    error = y_true - y_pred

    For each weight:
        w_i = w_i + learning_rate * error * x_i
    bias = bias + learning_rate * error

Si la prediccion es correcta, error = 0, nada cambia. Si predice 0 pero deberia ser 1, los pesos aumentan. Si predice 1 pero deberia ser 0, los pesos disminuyen. La tasa de aprendizaje controla el tamano de cada ajuste.

El Problema del XOR

Aqui es donde se rompe. Observa estas compuertas logicas:

AND gate:           OR gate:            XOR gate:
x1  x2  out         x1  x2  out         x1  x2  out
0   0   0           0   0   0           0   0   0
0   1   0           0   1   1           0   1   1
1   0   0           1   0   1           1   0   1
1   1   1           1   1   1           1   1   0

AND y OR son linealmente separables: puedes trazar una sola recta para separar los 0 de los 1. El XOR no lo es. Ninguna recta por si sola puede separar [0,1] y [1,0] de [0,0] y [1,1].

AND (separable):        XOR (not separable):

  x2                      x2
  1 ┤  0     1            1 ┤  1     0
    │     /                 │
  0 ┤  0 / 0              0 ┤  0     1
    ┼──/──────── x1         ┼──────────── x1
       line works!          no single line works!

Este es un limite fundamental. Un solo perceptron solo puede resolver problemas linealmente separables. Minsky y Papert lo demostraron en 1969 y casi acaban con la investigacion en redes neuronales por una decada.

La solucion: apilar perceptrones en capas. Un perceptron multicapa puede resolver el XOR combinando dos decisiones lineales en una no lineal.

Construyelo

Paso 1: La clase Perceptron

class Perceptron:
    def __init__(self, n_inputs, learning_rate=0.1):
        self.weights = [0.0] * n_inputs
        self.bias = 0.0
        self.lr = learning_rate

    def predict(self, inputs):
        total = sum(w * x for w, x in zip(self.weights, inputs))
        total += self.bias
        return 1 if total >= 0 else 0

    def train(self, training_data, epochs=100):
        for epoch in range(epochs):
            errors = 0
            for inputs, target in training_data:
                prediction = self.predict(inputs)
                error = target - prediction
                if error != 0:
                    errors += 1
                    for i in range(len(self.weights)):
                        self.weights[i] += self.lr * error * inputs[i]
                    self.bias += self.lr * error
            if errors == 0:
                print(f"Converged at epoch {epoch + 1}")
                return
        print(f"Did not converge after {epochs} epochs")

Paso 2: Entrenar en compuertas logicas

and_data = [
    ([0, 0], 0),
    ([0, 1], 0),
    ([1, 0], 0),
    ([1, 1], 1),
]

or_data = [
    ([0, 0], 0),
    ([0, 1], 1),
    ([1, 0], 1),
    ([1, 1], 1),
]

not_data = [
    ([0], 1),
    ([1], 0),
]

print("=== AND Gate ===")
p_and = Perceptron(2)
p_and.train(and_data)
for inputs, _ in and_data:
    print(f"  {inputs} -> {p_and.predict(inputs)}")

print("\n=== OR Gate ===")
p_or = Perceptron(2)
p_or.train(or_data)
for inputs, _ in or_data:
    print(f"  {inputs} -> {p_or.predict(inputs)}")

print("\n=== NOT Gate ===")
p_not = Perceptron(1)
p_not.train(not_data)
for inputs, _ in not_data:
    print(f"  {inputs} -> {p_not.predict(inputs)}")

Paso 3: Observa fallar el XOR

xor_data = [
    ([0, 0], 0),
    ([0, 1], 1),
    ([1, 0], 1),
    ([1, 1], 0),
]

print("\n=== XOR Gate (single perceptron) ===")
p_xor = Perceptron(2)
p_xor.train(xor_data, epochs=1000)
for inputs, expected in xor_data:
    result = p_xor.predict(inputs)
    status = "OK" if result == expected else "WRONG"
    print(f"  {inputs} -> {result} (expected {expected}) {status}")

Nunca convergera. Esta es la prueba contundente de que un solo perceptron no puede aprender el XOR.

Paso 4: Resolver el XOR con dos capas

El truco: XOR = (x1 OR x2) AND NOT (x1 AND x2). Combina tres perceptrones:

graph LR
    x1["x1"] --> OR["neurona OR"]
    x1 --> NAND["neurona NAND"]
    x2["x2"] --> OR
    x2 --> NAND
    OR --> AND["neurona AND"]
    NAND --> AND
    AND --> out["salida"]
def xor_network(x1, x2):
    or_neuron = Perceptron(2)
    or_neuron.weights = [1.0, 1.0]
    or_neuron.bias = -0.5

    nand_neuron = Perceptron(2)
    nand_neuron.weights = [-1.0, -1.0]
    nand_neuron.bias = 1.5

    and_neuron = Perceptron(2)
    and_neuron.weights = [1.0, 1.0]
    and_neuron.bias = -1.5

    hidden1 = or_neuron.predict([x1, x2])
    hidden2 = nand_neuron.predict([x1, x2])
    output = and_neuron.predict([hidden1, hidden2])
    return output


print("\n=== XOR Gate (multi-layer network) ===")
for inputs, expected in xor_data:
    result = xor_network(inputs[0], inputs[1])
    print(f"  {inputs} -> {result} (expected {expected})")

Los cuatro casos correctos. Apilar perceptrones en capas crea fronteras de decision que ningun perceptron aislado puede producir.

Paso 5: Entrenar una Red de Dos Capas

El Paso 4 cableo los pesos a mano. Eso funciona para el XOR, pero no para problemas reales donde no conoces los pesos correctos de antemano. La solucion: reemplazar la funcion escalon por la sigmoide y aprender los pesos automaticamente mediante retropropagacion.

class TwoLayerNetwork:
    def __init__(self, learning_rate=0.5):
        import random
        random.seed(0)
        self.w_hidden = [[random.uniform(-1, 1), random.uniform(-1, 1)] for _ in range(2)]
        self.b_hidden = [random.uniform(-1, 1), random.uniform(-1, 1)]
        self.w_output = [random.uniform(-1, 1), random.uniform(-1, 1)]
        self.b_output = random.uniform(-1, 1)
        self.lr = learning_rate

    def sigmoid(self, x):
        import math
        x = max(-500, min(500, x))
        return 1.0 / (1.0 + math.exp(-x))

    def forward(self, inputs):
        self.inputs = inputs
        self.hidden_outputs = []
        for i in range(2):
            z = sum(w * x for w, x in zip(self.w_hidden[i], inputs)) + self.b_hidden[i]
            self.hidden_outputs.append(self.sigmoid(z))
        z_out = sum(w * h for w, h in zip(self.w_output, self.hidden_outputs)) + self.b_output
        self.output = self.sigmoid(z_out)
        return self.output

    def train(self, training_data, epochs=10000):
        for epoch in range(epochs):
            total_error = 0
            for inputs, target in training_data:
                output = self.forward(inputs)
                error = target - output
                total_error += error ** 2

                d_output = error * output * (1 - output)

                saved_w_output = self.w_output[:]
                hidden_deltas = []
                for i in range(2):
                    h = self.hidden_outputs[i]
                    hd = d_output * saved_w_output[i] * h * (1 - h)
                    hidden_deltas.append(hd)

                for i in range(2):
                    self.w_output[i] += self.lr * d_output * self.hidden_outputs[i]
                self.b_output += self.lr * d_output

                for i in range(2):
                    for j in range(len(inputs)):
                        self.w_hidden[i][j] += self.lr * hidden_deltas[i] * inputs[j]
                    self.b_hidden[i] += self.lr * hidden_deltas[i]
net = TwoLayerNetwork(learning_rate=2.0)
net.train(xor_data, epochs=10000)
for inputs, expected in xor_data:
    result = net.forward(inputs)
    predicted = 1 if result >= 0.5 else 0
    print(f"  {inputs} -> {result:.4f} (rounded: {predicted}, expected {expected})")

Dos diferencias clave respecto al Paso 4. Primera, la sigmoide reemplaza la funcion escalon -- es suave, por lo que existen gradientes. Segunda, el metodo train propaga el error hacia atras desde la salida hasta la capa oculta, ajustando cada peso proporcionalmente a su contribucion al error. Eso es retropropagacion en 20 lineas.

Este es el puente hacia la Leccion 03. La matematica detras de d_output y hidden_deltas es la regla de la cadena aplicada al grafo de la red. La derivaremos correctamente alli.

Usalo

Todo lo que acabas de construir desde cero existe en un solo import:

from sklearn.linear_model import Perceptron as SkPerceptron
import numpy as np

X = np.array([[0,0],[0,1],[1,0],[1,1]])
y = np.array([0, 0, 0, 1])

clf = SkPerceptron(max_iter=100, tol=1e-3)
clf.fit(X, y)
print([clf.predict([x])[0] for x in X])

Cinco lineas. Tu clase Perceptron de 30 lineas hace lo mismo. La version de sklearn agrega verificaciones de convergencia, varias funciones de perdida y soporte para entradas dispersas -- pero el bucle central es identico: suma ponderada, funcion escalon, actualizacion de pesos ante el error.

La diferencia real aparece a escala. Lo que cambia en las redes de produccion:

  • La funcion escalon se vuelve sigmoide, ReLU u otras activaciones suaves
  • Los pesos se aprenden automaticamente mediante retropropagacion (Leccion 03)
  • Las capas se hacen mas profundas: 3, 10, mas de 100 capas
  • El mismo principio se mantiene: cada capa crea nuevas features a partir de las salidas de la capa anterior

Un solo perceptron solo puede trazar rectas. Apilalos y podras dibujar cualquier forma.

Entregalo

Esta leccion produce:

  • outputs/skill-perceptron.md - una skill que cubre cuando se necesitan arquitecturas de una capa frente a multicapa

Ejercicios

  1. Entrena un perceptron en una compuerta NAND (la compuerta universal - cualquier circuito logico puede construirse a partir de NAND). Verifica que sus pesos y sesgo formen una frontera de decision valida.
  2. Modifica la clase Perceptron para rastrear la frontera de decision (w1x1 + w2x2 + b = 0) en cada epoca. Imprime como se desplaza la recta durante el entrenamiento en la compuerta AND.
  3. Construye un perceptron de 3 entradas que produzca 1 solo cuando al menos 2 de las 3 entradas sean 1 (una funcion de voto mayoritario). Es linealmente separable? Por que?

Terminos Clave

Termino Lo que la gente dice Lo que realmente significa
Perceptron "Una neurona falsa" Un clasificador lineal: producto punto de entradas y pesos, mas sesgo, a traves de una funcion escalon
Peso "Que tan importante es una entrada" Un multiplicador que escala la contribucion de cada entrada a la decision
Sesgo "El umbral" Una constante que desplaza la frontera de decision, permitiendo que el perceptron se active incluso con entradas cero
Funcion de activacion "La cosa que comprime valores" Una funcion aplicada despues de la suma ponderada - funcion escalon para perceptrones, sigmoide/ReLU para redes modernas
Linealmente separable "Puedes trazar una recta entre ellos" Un conjunto de datos donde un solo hiperplano puede separar perfectamente las clases
Problema del XOR "Lo que los perceptrones no pueden hacer" Prueba de que las redes de una capa no pueden aprender funciones no linealmente separables
Frontera de decision "Donde el clasificador cambia" El hiperplano w*x + b = 0 que divide el espacio de entrada en dos clases
Perceptron multicapa "Una red neuronal de verdad" Perceptrones apilados en capas, donde la salida de cada capa alimenta la entrada de la siguiente

Lectura Adicional

  • Frank Rosenblatt, "The Perceptron: A Probabilistic Model for Information Storage and Organization in the Brain" (1958) -- el articulo original que lo inicio todo
  • Minsky & Papert, "Perceptrons" (1969) -- el libro que demostro que el XOR era irresoluble por redes de una capa y acabo con la investigacion en perceptrones por una decada
  • Michael Nielsen, "Neural Networks and Deep Learning", Capitulo 1 (http://neuralnetworksanddeeplearning.com/) -- gratuito en linea, la mejor explicacion visual de como los perceptrones se componen en redes
0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).