Skip to content

Importance sampling for the stochastic Frank-Wolfe variants - #112

Open
GeoffNN wants to merge 3 commits into
openopt:masterfrom
GeoffNN:sfw-importance-sampling
Open

Importance sampling for the stochastic Frank-Wolfe variants#112
GeoffNN wants to merge 3 commits into
openopt:masterfrom
GeoffNN:sfw-importance-sampling

Conversation

@GeoffNN

@GeoffNN GeoffNN commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Importance sampling for the stochastic Frank-Wolfe variants

minimize_sfw's SAG-style estimator keeps one stale scalar per datapoint and refreshes a
uniformly sampled one each step. Its error constant is

Psi(q) = sum_{m>=1} max_j d_j (1 - q_j)^m,    d_j = max_{u,v in C} |a_j^T (u - v)|

where q_j is the probability of sampling datapoint j and d_j is how far that
datapoint's prediction can move across the constraint set. Uniform sampling gives exactly
Psi = (n-1) max_j d_j. Taking q_j proportional to d_j reduces it to about sum_j d_j
(up to a log factor) — a gain of max_j d_j / mean_j d_j, which is 1 on a homogeneous
design and large on a heavy-tailed one. For the l1 ball of radius alpha,
d_j = 2 * alpha * ||a_j||_inf.

What this adds

  • sfw_importance_probs(A, alpha, ord=1) — computes the weights for an l1 ball.
  • sampling_probs=None on minimize_sfw — pass the weights to use them.

Scope, and why

Restricted to the SAG and SAGA variants at batch_size=1. Both restrictions are real
rather than conservative: the batch sampler draws without replacement, which non-uniform
probabilities do not describe, and the analysis assumes unit batches. Probabilities must be
strictly positive — a datapoint that is never resampled keeps a stale gradient forever.

The improvement is asserted for both SAG and SAGA, but only after fixing a second thing —
see below. Psi(q) is derived for the SAG-style estimator, whose per-datapoint error decays at
rate q_j, so these weights are principled for SAG and merely effective for SAGA; they
are not claimed to be variance-optimal for the latter.

SAGA's correction had to be made unbiased first

SAGA rescaled the change in the sampled dual variable by the constant n_samples - 1. That
constant is 1/q_j - 1 evaluated at the uniform q_j = 1/n: the estimator is
grad_agg_prev + (1/(n q_j)) a_j (f'_j(fresh) - f'_j(stale)), and since dual_var already
carries the 1/n, the factor is 1/q_j — of which grad_agg has supplied 1. Left constant,
SAGA's gradient estimate is biased the moment sampling stops being uniform.

Computing the factor from the sampled probability restores unbiasedness and is exactly
equivalent under uniform sampling, so nothing changes when sampling_probs is not passed.
SAG is untouched either way — it reads grad_agg directly and never forms grad_est.

The effect is large. Mean suboptimality against a projected-gradient reference over 12 seeds,
importance sampling vs uniform:

epoch 30 epoch 100 epoch 300
before 1.4× (8/12 seeds) 1.1× (5/12) 1.6× (12/12)
after 13.7× (12/12) 34.0× (12/12) 85.1× (12/12)

Also documented, not fixed

step_size='DR' builds its certificate from the stochastic gap
<-grad_agg, update_direction>, which is not a lower bound on the true directional
derivative of the objective. It is therefore a heuristic step size rather than one backed by
a sufficient-decrease guarantee. This matters if anyone tries to build a backtracking line
search on it: when the stochastic gap overestimates the true gap, no Lipschitz estimate
satisfies the sufficient-decrease test and the loop diverges — observed reaching ~1e17
within a few thousand iterations. Only a docstring note here; no behaviour change.

Follow-up deliberately left out

A backtracking line search using the exact Frank-Wolfe gap. For this problem class the
exact gap costs O(n) rather than O(nd) — maintaining theta = A @ x makes both the
objective and the gap cheap, since the vertex is sparse. But evaluating the objective from
the linear predictions needs a value counterpart to partial_deriv, which the loss classes
do not have. Adding one to every loss class is an API decision that belongs to the
maintainer, so it is not in this PR. Happy to follow up if you want it.

Tests

pytest tests/test_stochastic_fw.py — 67 passed (60 pre-existing, 7 new).

A second, separable commit: the CI workflow

CI on this repo was red before this PR — master included. Every job died after
~3s at Set up Python with Version 3.9 with arch x64 not found, because
ubuntu-latest is now ubuntu-24.04 (no Python below 3.10 in the image) and
setup-python@v1 only consumes pre-installed interpreters instead of downloading
one. So there was no way to show this PR green without touching the workflow.

The second commit does the minimum for that: checkout@v1v4,
setup-python@v1v5, matrix 3.8/3.9/3.103.10/3.11/3.12, and
pipconflictcheckerpip check (pip-conflict-checker was last released in 2016
and imports pkg_resources, which setuptools 84 removed, so it now dies on
import). flake8 and pytest become explicit installs since they were only
arriving as transitive dependencies of the packages being dropped.

Result: 208 passed on each of 3.10, 3.11 and 3.12. It is a separate commit on
purpose — drop or rework it freely if you would rather fix CI on its own terms.

Attribution

The estimator and its analysis are from Négiar, Dresdner, Tsai, El Ghaoui, Locatello,
Freund and Pedregosa, Stochastic Frank-Wolfe for Constrained Finite-Sum Minimization,
ICML 2020 (arXiv:2002.11860). The importance-sampling
refinement is work in preparation by the same first author.


🤖 Generated with Claude Code

GeoffNN and others added 3 commits September 4, 2026 01:49
The rate of the SAG-style estimator in minimize_sfw is governed by the constant
Psi(q) = sum_m max_j d_j (1-q_j)^m, where d_j = max_{u,v in C} |a_j^T(u-v)| is
how far datapoint j's prediction can move across the constraint set. Uniform
sampling gives Psi = (n-1) max_j d_j; sampling proportionally to d_j reduces it
to roughly sum_j d_j, a gain of max_j d_j / mean_j d_j. That is 1 on a
homogeneous design and large on a heavy-tailed one.

minimize_sfw now takes sampling_probs, and sfw_importance_probs computes the
weights for an l1 ball. Restricted to the SAG and SAGA variants at batch_size=1,
since the batch sampler draws without replacement and the analysis assumes unit
batches; probabilities must be strictly positive, as a datapoint that is never
resampled keeps a stale gradient forever. On a heavy-tailed design SAG improves
on all 12 seeds tried, SAGA on 8 of 12 -- the guarantee describes the biased
SAG-style estimator, so only the former is asserted in the tests.

Also documents that the existing 'DR' step size builds its certificate from the
stochastic gap, which is not a lower bound on the true directional derivative,
and so is a heuristic rather than a sufficient-decrease guarantee.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every run on the repo currently fails in ~3s at "Set up Python" with
"Version 3.9 with arch x64 not found": ubuntu-latest is now ubuntu-24.04,
whose image ships no Python below 3.10, and setup-python@v1 only consumes
pre-installed interpreters rather than downloading one. This is unrelated
to any PR -- master is equally red.

- checkout@v1 -> v4, setup-python@v1 -> v5 (both were Node 12 actions).
- Matrix 3.8/3.9/3.10 -> 3.10/3.11/3.12. 3.8 and 3.9 are both EOL and
  neither is obtainable on ubuntu-24.04.
- pipconflictchecker -> pip check. pip-conflict-checker was last released
  in 2016 and imports pkg_resources, which setuptools 81 deprecated and
  setuptools 84 removed, so it now dies on import. pip check does the same
  job natively.
- flake8 and pytest are now installed explicitly. They were only reaching
  the runner as transitive dependencies of pip-conflict-checker and
  pytest-parallel, so dropping those would otherwise break the Lint step.
- Drop pytest-parallel. It is unmaintained and nothing passes --workers.

Verified locally on 3.12: pip check clean, flake8 clean, 208 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SAGA's update rescales the change in the sampled dual variable by a constant
n_samples - 1. That constant is 1/q_j - 1 evaluated at the uniform q_j = 1/n:
the estimator is grad_agg_prev + (1/(n q_j)) a_j (f'_j(fresh) - f'_j(stale)),
and with the 1/n already carried by dual_var the factor is 1/q_j, of which
grad_agg has already supplied 1. Left as a constant, SAGA's gradient estimate
is biased as soon as sampling_probs is not uniform.

Computing the factor from the sampled probability restores unbiasedness and is
exactly equivalent for uniform sampling, so nothing changes without
sampling_probs -- SAG is untouched either way, since it reads grad_agg directly
and never forms grad_est.

The effect on the heavy-tailed design in the tests is large. Mean suboptimality
against a projected-gradient reference over 12 seeds, importance sampling vs
uniform:

           epoch 30    epoch 100    epoch 300
  before       1.4x         1.1x         1.6x   (8/12, 5/12, 12/12 seeds)
  after       13.7x        34.0x        85.1x   (12/12 at all three)

So the improvement can now be asserted for both memory-based variants rather
than SAG alone, and the test is parametrized over the two. The docstring no
longer claims 'SAGA' is uncovered; it notes instead that Psi(q) is derived for
the SAG-style estimator, so d_j-proportional weights are principled for SAG and
merely effective for SAGA, not known to be variance-optimal there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@GeoffNN
GeoffNN marked this pull request as ready for review September 7, 2026 05:52
@fabianp

fabianp commented Sep 7, 2026

Copy link
Copy Markdown
Member

hmm something's weird. its trying to duplicate everything into a build/ directory and comitting that it seems

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants