Spaces:
Paused
Paused
Commit ·
1ac70ff
1
Parent(s): 8fdc935
Fix GRPO all-zero rewards: parse JSON actions from completions
Browse filesRemove environment_factory from GRPOTrainer — reward_func now parses
JSON action objects from model text completions via regex and executes
them in fresh HCM21Env instances. This bypasses TRL's tool-calling
infrastructure which fails with a 0.6B model + Unsloth patches.
Changes:
- SYSTEM_PROMPT: instruct model to output JSON action lines
- reward_func: parse actions, run in env, return scores
- GRPOTrainer: remove environment_factory arg
- Evaluation: single-shot generation matching training format
- Fix BatchEncoding .shape bug in eval cell
- Add CLAUDE.md with GRPO training architecture docs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CLAUDE.md +177 -0
- notebooks/hcm21_trl_training.ipynb +5 -125
CLAUDE.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HR Productivity Environment — AgentX-AgentBeats
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
A fully-implemented HR simulation environment (OpenEnv-compliant) where an LLM agent serves as Chief Human Capital Officer (CHCO), managing a simulated company over 6 quarters. The agent uses Fitz-enz HR Analytics frameworks to optimize human capital metrics through strategic hiring, training, compensation, and retention decisions.
|
| 5 |
+
|
| 6 |
+
## Repository Structure
|
| 7 |
+
```
|
| 8 |
+
/tmp/autoresearch/
|
| 9 |
+
├── hr_env/
|
| 10 |
+
│ └── server/
|
| 11 |
+
│ ├── environment.py # Main simulation loop (OpenEnv Environment)
|
| 12 |
+
│ ├── company.py # Company + Department models (Cobb-Douglas production)
|
| 13 |
+
│ ├── employee.py # Employee dataclass with lifecycle methods
|
| 14 |
+
│ ├── metrics.py # Fitz-enz metrics: HCVA, HCROI, QIPS, Five Indexes
|
| 15 |
+
│ ├── scoring.py # Reward computation + final score (normalized [0,1])
|
| 16 |
+
│ ├── phases.py # HCM:21 phase management (Scanning→Planning→Producing→Controlling)
|
| 17 |
+
│ ├── events.py # Stochastic quarterly events (market, competitor, etc.)
|
| 18 |
+
│ ├── data_gen.py # Synthetic company generation (Faker + numpy)
|
| 19 |
+
│ └── app.py # OpenEnv server entrypoint
|
| 20 |
+
├── agent/
|
| 21 |
+
│ ├── hr_agent.py # Claude-based CHCO agent with phase-aware prompting
|
| 22 |
+
│ ├── prompts.py # System prompt, phase prompts, summary prompts
|
| 23 |
+
│ └── run.py # Agent runner script
|
| 24 |
+
├── tests/ # Test suite
|
| 25 |
+
├── openenv.yaml # OpenEnv configuration
|
| 26 |
+
├── pyproject.toml # Python project config (uv)
|
| 27 |
+
└── Dockerfile # Container build
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
## Development Commands
|
| 31 |
+
```bash
|
| 32 |
+
# Install dependencies
|
| 33 |
+
uv sync
|
| 34 |
+
|
| 35 |
+
# Run tests
|
| 36 |
+
uv run pytest tests/ -v
|
| 37 |
+
|
| 38 |
+
# Start OpenEnv server
|
| 39 |
+
uv run python -m hr_env.server.app
|
| 40 |
+
|
| 41 |
+
# Run agent against server
|
| 42 |
+
uv run python -m agent.run
|
| 43 |
+
|
| 44 |
+
# Docker build
|
| 45 |
+
docker build -t hr-productivity-env .
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
## Architecture
|
| 49 |
+
|
| 50 |
+
### Simulation Engine
|
| 51 |
+
- **Company Model**: 5 departments (Engineering, Sales, Operations, HR, Finance) with Cobb-Douglas production functions
|
| 52 |
+
- **Employee Model**: 17 tracked attributes including performance, engagement, flight_risk, promotability
|
| 53 |
+
- **Revenue**: `A * headcount^alpha * avg_skill^beta * engagement_mult * market_modifier` per department
|
| 54 |
+
- **Turnover**: Stochastic based on flight_risk (logistic function of pay, engagement, tenure, market)
|
| 55 |
+
|
| 56 |
+
### Fitz-enz Metrics
|
| 57 |
+
- **HCVA**: (Revenue - Non-Employment Costs) / FTE
|
| 58 |
+
- **HCROI**: Revenue / Employment Cost
|
| 59 |
+
- **QIPS**: Quality(0.25) + Innovation(0.25) + Productivity(0.30) + Service(0.20)
|
| 60 |
+
- **Five Indexes**: Cost, Time, Quantity, Quality, Human Reactions (% change vs prior quarter)
|
| 61 |
+
- **Employee Value**: avg(Productivity + Promotability + Transferability + Retainability)
|
| 62 |
+
|
| 63 |
+
### HCM:21 Phase System (per quarter)
|
| 64 |
+
1. **Scanning** (min 2 actions): query_department, query_employees, calculate_metric, review_financials
|
| 65 |
+
2. **Planning** (min 1): set_hiring_target, set_training_budget, set_compensation_policy, set_retention_program
|
| 66 |
+
3. **Producing** (min 1): execute_hiring, execute_promotion, execute_transfer, execute_training, execute_termination
|
| 67 |
+
4. **Controlling** (min 1): submit_report, calculate_metric → then advance_quarter
|
| 68 |
+
|
| 69 |
+
### Scoring
|
| 70 |
+
- **Quarterly reward**: HCVA improvement (40%) + HCROI (20%) + QIPS (20%) + Five Indexes (20%)
|
| 71 |
+
- **Final score** [0,1]: HCVA trajectory (25%) + HCROI improvement (20%) + Employee Value (20%) + QIPS consistency (15%) + Five Indexes cumulative (10%) + Financial health (10%)
|
| 72 |
+
|
| 73 |
+
### Stochastic Events
|
| 74 |
+
MarketDownturn, CompetitorPoaching, ProductLaunch, ExecDeparture, MinWageIncrease, EmployerAward, BudgetCut, TrainingBreakthrough — each with independent probability per quarter, max 2 per quarter.
|
| 75 |
+
|
| 76 |
+
## Databricks Integration
|
| 77 |
+
The simulation is adapted into a Databricks notebook (`/tmp/AimpointDigital/01_agentic_wikipedia_aimpoint_interview.ipynb`) with:
|
| 78 |
+
- **Wikipedia knowledge layer**: HR theory articles (Fitz-enz, HCM, talent management) indexed into FAISS via BGE-Large embeddings
|
| 79 |
+
- **LangGraph orchestration**: StateGraph with 5 nodes (initialize → agent_decide → execute_tools → check_done → summarize)
|
| 80 |
+
- **RAG bridge**: `consult_hr_knowledge` tool lets the agent ground decisions in Wikipedia-sourced HR theory
|
| 81 |
+
- **Databricks LLM**: `databricks-meta-llama-3-1-8b-instruct` for agent reasoning
|
| 82 |
+
|
| 83 |
+
## Key Conventions
|
| 84 |
+
- Pydantic models for OpenEnv; plain dataclasses/dicts for Databricks adaptation
|
| 85 |
+
- Phase-gated action space prevents random action sequences
|
| 86 |
+
- Company generation uses Faker (names) + numpy (distributions) with deterministic seeds
|
| 87 |
+
- Salary distributions: lognormal; tenure: exponential; engagement: beta(7,3)
|
| 88 |
+
- Level distribution: pyramid [0.35, 0.30, 0.20, 0.10, 0.05] for levels 1-5
|
| 89 |
+
- Recruiting cost: $8,000/hire; Benefits: 30% of salary; Training: $50/hr/employee
|
| 90 |
+
|
| 91 |
+
## Databricks Deployment
|
| 92 |
+
|
| 93 |
+
### Workspace
|
| 94 |
+
- **URL**: `https://dbc-c005fe3a-5584.cloud.databricks.com`
|
| 95 |
+
- **Org ID**: `7474655330631867`
|
| 96 |
+
- **Target Folder ID**: `1876640772048528`
|
| 97 |
+
- **Folder URL**: `https://dbc-c005fe3a-5584.cloud.databricks.com/browse/folders/1876640772048528?o=7474655330631867`
|
| 98 |
+
- **PAT**: (stored in env / .databrickscfg — not committed)
|
| 99 |
+
- **User**: `n8.mauer@gmail.com`
|
| 100 |
+
- **Edition**: Community Edition (Free)
|
| 101 |
+
- **LLM endpoint**: `databricks-meta-llama-3-1-8b-instruct`
|
| 102 |
+
- **Embedding endpoint**: `databricks-bge-large-en`
|
| 103 |
+
|
| 104 |
+
### .env file (for local development)
|
| 105 |
+
```
|
| 106 |
+
DATABRICKS_HOST=https://dbc-c005fe3a-5584.cloud.databricks.com
|
| 107 |
+
DATABRICKS_TOKEN=<your-token>
|
| 108 |
+
DATABRICKS_ORG_ID=7474655330631867
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
### Cold-Start Setup
|
| 112 |
+
1. `pip install databricks-cli`
|
| 113 |
+
2. Write `~/.databrickscfg`:
|
| 114 |
+
```ini
|
| 115 |
+
[DEFAULT]
|
| 116 |
+
host = https://dbc-c005fe3a-5584.cloud.databricks.com
|
| 117 |
+
token = <your-token>
|
| 118 |
+
```
|
| 119 |
+
3. Deploy notebook:
|
| 120 |
+
```bash
|
| 121 |
+
databricks workspace import \
|
| 122 |
+
"/tmp/AimpointDigital/01_agentic_wikipedia_aimpoint_interview.ipynb" \
|
| 123 |
+
/Users/n8.mauer@gmail.com/01_agentic_wikipedia_aimpoint_interview \
|
| 124 |
+
--format JUPYTER --language PYTHON --overwrite
|
| 125 |
+
```
|
| 126 |
+
4. Open workspace URL → attach cluster → Run All
|
| 127 |
+
|
| 128 |
+
### Deploy Script
|
| 129 |
+
```bash
|
| 130 |
+
./deploy.sh # from /tmp/AimpointDigital/
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
### Runtime Fixes Applied
|
| 134 |
+
- **Rate limiting**: `agent_decide` LLM calls wrapped with `backoff.on_exception` (exponential, max 8 retries, 120s) + 1.5s proactive delay between calls. Community Edition has low QPS for foundation model endpoints.
|
| 135 |
+
- **MLflow tracing**: `mlflow.langchain.autolog()` added at top of Cell A2 to enable tracing and suppress Databricks "Enhance GenAI observability" hint. `mlflow-skinny[databricks]` already in pip install cell.
|
| 136 |
+
|
| 137 |
+
### GitHub Repo
|
| 138 |
+
- URL: `https://github.com/n8mauer/AimpointDigital`
|
| 139 |
+
- Contains: notebook, build script, deploy script, README (runbook + talk track), assignment PDF
|
| 140 |
+
|
| 141 |
+
## TRL/GRPO Training (`notebooks/hcm21_trl_training.ipynb`)
|
| 142 |
+
|
| 143 |
+
### Architecture
|
| 144 |
+
- **Model**: `unsloth/Qwen3-0.6B` with LoRA (r=16, 4-bit quantization)
|
| 145 |
+
- **Method**: GRPO (Group Relative Policy Optimization) via TRL's `GRPOTrainer`
|
| 146 |
+
- **Environment**: HCM:21 OpenEnv on HF Spaces (`https://paretooptimal-hcm21.hf.space`)
|
| 147 |
+
- **Colab URL**: `https://colab.research.google.com/github/n8mauer/autoresearch/blob/main/notebooks/hcm21_trl_training.ipynb`
|
| 148 |
+
|
| 149 |
+
### GRPO Reward Strategy (JSON action parsing — no environment_factory)
|
| 150 |
+
The model generates text completions containing JSON action objects (one per line).
|
| 151 |
+
`reward_func` parses these with regex (`\{[^{}]*"action_type"[^{}]*\}`), executes
|
| 152 |
+
them in a fresh `HCM21Env` instance, and returns the environment's final score.
|
| 153 |
+
|
| 154 |
+
**Why not `environment_factory`?**
|
| 155 |
+
- TRL's `environment_factory` exposes env methods as tools for tool-calling, but a 0.6B model can't reliably generate structured tool calls
|
| 156 |
+
- `environment_factory` requires `transformers>=5.2.0` which conflicts with Unsloth patches
|
| 157 |
+
- Disabling Qwen3 thinking mode via `chat_template.replace("enable_thinking", "")` may break tool-calling templates
|
| 158 |
+
- Direct JSON parsing in `reward_func` is simpler, more robust, and works with Unsloth
|
| 159 |
+
|
| 160 |
+
### Key Implementation Details
|
| 161 |
+
- `SYSTEM_PROMPT` instructs the model to output 20-40 JSON action objects covering 6 quarters
|
| 162 |
+
- `reward_func(completions, **kwargs)` — no `environments` param (environments created internally)
|
| 163 |
+
- `GRPOTrainer` has NO `environment_factory` argument
|
| 164 |
+
- Evaluation (cell 20) generates one completion per seed, parses all actions, executes them
|
| 165 |
+
- `tokenizer.apply_chat_template()` may return `BatchEncoding` — extract `.input_ids` before using `.shape`
|
| 166 |
+
|
| 167 |
+
### Training Config
|
| 168 |
+
- `per_device_train_batch_size=2`, `gradient_accumulation_steps=4`, `num_generations=4`
|
| 169 |
+
- `max_completion_length=2048`, `learning_rate=5e-6`, `fp16=True`
|
| 170 |
+
- Dataset: 20 prompts with different episode seeds
|
| 171 |
+
|
| 172 |
+
### Verification Checklist
|
| 173 |
+
After running in Colab, confirm:
|
| 174 |
+
- `reward` column shows **non-zero values**
|
| 175 |
+
- `reward_std` is **non-zero** (learning signal exists)
|
| 176 |
+
- `Training Loss` is **non-zero** (GRPO has gradients)
|
| 177 |
+
- Rewards trend upward over training steps
|
notebooks/hcm21_trl_training.ipynb
CHANGED
|
@@ -301,39 +301,7 @@
|
|
| 301 |
"execution_count": null,
|
| 302 |
"metadata": {},
|
| 303 |
"outputs": [],
|
| 304 |
-
"source": [
|
| 305 |
-
"from datasets import Dataset\n",
|
| 306 |
-
"\n",
|
| 307 |
-
"SYSTEM_PROMPT = \"\"\"You are the Chief Human Capital Officer (CHCO) of a simulated company.\n",
|
| 308 |
-
"You manage 300 employees across 5 departments over 6 quarters.\n",
|
| 309 |
-
"Each quarter has 4 phases: Scanning, Planning, Producing, Controlling.\n",
|
| 310 |
-
"Maximize HCVA, HCROI, and QIPS metrics by making strategic HR decisions.\n",
|
| 311 |
-
"\n",
|
| 312 |
-
"When calling the step tool, provide a JSON action object like:\n",
|
| 313 |
-
"{\"action_type\": \"query_department\", \"department\": \"Engineering\"}\n",
|
| 314 |
-
"\n",
|
| 315 |
-
"Available action types vary by phase:\n",
|
| 316 |
-
"- Scanning: query_department, query_employees, calculate_metric, review_financials\n",
|
| 317 |
-
"- Planning: set_hiring_target, set_training_budget, set_compensation_policy, set_retention_program\n",
|
| 318 |
-
"- Producing: execute_hiring, execute_promotion, execute_transfer, execute_training, execute_termination\n",
|
| 319 |
-
"- Controlling: submit_report, calculate_metric, advance_quarter\n",
|
| 320 |
-
"- advance_phase: move to next phase (after minimum actions taken)\n",
|
| 321 |
-
"\n",
|
| 322 |
-
"Think step by step. Start by scanning each department.\"\"\"\n",
|
| 323 |
-
"\n",
|
| 324 |
-
"# Create prompts with different seeds for diversity\n",
|
| 325 |
-
"prompts = []\n",
|
| 326 |
-
"for seed in range(20):\n",
|
| 327 |
-
" prompts.append({\n",
|
| 328 |
-
" \"prompt\": [\n",
|
| 329 |
-
" {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n",
|
| 330 |
-
" {\"role\": \"user\", \"content\": f\"Begin managing the company. Episode seed: {seed}. Start by scanning your organization to understand its current state.\"},\n",
|
| 331 |
-
" ]\n",
|
| 332 |
-
" })\n",
|
| 333 |
-
"\n",
|
| 334 |
-
"dataset = Dataset.from_list(prompts)\n",
|
| 335 |
-
"print(f\"Training dataset: {len(dataset)} episodes\")"
|
| 336 |
-
]
|
| 337 |
},
|
| 338 |
{
|
| 339 |
"cell_type": "markdown",
|
|
@@ -401,40 +369,17 @@
|
|
| 401 |
"execution_count": null,
|
| 402 |
"metadata": {},
|
| 403 |
"outputs": [],
|
| 404 |
-
"source": [
|
| 405 |
-
"def reward_func(completions, environments, **kwargs):\n",
|
| 406 |
-
" \"\"\"Extract rewards from the HCM:21 environment after each rollout.\"\"\"\n",
|
| 407 |
-
" rewards = []\n",
|
| 408 |
-
" for env in environments:\n",
|
| 409 |
-
" r = env.get_reward()\n",
|
| 410 |
-
" rewards.append(r)\n",
|
| 411 |
-
" return rewards\n",
|
| 412 |
-
"\n",
|
| 413 |
-
"print(\"Reward function defined.\")"
|
| 414 |
-
]
|
| 415 |
},
|
| 416 |
{
|
| 417 |
"cell_type": "markdown",
|
| 418 |
"metadata": {},
|
| 419 |
-
"source":
|
| 420 |
-
"## 6. Configure and Run GRPO Training\n",
|
| 421 |
-
"\n",
|
| 422 |
-
"We use TRL's `GRPOTrainer` with `environment_factory=HCM21Env`.\n",
|
| 423 |
-
"The model generates tool calls to interact with the environment,\n",
|
| 424 |
-
"and GRPO optimizes the policy based on the environment rewards.\n",
|
| 425 |
-
"\n",
|
| 426 |
-
"On a free T4 GPU, we use:\n",
|
| 427 |
-
"- Small batch size (2) with gradient accumulation\n",
|
| 428 |
-
"- 4-bit quantization via Unsloth\n",
|
| 429 |
-
"- Short max_completion_length to fit in memory\n",
|
| 430 |
-
"\n",
|
| 431 |
-
"After training, we plot the reward trajectory to visualize learning progress."
|
| 432 |
-
]
|
| 433 |
},
|
| 434 |
{
|
| 435 |
"cell_type": "code",
|
| 436 |
"metadata": {},
|
| 437 |
-
"source": "from trl import GRPOTrainer, GRPOConfig\nimport os\n\n# Disable Qwen3 thinking mode at the tokenizer level (Unsloth-compatible)\ntokenizer.chat_template = tokenizer.chat_template.replace(\"enable_thinking\", \"\")\n\nos.environ[\"TENSORBOARD_LOGGING_DIR\"] = \"./hcm21-grpo-logs\"\n\ntraining_args = GRPOConfig(\n output_dir=\"./hcm21-grpo-output\",\n num_train_epochs=1,\n per_device_train_batch_size=2,\n gradient_accumulation_steps=4,\n num_generations=4,\n max_completion_length=2048,\n learning_rate=5e-6,\n logging_steps=1,\n save_steps=10,\n fp16=True,\n)\n\ntrainer = GRPOTrainer(\n model=model,\n processing_class=tokenizer,\n train_dataset=dataset,\n reward_funcs=reward_func,\n
|
| 438 |
"outputs": [],
|
| 439 |
"execution_count": null
|
| 440 |
},
|
|
@@ -476,72 +421,7 @@
|
|
| 476 |
{
|
| 477 |
"cell_type": "code",
|
| 478 |
"metadata": {},
|
| 479 |
-
"source": [
|
| 480 |
-
"import json\n",
|
| 481 |
-
"\n",
|
| 482 |
-
"FastLanguageModel.for_inference(model)\n",
|
| 483 |
-
"\n",
|
| 484 |
-
"def run_trained_episode(seed, max_steps=200):\n",
|
| 485 |
-
" \"\"\"Run one episode with the trained model, return final score.\"\"\"\n",
|
| 486 |
-
" env = HCM21Env()\n",
|
| 487 |
-
" try:\n",
|
| 488 |
-
" obs_text = env.reset(seed=seed)\n",
|
| 489 |
-
" messages = [\n",
|
| 490 |
-
" {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n",
|
| 491 |
-
" {\"role\": \"user\", \"content\": f\"Begin. Observation:\\n{obs_text}\"},\n",
|
| 492 |
-
" ]\n",
|
| 493 |
-
" step_num = 0\n",
|
| 494 |
-
" while not env._done and step_num < max_steps:\n",
|
| 495 |
-
" inputs = tokenizer.apply_chat_template(\n",
|
| 496 |
-
" messages, return_tensors=\"pt\", add_generation_prompt=True\n",
|
| 497 |
-
" ).to(model.device)\n",
|
| 498 |
-
" outputs = model.generate(\n",
|
| 499 |
-
" input_ids=inputs, max_new_tokens=256,\n",
|
| 500 |
-
" temperature=0.7, do_sample=True,\n",
|
| 501 |
-
" )\n",
|
| 502 |
-
" response = tokenizer.decode(outputs[0][inputs.shape[1]:], skip_special_tokens=True)\n",
|
| 503 |
-
" try:\n",
|
| 504 |
-
" start = response.index(\"{\")\n",
|
| 505 |
-
" depth = 0\n",
|
| 506 |
-
" for i, c in enumerate(response[start:], start):\n",
|
| 507 |
-
" if c == \"{\": depth += 1\n",
|
| 508 |
-
" elif c == \"}\": depth -= 1\n",
|
| 509 |
-
" if depth == 0:\n",
|
| 510 |
-
" action_json = response[start:i+1]\n",
|
| 511 |
-
" break\n",
|
| 512 |
-
" obs_text = env.step(action_json)\n",
|
| 513 |
-
" except (ValueError, json.JSONDecodeError):\n",
|
| 514 |
-
" obs_text = env.step(json.dumps({\"action_type\": \"advance_phase\"}))\n",
|
| 515 |
-
" messages.append({\"role\": \"assistant\", \"content\": response})\n",
|
| 516 |
-
" messages.append({\"role\": \"user\", \"content\": obs_text})\n",
|
| 517 |
-
" if len(messages) > 12:\n",
|
| 518 |
-
" messages = messages[:2] + messages[-10:]\n",
|
| 519 |
-
" step_num += 1\n",
|
| 520 |
-
" return env.get_reward(), step_num\n",
|
| 521 |
-
" finally:\n",
|
| 522 |
-
" env.close()\n",
|
| 523 |
-
"\n",
|
| 524 |
-
"print(\"Evaluating trained agent on baseline seeds...\")\n",
|
| 525 |
-
"trained_scores = []\n",
|
| 526 |
-
"for seed in BASELINE_SEEDS:\n",
|
| 527 |
-
" score, steps = run_trained_episode(seed)\n",
|
| 528 |
-
" trained_scores.append(score)\n",
|
| 529 |
-
" print(f\" Seed {seed:>3d}: {score:.4f} ({steps} steps)\")\n",
|
| 530 |
-
"\n",
|
| 531 |
-
"sep = \"=\" * 50\n",
|
| 532 |
-
"print(f\"\\n{sep}\")\n",
|
| 533 |
-
"print(f\" RESULTS COMPARISON\")\n",
|
| 534 |
-
"print(f\"{sep}\")\n",
|
| 535 |
-
"print(f\" Random Agent: {np.mean(random_scores):.4f} +/- {np.std(random_scores):.4f}\")\n",
|
| 536 |
-
"print(f\" Heuristic Agent: {np.mean(heuristic_scores):.4f} +/- {np.std(heuristic_scores):.4f}\")\n",
|
| 537 |
-
"print(f\" Trained Agent: {np.mean(trained_scores):.4f} +/- {np.std(trained_scores):.4f}\")\n",
|
| 538 |
-
"print(f\"{sep}\")\n",
|
| 539 |
-
"\n",
|
| 540 |
-
"improvement = np.mean(trained_scores) - np.mean(random_scores)\n",
|
| 541 |
-
"print(f\"\\n Improvement over random: +{improvement:.4f} ({improvement/max(np.mean(random_scores), 0.01)*100:.1f}%)\")\n",
|
| 542 |
-
"if np.mean(trained_scores) > np.mean(random_scores):\n",
|
| 543 |
-
" print(\" >>> Training shows positive improvement! <<<\")"
|
| 544 |
-
],
|
| 545 |
"outputs": [],
|
| 546 |
"execution_count": null
|
| 547 |
},
|
|
|
|
| 301 |
"execution_count": null,
|
| 302 |
"metadata": {},
|
| 303 |
"outputs": [],
|
| 304 |
+
"source": "from datasets import Dataset\n\nSYSTEM_PROMPT = \"\"\"You are the Chief Human Capital Officer (CHCO) of a simulated company.\nYou manage 300 employees across 5 departments over 6 quarters.\nEach quarter has 4 phases: Scanning, Planning, Producing, Controlling.\nMaximize HCVA, HCROI, and QIPS metrics by making strategic HR decisions.\n\nOutput a sequence of JSON action objects, one per line. Example:\n{\"action_type\": \"query_department\", \"department\": \"Engineering\"}\n{\"action_type\": \"query_department\", \"department\": \"Sales\"}\n{\"action_type\": \"advance_phase\"}\n{\"action_type\": \"set_hiring_target\", \"department\": \"Engineering\", \"count\": 5}\n{\"action_type\": \"advance_phase\"}\n\nAvailable action types by phase:\n- Scanning: query_department, query_employees, calculate_metric, review_financials\n- Planning: set_hiring_target, set_training_budget, set_compensation_policy, set_retention_program\n- Producing: execute_hiring, execute_promotion, execute_transfer, execute_training, execute_termination\n- Controlling: submit_report, calculate_metric\n- advance_phase / advance_quarter: transition between phases/quarters\n\nOutput 20-40 actions covering all 6 quarters. Include advance_phase and advance_quarter actions to progress through the simulation.\"\"\"\n\n# Create prompts with different seeds for diversity\nprompts = []\nfor seed in range(20):\n prompts.append({\n \"prompt\": [\n {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": f\"Begin managing the company. Episode seed: {seed}. Start by scanning your organization to understand its current state.\"},\n ]\n })\n\ndataset = Dataset.from_list(prompts)\nprint(f\"Training dataset: {len(dataset)} episodes\")"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
},
|
| 306 |
{
|
| 307 |
"cell_type": "markdown",
|
|
|
|
| 369 |
"execution_count": null,
|
| 370 |
"metadata": {},
|
| 371 |
"outputs": [],
|
| 372 |
+
"source": "import re\n\ndef reward_func(completions, **kwargs):\n \"\"\"Parse actions from model completions, execute in environment, return rewards.\"\"\"\n rewards = []\n for completion in completions:\n # Extract text content from completion\n if isinstance(completion, list):\n text = completion[-1].get(\"content\", \"\") if completion else \"\"\n else:\n text = str(completion)\n\n # Parse all JSON objects that have action_type\n actions = []\n for match in re.finditer(r'\\{[^{}]*\"action_type\"[^{}]*\\}', text):\n try:\n action = json.loads(match.group())\n actions.append(action)\n except json.JSONDecodeError:\n continue\n\n if not actions:\n rewards.append(0.0)\n continue\n\n # Run episode with parsed actions\n try:\n env = HCM21Env()\n env.reset(seed=42)\n for action in actions:\n env.step(json.dumps(action))\n if env._done:\n break\n reward = env.get_reward()\n env.close()\n rewards.append(float(reward))\n except Exception as e:\n rewards.append(0.0)\n\n return rewards\n\nprint(\"Reward function defined.\")"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
},
|
| 374 |
{
|
| 375 |
"cell_type": "markdown",
|
| 376 |
"metadata": {},
|
| 377 |
+
"source": "## 6. Configure and Run GRPO Training\n\nWe use TRL's `GRPOTrainer` with a custom `reward_func` that parses JSON\naction sequences from model completions and executes them in the HCM:21\nenvironment. GRPO generates G completions per prompt, scores them via\nthe reward function, and reinforces higher-scoring completions.\n\nOn a free T4 GPU, we use:\n- Small batch size (2) with gradient accumulation\n- 4-bit quantization via Unsloth\n- Short max_completion_length to fit in memory\n\nAfter training, we plot the reward trajectory to visualize learning progress."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
},
|
| 379 |
{
|
| 380 |
"cell_type": "code",
|
| 381 |
"metadata": {},
|
| 382 |
+
"source": "from trl import GRPOTrainer, GRPOConfig\nimport os\n\n# Disable Qwen3 thinking mode at the tokenizer level (Unsloth-compatible)\ntokenizer.chat_template = tokenizer.chat_template.replace(\"enable_thinking\", \"\")\n\nos.environ[\"TENSORBOARD_LOGGING_DIR\"] = \"./hcm21-grpo-logs\"\n\ntraining_args = GRPOConfig(\n output_dir=\"./hcm21-grpo-output\",\n num_train_epochs=1,\n per_device_train_batch_size=2,\n gradient_accumulation_steps=4,\n num_generations=4,\n max_completion_length=2048,\n learning_rate=5e-6,\n logging_steps=1,\n save_steps=10,\n fp16=True,\n)\n\ntrainer = GRPOTrainer(\n model=model,\n processing_class=tokenizer,\n train_dataset=dataset,\n reward_funcs=reward_func,\n args=training_args,\n)\n\nprint(\"Trainer initialized. Starting training...\")\ntrainer.train()\nprint(\"Training complete!\")\n\n# Extract and plot reward curve from training logs\nimport matplotlib.pyplot as plt\n\nlog_history = trainer.state.log_history\nrewards = [entry[\"reward\"] for entry in log_history if \"reward\" in entry]\nlosses = [entry[\"loss\"] for entry in log_history if \"loss\" in entry]\n\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\n\n# Reward curve\nif rewards:\n axes[0].plot(rewards, alpha=0.4, label=\"Per-step reward\", color=\"steelblue\")\n if len(rewards) >= 3:\n window = min(5, len(rewards))\n rolling = np.convolve(rewards, np.ones(window)/window, mode=\"valid\")\n axes[0].plot(range(window-1, len(rewards)), rolling, linewidth=2,\n label=f\"{window}-step rolling avg\", color=\"darkblue\")\n axes[0].axhline(y=np.mean(random_scores), color=\"red\", linestyle=\"--\",\n label=f\"Random baseline ({np.mean(random_scores):.3f})\")\n axes[0].axhline(y=np.mean(heuristic_scores), color=\"green\", linestyle=\"--\",\n label=f\"Heuristic baseline ({np.mean(heuristic_scores):.3f})\")\n axes[0].set_xlabel(\"Training Step\")\n axes[0].set_ylabel(\"Episode Reward\")\n axes[0].set_title(\"GRPO Training Reward Curve\")\n axes[0].legend()\n axes[0].grid(True, alpha=0.3)\n\n# Loss curve\nif losses:\n axes[1].plot(losses, color=\"orange\", alpha=0.7)\n axes[1].set_xlabel(\"Training Step\")\n axes[1].set_ylabel(\"Loss\")\n axes[1].set_title(\"Training Loss\")\n axes[1].grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.savefig(\"./hcm21-grpo-output/training_curves.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()\nprint(f\"\\nTraining stats: {len(rewards)} reward entries, mean={np.mean(rewards):.4f}\" if rewards else \"No reward data logged.\")",
|
| 383 |
"outputs": [],
|
| 384 |
"execution_count": null
|
| 385 |
},
|
|
|
|
| 421 |
{
|
| 422 |
"cell_type": "code",
|
| 423 |
"metadata": {},
|
| 424 |
+
"source": "import json\nimport re\n\nFastLanguageModel.for_inference(model)\n\ndef run_trained_episode(seed):\n \"\"\"Run one episode with the trained model, return final score.\"\"\"\n env = HCM21Env()\n try:\n env.reset(seed=seed)\n messages = [\n {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": f\"Begin managing the company. Episode seed: {seed}. Start by scanning your organization to understand its current state.\"},\n ]\n\n # Generate a single completion with all actions (matches training format)\n input_ids = tokenizer.apply_chat_template(\n messages, return_tensors=\"pt\", add_generation_prompt=True\n )\n # Handle both tensor and BatchEncoding returns\n if hasattr(input_ids, \"input_ids\"):\n input_ids = input_ids.input_ids\n input_ids = input_ids.to(model.device)\n\n outputs = model.generate(\n input_ids=input_ids, max_new_tokens=2048,\n temperature=0.7, do_sample=True,\n )\n response = tokenizer.decode(outputs[0][input_ids.shape[1]:], skip_special_tokens=True)\n\n # Parse JSON actions using the same regex as reward_func\n actions = []\n for match in re.finditer(r'\\{[^{}]*\"action_type\"[^{}]*\\}', response):\n try:\n action = json.loads(match.group())\n actions.append(action)\n except json.JSONDecodeError:\n continue\n\n # Execute all parsed actions\n for action in actions:\n env.step(json.dumps(action))\n if env._done:\n break\n\n return env.get_reward(), len(actions)\n finally:\n env.close()\n\nprint(\"Evaluating trained agent on baseline seeds...\")\ntrained_scores = []\nfor seed in BASELINE_SEEDS:\n score, num_actions = run_trained_episode(seed)\n trained_scores.append(score)\n print(f\" Seed {seed:>3d}: {score:.4f} ({num_actions} actions parsed)\")\n\nsep = \"=\" * 50\nprint(f\"\\n{sep}\")\nprint(f\" RESULTS COMPARISON\")\nprint(f\"{sep}\")\nprint(f\" Random Agent: {np.mean(random_scores):.4f} +/- {np.std(random_scores):.4f}\")\nprint(f\" Heuristic Agent: {np.mean(heuristic_scores):.4f} +/- {np.std(heuristic_scores):.4f}\")\nprint(f\" Trained Agent: {np.mean(trained_scores):.4f} +/- {np.std(trained_scores):.4f}\")\nprint(f\"{sep}\")\n\nimprovement = np.mean(trained_scores) - np.mean(random_scores)\nprint(f\"\\n Improvement over random: +{improvement:.4f} ({improvement/max(np.mean(random_scores), 0.01)*100:.1f}%)\")\nif np.mean(trained_scores) > np.mean(random_scores):\n print(\" >>> Training shows positive improvement! <<<\")",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
"outputs": [],
|
| 426 |
"execution_count": null
|
| 427 |
},
|