Spaces:
Sleeping
Sleeping
Download app.py from Calamar49/Perceptron: direct link, hf CLI and curl.
- Browser
- Download file 9.56 kB
-
https://huggingface.co/spaces/Calamar49/Perceptron/resolve/main/app.py
- Command line
-
hf download hf://spaces/Calamar49/Perceptron/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Calamar49/Perceptron/resolve/main/app.py
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" | |
| 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")) | |