ailerobothumanoidwhole-bodyunitree

Humanoid Manipulation: Whole-body Control với LeRobot

Robot hình người là nền tảng manipulation tối thượng — high-DOF control, egocentric vision, và humanoid foundation models.

Nguyễn Anh Tuấn5 tháng 4, 20269 phút đọc
Humanoid Manipulation: Whole-body Control với LeRobot

Giới thiệu: Đỉnh cao của Manipulation

Trong suốt series này, chúng ta đã đi từ single-arm (bài 3), qua dual-arm (bài 6-7), đến mobile manipulation (bài 8). Mỗi bước đều thêm độ phức tạp — thêm DOF, thêm coordination, thêm perception.

Bài viết này đưa chúng ta đến đỉnh cao: robot hình người (humanoid). Với 20-50+ DOF, hai bàn tay khéo léo (dexterous hands), khả năng di chuyển bằng hai chân, và hình dáng tương tự con người — humanoid robot là nền tảng manipulation tối thượng và cũng là thách thức lớn nhất trong robotics.

Humanoid robot for manipulation

Tại sao Humanoid cho Manipulation?

Ưu điểm Giải thích
Human environment compatible Thế giới được thiết kế cho người — cửa, bàn, tay vịn
Dexterous hands 5 ngón tay × 4 DOF = 20 DOF mỗi tay
Large workspace Toàn bộ cơ thể là workspace — tầm với lớn
Whole-body dynamics Dùng momentum, trọng lực để hỗ trợ manipulation
Transfer from human data Có thể học từ video người thật
Thách thức Mô tả
Extreme DOF 30-50+ joints cần điều khiển đồng thời
Balance Phải giữ cân bằng trong khi manipulation
Contact-rich Cả chân và tay đều tiếp xúc môi trường
Compute Inference phải real-time trên edge devices

High-DOF Action Spaces

Anatomy of a Humanoid Action Space

import numpy as np
from dataclasses import dataclass

@dataclass
class HumanoidActionSpace:
    """Action space cho humanoid robot manipulation.
    
    Ví dụ: Unitree H1
    - Base: 6 DOF (floating base — controlled by legs)
    - Left leg: 6 DOF
    - Right leg: 6 DOF
    - Torso: 3 DOF (waist yaw, pitch, roll)
    - Left arm: 7 DOF (shoulder 3 + elbow 1 + wrist 3)
    - Right arm: 7 DOF
    - Left hand: 12 DOF (Inspire hand)
    - Right hand: 12 DOF
    Total: ~59 DOF
    """
    
    # Joint groups
    leg_dof: int = 12      # 6 per leg
    torso_dof: int = 3
    arm_dof: int = 14      # 7 per arm
    hand_dof: int = 24     # 12 per hand (optional)
    
    @property
    def total_dof(self):
        return self.leg_dof + self.torso_dof + self.arm_dof + self.hand_dof
    
    @property
    def manipulation_dof(self):
        """DOF liên quan trực tiếp đến manipulation."""
        return self.torso_dof + self.arm_dof + self.hand_dof
    
    def split_action(self, action):
        """Tách action thành các nhóm joints."""
        idx = 0
        legs = action[idx:idx + self.leg_dof]; idx += self.leg_dof
        torso = action[idx:idx + self.torso_dof]; idx += self.torso_dof
        arms = action[idx:idx + self.arm_dof]; idx += self.arm_dof
        hands = action[idx:idx + self.hand_dof]; idx += self.hand_dof
        
        return {
            "legs": legs,
            "torso": torso,
            "left_arm": arms[:7],
            "right_arm": arms[7:],
            "left_hand": hands[:12],
            "right_hand": hands[12:],
        }


# Ví dụ
action_space = HumanoidActionSpace()
print(f"Total DOF: {action_space.total_dof}")
print(f"Manipulation DOF: {action_space.manipulation_dof}")
# Output: Total DOF: 53, Manipulation DOF: 41

Giảm dimensionality cho learning

class ActionSpaceReduction:
    """Giảm action space cho humanoid để dễ train hơn.
    
    Chiến lược:
    1. Fixed legs: Đứng yên, chỉ train upper body
    2. Coupled joints: Ghép các joints liên quan
    3. Latent actions: Dùng VAE encode action space
    4. Primitive-based: Ghép từ motion primitives
    """
    
    def __init__(self, mode="fixed_legs"):
        self.mode = mode
    
    def reduce(self, full_action):
        """Giảm action space."""
        parts = HumanoidActionSpace().split_action(full_action)
        
        if self.mode == "fixed_legs":
            # Chỉ điều khiển upper body (torso + arms + hands)
            return np.concatenate([
                parts["torso"],      # 3 DOF
                parts["left_arm"],   # 7 DOF
                parts["right_arm"],  # 7 DOF
                parts["left_hand"][:6],   # Simplified hand: 6 DOF
                parts["right_hand"][:6],  # Simplified hand: 6 DOF
            ])
            # Total: 29 DOF (giảm từ 53)
        
        elif self.mode == "coupled_fingers":
            # Ghép ngón tay: open/close/spread thay vì từng joint
            left_hand_simple = np.array([
                parts["left_hand"][:4].mean(),    # Fingers curl
                parts["left_hand"][4:8].mean(),   # Fingers spread
                parts["left_hand"][8:12].mean(),  # Thumb
            ])
            right_hand_simple = np.array([
                parts["right_hand"][:4].mean(),
                parts["right_hand"][4:8].mean(),
                parts["right_hand"][8:12].mean(),
            ])
            
            return np.concatenate([
                parts["torso"],
                parts["left_arm"],
                parts["right_arm"],
                left_hand_simple,
                right_hand_simple,
            ])
            # Total: 23 DOF
        
        return full_action


reducer = ActionSpaceReduction(mode="fixed_legs")
full_action = np.random.randn(53)
reduced_action = reducer.reduce(full_action)
print(f"Reduced from {len(full_action)} to {len(reduced_action)} DOF")

Observation Design cho Humanoid

Egocentric cameras

class HumanoidObservation:
    """Thiết kế observation cho humanoid manipulation.
    
    Cameras:
    - Head camera: Bird's eye view, navigation
    - Chest camera: Mid-range manipulation view
    - Left/Right wrist cameras: Close-up manipulation
    
    Proprioception:
    - Joint positions (all joints)
    - Joint velocities
    - IMU data (body orientation)
    - Contact forces (feet + hands)
    """
    
    def __init__(self):
        self.camera_configs = {
            "head": {
                "resolution": (480, 640),
                "fov": 90,
                "position": "head_link",
                "purpose": "Navigation + overview",
            },
            "chest": {
                "resolution": (480, 640),
                "fov": 70,
                "position": "chest_link",
                "purpose": "Mid-range manipulation",
            },
            "left_wrist": {
                "resolution": (480, 640),
                "fov": 90,
                "position": "left_wrist_link",
                "purpose": "Left hand close-up",
            },
            "right_wrist": {
                "resolution": (480, 640),
                "fov": 90,
                "position": "right_wrist_link",
                "purpose": "Right hand close-up",
            },
        }
    
    def get_observation(self, robot) -> dict:
        """Thu thập observation từ humanoid robot."""
        obs = {}
        
        # Camera images
        for cam_name, config in self.camera_configs.items():
            obs[f"observation.images.{cam_name}"] = robot.get_camera_image(
                cam_name, config["resolution"]
            )
        
        # Proprioception
        obs["observation.state"] = np.concatenate([
            robot.get_joint_positions(),    # All joint positions
            robot.get_joint_velocities(),   # All joint velocities
            robot.get_imu_data(),           # Orientation (quaternion)
            robot.get_contact_forces(),     # Feet + hand contacts
        ])
        
        return obs
    
    @property
    def state_dim(self):
        """Tổng dimension của proprioceptive state."""
        return (
            53 +    # Joint positions
            53 +    # Joint velocities
            4 +     # IMU quaternion
            8       # Contact forces (4 foot corners + 2 hands)
        )
        # Total: 118D

Humanoid robot egocentric view

Humanoid Foundation Models

GR00T (NVIDIA)

class GR00TInspired:
    """Architecture lấy cảm hứng từ NVIDIA GR00T N1.5.
    
    GR00T N1.5 components:
    - Vision encoder: Pretrained ViT
    - Language encoder: CLIP text encoder  
    - Action head: Diffusion-based action generation
    - Dual-system architecture:
      * System 1 (fast): Reactive policy, low latency
      * System 2 (slow): Deliberative planning
    
    Reference: https://arxiv.org/abs/2503.14734
    """
    
    def __init__(self):
        self.vision_encoder = None  # Pretrained ViT-B/16
        self.language_encoder = None  # CLIP text encoder
        self.action_head = None  # Diffusion action head
    
    def forward(self, images, language_instruction, proprioception):
        """
        Args:
            images: Dict of camera images
            language_instruction: Text instruction
            proprioception: Joint states + IMU
        
        Returns:
            action: Full-body action (53D+)
        """
        # Encode visual observations
        visual_tokens = []
        for cam_name, image in images.items():
            tokens = self.vision_encoder(image)
            visual_tokens.append(tokens)
        
        # Encode language
        lang_tokens = self.language_encoder(language_instruction)
        
        # Concatenate all tokens
        all_tokens = torch.cat(visual_tokens + [lang_tokens], dim=1)
        
        # Generate action via diffusion
        action = self.action_head.sample(
            condition=all_tokens,
            proprioception=proprioception,
        )
        
        return action

Training humanoid manipulation policy với LeRobot

from lerobot.common.policies.act.configuration_act import ACTConfig
from lerobot.common.policies.act.modeling_act import ACTPolicy

def create_humanoid_manip_policy():
    """Tạo policy cho humanoid manipulation.
    
    Strategy: Fixed legs, chỉ train upper body (29 DOF)
    """
    config = ACTConfig(
        input_shapes={
            "observation.images.head": [3, 480, 640],
            "observation.images.left_wrist": [3, 480, 640],
            "observation.images.right_wrist": [3, 480, 640],
            "observation.state": [70],  # Reduced proprioception
        },
        output_shapes={
            "action": [29],  # Upper body only
        },
        input_normalization_modes={
            "observation.images.head": "mean_std",
            "observation.images.left_wrist": "mean_std",
            "observation.images.right_wrist": "mean_std",
            "observation.state": "min_max",
        },
        output_normalization_modes={
            "action": "min_max",
        },
        
        # Humanoid cần model lớn hơn
        chunk_size=50,
        n_action_steps=50,
        dim_model=768,       # Lớn hơn cho high-DOF
        n_heads=12,
        n_layers=6,
        use_vae=True,
        latent_dim=64,       # Lớn hơn cho action diversity
        kl_weight=10.0,
        
        vision_backbone="resnet18",
        pretrained_backbone=True,
    )
    
    policy = ACTPolicy(config)
    print(f"Parameters: {sum(p.numel() for p in policy.parameters()):,}")
    return policy


def train_humanoid_policy(policy, dataset, num_epochs=500):
    """Training loop cho humanoid manipulation."""
    device = torch.device("cuda")
    policy.to(device)
    
    # Larger batch size nếu GPU đủ VRAM
    optimizer = torch.optim.AdamW(
        policy.parameters(), lr=1e-5, weight_decay=1e-4
    )
    scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(
        optimizer, T_0=100, T_mult=2, eta_min=1e-7
    )
    
    dataloader = torch.utils.data.DataLoader(
        dataset, batch_size=4, shuffle=True, num_workers=4
    )
    
    for epoch in range(num_epochs):
        epoch_loss = 0
        n_batches = 0
        
        for batch in dataloader:
            batch = {k: v.to(device) for k, v in batch.items()}
            output = policy.forward(batch)
            loss = output["loss"]
            
            optimizer.zero_grad()
            loss.backward()
            torch.nn.utils.clip_grad_norm_(policy.parameters(), 10.0)
            optimizer.step()
            
            epoch_loss += loss.item()
            n_batches += 1
        
        scheduler.step()
        
        if (epoch + 1) % 50 == 0:
            avg_loss = epoch_loss / n_batches
            print(f"Epoch {epoch+1}/{num_epochs} | Loss: {avg_loss:.4f}")
    
    return policy

Simulation với Unitree H1/G1

import mujoco

class UnitreeH1Sim:
    """Simulation cho Unitree H1 humanoid trong MuJoCo.
    
    H1 specs:
    - Height: 1.8m
    - Weight: 47kg
    - DOF: 19 (legs 10 + waist 1 + arms 8)
    - Payload: 3kg per arm
    """
    
    def __init__(self, xml_path="unitree_h1/scene.xml"):
        self.model = mujoco.MjModel.from_xml_path(xml_path)
        self.data = mujoco.MjData(self.model)
        
        # Joint mappings
        self.leg_joints = list(range(10))
        self.waist_joint = [10]
        self.arm_joints = list(range(11, 19))
    
    def reset(self, standing=True):
        """Reset robot — đứng thẳng hoặc vị trí custom."""
        mujoco.mj_resetData(self.model, self.data)
        
        if standing:
            # Set standing pose
            self.data.qpos[0:3] = [0, 0, 1.0]  # Position (x, y, z)
            self.data.qpos[3:7] = [1, 0, 0, 0]  # Quaternion (standing)
        
        mujoco.mj_forward(self.model, self.data)
        return self._get_obs()
    
    def step(self, action):
        """Thực thi action.
        
        Args:
            action: Joint position targets [19D for full body]
        """
        # Apply PD control
        self.data.ctrl[:] = action
        
        # Step simulation
        for _ in range(4):  # 4 substeps
            mujoco.mj_step(self.model, self.data)
        
        obs = self._get_obs()
        reward = self._compute_reward()
        done = self._check_done()
        
        return obs, reward, done, {}
    
    def _get_obs(self):
        return {
            "qpos": self.data.qpos.copy(),
            "qvel": self.data.qvel.copy(),
            "head_camera": self._render("head_cam"),
            "left_wrist_camera": self._render("left_wrist_cam"),
            "right_wrist_camera": self._render("right_wrist_cam"),
        }
    
    def _check_done(self):
        """Kiểm tra robot đã ngã chưa."""
        head_height = self.data.xpos[self.model.body("head").id][2]
        return head_height < 0.5  # Đầu dưới 50cm = ngã


# Ví dụ: Humanoid gắp object trên bàn
env = UnitreeH1Sim()
obs = env.reset(standing=True)

policy = create_humanoid_manip_policy()

for step in range(500):
    action = policy.predict(obs)
    obs, reward, done, info = env.step(action)
    
    if done:
        print(f"Fell down at step {step}!")
        break

Balance during Manipulation

class BalanceAwarePolicy:
    """Policy biết giữ cân bằng trong khi manipulation.
    
    Dùng ZMP (Zero Moment Point) để đảm bảo stability.
    """
    
    def __init__(self, manip_policy, balance_controller):
        self.manip_policy = manip_policy
        self.balance_controller = balance_controller
        self.stability_threshold = 0.05  # ZMP margin
    
    def act(self, observation):
        """Sinh action có cân nhắc balance."""
        # Lấy manipulation action (upper body)
        manip_action = self.manip_policy.predict(observation)
        
        # Tính ZMP dự đoán
        predicted_zmp = self.balance_controller.predict_zmp(
            observation["qpos"],
            observation["qvel"],
            manip_action,
        )
        
        # Kiểm tra stability
        support_polygon = self.balance_controller.get_support_polygon(
            observation["contact_forces"]
        )
        
        if self.balance_controller.is_stable(predicted_zmp, support_polygon):
            # Ổn định — thực hiện manipulation action
            leg_action = self.balance_controller.maintain_balance(observation)
        else:
            # Không ổn định — ưu tiên balance
            print("WARNING: Adjusting for balance!")
            manip_action *= 0.5  # Giảm cường độ manipulation
            leg_action = self.balance_controller.recover_balance(observation)
        
        # Ghép full action
        full_action = np.concatenate([leg_action, manip_action])
        return full_action

Papers tham khảo

  1. HumanPlus: Humanoid Shadowing and Imitation from HumansFu et al., 2024 — Human-to-humanoid transfer learning
  2. GR00T N1: Open Foundation Model for Humanoid RobotsNVIDIA, 2024 — Foundation model cho humanoid
  3. Humanoid-Gym: Reinforcement Learning for Humanoid Robot with Zero-Shot Sim2Real Transfer2024 — Sim2real cho humanoid locomotion

Kết luận và bước tiếp theo

Humanoid manipulation là frontier của robotics research. Key insights:

  • Action space reduction rất quan trọng — không train toàn bộ 50+ DOF cùng lúc
  • Balance + manipulation phải được phối hợp — humanoid ngã = thất bại hoàn toàn
  • Foundation models (GR00T, HumanPlus) là hướng đi tương lai — pretrain trên data lớn, fine-tune cho task cụ thể

Bài cuối cùng của series — Sim-to-Real Transfer: Deploy VLA Policy lên Robot thật — sẽ mang tất cả những gì chúng ta đã học vào thế giới thực.

Bài viết liên quan

NT

Nguyễn Anh Tuấn

Robotics & AI Engineer. Building VnRobo — sharing knowledge about robot learning, VLA models, and automation.

Bài viết liên quan

NEWTutorial
Hướng dẫn fine-tune NVIDIA GR00T N1
vlahumanoidnvidiaisaac-labfine-tuningdeep-learninggrootsim2real

Hướng dẫn fine-tune NVIDIA GR00T N1

Hướng dẫn chi tiết fine-tune VLA model GR00T N1 cho humanoid robot với Isaac Lab và dữ liệu AGIBOT World — từ cài đặt đến inference.

12/4/202612 phút đọc
NEWDeep Dive
WholebodyVLA Open-Source: Hướng Dẫn Kiến Trúc & Code
vlahumanoidloco-manipulationiclrrlopen-sourceisaac-lab

WholebodyVLA Open-Source: Hướng Dẫn Kiến Trúc & Code

Deep-dive vào codebase WholebodyVLA — kiến trúc latent action, LMO RL policy, và cách xây dựng pipeline whole-body loco-manipulation cho humanoid.

12/4/202619 phút đọc
NEWTutorial
SimpleVLA-RL (7): Collect Data cho OpenArm
openarmlerobotdata-collectionteleoperationPhần 7

SimpleVLA-RL (7): Collect Data cho OpenArm

Hướng dẫn từng bước setup OpenArm, calibrate, teleoperate và thu thập 50 episodes gắp hộp carton với LeRobot.

11/4/202616 phút đọc