Domain Randomization for Robust Robot Learning
Domain randomization is one of the most effective and widely used techniques for training robot policies in simulation that actually work on real hardware. Instead of trying to make simulation match reality as precisely as possible, domain randomization deliberately varies simulation parameters across a wide range during training, so the policy learns to be robust to variation rather than overfit to one specific (and inevitably slightly wrong) simulated world. This tutorial covers what to randomize, how much, and how to structure training around it.
The core idea
If a policy is trained across thousands of simulated variations — different friction coefficients, different lighting, different object textures, different camera positions — the real world simply becomes one more sample from that broad training distribution, rather than an unfamiliar edge case the policy has never encountered. The policy learns to rely on features and strategies that are robust across the whole distribution, rather than any single simulation's specific quirks.
What to randomize: dynamics parameters
For contact-rich and physically demanding tasks, randomizing the physics parameters that most affect real-world dynamics mismatch:
- Friction coefficients (ground, object surfaces, gripper pads)
- Mass and inertia of manipulated objects and robot links
- Joint damping and friction
- Actuator strength/gain and torque limits
- Contact stiffness/softness parameters
import numpy as np
def randomize_dynamics(model, rng):
for i in range(model.nbody):
model.body_mass[i] *= rng.uniform(0.8, 1.2)
for i in range(model.njnt):
model.dof_damping[i] *= rng.uniform(0.7, 1.3)
for i in range(model.ngeom):
model.geom_friction[i] *= rng.uniform(0.6, 1.4)
A practical starting point is randomizing by a percentage range (e.g., ±20–30%) around your best system-identified estimate, rather than guessing an absolute range — this keeps the distribution centered on plausible reality instead of centered on an arbitrary default.
What to randomize: visual parameters
For vision-based policies, visual domain randomization is often the highest-leverage category, since visual mismatch tends to dominate the sim-to-real gap for camera-driven tasks:
- Lighting — position, intensity, color temperature, number of light sources.
- Textures — randomize materials on the ground, background, table, and even the robot itself, sampling from large texture datasets rather than a handful of hand-picked options.
- Camera parameters — position, orientation, field of view, and even slight lens distortion, within a range consistent with real mounting tolerances.
- Distractor objects — randomly placed irrelevant objects in the scene so the policy learns to attend to task-relevant objects specifically, rather than memorizing "the third object in the scene."
- Color/appearance of the target object — if applicable, so the policy generalizes to object appearance variation rather than one hardcoded color/texture.
def randomize_visuals(scene, rng):
scene.light_intensity = rng.uniform(300, 1500)
scene.light_position = rng.uniform([-2, -2, 2], [2, 2, 4])
scene.ground_texture = rng.choice(TEXTURE_LIBRARY)
scene.camera_position += rng.uniform(-0.02, 0.02, size=3)
scene.camera_fov = rng.uniform(55, 65)
What to randomize: control and sensing
Beyond physics and visuals, randomizing aspects of the control loop itself helps close the latency and noise gap:
- Observation noise — add Gaussian noise to joint position/velocity readings and camera images to mimic real sensor imperfection.
- Action latency — randomly delay applied actions by a few control steps to simulate real communication and processing latency.
- Control frequency jitter — occasionally skip or double a control step to mimic the timing irregularities of a real control loop under load.
def add_sensor_noise(obs, rng, noise_std=0.01):
return obs + rng.normal(0, noise_std, size=obs.shape)
How much randomization is too much
There's a real tradeoff here: too little randomization and the policy remains brittle to the exact gap sources it wasn't exposed to; too much randomization and the task becomes so variable that the policy struggles to learn anything useful, or learns an overly conservative, low-performance strategy just to survive the worst-case samples.
Practical guidance:
- Start narrow, then widen. Begin with a modest randomization range validated against your system-identified real-world parameters, confirm the policy still learns the task well in simulation, then progressively widen the range while monitoring simulated task performance.
- Randomize what plausibly varies in the real deployment, not everything imaginable. If your robot always operates under consistent lighting, heavy lighting randomization mostly wastes training capacity; if it operates in varied environments, it's essential.
- Track both simulated success rate and (periodically) real-world success rate as you widen randomization ranges — the goal is the real-world number going up, not the simulated one going down as little as possible.
Automatic domain randomization (ADR)
A more advanced approach automatically adjusts randomization ranges during training based on policy performance, rather than using fixed ranges chosen up front:
- Start with a narrow randomization range.
- Periodically evaluate the policy at the edges of the current range.
- If performance at the edges is still good, widen the range slightly; if performance degrades, hold or narrow it.
This produces a curriculum that automatically scales randomization difficulty to match the policy's current capability, rather than requiring the practitioner to guess the right fixed range in advance — but it adds meaningful implementation complexity and is usually only worth it once you've validated the basic fixed-range approach works for your task.
Structuring the training loop
A minimal per-episode randomization hook, integrated into an RL or imitation-learning-with-simulated-rollout training loop:
def reset_randomized_episode(env, rng, randomization_config):
obs = env.reset()
randomize_dynamics(env.model, rng)
randomize_visuals(env.scene, rng)
return obs
for episode in range(num_episodes):
obs = reset_randomized_episode(env, rng, randomization_config)
done = False
while not done:
action = policy.act(obs)
action = maybe_delay_action(action) # simulate control latency
obs, reward, done, info = env.step(action)
obs = add_sensor_noise(obs, rng)
Randomizing every episode (not just occasionally) is important — if only a fraction of training episodes are randomized, the policy can still find shortcuts that work for the common, non-randomized case and fail exactly where robustness matters most.
Measuring whether it's working
The real test of domain randomization isn't simulated performance — it's whether real-world performance improves, and ideally with less real-world fine-tuning data needed than a policy trained without randomization. Track a fixed real-world evaluation suite (as discussed in the sim-to-real tutorial) before and after adding or widening randomization, and treat any randomization change that doesn't measurably help real-world performance as a hyperparameter not worth keeping.
From randomized simulation to structured datasets
Domain randomization is also the foundation of another major use case: generating large-scale synthetic training datasets with rich, automatically-labeled ground truth — which is exactly what we build in the final tutorial of this series.
Useful Links
Website: www.v-modal.com
SDK Flutter: v-modal/vmodal_sdk_flutter
SDK Android: v-modal/vmodal_sdk_android
Discord: https://discord.gg/K72z28KUx
Top comments (0)