Skip to content

LDG-11: Time Series MIA - #368

Open
fazelehh wants to merge 34 commits into
mainfrom
pr-c-core-module-changes
Open

LDG-11: Time Series MIA#368
fazelehh wants to merge 34 commits into
mainfrom
pr-c-core-module-changes

Conversation

@fazelehh

@fazelehh fazelehh commented Feb 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR adds time series MIA support: two new distance signals (DTW and MSM), configurable shadow model sampling, handler fixes, and a reworked multi-signal LiRA attack (functional signals + corrected offline scoring).

New signals

  • DTW (DTW): Dynamic Time Warping distance between model output and target series. Requires optional sktime dependency — raises a clear ImportError if not installed.
  • MSM (MSM): Move-Split-Merge distance, implemented from scratch in leakpro/signals/utils/msm.py with no external dependencies.
  • Both are registered in SIGNAL_REGISTRY and usable via signals in attack configs (e.g. multi_signal_lira).

MS-LiRA: functional signals + corrected offline scoring

  • Functional-signal migration. multi_signal_lira now scores cached logits with functional signals instead of re-querying every shadow model once per signal. The old class-based path re-ran each model n_signals times (issue Getting model logits is very slow #390); prepare_attack now loads each model's logits exactly once (num_shadow_models + 1 loads total) and applies every signal to the cached array.
  • Corrected offline score. The offline path now implements arXiv:2509.04169 Eq. (2): the one-sided multivariate Gaussian tail under the OUT model (summed per-signal norm.logcdf), with each signal oriented so higher = more member-like via SIGNAL_MEMBERSHIP_DIRECTION. Previously the offline branch reused the online log-likelihood-ratio form with in_prs = 0, which is not the paper's offline score. For a single signal it now reduces exactly to offline LiRA (Carlini Eq. 4).
  • Online score factored into _online_score (summed per-signal LLR, Eq. 1); both score paths are static helpers and unit-tested.
  • Added a functional loss signal.
  • Undeclared signals (no membership-direction entry) now fail loudly at construction, before any models are trained.

Config rename

  • The multi_signal_lira config field signal_names is renamed to signals (user-facing). The internal resolved-callables list is signal_fns. Class-style names (ModelRescaledLogits) and functional names (rescaled_logits) are both accepted for back-compat.

Configurable shadow model sampling

  • Added sampling_method field to ShadowModelConfig (default: "balanced").
  • create_shadow_models() now accepts sampling_method="balanced" (existing behavior) or "random" (delegates to handler for custom sampling logic).
  • Added sample_shadow_indices() to AbstractInputHandler as an overrideable method — time series handlers (e.g. IndividualizedInputHandler) can override it to sample by individual rather than by record.

Handler fixes

  • MIAHandler.get_dataset(): default params now uses population.return_params() instead of {}, preserving dataset-specific settings (e.g. scaler, lookback) when recreating datasets for shadow model training.
  • MIAHandler.get_optimizer(): uses inspect.signature() to filter out invalid optimizer kwargs before instantiation, preventing crashes when time series configs pass extra parameters.

Bug fix

  • Fixed a DotMap compatibility bug in shadow_model_handler.py where sampling_method would be read as an empty DotMap() instead of defaulting to "balanced" when the key was absent from the config.

Dependencies

  • Added sktime to mia optional deps and dev group in pyproject.toml.

Tests

  • leakpro/tests/mia_attacks/utils/test_time_series_signals.py (18 tests): MSM cost/distance units, end-to-end MSM signal, DTW ImportError without sktime, registry presence, shadow sampling, get_optimizer() kwarg filtering.
  • leakpro/tests/mia_attacks/attacks/test_multi_signal_lira.py (new): online/offline, single- and multi-signal runs, signals resolve to functional functions, no per-signal model re-query (load_logits called exactly num_shadow_models + 1 times), undeclared-direction failure, offline/online scoring formulas.
  • leakpro/tests/signals/test_functional.py (new): functional signal unit tests.

Test plan

  • All existing tests pass with no regressions
  • New unit/integration tests pass (time series signals, MS-LiRA, functional signals)
  • Manual run: time series example (examples/mia/time_series_mia) with multi_signal_lira over the full signal set
  • Manual run: existing MIA attacks (lira, rmia) still work end-to-end with default balanced sampling

- Add DTW and MSM signal classes for time series distance metrics
- Add msm.py utility for Move-Split-Merge distance calculation
- Add sample_shadow_indices() to AbstractInputHandler for custom sampling
- Add configurable sampling_method ('balanced'/'random') to shadow models
- Fix dataset params propagation using population.return_params()
- Add optimizer param filtering to prevent invalid argument errors
@fazelehh fazelehh changed the title Add time series signals and configurable shadow model sampling Add time series signals and configurable shadow model sampling[working] Feb 3, 2026
@TheColdIce TheColdIce self-assigned this Mar 11, 2026
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@fazelehh fazelehh changed the title Add time series signals and configurable shadow model sampling[working] LDG-11: Time Series MIA May 21, 2026
…g, also added model_config = ConfigDict(extra='forbid') so exceptions are thrown if unkown params are initalized.
TheColdIce and others added 9 commits June 5, 2026 17:46
…fig rename

The image_handler fixture constructs every attack in the audit config, and
LiRA crashed on init: its config default 'ModelRescaledLogits' is the old
signal class name, but lira.py now resolves signals from
leakpro.signals.functional, which exposes 'rescaled_logits'. This blocked
every MIA test. Default updated, legacy class names aliased so existing
user configs keep working, and unknown names now fail with the list of
available signals.

Test fixtures had also gone stale: constants.py still set
z_data_sample_fraction (renamed to attack_data_fraction), tests passed the
'attack' routing key straight into RMIA's now extra='forbid' config
(stripped the same way AttackScheduler does), and test_lira asserted on
target_logits where the attack sets target_model_logits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dual likelihood

RMIA assumed classification: it indexed the softmax output with the label
(softmax_logits(...)[arange(n), labels]), which crashes for forecasting
models where labels are continuous horizons, and is conceptually undefined
there. The RMIA paper scopes itself to classifiers and offers no guidance
for continuous outputs.

Pr(x|theta) is now task-dependent, resolved from the target's training
criterion: classification keeps the temperature softmax at the true class
(bit-identical to before); regression losses (MSE/L1/SmoothL1/Huber) use a
Gaussian residual likelihood exp(-MSE(y, y_hat)/(2 sigma^2)). The
normalizing constant is dropped deliberately: with sigma^2 shared across
the target and all reference models it cancels in RMIA's ratios, and the
surrogate stays in (0,1] so the offline marginal approximation
0.5*((1+a)*p_out + (1-a)) remains well-defined. sigma acts as the
regression analogue of the softmax temperature: configurable via the new
'sigma' field, defaulting to a one-shot estimate from shadow-model
residuals on the z-population that is reused for the audit points so the
scale is consistent across all ratios.

Calibration (FPR = 1-beta at gamma=1) is proven only for the
classification likelihood; empirical validation for the Gaussian variant
is tracked in LDG-22. End-to-end forecasting coverage needs a time-series
test fixture (GitHub #365); the new tests cover the signal function,
dispatch, and sigma resolution at the unit level.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e dead 'signal' config field

The Gaussian residual likelihood added for forecasting/regression models
treated every regression criterion as MSE: a model trained with L1 or
Huber loss was scored under a mismatched noise model. Each criterion
implies its own noise model, and the signal probability now uses the
matching likelihood: MSE -> Gaussian exp(-MSE/(2*scale)), L1 -> Laplace
exp(-MAE/scale), SmoothL1/Huber -> Huber density (Gaussian core, Laplace
tails; SmoothL1(beta) maps to Huber delta=beta since the 1/beta factor is
absorbed by the scale). Scale estimation is family-specific: mean squared
residual (Gaussian MLE), mean absolute residual (Laplace MLE), mean Huber
energy (heuristic, no closed-form MLE). A user-provided sigma keeps its
'residual std' meaning and is mapped per family (sigma^2, sigma/sqrt(2),
quadratic-core variance).

Also removes the AttackConfig 'signal' field: it was never read by the
attack, so configs like the time-series example's 'signal: mse' validated
and silently did nothing - a user believed they chose a signal while RMIA
dispatched on the criterion regardless. With extra='forbid' it now fails
loudly; the example audit.yaml is updated. Kernelized support for the
time-series signal family (DTW, MSM, trend, ...) where likelihood
semantics no longer hold is deliberately out of scope and tracked as a
follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ame config to `signals`

Score cached logits with functional signals instead of re-querying every shadow
model once per signal (the old class-based path re-ran each model N_signals times).
prepare_attack now loads each model's logits once and applies all signals to the
cached array.

Fix the offline path to match arXiv:2509.04169 Eq. (2): the one-sided multivariate
Gaussian tail under the OUT model (summed per-signal norm.logcdf), with each signal
oriented so higher = more member-like via SIGNAL_MEMBERSHIP_DIRECTION. Previously the
offline branch reused the online log-likelihood-ratio form with in_prs = 0, which is
not the paper's offline score. Add a functional `loss` signal.

Rename the attack config field `signal_names` -> `signals` (user-facing) and rename
the internal resolved-callables list to `signal_fns` to avoid the collision.

Add test coverage: test_multi_signal_lira.py (online/offline, single/multi-signal,
no per-signal model re-query, undeclared-direction failure) and tests/signals/
test_functional.py. Update the time_series example audit.yaml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI ran ruff over the whole file once this branch touched it, surfacing
pre-existing violations in the time-series signal functions. Fix them:

- Reformat the module docstring (D205/D208/D209) and fix typos.
- Add missing public-function docstrings (D103) for logits/loss/mse/mae/
  smape/rescaled_smape/seasonality/trend/ts2vec/dtw/msm.
- Correct the `true_labels` arg name in rescaled_logits' docstring (D417).
- Rename uppercase locals Y/Z/P to lowercase (N803/N806).
- Strip trailing whitespace and add the final newline (W291/W292).
- Sort imports (I001).

No behavior change. Verified clean with ruff 0.11.2 (CI's pinned version)
and 44 related tests still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This branch changed ShadowModelConfig.init_params from `default=None` (main)
to `default_factory=dict`, which collapsed the distinction between a missing
init_params (inherit from the target model) and an explicitly empty one (stay
empty). ModelHandler relies on `None` meaning "missing" to inherit the target's
init_params; with `{}` it always took the empty branch.

Revert the default to None, matching main. Fixes
test_shadow_model_init_params_distinguish_missing_partial_and_empty.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The configurable-sampling work diverged create_shadow_models from main in two
ways that broke CI for num_models=1 (the E2E smoke value):

- It added an axis=0 assertion (each point used in exactly num_models//2
  models) that main never had. Balanced in/out assignment can only satisfy
  this for even num_models; for odd counts (incl. 1) the construction tacks a
  half-load onto the last model, so it crashed. Restore main's check: only the
  per-model size invariant (axis=1), raised as a ValueError rather than a bare
  assert (which -O strips and which gives no message).
- It dropped `batch_size=self.batch_size` from the shadow training dataloader,
  so shadow models trained at the default batch size instead of the configured
  one. Restore the argument to match main.

Fixes test_all_attacks_end_to_end[loss_traj/seqmia] and
test_shadow_model_creation_uses_shadow_batch_size. Whether balanced sampling
should support odd num_models is left open (see follow-up task).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@TheColdIce
TheColdIce requested a review from henrikfo June 9, 2026 16:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants