Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -19,32 +19,32 @@ class DynamicMLPAutoencoder(nn.Module):
|
|
| 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)
|
|
@@ -58,21 +58,26 @@ class DynamicMLPAutoencoder(nn.Module):
|
|
| 58 |
# 2. HELPER UTILITIES
|
| 59 |
# =====================================================================
|
| 60 |
|
| 61 |
-
def preprocess_image(pil_img,
|
| 62 |
-
"""Resizes, converts to tensor, and flattens an image.
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 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)
|
|
@@ -98,13 +103,38 @@ def create_loss_plot(losses):
|
|
| 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 |
-
|
|
|
|
|
|
|
| 108 |
latent_dim,
|
| 109 |
hidden_dim,
|
| 110 |
num_hidden_layers,
|
|
@@ -117,20 +147,26 @@ def run_autoencoder_sandbox(
|
|
| 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,
|
| 123 |
-
|
| 124 |
# Flatten dimensions using .reshape() instead of .view() to avoid layout conflicts
|
| 125 |
-
input_dim = 3 *
|
| 126 |
flat_clean = img_tensor.reshape(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.reshape(1, -1)
|
| 131 |
-
|
| 132 |
# Pre-render the noisy input for user preview
|
| 133 |
-
noisy_preview = postprocess_tensor(noisy_img_tensor,
|
| 134 |
|
| 135 |
# 3. Model Initialization
|
| 136 |
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
@@ -140,7 +176,7 @@ def run_autoencoder_sandbox(
|
|
| 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 |
|
|
@@ -149,48 +185,49 @@ def run_autoencoder_sandbox(
|
|
| 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 using .reshape()
|
| 181 |
-
reconstructed_tensor = reconstructed.reshape(1, 3,
|
| 182 |
-
reconstructed_pil = postprocess_tensor(reconstructed_tensor,
|
| 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,
|
|
@@ -205,6 +242,16 @@ def run_autoencoder_sandbox(
|
|
| 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 |
"""
|
|
@@ -212,87 +259,104 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Autoencoder Sandbox") as demo:
|
|
| 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 |
-
|
| 224 |
-
choices=
|
| 225 |
-
value=
|
| 226 |
-
label="
|
| 227 |
-
info="
|
| 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=2.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(
|
|
@@ -300,14 +364,16 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Autoencoder Sandbox") as demo:
|
|
| 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 |
# Added show_progress="hidden" to suppress output-level flickering/loading animations
|
| 306 |
train_btn.click(
|
| 307 |
fn=run_autoencoder_sandbox,
|
| 308 |
inputs=[
|
| 309 |
input_img,
|
| 310 |
-
|
|
|
|
|
|
|
| 311 |
latent_dim,
|
| 312 |
hidden_dim,
|
| 313 |
num_hidden_layers,
|
|
|
|
| 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)
|
|
|
|
| 58 |
# 2. HELPER UTILITIES
|
| 59 |
# =====================================================================
|
| 60 |
|
| 61 |
+
def preprocess_image(pil_img, width, height):
|
| 62 |
+
"""Resizes (to width x height), converts to tensor, and flattens an image.
|
| 63 |
+
|
| 64 |
+
Note: PIL's `.resize()` takes a (width, height) tuple, which is exactly
|
| 65 |
+
the ordering we want to preserve here so non-square (n x m) resolutions
|
| 66 |
+
work correctly.
|
| 67 |
+
"""
|
| 68 |
+
img_resized = pil_img.resize((width, height), Image.Resampling.LANCZOS)
|
| 69 |
img_np = np.array(img_resized).astype(np.float32) / 255.0
|
| 70 |
+
|
| 71 |
# Handle Grayscale / RGBA conversions
|
| 72 |
if len(img_np.shape) == 2: # Grayscale to RGB
|
| 73 |
img_np = np.stack([img_np] * 3, axis=-1)
|
| 74 |
elif img_np.shape[2] == 4: # RGBA to RGB
|
| 75 |
img_np = img_np[:, :, :3]
|
| 76 |
+
|
| 77 |
img_tensor = torch.tensor(img_np).permute(2, 0, 1).unsqueeze(0) # Shape: [1, 3, H, W]
|
| 78 |
return img_tensor, img_resized
|
| 79 |
|
| 80 |
+
def postprocess_tensor(tensor, width, height):
|
| 81 |
"""Converts flattened/raw image tensors back to PIL Images."""
|
| 82 |
img_np = tensor.squeeze(0).permute(1, 2, 0).detach().cpu().numpy()
|
| 83 |
img_np = np.clip(img_np * 255.0, 0, 255).astype(np.uint8)
|
|
|
|
| 103 |
fig.tight_layout()
|
| 104 |
return fig
|
| 105 |
|
| 106 |
+
def resolve_resolution(res_mode, custom_width, custom_height, input_image):
|
| 107 |
+
"""
|
| 108 |
+
Determines the final (width, height) to train on based on the selected mode:
|
| 109 |
+
- "Use image's native resolution": pulls straight from the uploaded image
|
| 110 |
+
- Preset choices like "64x64", "128x128", "64x256": parsed directly
|
| 111 |
+
- "Custom": uses the custom_width / custom_height slider values
|
| 112 |
+
"""
|
| 113 |
+
if res_mode == "Use image's native resolution":
|
| 114 |
+
if input_image is None:
|
| 115 |
+
raise gr.Error("Please upload an image first so its native resolution can be used!")
|
| 116 |
+
width, height = input_image.size # PIL gives (width, height)
|
| 117 |
+
return width, height
|
| 118 |
+
|
| 119 |
+
if res_mode == "Custom (set width/height below)":
|
| 120 |
+
return int(custom_width), int(custom_height)
|
| 121 |
+
|
| 122 |
+
# Preset like "64x64" or "64x256"
|
| 123 |
+
try:
|
| 124 |
+
w_str, h_str = res_mode.lower().split("x")
|
| 125 |
+
return int(w_str), int(h_str)
|
| 126 |
+
except Exception:
|
| 127 |
+
raise gr.Error(f"Could not parse resolution preset: {res_mode}")
|
| 128 |
+
|
| 129 |
# =====================================================================
|
| 130 |
# 3. INTERACTIVE TRAINING FUNCTION (GRADIO GENERATOR)
|
| 131 |
# =====================================================================
|
| 132 |
|
| 133 |
def run_autoencoder_sandbox(
|
| 134 |
input_image,
|
| 135 |
+
res_mode,
|
| 136 |
+
custom_width,
|
| 137 |
+
custom_height,
|
| 138 |
latent_dim,
|
| 139 |
hidden_dim,
|
| 140 |
num_hidden_layers,
|
|
|
|
| 147 |
if input_image is None:
|
| 148 |
raise gr.Error("Please upload or select an image first!")
|
| 149 |
|
| 150 |
+
# 0. Resolve target width/height (supports n x n and n x m, or native size)
|
| 151 |
+
width, height = resolve_resolution(res_mode, custom_width, custom_height, input_image)
|
| 152 |
+
|
| 153 |
+
if width < 4 or height < 4:
|
| 154 |
+
raise gr.Error("Width and height must each be at least 4 pixels.")
|
| 155 |
+
|
| 156 |
# 1. Preprocessing
|
| 157 |
+
progress(0, desc=f"Preprocessing image to {width}x{height}...")
|
| 158 |
+
img_tensor, original_resized = preprocess_image(input_image, width, height)
|
| 159 |
+
|
| 160 |
# Flatten dimensions using .reshape() instead of .view() to avoid layout conflicts
|
| 161 |
+
input_dim = 3 * width * height
|
| 162 |
flat_clean = img_tensor.reshape(1, -1)
|
| 163 |
+
|
| 164 |
# 2. Setup Noise (for Denoising mode)
|
| 165 |
noisy_img_tensor = add_gaussian_noise(img_tensor, noise_factor)
|
| 166 |
flat_input = noisy_img_tensor.reshape(1, -1)
|
| 167 |
+
|
| 168 |
# Pre-render the noisy input for user preview
|
| 169 |
+
noisy_preview = postprocess_tensor(noisy_img_tensor, width, height)
|
| 170 |
|
| 171 |
# 3. Model Initialization
|
| 172 |
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
|
|
| 176 |
latent_dim=latent_dim,
|
| 177 |
num_hidden_layers=num_hidden_layers
|
| 178 |
).to(device)
|
| 179 |
+
|
| 180 |
flat_input = flat_input.to(device)
|
| 181 |
flat_clean = flat_clean.to(device)
|
| 182 |
|
|
|
|
| 185 |
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
|
| 186 |
else:
|
| 187 |
optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9)
|
| 188 |
+
|
| 189 |
criterion = nn.MSELoss()
|
| 190 |
losses = []
|
| 191 |
+
|
| 192 |
# Determine UI update frequency to preserve execution speed
|
| 193 |
+
update_every = max(1, epochs // 40)
|
| 194 |
|
| 195 |
# 5. Training Loop
|
| 196 |
for epoch in range(epochs):
|
| 197 |
model.train()
|
| 198 |
optimizer.zero_grad()
|
| 199 |
+
|
| 200 |
# Forward Pass
|
| 201 |
reconstructed, latent = model(flat_input)
|
| 202 |
+
|
| 203 |
# Calculate loss (against clean image always, to support denoising autoencoder structure)
|
| 204 |
loss = criterion(reconstructed, flat_clean)
|
| 205 |
+
|
| 206 |
# Backward Pass & Step
|
| 207 |
loss.backward()
|
| 208 |
optimizer.step()
|
| 209 |
+
|
| 210 |
losses.append(loss.item())
|
| 211 |
+
|
| 212 |
# Emit live training updates
|
| 213 |
if epoch % update_every == 0 or epoch == epochs - 1:
|
| 214 |
progress((epoch + 1) / epochs, desc=f"Epoch {epoch+1}/{epochs} | Loss: {loss.item():.5f}")
|
| 215 |
+
|
| 216 |
# Format reconstructed image back to normal shape using .reshape()
|
| 217 |
+
reconstructed_tensor = reconstructed.reshape(1, 3, height, width)
|
| 218 |
+
reconstructed_pil = postprocess_tensor(reconstructed_tensor, width, height)
|
| 219 |
+
|
| 220 |
# Generate loss graph
|
| 221 |
loss_plot = create_loss_plot(losses)
|
| 222 |
+
|
| 223 |
status_text = (
|
| 224 |
f"### Training Diagnostics\n"
|
| 225 |
f"- **Current Epoch:** {epoch + 1} / {epochs}\n"
|
| 226 |
f"- **Current Loss (MSE):** `{loss.item():.6f}`\n"
|
| 227 |
+
f"- **Resolution:** `{width} x {height}` ({input_dim} input values)\n"
|
| 228 |
f"- **Compression Ratio:** `{input_dim} inputs ➜ {latent_dim} bottleneck` (Compacted by **{input_dim / latent_dim:.1f}x**)"
|
| 229 |
)
|
| 230 |
+
|
| 231 |
# Yield components iteratively for active visual rendering
|
| 232 |
yield (
|
| 233 |
noisy_preview,
|
|
|
|
| 242 |
# 4. GRADIO APP INTERFACE LAYOUT
|
| 243 |
# =====================================================================
|
| 244 |
|
| 245 |
+
RESOLUTION_PRESETS = [
|
| 246 |
+
"Use image's native resolution",
|
| 247 |
+
"32x32",
|
| 248 |
+
"64x64",
|
| 249 |
+
"128x128",
|
| 250 |
+
"64x256",
|
| 251 |
+
"256x64",
|
| 252 |
+
"Custom (set width/height below)",
|
| 253 |
+
]
|
| 254 |
+
|
| 255 |
with gr.Blocks(theme=gr.themes.Soft(), title="Autoencoder Sandbox") as demo:
|
| 256 |
gr.Markdown(
|
| 257 |
"""
|
|
|
|
| 259 |
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!
|
| 260 |
"""
|
| 261 |
)
|
| 262 |
+
|
| 263 |
with gr.Row():
|
| 264 |
# --- LEFT SIDEBAR: CONTROLS & ARCHITECTURE ---
|
| 265 |
with gr.Column(scale=1):
|
| 266 |
gr.Markdown("### 🛠️ Step 1: Input & Parameters")
|
| 267 |
input_img = gr.Image(type="pil", label="Upload Source Image", value=None)
|
| 268 |
+
|
| 269 |
with gr.Tab("Network Architecture"):
|
| 270 |
+
res_mode = gr.Dropdown(
|
| 271 |
+
choices=RESOLUTION_PRESETS,
|
| 272 |
+
value="64x64",
|
| 273 |
+
label="Training Resolution",
|
| 274 |
+
info="Pick a preset (n x n or n x m), use the image's native size, or set a custom width/height below."
|
| 275 |
)
|
| 276 |
+
with gr.Row():
|
| 277 |
+
custom_width = gr.Slider(
|
| 278 |
+
minimum=8,
|
| 279 |
+
maximum=512,
|
| 280 |
+
value=64,
|
| 281 |
+
step=8,
|
| 282 |
+
label="Custom Width",
|
| 283 |
+
info="Only used when 'Custom' is selected above."
|
| 284 |
+
)
|
| 285 |
+
custom_height = gr.Slider(
|
| 286 |
+
minimum=8,
|
| 287 |
+
maximum=512,
|
| 288 |
+
value=64,
|
| 289 |
+
step=8,
|
| 290 |
+
label="Custom Height",
|
| 291 |
+
info="Only used when 'Custom' is selected above."
|
| 292 |
+
)
|
| 293 |
latent_dim = gr.Slider(
|
| 294 |
+
minimum=1,
|
| 295 |
+
maximum=256,
|
| 296 |
+
value=16,
|
| 297 |
+
step=1,
|
| 298 |
+
label="Latent Dimension (Bottleneck)",
|
| 299 |
info="Lower values yield abstract, blurry representations ('not 1:1')."
|
| 300 |
)
|
| 301 |
hidden_dim = gr.Slider(
|
| 302 |
+
minimum=16,
|
| 303 |
+
maximum=512,
|
| 304 |
+
value=128,
|
| 305 |
+
step=16,
|
| 306 |
+
label="Hidden Dimension",
|
| 307 |
info="Size of the intermediate neural network layers."
|
| 308 |
)
|
| 309 |
num_hidden_layers = gr.Slider(
|
| 310 |
+
minimum=1,
|
| 311 |
+
maximum=3,
|
| 312 |
+
value=1,
|
| 313 |
+
step=1,
|
| 314 |
+
label="Number of Hidden Layers",
|
| 315 |
info="Deepens the feature extraction depth."
|
| 316 |
)
|
| 317 |
+
|
| 318 |
with gr.Tab("Training Configuration"):
|
| 319 |
noise_factor = gr.Slider(
|
| 320 |
+
minimum=0.0,
|
| 321 |
+
maximum=2.0,
|
| 322 |
+
value=0.0,
|
| 323 |
+
step=0.05,
|
| 324 |
+
label="Noise Injection (Denoising Mode)",
|
| 325 |
info="Introduces random noise to the training inputs. Model learns to clean it!"
|
| 326 |
)
|
| 327 |
epochs = gr.Slider(
|
| 328 |
+
minimum=20,
|
| 329 |
+
maximum=1500,
|
| 330 |
+
value=400,
|
| 331 |
+
step=20,
|
| 332 |
+
label="Training Epochs",
|
| 333 |
info="Total gradient updates."
|
| 334 |
)
|
| 335 |
learning_rate = gr.Slider(
|
| 336 |
+
minimum=0.0001,
|
| 337 |
+
maximum=0.05,
|
| 338 |
+
value=0.005,
|
| 339 |
+
step=0.0005,
|
| 340 |
label="Learning Rate"
|
| 341 |
)
|
| 342 |
optimizer_name = gr.Radio(
|
| 343 |
+
choices=["Adam", "SGD"],
|
| 344 |
+
value="Adam",
|
| 345 |
label="Optimizer Mode"
|
| 346 |
)
|
| 347 |
+
|
| 348 |
train_btn = gr.Button("🚀 Begin Real-Time Training", variant="primary")
|
| 349 |
|
| 350 |
# --- RIGHT SIDEBAR: OUTPUTS & METRICS ---
|
| 351 |
with gr.Column(scale=2):
|
| 352 |
gr.Markdown("### 📊 Step 2: Live Training Output")
|
| 353 |
+
|
| 354 |
with gr.Row():
|
| 355 |
with gr.Column():
|
| 356 |
noisy_output_img = gr.Image(label="Model Input (Noisy / Resized)", interactive=False)
|
| 357 |
with gr.Column():
|
| 358 |
reconstruction_output_img = gr.Image(label="Live Model Reconstruction", interactive=False)
|
| 359 |
+
|
| 360 |
with gr.Row():
|
| 361 |
with gr.Column(scale=1):
|
| 362 |
diagnostics = gr.Markdown(
|
|
|
|
| 364 |
)
|
| 365 |
with gr.Column(scale=1.2):
|
| 366 |
loss_curve_plot = gr.Plot(label="Live Loss Curve")
|
| 367 |
+
|
| 368 |
# Link action to the training function
|
| 369 |
# Added show_progress="hidden" to suppress output-level flickering/loading animations
|
| 370 |
train_btn.click(
|
| 371 |
fn=run_autoencoder_sandbox,
|
| 372 |
inputs=[
|
| 373 |
input_img,
|
| 374 |
+
res_mode,
|
| 375 |
+
custom_width,
|
| 376 |
+
custom_height,
|
| 377 |
latent_dim,
|
| 378 |
hidden_dim,
|
| 379 |
num_hidden_layers,
|