transformer / baselines.py
Mohith202's picture
Deploy BrainRL OpenEnv server
32d14f4 verified
Raw
History Blame Contribute Delete
8.51 kB
"""Baseline policies for BrainRL evaluation.
These are intentionally non-RL policies that share the same OpenEnv interface
as the LLM/GRPO policy. They give the hackathon demo something concrete to
beat without dragging in any Gym/PPO machinery.
"""
from __future__ import annotations
import random
from dataclasses import dataclass
from statistics import mean
from typing import Protocol
from metrics import EpisodeMetrics, score_selection_order
from prompts import llm_prompt_policy_action, static_prompt_policy_action
from server.brain_environment import BrainRegionSelectionEnvironment
class RegionPolicy(Protocol):
name: str
def reset(self, env: BrainRegionSelectionEnvironment) -> None: ...
def select_region(self, env: BrainRegionSelectionEnvironment) -> str: ...
@dataclass
class EpisodeResult:
policy: str
rewards: list[float]
r2_curve: list[float]
selected_regions: list[str]
errors: list[str]
subject_id: str | None = None
run_id: str | None = None
condition: str | None = None
stimulus_window: int | None = None
stimulus_summary: dict | None = None
metrics: EpisodeMetrics | None = None
@property
def final_r2(self) -> float:
return self.r2_curve[-1] if self.r2_curve else 0.0
@property
def total_reward(self) -> float:
return float(sum(self.rewards))
@property
def priority_correlation(self) -> float:
return float(self.metrics.priority_correlation) if self.metrics else 0.0
@property
def two_v_two_accuracy(self) -> float:
return float(self.metrics.two_v_two_accuracy) if self.metrics else 0.0
class RandomPolicy:
name = "random"
def __init__(self, seed: int = 42):
self._rng = random.Random(seed)
def reset(self, env: BrainRegionSelectionEnvironment) -> None:
return None
def select_region(self, env: BrainRegionSelectionEnvironment) -> str:
selected = set(env._selected_region_ids)
remaining = [
candidate.region_id
for candidate in env._subset.candidates
if candidate.region_id not in selected
]
return self._rng.choice(remaining)
class SemanticPriorPolicy:
name = "semantic_prior"
def reset(self, env: BrainRegionSelectionEnvironment) -> None:
self._ordered = [
candidate.region_id
for candidate in sorted(
env._subset.candidates,
key=lambda item: (item.semantic_prior, item.prune_score),
reverse=True,
)
]
def select_region(self, env: BrainRegionSelectionEnvironment) -> str:
selected = set(env._selected_region_ids)
for region_id in self._ordered:
if region_id not in selected:
return region_id
return self._ordered[0]
class GreedyImprovementPolicy:
name = "greedy"
def reset(self, env: BrainRegionSelectionEnvironment) -> None:
return None
def select_region(self, env: BrainRegionSelectionEnvironment) -> str:
selected = list(env._selected_region_ids)
selected_set = set(selected)
best_region: str | None = None
best_gain = float("-inf")
for candidate in env._subset.candidates:
if candidate.region_id in selected_set:
continue
next_regions = selected + [candidate.region_id]
gain = env._score_regions(next_regions) - env._current_r2
gain -= env._subset.cost_penalty * candidate.cost
if gain > best_gain:
best_gain = gain
best_region = candidate.region_id
if best_region is None:
return env._subset.candidates[0].region_id
return best_region
class PromptPolicy:
"""OpenEnv prompt-action policy.
In static mode it uses the exact prompt interface but selects deterministically
from prune_score/semantic_prior. In LLM mode it asks a chat model for one
JSON action.
"""
def __init__(self, use_llm: bool = False):
self.use_llm = use_llm
self.name = "llm_prompt" if use_llm else "prompt_static"
def reset(self, env: BrainRegionSelectionEnvironment) -> None:
return None
def select_region(self, env: BrainRegionSelectionEnvironment) -> str:
state = env._build_selection_state()
if self.use_llm:
region_id, _ = llm_prompt_policy_action(state)
return region_id
return static_prompt_policy_action(state)
def run_policy_episode(
policy: RegionPolicy,
seed: int = 42,
*,
subject_id: str | None = None,
run_id: str | None = None,
condition: str | None = None,
stimulus_window: int | None = None,
env: BrainRegionSelectionEnvironment | None = None,
) -> EpisodeResult:
env = env or BrainRegionSelectionEnvironment()
env.reset(
seed=seed,
subject_id=subject_id,
run_id=run_id,
condition=condition,
stimulus_window=stimulus_window,
)
policy.reset(env)
rewards: list[float] = []
r2_curve: list[float] = []
errors: list[str] = []
done = False
while not done:
region_id = policy.select_region(env)
result = env._process_action(region_id)
rewards.append(float(result["reward"]))
r2_curve.append(float(result["current_r2"]))
if result.get("error"):
errors.append(str(result["error"]))
done = bool(result["done"])
stim = env._stimulus_features
candidate_ids = [candidate.region_id for candidate in env._subset.candidates]
metrics = score_selection_order(
candidate_ids=candidate_ids,
selected_region_ids=env._selected_region_ids,
target_scores=env._effective_base_r2,
)
return EpisodeResult(
policy=policy.name,
rewards=rewards,
r2_curve=r2_curve,
selected_regions=list(env._selected_region_ids),
errors=errors,
subject_id=subject_id,
run_id=run_id,
condition=condition,
stimulus_window=int(stim["window_index"]) if stim else None,
stimulus_summary={
"window_index": int(stim["window_index"]),
"n_windows": int(stim["n_windows"]),
"dominant_pos": stim.get("dominant_pos"),
"n_words": int(stim.get("n_words", 0)),
"speech_density": float(stim.get("speech_density", 0.0)),
}
if stim
else None,
metrics=metrics,
)
def summarize_results(
results: list[EpisodeResult],
example_limit: int = 5,
split_label: str | None = None,
) -> list[dict[str, object]]:
rows: list[dict[str, object]] = []
for policy_name in sorted({result.policy for result in results}):
policy_results = [result for result in results if result.policy == policy_name]
example_order = " -> ".join(policy_results[0].selected_regions[:example_limit])
if len(policy_results[0].selected_regions) > example_limit:
example_order += f" -> (+{len(policy_results[0].selected_regions) - example_limit} more)"
row: dict[str, object] = {
"policy": policy_name,
"episodes": len(policy_results),
"mean_final_r2": mean(result.final_r2 for result in policy_results),
"mean_priority_correlation": mean(
result.priority_correlation for result in policy_results
),
"mean_2v2_accuracy": mean(
result.two_v_two_accuracy for result in policy_results
),
"mean_total_reward": mean(result.total_reward for result in policy_results),
"mean_regions_selected": mean(
len(result.selected_regions) for result in policy_results
),
"example_order": example_order,
}
if split_label:
row["split"] = split_label
rows.append(row)
return rows
def r2_curves_by_policy(
results: list[EpisodeResult],
) -> dict[str, list[list[float]]]:
"""Group r2_curve lists by policy name for plotting."""
grouped: dict[str, list[list[float]]] = {}
for result in results:
grouped.setdefault(result.policy, []).append(list(result.r2_curve))
return grouped
def default_baselines(seed: int = 42) -> list[RegionPolicy]:
return [
RandomPolicy(seed=seed),
SemanticPriorPolicy(),
PromptPolicy(use_llm=False),
GreedyImprovementPolicy(),
]