LatentNN (Latent-Input Neural Regression)¶
← RC | Error-Aware Encoding →
LatentNN is a lightweight algorithm for regression with noisy input features (errors-in-variables). Unlike SIMEX (which trains multiple models at different noise levels) or RC (which analytically debiases), LatentNN jointly optimizes network parameters and per-sample latent clean inputs in a single training loop.
The idea
Keep a learned latent representation \(x_{\text{latent}}\) for each training sample that is pulled toward the observed noisy input \(x_{\text{obs}}\) via a Gaussian penalty, while the model \(f_\theta\) is trained on the cleaner latent inputs.
Mathematical Background¶
Joint Objective¶
LatentNN minimizes a composite loss over model parameters \(\theta\) and per-sample latent inputs \(X_{\text{latent}}\):
where:
- \(\mathcal{L}_{\text{model}}\) is any regression loss (MSE, Gaussian NLL, etc.)
- \(\lambda_x\) is the latent_penalty_weight controlling the tradeoff
- \(\sigma_x\) is the known input measurement noise (scalar, per-feature, or per-sample)
What the Penalty Does¶
The quadratic penalty acts as a Gaussian prior on the latent inputs: \(x_{\text{latent}} \sim \mathcal{N}(x_{\text{obs}}, \sigma_x^2)\). When \(\sigma_x\) is small (precise measurement), the penalty is strong and \(x_{\text{latent}}\) stays close to \(x_{\text{obs}}\). When \(\sigma_x\) is large (noisy measurement), the penalty relaxes and the model can "denoise" the input.
Usage¶
Basic Setup¶
import torch
import torch.nn as nn
from torchregress.algorithms import LatentNN
# Model factory — returns a fresh model for each fit
def make_model():
return nn.Sequential(
nn.Linear(5, 32), nn.ReLU(),
nn.Linear(32, 1),
)
latent = LatentNN(
model_factory=make_model,
loss_fn=nn.MSELoss(),
sigma_x=0.5, # Known measurement noise std
epochs=500,
lr=1e-3,
latent_lr=1e-3, # Learning rate for latent inputs
latent_penalty_weight=1.0,
)
# Fit jointly learns latent inputs + model parameters
latent.fit(X_observed, y_observed)
# Predict on new (potentially noisy) data
y_pred = latent.predict(X_test)
| Parameter | Type | Default | Description |
|---|---|---|---|
model_factory |
callable |
— | Returns a freshly initialized nn.Module |
loss_fn |
callable |
MSELoss() |
Loss applied to model predictions |
sigma_x |
float or Tensor |
1.0 |
Known input noise. Scalar, per-feature vector (D,), or per-sample matrix (N, D). |
sigma_y |
float or Tensor or None |
None |
Known target noise for loss rescaling |
epochs |
int |
500 |
Number of training epochs |
lr |
float |
1e-3 |
Learning rate for model parameters |
latent_lr |
float or None |
None |
Learning rate for latent inputs (defaults to lr) |
batch_size |
int or None |
None |
Mini-batch size (defaults to full-batch) |
weight_decay |
float |
0.0 |
Weight decay on model parameters |
latent_weight_decay |
float |
0.0 |
Weight decay on latent inputs |
latent_penalty_weight |
float |
1.0 |
Multiplier \(\lambda_x\) on the quadratic penalty |
max_grad_norm |
float or None |
None |
Optional gradient clipping |
Per-Sample Noise¶
When different samples have different measurement quality, pass a per-sample \(\sigma_x\):
# Each of 1000 samples has 5 features with different noise levels
sigma_x = 0.1 + 0.5 * torch.rand(1000, 5) # (N, D) per-sample stds
latent = LatentNN(
model_factory=make_model,
sigma_x=sigma_x,
epochs=300,
)
latent.fit(X_observed, y_observed)
Target Noise Rescaling¶
When targets have known uncertainty \(\sigma_y\), the model loss is rescaled. For MSELoss, residuals are divided by \(\sigma_y\) per sample: \(\frac{(\hat{y} - y)^2}{\sigma_y^2}\). For other loss functions, the behavior depends on the loss implementation — verify that your chosen loss function handles per-sample target uncertainty correctly.
sigma_y = 0.05 * torch.ones(1000, 1) # Known target noise
latent = LatentNN(
model_factory=make_model,
loss_fn=nn.MSELoss(),
sigma_x=0.5,
sigma_y=sigma_y, # Rescales MSE by 1/σ_y² per sample
)
Comparison with Other EIV Methods¶
| Method | Training Cost | Handles Nonlinear \(f\) | Per-Sample Noise | Latent Recovery |
|---|---|---|---|---|
| LatentNN | \(1\times\) (single joint training) | ✅ | ✅ | ✅ (per-sample) |
| RC | \(O(D^3)\) (matrix inversion) | ❌ (linear correction) | ❌ (single \(\Sigma_u\)) | ❌ |
| SIMEX | $( | \lambda | + 1)\times$ | ✅ |
| LatentMarginalizationLoss | \(N_{\text{samples}}\times\) | ✅ | ✅ | ❌ |
When to Use¶
LatentNN is a good fit when
- Input measurement noise is known and potentially different per sample
- The noise structure is simple (additive Gaussian)
- You want a single training run (unlike SIMEX's multiple models)
- You need access to the denoised latent inputs after training (
latent.x_latent_)
LatentNN is not ideal when
- Input noise is unknown — the algorithm requires \(\sigma_x\) as input
- The data has complex noise (non-Gaussian, correlated across features) — consider
ErrorAwareFeatureEncoder - The model is extremely large — storing per-sample latent inputs adds \(N \times D\) parameters
Limitations¶
Practical constraints
- Noise must be known:
sigma_xis a required input. If you don't know the measurement noise, LatentNN can't help — the latent inputs will drift toward whatever values minimize the model loss without meaningful denoising. - Per-sample storage: LatentNN stores one latent vector per training sample (\(N \times D\) parameters), which can exceed the model parameter count for large \(N\).
- Gaussian noise assumption: The quadratic penalty corresponds to a Gaussian likelihood for \(x_{\text{obs}} \mid x_{\text{latent}}\). For non-Gaussian noise (e.g., quantization error, Poisson counts), the penalty form is suboptimal.
- Validation-aware fit: The
.fit()method tracks validation loss and restores the best model state, but the latent inputs are not validated — the best model checkpoint may be paired with suboptimal latent inputs if the validation set has different noise characteristics.
Complete Example¶
import torch
import torch.nn as nn
from torchregress.algorithms import LatentNN
torch.manual_seed(42)
# True clean data
X_true = torch.randn(200, 3)
y_true = X_true @ torch.tensor([1.5, -0.8, 0.3]) + 0.1 * torch.randn(200)
# Add measurement noise to inputs
X_obs = X_true + 0.4 * torch.randn(200, 3)
def make_model():
return nn.Sequential(
nn.Linear(3, 32), nn.ReLU(),
nn.Linear(32, 16), nn.ReLU(),
nn.Linear(16, 1),
)
latent = LatentNN(
model_factory=make_model,
sigma_x=0.4,
epochs=300,
lr=1e-3,
latent_penalty_weight=1.0,
)
# Fit — jointly learns model and latent clean inputs
latent.fit(X_obs, y_true.unsqueeze(1))
# Access the learned latent inputs
print(f"Latent input shape: {latent.x_latent_.shape}")
# Denoising quality: how close are latent inputs to true clean inputs?
denoising_mae = (latent.x_latent_ - X_true).abs().mean()
print(f"Denoising MAE: {denoising_mae:.4f} (obs MAE: {(X_obs - X_true).abs().mean():.4f})")
# Predict on test data
X_test = torch.randn(50, 3) + 0.3 * torch.randn(50, 3)
y_pred = latent.predict(X_test)
Next steps¶
- SIMEX — multi-model extrapolation baseline; compare single-run LatentNN vs multi-model cost
- Regression Calibration — fast matrix-based correction when a linear model suffices
- Error-Aware Encoding — lightweight alternative with no per-sample parameter storage
- EIV losses — loss-level handling when you prefer not to learn latent inputs
References¶
| # | Reference |
|---|---|
| 1 | R.J. Carroll, D. Ruppert, L.A. Stefanski, C.M. Crainiceanu. Measurement Error in Nonlinear Models. 2nd ed., Chapman & Hall/CRC, 2006. |
| 2 | S. Fabbro et al. "Latent-input neural regression for tabular errors-in-variables." torchregress documentation, 2025. |