Monte Carlo Dropout for Beam Deflection Regression¶
Estimate predictive uncertainty for a beam deflection model by keeping dropout active at inference.
Goal¶
Build an intuitive, end-to-end MC Dropout workflow where each step is explicit:
- generate a challenging synthetic beam-response dataset,
- train a dropout MLP regressor,
- keep dropout active at inference,
- visualize predictive mean and uncertainty intervals.
This notebook is written as a practical tutorial, so each cell includes comments explaining why each line exists.
Case Study: Beam with Local Compliance Variations¶
We use a physics-inspired composite surrogate (harder than a smooth single equation):
- Baseline simply-supported Euler-Bernoulli trend.
- Two localized stiffness-reduction zones (compliance dips).
- Small oscillatory residuals (unmodeled effects surrogate).
We also make data realism harder:
- Imbalanced training samples (sparser in a critical region), and
- Heteroscedastic observation noise (noise depends on position).
This setup makes MC Dropout uncertainty behavior easier to interpret and stress-test.
# ----------------------------------------------------------------------------
# Environment bootstrap
# - Ensure notebook imports local in-repo `deepuq` package from `src/`.
# - Avoids accidentally using a stale globally installed version.
# ----------------------------------------------------------------------------
import os
import sys
from pathlib import Path
PROJECT_ROOT = Path(os.getcwd())
if not (PROJECT_ROOT / 'src').exists():
PROJECT_ROOT = PROJECT_ROOT.parent
SRC_PATH = str(PROJECT_ROOT / 'src')
if SRC_PATH not in sys.path:
sys.path.insert(0, SRC_PATH)
Step 1 — Imports and Library Hooks¶
In the next cell we import PyTorch + deepuq components and define set_seed.
Pay attention to:
deepuq.models.MLP: backbone network.deepuq.methods.MCDropoutWrapper: uncertainty estimator used at inference.
# ----------------------------------------------------------------------------
# Core imports and reproducibility utilities
# - `MLP` is the base model architecture from deepuq.
# - `MCDropoutWrapper` performs stochastic inference with dropout enabled.
# ----------------------------------------------------------------------------
import random
import numpy as np
import torch
import torch.nn.functional as F
from torch import optim
from torch.utils.data import DataLoader, TensorDataset
import matplotlib.pyplot as plt
from deepuq.models import MLP
from deepuq.methods import MCDropoutWrapper
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
def set_seed(seed: int) -> None:
"""Set RNG seeds for reproducible datasets and training runs."""
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
Step 2 — Build a Harder Synthetic Dataset¶
The next cell defines a more complex target function and a realistic data generation process.
Why this matters:
- simple functions can hide uncertainty behavior;
- this setup creates regions where MC Dropout should respond with larger predictive spread.
# ----------------------------------------------------------------------------
# Data generation: complex beam-response surrogate
# - baseline beam deflection + local compliance effects + oscillatory residuals
# - imbalanced sampling in training region
# - heteroscedastic observation noise
# ----------------------------------------------------------------------------
set_seed(7)
# Beam/material constants (baseline trend)
L = 2.0
w_load = 4000.0
E = 210e9
I = 1e-6
def baseline_beam_mm(x: torch.Tensor) -> torch.Tensor:
"""Euler-Bernoulli simply-supported beam deflection under uniform load (mm)."""
y_m = (w_load * x * (L**3 - 2 * L * x**2 + x**3)) / (24 * E * I)
return 1e3 * y_m
def complex_beam_response_mm(x: torch.Tensor) -> torch.Tensor:
"""Harder, physics-inspired response with local compliance variation."""
base = baseline_beam_mm(x)
# Localized stiffness dips (simulating softer zones / defects).
dip1 = 0.32 * torch.exp(-0.5 * ((x - 0.70) / 0.10) ** 2)
dip2 = 0.24 * torch.exp(-0.5 * ((x - 1.45) / 0.08) ** 2)
eff_stiffness = (1.0 - dip1 - dip2).clamp_min(0.45)
# Smooth envelope to keep residual components bounded near supports.
xi = x / L
envelope = xi * (1.0 - xi)
# Higher-frequency residual corrections (surrogate for unmodeled effects).
residual = (
4.5 * envelope * torch.sin(8.0 * torch.pi * xi)
+ 2.0 * envelope * torch.cos(15.0 * torch.pi * xi)
)
return base / eff_stiffness + residual
def sample_train_x(n_samples: int, generator: torch.Generator) -> torch.Tensor:
"""Intentionally imbalanced training x-samples with sparse center region."""
n_left = int(0.45 * n_samples)
n_mid = int(0.10 * n_samples) # sparse zone where function is hardest
n_right = n_samples - n_left - n_mid
left = torch.rand(n_left, 1, generator=generator) * 0.75
mid = 0.75 + torch.rand(n_mid, 1, generator=generator) * 0.50
right = 1.25 + torch.rand(n_right, 1, generator=generator) * 0.75
x = torch.cat([left, mid, right], dim=0)
perm = torch.randperm(x.shape[0], generator=generator)
return x[perm]
def make_dataset(n_samples: int, base_noise_std: float, seed: int, train_like: bool = False):
"""Create noisy observations with position-dependent (heteroscedastic) noise."""
gen = torch.Generator().manual_seed(seed)
if train_like:
x = sample_train_x(n_samples, gen)
else:
x = torch.rand(n_samples, 1, generator=gen) * L
y_true = complex_beam_response_mm(x)
# Noise increases near center/critical region.
local_scale = 1.0 + 1.8 * torch.exp(-0.5 * ((x - 1.0) / 0.22) ** 2)
noise = base_noise_std * local_scale * torch.randn(n_samples, 1, generator=gen)
y = y_true + noise
return x, y, y_true
# Build train/test sets (train has imbalanced sampling pattern)
x_train, y_train, _ = make_dataset(700, base_noise_std=0.10, seed=7, train_like=True)
x_test, y_test, y_test_true = make_dataset(320, base_noise_std=0.10, seed=17, train_like=False)
# Mini-batch loader for optimization
train_loader = DataLoader(TensorDataset(x_train, y_train), batch_size=128, shuffle=True)
# Dense grid for plotting smooth predictive curves and uncertainty bands
x_grid = torch.linspace(0, L, 280).unsqueeze(-1)
y_grid_true = complex_beam_response_mm(x_grid)
Step 3 — Train the Dropout MLP¶
We train a larger network with dropout and print periodic RMSE to monitor convergence.
This stage learns the mapping; uncertainty is extracted in the next stage.
# ----------------------------------------------------------------------------
# Model training
# - Use a deeper dropout MLP than the basic tutorial version.
# - Dropout is active during training by default (standard behavior).
# ----------------------------------------------------------------------------
model = MLP(input_dim=1, hidden_dims=[256, 256, 128], output_dim=1, p_drop=0.20).to(DEVICE)
optimizer = optim.Adam(model.parameters(), lr=8e-4, weight_decay=1e-5)
for epoch in range(10000):
model.train()
total_loss = 0.0
seen = 0
for x_batch, y_batch in train_loader:
x_batch = x_batch.to(DEVICE)
y_batch = y_batch.to(DEVICE)
# Standard supervised regression step
optimizer.zero_grad(set_to_none=True)
preds = model(x_batch)
loss = F.mse_loss(preds, y_batch)
loss.backward()
optimizer.step()
total_loss += loss.item() * y_batch.numel()
seen += y_batch.numel()
if (epoch + 1) % 75 == 0:
train_rmse = (total_loss / seen) ** 0.5
print(f'Epoch {epoch + 1:03d} | train RMSE={train_rmse:.4f}')
Epoch 075 | train RMSE=0.5727
Epoch 150 | train RMSE=0.4751
Epoch 225 | train RMSE=0.4492
Epoch 300 | train RMSE=0.4619
Epoch 375 | train RMSE=0.4219
Epoch 450 | train RMSE=0.3899
Epoch 525 | train RMSE=0.3510
Epoch 600 | train RMSE=0.3431
Epoch 675 | train RMSE=0.3275
Epoch 750 | train RMSE=0.3168
Epoch 825 | train RMSE=0.3163
Epoch 900 | train RMSE=0.2917
Epoch 975 | train RMSE=0.3082
Epoch 1050 | train RMSE=0.2806
Epoch 1125 | train RMSE=0.2850
Epoch 1200 | train RMSE=0.2811
Epoch 1275 | train RMSE=0.2797
Epoch 1350 | train RMSE=0.2649
Epoch 1425 | train RMSE=0.2641
Epoch 1500 | train RMSE=0.2618
Epoch 1575 | train RMSE=0.2493
Epoch 1650 | train RMSE=0.2366
Epoch 1725 | train RMSE=0.2367
Epoch 1800 | train RMSE=0.2622
Epoch 1875 | train RMSE=0.2390
Epoch 1950 | train RMSE=0.2211
Epoch 2025 | train RMSE=0.2439
Epoch 2100 | train RMSE=0.2288
Epoch 2175 | train RMSE=0.2374
Epoch 2250 | train RMSE=0.2588
Epoch 2325 | train RMSE=0.2204
Epoch 2400 | train RMSE=0.2306
Epoch 2475 | train RMSE=0.2320
Epoch 2550 | train RMSE=0.2236
Epoch 2625 | train RMSE=0.2200
Epoch 2700 | train RMSE=0.2143
Epoch 2775 | train RMSE=0.2415
Epoch 2850 | train RMSE=0.2352
Epoch 2925 | train RMSE=0.2265
Epoch 3000 | train RMSE=0.2317
Epoch 3075 | train RMSE=0.2206
Epoch 3150 | train RMSE=0.2291
Epoch 3225 | train RMSE=0.2206
Epoch 3300 | train RMSE=0.2241
Epoch 3375 | train RMSE=0.2218
Epoch 3450 | train RMSE=0.2215
Epoch 3525 | train RMSE=0.2073
Epoch 3600 | train RMSE=0.2141
Epoch 3675 | train RMSE=0.2094
Epoch 3750 | train RMSE=0.2178
Epoch 3825 | train RMSE=0.2205
Epoch 3900 | train RMSE=0.2169
Epoch 3975 | train RMSE=0.2162
Epoch 4050 | train RMSE=0.2151
Epoch 4125 | train RMSE=0.2080
Epoch 4200 | train RMSE=0.2192
Epoch 4275 | train RMSE=0.2102
Epoch 4350 | train RMSE=0.2021
Epoch 4425 | train RMSE=0.2187
Epoch 4500 | train RMSE=0.1972
Epoch 4575 | train RMSE=0.2270
Epoch 4650 | train RMSE=0.2166
Epoch 4725 | train RMSE=0.2073
Epoch 4800 | train RMSE=0.2138
Epoch 4875 | train RMSE=0.2162
Epoch 4950 | train RMSE=0.2092
Epoch 5025 | train RMSE=0.2160
Epoch 5100 | train RMSE=0.2189
Epoch 5175 | train RMSE=0.1963
Epoch 5250 | train RMSE=0.2038
Epoch 5325 | train RMSE=0.2173
Epoch 5400 | train RMSE=0.2040
Epoch 5475 | train RMSE=0.2034
Epoch 5550 | train RMSE=0.2123
Epoch 5625 | train RMSE=0.2031
Epoch 5700 | train RMSE=0.2072
Epoch 5775 | train RMSE=0.2190
Epoch 5850 | train RMSE=0.2297
Epoch 5925 | train RMSE=0.1973
Epoch 6000 | train RMSE=0.2116
Epoch 6075 | train RMSE=0.2049
Epoch 6150 | train RMSE=0.2095
Epoch 6225 | train RMSE=0.2000
Epoch 6300 | train RMSE=0.2153
Epoch 6375 | train RMSE=0.2101
Epoch 6450 | train RMSE=0.2003
Epoch 6525 | train RMSE=0.2133
Epoch 6600 | train RMSE=0.2008
Epoch 6675 | train RMSE=0.1986
Epoch 6750 | train RMSE=0.2164
Epoch 6825 | train RMSE=0.2191
Epoch 6900 | train RMSE=0.2084
Epoch 6975 | train RMSE=0.2077
Epoch 7050 | train RMSE=0.2161
Epoch 7125 | train RMSE=0.2165
Epoch 7200 | train RMSE=0.2154
Epoch 7275 | train RMSE=0.2144
Epoch 7350 | train RMSE=0.2216
Epoch 7425 | train RMSE=0.2118
Epoch 7500 | train RMSE=0.2082
Epoch 7575 | train RMSE=0.2105
Epoch 7650 | train RMSE=0.2118
Epoch 7725 | train RMSE=0.2018
Epoch 7800 | train RMSE=0.2123
Epoch 7875 | train RMSE=0.2113
Epoch 7950 | train RMSE=0.1943
Epoch 8025 | train RMSE=0.2086
Epoch 8100 | train RMSE=0.2174
Epoch 8175 | train RMSE=0.2167
Epoch 8250 | train RMSE=0.2126
Epoch 8325 | train RMSE=0.1955
Epoch 8400 | train RMSE=0.2037
Epoch 8475 | train RMSE=0.2201
Epoch 8550 | train RMSE=0.2133
Epoch 8625 | train RMSE=0.2058
Epoch 8700 | train RMSE=0.2172
Epoch 8775 | train RMSE=0.2150
Epoch 8850 | train RMSE=0.2143
Epoch 8925 | train RMSE=0.2005
Epoch 9000 | train RMSE=0.2123
Epoch 9075 | train RMSE=0.2071
Epoch 9150 | train RMSE=0.2131
Epoch 9225 | train RMSE=0.2151
Epoch 9300 | train RMSE=0.1999
Epoch 9375 | train RMSE=0.2047
Epoch 9450 | train RMSE=0.2128
Epoch 9525 | train RMSE=0.2076
Epoch 9600 | train RMSE=0.2058
Epoch 9675 | train RMSE=0.2154
Epoch 9750 | train RMSE=0.2050
Epoch 9825 | train RMSE=0.2039
Epoch 9900 | train RMSE=0.2130
Epoch 9975 | train RMSE=0.2036
Step 4 — Monte Carlo Inference¶
During inference we run many stochastic passes (n_mc=300) with dropout active.
Outputs:
- predictive mean,
- predictive variance/std,
- confidence interval visualization.
# ----------------------------------------------------------------------------
# MC Dropout inference
# - `MCDropoutWrapper` performs many stochastic forward passes at test time.
# - Dropout remains active during prediction to estimate epistemic uncertainty.
# ----------------------------------------------------------------------------
uq = MCDropoutWrapper(model, n_mc=300, apply_softmax=False)
# Predictive mean/variance on a dense plotting grid
mean, var = uq.predict(x_grid.to(DEVICE))
mean = mean.cpu()
std = var.sqrt().cpu()
# Approximate 95% interval using Gaussian approximation
lower = mean - 1.96 * std
upper = mean + 1.96 * std
# Quick generalization check on held-out noisy test set
with torch.no_grad():
test_mean, _ = uq.predict(x_test.to(DEVICE))
test_rmse = torch.sqrt(F.mse_loss(test_mean.cpu(), y_test)).item()
print(f'Test RMSE: {test_rmse:.4f} mm')
# Main predictive plot
plt.figure(figsize=(8, 4.5))
plt.scatter(x_train.numpy(), y_train.numpy(), s=13, alpha=0.35, label='Train noisy samples')
plt.plot(x_grid.numpy(), y_grid_true.numpy(), color='black', linewidth=2, label='Complex true response')
plt.plot(x_grid.numpy(), mean.numpy(), color='tab:blue', linewidth=2, label='MC Dropout mean')
plt.fill_between(
x_grid.squeeze().numpy(),
lower.squeeze().numpy(),
upper.squeeze().numpy(),
color='tab:blue',
alpha=0.22,
label='95% interval',
)
# Highlight sparse train region to interpret uncertainty behavior
plt.axvspan(0.75, 1.25, color='tab:red', alpha=0.08, label='Sparse train region')
plt.xlabel('Position x (m)')
plt.ylabel('Deflection y (mm)')
plt.title('MC Dropout on complex beam-response surrogate')
plt.legend(frameon=False)
plt.tight_layout()
plt.show()
Test RMSE: 0.1786 mm
Step 5 — Read the Uncertainty Curve¶
The final plot focuses only on predictive standard deviation across beam position.
Use it to verify whether uncertainty aligns with sparse or difficult regions.
# ----------------------------------------------------------------------------
# Uncertainty profile plot
# - Visualize predictive std as a function of x.
# - Useful for checking whether uncertainty increases in sparse/complex regions.
# ----------------------------------------------------------------------------
plt.figure(figsize=(6.5, 4))
plt.plot(x_grid.numpy(), std.numpy(), color='tab:orange', linewidth=2)
plt.axvspan(0.75, 1.25, color='tab:red', alpha=0.08, label='Sparse train region')
plt.xlabel('Position x (m)')
plt.ylabel('Predictive std (mm)')
plt.title('MC Dropout uncertainty along the beam')
plt.grid(True, linestyle='--', linewidth=0.5, alpha=0.4)
plt.legend(frameon=False)
plt.tight_layout()
plt.show()