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
- 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.
- 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.
- 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