Spaces:
Sleeping
Sleeping
File size: 9,561 Bytes
b4faa49 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 | """
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"))
|