initial commit

This commit is contained in:
2026-05-29 18:49:01 +08:00
commit 9f0321eff8
21 changed files with 3143 additions and 0 deletions
+170
View File
@@ -0,0 +1,170 @@
"""
VelocityPredictionModel: CNN + PoseMLP → concat → GRU → Head → [vx_body, vy_body].
Architecture:
Event frame (1, H, W) ──► CNN ──┐
Tilt angles (3,) ──► MLP ──┤──► concat ──► GRU ──► Head ──► [vx, vy]
"""
import torch
import torch.nn as nn
from src.velocity_prediction.config import model_cfg
class CNNEncoder(nn.Module):
"""
4-layer ConvNet with BatchNorm, ReLU, MaxPool, ending with Global Avg Pool.
Input: (B, S, 1, H, W) — processed per-frame (flattened to (B*S, 1, H, W))
Output: (B, S, C_out) — per-frame feature vectors
"""
def __init__(self, cfg=model_cfg.cnn):
super().__init__()
channels = cfg.channels
in_ch = cfg.in_channels
layers = []
for out_ch in channels:
layers.extend([
nn.Conv2d(in_ch, out_ch, kernel_size=cfg.kernel_size, padding=cfg.kernel_size // 2),
# nn.BatchNorm2d(out_ch) if cfg.use_bn else nn.Identity(),
nn.Identity(),
nn.LeakyReLU(inplace=True),
nn.MaxPool2d(cfg.pool_size),
])
in_ch = out_ch
self.conv = nn.Sequential(*layers)
self.gap = nn.AdaptiveAvgPool2d(1)
self.out_dim = channels[-1]
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Args:
x: (B, S, 1, H, W) event frame sequence
Returns:
features: (B, S, C_out)
"""
B, S, C, H, W = x.shape
x = x.view(B * S, C, H, W) # (B*S, 1, H, W)
x = self.conv(x) # (B*S, C_out, H', W')
x = self.gap(x) # (B*S, C_out, 1, 1)
x = x.view(B, S, self.out_dim) # (B, S, C_out)
return x
class PoseMLP(nn.Module):
"""
Encode tilt rotation vector (3,) into a compact feature vector.
Input: (B, S, 3)
Output: (B, S, output_dim)
"""
def __init__(self, cfg=model_cfg.pose_mlp):
super().__init__()
self.net = nn.Sequential(
nn.Linear(cfg.input_dim, cfg.hidden_dim),
nn.LeakyReLU(inplace=True),
nn.Linear(cfg.hidden_dim, cfg.output_dim),
nn.LeakyReLU(inplace=True),
)
self.out_dim = cfg.output_dim
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
x: (B, S, 3) → (B, S, output_dim)
"""
B, S, D = x.shape
x = x.view(B * S, D)
x = self.net(x)
x = x.view(B, S, self.out_dim)
return x
class VelocityPredictionModel(nn.Module):
"""
Full model: CNN + PoseMLP → concat → GRU → Head → [vx, vy].
Input:
events: (B, S, 1, H, W)
tilt: (B, S, 3)
Output:
v_body: (B, 2) — body-frame [vx, vy] for the last frame in the sequence
"""
def __init__(self, cnn_cfg=model_cfg.cnn, pose_cfg=model_cfg.pose_mlp,
gru_cfg=model_cfg.gru, head_cfg=model_cfg.head):
super().__init__()
self.cnn = CNNEncoder(cnn_cfg)
self.pose_mlp = PoseMLP(pose_cfg)
fused_dim = self.cnn.out_dim + self.pose_mlp.out_dim # 256 + 64 = 320
self.gru = nn.GRU(
input_size=fused_dim,
hidden_size=gru_cfg.hidden_size,
num_layers=gru_cfg.num_layers,
dropout=gru_cfg.dropout if gru_cfg.num_layers > 1 else 0.0,
batch_first=True,
)
self.head = nn.Sequential(
nn.Linear(gru_cfg.hidden_size, head_cfg.hidden_dim),
nn.LeakyReLU(inplace=True),
nn.Linear(head_cfg.hidden_dim, head_cfg.output_dim),
)
# # Small init for the final layer: start from near-zero output
# self.head[-1].weight.data.mul_(0.01)
# self.head[-1].bias.data.zero_()
def forward(self, events: torch.Tensor, tilt: torch.Tensor) -> torch.Tensor:
"""
Args:
events: (B, S, 1, H, W)
tilt: (B, S, 3)
Returns:
v_body: (B, 2) predicted body-frame [vx, vy] at the last timestep
"""
# Per-frame encoding
cnn_feat = self.cnn(events) # (B, S, 256)
pose_feat = self.pose_mlp(tilt) # (B, S, 64)
# Fuse per frame
fused = torch.cat([cnn_feat, pose_feat], dim=-1) # (B, S, 320)
# GRU temporal modelling
gru_out, h_n = self.gru(fused) # gru_out: (B, S, 128), h_n: (1, B, 128)
# Use last hidden state
last_hidden = h_n[-1] # (B, 128)
# Head regression
v_body = self.head(last_hidden) # (B, 2)
return v_body
def count_parameters(model: nn.Module) -> int:
"""Count trainable parameters."""
return sum(p.numel() for p in model.parameters() if p.requires_grad)
if __name__ == "__main__":
# Quick sanity check
model = VelocityPredictionModel()
total = count_parameters(model)
print(f"Total trainable parameters: {total:,} ({total/1e6:.3f} M)")
# Forward pass test
B, S, H, W = 4, 8, 240, 320
events = torch.randn(B, S, 1, H, W)
tilt = torch.randn(B, S, 3)
out = model(events, tilt)
print(f"Input events: {events.shape}")
print(f"Input tilt: {tilt.shape}")
print(f"Output: {out.shape} (should be [4, 2])")