manipulationrlmanipulationmujocoreward-design

RL cho Manipulation: MDP, Reward Design và Environment Setup

Xây dựng nền tảng RL cho manipulation — từ MDP formulation, thiết kế reward function đến setup môi trường MuJoCo từ đầu.

Nguyễn Anh Tuấn10 tháng 3, 202613 phút đọc
RL cho Manipulation: MDP, Reward Design và Environment Setup

Bạn đã bao giờ tự hỏi — làm thế nào để một cánh tay robot học được cách cầm nắm đồ vật, mà không cần ai lập trình từng chuyển động? Câu trả lời nằm ở Reinforcement Learning (RL) — một paradigm cho phép robot tự khám phá, thử nghiệm, và dần dần "ngộ ra" cách thao tác với thế giới vật lý.

Đây là bài đầu tiên trong series RL for Manipulation, nơi chúng ta sẽ đi từ nền tảng lý thuyết đến huấn luyện robot thực hiện các tác vụ manipulation phức tạp. Trong bài này, chúng ta sẽ tập trung vào ba trụ cột: MDP formulation, reward design, và environment setup trong MuJoCo.

Nếu bạn chưa quen với RL cơ bản, hãy đọc trước bài RL Basics cho Robotics để nắm vững các khái niệm nền tảng.

Manipulation dưới góc nhìn MDP

Markov Decision Process là gì?

Mọi bài toán RL đều được mô hình hóa dưới dạng Markov Decision Process (MDP), định nghĩa bởi tuple $(S, A, T, R, \gamma)$:

Thành phần Ý nghĩa Ví dụ trong Manipulation
$S$ (State space) Tập trạng thái Vị trí khớp, vận tốc, pose vật thể
$A$ (Action space) Tập hành động Torque hoặc vị trí mục tiêu cho khớp
$T$ (Transition) Xác suất chuyển trạng thái Vật lý mô phỏng (MuJoCo dynamics)
$R$ (Reward) Hàm phần thưởng +1 khi nắm được vật, -0.01 mỗi bước
$\gamma$ (Discount) Hệ số chiết khấu Thường 0.99 cho manipulation

Điểm mấu chốt khi áp dụng MDP vào manipulation là thiết kế đúng state space và action space. Nếu bạn cho robot quá ít thông tin (state nghèo nàn), nó không thể học. Nếu bạn cho quá nhiều thông tin không cần thiết, việc học trở nên chậm và không ổn định.

Robot arm trong môi trường simulation

State Space cho Manipulation

State space trong manipulation thường bao gồm ba nhóm thông tin chính:

1. Robot State (Proprioception)

  • Vị trí các khớp (joint positions): $q \in \mathbb{R}^n$
  • Vận tốc các khớp (joint velocities): $\dot{q} \in \mathbb{R}^n$
  • Vị trí và hướng gripper (end-effector pose): $x_{ee} \in SE(3)$
  • Trạng thái gripper (mở/đóng): $g \in [0, 1]$

2. Object State

  • Vị trí vật thể (position): $p_{obj} \in \mathbb{R}^3$
  • Hướng vật thể (orientation): $q_{obj} \in SO(3)$
  • Vận tốc tuyến tính và góc: $v_{obj}, \omega_{obj}$

3. Task-Specific Information

  • Vị trí mục tiêu (goal position): $p_{goal} \in \mathbb{R}^3$
  • Thông tin tiếp xúc (contact forces)
  • Dữ liệu cảm biến xúc giác (tactile sensing)
import numpy as np

def get_manipulation_state(sim):
    """Trích xuất state vector cho manipulation task."""
    # Robot proprioception
    joint_pos = sim.data.qpos[:7]      # 7-DOF arm
    joint_vel = sim.data.qvel[:7]
    gripper_pos = sim.data.qpos[7:9]   # 2-finger gripper

    # End-effector pose
    ee_pos = sim.data.site_xpos[0]     # Vị trí end-effector
    ee_rot = sim.data.site_xmat[0].reshape(3, 3)  # Ma trận xoay

    # Object state
    obj_pos = sim.data.qpos[9:12]      # Vị trí vật thể
    obj_quat = sim.data.qpos[12:16]    # Quaternion vật thể
    obj_vel = sim.data.qvel[9:15]      # 6-DOF velocity

    # Relative information (rất quan trọng!)
    rel_pos = obj_pos - ee_pos         # Vector từ gripper đến vật

    state = np.concatenate([
        joint_pos, joint_vel,           # 14 dims
        gripper_pos,                    # 2 dims
        ee_pos,                        # 3 dims
        obj_pos, obj_quat, obj_vel,    # 13 dims
        rel_pos,                       # 3 dims
    ])
    return state  # Total: 35 dims

Mẹo quan trọng: Luôn bao gồm relative position (khoảng cách từ gripper đến vật thể) trong state. Thông tin tương đối này giúp policy tổng quát hóa tốt hơn so với chỉ dùng vị trí tuyệt đối.

Action Space: Torque vs Position vs Velocity

Việc chọn action space ảnh hưởng rất lớn đến khả năng học và chất lượng chuyển động:

Action Space Ưu điểm Nhược điểm Khi nào dùng
Joint Torque Linh hoạt nhất, kiểm soát lực trực tiếp Khó học, cần nhiều sample, chuyển động giật Nghiên cứu force control
Joint Position Dễ học, chuyển động mượt Không kiểm soát lực trực tiếp Đa số tác vụ manipulation
Joint Velocity Cân bằng giữa torque và position Cần tuning PD gains Pick-and-place, trajectory following
End-effector Delta Trực quan, dễ transfer Cần IK solver, singularity issues Teleoperation-style tasks

Trong thực tế, Joint Position control (hay Delta Position) được dùng phổ biến nhất vì nó cho phép policy tập trung vào "đi đâu" thay vì "dùng bao nhiêu lực":

# Action space: delta joint position
action_space = spaces.Box(
    low=-0.05,  # Giới hạn thay đổi mỗi bước
    high=0.05,
    shape=(8,),  # 7 joints + 1 gripper
    dtype=np.float32
)

def apply_action(sim, action):
    """Áp dụng delta position action."""
    current_pos = sim.data.qpos[:8].copy()
    target_pos = current_pos + action
    # Clip vào joint limits
    target_pos = np.clip(target_pos, joint_lower, joint_upper)
    sim.data.ctrl[:8] = target_pos

Reward Design: Nghệ thuật và Khoa học

Reward design là phần khó nhấtquan trọng nhất khi làm RL cho manipulation. Một reward function tốt có thể giúp robot học trong vài giờ, trong khi reward function tệ khiến robot không bao giờ học được gì — hoặc tệ hơn, học được những hành vi "hack reward" kỳ quái.

Visualization of reward shaping concept

Sparse vs Dense Reward

Sparse Reward — chỉ cho phần thưởng khi hoàn thành task:

def sparse_reward(achieved_goal, desired_goal, threshold=0.05):
    """Reward = 1 nếu đạt goal, 0 nếu không."""
    distance = np.linalg.norm(achieved_goal - desired_goal)
    return 1.0 if distance < threshold else 0.0

Dense Reward — cho phản hồi liên tục dựa trên tiến trình:

def dense_reward(ee_pos, obj_pos, goal_pos, grasped):
    """Reward liên tục dựa trên khoảng cách và trạng thái."""
    # Phase 1: Tiến đến vật thể
    reach_dist = np.linalg.norm(ee_pos - obj_pos)
    reach_reward = 1.0 - np.tanh(5.0 * reach_dist)

    # Phase 2: Nâng vật thể lên goal
    if grasped:
        place_dist = np.linalg.norm(obj_pos - goal_pos)
        place_reward = 1.0 - np.tanh(5.0 * place_dist)
        return reach_reward + 5.0 * place_reward
    
    return reach_reward
Tiêu chí Sparse Reward Dense Reward
Dễ thiết kế Rất dễ Khó, dễ sai
Tốc độ học Rất chậm Nhanh hơn nhiều
Risk of reward hacking Thấp Cao nếu thiết kế sai
Cần HER? Gần như bắt buộc Không nhất thiết
Scalable Tốt Khó scale cho multi-step

Nguyên tắc thiết kế Reward Function

Qua nhiều năm nghiên cứu, cộng đồng RL đã đúc kết một số nguyên tắc vàng:

1. Chia reward thành phases: Mỗi phase tương ứng một giai đoạn của task (tiếp cận → nắm → di chuyển → thả).

2. Dùng distance-based shaping: Hàm tanh hoặc exponential decay giúp gradient ổn định:

# Tốt: gradient ổn định ở mọi khoảng cách
reward = 1.0 - np.tanh(5.0 * distance)

# Kém: gradient quá nhỏ khi xa, quá lớn khi gần
reward = -distance

# Kém: gradient = 0 ở mọi nơi trừ boundary
reward = float(distance < threshold)

3. Penalize undesired behavior: Thêm penalty cho hành động quá lớn, va chạm mạnh, hoặc rơi vật thể:

# Penalty cho action quá lớn (smooth motion)
action_penalty = -0.01 * np.sum(action ** 2)

# Penalty cho rơi vật (nếu đã nắm)
drop_penalty = -10.0 if was_grasped and not is_grasped else 0.0

4. Normalize reward components: Đảm bảo các thành phần reward có scale tương đương để agent không bỏ qua thành phần quan trọng.

Setup Manipulation Environment trong MuJoCo

Tại sao MuJoCo?

MuJoCo (Multi-Joint dynamics with Contact) là simulator hàng đầu cho manipulation RL nhờ:

  • Contact dynamics chính xác — mô phỏng ma sát, va chạm phức tạp
  • Tốc độ cao — hàng nghìn bước mỗi giây
  • Miễn phí (từ 2022, khi DeepMind mua lại)
  • Tích hợp tốt với Gymnasium, Stable-Baselines3

Tạo Custom Manipulation Environment

Dưới đây là code hoàn chỉnh để tạo một environment manipulation cơ bản — robot arm phải nắm một khối lập phương và đưa đến vị trí mục tiêu:

import gymnasium as gym
from gymnasium import spaces
import mujoco
import mujoco.viewer
import numpy as np


# ---- MuJoCo XML model ----
MANIPULATION_XML = """
<mujoco model="simple_manipulation">
  <option timestep="0.002" gravity="0 0 -9.81"/>
  
  <worldbody>
    <!-- Ground plane -->
    <geom type="plane" size="1 1 0.1" rgba="0.9 0.9 0.9 1"/>
    
    <!-- Table -->
    <body name="table" pos="0.5 0 0.4">
      <geom type="box" size="0.4 0.4 0.02" rgba="0.6 0.3 0.1 1"
            mass="100" contype="1" conaffinity="1"/>
    </body>
    
    <!-- Robot Arm (simplified 4-DOF) -->
    <body name="base" pos="0.0 0 0.42">
      <joint name="j0" type="hinge" axis="0 0 1" range="-3.14 3.14"/>
      <geom type="cylinder" size="0.04 0.05" rgba="0.2 0.2 0.8 1"/>
      
      <body name="link1" pos="0 0 0.1">
        <joint name="j1" type="hinge" axis="0 1 0" range="-1.57 1.57"/>
        <geom type="capsule" fromto="0 0 0 0.3 0 0" size="0.03" rgba="0.3 0.3 0.9 1"/>
        
        <body name="link2" pos="0.3 0 0">
          <joint name="j2" type="hinge" axis="0 1 0" range="-2.0 2.0"/>
          <geom type="capsule" fromto="0 0 0 0.25 0 0" size="0.025" rgba="0.4 0.4 1.0 1"/>
          
          <body name="link3" pos="0.25 0 0">
            <joint name="j3" type="hinge" axis="0 1 0" range="-2.0 2.0"/>
            <geom type="capsule" fromto="0 0 0 0.1 0 0" size="0.02" rgba="0.5 0.5 1.0 1"/>
            
            <!-- Simple Gripper -->
            <body name="gripper" pos="0.1 0 0">
              <site name="grip_site" pos="0 0 0" size="0.01"/>
              <body name="finger_left" pos="0 0.02 0">
                <joint name="finger_l" type="slide" axis="0 1 0" range="0 0.04"/>
                <geom type="box" size="0.01 0.005 0.03" rgba="0.8 0.2 0.2 1"
                      contype="1" conaffinity="1" friction="2 0.05 0.01"/>
              </body>
              <body name="finger_right" pos="0 -0.02 0">
                <joint name="finger_r" type="slide" axis="0 -1 0" range="0 0.04"/>
                <geom type="box" size="0.01 0.005 0.03" rgba="0.8 0.2 0.2 1"
                      contype="1" conaffinity="1" friction="2 0.05 0.01"/>
              </body>
            </body>
          </body>
        </body>
      </body>
    </body>
    
    <!-- Object to manipulate -->
    <body name="object" pos="0.5 0 0.45">
      <freejoint name="obj_joint"/>
      <geom type="box" size="0.025 0.025 0.025" rgba="0.1 0.8 0.1 1"
            mass="0.1" contype="1" conaffinity="1" friction="1 0.05 0.01"/>
    </body>
    
    <!-- Goal marker (visual only) -->
    <body name="goal" pos="0.4 0.2 0.55">
      <geom type="sphere" size="0.02" rgba="1 0 0 0.5" contype="0" conaffinity="0"/>
    </body>
  </worldbody>
  
  <actuator>
    <position name="a_j0" joint="j0" kp="100"/>
    <position name="a_j1" joint="j1" kp="100"/>
    <position name="a_j2" joint="j2" kp="100"/>
    <position name="a_j3" joint="j3" kp="100"/>
    <position name="a_fl" joint="finger_l" kp="50"/>
    <position name="a_fr" joint="finger_r" kp="50"/>
  </actuator>
</mujoco>
"""


class SimpleManipulationEnv(gym.Env):
    """Custom MuJoCo manipulation environment.
    
    Task: Robot arm phải nắm khối lập phương xanh
    và đưa đến vị trí goal (hình cầu đỏ).
    """
    metadata = {"render_modes": ["human", "rgb_array"]}
    
    def __init__(self, render_mode=None, reward_type="dense"):
        super().__init__()
        
        self.render_mode = render_mode
        self.reward_type = reward_type
        
        # Load MuJoCo model
        self.model = mujoco.MjModel.from_xml_string(MANIPULATION_XML)
        self.data = mujoco.MjData(self.model)
        
        # Action: 4 joint deltas + 1 gripper command
        self.action_space = spaces.Box(
            low=-1.0, high=1.0, shape=(5,), dtype=np.float32
        )
        
        # Observation: joint pos/vel + ee pos + obj pos/vel + goal + relative
        obs_dim = 4 + 4 + 3 + 3 + 3 + 3 + 3  # = 23
        self.observation_space = spaces.Box(
            low=-np.inf, high=np.inf, shape=(obs_dim,), dtype=np.float64
        )
        
        self.max_steps = 200
        self.current_step = 0
        self._goal_pos = np.array([0.4, 0.2, 0.55])
        
    def _get_obs(self):
        """Xây dựng observation vector."""
        joint_pos = self.data.qpos[:4]
        joint_vel = self.data.qvel[:4]
        ee_pos = self.data.site_xpos[0]  # grip_site position
        obj_pos = self.data.qpos[6:9]     # Object position (freejoint)
        obj_vel = self.data.qvel[6:9]
        rel_obj = obj_pos - ee_pos
        rel_goal = self._goal_pos - obj_pos
        
        return np.concatenate([
            joint_pos, joint_vel, ee_pos,
            obj_pos, obj_vel, rel_obj, rel_goal
        ])
    
    def _compute_reward(self, obs):
        """Tính reward dựa trên trạng thái hiện tại."""
        ee_pos = obs[8:11]
        obj_pos = obs[11:14]
        
        reach_dist = np.linalg.norm(ee_pos - obj_pos)
        goal_dist = np.linalg.norm(obj_pos - self._goal_pos)
        
        if self.reward_type == "sparse":
            return 1.0 if goal_dist < 0.05 else 0.0
        
        # Dense reward: phần thưởng liên tục
        reach_reward = 1.0 - np.tanh(5.0 * reach_dist)
        goal_reward = 1.0 - np.tanh(5.0 * goal_dist)
        
        # Bonus khi object ở gần goal
        success_bonus = 10.0 if goal_dist < 0.05 else 0.0
        
        return reach_reward + 2.0 * goal_reward + success_bonus
    
    def reset(self, seed=None, options=None):
        super().reset(seed=seed)
        mujoco.mj_resetData(self.model, self.data)
        
        # Randomize object position trên bàn
        self.data.qpos[6] = 0.5 + self.np_random.uniform(-0.1, 0.1)
        self.data.qpos[7] = self.np_random.uniform(-0.1, 0.1)
        self.data.qpos[8] = 0.45
        
        # Randomize goal
        self._goal_pos = np.array([
            0.4 + self.np_random.uniform(-0.1, 0.1),
            0.2 + self.np_random.uniform(-0.1, 0.1),
            0.55 + self.np_random.uniform(-0.05, 0.05),
        ])
        
        mujoco.mj_forward(self.model, self.data)
        self.current_step = 0
        
        return self._get_obs(), {}
    
    def step(self, action):
        # Scale actions
        joint_delta = action[:4] * 0.05  # Max 0.05 rad per step
        gripper_cmd = (action[4] + 1) / 2 * 0.04  # [0, 0.04]
        
        # Apply actions
        self.data.ctrl[:4] = self.data.qpos[:4] + joint_delta
        self.data.ctrl[4] = gripper_cmd   # Left finger
        self.data.ctrl[5] = gripper_cmd   # Right finger
        
        # Simulate
        for _ in range(10):  # 10 substeps
            mujoco.mj_step(self.model, self.data)
        
        obs = self._get_obs()
        reward = self._compute_reward(obs)
        
        self.current_step += 1
        terminated = self.current_step >= self.max_steps
        truncated = False
        
        return obs, reward, terminated, truncated, {}

Huấn luyện với Stable-Baselines3

Với environment đã tạo, chúng ta có thể huấn luyện agent bằng PPO hoặc SAC:

from stable_baselines3 import PPO, SAC
from stable_baselines3.common.vec_env import SubprocVecEnv
from stable_baselines3.common.callbacks import EvalCallback

def make_env(rank, seed=0):
    def _init():
        env = SimpleManipulationEnv(reward_type="dense")
        env.reset(seed=seed + rank)
        return env
    return _init

# Tạo parallel environments
num_envs = 8
env = SubprocVecEnv([make_env(i) for i in range(num_envs)])

# Train PPO
model = PPO(
    "MlpPolicy",
    env,
    learning_rate=3e-4,
    n_steps=2048,
    batch_size=256,
    n_epochs=10,
    gamma=0.99,
    gae_lambda=0.95,
    clip_range=0.2,
    ent_coef=0.01,
    verbose=1,
    tensorboard_log="./logs/"
)

# Callback để đánh giá
eval_env = SimpleManipulationEnv(reward_type="dense")
eval_callback = EvalCallback(
    eval_env, 
    best_model_save_path="./best_model/",
    eval_freq=10000,
    n_eval_episodes=20,
    deterministic=True
)

# Bắt đầu huấn luyện
model.learn(total_timesteps=1_000_000, callback=eval_callback)
model.save("manipulation_ppo")

So sánh Sparse vs Dense Reward trong thực nghiệm

Sau khi huấn luyện 1 triệu timesteps, đây là kết quả điển hình:

Metric Sparse Reward (PPO) Dense Reward (PPO) Dense Reward (SAC)
Success Rate ~5% ~65% ~78%
Avg Return 0.12 145.3 187.6
Convergence Không converge ~400K steps ~300K steps
Behavior quality Random Hợp lý Mượt mà

Sparse reward gần như không thể học được với PPO thuần túy — bạn sẽ cần Hindsight Experience Replay (HER) để giải quyết, điều chúng ta sẽ thảo luận chi tiết trong bài 4 về Pick-and-Place.

Biểu đồ training curves

Gymnasium Robotics: Environments có sẵn

Thay vì tự tạo environment từ đầu, bạn có thể dùng Gymnasium Robotics — bộ sưu tập environments manipulation chuẩn:

import gymnasium as gym
import gymnasium_robotics

# Fetch environments (7-DOF arm + gripper)
env = gym.make("FetchReach-v3")        # Đưa gripper đến mục tiêu
env = gym.make("FetchPush-v3")         # Đẩy vật đến vị trí
env = gym.make("FetchSlide-v3")        # Trượt vật trên mặt bàn
env = gym.make("FetchPickAndPlace-v3") # Nhặt và đặt

# Shadow Hand environments (24-DOF)
env = gym.make("HandReach-v2")         # Đưa ngón tay đến vị trí
env = gym.make("HandManipulateBlock-v2")  # Xoay khối

Mỗi environment cung cấp cả observation (state đầy đủ) và desired_goal / achieved_goal — cấu trúc lý tưởng cho HER.

Tài liệu tham khảo

  1. Gymnasium Robotics: A Standard Interface for Reinforcement Learning in Robot Manipulation — Towers et al., 2024
  2. MuJoCo: A physics engine for model-based control — Todorov et al., 2012
  3. A Survey on Robot Manipulation — Cui et al., 2023

Tiếp theo trong Series

Bài tiếp theo — Grasping với RL: Stable Grasp và Object Variety — chúng ta sẽ áp dụng nền tảng đã học để huấn luyện robot nắm đồ vật với nhiều hình dạng và kích thước khác nhau. Chúng ta sẽ so sánh PPO vs SAC và thiết kế curriculum learning cho grasping.

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 GigaBrain-0: VLA + World Model + RL
vlaworld-modelreinforcement-learninggigabrainroboticsmanipulation

Hướng dẫn GigaBrain-0: VLA + World Model + RL

Hướng dẫn chi tiết huấn luyện VLA bằng World Model và Reinforcement Learning với framework RAMP từ GigaBrain — open-source, 3.5B params.

12/4/202611 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
Sim-to-Real cho Humanoid: Deployment Best Practices
sim2realhumanoiddeploymentrlunitreePhần 10

Sim-to-Real cho Humanoid: Deployment Best Practices

Pipeline hoàn chỉnh deploy RL locomotion policy lên robot humanoid thật — domain randomization, system identification, safety, và Unitree SDK.

9/4/202611 phút đọc