Spaces:
Running on Zero
Running on Zero
File size: 13,572 Bytes
632d805 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | """
GPU-accelerated data augmentation for training.
All augmentations operate on GPU tensors and are applied in the training
loop after the batch has been moved to the GPU.
data.augmentation keys:
- rotation_augmentation: rotation about the vertical (z) axis. One angle per
batch, a multiple of angle_step_size, with probability prob_use_rotation;
with per_sample: true every sample gets its own uniform angle in [0, 360)
(probability prob_use_rotation per sample).
- reflection_augmentation: per sample with probability prob, the scene is
mirrored in the vertical x = 0 plane (x -> -x). Mirrored rigid bodies under
gravity, contact and isotropic friction follow the mirrored trajectories,
so the mirrored sequence is again a valid one.
- rigid_noise: the input frames (all but the last frame of a sample) get a
rigid random-walk perturbation per object (rotation about the object's
centroid plus a translation), written to batch['obj_positions_input'];
batch['obj_positions'] stays the ground truth the losses compare against.
pos_std (m) and rot_std_deg are the standard deviations at the last input
frame for step size 1, scaled by step_size ** step_size_exponent.
"""
import torch
import math
from typing import Dict
import logging
logger = logging.getLogger(__name__)
def rotvec_to_matrix(rotvec: torch.Tensor) -> torch.Tensor:
"""Rotation matrices (..., 3, 3) of rotation vectors (..., 3) (Rodrigues' formula)."""
theta = rotvec.norm(dim=-1, keepdim=True) # (..., 1)
small = theta < 1e-8
axis = rotvec / torch.where(small, torch.ones_like(theta), theta)
x, y, z = axis.unbind(-1)
zero = torch.zeros_like(x)
K = torch.stack([zero, -z, y, z, zero, -x, -y, x, zero], dim=-1).view(*rotvec.shape[:-1], 3, 3)
s = torch.sin(theta).unsqueeze(-1)
c = torch.cos(theta).unsqueeze(-1)
eye = torch.eye(3, dtype=rotvec.dtype, device=rotvec.device).expand(K.shape)
R = eye + s * K + (1.0 - c) * (K @ K)
return torch.where(small.unsqueeze(-1), eye, R)
class GPUAugmentation:
"""
GPU-accelerated augmentation pipeline.
Features:
- Rotation augmentation (Z-axis only, no ground mesh), per batch or per sample
- Reflection augmentation (x -> -x, per sample)
- Rigid random-walk noise on the input frames
- All operations on GPU tensors
"""
def __init__(self, config: Dict, device: torch.device):
"""
Initialize GPU augmentation.
Args:
config: Augmentation configuration
device: Target GPU device
"""
self.device = device
self.config = config
aug_cfg = config.get('data', {}).get('augmentation', {})
# Rotation augmentation settings
rotation_cfg = aug_cfg.get('rotation_augmentation', {})
self.rotation_enabled = rotation_cfg.get('enable', False)
self.rotation_angle_step = rotation_cfg.get('angle_step_size', 30)
self.rotation_prob = rotation_cfg.get('prob_use_rotation', 0.3)
self.rotation_per_sample = bool(rotation_cfg.get('per_sample', False))
# Pre-compute rotation angles (only if rotation enabled and valid step size)
if self.rotation_enabled and self.rotation_angle_step > 0:
self.rotation_angles = torch.arange(
self.rotation_angle_step, 360, self.rotation_angle_step,
dtype=torch.float32, device=device
)
else:
# Rotation disabled or invalid step size - create empty tensor
self.rotation_angles = torch.tensor([], dtype=torch.float32, device=device)
reflection_cfg = aug_cfg.get('reflection_augmentation', {}) or {}
self.reflection_enabled = bool(reflection_cfg.get('enable', False))
self.reflection_prob = float(reflection_cfg.get('prob', 0.5))
noise_cfg = aug_cfg.get('rigid_noise', {}) or {}
self.noise_enabled = bool(noise_cfg.get('enable', False))
self.noise_pos_std = float(noise_cfg.get('pos_std', 0.0))
self.noise_rot_std = math.radians(float(noise_cfg.get('rot_std_deg', 0.0)))
self.noise_step_exponent = float(noise_cfg.get('step_size_exponent', 1.0))
if self.noise_enabled and config.get('data', {}).get('center_scene_by_t0', False):
# centring shifts the network inputs in place; with separate input frames the targets would not follow
raise ValueError("data.augmentation.rigid_noise cannot be combined with data.center_scene_by_t0")
# Counters for periodic logging.
self.rotation_total_calls = 0
self.rotation_applied_count = 0
logger.info(f"\n{'='*80}")
logger.info(f"[GPU Augmentation] Initialization")
logger.info(f"{'='*80}")
logger.info(f"Rotation Augmentation:")
logger.info(f" - Enabled: {self.rotation_enabled}")
if self.rotation_enabled:
if self.rotation_per_sample:
logger.info(f" - Per sample, uniform angle in [0, 360)")
else:
logger.info(f" - Angle step size: {self.rotation_angle_step}°")
logger.info(f" - Rotation angles: {self.rotation_angles.cpu().tolist()}")
logger.info(f" - Probability: {self.rotation_prob:.2%}")
logger.info(f"Reflection Augmentation (x -> -x): enabled={self.reflection_enabled}"
+ (f", probability {self.reflection_prob:.2%} per sample" if self.reflection_enabled else ""))
logger.info(f"Rigid input noise: enabled={self.noise_enabled}"
+ (f", pos_std={self.noise_pos_std} m, rot_std={math.degrees(self.noise_rot_std):.3f} deg at the last "
f"input frame (step size 1), x step_size^{self.noise_step_exponent:g}" if self.noise_enabled else ""))
logger.info(f"{'='*80}\n")
@property
def enabled(self) -> bool:
return self.rotation_enabled or self.reflection_enabled or self.noise_enabled
def apply_rotation_augmentation(
self,
batch: Dict[str, torch.Tensor],
split: str = 'train'
) -> Dict[str, torch.Tensor]:
"""
Apply rotation augmentation around Z-axis (GPU version, no ground mesh).
Args:
batch: Batch dict with GPU tensors
split: 'train', 'val', or 'test' (only train gets augmented)
Returns:
Augmented batch dict
"""
# Only apply to training split
if split != 'train':
batch['rotation_angle_deg'] = 0.0
return batch
self.rotation_total_calls += 1
if not self.rotation_enabled:
batch['rotation_angle_deg'] = 0.0
return batch
if self.rotation_per_sample:
return self._rotate_per_sample(batch)
if len(self.rotation_angles) == 0:
raise ValueError(
f"Rotation enabled but no valid angles available. "
f"angle_step_size={self.rotation_angle_step} must be > 0"
)
# Decide whether to rotate (probabilistic)
rand_val = torch.rand(1, device=self.device).item()
if rand_val > self.rotation_prob:
batch['rotation_angle_deg'] = 0.0
return batch
# Randomly select rotation angle
angle_idx = torch.randint(0, len(self.rotation_angles), (1,), device=self.device)
angle_deg = self.rotation_angles[angle_idx].item()
if angle_deg == 0:
batch['rotation_angle_deg'] = 0.0
return batch
self.rotation_applied_count += 1
if self.rotation_applied_count <= 5 or self.rotation_total_calls % 100 == 0:
apply_rate = self.rotation_applied_count / self.rotation_total_calls
logger.info(
f"[GPU Rotation Aug] Applying {angle_deg:.0f}° | "
f"Batch {self.rotation_total_calls} | "
f"Applied: {self.rotation_applied_count}/{self.rotation_total_calls} ({apply_rate:.2%})"
)
# Create rotation matrix (Z-axis)
angle_rad = math.radians(angle_deg)
cos_a = math.cos(angle_rad)
sin_a = math.sin(angle_rad)
R = torch.tensor([
[cos_a, -sin_a, 0],
[sin_a, cos_a, 0],
[0, 0, 1]
], dtype=torch.float32, device=self.device)
# Rotate obj_positions (B, T, Nobj, Nv, 3); all objects rotate since
# there is no ground mesh.
obj_pos = batch['obj_positions']
obj_pos_flat = obj_pos.reshape(-1, 3)
obj_pos_rotated = torch.matmul(obj_pos_flat, R.T)
batch['obj_positions'] = obj_pos_rotated.view(obj_pos.shape)
# Rotate ref_obj_positions: (B, Nobj, Nv, 3)
ref_pos = batch['ref_obj_positions']
ref_pos_flat = ref_pos.reshape(-1, 3)
ref_pos_rotated = torch.matmul(ref_pos_flat, R.T)
batch['ref_obj_positions'] = ref_pos_rotated.view(ref_pos.shape)
# Record the applied angle so ground-truth overlays can be rotated to match.
batch['rotation_angle_deg'] = angle_deg
return batch
def _per_sample_linear(self, batch: Dict[str, torch.Tensor], M: torch.Tensor) -> None:
"""Apply the per-sample linear maps M (B, 3, 3) to obj_positions (B,T,Nobj,Nv,3) and ref_obj_positions."""
# einsum may return non-contiguous strides; the model reshapes these tensors with .view()
batch['obj_positions'] = torch.einsum('bij,btonj->btoni', M, batch['obj_positions']).contiguous()
ref = batch['ref_obj_positions']
if ref.dim() == 4:
batch['ref_obj_positions'] = torch.einsum('bij,bonj->boni', M, ref).contiguous()
else:
raise ValueError(f"per-sample augmentation needs per-sample ref_obj_positions (B,Nobj,Nv,3); got {tuple(ref.shape)}")
def _rotate_per_sample(self, batch: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
B = batch['obj_positions'].shape[0]
angle = torch.rand(B, device=self.device) * (2 * math.pi)
angle = torch.where(torch.rand(B, device=self.device) < self.rotation_prob, angle, torch.zeros_like(angle))
c, s = torch.cos(angle), torch.sin(angle)
zero, one = torch.zeros_like(c), torch.ones_like(c)
R = torch.stack([c, -s, zero, s, c, zero, zero, zero, one], dim=-1).view(B, 3, 3)
self._per_sample_linear(batch, R)
batch['rotation_angle_deg'] = 0.0 # no single angle: ground-truth overlays cannot be rotated to match
batch['rotation_angles_deg_per_sample'] = torch.rad2deg(angle)
return batch
def apply_reflection(self, batch: Dict[str, torch.Tensor], split: str = 'train') -> Dict[str, torch.Tensor]:
if split != 'train' or not self.reflection_enabled:
return batch
B = batch['obj_positions'].shape[0]
flip = torch.rand(B, device=self.device) < self.reflection_prob
sx = torch.where(flip, -torch.ones(B, device=self.device), torch.ones(B, device=self.device))
M = torch.diag_embed(torch.stack([sx, torch.ones_like(sx), torch.ones_like(sx)], dim=-1))
self._per_sample_linear(batch, M)
batch['reflected'] = flip
return batch
def apply_rigid_noise(self, batch: Dict[str, torch.Tensor], split: str = 'train') -> Dict[str, torch.Tensor]:
if split != 'train' or not self.noise_enabled:
return batch
pos = batch['obj_positions'] # (B,T,Nobj,Nv,3)
B, T, Nobj, Nv, _ = pos.shape
Tin = T - 1 # the network inputs are frames 0..T-2; frame T-1 is only a target
if Tin < 1:
return batch
scale = torch.ones(B, device=pos.device)
if 'skip_step_size' in batch:
scale = batch['skip_step_size'].to(pos.device).float() ** self.noise_step_exponent
scale = scale.view(B, 1, 1, 1)
# random walk over the input frames: the last input frame has the configured standard deviation
rotvec = (torch.randn(B, Tin, Nobj, 3, device=pos.device) * (self.noise_rot_std / math.sqrt(Tin))).cumsum(1) * scale
shift = (torch.randn(B, Tin, Nobj, 3, device=pos.device) * (self.noise_pos_std / math.sqrt(Tin))).cumsum(1) * scale
R = rotvec_to_matrix(rotvec) # (B,Tin,Nobj,3,3)
x = pos[:, :Tin]
vmask = batch['valid_vtx_mask'][:, :Tin].to(torch.bool) # (B,Tin,Nobj,Nv)
w = vmask.unsqueeze(-1).to(x.dtype)
centroid = (x * w).sum(3, keepdim=True) / w.sum(3, keepdim=True).clamp_min(1.0) # (B,Tin,Nobj,1,3)
xn = torch.einsum('btoij,btonj->btoni', R, x - centroid) + centroid + shift.unsqueeze(3)
xn = torch.where(vmask.unsqueeze(-1), xn, x) # padding stays as it is
batch['obj_positions_input'] = torch.cat([xn, pos[:, Tin:]], dim=1).contiguous()
return batch
def __call__(
self,
batch: Dict[str, torch.Tensor],
split: str = 'train'
) -> Dict[str, torch.Tensor]:
"""
Apply all enabled augmentations.
Args:
batch: Batch dict with GPU tensors
split: 'train', 'val', or 'test'
Returns:
Augmented batch dict
"""
if self.rotation_enabled:
batch = self.apply_rotation_augmentation(batch, split)
else:
# Rotation disabled: record a zero angle for consistency.
batch['rotation_angle_deg'] = 0.0
batch = self.apply_reflection(batch, split)
# last: the noise perturbs the (rotated, mirrored) input frames
batch = self.apply_rigid_noise(batch, split)
return batch
|