FlameF0X commited on
Commit
fcd9f8e
·
verified ·
1 Parent(s): 6c9d0cb

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +327 -0
app.py ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.optim as optim
5
+ import numpy as np
6
+ from PIL import Image
7
+ import matplotlib.pyplot as plt
8
+ import io
9
+ import time
10
+
11
+ # =====================================================================
12
+ # 1. MODEL DEFINITION
13
+ # =====================================================================
14
+
15
+ class DynamicMLPAutoencoder(nn.Module):
16
+ """
17
+ A customizable Multi-Layer Perceptron Autoencoder.
18
+ Allows dynamic configuration of input size, hidden layers, and latent bottleneck.
19
+ """
20
+ def __init__(self, input_dim, hidden_dim, latent_dim, num_hidden_layers=1):
21
+ super().__init__()
22
+
23
+ # Build Encoder
24
+ encoder_layers = []
25
+ current_dim = input_dim
26
+
27
+ # Add progressive downscaling hidden layers
28
+ for i in range(num_hidden_layers):
29
+ next_dim = max(hidden_dim // (2 ** i), latent_dim * 2)
30
+ encoder_layers.append(nn.Linear(current_dim, next_dim))
31
+ encoder_layers.append(nn.ReLU())
32
+ current_dim = next_dim
33
+
34
+ encoder_layers.append(nn.Linear(current_dim, latent_dim))
35
+ self.encoder = nn.Sequential(*encoder_layers)
36
+
37
+ # Build Decoder
38
+ decoder_layers = []
39
+ current_dim = latent_dim
40
+
41
+ # Add progressive upscaling hidden layers matching the encoder's reverse path
42
+ for i in reversed(range(num_hidden_layers)):
43
+ next_dim = max(hidden_dim // (2 ** i), latent_dim * 2)
44
+ decoder_layers.append(nn.Linear(current_dim, next_dim))
45
+ decoder_layers.append(nn.ReLU())
46
+ current_dim = next_dim
47
+
48
+ decoder_layers.append(nn.Linear(current_dim, input_dim))
49
+ decoder_layers.append(nn.Sigmoid()) # Clamp output pixels between [0, 1]
50
+ self.decoder = nn.Sequential(*decoder_layers)
51
+
52
+ def forward(self, x):
53
+ latent = self.encoder(x)
54
+ reconstruction = self.decoder(latent)
55
+ return reconstruction, latent
56
+
57
+ # =====================================================================
58
+ # 2. HELPER UTILITIES
59
+ # =====================================================================
60
+
61
+ def preprocess_image(pil_img, resolution):
62
+ """Resizes, converts to tensor, and flattens an image."""
63
+ img_resized = pil_img.resize((resolution, resolution), Image.Resampling.LANCZOS)
64
+ img_np = np.array(img_resized).astype(np.float32) / 255.0
65
+
66
+ # Handle Grayscale / RGBA conversions
67
+ if len(img_np.shape) == 2: # Grayscale to RGB
68
+ img_np = np.stack([img_np] * 3, axis=-1)
69
+ elif img_np.shape[2] == 4: # RGBA to RGB
70
+ img_np = img_np[:, :, :3]
71
+
72
+ img_tensor = torch.tensor(img_np).permute(2, 0, 1).unsqueeze(0) # Shape: [1, 3, H, W]
73
+ return img_tensor, img_resized
74
+
75
+ def postprocess_tensor(tensor, resolution):
76
+ """Converts flattened/raw image tensors back to PIL Images."""
77
+ img_np = tensor.squeeze(0).permute(1, 2, 0).detach().cpu().numpy()
78
+ img_np = np.clip(img_np * 255.0, 0, 255).astype(np.uint8)
79
+ return Image.fromarray(img_np)
80
+
81
+ def add_gaussian_noise(tensor, noise_factor):
82
+ """Adds zero-mean Gaussian noise to the image tensor."""
83
+ if noise_factor <= 0.0:
84
+ return tensor
85
+ noise = torch.randn_like(tensor) * noise_factor
86
+ noisy_tensor = torch.clamp(tensor + noise, 0.0, 1.0)
87
+ return noisy_tensor
88
+
89
+ def create_loss_plot(losses):
90
+ """Generates a matplotlib line plot for training loss history."""
91
+ fig, ax = plt.subplots(figsize=(6, 3))
92
+ ax.plot(losses, color='#4F46E5', linewidth=2, label="Reconstruction Loss")
93
+ ax.set_title("Training Loss Curve", fontsize=11, fontweight='bold', pad=10)
94
+ ax.set_xlabel("Epoch", fontsize=9)
95
+ ax.set_ylabel("Loss (MSE)", fontsize=9)
96
+ ax.grid(True, linestyle='--', alpha=0.5)
97
+ ax.legend(loc="upper right")
98
+ fig.tight_layout()
99
+ return fig
100
+
101
+ # =====================================================================
102
+ # 3. INTERACTIVE TRAINING FUNCTION (GRADIO GENERATOR)
103
+ # =====================================================================
104
+
105
+ def run_autoencoder_sandbox(
106
+ input_image,
107
+ resolution,
108
+ latent_dim,
109
+ hidden_dim,
110
+ num_hidden_layers,
111
+ noise_factor,
112
+ epochs,
113
+ learning_rate,
114
+ optimizer_name,
115
+ progress=gr.Progress()
116
+ ):
117
+ if input_image is None:
118
+ raise gr.Error("Please upload or select an image first!")
119
+
120
+ # 1. Preprocessing
121
+ progress(0, desc="Preprocessing image...")
122
+ img_tensor, original_resized = preprocess_image(input_image, resolution)
123
+
124
+ # Flatten dimensions
125
+ input_dim = 3 * resolution * resolution
126
+ flat_clean = img_tensor.view(1, -1)
127
+
128
+ # 2. Setup Noise (for Denoising mode)
129
+ noisy_img_tensor = add_gaussian_noise(img_tensor, noise_factor)
130
+ flat_input = noisy_img_tensor.view(1, -1)
131
+
132
+ # Pre-render the noisy input for user preview
133
+ noisy_preview = postprocess_tensor(noisy_img_tensor, resolution)
134
+
135
+ # 3. Model Initialization
136
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
137
+ model = DynamicMLPAutoencoder(
138
+ input_dim=input_dim,
139
+ hidden_dim=hidden_dim,
140
+ latent_dim=latent_dim,
141
+ num_hidden_layers=num_hidden_layers
142
+ ).to(device)
143
+
144
+ flat_input = flat_input.to(device)
145
+ flat_clean = flat_clean.to(device)
146
+
147
+ # 4. Optimizer & Loss Config
148
+ if optimizer_name == "Adam":
149
+ optimizer = optim.Adam(model.parameters(), lr=learning_rate)
150
+ else:
151
+ optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9)
152
+
153
+ criterion = nn.MSELoss()
154
+ losses = []
155
+
156
+ # Determine UI update frequency to preserve execution speed
157
+ update_every = max(1, epochs // 40)
158
+
159
+ # 5. Training Loop
160
+ for epoch in range(epochs):
161
+ model.train()
162
+ optimizer.zero_grad()
163
+
164
+ # Forward Pass
165
+ reconstructed, latent = model(flat_input)
166
+
167
+ # Calculate loss (against clean image always, to support denoising autoencoder structure)
168
+ loss = criterion(reconstructed, flat_clean)
169
+
170
+ # Backward Pass & Step
171
+ loss.backward()
172
+ optimizer.step()
173
+
174
+ losses.append(loss.item())
175
+
176
+ # Emit live training updates
177
+ if epoch % update_every == 0 or epoch == epochs - 1:
178
+ progress((epoch + 1) / epochs, desc=f"Epoch {epoch+1}/{epochs} | Loss: {loss.item():.5f}")
179
+
180
+ # Format reconstructed image back to normal shape
181
+ reconstructed_tensor = reconstructed.view(1, 3, resolution, resolution)
182
+ reconstructed_pil = postprocess_tensor(reconstructed_tensor, resolution)
183
+
184
+ # Generate loss graph
185
+ loss_plot = create_loss_plot(losses)
186
+
187
+ status_text = (
188
+ f"### Training Diagnostics\n"
189
+ f"- **Current Epoch:** {epoch + 1} / {epochs}\n"
190
+ f"- **Current Loss (MSE):** `{loss.item():.6f}`\n"
191
+ f"- **Compression Ratio:** `{input_dim} inputs ➜ {latent_dim} bottleneck` (Compacted by **{input_dim / latent_dim:.1f}x**)"
192
+ )
193
+
194
+ # Yield components iteratively for active visual rendering
195
+ yield (
196
+ noisy_preview,
197
+ reconstructed_pil,
198
+ loss_plot,
199
+ status_text
200
+ )
201
+ plt.close(loss_plot) # Cleanup plots to prevent memory overflow
202
+ time.sleep(0.01)
203
+
204
+ # =====================================================================
205
+ # 4. GRADIO APP INTERFACE LAYOUT
206
+ # =====================================================================
207
+
208
+ with gr.Blocks(theme=gr.themes.Soft(), title="Autoencoder Sandbox") as demo:
209
+ gr.Markdown(
210
+ """
211
+ # 🧠 Autoencoder Image Bottleneck Sandbox
212
+ Explore how deep neural networks compress, reconstruct, and denoise raw images. By training an autoencoder on *just* this single image, you can observe how limiting the **latent bottleneck** restricts the reconstruction capability—forcing the network to blur details or filter noise!
213
+ """
214
+ )
215
+
216
+ with gr.Row():
217
+ # --- LEFT SIDEBAR: CONTROLS & ARCHITECTURE ---
218
+ with gr.Column(scale=1):
219
+ gr.Markdown("### 🛠️ Step 1: Input & Parameters")
220
+ input_img = gr.Image(type="pil", label="Upload Source Image", value=None)
221
+
222
+ with gr.Tab("Network Architecture"):
223
+ resolution = gr.Dropdown(
224
+ choices=[32, 64, 128],
225
+ value=64,
226
+ label="Image Training Resolution",
227
+ info="Higher resolutions take longer to train. 64x64 is recommended."
228
+ )
229
+ latent_dim = gr.Slider(
230
+ minimum=1,
231
+ maximum=256,
232
+ value=16,
233
+ step=1,
234
+ label="Latent Dimension (Bottleneck)",
235
+ info="Lower values yield abstract, blurry representations ('not 1:1')."
236
+ )
237
+ hidden_dim = gr.Slider(
238
+ minimum=16,
239
+ maximum=512,
240
+ value=128,
241
+ step=16,
242
+ label="Hidden Dimension",
243
+ info="Size of the intermediate neural network layers."
244
+ )
245
+ num_hidden_layers = gr.Slider(
246
+ minimum=1,
247
+ maximum=3,
248
+ value=1,
249
+ step=1,
250
+ label="Number of Hidden Layers",
251
+ info="Deepens the feature extraction depth."
252
+ )
253
+
254
+ with gr.Tab("Training Configuration"):
255
+ noise_factor = gr.Slider(
256
+ minimum=0.0,
257
+ maximum=1.0,
258
+ value=0.0,
259
+ step=0.05,
260
+ label="Noise Injection (Denoising Mode)",
261
+ info="Introduces random noise to the training inputs. Model learns to clean it!"
262
+ )
263
+ epochs = gr.Slider(
264
+ minimum=20,
265
+ maximum=1500,
266
+ value=400,
267
+ step=20,
268
+ label="Training Epochs",
269
+ info="Total gradient updates."
270
+ )
271
+ learning_rate = gr.Slider(
272
+ minimum=0.0001,
273
+ maximum=0.05,
274
+ value=0.005,
275
+ step=0.0005,
276
+ label="Learning Rate"
277
+ )
278
+ optimizer_name = gr.Radio(
279
+ choices=["Adam", "SGD"],
280
+ value="Adam",
281
+ label="Optimizer Mode"
282
+ )
283
+
284
+ train_btn = gr.Button("🚀 Begin Real-Time Training", variant="primary")
285
+
286
+ # --- RIGHT SIDEBAR: OUTPUTS & METRICS ---
287
+ with gr.Column(scale=2):
288
+ gr.Markdown("### 📊 Step 2: Live Training Output")
289
+
290
+ with gr.Row():
291
+ with gr.Column():
292
+ noisy_output_img = gr.Image(label="Model Input (Noisy / Resized)", interactive=False)
293
+ with gr.Column():
294
+ reconstruction_output_img = gr.Image(label="Live Model Reconstruction", interactive=False)
295
+
296
+ with gr.Row():
297
+ with gr.Column(scale=1):
298
+ diagnostics = gr.Markdown(
299
+ "### Training Diagnostics\n*Click **Begin Real-Time Training** to launch the network optimization loop.*"
300
+ )
301
+ with gr.Column(scale=1.2):
302
+ loss_curve_plot = gr.Plot(label="Live Loss Curve")
303
+
304
+ # Link action to the training function
305
+ train_btn.click(
306
+ fn=run_autoencoder_sandbox,
307
+ inputs=[
308
+ input_img,
309
+ resolution,
310
+ latent_dim,
311
+ hidden_dim,
312
+ num_hidden_layers,
313
+ noise_factor,
314
+ epochs,
315
+ learning_rate,
316
+ optimizer_name
317
+ ],
318
+ outputs=[
319
+ noisy_output_img,
320
+ reconstruction_output_img,
321
+ loss_curve_plot,
322
+ diagnostics
323
+ ]
324
+ )
325
+
326
+ if __name__ == "__main__":
327
+ demo.queue().launch()