Does critic reward scaling change SAC on Hopper-v4? A pre-registered null result

From-scratch PyTorch SAC · three arms · CPU · 120k steps/run · 3 seeds/arm

Repository: dafahaha/embodied-offpolicy-study · pre-registered 2026-10-01 before any Hopper result was inspected · independent recompute script: docs/assets/recompute_stats.py

Bottom line At 120k CPU steps and n = 3 seeds, the pre-registered contrast (Baseline vs RewardScaled) shows no detectable effect: Welch p = 0.953, mean difference −33.6 return points with a 95% CI of [−1540, 1473], Cohen's d = −0.05. One-way ANOVA across the three arms: F = 0.22, p = 0.811. This is "not detectable at this budget and variance", not "reward scaling has no effect".

1 · Motivation

SAC (Haarnoja et al., 2018) couples the Q-magnitude to the entropy temperature α through the max-entropy objective. A common engineering knob is to multiply the reward entering the critic's Bellman backup by a fixed reward_scale, which shrinks Q-target magnitude. The pre-registered hypothesis was that smaller Q targets reduce critic over-estimation bias and stabilise learning, yielding higher asymptotic deterministic return and faster early learning on Hopper-v4. The goal of the project was not to invent an algorithm: it was to implement SAC correctly from scratch, instrument it, run a controlled multi-seed measurement, and report whatever the data said.

2 · Pre-registered design

The hypothesis and primary outcome were written in PRE_REGISTRATION.md on 2026-10-01, immediately after Pendulum validation and before launching any Hopper run. The logged Bellman target in src/sac.py is:

y = reward_scale · r + γ(1 − done)·(min Qtarget − α·log π)

The logged episode return is always the unscaled env return, so arms are compared on the true objective.

Armreward_scaleentropy temperaturePurpose
Baseline1.0auto-tuned α (init 0.2)rewards as emitted by the env
RewardScaled0.1auto-tuned α (init 0.2)pre-registered arm: shrink Q targets
FixedAlpha0.1α = 0.2 frozen, no auto-tunepost-hoc control: separate reward scaling from the auto-tuned α trajectory

Shared settings across all arms (from configs/sac_hopper_*.yaml):

Pre-registered vs added later The primary confirmatory contrast is Baseline vs RewardScaled. The FixedAlpha arm, the three pairwise Welch tests, and the one-way ANOVA were added after the first 60k pass as exploratory post-hoc analyses and are uncorrected for multiple comparisons; they are reported descriptively only. See Methodology & honesty.

3 · Implementation & end-to-end validation

SAC is implemented from scratch in PyTorch (no Stable-Baselines3): uniform circular replay buffer, tanh-squashed diagonal-Gaussian actor whose analytic log-prob includes both the tanh Jacobian and the linear action-rescale Jacobian, twin critics with min-over-Q, soft target updates, and automatic entropy temperature. Before any Hopper run the loop was validated on Pendulum-v1: deterministic eval return improved from roughly −1600 to roughly −85 within 6k steps (per RESEARCH_RETRO.md). CI keeps a loose learn gate (Δ ≥ 150) that asserts the deterministic eval return actually improves on Pendulum rather than merely running.

4 · Results — measured null

Asymptotic window = last 4 eval points (105k–120k); each per-seed value is the mean of those four deterministic eval returns. Mean ± sample SD (ddof = 1), n = 3. Every figure below is recomputed independently from the committed raw CSVs by docs/assets/recompute_stats.py (machine-readable output: docs/assets/results.json).

Armper-seed tail means (s0, s1, s2)MeanSDt-95% half-width (df=2)
Baseline (scale 1.0, auto α)547.4, 1873.2, 833.11084.6697.7±1733.2
RewardScaled (scale 0.1, auto α)644.6, 886.1, 1823.81118.2622.9±1547.3
FixedAlpha (scale 0.1, α=0.2)2133.1, 1038.7, 1039.21403.6631.7±1569.2

Source: logs/hopper_{baseline,rewardscale,fixedalpha}_s{0,1,2}/progress.csv, phase == "eval", last 4 rows; recomputed by docs/assets/recompute_stats.py. (The README rounds these to 1085 ± 698, 1118 ± 623, 1404 ± 632.)

Pairwise Welch t-tests (scipy ttest_ind, equal_var = False)

Contrasttpmean diff95% CI (Welch)
Baseline vs RewardScaled (pre-registered)−0.0620.953−33.6[−1540.4, 1473.3]
Baseline vs FixedAlpha−0.5870.589−319.1[−1833.6, 1195.5]
FixedAlpha vs RewardScaled+0.5570.607+285.5[−1136.7, 1707.7]

One-way ANOVA (scipy.f_oneway): F = 0.217, p = 0.811. Primary-contrast effect size: Cohen's d = −0.051, Hedges' g = −0.041 (pooled SD 661.4) — a trivially small point estimate that is itself unreliable at n = 3.

Pre-registered secondary endpoint: early learning at 50k (descriptive only)

Hypothesis #2 expected higher eval return at step 50k (faster early learning). Reading the single 50k eval point per seed from the committed CSVs (recomputed by docs/assets/recompute_stats.py): Baseline ≈ 332 (357.1, 334.5, 303.4), RewardScaled ≈ 354 (307.5, 368.5, 385.6), FixedAlpha ≈ 667 (639.2, 332.8, 1028.4); n = 3, descriptive only, no inferential test. The pre-registered RewardScaled arm shows no early-learning advantage over Baseline; FixedAlpha's higher mean rests on one seed and belongs to the post-hoc arm.

SAC learning curves on Hopper-v4; shade is +/- 1 SD across seeds
Figure 1. Deterministic eval return over 120k steps; mean line across seeds, shaded ±1 SD. Produced by plot.py from the committed CSVs.
Asymptotic eval return: mean plus t-based 95% CI, dots are individual seeds
Figure 2. Asymptotic mean + t-based 95% CI (df = 2); dots are individual seeds. The intervals are wider than the arm-to-arm gaps, which is the quantitative content of the null.

Descriptive training dynamics (last logged train row per seed)

Reward scaling couples to the auto-tuned temperature, which is why the FixedAlpha control was added. From the committed CSVs: final α mean ≈ 0.080 (Baseline; per-seed 0.066, 0.106, 0.068) vs ≈ 0.0055 (RewardScaled; 0.0037, 0.0066, 0.0063), and FixedAlpha holds α = 0.2 by construction. Final mean Q₁: Baseline ≈ 170, RewardScaled ≈ 19, FixedAlpha ≈ 36. These are descriptive, not inferential.

5 · Why this null result is worth reporting

6 · The bug that taught the workflow lesson

The first Pendulum run appeared to learn nothing: Q-values went positive despite all-negative rewards, and deterministic eval returned the same bad number at every checkpoint. A 20-line diagnostic found log π exploding to ~1e15: the Gaussian log-prob was centred at zero x² instead of at the predicted mean (x − μ)², and that spurious gradient collapsed the policy. One-line fix, but the lesson was operational: check the quantities that must have the right sign/magnitude (Q sign here) before concluding "more data will fix it". The fix is locked by analytic log-prob tests in tests/test_core.py (including a non-zero-mean, non-unit-std case checked against scipy's multivariate-normal logpdf).

7 · Reproduction & tests

pip install -r requirements.txt

# unit tests (buffer, actor log-prob & Jacobian, critic shapes, one SAC update,
# reward_scale linearity, plot stats, learn gate, divergence guards)
pytest tests/ -q

# end-to-end learn gate on Pendulum (asserts eval improves, Delta >= 150)
python ci_check.py

# validation run (not part of the Hopper study)
python train.py --config configs/sac_pendulum.yaml --seed 0 --out logs/pendulum_val_s0

# one arm of the study (repeat per seed / config)
python train.py --config configs/sac_hopper_baseline.yaml    --seed 0 --out logs/hopper_baseline_s0
python train.py --config configs/sac_hopper_rewardscale.yaml --seed 0 --out logs/hopper_rewardscale_s0
python train.py --config configs/sac_hopper_fixedalpha.yaml  --seed 0 --out logs/hopper_fixedalpha_s0

# plots + summary stats from the committed CSVs
python plot.py

# independent recompute used on this page
python docs/assets/recompute_stats.py

Environment note: PRE_REGISTRATION.md planned gymnasium 1.3.0 / mujoco 3.14.0; the committed Hopper CSVs were produced on gymnasium 1.4.0 / mujoco 3.15.0 (pinned in requirements.txt). Hopper-v4 reward semantics are unchanged across these minor bumps; the discrepancy is logged in the README rather than retro-edited into the pre-registration.

8 · Honest limitations