""" 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"))