Manas281 commited on
Commit
1c26de1
Β·
verified Β·
1 Parent(s): 4d6576a

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +363 -34
src/streamlit_app.py CHANGED
@@ -1,40 +1,369 @@
1
- import altair as alt
 
 
 
 
 
 
2
  import numpy as np
3
- import pandas as pd
 
4
  import streamlit as st
 
5
 
6
- """
7
- # Welcome to Streamlit!
 
8
 
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
 
 
12
 
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
1
+ """
2
+ πŸŒ€ Two-Spiral Neural Network Classifier β€” Streamlit App
3
+ ========================================================
4
+ Interactive exploration of learning non-linear decision boundaries
5
+ using shallow neural networks on the classic Two-Spiral problem.
6
+ """
7
+
8
  import numpy as np
9
+ import matplotlib.pyplot as plt
10
+ import matplotlib.colors as mcolors
11
  import streamlit as st
12
+ import time, io
13
 
14
+ # ──────────────────────────────────────────────────────────────
15
+ # CONFIGURATION & PAGE SETUP
16
+ # ──────────────────────────────────────────────────────────────
17
 
18
+ st.set_page_config(
19
+ page_title="πŸŒ€ Two-Spiral NN Classifier",
20
+ page_icon="πŸŒ€",
21
+ layout="wide",
22
+ )
23
 
24
+ # Custom CSS for a polished UI
25
+ st.markdown("""
26
+ <style>
27
+ /* Main background */
28
+ .stApp {
29
+ background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
30
+ }
31
+ /* Sidebar */
32
+ section[data-testid="stSidebar"] {
33
+ background: rgba(15, 12, 41, 0.92);
34
+ }
35
+ /* Card-like containers */
36
+ div[data-testid="stVerticalBlock"] > div {
37
+ border-radius: 12px;
38
+ }
39
+ /* Headers */
40
+ h1, h2, h3 {
41
+ color: #e0e0ff !important;
42
+ }
43
+ /* Metric labels */
44
+ [data-testid="stMetricLabel"] {
45
+ color: #b0b0e0 !important;
46
+ }
47
+ [data-testid="stMetricValue"] {
48
+ color: #ffffff !important;
49
+ }
50
+ </style>
51
+ """, unsafe_allow_html=True)
52
+
53
+ # ──────────────────────────────────────────────────────────────
54
+ # UTILITY FUNCTIONS
55
+ # ──────────────────────────────────────────────────────────────
56
+
57
+ def generate_two_spirals(n_points=200, noise=0.5, n_turns=2, seed=42):
58
+ """Generate the classic two-spiral dataset."""
59
+ rng = np.random.RandomState(seed)
60
+ n = n_points
61
+ theta = np.linspace(0, n_turns * 2 * np.pi, n)
62
+ r = theta
63
+ x1 = r * np.cos(theta) + rng.randn(n) * noise
64
+ y1 = r * np.sin(theta) + rng.randn(n) * noise
65
+ x2 = -r * np.cos(theta) + rng.randn(n) * noise
66
+ y2 = -r * np.sin(theta) + rng.randn(n) * noise
67
+ X = np.vstack([np.column_stack([x1, y1]),
68
+ np.column_stack([x2, y2])])
69
+ y = np.hstack([np.zeros(n), np.ones(n)])
70
+ return X, y
71
+
72
+
73
+ class ShallowNN:
74
+ """A simple NumPy-based shallow Neural Network (1-2 hidden layers)."""
75
+
76
+ def __init__(self, input_size, hidden_size, output_size=1,
77
+ activation="tanh", learning_rate=0.01):
78
+ self.input_size = input_size
79
+ self.hidden_size = hidden_size
80
+ self.output_size = output_size
81
+ self.activation = activation
82
+ self.lr = learning_rate
83
+ self._init_weights()
84
+
85
+ # ── weight initialisation ──────────────────────────────────
86
+ def _init_weights(self):
87
+ scale = np.sqrt(2.0 / self.input_size)
88
+ self.W1 = np.random.randn(self.input_size, self.hidden_size) * scale
89
+ self.b1 = np.zeros((1, self.hidden_size))
90
+ scale2 = np.sqrt(2.0 / self.hidden_size)
91
+ self.W2 = np.random.randn(self.hidden_size, self.output_size) * scale2
92
+ self.b2 = np.zeros((1, self.output_size))
93
+
94
+ # ── activation helpers ─────────────────────────────────────
95
+ def _activate(self, z):
96
+ if self.activation == "tanh":
97
+ return np.tanh(z)
98
+ elif self.activation == "relu":
99
+ return np.maximum(0, z)
100
+ elif self.activation == "sigmoid":
101
+ return 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))
102
+ return np.tanh(z)
103
+
104
+ def _activate_deriv(self, z):
105
+ if self.activation == "tanh":
106
+ t = np.tanh(z)
107
+ return 1 - t ** 2
108
+ elif self.activation == "relu":
109
+ return (z > 0).astype(float)
110
+ elif self.activation == "sigmoid":
111
+ s = 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))
112
+ return s * (1 - s)
113
+ t = np.tanh(z)
114
+ return 1 - t ** 2
115
+
116
+ @staticmethod
117
+ def _sigmoid(z):
118
+ return 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))
119
+
120
+ # ── forward / backward ───────────────────────��─────────────
121
+ def forward(self, X):
122
+ self.z1 = X @ self.W1 + self.b1
123
+ self.a1 = self._activate(self.z1)
124
+ self.z2 = self.a1 @ self.W2 + self.b2
125
+ self.a2 = self._sigmoid(self.z2)
126
+ return self.a2
127
+
128
+ def _loss(self, y_true, y_pred):
129
+ eps = 1e-8
130
+ y_pred = np.clip(y_pred, eps, 1 - eps)
131
+ return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
132
+
133
+ def backward(self, X, y_true, y_pred):
134
+ m = X.shape[0]
135
+ dz2 = y_pred - y_true.reshape(-1, 1)
136
+ dW2 = (self.a1.T @ dz2) / m
137
+ db2 = np.sum(dz2, axis=0, keepdims=True) / m
138
+ dz1 = (dz2 @ self.W2.T) * self._activate_deriv(self.z1)
139
+ dW1 = (X.T @ dz1) / m
140
+ db1 = np.sum(dz1, axis=0, keepdims=True) / m
141
+ self.W2 -= self.lr * dW2
142
+ self.b2 -= self.lr * db2
143
+ self.W1 -= self.lr * dW1
144
+ self.b1 -= self.lr * db1
145
+
146
+ # ── training loop ──────────────────────────────────────────
147
+ def train(self, X, y, epochs=1000, log_every=100):
148
+ losses, accs = [], []
149
+ for ep in range(1, epochs + 1):
150
+ y_pred = self.forward(X)
151
+ loss = self._loss(y, y_pred)
152
+ self.backward(X, y, y_pred)
153
+ if ep % log_every == 0 or ep == 1:
154
+ acc = self.accuracy(X, y)
155
+ losses.append(loss)
156
+ accs.append(acc)
157
+ return losses, accs
158
+
159
+ def predict(self, X):
160
+ return (self.forward(X) >= 0.5).astype(int).flatten()
161
+
162
+ def accuracy(self, X, y):
163
+ return np.mean(self.predict(X) == y) * 100
164
+
165
+
166
+ def plot_dataset(X, y, title="Two-Spiral Dataset", ax=None):
167
+ if ax is None:
168
+ fig, ax = plt.subplots(figsize=(6, 6))
169
+ colors = ['#E74C3C', '#3498DB']
170
+ labels = ['Spiral 0', 'Spiral 1']
171
+ for cls in [0, 1]:
172
+ mask = y == cls
173
+ ax.scatter(X[mask, 0], X[mask, 1], c=colors[cls],
174
+ label=labels[cls], alpha=0.8, s=20,
175
+ edgecolors='white', linewidth=0.3)
176
+ ax.set_title(title, fontsize=13, fontweight='bold', pad=10)
177
+ ax.set_xlabel('$x_1$'); ax.set_ylabel('$x_2$')
178
+ ax.legend(fontsize=9); ax.set_aspect('equal'); ax.grid(True, alpha=0.25)
179
+ return ax
180
+
181
+
182
+ def plot_decision_boundary(nn, X, y, title="Decision Boundary", ax=None):
183
+ if ax is None:
184
+ fig, ax = plt.subplots(figsize=(6, 6))
185
+ h = 0.25
186
+ x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
187
+ y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
188
+ xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
189
+ np.arange(y_min, y_max, h))
190
+ grid = np.c_[xx.ravel(), yy.ravel()]
191
+ Z = nn.predict(grid).reshape(xx.shape)
192
+ cmap_bg = mcolors.LinearSegmentedColormap.from_list(
193
+ "bg", ["#FADBD8", "#D6EAF8"], N=2)
194
+ ax.contourf(xx, yy, Z, alpha=0.4, cmap=cmap_bg, levels=1)
195
+ ax.contour(xx, yy, Z, colors='gray', linewidths=0.5, levels=1)
196
+ plot_dataset(X, y, title=title, ax=ax)
197
+ return ax
198
+
199
+ # ──────────────────────────────────────────────────────────────
200
+ # SIDEBAR β€” HYPER-PARAMETERS
201
+ # ──────────────────────────────────────────────────────────────
202
+
203
+ with st.sidebar:
204
+ st.markdown("## βš™οΈ Hyper-parameters")
205
+ st.markdown("---")
206
+
207
+ n_points = st.slider("Points per spiral", 50, 500, 200, 50)
208
+ noise = st.slider("Noise Οƒ", 0.1, 1.5, 0.4, 0.1)
209
+ n_turns = st.slider("Spiral turns", 1, 4, 2, 1)
210
+ seed = st.number_input("Random seed", value=42, step=1)
211
+
212
+ st.markdown("---")
213
+ hidden_size = st.slider("Hidden-layer neurons", 8, 256, 64, 8)
214
+ activation = st.selectbox("Activation", ["tanh", "relu", "sigmoid"])
215
+ learning_rate = st.select_slider("Learning rate",
216
+ options=[0.001, 0.005, 0.01, 0.05, 0.1, 0.5], value=0.01)
217
+ epochs = st.slider("Epochs", 500, 10000, 3000, 500)
218
+
219
+ st.markdown("---")
220
+ run_btn = st.button("πŸš€ Train network", use_container_width=True)
221
+
222
+ # ──────────────────────────────────────────────────────────────
223
+ # MAIN AREA
224
+ # ──────────────────────────────────────────────────────────────
225
+
226
+ st.markdown("# πŸŒ€ Two-Spiral Neural Network Classifier")
227
+ st.markdown("""
228
+ > **Explore** how a *shallow neural network* learns highly non-linear decision
229
+ > boundaries on the classic **Two-Spiral Problem** introduced by
230
+ > Lang & Witbrock (1988). Adjust hyper-parameters in the sidebar and click
231
+ > **Train network** to watch the model learn.
232
+ """)
233
+
234
+ # Generate data
235
+ X, y = generate_two_spirals(n_points, noise, n_turns, int(seed))
236
+
237
+ # Normalise
238
+ X_mean = X.mean(axis=0)
239
+ X_std = X.std(axis=0)
240
+ X_norm = (X - X_mean) / X_std
241
+
242
+ tab_data, tab_train, tab_analysis = st.tabs(
243
+ ["πŸ“Š Dataset", "πŸ‹οΈ Training", "πŸ”¬ Activation Analysis"])
244
+
245
+ # ── TAB 1 β€” Dataset ───────────────────────────────────────────
246
+ with tab_data:
247
+ col1, col2 = st.columns(2)
248
+ with col1:
249
+ fig1, ax1 = plt.subplots(figsize=(6, 6), facecolor='#1a1a2e')
250
+ ax1.set_facecolor('#1a1a2e')
251
+ ax1.tick_params(colors='white'); ax1.xaxis.label.set_color('white')
252
+ ax1.yaxis.label.set_color('white'); ax1.title.set_color('white')
253
+ for spine in ax1.spines.values(): spine.set_color('#444')
254
+ plot_dataset(X, y, "Two-Spiral Dataset", ax=ax1)
255
+ ax1.legend(facecolor='#2a2a4e', edgecolor='#444', labelcolor='white')
256
+ st.pyplot(fig1)
257
+
258
+ with col2:
259
+ st.markdown("### πŸ“Œ Dataset statistics")
260
+ st.metric("Total samples", f"{len(y)}")
261
+ st.metric("Class 0", f"{int((y==0).sum())}")
262
+ st.metric("Class 1", f"{int((y==1).sum())}")
263
+ st.metric("Feature range (x₁)",
264
+ f"[{X[:,0].min():.2f}, {X[:,0].max():.2f}]")
265
+ st.metric("Feature range (xβ‚‚)",
266
+ f"[{X[:,1].min():.2f}, {X[:,1].max():.2f}]")
267
+ st.info("The two spirals are **completely interleaved** β€” "
268
+ "no linear boundary can separate them.")
269
+
270
+ # ── TAB 2 β€” Training ──────────────────────────────────────────
271
+ with tab_train:
272
+ if run_btn:
273
+ np.random.seed(int(seed))
274
+ nn = ShallowNN(2, hidden_size, activation=activation,
275
+ learning_rate=learning_rate)
276
+
277
+ log_every = max(1, epochs // 50)
278
+ progress_bar = st.progress(0, text="Training …")
279
+ metric_col1, metric_col2 = st.columns(2)
280
+ loss_placeholder = metric_col1.empty()
281
+ acc_placeholder = metric_col2.empty()
282
+
283
+ losses, accs = [], []
284
+ for ep in range(1, epochs + 1):
285
+ y_pred = nn.forward(X_norm)
286
+ loss = nn._loss(y, y_pred)
287
+ nn.backward(X_norm, y, y_pred)
288
+ if ep % log_every == 0 or ep == 1:
289
+ acc = nn.accuracy(X_norm, y)
290
+ losses.append(loss)
291
+ accs.append(acc)
292
+ progress_bar.progress(ep / epochs,
293
+ text=f"Epoch {ep}/{epochs} β€” Loss {loss:.4f} β€” Acc {acc:.1f}%")
294
+ loss_placeholder.metric("Loss", f"{loss:.4f}")
295
+ acc_placeholder.metric("Accuracy", f"{acc:.1f}%")
296
+
297
+ progress_bar.empty()
298
+
299
+ st.success(f"βœ… Training finished β€” **Final accuracy: {accs[-1]:.1f}%**")
300
+
301
+ # Charts
302
+ col_loss, col_acc, col_boundary = st.columns(3)
303
+ with col_loss:
304
+ fig_l, ax_l = plt.subplots(figsize=(5, 4), facecolor='#1a1a2e')
305
+ ax_l.set_facecolor('#1a1a2e')
306
+ ax_l.plot(losses, color='#E74C3C', linewidth=1.5)
307
+ ax_l.set_title("Loss", color='white', fontweight='bold')
308
+ ax_l.set_xlabel("log step", color='white')
309
+ ax_l.tick_params(colors='white')
310
+ for sp in ax_l.spines.values(): sp.set_color('#444')
311
+ st.pyplot(fig_l)
312
+ with col_acc:
313
+ fig_a, ax_a = plt.subplots(figsize=(5, 4), facecolor='#1a1a2e')
314
+ ax_a.set_facecolor('#1a1a2e')
315
+ ax_a.plot(accs, color='#2ECC71', linewidth=1.5)
316
+ ax_a.set_title("Accuracy (%)", color='white', fontweight='bold')
317
+ ax_a.set_xlabel("log step", color='white')
318
+ ax_a.tick_params(colors='white')
319
+ for sp in ax_a.spines.values(): sp.set_color('#444')
320
+ st.pyplot(fig_a)
321
+ with col_boundary:
322
+ fig_b, ax_b = plt.subplots(figsize=(5, 4), facecolor='#1a1a2e')
323
+ ax_b.set_facecolor('#1a1a2e')
324
+ ax_b.tick_params(colors='white'); ax_b.xaxis.label.set_color('white')
325
+ ax_b.yaxis.label.set_color('white'); ax_b.title.set_color('white')
326
+ for sp in ax_b.spines.values(): sp.set_color('#444')
327
+ plot_decision_boundary(nn, X_norm, y, "Decision Boundary", ax=ax_b)
328
+ ax_b.legend(facecolor='#2a2a4e', edgecolor='#444', labelcolor='white')
329
+ st.pyplot(fig_b)
330
+ else:
331
+ st.info("πŸ‘ˆ Click **Train network** in the sidebar to start.")
332
+
333
+ # ── TAB 3 β€” Activation analysis ───────────────────────────────
334
+ with tab_analysis:
335
+ st.markdown("### πŸ”¬ Comparing activation functions")
336
+ st.markdown("Train the same architecture with **tanh**, **relu**, and "
337
+ "**sigmoid** to see which one separates the spirals best.")
338
+ if st.button("▢️ Run comparison", use_container_width=True):
339
+ acts = ["tanh", "relu", "sigmoid"]
340
+ results = {}
341
+ for act in acts:
342
+ np.random.seed(int(seed))
343
+ _nn = ShallowNN(2, hidden_size, activation=act,
344
+ learning_rate=learning_rate)
345
+ _losses, _accs = _nn.train(X_norm, y, epochs=epochs,
346
+ log_every=max(1, epochs // 50))
347
+ results[act] = {"nn": _nn, "losses": _losses, "accs": _accs}
348
+
349
+ cols = st.columns(3)
350
+ for idx, act in enumerate(acts):
351
+ with cols[idx]:
352
+ fig_c, ax_c = plt.subplots(figsize=(5, 5), facecolor='#1a1a2e')
353
+ ax_c.set_facecolor('#1a1a2e')
354
+ ax_c.tick_params(colors='white')
355
+ ax_c.xaxis.label.set_color('white')
356
+ ax_c.yaxis.label.set_color('white')
357
+ ax_c.title.set_color('white')
358
+ for sp in ax_c.spines.values(): sp.set_color('#444')
359
+ plot_decision_boundary(results[act]["nn"], X_norm, y,
360
+ f"{act} β€” {results[act]['accs'][-1]:.1f}%", ax=ax_c)
361
+ ax_c.legend(facecolor='#2a2a4e', edgecolor='#444',
362
+ labelcolor='white')
363
+ st.pyplot(fig_c)
364
+ else:
365
+ st.info("Click **Run comparison** to start the analysis.")
366
 
367
+ # ──────────────────────────────────────────────────────────────
368
+ st.markdown("---")
369
+ st.caption("Built with ❀️ using Streamlit · Two-Spiral classification experiment")