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.
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
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ả:
- Hàng trăm parameters được randomize đồng thời: friction, mass, damping, lighting, camera, textures
- Policy tự develop emergent adaptation — có thể adapt real-time khi gặp dynamics mới
- Success rate sim-to-real: ~84% cho finger repositioning — kết quả unprecedented cho dexterous manipulation
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:
- Train trong Isaac Gym với 4096 parallel environments
- DR trên: terrain (flat, rough, slopes), friction (0.3-1.5), mass distribution (+-20%), motor strength (+-15%)
- Dùng teacher-student distillation: teacher có privileged terrain info, student chỉ dùng proprioception
- Success rate sim-to-real: ~91% trên diverse real-world terrains
ANYmal Parkour (2023)
Paper: Extreme Parkour with Legged Robots
ETH Zurich train ANYmal quadruped vượt địa hình cực khó:
- DR trên terrain geometry, friction (0.2-1.5), external perturbations (random pushes)
- Curriculum từ flat ground đến parkour obstacles
- Success rate sim-to-real: ~93% cho basic locomotion, ~87% cho parkour tasks
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.
Tham khảo thêm
Các papers quan trọng về domain randomization:
- Tobin et al., 2017 — Domain Randomization for Sim-to-Real Transfer — Paper gốc
- OpenAI, 2019 — Solving Rubik's Cube (ADR) — Automatic Domain Randomization
- Mehta et al., 2020 — Active Domain Randomization — Chọn DR params thông minh hơn
- Muratore et al., 2022 — Robot Learning from Randomized Simulations — Survey toàn diện
Bài viết liên quan
- Sim-to-Real Transfer: Train simulation, chạy thực tế — Tổng quan về sim-to-real transfer và các kỹ thuật chính
- Sim-to-Real Pipeline: Từ training đến robot thật — End-to-end guide deploy policy lên robot thật
- Digital Twins và ROS 2: Simulation trong sản xuất — Ứng dụng simulation trong công nghiệp
- RL cho Robot đi 2 chân: Từ MuJoCo đến thực tế — Case study RL locomotion