Downloads · 30 days
3
2% of all-time downloads
datamatters24/attuned-resonance-router
attuned-resonance-router is a reinforcement learning model from datamatters24. Use it for the reinforcement learning task on the model card, and read the license before you ship it in a product. It is set up for stable-baselines3. The card lists the license as cc-by-nc-4.0.
The GitHub repo slug is still "CEPM" during the gradual rename; the Hugging Face slugs were migrated to attuned-resonance- on 2026-05-09 (HF preserves the old cepm- URLs as redirects).
Downloads · 30 days
3
2% of all-time downloads
All-time downloads
141
Public
Repo size
2.6 MB
Likes
0
Public
Click a slice to open those files.
.zip2.3 MB · 99%
From the Hugging Face model README
The GitHub repo slug is still "CEPM" during the gradual rename; the Hugging Face slugs were migrated to
attuned-resonance-*on 2026-05-09 (HF preserves the oldcepm-*URLs as redirects).
PPO agent (stable-baselines3) that selects an advisor for an incoming call, trained against the outcome predictor as a learned reward model.
Research/educational use only. See disclaimer below.
v3 update (2026-05): the v2 PPO run reported on this card was a baseline-equivalent policy — it routed validly but did not outperform the skill-weighted-random heuristic. v3 introduces Δ-reward shaping against that heuristic and reduces top-k pre-filtering from 10 to 50; the resulting policy beats the heuristic by +1.27 reward units per step on average, climbing monotonically across 250k timesteps. Full v2 root-cause analysis is preserved at the bottom of this card under "v2 → v3 history."
| v2 (50k steps, top_k=10, abs reward) | v3 (250k steps, top_k=50, Δ-reward) | |
|---|---|---|
| Reward sign | Absolute (positive only) | Δ vs heuristic (zero-centered) |
| Mean episode reward | flat ~171.5 | +1.27 (climbing, see curve) |
| Std across checkpoints | 0.05 (0.03% relative) | 0.13 over second-half evals |
| Beats heuristic? | No (tied within noise) | Yes (+1.27 > 0) |
A reward of +1.27 means: averaged over an episode, the policy's chosen advisor scored 1.27 composite-reward units better than the heuristic baseline's pick on the same call. Composite reward is a weighted sum over predicted FCR / handle-time / CSAT (weights from generator/config.py:routing_reward_weights), so a one-unit shift roughly corresponds to one full bucket of improvement on one of those axes.
call_intake (action label, urgency, complexity, ...) ┐
├──▶ CallRoutingEnv ──▶ PPO MlpPolicy ──▶ advisor_id
top_k=50 candidate advisors (filtered by ─┘ (action)
shift availability + skill match)
CallRoutingEnv — current call's intake features + 12-dim summary of each of the top-50 candidate advisors + queue features (~613-dim total)top_k=50 pre-filtered advisor candidatescomposite(predictor(chosen)) − composite(predictor(heuristic_pick))
composite = weighted sum of (1 − handle_time_normalized) + fcr_probability + csat/5 + workload_balance + burnout_protectionheuristic_pick reuses the skill-weighted-random sampling from generator/routing_generator.pyThe env uses the trained predictor as a simulator — rollouts are synthesized from predictor outputs rather than collected from real calls. Δ-reward calls the predictor twice per step (once for the policy's pick, once for the baseline's), which adds latency but is fast enough on CPU when env stepping is parallelized.
MlpPolicy (stable-baselines3 default)learning_rate=3e-4, n_steps=512, batch_size=64, n_epochs=10SubprocVecEnv(n_envs=4) with torch.set_num_threads(1) per subprocess (without that thread limit, vec-env gives no speedup — see "Engineering notes")cpu (MlpPolicy is small enough that GPU is wasted; env stepping dominates wall-clock)nice -n 19prod-router-v3CheckpointCallback(save_freq=25000) — every 25k timesteps a checkpoint is saved; the empirical peak was at step 225k (see "Checkpoint selection")| step | mean_episode_reward |
|---|---|
| 2,500 | −0.0159 |
| 27,500 | +0.1803 |
| 52,500 | +0.5019 |
| 77,500 | +0.6876 |
| 102,500 | +0.8230 |
| 127,500 | +0.9433 |
| 152,500 | +1.0239 |
| 177,500 | +1.1472 |
| 202,500 | +1.2535 |
| 227,500 | +1.3386 |
| 250,000 | +1.2705 |
100 evaluation points logged at eval_freq=2500 callback-call units; only every 10th is shown above. Curve is monotonic up to ~225k steps; the last ~25k drift downward by ~0.05, which is small but real (see "Limitations").
The shipped ppo_router.zip is the final model at step 250k (mean reward +1.2705). The peak checkpoint (ppo_router_peak_225000_steps.zip, also uploaded) is at step 225k with a window-mean reward of +1.3284. Either is suitable for inference; we ship the final by convention but document both for reproducibility.
The peak window mean (+1.3284 at step 225k) is ~0.05 higher than the final model (+1.2801 at step 250k). This is small but consistent — the curve genuinely turns over in the last ~25k steps. Plausible causes:
For applications where the absolute peak matters, prefer the 225k checkpoint. For "final policy" comparability with the conventional training-end model, use the 250k.
Two issues that ate session-days during v3 and are worth flagging:
torch.set_num_threads(1) inside CallRoutingEnv.__init__. Without it, each SubprocVecEnv subprocess spawns ~num_cores PyTorch threads via OpenMP/MKL, and n_envs=4 × ~16 threads each = 64 threads competing for 16 cores. Net effect: vec env produces zero speedup (or worse — context-switch overhead). With the fix, parallelism actually scales. Setting it at module level does not help — the call must run inside the subprocess.
CheckpointCallback.save_freq is in callback-call units, not timestep units. With n_envs=4, you must pass save_freq // n_envs to get checkpoints at the timesteps you intend.
Both issues now have notes in the project's auto-memory and are documented in source-code comments.
The v1/v2 model on this same env converged to a flat reward of ~171.5 with std ~0.05 across 50k timesteps — a non-learning policy. The root cause analysis at the time identified three issues:
top_k=10 pre-filtering hides advisor differentiation — the policy was being asked to discriminate among ~10 already-well-matched candidates, where predicted outcomes are tight. v3 bumps this to 50.reward = composite(chosen) − composite(heuristic_pick).v3's Tier 1 implements (1) and (2)+(3); the result is the curve above. Tier 2 (reward decomposition into separate FCR / handle-time / CSAT channels, multi-objective continuous action) and Tier 3 (curriculum learning, predictor uncertainty as auxiliary reward) remain future work and would address how the policy generalizes rather than whether it learns.
from stable_baselines3 import PPO
model = PPO.load("ppo_router.zip")
# Inference: action is the index of the chosen advisor in the top-k candidate list,
# not a global advisor_id. Pair with the env to map back to the advisor pool.
# IMPORTANT: must construct the env with top_k=50 to match the training-time
# observation/action shapes (a saved PPO model encodes those shapes).
obs, _ = env.reset()
action, _ = model.predict(obs, deterministic=True)
Full pipeline code (env, training loop, inference) at tedrubin80/CEPM.
CC-BY-NC-4.0. Non-commercial use only. Attribution required.
@software{attuned_resonance_router_2026,
author = {Rubin, Ted},
title = {Attuned Resonance RL Router: PPO Agent for Call-Center Routing (v3)},
year = {2026},
url = {https://github.com/tedrubin80/CEPM},
note = {v3 reward shaping (Δ-reward + top_k=50) produces a policy that beats the skill-weighted-random heuristic by +1.27 reward units per step over 250k timesteps.}
}