← Quay lại Blog
simulationsimulationsim2realrl

Domain Randomization: Chìa khóa Sim-to-Real Transfer

Lý thuyết và thực hành domain randomization — visual, dynamics, sensor randomization với case studies thành công.

Nguyễn Anh Tuấn26 tháng 3, 202610 phút đọc
Domain Randomization: Chìa khóa Sim-to-Real Transfer

Domain Randomization là gì và tại sao nó hoạt động?

Domain Randomization (DR) là kỹ thuật training AI policy trên nhiều variations của simulation thay vì một cấu hình duy nhất. Ý tưởng cốt lõi rất đơn giản: nếu model đã thấy đủ nhiều biến thể trong sim, thì thực tế chỉ là thêm một biến thể nữa mà thôi.

Thay vì cố gắng làm simulation giống thật 100% (điều gần như bất khả thi), DR chấp nhận rằng sim sẽ không bao giờ perfect — nhưng nếu train trên distribution rộng đủ, policy sẽ tự học cách generalize.

Paper gốc của Tobin et al. (2017) — Domain Randomization for Transferring Deep Neural Networks from Simulation to the Real World — đã chứng minh rằng với đủ randomization, một object detector train hoàn toàn trong sim có thể detect objects trong thế giới thật với độ chính xác cao.

Tại sao DR hoạt động? — Góc nhìn Bayesian

Từ góc nhìn Bayesian, DR tương đương với việc train trên posterior distribution của environment parameters:

P(policy works | real world) = integral P(policy works | env_params) * P(env_params) d(env_params)

Khi randomization range đủ rộng, P(env_params) cover được real-world params, nên policy tự nhiên robust. Thực tế chỉ là một sample từ distribution mà policy đã được train trên.

Domain randomization visualization cho robot training

3 loại Domain Randomization

1. Visual Domain Randomization

Visual DR randomize mọi thứ liên quan đến những gì camera nhìn thấy. Đây là loại DR quan trọng nhất cho vision-based policies.

Các parameters cần randomize:

Parameter Range tham khảo Lý do
Light intensity 200-5000 lux Real-world lighting thay đổi liên tục
Light color 3000-8000K Từ tungsten đến daylight
Light position Toàn bộ workspace Shadow patterns thay đổi
Camera FOV 55-75 degrees Lens variations
Camera noise sigma 0.01-0.05 Sensor noise
Texture colors Full RGB range Surface appearance thay đổi
Background Random images Distractor background
Distractor objects 0-10 objects Clutter trong real environment

Implementation trong Isaac Lab dùng YAML config:

# config/visual_randomization.yaml
# Isaac Lab visual domain randomization config

visual_randomization:
  lighting:
    intensity:
      distribution: "uniform"
      range: [300.0, 3000.0]
    color_temperature:
      distribution: "uniform"
      range: [3000.0, 7000.0]
    num_random_lights:
      distribution: "uniform_int"
      range: [1, 5]
    position:
      distribution: "uniform"
      range: [[-2.0, -2.0, 1.0], [2.0, 2.0, 3.0]]

  camera:
    fov:
      distribution: "uniform"
      range: [55.0, 75.0]
    position_noise:
      distribution: "gaussian"
      mean: 0.0
      std: 0.02
    rotation_noise_deg:
      distribution: "gaussian"
      mean: 0.0
      std: 3.0

  textures:
    table_color:
      distribution: "uniform"
      range: [[0.2, 0.2, 0.2], [0.9, 0.9, 0.9]]
    object_color:
      distribution: "uniform"
      range: [[0.1, 0.1, 0.1], [1.0, 1.0, 1.0]]
    randomize_background: true

  distractors:
    num_objects:
      distribution: "uniform_int"
      range: [0, 5]
    scale:
      distribution: "uniform"
      range: [0.02, 0.1]

2. Dynamics Domain Randomization

Dynamics DR randomize physics parameters — đây là loại quan trọng nhất cho control policies vì trực tiếp ảnh hưởng đến robot behavior.

"""
Dynamics Domain Randomization implementation.
Compatible với Isaac Lab ManagerBasedRLEnv.
"""
import numpy as np
from dataclasses import dataclass, field


@dataclass
class DynamicsRandomizationConfig:
    """Dynamics DR config với các range đã được validate."""

    # Object properties
    mass_scale_range: tuple = (0.5, 2.0)
    friction_range: tuple = (0.3, 1.5)
    restitution_range: tuple = (0.0, 0.5)

    # Robot joint properties
    joint_damping_scale: tuple = (0.8, 1.2)
    joint_friction_scale: tuple = (0.5, 2.0)
    actuator_strength_scale: tuple = (0.85, 1.15)

    # Control latency và noise
    action_delay_frames: tuple = (0, 3)
    action_noise_std: float = 0.02
    observation_noise_std: float = 0.01

    # Environment
    gravity_noise: float = 0.05  # m/s^2 variation quanh 9.81


@dataclass
class RandomizedParams:
    """Container cho các params đã randomize."""

    mass_scale: float = 1.0
    friction: float = 0.8
    restitution: float = 0.1
    damping_scale: float = 1.0
    joint_friction_scale: float = 1.0
    actuator_scale: float = 1.0
    action_delay: int = 0
    gravity: np.ndarray = field(
        default_factory=lambda: np.array([0.0, 0.0, -9.81])
    )


def sample_dynamics_params(
    config: DynamicsRandomizationConfig,
    rng: np.random.Generator | None = None,
) -> RandomizedParams:
    """Sample một bộ parameters từ DR config."""
    if rng is None:
        rng = np.random.default_rng()

    gravity_z = -9.81 + rng.normal(0, config.gravity_noise)

    return RandomizedParams(
        mass_scale=rng.uniform(*config.mass_scale_range),
        friction=rng.uniform(*config.friction_range),
        restitution=rng.uniform(*config.restitution_range),
        damping_scale=rng.uniform(*config.joint_damping_scale),
        joint_friction_scale=rng.uniform(*config.joint_friction_scale),
        actuator_scale=rng.uniform(*config.actuator_strength_scale),
        action_delay=rng.integers(*config.action_delay_frames),
        gravity=np.array([0.0, 0.0, gravity_z]),
    )


def apply_dynamics_params(env, params: RandomizedParams):
    """Apply randomized params vào environment."""
    # Object physics
    env.scene.target_object.set_mass(
        env.scene.target_object.default_mass * params.mass_scale
    )
    env.scene.target_object.set_friction(params.friction)
    env.scene.target_object.set_restitution(params.restitution)

    # Robot joints
    for joint in env.robot.joints:
        joint.set_damping(joint.default_damping * params.damping_scale)
        joint.set_friction(
            joint.default_friction * params.joint_friction_scale
        )
        joint.set_max_force(
            joint.default_max_force * params.actuator_scale
        )

    # Gravity
    env.sim.set_gravity(params.gravity)

    # Action delay buffer
    env.action_delay = params.action_delay

    return env

3. Sensor Domain Randomization

Sensor DR mô phỏng imperfections của sensor thật — noise, delay, dropout, bias. Đây là loại thường bị bỏ qua nhưng rất quan trọng cho sim-to-real transfer.

"""
Sensor Domain Randomization.
Wrap observation pipeline để thêm realistic noise.
"""
import numpy as np
from dataclasses import dataclass


@dataclass
class SensorRandomizationConfig:
    """Config cho sensor noise và imperfections."""

    # Joint encoder noise
    joint_position_noise_std: float = 0.005    # radians
    joint_velocity_noise_std: float = 0.05     # rad/s
    joint_position_bias_range: tuple = (-0.01, 0.01)

    # IMU noise (dựa trên datasheet MPU-6050)
    imu_accel_noise_std: float = 0.1           # m/s^2
    imu_gyro_noise_std: float = 0.01           # rad/s
    imu_bias_instability: float = 0.001        # random walk

    # Force/torque sensor
    ft_noise_std: float = 0.5                  # Newton
    ft_dropout_prob: float = 0.01              # 1% chance mất signal

    # Communication delay
    observation_delay_frames: tuple = (0, 2)


class SensorRandomizer:
    """Apply realistic sensor noise vào observations."""

    def __init__(
        self,
        config: SensorRandomizationConfig,
        seed: int = 42,
    ):
        self.config = config
        self.rng = np.random.default_rng(seed)
        self.joint_bias = None
        self.imu_bias = None
        self._reset_biases()

    def _reset_biases(self):
        """Reset systematic biases mỗi episode."""
        self.joint_bias = self.rng.uniform(
            *self.config.joint_position_bias_range, size=7
        )
        self.imu_bias = self.rng.normal(0, 0.01, size=6)

    def add_joint_noise(
        self, joint_pos: np.ndarray, joint_vel: np.ndarray
    ) -> tuple[np.ndarray, np.ndarray]:
        """Thêm noise vào joint encoder readings."""
        noisy_pos = (
            joint_pos
            + self.rng.normal(
                0, self.config.joint_position_noise_std,
                size=joint_pos.shape,
            )
            + self.joint_bias[: len(joint_pos)]
        )
        noisy_vel = joint_vel + self.rng.normal(
            0, self.config.joint_velocity_noise_std,
            size=joint_vel.shape,
        )
        return noisy_pos, noisy_vel

    def add_imu_noise(
        self, accel: np.ndarray, gyro: np.ndarray
    ) -> tuple[np.ndarray, np.ndarray]:
        """Thêm noise vào IMU readings kèm bias drift."""
        noisy_accel = accel + self.rng.normal(
            0, self.config.imu_accel_noise_std, size=3
        )
        noisy_gyro = gyro + self.rng.normal(
            0, self.config.imu_gyro_noise_std, size=3
        )
        # Bias instability (random walk)
        self.imu_bias[:3] += self.rng.normal(
            0, self.config.imu_bias_instability, size=3
        )
        self.imu_bias[3:] += self.rng.normal(
            0, self.config.imu_bias_instability, size=3
        )
        return noisy_accel + self.imu_bias[:3], noisy_gyro + self.imu_bias[3:]

    def add_ft_noise(self, force_torque: np.ndarray) -> np.ndarray:
        """Thêm noise và dropout vào force/torque sensor."""
        noisy_ft = force_torque + self.rng.normal(
            0, self.config.ft_noise_std, size=force_torque.shape
        )
        if self.rng.random() < self.config.ft_dropout_prob:
            return np.zeros_like(force_torque)
        return noisy_ft

    def randomize_observation(self, obs: dict) -> dict:
        """Apply tất cả sensor noise vào observation dict."""
        noisy_obs = {}

        if "joint_pos" in obs and "joint_vel" in obs:
            noisy_obs["joint_pos"], noisy_obs["joint_vel"] = (
                self.add_joint_noise(obs["joint_pos"], obs["joint_vel"])
            )

        if "imu_accel" in obs and "imu_gyro" in obs:
            noisy_obs["imu_accel"], noisy_obs["imu_gyro"] = (
                self.add_imu_noise(obs["imu_accel"], obs["imu_gyro"])
            )

        if "force_torque" in obs:
            noisy_obs["force_torque"] = self.add_ft_noise(
                obs["force_torque"]
            )

        # Copy qua các keys không randomize
        for key in obs:
            if key not in noisy_obs:
                noisy_obs[key] = obs[key]

        return noisy_obs

Sensor noise visualization trong robotics simulation

Case Studies thành công

OpenAI Rubik's Cube (2019)

Paper: Solving Rubik's Cube with a Robot Hand

Đây là demo ấn tượng nhất của domain randomization. OpenAI train Shadow Dexterous Hand hoàn toàn trong simulation với Automatic Domain Randomization (ADR) — kỹ thuật tự động mở rộng randomization range khi policy đã master range hiện tại.

Kết quả:

ADR workflow:

Bước 1: Bắt đầu với range hẹp — friction in [0.9, 1.1]
Bước 2: Train policy đến khi success rate > 80%
Bước 3: Mở rộng range — friction in [0.7, 1.3]
Bước 4: Lặp lại cho đến khi range rộng tối đa
Kết quả: friction in [0.1, 2.0] — policy robust đến mức real world là "easy mode"

Agility Digit Humanoid Walking (2024)

Paper: Real-World Humanoid Locomotion with Reinforcement Learning

Agility Robotics train locomotion policy cho Digit humanoid robot:

ANYmal Parkour (2023)

Paper: Extreme Parkour with Legged Robots

ETH Zurich train ANYmal quadruped vượt địa hình cực khó:

Metrics và đánh giá

Metric Mô tả Target
Sim success rate (no DR) Baseline performance >95%
Sim success rate (full DR) Robust performance >85%
Sim-to-real success rate Transfer hiệu quả >80%
Sim-to-real gap Chênh lệch sim vs real <15%
Adaptation time Thời gian adapt trên real <10 episodes

Anti-patterns: Những lỗi thường gặp

1. Over-randomization

Randomize quá rộng sẽ làm policy không học được gì. Ví dụ randomize friction từ 0.01 đến 10.0 — range này vô lý vì real-world friction chỉ nằm trong 0.2-1.5.

Giải pháp: Dùng System Identification để xác định nominal values, rồi randomize +-30-50% quanh đó.

2. Correlated Parameters

Trong thực tế, nhiều parameters có correlation. Ví dụ: object nặng thường có friction cao hơn (kim loại vs nhựa). Randomize độc lập sẽ tạo ra các combinations vô lý.

# SAI: randomize độc lập
mass = np.random.uniform(0.1, 5.0)
friction = np.random.uniform(0.1, 2.0)

# ĐÚNG: randomize có correlation theo material
material = np.random.choice(["plastic", "metal", "rubber", "wood"])
material_params = {
    "plastic": {"mass_range": (0.05, 0.5), "friction_range": (0.3, 0.6)},
    "metal":   {"mass_range": (0.5, 5.0),  "friction_range": (0.2, 0.5)},
    "rubber":  {"mass_range": (0.1, 1.0),  "friction_range": (0.8, 1.5)},
    "wood":    {"mass_range": (0.1, 2.0),  "friction_range": (0.4, 0.8)},
}
params = material_params[material]
mass = np.random.uniform(*params["mass_range"])
friction = np.random.uniform(*params["friction_range"])

3. Không có Curriculum

Ném policy vào full randomization ngay từ đầu thường dẫn đến policy không converge. Luôn bắt đầu với range hẹp, tăng dần theo training progress.

4. Chỉ randomize Visual, bỏ qua Dynamics

Nhiều người chỉ focus vào visual DR vì dễ implement hơn. Nhưng đối với control tasks, dynamics DR quan trọng hơn nhiều. Một policy thất bại vì friction sai 20% phổ biến hơn thất bại vì lighting khác.

Robot arm trong manufacturing environment

Tham khảo thêm

Các papers quan trọng về domain randomization:

  1. Tobin et al., 2017 — Domain Randomization for Sim-to-Real Transfer — Paper gốc
  2. OpenAI, 2019 — Solving Rubik's Cube (ADR) — Automatic Domain Randomization
  3. Mehta et al., 2020 — Active Domain Randomization — Chọn DR params thông minh hơn
  4. Muratore et al., 2022 — Robot Learning from Randomized Simulations — Survey toàn diện

Bài viết liên quan

Bài viết liên quan

Deep DiveDigital Twins và ROS 2: Simulation trong sản xuất
simulationros2digital-twinPhần 6

Digital Twins và ROS 2: Simulation trong sản xuất

Ứng dụng simulation trong công nghiệp — digital twins, ROS 2 + Gazebo/Isaac integration cho nhà máy thông minh.

3/4/202611 phút đọc
TutorialSim-to-Real Pipeline: Từ training đến robot thật
simulationsim2realtutorialPhần 5

Sim-to-Real Pipeline: Từ training đến robot thật

End-to-end guide: train policy trong sim, evaluate, domain randomization, deploy lên robot thật và iterate.

2/4/202615 phút đọc
TutorialNVIDIA Isaac Lab: GPU-accelerated RL training từ zero
simulationisaac-simrlPhần 3

NVIDIA Isaac Lab: GPU-accelerated RL training từ zero

Setup Isaac Lab, train locomotion policy với 4096 parallel environments và domain randomization trên GPU.

1/4/202611 phút đọc