Files
roboimi/roboimi/vla/models/backbones/resnet_diffusion.py
T

467 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from roboimi.vla.core.interfaces import VLABackbone
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import numpy as np
from typing import Callable, Optional, Tuple, Union
from .attnres_resnet2d import AttnResResNetLikeBackbone2D
def _replace_submodules(
root_module: nn.Module, predicate: Callable[[nn.Module], bool], func: Callable[[nn.Module], nn.Module]
) -> nn.Module:
"""
Args:
root_module: 需要替换子模块的根模块
predicate: 接受一个模块作为参数,如果该模块需要被替换则返回 True。
func: 接受一个模块作为参数,并返回一个新的模块来替换它。
Returns:
子模块已被替换的根模块。
"""
if predicate(root_module):
return func(root_module)
replace_list = [k.split(".") for k, m in root_module.named_modules(remove_duplicate=True) if predicate(m)]
for *parents, k in replace_list:
parent_module = root_module
if len(parents) > 0:
parent_module = root_module.get_submodule(".".join(parents))
if isinstance(parent_module, nn.Sequential):
src_module = parent_module[int(k)]
else:
src_module = getattr(parent_module, k)
tgt_module = func(src_module)
if isinstance(parent_module, nn.Sequential):
parent_module[int(k)] = tgt_module
else:
setattr(parent_module, k, tgt_module)
# 验证所有 BN 是否已被替换
assert not any(predicate(m) for _, m in root_module.named_modules(remove_duplicate=True))
return root_module
class SpatialSoftmax(nn.Module):
"""
Finn 等人在 "Deep Spatial Autoencoders for Visuomotor Learning" 中描述的空间软 Argmax 操作
(https://huggingface.co/papers/1509.06113)。这是 robomimic 实现的一个最小移植版本。
"""
def __init__(self, input_shape, num_kp=None):
"""
Args:
input_shape (list): (C, H, W) 输入特征图形状。
num_kp (int): 输出中的关键点数量。如果为 None,输出将具有与输入相同的通道数。
"""
super().__init__()
assert len(input_shape) == 3
self._in_c, self._in_h, self._in_w = input_shape
if num_kp is not None:
self.nets = torch.nn.Conv2d(self._in_c, num_kp, kernel_size=1)
self._out_c = num_kp
else:
self.nets = None
self._out_c = self._in_c
# 我们可以直接使用 torch.linspace,但这似乎与 numpy 的行为略有不同
# 并且会导致预训练模型的 pc_success 略有下降。
pos_x, pos_y = np.meshgrid(np.linspace(-1.0, 1.0, self._in_w), np.linspace(-1.0, 1.0, self._in_h))
pos_x = torch.from_numpy(pos_x.reshape(self._in_h * self._in_w, 1)).float()
pos_y = torch.from_numpy(pos_y.reshape(self._in_h * self._in_w, 1)).float()
# 注册为 buffer,以便将其移动到正确的设备。
self.register_buffer("pos_grid", torch.cat([pos_x, pos_y], dim=1))
def forward(self, features: torch.Tensor) -> torch.Tensor:
"""
Args:
features: (B, C, H, W) 输入特征图。
Returns:
(B, K, 2) 关键点的图像空间坐标。
"""
if self.nets is not None:
features = self.nets(features)
# [B, K, H, W] -> [B * K, H * W],其中 K 是关键点数量
features = features.reshape(-1, self._in_h * self._in_w)
# 2d softmax 归一化
attention = F.softmax(features, dim=-1)
# [B * K, H * W] x [H * W, 2] -> [B * K, 2] 用于 x 和 y 维度的空间坐标均值
expected_xy = attention @ self.pos_grid
# 重塑为 [B, K, 2]
feature_keypoints = expected_xy.view(-1, self._out_c, 2)
return feature_keypoints
class _SingleRgbEncoder(nn.Module):
"""单个摄像头的 RGB 编码器,支持独立或共享使用"""
def __init__(
self,
vision_backbone: str,
pretrained_backbone_weights: str | None,
input_shape: Tuple[int, int, int],
crop_shape: Optional[Tuple[int, int]],
crop_is_random: bool,
use_group_norm: bool,
spatial_softmax_num_keypoints: int,
freeze_backbone: bool = True, # 新增:是否冻结backbone
vision_backbone_mode: str = "resnet",
attnres_stem_dim: int = 64,
attnres_stage_dims: Optional[Tuple[int, ...]] = None,
attnres_stage_depths: Optional[Tuple[int, ...]] = None,
attnres_stage_heads: Optional[Tuple[int, ...]] = None,
attnres_stage_kv_heads: Optional[Tuple[int, ...]] = None,
attnres_stage_window_sizes: Optional[Tuple[int, ...]] = None,
attnres_dropout: float = 0.0,
attnres_ffn_mult: float = 2.667,
attnres_eps: float = 1e-6,
attnres_rope_theta: float = 10000.0,
):
super().__init__()
# 设置可选的预处理
if crop_shape is not None:
self.do_crop = True
# 评估时始终使用中心裁剪
self.center_crop = torchvision.transforms.CenterCrop(crop_shape)
if crop_is_random:
self.maybe_random_crop = torchvision.transforms.RandomCrop(crop_shape)
else:
self.maybe_random_crop = self.center_crop
else:
self.do_crop = False
crop_shape = input_shape[1:]
if vision_backbone_mode == "resnet":
# 设置骨干网络
backbone_model = getattr(torchvision.models, vision_backbone)(
weights=pretrained_backbone_weights
)
# 移除 AvgPool 和 FC (假设 layer4 是 children()[-3])
self.backbone = nn.Sequential(*(list(backbone_model.children())[:-2]))
if use_group_norm:
self.backbone = _replace_submodules(
root_module=self.backbone,
predicate=lambda x: isinstance(x, nn.BatchNorm2d),
func=lambda x: nn.GroupNorm(
num_groups=max(1, x.num_features // 16),
num_channels=x.num_features,
),
)
elif vision_backbone_mode == "attnres_resnet":
self.backbone = AttnResResNetLikeBackbone2D(
input_channels=input_shape[0],
stem_dim=attnres_stem_dim,
stage_dims=tuple(attnres_stage_dims or (64, 128, 256, 512)),
stage_depths=tuple(attnres_stage_depths or (2, 2, 2, 2)),
stage_heads=tuple(attnres_stage_heads or (4, 4, 8, 8)),
stage_kv_heads=tuple(attnres_stage_kv_heads or (1, 1, 1, 1)),
stage_window_sizes=tuple(attnres_stage_window_sizes or (7, 7, 7, 7)),
use_group_norm=use_group_norm,
dropout=attnres_dropout,
ffn_mult=attnres_ffn_mult,
eps=attnres_eps,
rope_theta=attnres_rope_theta,
)
else:
raise ValueError(f"不支持的 vision_backbone_mode: {vision_backbone_mode}")
# 冻结backbone参数(可选)
if freeze_backbone:
for param in self.backbone.parameters():
param.requires_grad = False
# 设置池化和最终层
# 使用试运行来获取特征图形状
dummy_shape = (1, input_shape[0], *crop_shape)
with torch.no_grad():
dummy_out = self.backbone(torch.zeros(dummy_shape))
feature_map_shape = dummy_out.shape[1:] # (C, H, W)
self.pool = SpatialSoftmax(feature_map_shape, num_kp=spatial_softmax_num_keypoints)
self.feature_dim = spatial_softmax_num_keypoints * 2
self.out = nn.Linear(spatial_softmax_num_keypoints * 2, self.feature_dim)
self.relu = nn.ReLU()
# 注册ImageNet标准化参数为buffer(会自动移到GPU)
self.register_buffer('mean', torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1))
self.register_buffer('std', torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1))
def forward_single_image(self, x: torch.Tensor) -> torch.Tensor:
if self.do_crop:
x = self.maybe_random_crop(x) if self.training else self.center_crop(x)
# ImageNet标准化(预训练权重期望的输入分布)
x = (x - self.mean) / self.std
x = self.relu(self.out(torch.flatten(self.pool(self.backbone(x)), start_dim=1)))
return x
class ResNetDiffusionBackbone(VLABackbone):
def __init__(
self,
vision_backbone: str = "resnet18",
pretrained_backbone_weights: str | None = None,
input_shape: Tuple[int, int, int] = (3, 84, 84), # (C, H, W)
crop_shape: Optional[Tuple[int, int]] = None,
crop_is_random: bool = True,
use_group_norm: bool = True,
spatial_softmax_num_keypoints: int = 32,
use_separate_rgb_encoder_per_camera: bool = False, # 新增:是否为每个摄像头使用独立编码器
output_tokens_per_camera: bool = False, # 是否按相机返回多token,而不是拼成一个token
num_cameras: int = 1, # 新增:摄像头数量(仅在独立编码器模式下使用)
camera_names: Optional[Tuple[str, ...]] = None, # 显式相机顺序
freeze_backbone: bool = True, # 新增:是否冻结ResNet backbone(推荐True
vision_backbone_mode: str = "resnet",
attnres_stem_dim: int = 64,
attnres_stage_dims: Optional[Tuple[int, ...]] = None,
attnres_stage_depths: Optional[Tuple[int, ...]] = None,
attnres_stage_heads: Optional[Tuple[int, ...]] = None,
attnres_stage_kv_heads: Optional[Tuple[int, ...]] = None,
attnres_stage_window_sizes: Optional[Tuple[int, ...]] = None,
attnres_dropout: float = 0.0,
attnres_ffn_mult: float = 2.667,
attnres_eps: float = 1e-6,
attnres_rope_theta: float = 10000.0,
):
super().__init__()
self.use_separate_rgb_encoder_per_camera = use_separate_rgb_encoder_per_camera
self.output_tokens_per_camera = bool(output_tokens_per_camera)
self.num_cameras = num_cameras
self.tokens_per_step = self.num_cameras if self.output_tokens_per_camera else 1
self.camera_names = tuple(camera_names) if camera_names is not None else None
if self.camera_names is not None and len(self.camera_names) != self.num_cameras:
raise ValueError(
f"camera_names 长度({len(self.camera_names)})与 num_cameras({self.num_cameras})不一致"
)
if use_separate_rgb_encoder_per_camera:
# 独立编码器模式:为每个摄像头创建独立的编码器
encoders = [
_SingleRgbEncoder(
vision_backbone=vision_backbone,
pretrained_backbone_weights=pretrained_backbone_weights,
input_shape=input_shape,
crop_shape=crop_shape,
crop_is_random=crop_is_random,
use_group_norm=use_group_norm,
spatial_softmax_num_keypoints=spatial_softmax_num_keypoints,
freeze_backbone=freeze_backbone,
vision_backbone_mode=vision_backbone_mode,
attnres_stem_dim=attnres_stem_dim,
attnres_stage_dims=attnres_stage_dims,
attnres_stage_depths=attnres_stage_depths,
attnres_stage_heads=attnres_stage_heads,
attnres_stage_kv_heads=attnres_stage_kv_heads,
attnres_stage_window_sizes=attnres_stage_window_sizes,
attnres_dropout=attnres_dropout,
attnres_ffn_mult=attnres_ffn_mult,
attnres_eps=attnres_eps,
attnres_rope_theta=attnres_rope_theta,
)
for _ in range(num_cameras)
]
self.rgb_encoder = nn.ModuleList(encoders)
# 重要:output_dim 始终表示单个编码器的特征维度(与 lerobot 保持一致)
self.feature_dim = encoders[0].feature_dim
else:
# 共享编码器模式:所有摄像头共享同一个编码器
self.rgb_encoder = _SingleRgbEncoder(
vision_backbone=vision_backbone,
pretrained_backbone_weights=pretrained_backbone_weights,
input_shape=input_shape,
crop_shape=crop_shape,
crop_is_random=crop_is_random,
use_group_norm=use_group_norm,
spatial_softmax_num_keypoints=spatial_softmax_num_keypoints,
freeze_backbone=freeze_backbone,
vision_backbone_mode=vision_backbone_mode,
attnres_stem_dim=attnres_stem_dim,
attnres_stage_dims=attnres_stage_dims,
attnres_stage_depths=attnres_stage_depths,
attnres_stage_heads=attnres_stage_heads,
attnres_stage_kv_heads=attnres_stage_kv_heads,
attnres_stage_window_sizes=attnres_stage_window_sizes,
attnres_dropout=attnres_dropout,
attnres_ffn_mult=attnres_ffn_mult,
attnres_eps=attnres_eps,
attnres_rope_theta=attnres_rope_theta,
)
self.feature_dim = self.rgb_encoder.feature_dim
def _ordered_camera_names(self, images) -> Tuple[str, ...]:
if self.camera_names is None:
camera_names = tuple(sorted(images.keys()))
if len(camera_names) != self.num_cameras:
raise ValueError(
f"图像输入相机数量({len(camera_names)})与 num_cameras({self.num_cameras})不一致"
)
return camera_names
missing = [cam_name for cam_name in self.camera_names if cam_name not in images]
if missing:
raise ValueError(
f"图像输入缺少必需相机。missing={missing}, expected={list(self.camera_names)}"
)
return self.camera_names
def forward(self, images):
"""
Args:
images: Dict[str, Tensor], 每个摄像头的图像
形状: {cam_name: (B, T, C, H, W)}
Returns:
Tensor: (B, T, total_feature_dim)
"""
any_tensor = next(iter(images.values()))
B, T = any_tensor.shape[:2]
cam_names = self._ordered_camera_names(images)
features_all = []
if self.use_separate_rgb_encoder_per_camera:
# 独立编码器模式:每个摄像头使用对应的编码器
for cam_idx, cam_name in enumerate(cam_names):
img = images[cam_name]
encoder = self.rgb_encoder[cam_idx]
features = encoder.forward_single_image(img.reshape(B * T, *img.shape[2:]))
features_all.append(features)
else:
# 共享编码器模式:所有摄像头共享同一个编码器
for cam_name in cam_names:
img = images[cam_name]
features = self.rgb_encoder.forward_single_image(img.reshape(B * T, *img.shape[2:]))
features_all.append(features)
if self.output_tokens_per_camera:
stacked = torch.stack(features_all, dim=1) # (B*T, num_cams, feature_dim)
return stacked.view(B, T, len(cam_names), self.feature_dim)
return torch.cat(features_all, dim=1).view(B, T, -1)
@property
def output_dim(self):
return self.feature_dim
if __name__ == "__main__":
print("=" * 60)
print("🚀 Testing ResNetDiffusionBackbone")
print("=" * 60)
# Configuration
B, T = 2, 5
C, H, W = 3, 96, 96
crop_h, crop_w = 84, 84
num_keypoints = 32
feature_dim_per_cam = num_keypoints * 2
# Create dummy input (2 cameras)
images = {
"cam_high": torch.randn(B, T, C, H, W),
"cam_wrist": torch.randn(B, T, C, H, W)
}
num_cameras = len(images)
# ============================================================================
# Test 1: Shared Encoder (默认模式)
# ============================================================================
print("\n[Test 1] Shared Encoder Mode")
print("-" * 60)
backbone_shared = ResNetDiffusionBackbone(
vision_backbone="resnet18",
pretrained_backbone_weights=None, # Speed up test
input_shape=(C, H, W),
crop_shape=(crop_h, crop_w),
crop_is_random=True,
use_group_norm=True,
spatial_softmax_num_keypoints=num_keypoints,
use_separate_rgb_encoder_per_camera=False, # 共享编码器
)
print(f"✅ Shared encoder model instantiated")
print(f" Output dim per camera: {feature_dim_per_cam}")
print(f" Number of cameras: {num_cameras}")
print(f" Expected total dim: {num_cameras * feature_dim_per_cam}")
output = backbone_shared(images)
print(f"\n🔄 Forward pass completed")
print(f" Input shapes: {[v.shape for v in images.values()]}")
print(f" Output shape: {output.shape}")
expected_dim = num_cameras * feature_dim_per_cam
assert output.shape == (B, T, expected_dim), f"Expected shape {(B, T, expected_dim)}, got {output.shape}"
print(f"✨ Test passed!")
# ============================================================================
# Test 2: Separate Encoders (独立编码器模式)
# ============================================================================
print("\n[Test 2] Separate Encoders Mode")
print("-" * 60)
backbone_separate = ResNetDiffusionBackbone(
vision_backbone="resnet18",
pretrained_backbone_weights=None, # Speed up test
input_shape=(C, H, W),
crop_shape=(crop_h, crop_w),
crop_is_random=True,
use_group_norm=True,
spatial_softmax_num_keypoints=num_keypoints,
use_separate_rgb_encoder_per_camera=True, # 独立编码器
num_cameras=num_cameras,
)
print(f"✅ Separate encoders model instantiated")
print(f" Output dim per camera: {feature_dim_per_cam}")
print(f" Number of cameras: {num_cameras}")
print(f" Number of encoders: {len(backbone_separate.rgb_encoder)}")
output = backbone_separate(images)
print(f"\n🔄 Forward pass completed")
print(f" Input shapes: {[v.shape for v in images.values()]}")
print(f" Output shape: {output.shape}")
expected_dim = num_cameras * feature_dim_per_cam
assert output.shape == (B, T, expected_dim), f"Expected shape {(B, T, expected_dim)}, got {output.shape}"
print(f"✨ Test passed!")
# ============================================================================
# Test 3: Verify parameters count
# ============================================================================
print("\n[Test 3] Parameter Count Comparison")
print("-" * 60)
shared_params = sum(p.numel() for p in backbone_shared.parameters())
separate_params = sum(p.numel() for p in backbone_separate.parameters())
print(f" Shared encoder parameters: {shared_params:,}")
print(f" Separate encoders parameters: {separate_params:,}")
print(f" Ratio: {separate_params / shared_params:.2f}x")
assert separate_params > shared_params, "Separate encoders should have more parameters"
print(f"✨ Verification passed!")
# ============================================================================
# Test 4: Verify independent parameters
# ============================================================================
print("\n[Test 4] Verify Independent Parameters")
print("-" * 60)
# Check that encoders have independent parameters
encoder_0_first_param = list(backbone_separate.rgb_encoder[0].parameters())[0]
encoder_1_first_param = list(backbone_separate.rgb_encoder[1].parameters())[0]
# Modify first encoder's parameter
with torch.no_grad():
encoder_0_first_param += 1.0
# Verify they are not the same tensor
assert not torch.allclose(encoder_0_first_param, encoder_1_first_param), \
"Encoders should have independent parameters"
print(f"✅ Encoders have independent parameters")
print(f"✨ All tests passed!")
print("\n" + "=" * 60)
print("🎉 All tests completed successfully!")
print("=" * 60)