Perceptron / app.py
Calamar49's picture
Upload 3 files
b4faa49 verified
Raw History Blame Contribute Delete
9.56 kB
"""
Simulador del Perceptron — Gradio
Ejecuta la clase Perceptron real (NumPy puro, sin scikit-learn) en vivo.
Correr localmente: python app.py
Desplegar: Hugging Face Spaces, SDK "Gradio", subiendo estos
archivos por arrastrar-y-soltar (sin git). Ver requirements.txt.
"""
import gradio as gr
import matplotlib.pyplot as plt
import numpy as np
import spaces
plt.rcParams["figure.facecolor"] = "#ffffff"
plt.rcParams["axes.facecolor"] = "#ffffff"
# ------------------------------------------------------------------ MODELO
class Perceptron:
"""Perceptron de Rosenblatt, implementado unicamente con NumPy."""
def __init__(self, input_size, learning_rate=0.1, epochs=100):
self.lr = learning_rate
self.epochs = epochs
self.weights = np.zeros(input_size)
self.bias = 0.0
self.history = [] # (epoca, pesos, bias, errores) por epoca
def activation_function(self, z):
return 1 if z >= 0 else 0
def predict(self, x):
z = np.dot(x, self.weights) + self.bias
return self.activation_function(z)
def fit(self, X, y):
self.history = [(0, self.weights.copy(), self.bias, None)]
converged_at = None
for epoch in range(1, self.epochs + 1):
total_errors = 0
for xi, target in zip(X, y):
prediction = self.predict(xi)
error = target - prediction
if error != 0:
self.weights = self.weights + self.lr * error * xi
self.bias = self.bias + self.lr * error
total_errors += 1
self.history.append((epoch, self.weights.copy(), self.bias, total_errors))
if total_errors == 0 and converged_at is None:
converged_at = epoch
break
self.converged_at = converged_at
return self
GATES = {
"AND": np.array([0, 0, 0, 1]),
"OR": np.array([0, 1, 1, 1]),
"XOR": np.array([0, 1, 1, 0]),
}
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
COLOR_C0, COLOR_C1, COLOR_ACCENT, COLOR_DIM = "#1a5fb4", "#c1440e", "#0a8f78", "#d9d0bd"
@spaces.GPU()
def entrenar(gate_name, lr, max_epochs):
# El entrenamiento en si es puro NumPy en CPU -- este decorador solo
# existe para satisfacer el requisito de arranque de ZeroGPU en el
# plan gratuito de Hugging Face Spaces (no cambia el algoritmo).
y = GATES[gate_name]
p = Perceptron(input_size=2, learning_rate=lr, epochs=int(max_epochs))
p.fit(X, y)
return p.history, p.converged_at
def fig_frontera(snapshot, y):
_, w, b, _ = snapshot
fig, ax = plt.subplots(figsize=(4.3, 4.3))
for clase, color, marcador in [(0, COLOR_C0, "o"), (1, COLOR_C1, "s")]:
mascara = y == clase
ax.scatter(X[mascara, 0], X[mascara, 1], c=color, marker=marcador,
s=280, edgecolors="white", linewidths=2, zorder=3, label=f"clase {clase}")
x1_vals = np.linspace(-0.5, 1.5, 100)
if abs(w[1]) > 1e-9:
ax.plot(x1_vals, -(w[0] * x1_vals + b) / w[1], "--", color=COLOR_ACCENT, linewidth=2.5)
elif abs(w[0]) > 1e-9:
ax.axvline(-b / w[0], linestyle="--", color=COLOR_ACCENT, linewidth=2.5)
ax.set_xlim(-0.5, 1.5); ax.set_ylim(-0.5, 1.5)
ax.set_xlabel("x1"); ax.set_ylabel("x2")
ax.set_title("Frontera de decisión")
ax.grid(alpha=0.25)
ax.legend(loc="upper left", bbox_to_anchor=(1.02, 1), frameon=False)
fig.tight_layout()
return fig
def fig_convergencia(history, epoca_actual):
epocas = [h[0] for h in history[1:]]
errores = [h[3] for h in history[1:]]
colores = [COLOR_ACCENT if e == epoca_actual else COLOR_DIM for e in epocas]
fig, ax = plt.subplots(figsize=(4.6, 4.3))
ax.bar(epocas, errores, color=colores, width=0.8)
ax.set_xlabel("Época"); ax.set_ylabel("Errores")
ax.set_ylim(0, max(4, max(errores) if errores else 4))
ax.set_title("Convergencia — errores por época")
ax.grid(alpha=0.25, axis="y")
fig.tight_layout()
return fig
def texto_metricas(snapshot):
_, w, b, errores = snapshot
errores_txt = "–" if errores is None else str(errores)
return (
f"| Peso w₁ | Peso w₂ | Sesgo b | Errores esta época |\n"
f"|---|---|---|---|\n"
f"| **{w[0]:.2f}** | **{w[1]:.2f}** | **{b:.2f}** | **{errores_txt}** |\n"
)
def texto_ecuacion(snapshot):
_, w, b, _ = snapshot
return f"```\nz = {w[0]:.2f}·x1 + {w[1]:.2f}·x2 + ({b:.2f})\n```"
def texto_veredicto(history, converged_at, epoca_idx, gate, max_epochs):
snapshot = history[epoca_idx]
errores = snapshot[3]
total_epocas = len(history) - 1
if converged_at is not None and snapshot[0] >= converged_at:
return f"### ✅ Convergió\nEl Perceptrón encontró una frontera que separa las dos clases sin errores en la época **{converged_at}**. La compuerta **{gate}** es linealmente separable."
if epoca_idx == total_epocas and converged_at is None:
return (
f"### ❌ No converge\nTras **{int(max_epochs)}** épocas, el error sigue sin llegar a cero "
f"(quedó en {errores}). **{gate}** no es linealmente separable — no existe ninguna recta que "
"separe sus dos clases, por eso el Perceptrón simple oscila indefinidamente en vez de converger."
)
return f"### ⏳ Entrenando…\nÉpoca **{snapshot[0]}** de {int(max_epochs)} — {errores} error(es) en esta pasada."
def tabla_lr(gate):
filas = []
for lr_cmp in [0.1, 0.01]:
hist_cmp, conv_cmp = entrenar(gate, lr_cmp, 300)
ultima = hist_cmp[-1]
filas.append([
lr_cmp,
conv_cmp if conv_cmp is not None else "> 300 (no converge)",
round(float(ultima[1][0]), 2),
round(float(ultima[1][1]), 2),
round(float(ultima[2]), 2),
])
return filas
# ------------------------------------------------------------------ CALLBACKS
def on_params_change(gate, lr, max_epochs):
history, converged_at = entrenar(gate, lr, max_epochs)
total_epocas = len(history) - 1
snapshot = history[-1]
return (
history, converged_at,
gr.update(minimum=0, maximum=total_epocas, value=total_epocas),
fig_frontera(snapshot, GATES[gate]),
fig_convergencia(history, snapshot[0]),
texto_metricas(snapshot),
texto_ecuacion(snapshot),
texto_veredicto(history, converged_at, total_epocas, gate, max_epochs),
tabla_lr(gate),
f"### Efecto de la tasa de aprendizaje — {gate}",
)
def on_epoch_change(history, converged_at, epoca_idx, gate, max_epochs):
epoca_idx = int(epoca_idx)
snapshot = history[epoca_idx]
return (
fig_frontera(snapshot, GATES[gate]),
fig_convergencia(history, snapshot[0]),
texto_metricas(snapshot),
texto_ecuacion(snapshot),
texto_veredicto(history, converged_at, epoca_idx, gate, max_epochs),
)
# ------------------------------------------------------------------ UI
TEORIA_MD = """
| Suma ponderada | Activación (escalón) | Regla de aprendizaje |
|---|---|---|
| `z = w1·x1 + w2·x2 + b` | `ŷ = 1 si z≥0, si no 0` | `w += η·e·x` `b += η·e` |
"""
with gr.Blocks(title="Simulador del Perceptrón") as demo:
gr.Markdown("# 🧠 Simulador del Perceptrón")
gr.Markdown("Rosenblatt · 1958 · Mark I Perceptron — la misma clase `Perceptron` en NumPy del notebook, corriendo en vivo.")
with gr.Accordion("📐 El modelo matemático", open=False):
gr.Markdown(TEORIA_MD)
with gr.Row():
gate_radio = gr.Radio(list(GATES.keys()), value="AND", label="Compuerta lógica")
lr_dropdown = gr.Dropdown([0.01, 0.05, 0.1, 0.2, 0.5], value=0.1, label="Tasa de aprendizaje (η)")
epochs_number = gr.Number(value=100, minimum=5, maximum=300, step=5, label="Épocas máx.")
epoch_slider = gr.Slider(minimum=0, maximum=4, value=4, step=1, label="Época")
with gr.Row():
plot_frontera = gr.Plot(label="Frontera de decisión")
plot_convergencia = gr.Plot(label="Convergencia")
metricas_md = gr.Markdown()
ecuacion_md = gr.Markdown()
veredicto_md = gr.Markdown()
lr_titulo_md = gr.Markdown("### Efecto de la tasa de aprendizaje — AND")
lr_tabla = gr.Dataframe(
headers=["η", "Épocas hasta converger", "w1", "w2", "bias"],
interactive=False,
)
gr.Markdown("Mismo algoritmo que `ANALISIS_PERCEPTRON.ipynb` — Taller de Inteligencia Artificial, Perceptrón de Rosenblatt.")
history_state = gr.State()
converged_state = gr.State()
params_outputs = [
history_state, converged_state, epoch_slider,
plot_frontera, plot_convergencia, metricas_md, ecuacion_md, veredicto_md,
lr_tabla, lr_titulo_md,
]
gate_radio.change(on_params_change, [gate_radio, lr_dropdown, epochs_number], params_outputs)
lr_dropdown.change(on_params_change, [gate_radio, lr_dropdown, epochs_number], params_outputs)
epochs_number.change(on_params_change, [gate_radio, lr_dropdown, epochs_number], params_outputs)
epoch_slider.change(
on_epoch_change,
[history_state, converged_state, epoch_slider, gate_radio, epochs_number],
[plot_frontera, plot_convergencia, metricas_md, ecuacion_md, veredicto_md],
)
demo.load(on_params_change, [gate_radio, lr_dropdown, epochs_number], params_outputs)
if __name__ == "__main__":
demo.launch(theme=gr.themes.Soft(primary_hue="teal"))