eaglelandsonce commited on
Commit
3276eea
·
verified ·
1 Parent(s): 164440b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +479 -0
app.py ADDED
@@ -0,0 +1,479 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import random
3
+ import tempfile
4
+ from dataclasses import dataclass
5
+
6
+ import gradio as gr
7
+ import matplotlib
8
+
9
+ matplotlib.use("Agg") # headless-friendly for Hugging Face Spaces
10
+ import matplotlib.pyplot as plt
11
+ import numpy as np
12
+ import pandas as pd
13
+ import torch
14
+ import torch.nn as nn
15
+ import torch.nn.functional as F
16
+ from torch.utils.data import DataLoader, TensorDataset
17
+
18
+ import lightning.pytorch as pl
19
+
20
+
21
+ # -----------------------------
22
+ # Data
23
+ # -----------------------------
24
+ @dataclass
25
+ class DataSpec:
26
+ n_samples: int = 1024
27
+ n_features: int = 10
28
+ noise_std: float = 0.3
29
+ train_frac: float = 0.8
30
+
31
+
32
+ def set_seed(seed: int) -> None:
33
+ random.seed(seed)
34
+ np.random.seed(seed)
35
+ torch.manual_seed(seed)
36
+ torch.cuda.manual_seed_all(seed)
37
+
38
+
39
+ def make_synthetic_regression(spec: DataSpec, seed: int = 42):
40
+ """
41
+ Synthetic regression:
42
+ y = X @ w_true + b_true + noise
43
+
44
+ X: (n_samples, 10)
45
+ y: (n_samples, 1)
46
+ """
47
+ set_seed(seed)
48
+
49
+ w_true = torch.randn(spec.n_features, 1) * 2.0
50
+ b_true = torch.randn(1) * 0.5
51
+
52
+ X = torch.randn(spec.n_samples, spec.n_features)
53
+ noise = torch.randn(spec.n_samples, 1) * spec.noise_std
54
+ y = X @ w_true + b_true + noise
55
+
56
+ n_train = int(spec.n_samples * spec.train_frac)
57
+ X_train, y_train = X[:n_train], y[:n_train]
58
+ X_val, y_val = X[n_train:], y[n_train:]
59
+
60
+ return X_train, y_train, X_val, y_val, w_true, b_true
61
+
62
+
63
+ def build_full_dataset_df(X_train, y_train, X_val, y_val) -> pd.DataFrame:
64
+ cols = [f"x{i}" for i in range(10)]
65
+
66
+ train_df = pd.DataFrame(X_train.cpu().numpy(), columns=cols)
67
+ train_df["y"] = y_train.cpu().numpy().reshape(-1)
68
+ train_df["split"] = "train"
69
+
70
+ val_df = pd.DataFrame(X_val.cpu().numpy(), columns=cols)
71
+ val_df["y"] = y_val.cpu().numpy().reshape(-1)
72
+ val_df["split"] = "val"
73
+
74
+ return pd.concat([train_df, val_df], ignore_index=True)
75
+
76
+
77
+ def save_df_to_temp_csv(df: pd.DataFrame) -> str:
78
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv", prefix="synthetic_regression_")
79
+ df.to_csv(tmp.name, index=False)
80
+ return tmp.name
81
+
82
+
83
+ # -----------------------------
84
+ # Plot helper
85
+ # -----------------------------
86
+ def fig_to_image(fig) -> np.ndarray:
87
+ buf = io.BytesIO()
88
+ fig.savefig(buf, format="png", bbox_inches="tight", dpi=160)
89
+ plt.close(fig)
90
+ buf.seek(0)
91
+ return plt.imread(buf)
92
+
93
+
94
+ def plot_losses(train_losses, val_losses, title: str) -> np.ndarray:
95
+ fig = plt.figure()
96
+ if len(train_losses) > 0:
97
+ plt.plot(range(1, len(train_losses) + 1), train_losses, marker="o", label="train")
98
+ if len(val_losses) > 0:
99
+ plt.plot(range(1, len(val_losses) + 1), val_losses, marker="o", label="val")
100
+ plt.xlabel("Epoch")
101
+ plt.ylabel("MSE Loss")
102
+ plt.title(title)
103
+ plt.grid(True, alpha=0.3)
104
+ plt.legend()
105
+ return fig_to_image(fig)
106
+
107
+
108
+ def weights_table(w_true: torch.Tensor, w_learned: torch.Tensor) -> pd.DataFrame:
109
+ rows = []
110
+ for i in range(10):
111
+ wt = float(w_true[i].item())
112
+ wl = float(w_learned[i].item())
113
+ rows.append(
114
+ {"feature": f"x{i}", "w_true": wt, "w_learned": wl, "abs_error": abs(wt - wl)}
115
+ )
116
+ df = pd.DataFrame(rows).round(4)
117
+ df = df.sort_values("abs_error", ascending=False).reset_index(drop=True)
118
+ return df
119
+
120
+
121
+ # -----------------------------
122
+ # Raw PyTorch training
123
+ # -----------------------------
124
+ def train_raw(
125
+ X_train, y_train, X_val, y_val,
126
+ init_state_dict,
127
+ lr: float,
128
+ batch_size: int,
129
+ epochs: int,
130
+ device: torch.device
131
+ ):
132
+ train_loader = DataLoader(
133
+ TensorDataset(X_train, y_train),
134
+ batch_size=batch_size,
135
+ shuffle=False, # fixed order to make raw vs lightning comparable
136
+ num_workers=0,
137
+ )
138
+ val_loader = DataLoader(
139
+ TensorDataset(X_val, y_val),
140
+ batch_size=batch_size,
141
+ shuffle=False,
142
+ num_workers=0,
143
+ )
144
+
145
+ model = nn.Linear(10, 1)
146
+ model.load_state_dict(init_state_dict)
147
+ model.to(device)
148
+
149
+ loss_fn = nn.MSELoss()
150
+ optimizer = torch.optim.SGD(model.parameters(), lr=lr)
151
+
152
+ train_losses, val_losses = [], []
153
+
154
+ for _epoch in range(epochs):
155
+ model.train()
156
+ running, seen = 0.0, 0
157
+
158
+ for x, y in train_loader:
159
+ x, y = x.to(device), y.to(device)
160
+
161
+ optimizer.zero_grad()
162
+ y_pred = model(x)
163
+ loss = loss_fn(y_pred, y)
164
+ loss.backward()
165
+ optimizer.step()
166
+
167
+ bs = x.size(0)
168
+ running += loss.item() * bs
169
+ seen += bs
170
+
171
+ train_losses.append(running / max(seen, 1))
172
+
173
+ model.eval()
174
+ running, seen = 0.0, 0
175
+ with torch.no_grad():
176
+ for x, y in val_loader:
177
+ x, y = x.to(device), y.to(device)
178
+ y_pred = model(x)
179
+ loss = loss_fn(y_pred, y)
180
+ bs = x.size(0)
181
+ running += loss.item() * bs
182
+ seen += bs
183
+
184
+ val_losses.append(running / max(seen, 1))
185
+
186
+ with torch.no_grad():
187
+ w_learned = model.weight.detach().view(-1, 1).cpu()
188
+ b_learned = model.bias.detach().view(1).cpu()
189
+
190
+ return train_losses, val_losses, w_learned, b_learned
191
+
192
+
193
+ # -----------------------------
194
+ # Lightning training
195
+ # -----------------------------
196
+ class LitModel(pl.LightningModule):
197
+ def __init__(self, lr: float, init_state_dict):
198
+ super().__init__()
199
+ self.save_hyperparameters(ignore=["init_state_dict"])
200
+ self.model = nn.Linear(10, 1)
201
+ self.model.load_state_dict(init_state_dict)
202
+ self.lr = lr
203
+
204
+ def forward(self, x):
205
+ return self.model(x)
206
+
207
+ def training_step(self, batch, _batch_idx):
208
+ x, y = batch
209
+ loss = F.mse_loss(self(x), y)
210
+ self.log("train_loss", loss, on_step=False, on_epoch=True)
211
+ return loss
212
+
213
+ def validation_step(self, batch, _batch_idx):
214
+ x, y = batch
215
+ loss = F.mse_loss(self(x), y)
216
+ self.log("val_loss", loss, on_step=False, on_epoch=True)
217
+ return loss
218
+
219
+ def configure_optimizers(self):
220
+ return torch.optim.SGD(self.parameters(), lr=self.lr)
221
+
222
+
223
+ class LossHistoryCallback(pl.Callback):
224
+ def __init__(self):
225
+ self.train_losses = []
226
+ self.val_losses = []
227
+
228
+ def on_train_epoch_end(self, trainer, pl_module):
229
+ m = trainer.callback_metrics
230
+ if "train_loss" in m:
231
+ self.train_losses.append(float(m["train_loss"].detach().cpu().item()))
232
+
233
+ def on_validation_epoch_end(self, trainer, pl_module):
234
+ m = trainer.callback_metrics
235
+ if "val_loss" in m:
236
+ self.val_losses.append(float(m["val_loss"].detach().cpu().item()))
237
+
238
+
239
+ def train_lightning(
240
+ X_train, y_train, X_val, y_val,
241
+ init_state_dict,
242
+ lr: float,
243
+ batch_size: int,
244
+ epochs: int,
245
+ device_choice: str,
246
+ seed: int
247
+ ):
248
+ pl.seed_everything(seed, workers=True)
249
+
250
+ train_loader = DataLoader(
251
+ TensorDataset(X_train, y_train),
252
+ batch_size=batch_size,
253
+ shuffle=False, # fixed order to make raw vs lightning comparable
254
+ num_workers=0,
255
+ )
256
+ val_loader = DataLoader(
257
+ TensorDataset(X_val, y_val),
258
+ batch_size=batch_size,
259
+ shuffle=False,
260
+ num_workers=0,
261
+ )
262
+
263
+ want_cuda = (device_choice == "cuda")
264
+ has_cuda = torch.cuda.is_available()
265
+ using_cuda = want_cuda and has_cuda
266
+ accelerator = "gpu" if using_cuda else "cpu"
267
+
268
+ model = LitModel(lr=lr, init_state_dict=init_state_dict)
269
+ history = LossHistoryCallback()
270
+
271
+ trainer = pl.Trainer(
272
+ max_epochs=epochs,
273
+ accelerator=accelerator,
274
+ devices=1,
275
+ deterministic=True,
276
+ logger=False,
277
+ enable_checkpointing=False,
278
+ enable_progress_bar=False,
279
+ enable_model_summary=False,
280
+ callbacks=[history],
281
+ )
282
+
283
+ trainer.fit(model, train_dataloaders=train_loader, val_dataloaders=val_loader)
284
+
285
+ with torch.no_grad():
286
+ w_learned = model.model.weight.detach().view(-1, 1).cpu()
287
+ b_learned = model.model.bias.detach().view(1).cpu()
288
+
289
+ return history.train_losses, history.val_losses, w_learned, b_learned, using_cuda
290
+
291
+
292
+ # -----------------------------
293
+ # Run BOTH on same data & same init weights
294
+ # -----------------------------
295
+ def run_both(
296
+ n_samples: int,
297
+ noise_std: float,
298
+ lr: float,
299
+ batch_size: int,
300
+ epochs: int,
301
+ seed: int,
302
+ device_choice: str,
303
+ ):
304
+ # 1) Generate data ONCE
305
+ spec = DataSpec(n_samples=n_samples, n_features=10, noise_std=noise_std, train_frac=0.8)
306
+ X_train, y_train, X_val, y_val, w_true, b_true = make_synthetic_regression(spec, seed=seed)
307
+
308
+ # Preview + CSV download
309
+ preview_n = min(20, X_train.shape[0])
310
+ df_preview = pd.DataFrame(
311
+ X_train[:preview_n].cpu().numpy(),
312
+ columns=[f"x{i}" for i in range(10)]
313
+ )
314
+ df_preview["y"] = y_train[:preview_n].cpu().numpy().reshape(-1)
315
+ df_preview = df_preview.round(4)
316
+
317
+ full_df = build_full_dataset_df(X_train, y_train, X_val, y_val).round(4)
318
+ csv_path = save_df_to_temp_csv(full_df)
319
+
320
+ # 2) Create ONE initial weight state and reuse it for both trainings
321
+ set_seed(seed + 123) # separate seed so "data seed" vs "init seed" is clear & repeatable
322
+ base = nn.Linear(10, 1)
323
+ init_state = base.state_dict()
324
+
325
+ # 3) Train RAW
326
+ if device_choice == "cuda" and torch.cuda.is_available():
327
+ device = torch.device("cuda")
328
+ else:
329
+ device = torch.device("cpu")
330
+
331
+ raw_train_losses, raw_val_losses, raw_w, raw_b = train_raw(
332
+ X_train, y_train, X_val, y_val,
333
+ init_state_dict=init_state,
334
+ lr=lr,
335
+ batch_size=batch_size,
336
+ epochs=epochs,
337
+ device=device
338
+ )
339
+
340
+ raw_loss_img = plot_losses(raw_train_losses, raw_val_losses, "Raw PyTorch (Manual Loop)")
341
+ raw_weights_df = weights_table(w_true.cpu(), raw_w)
342
+
343
+ raw_summary = (
344
+ f"Device: {device}\n"
345
+ f"Final train loss: {raw_train_losses[-1]:.6f}\n"
346
+ f"Final val loss: {raw_val_losses[-1]:.6f}\n\n"
347
+ f"True bias (b_true): {float(b_true.item()):.4f}\n"
348
+ f"Learned bias (raw): {float(raw_b.item()):.4f}\n"
349
+ )
350
+
351
+ # 4) Train LIGHTNING (same data + same init weights)
352
+ lt_train_losses, lt_val_losses, lt_w, lt_b, using_cuda = train_lightning(
353
+ X_train, y_train, X_val, y_val,
354
+ init_state_dict=init_state,
355
+ lr=lr,
356
+ batch_size=batch_size,
357
+ epochs=epochs,
358
+ device_choice=device_choice,
359
+ seed=seed + 999,
360
+ )
361
+
362
+ lt_loss_img = plot_losses(lt_train_losses, lt_val_losses, "Lightning (Trainer.fit)")
363
+ lt_weights_df = weights_table(w_true.cpu(), lt_w)
364
+
365
+ lt_summary = (
366
+ f"Requested device: {device_choice}\n"
367
+ f"Using device: {'cuda' if using_cuda else 'cpu'}\n"
368
+ f"Final train loss: {lt_train_losses[-1]:.6f}\n"
369
+ f"Final val loss: {lt_val_losses[-1]:.6f}\n\n"
370
+ f"True bias (b_true): {float(b_true.item()):.4f}\n"
371
+ f"Learned bias (lightning): {float(lt_b.item()):.4f}\n"
372
+ )
373
+
374
+ raw_snippet = """# Raw PyTorch: manual training loop
375
+ model = nn.Linear(10, 1)
376
+ optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
377
+ loss_fn = nn.MSELoss()
378
+
379
+ for x, y in dataloader:
380
+ optimizer.zero_grad()
381
+ y_pred = model(x)
382
+ loss = loss_fn(y_pred, y)
383
+ loss.backward()
384
+ optimizer.step()
385
+ """
386
+
387
+ lightning_snippet = """# Lightning: training logic organized in a class
388
+ import lightning.pytorch as pl
389
+ import torch.nn as nn
390
+ import torch.nn.functional as F
391
+ import torch
392
+
393
+ class LitModel(pl.LightningModule):
394
+ def __init__(self):
395
+ super().__init__()
396
+ self.model = nn.Linear(10, 1)
397
+
398
+ def training_step(self, batch, _):
399
+ x, y = batch
400
+ return F.mse_loss(self.model(x), y)
401
+
402
+ def configure_optimizers(self):
403
+ return torch.optim.SGD(self.parameters(), lr=0.01)
404
+
405
+ # pl.Trainer(max_epochs=1).fit(LitModel(), dataloader)
406
+ """
407
+
408
+ return (
409
+ df_preview,
410
+ csv_path,
411
+ raw_loss_img, raw_weights_df, raw_summary, raw_snippet,
412
+ lt_loss_img, lt_weights_df, lt_summary, lightning_snippet
413
+ )
414
+
415
+
416
+ # -----------------------------
417
+ # Gradio UI
418
+ # -----------------------------
419
+ with gr.Blocks(title="Raw PyTorch vs Lightning (Same Data)") as demo:
420
+ gr.Markdown(
421
+ """
422
+ # Raw PyTorch vs PyTorch Lightning — Same Data, Same Initialization
423
+
424
+ This Space trains **two versions** of the same model on the **same synthetic dataset**:
425
+
426
+ - **Raw PyTorch**: manual training loop (`zero_grad → forward → loss → backward → step`)
427
+ - **Lightning**: training organized in `LightningModule` + `Trainer.fit(...)`
428
+
429
+ To make comparisons fair:
430
+ - The dataset is generated once per run using the same seed
431
+ - The **initial model weights are copied** so both start identically
432
+ - Batch order is fixed (no shuffle) so both see batches in the same order
433
+ """
434
+ )
435
+
436
+ with gr.Row():
437
+ n_samples = gr.Slider(256, 8192, value=1024, step=256, label="Number of samples")
438
+ noise_std = gr.Slider(0.0, 2.0, value=0.3, step=0.05, label="Noise (std dev)")
439
+
440
+ with gr.Row():
441
+ lr = gr.Slider(1e-4, 1.0, value=0.01, step=1e-4, label="Learning rate (SGD)")
442
+ batch_size = gr.Dropdown([16, 32, 64, 128, 256], value=64, label="Batch size")
443
+
444
+ with gr.Row():
445
+ epochs = gr.Slider(1, 50, value=10, step=1, label="Epochs")
446
+ seed = gr.Number(value=42, precision=0, label="Seed (controls data)")
447
+
448
+ device_choice = gr.Radio(["cpu", "cuda"], value="cpu", label="Device (cuda only if available)")
449
+ run_btn = gr.Button("Generate Data + Train BOTH", variant="primary")
450
+
451
+ with gr.Tab("Data"):
452
+ data_preview = gr.Dataframe(label="First 20 rows of TRAIN split", wrap=True)
453
+ download_file = gr.File(label="Download full dataset CSV (train + val)")
454
+
455
+ with gr.Tab("Raw PyTorch"):
456
+ raw_loss_img = gr.Image(label="Loss Curve (Raw)", type="numpy")
457
+ raw_weights_df = gr.Dataframe(label="Weights: True vs Learned (Raw)", wrap=True)
458
+ raw_summary_txt = gr.Textbox(label="Summary (Raw)", lines=8)
459
+ raw_code = gr.Code(label="Raw loop snippet", language="python")
460
+
461
+ with gr.Tab("Lightning"):
462
+ lt_loss_img = gr.Image(label="Loss Curve (Lightning)", type="numpy")
463
+ lt_weights_df = gr.Dataframe(label="Weights: True vs Learned (Lightning)", wrap=True)
464
+ lt_summary_txt = gr.Textbox(label="Summary (Lightning)", lines=8)
465
+ lt_code = gr.Code(label="Lightning snippet", language="python")
466
+
467
+ run_btn.click(
468
+ fn=run_both,
469
+ inputs=[n_samples, noise_std, lr, batch_size, epochs, seed, device_choice],
470
+ outputs=[
471
+ data_preview,
472
+ download_file,
473
+ raw_loss_img, raw_weights_df, raw_summary_txt, raw_code,
474
+ lt_loss_img, lt_weights_df, lt_summary_txt, lt_code,
475
+ ],
476
+ )
477
+
478
+ if __name__ == "__main__":
479
+ demo.launch()