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