Skip to content

Commit 286107f

Browse files
committed
fix pytest error
1 parent 299bc27 commit 286107f

97 files changed

Lines changed: 308112 additions & 1490 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CPC_FOR_IQN_IMPLEMENTATION_PLAN.md

Lines changed: 0 additions & 926 deletions
This file was deleted.

PLAN_ADD_ACTION_TO_LSTM.md

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
# Plan: Add Last Step Action as Input to LSTM in IQN and PPO
2+
3+
## Overview
4+
This plan outlines the changes needed to concatenate the last step action with gridworld feature extractions before feeding them into the LSTM in both IQN and PPO implementations.
5+
6+
## Current Architecture
7+
8+
### IQN (RecurrentIQNModelCPC)
9+
- **Current flow**: `o_t -> encoder -> z_t -> LSTM -> h_t -> IQN head`
10+
- **LSTM input**: Only encoded observation features `z_t` (shape: `hidden_size`)
11+
- **Actions**: Stored in buffer but not used as LSTM input
12+
13+
### PPO (RecurrentPPOLSTM / RecurrentPPOLSTMCPC)
14+
- **Current flow**: `o_t -> encoder -> z_t -> LSTM -> h_t -> Actor/Critic`
15+
- **LSTM input**: Only encoded observation features `z_t` (shape: `hidden_size`)
16+
- **Actions**: Stored in rollout memory but not used as LSTM input
17+
18+
## Proposed Architecture
19+
20+
### Modified Flow
21+
- **New flow**: `o_t -> encoder -> z_t` + `a_{t-1} -> embed -> a_emb` -> `concat([z_t, a_emb]) -> LSTM -> h_t -> heads`
22+
- **LSTM input**: Concatenated features `[z_t, a_emb]` (shape: `hidden_size + action_embed_dim`)
23+
24+
## Implementation Plan
25+
26+
### 1. Action Embedding Strategy
27+
28+
**Option A: One-Hot Encoding (Simple)**
29+
- Convert action index to one-hot vector (shape: `action_space`)
30+
- Pros: Simple, no learnable parameters, interpretable
31+
- Cons: Sparse, doesn't scale well with large action spaces
32+
33+
**Option B: Learned Embedding (Recommended)**
34+
- Use `nn.Embedding(action_space, action_embed_dim)`
35+
- Pros: Compact, learnable, scales well
36+
- Cons: Adds parameters
37+
38+
**Recommendation**: Use learned embedding with `action_embed_dim = min(32, action_space // 2)` to balance expressiveness and efficiency.
39+
40+
### 2. Architecture Changes
41+
42+
#### 2.1 Add Action Embedding Layer
43+
```python
44+
# In __init__:
45+
self.action_embed_dim = min(32, action_space // 2) # or configurable
46+
self.action_embedding = nn.Embedding(action_space, self.action_embed_dim)
47+
```
48+
49+
#### 2.2 Modify LSTM Input Dimension
50+
```python
51+
# Current: LSTM(hidden_size, hidden_size)
52+
# New: LSTM(hidden_size + action_embed_dim, hidden_size)
53+
# NOTE: IQN uses batch_first=False, PPO uses batch_first=True
54+
self.lstm = nn.LSTM(
55+
hidden_size + self.action_embed_dim, # Input size increased
56+
hidden_size, # Hidden size unchanged
57+
batch_first=False # IQN: False, PPO: True (keep existing setting)
58+
)
59+
```
60+
61+
#### 2.3 Track Last Action
62+
- Add `self._last_action: Optional[int] = None` to track previous action
63+
- Reset to `None` at episode start (when `done=True`)
64+
65+
### 3. Code Changes by File
66+
67+
#### 3.1 IQN: `recurrent_iqn_lstm_cpc_fixed.py`
68+
69+
**Changes in `__init__`:**
70+
1. Add action embedding layer
71+
2. Increase LSTM input size
72+
3. Initialize `self._last_action = None`
73+
74+
**Changes in `take_action()`:**
75+
1. Get last action from `self._last_action` (or None if first step)
76+
2. Embed last action (use zero embedding if None)
77+
3. Concatenate `z_t` with `a_emb` before LSTM
78+
4. **CRITICAL**: Store the action returned from `base_model.take_action()` as `self._last_action` for the NEXT step
79+
- The action is selected AFTER LSTM forward pass, so we store it for next call
80+
81+
**Changes in `add_memory()`:**
82+
1. Reset `self._last_action = None` when `done=True`
83+
84+
**Changes in `train_step()`:**
85+
1. **Action shifting logic**:
86+
- For burn-in: Use `actions_t[:, :burn_in]` (these are the actions taken at each step)
87+
- For unroll: Need PREVIOUS actions, so use `actions_t[:, burn_in-1:burn_in+unroll]`
88+
- First step of unroll uses last action from burn-in: `actions_t[:, burn_in-1]`
89+
- Handle first step of each sequence: Use zero embedding (action index 0 or zero vector)
90+
2. Embed actions: `prev_actions = actions_t[:, max(0, burn_in-1):burn_in+unroll]` with zeros prepended for first step
91+
3. Concatenate encoded states with action embeddings before LSTM
92+
4. **Important**: Actions at index `t` correspond to the action taken AFTER observing state `t`, so for LSTM input at step `t`, we need action from step `t-1`
93+
94+
**Changes in `_recompute_sequence_with_gradients()`:**
95+
1. **Add actions parameter**: `raw_states: List[np.ndarray], actions: List[int]`
96+
2. **Action shifting**: For state at index `t`, use action from index `t-1` (previous action)
97+
- First step (t=0): Use zero embedding
98+
- Subsequent steps: Use `actions[t-1]`
99+
3. Embed actions (or zeros for first step)
100+
4. Concatenate with encoded features before LSTM
101+
102+
**Changes in `_compute_cpc_loss()`:**
103+
1. Extract actions from episode: `episode['actions']`
104+
2. Pass actions to `_recompute_sequence_with_gradients()`: `_recompute_sequence_with_gradients(raw_states, actions)`
105+
3. Handle action shifting: For state at index `t`, use action from index `t-1` (first step uses zero)
106+
107+
#### 3.2 PPO: `recurrent_ppo_lstm_generic.py`
108+
109+
**Changes in `__init__`:**
110+
1. Add action embedding layer
111+
2. Increase LSTM input size
112+
3. Initialize `self._last_action = None`
113+
114+
**Changes in `take_action()`:**
115+
1. Get last action from `self._last_action` (or None if first step)
116+
2. Embed last action (use zero embedding if None)
117+
3. Concatenate encoded features with `a_emb` before LSTM in `_forward_base()`
118+
4. **CRITICAL**: Store the action returned from policy sampling as `self._last_action` for the NEXT step
119+
- Action is selected AFTER LSTM forward pass, so store it for next call
120+
121+
**Changes in `add_memory_ppo()` / `store_memory()`:**
122+
1. Reset `self._last_action = None` when `done=True`
123+
124+
**Changes in `learn()`:**
125+
1. **CRITICAL**: PPO currently uses stored `h_states` from rollout. With action inputs, we must RECOMPUTE LSTM states during training (similar to how IQN does it)
126+
2. **Rollout Memory Structure**:
127+
- `rollout_memory["states"][i]`: State observed at step `i`
128+
- `rollout_memory["actions"][i]`: Action taken at step `i` (AFTER observing state `i`)
129+
- `rollout_memory["dones"][i]`: Whether episode ended at step `i`
130+
- For LSTM input at step `i`, we need action from step `i-1` (previous action)
131+
3. Extract previous actions from rollout memory:
132+
- Create `prev_actions` array: `prev_actions[0] = 0` (or None), `prev_actions[i] = actions[i-1]` for i > 0
133+
- Handle episode boundaries: When `dones[i-1] == True`, set `prev_actions[i] = 0` (new episode)
134+
4. **Recompute LSTM states** (don't use stored `h_states`):
135+
- Process states through encoder + action embedding + LSTM sequentially
136+
- For each epoch in `K_epochs`, recompute from scratch (proper BPTT)
137+
- Initialize hidden state at start of sequence: `h0, c0 = zeros`
138+
- Process sequence: For each state `i`, use `prev_actions[i]` for LSTM input
139+
5. Update `_forward_base()` to accept optional action parameter
140+
6. In minibatch loop: Process states sequentially with their corresponding previous actions
141+
142+
#### 3.3 PPO with CPC: `recurrent_ppo_lstm_cpc.py`
143+
144+
**Same changes as `recurrent_ppo_lstm_generic.py` plus:**
145+
1. Update `_recompute_sequence_with_gradients()` to accept and handle actions parameter
146+
2. Update CPC loss computation to extract actions from rollout and pass to `_recompute_sequence_with_gradients()`
147+
3. Handle action shifting in CPC sequence recomputation (same as IQN)
148+
149+
### 4. Edge Cases and Special Handling
150+
151+
#### 4.1 First Step of Episode
152+
- **Problem**: No previous action exists
153+
- **Solution**: Use zero embedding or special "no-action" token
154+
- Option 1: `action_embedding(torch.zeros(..., dtype=torch.long))` (action 0)
155+
- Option 2: `torch.zeros(action_embed_dim)` (zero vector)
156+
- **Recommendation**: Use zero vector for simplicity
157+
158+
#### 4.2 Episode Boundaries
159+
- **Problem**: Action from previous episode shouldn't influence new episode
160+
- **Solution**: Reset `self._last_action = None` when `done=True`
161+
- In training: Detect episode boundaries using `dones` tensor and reset action embeddings
162+
163+
#### 4.3 Batch Processing
164+
- **Problem**: Different episodes in batch may have different lengths
165+
- **Solution**:
166+
- In burn-in: Use zero embeddings for first step of each sequence
167+
- In unroll: Shift actions by 1 timestep: `prev_actions = actions[:, burn_in-1:burn_in+unroll]` with zeros prepended for first step
168+
- **Episode boundaries in batches**: When `dones[i-1] == True`, the next step should use zero embedding (new episode started)
169+
- For IQN: Handle this in the sequence sampling (EpisodeBuffer should handle episode boundaries)
170+
- For PPO: Detect episode boundaries from `dones` tensor and reset action embeddings accordingly
171+
172+
### 5. Implementation Details
173+
174+
#### 5.1 Action Embedding Function
175+
```python
176+
def _embed_action(self, action: int | torch.Tensor | None, batch_size: int = 1) -> torch.Tensor:
177+
"""Embed action index to vector representation.
178+
179+
Args:
180+
action: Action index (int), tensor of action indices, or None (for first step)
181+
batch_size: Batch size for batched operations (default: 1 for single step)
182+
183+
Returns:
184+
Action embedding tensor of shape (batch_size, action_embed_dim)
185+
"""
186+
# Handle None (first step) - use zero vector
187+
if action is None:
188+
return torch.zeros(batch_size, self.action_embed_dim, device=self.device)
189+
190+
# Handle single integer
191+
if isinstance(action, int):
192+
action_tensor = torch.tensor([action], device=self.device, dtype=torch.long)
193+
return self.action_embedding(action_tensor)
194+
195+
# Handle tensor (can be batched)
196+
action_tensor = action.to(self.device).long()
197+
# Clamp to valid range [0, action_space-1] to avoid index errors
198+
action_tensor = torch.clamp(action_tensor, 0, self.action_space - 1)
199+
return self.action_embedding(action_tensor)
200+
```
201+
202+
#### 5.2 Concatenation Pattern
203+
204+
**IQN (batch_first=False):**
205+
```python
206+
# During acting:
207+
z_t = self.encoder(frame_t) # (1, hidden_size)
208+
a_emb = self._embed_action(self._last_action, batch_size=1) # (1, action_embed_dim)
209+
lstm_input = torch.cat([z_t, a_emb], dim=-1) # (1, hidden_size + action_embed_dim)
210+
lstm_input_seq = lstm_input.unsqueeze(0) # (1, 1, hidden_size + action_embed_dim) for LSTM
211+
lstm_out, hidden = self.lstm(lstm_input_seq, hidden) # batch_first=False
212+
213+
# During training (batched, IQN):
214+
z_seq = self.encoder(states_flat) # (B*L, hidden_size)
215+
prev_actions = ... # (B*L,) - shifted actions (or zeros for first step)
216+
a_emb_seq = self._embed_action(prev_actions, batch_size=B*L) # (B*L, action_embed_dim)
217+
lstm_input_seq = torch.cat([z_seq, a_emb_seq], dim=-1) # (B*L, hidden_size + action_embed_dim)
218+
# Reshape for LSTM: (L, B, hidden_size + action_embed_dim)
219+
lstm_input_reshaped = lstm_input_seq.view(B, L, -1).permute(1, 0, 2)
220+
lstm_out, _ = self.lstm(lstm_input_reshaped, hidden)
221+
```
222+
223+
**PPO (batch_first=True):**
224+
```python
225+
# During acting:
226+
z_t = self.encoder(state_tensor) # (1, hidden_size) after encoder
227+
a_emb = self._embed_action(self._last_action, batch_size=1) # (1, action_embed_dim)
228+
lstm_input = torch.cat([z_t, a_emb], dim=-1) # (1, hidden_size + action_embed_dim)
229+
lstm_input_seq = lstm_input.unsqueeze(1) # (1, 1, hidden_size + action_embed_dim) for LSTM
230+
lstm_out, hidden = self.lstm(lstm_input_seq, hidden) # batch_first=True
231+
232+
# During training (batched, PPO):
233+
# Process each minibatch sequentially or in parallel
234+
# For each state at index i, use action from index i-1
235+
z_batch = self.encoder(mb_states) # (B, hidden_size)
236+
prev_actions = ... # (B,) - previous actions for each sample
237+
a_emb_batch = self._embed_action(prev_actions, batch_size=B) # (B, action_embed_dim)
238+
lstm_input_batch = torch.cat([z_batch, a_emb_batch], dim=-1) # (B, hidden_size + action_embed_dim)
239+
lstm_input_seq = lstm_input_batch.unsqueeze(1) # (B, 1, hidden_size + action_embed_dim)
240+
lstm_out, _ = self.lstm(lstm_input_seq, (mb_h, mb_c)) # batch_first=True
241+
```
242+
243+
### 6. Testing Considerations
244+
245+
1. **Backward Compatibility**: Ensure existing code still works (make action embedding optional via flag)
246+
2. **Gradient Flow**: Verify gradients flow through action embedding to encoder and LSTM
247+
3. **Memory**: Check that action tracking doesn't leak memory
248+
4. **Episode Boundaries**: Test that actions reset correctly at episode boundaries (both in acting and training)
249+
5. **First Step**: Verify zero embedding works correctly for first step of episodes
250+
6. **Action Shifting**: Verify that actions are correctly shifted (action at step t-1 used for LSTM input at step t)
251+
7. **Batch Processing**: Test with batches containing multiple episodes with different lengths
252+
8. **PPO State Recomputation**: Verify that PPO correctly recomputes LSTM states during training (not using stored states)
253+
9. **CPC Integration**: Test that CPC loss computation works correctly with action-aware LSTM states
254+
255+
### 7. Configuration Options
256+
257+
Add optional parameter to enable/disable action input:
258+
```python
259+
def __init__(
260+
...
261+
use_action_input: bool = True, # New parameter
262+
action_embed_dim: Optional[int] = None, # Auto if None
263+
...
264+
):
265+
self.use_action_input = use_action_input
266+
if use_action_input:
267+
# Add action embedding and modify LSTM
268+
else:
269+
# Keep original architecture
270+
```
271+
272+
### 8. Migration Path
273+
274+
1. **Phase 1**: Implement with `use_action_input=False` by default (backward compatible)
275+
2. **Phase 2**: Test with `use_action_input=True` on small experiments
276+
3. **Phase 3**: Enable by default after validation
277+
278+
## Files to Modify
279+
280+
1. `sorrel/models/pytorch/recurrent_iqn_lstm_cpc_fixed.py`
281+
- Add action embedding
282+
- Modify LSTM input size
283+
- Update `take_action()`, `train_step()`, `_recompute_sequence_with_gradients()`
284+
285+
2. `sorrel/models/pytorch/recurrent_ppo_lstm_generic.py`
286+
- Add action embedding
287+
- Modify LSTM input size
288+
- Update `take_action()`, `learn()`, `_forward_base()`
289+
290+
3. `sorrel/models/pytorch/recurrent_ppo_lstm_cpc.py`
291+
- Same as above plus CPC-specific updates
292+
293+
## Summary
294+
295+
This plan adds the last step action as input to the LSTM by:
296+
1. Creating an action embedding layer (learned embedding recommended)
297+
2. Concatenating action embeddings with encoded observations before LSTM
298+
3. Tracking last action during acting (store action AFTER selection for next step)
299+
4. Handling action shifting in training (action at step t-1 for LSTM input at step t)
300+
5. Handling edge cases (first step uses zero embedding, episode boundaries reset actions)
301+
6. **PPO-specific**: Recomputing LSTM states during training (not using stored states)
302+
7. Maintaining backward compatibility with optional flag
303+
304+
## Critical Implementation Notes
305+
306+
1. **Action Timing**: Actions are taken AFTER observing state, so for LSTM input at step `t`, we use action from step `t-1`
307+
2. **PPO State Recomputation**: PPO must recompute LSTM states during training when using action inputs (can't use stored `h_states`)
308+
3. **Batch First Difference**: IQN uses `batch_first=False`, PPO uses `batch_first=True` - account for this in reshaping
309+
4. **Episode Boundaries**: When `done=True`, next step should use zero embedding (new episode started)
310+
5. **First Step**: Always use zero embedding for the first step of each episode/sequence
311+
312+
The changes are focused but require careful handling of action timing and state recomputation, especially for PPO.
313+

0 commit comments

Comments
 (0)