TwoQuarks commited on
Commit
6d0d018
·
verified ·
1 Parent(s): 3658373

Update Down/down/levo/agent_levo_paradox.py

Browse files
Files changed (1) hide show
  1. Down/down/levo/agent_levo_paradox.py +200 -237
Down/down/levo/agent_levo_paradox.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
  from __future__ import annotations
3
 
4
  import math
@@ -13,8 +12,8 @@ try:
13
  import torch.optim as optim
14
  except Exception: # pragma: no cover - torch might not be installed
15
  torch = None # type: ignore[assignment]
16
- nn = object # type: ignore[assignment]
17
- optim = object # type: ignore[assignment]
18
 
19
 
20
  # ---------------------------------------------------------------------------
@@ -25,12 +24,9 @@ except Exception: # pragma: no cover - torch might not be installed
25
  class HFLevoAgent:
26
  """
27
  Single-head HF-Levo baseline agent.
28
-
29
  A simple Q-learning agent whose exploration temperature is modulated
30
  by a high-frequency term:
31
-
32
  T_t = tau * (1 + A * sin(omega * t))
33
-
34
  This creates oscillatory exploration / exploitation cycles.
35
  """
36
 
@@ -90,12 +86,9 @@ class HFLevoAgent:
90
  class LevoParadoxIsomerAgent:
91
  """
92
  Isomeric tabular agent with conservative and aggressive Q-functions.
93
-
94
  Two heads Q_L (conservative) and Q_R (aggressive) are mixed by a
95
  contextual polarization rho[s] in [0, 1]:
96
-
97
  Q_mix[s] = (1 - rho[s]) * Q_L[s] + rho[s] * Q_R[s]
98
-
99
  Polarization is nudged by failure modes reported by the environment:
100
  - "overconfident" -> push rho down (more conservative)
101
  - "overcautious" -> push rho up (more aggressive)
@@ -145,8 +138,6 @@ class LevoParadoxIsomerAgent:
145
  done: bool,
146
  failure_mode: str,
147
  ) -> None:
148
- # choose learning head: conservative for overconfidence,
149
- # aggressive for overcautious, otherwise both share credit.
150
  if failure_mode == "overconfident":
151
  heads = ("L",)
152
  elif failure_mode == "overcautious":
@@ -160,7 +151,6 @@ class LevoParadoxIsomerAgent:
160
  delta = target - float(Q[s_idx, a])
161
  Q[s_idx, a] += self.alpha * delta
162
 
163
- # polarization update
164
  if failure_mode == "overconfident":
165
  self.rho[s_idx] = np.clip(self.rho[s_idx] - self.eta, 0.0, 1.0)
166
  elif failure_mode == "overcautious":
@@ -169,231 +159,204 @@ class LevoParadoxIsomerAgent:
169
 
170
  # ---------------------------------------------------------------------------
171
  # PPO Hybrid Engine (GPU-ready, isomeric actor-critic)
 
172
  # ---------------------------------------------------------------------------
173
 
174
-
175
- @dataclass
176
- class Transition:
177
- state_idx: int
178
- action: int
179
- reward: float
180
- log_prob: float
181
- value: float
182
- rho: float
183
- failure_mode: str
184
-
185
-
186
- class ParadoxActorCritic(nn.Module): # type: ignore[misc]
187
- """
188
- Isomeric actor-critic.
189
-
190
- - shared MLP trunk over a one-hot encoding of the discrete state
191
- - two actor heads: conservative vs aggressive
192
- - one scalar gating head producing rho(s) in [0, 1]
193
- - one critic head V(s)
194
-
195
- The final policy is a mixture of the two isomers, combined inside the
196
- logits space and fed through softmax.
197
- """
198
-
199
- def __init__(self, n_states: int, n_actions: int, hidden_dim: int = 128) -> None:
200
- super().__init__()
201
- self.n_states = n_states
202
- self.n_actions = n_actions
203
-
204
- self.trunk = nn.Sequential(
205
- nn.Linear(n_states, hidden_dim),
206
- nn.ReLU(),
207
- nn.Linear(hidden_dim, hidden_dim),
208
- nn.ReLU(),
209
- )
210
- self.actor_cons = nn.Linear(hidden_dim, n_actions)
211
- self.actor_aggr = nn.Linear(hidden_dim, n_actions)
212
- self.gate = nn.Linear(hidden_dim, 1)
213
- self.critic = nn.Linear(hidden_dim, 1)
214
-
215
- def forward(self, state_one_hot: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
216
- z = self.trunk(state_one_hot)
217
- logits_cons = self.actor_cons(z)
218
- logits_aggr = self.actor_aggr(z)
219
- rho = torch.sigmoid(self.gate(z)) # [B, 1]
220
- value = self.critic(z).squeeze(-1) # [B]
221
-
222
- # mixture in logit space
223
- logits_mix = (1.0 - rho) * logits_cons + rho * logits_aggr
224
- return logits_mix, value, rho.squeeze(-1), logits_cons * 1.0 # last term unused but can help debugging
225
-
226
-
227
- class LevoParadoxPPOHybrid:
228
- """
229
- GPU-ready PPO hybrid engine with isomeric policy.
230
-
231
- This agent combines:
232
- - an isomeric actor (conservative + aggressive heads),
233
- - a gating network rho(s) trained end-to-end,
234
- - PPO-style clipped policy updates,
235
- - and a value baseline for variance reduction.
236
-
237
- It operates directly on the discrete Epistemic Valley state space using
238
- a one-hot encoding, which keeps things simple and fully reproducible.
239
- """
240
-
241
- def __init__(
242
- self,
243
- n_states: int,
244
- n_actions: int,
245
- gamma: float = 0.99,
246
- lam: float = 0.95,
247
- clip_eps: float = 0.2,
248
- entropy_coef: float = 0.01,
249
- value_coef: float = 0.5,
250
- lr: float = 3e-4,
251
- batch_size: int = 256,
252
- update_epochs: int = 8,
253
- seed: Optional[int] = None,
254
- ) -> None:
255
- if torch is None:
256
- raise RuntimeError("PyTorch is required for LevoParadoxPPOHybrid.")
257
-
258
- self.n_states = n_states
259
- self.n_actions = n_actions
260
- self.gamma = gamma
261
- self.lam = lam
262
- self.clip_eps = clip_eps
263
- self.entropy_coef = entropy_coef
264
- self.value_coef = value_coef
265
- self.batch_size = batch_size
266
- self.update_epochs = update_epochs
267
-
268
- if seed is not None:
269
- torch.manual_seed(seed)
270
- np.random.seed(seed)
271
-
272
- self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
273
- self.net = ParadoxActorCritic(n_states, n_actions).to(self.device)
274
- self.optimizer = optim.Adam(self.net.parameters(), lr=lr)
275
-
276
- self.buffer: List[Transition] = []
277
-
278
- # ---------------------------- utilities -----------------------------
279
-
280
- def _one_hot(self, idx: np.ndarray | int) -> torch.Tensor:
281
- idx_arr = np.atleast_1d(idx).astype(np.int64)
282
- x = np.zeros((idx_arr.shape[0], self.n_states), dtype=np.float32)
283
- x[np.arange(idx_arr.shape[0]), idx_arr] = 1.0
284
- return torch.from_numpy(x).to(self.device)
285
-
286
- # ---------------------------- interaction ---------------------------
287
-
288
- def select_action(self, s_idx: int) -> Tuple[int, float, float, float]:
289
- """Return (action, log_prob, value_estimate, rho)."""
290
- self.net.eval()
291
- state_one_hot = self._one_hot(s_idx)
292
- logits_mix, value, rho, _ = self.net(state_one_hot) # type: ignore[misc]
293
- dist = torch.distributions.Categorical(logits=logits_mix)
294
- action = dist.sample()
295
- log_prob = dist.log_prob(action)
296
- return int(action.item()), float(log_prob.item()), float(value.item()), float(rho.item())
297
-
298
- def store_transition(
299
- self,
300
- s_idx: int,
301
- action: int,
302
- reward: float,
303
- log_prob: float,
304
- value: float,
305
- rho: float,
306
- failure_mode: str,
307
- ) -> None:
308
- self.buffer.append(
309
- Transition(
310
- state_idx=int(s_idx),
311
- action=int(action),
312
- reward=float(reward),
313
- log_prob=float(log_prob),
314
- value=float(value),
315
- rho=float(rho),
316
- failure_mode=failure_mode,
317
  )
318
- )
319
-
320
- # ---------------------------- learning ------------------------------
321
-
322
- def _compute_advantages(
323
- self, rewards: np.ndarray, values: np.ndarray
324
- ) -> Tuple[np.ndarray, np.ndarray]:
325
- # For one-step episodes, advantage = reward - value, return = reward
326
- returns = rewards.copy()
327
- advantages = rewards - values
328
- # Normalise for stability
329
- advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
330
- return returns, advantages
331
-
332
- def update(self) -> Dict[str, float]:
333
- if not self.buffer:
334
- return {}
335
-
336
- # Collect batch
337
- states = np.array([t.state_idx for t in self.buffer], dtype=np.int64)
338
- actions = np.array([t.action for t in self.buffer], dtype=np.int64)
339
- rewards = np.array([t.reward for t in self.buffer], dtype=np.float32)
340
- old_log_probs = np.array([t.log_prob for t in self.buffer], dtype=np.float32)
341
- values = np.array([t.value for t in self.buffer], dtype=np.float32)
342
-
343
- returns, advantages = self._compute_advantages(rewards, values)
344
-
345
- # Convert to tensors
346
- states_t = self._one_hot(states)
347
- actions_t = torch.from_numpy(actions).to(self.device)
348
- returns_t = torch.from_numpy(returns).to(self.device)
349
- advantages_t = torch.from_numpy(advantages).to(self.device)
350
- old_log_probs_t = torch.from_numpy(old_log_probs).to(self.device)
351
-
352
- dataset_size = states_t.size(0)
353
- idxs = np.arange(dataset_size)
354
-
355
- stats: Dict[str, float] = {}
356
-
357
- self.net.train()
358
- for _ in range(self.update_epochs):
359
- np.random.shuffle(idxs)
360
- for start in range(0, dataset_size, self.batch_size):
361
- batch_idx = idxs[start : start + self.batch_size]
362
- if len(batch_idx) == 0:
363
- continue
364
-
365
- batch_states = states_t[batch_idx]
366
- batch_actions = actions_t[batch_idx]
367
- batch_returns = returns_t[batch_idx]
368
- batch_adv = advantages_t[batch_idx]
369
- batch_old_logp = old_log_probs_t[batch_idx]
370
-
371
- logits_mix, values_pred, _, _ = self.net(batch_states) # type: ignore[misc]
372
- dist = torch.distributions.Categorical(logits=logits_mix)
373
- log_probs = dist.log_prob(batch_actions)
374
- entropy = dist.entropy().mean()
375
-
376
- # PPO clipped objective
377
- ratio = torch.exp(log_probs - batch_old_logp)
378
- unclipped = ratio * batch_adv
379
- clipped = torch.clamp(ratio, 1.0 - self.clip_eps, 1.0 + self.clip_eps) * batch_adv
380
- policy_loss = -torch.min(unclipped, clipped).mean()
381
-
382
- value_loss = (batch_returns - values_pred).pow(2).mean()
383
-
384
- loss = policy_loss + self.value_coef * value_loss - self.entropy_coef * entropy
385
-
386
- self.optimizer.zero_grad()
387
- loss.backward()
388
- torch.nn.utils.clip_grad_norm_(self.net.parameters(), max_norm=1.0)
389
- self.optimizer.step()
390
-
391
- stats = {
392
- "policy_loss": float(policy_loss.item()),
393
- "value_loss": float(value_loss.item()),
394
- "entropy": float(entropy.item()),
395
- }
396
-
397
- # clear buffer
398
- self.buffer.clear()
399
- return stats
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
  import math
 
12
  import torch.optim as optim
13
  except Exception: # pragma: no cover - torch might not be installed
14
  torch = None # type: ignore[assignment]
15
+ nn = None # type: ignore[assignment]
16
+ optim = None # type: ignore[assignment]
17
 
18
 
19
  # ---------------------------------------------------------------------------
 
24
  class HFLevoAgent:
25
  """
26
  Single-head HF-Levo baseline agent.
 
27
  A simple Q-learning agent whose exploration temperature is modulated
28
  by a high-frequency term:
 
29
  T_t = tau * (1 + A * sin(omega * t))
 
30
  This creates oscillatory exploration / exploitation cycles.
31
  """
32
 
 
86
  class LevoParadoxIsomerAgent:
87
  """
88
  Isomeric tabular agent with conservative and aggressive Q-functions.
 
89
  Two heads Q_L (conservative) and Q_R (aggressive) are mixed by a
90
  contextual polarization rho[s] in [0, 1]:
 
91
  Q_mix[s] = (1 - rho[s]) * Q_L[s] + rho[s] * Q_R[s]
 
92
  Polarization is nudged by failure modes reported by the environment:
93
  - "overconfident" -> push rho down (more conservative)
94
  - "overcautious" -> push rho up (more aggressive)
 
138
  done: bool,
139
  failure_mode: str,
140
  ) -> None:
 
 
141
  if failure_mode == "overconfident":
142
  heads = ("L",)
143
  elif failure_mode == "overcautious":
 
151
  delta = target - float(Q[s_idx, a])
152
  Q[s_idx, a] += self.alpha * delta
153
 
 
154
  if failure_mode == "overconfident":
155
  self.rho[s_idx] = np.clip(self.rho[s_idx] - self.eta, 0.0, 1.0)
156
  elif failure_mode == "overcautious":
 
159
 
160
  # ---------------------------------------------------------------------------
161
  # PPO Hybrid Engine (GPU-ready, isomeric actor-critic)
162
+ # Only defined if torch is available.
163
  # ---------------------------------------------------------------------------
164
 
165
+ if torch is not None and nn is not None and optim is not None:
166
+
167
+ @dataclass
168
+ class Transition:
169
+ state_idx: int
170
+ action: int
171
+ reward: float
172
+ log_prob: float
173
+ value: float
174
+ rho: float
175
+ failure_mode: str
176
+
177
+
178
+ class ParadoxActorCritic(nn.Module): # type: ignore[misc]
179
+ """
180
+ Isomeric actor-critic.
181
+ - shared MLP trunk over a one-hot encoding of the discrete state
182
+ - two actor heads: conservative vs aggressive
183
+ - one scalar gating head producing rho(s) in [0, 1]
184
+ - one critic head V(s)
185
+ The final policy is a mixture of the two isomers, combined inside the
186
+ logits space and fed through softmax.
187
+ """
188
+
189
+ def __init__(self, n_states: int, n_actions: int, hidden_dim: int = 128) -> None:
190
+ super().__init__()
191
+ self.n_states = n_states
192
+ self.n_actions = n_actions
193
+
194
+ self.trunk = nn.Sequential(
195
+ nn.Linear(n_states, hidden_dim),
196
+ nn.ReLU(),
197
+ nn.Linear(hidden_dim, hidden_dim),
198
+ nn.ReLU(),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  )
200
+ self.actor_cons = nn.Linear(hidden_dim, n_actions)
201
+ self.actor_aggr = nn.Linear(hidden_dim, n_actions)
202
+ self.gate = nn.Linear(hidden_dim, 1)
203
+ self.critic = nn.Linear(hidden_dim, 1)
204
+
205
+ def forward(
206
+ self, state_one_hot: "torch.Tensor"
207
+ ) -> Tuple["torch.Tensor", "torch.Tensor", "torch.Tensor", "torch.Tensor"]:
208
+ z = self.trunk(state_one_hot)
209
+ logits_cons = self.actor_cons(z)
210
+ logits_aggr = self.actor_aggr(z)
211
+ rho = torch.sigmoid(self.gate(z)) # [B, 1]
212
+ value = self.critic(z).squeeze(-1) # [B]
213
+ logits_mix = (1.0 - rho) * logits_cons + rho * logits_aggr
214
+ return logits_mix, value, rho.squeeze(-1), logits_cons * 1.0
215
+
216
+
217
+ class LevoParadoxPPOHybrid:
218
+ """
219
+ GPU-ready PPO hybrid engine with isomeric policy.
220
+ """
221
+
222
+ def __init__(
223
+ self,
224
+ n_states: int,
225
+ n_actions: int,
226
+ gamma: float = 0.99,
227
+ lam: float = 0.95,
228
+ clip_eps: float = 0.2,
229
+ entropy_coef: float = 0.01,
230
+ value_coef: float = 0.5,
231
+ lr: float = 3e-4,
232
+ batch_size: int = 256,
233
+ update_epochs: int = 8,
234
+ seed: Optional[int] = None,
235
+ ) -> None:
236
+ self.n_states = n_states
237
+ self.n_actions = n_actions
238
+ self.gamma = gamma
239
+ self.lam = lam
240
+ self.clip_eps = clip_eps
241
+ self.entropy_coef = entropy_coef
242
+ self.value_coef = value_coef
243
+ self.batch_size = batch_size
244
+ self.update_epochs = update_epochs
245
+
246
+ if seed is not None:
247
+ torch.manual_seed(seed)
248
+ np.random.seed(seed)
249
+
250
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
251
+ self.net = ParadoxActorCritic(n_states, n_actions).to(self.device)
252
+ self.optimizer = optim.Adam(self.net.parameters(), lr=lr)
253
+
254
+ self.buffer: List[Transition] = []
255
+
256
+ def _one_hot(self, idx: np.ndarray | int) -> "torch.Tensor":
257
+ idx_arr = np.atleast_1d(idx).astype(np.int64)
258
+ x = np.zeros((idx_arr.shape[0], self.n_states), dtype=np.float32)
259
+ x[np.arange(idx_arr.shape[0]), idx_arr] = 1.0
260
+ return torch.from_numpy(x).to(self.device)
261
+
262
+ def select_action(self, s_idx: int) -> Tuple[int, float, float, float]:
263
+ self.net.eval()
264
+ state_one_hot = self._one_hot(s_idx)
265
+ logits_mix, value, rho, _ = self.net(state_one_hot) # type: ignore[misc]
266
+ dist = torch.distributions.Categorical(logits=logits_mix)
267
+ action = dist.sample()
268
+ log_prob = dist.log_prob(action)
269
+ return int(action.item()), float(log_prob.item()), float(value.item()), float(rho.item())
270
+
271
+ def store_transition(
272
+ self,
273
+ s_idx: int,
274
+ action: int,
275
+ reward: float,
276
+ log_prob: float,
277
+ value: float,
278
+ rho: float,
279
+ failure_mode: str,
280
+ ) -> None:
281
+ self.buffer.append(
282
+ Transition(
283
+ state_idx=int(s_idx),
284
+ action=int(action),
285
+ reward=float(reward),
286
+ log_prob=float(log_prob),
287
+ value=float(value),
288
+ rho=float(rho),
289
+ failure_mode=failure_mode,
290
+ )
291
+ )
292
+
293
+ def _compute_advantages(self, rewards: np.ndarray, values: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
294
+ returns = rewards.copy()
295
+ advantages = rewards - values
296
+ advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
297
+ return returns, advantages
298
+
299
+ def update(self) -> Dict[str, float]:
300
+ if not self.buffer:
301
+ return {}
302
+
303
+ states = np.array([t.state_idx for t in self.buffer], dtype=np.int64)
304
+ actions = np.array([t.action for t in self.buffer], dtype=np.int64)
305
+ rewards = np.array([t.reward for t in self.buffer], dtype=np.float32)
306
+ old_log_probs = np.array([t.log_prob for t in self.buffer], dtype=np.float32)
307
+ values = np.array([t.value for t in self.buffer], dtype=np.float32)
308
+
309
+ returns, advantages = self._compute_advantages(rewards, values)
310
+
311
+ states_t = self._one_hot(states)
312
+ actions_t = torch.from_numpy(actions).to(self.device)
313
+ returns_t = torch.from_numpy(returns).to(self.device)
314
+ advantages_t = torch.from_numpy(advantages).to(self.device)
315
+ old_log_probs_t = torch.from_numpy(old_log_probs).to(self.device)
316
+
317
+ dataset_size = states_t.size(0)
318
+ idxs = np.arange(dataset_size)
319
+
320
+ stats: Dict[str, float] = {}
321
+
322
+ self.net.train()
323
+ for _ in range(self.update_epochs):
324
+ np.random.shuffle(idxs)
325
+ for start in range(0, dataset_size, self.batch_size):
326
+ batch_idx = idxs[start : start + self.batch_size]
327
+ if len(batch_idx) == 0:
328
+ continue
329
+
330
+ batch_states = states_t[batch_idx]
331
+ batch_actions = actions_t[batch_idx]
332
+ batch_returns = returns_t[batch_idx]
333
+ batch_adv = advantages_t[batch_idx]
334
+ batch_old_logp = old_log_probs_t[batch_idx]
335
+
336
+ logits_mix, values_pred, _, _ = self.net(batch_states) # type: ignore[misc]
337
+ dist = torch.distributions.Categorical(logits=logits_mix)
338
+ log_probs = dist.log_prob(batch_actions)
339
+ entropy = dist.entropy().mean()
340
+
341
+ ratio = torch.exp(log_probs - batch_old_logp)
342
+ unclipped = ratio * batch_adv
343
+ clipped = torch.clamp(ratio, 1.0 - self.clip_eps, 1.0 + self.clip_eps) * batch_adv
344
+ policy_loss = -torch.min(unclipped, clipped).mean()
345
+
346
+ value_loss = (batch_returns - values_pred).pow(2).mean()
347
+
348
+ loss = policy_loss + self.value_coef * value_loss - self.entropy_coef * entropy
349
+
350
+ self.optimizer.zero_grad()
351
+ loss.backward()
352
+ torch.nn.utils.clip_grad_norm_(self.net.parameters(), max_norm=1.0)
353
+ self.optimizer.step()
354
+
355
+ stats = {
356
+ "policy_loss": float(policy_loss.item()),
357
+ "value_loss": float(value_loss.item()),
358
+ "entropy": float(entropy.item()),
359
+ }
360
+
361
+ self.buffer.clear()
362
+ return stats