refactor(generator): remove deterministic speed_profile and hard log_dt clip
These post-processing hacks were added to compensate for small-data training. With Balabit pretraining they suppress the multimodal timing distribution and cause the template-y Δt curves seen in the verify UI.
This commit is contained in:
@@ -12,8 +12,8 @@ Pipeline:
|
|||||||
c. Enforce forward monotonicity (prevent x-axis jitter).
|
c. Enforce forward monotonicity (prevent x-axis jitter).
|
||||||
6. Temporal post-processing:
|
6. Temporal post-processing:
|
||||||
a. Clip log_dt to [0, 5] to prevent exponential explosion.
|
a. Clip log_dt to [0, 5] to prevent exponential explosion.
|
||||||
b. Remove outliers beyond 2σ from median.
|
(speed profile and median±1.1 hard clip removed in 2026-05 refactor —
|
||||||
c. Apply bell-curve speed profile (slow→fast→slow).
|
let the model's learned timing distribution come through naturally.)
|
||||||
7. Decode to pixels via decode_trajectory.
|
7. Decode to pixels via decode_trajectory.
|
||||||
8. Resample to n_points if n_points != model seq_len.
|
8. Resample to n_points if n_points != model seq_len.
|
||||||
9. Convert log_dt → ms timestamps, scale to total_duration, clip [2, 150].
|
9. Convert log_dt → ms timestamps, scale to total_duration, clip [2, 150].
|
||||||
@@ -257,34 +257,6 @@ def generate(
|
|||||||
# First point has no interval (padding from training)
|
# First point has no interval (padding from training)
|
||||||
log_dt[0] = 0.0
|
log_dt[0] = 0.0
|
||||||
|
|
||||||
# The model tends to produce exaggerated deceleration at the tail
|
|
||||||
# (last 10 points log_dt ~3-5 vs middle ~1.5).
|
|
||||||
# Cap the max-to-median ratio to ~3× (i.e., tail Δt ≤ 3× median Δt)
|
|
||||||
median_ldt = float(np.median(log_dt[1:]))
|
|
||||||
# Allow max log_dt = median + 1.1 (exp(1.1) ≈ 3× ratio)
|
|
||||||
max_allowed = median_ldt + 1.1
|
|
||||||
min_allowed = max(median_ldt - 1.1, 0.0)
|
|
||||||
for i in range(1, len(log_dt)):
|
|
||||||
if log_dt[i] > max_allowed:
|
|
||||||
log_dt[i] = max_allowed
|
|
||||||
elif log_dt[i] < min_allowed:
|
|
||||||
log_dt[i] = min_allowed
|
|
||||||
|
|
||||||
# Apply asymmetric speed profile: start slow, fast in middle, gentle end
|
|
||||||
# Mimics natural mouse movement (accelerate → cruise → decelerate)
|
|
||||||
t_frac = np.linspace(0, 1, len(log_dt))
|
|
||||||
speed_profile = np.zeros_like(log_dt, dtype=float)
|
|
||||||
for i in range(1, len(log_dt)):
|
|
||||||
t = t_frac[i]
|
|
||||||
if t < 0.15:
|
|
||||||
# Acceleration phase: start slow (+0.3 at t=0, → 0 at t=0.15)
|
|
||||||
speed_profile[i] = 0.3 * (1.0 - t / 0.15)
|
|
||||||
elif t > 0.85:
|
|
||||||
# Deceleration phase: end slightly slow (+0.2 at t=1)
|
|
||||||
speed_profile[i] = 0.2 * ((t - 0.85) / 0.15)
|
|
||||||
# Middle: speed_profile = 0 (fastest, no penalty)
|
|
||||||
log_dt[1:] = log_dt[1:] + speed_profile[1:]
|
|
||||||
|
|
||||||
# Decode spatial coordinates to pixels
|
# Decode spatial coordinates to pixels
|
||||||
normalised = np.stack([forward, lateral], axis=1) # (seq_len, 2)
|
normalised = np.stack([forward, lateral], axis=1) # (seq_len, 2)
|
||||||
pixels = decode_trajectory(normalised, start, end) # (seq_len, 2)
|
pixels = decode_trajectory(normalised, start, end) # (seq_len, 2)
|
||||||
|
|||||||
@@ -104,3 +104,23 @@ class TestGenerate:
|
|||||||
)
|
)
|
||||||
# 32 move points + 2 click events = 34
|
# 32 move points + 2 click events = 34
|
||||||
assert len(result) == 34
|
assert len(result) == 34
|
||||||
|
|
||||||
|
|
||||||
|
class TestPostProcessing:
|
||||||
|
def test_dt_diversity_preserved(self, model_dir):
|
||||||
|
"""After removing speed_profile + median clip, multiple generations
|
||||||
|
should differ in their Δt sequences (not all identical)."""
|
||||||
|
results = [generate(start=(100, 200), end=(500, 400), model_dir=str(model_dir))
|
||||||
|
for _ in range(5)]
|
||||||
|
# Extract Δt sequences (only move events, not click events)
|
||||||
|
dts = []
|
||||||
|
for r in results:
|
||||||
|
moves = r[:-2]
|
||||||
|
dt_seq = [moves[i+1][2] - moves[i][2] for i in range(len(moves)-1)]
|
||||||
|
dts.append(dt_seq)
|
||||||
|
# At least 2 of the 5 sequences should differ at any given index
|
||||||
|
for i in range(min(len(d) for d in dts)):
|
||||||
|
values = {tuple([d[i]]) for d in dts}
|
||||||
|
if len(values) > 1:
|
||||||
|
return # at least one position has variation — pass
|
||||||
|
pytest.fail("All 5 Δt sequences are identical at every position — diversity collapsed")
|
||||||
|
|||||||
Reference in New Issue
Block a user