Learn AI Series (#141) - Robotics and Embodied AI
What will I learn
- The sim-to-real gap: why a policy that is flawless in simulation faceplants in reality, and how domain randomization quietly turns that into a problem we already know how to solve;
- the three robot-learning paradigms -- imitation, reinforcement learning, and the hybrid that real systems actually ship -- and where each one breaks;
- language-conditioned robotics: how a plain-English instruction becomes motor torque through a vision-language-action model, reusing the exact perception backbones from earlier in this series;
- foundation models for robots, the brutal data bottleneck they run into, and the tricks (cross-embodiment, massive sim, teleop) people use to climb out of it;
- the hardware reality -- sensors, actuators, on-board compute, safety -- that software people love to wave away and physics refuses to let them;
- a small, runnable robot-learning pipeline in PyTorch that you can actually poke at.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- Python 3.10+ with PyTorch installed (
pip install torch) -- every snippet here is illustrative, nothing needs a GPU, a physics engine, or (thankfully) an actual robot to break; - You've been through the reinforcement learning arc (#102-116) and foundation models (#137). We lean on both, plus callbacks to attention (#51), CLIP-style multimodal perception (#75, #138), and edge AI (#122).
Difficulty
- Beginner
Curriculum (of the Learn AI Series):
- Learn AI Series (#1) - What Machine Learning Actually Is
- Learn AI Series (#2) - Setting Up Your AI Workbench - Python and NumPy
- Learn AI Series (#3) - Your Data Is Just Numbers - How Machines See the World
- Learn AI Series (#4) - Your First Prediction - No Math, Just Intuition
- Learn AI Series (#5) - Patterns in Data - What "Learning" Actually Looks Like
- Learn AI Series (#6) - From Intuition to Math - Why We Need Formulas
- Learn AI Series (#7) - The Training Loop - See It Work Step by Step
- Learn AI Series (#8) - The Math You Actually Need (Part 1) - Linear Algebra
- Learn AI Series (#9) - The Math You Actually Need (Part 2) - Calculus and Probability
- Learn AI Series (#10) - Your First ML Model - Linear Regression From Scratch
- Learn AI Series (#11) - Making Linear Regression Real
- Learn AI Series (#12) - Classification - Logistic Regression From Scratch
- Learn AI Series (#13) - Evaluation - How to Know If Your Model Actually Works
- Learn AI Series (#14) - Data Preparation - The 80% Nobody Talks About
- Learn AI Series (#15) - Feature Engineering and Selection
- Learn AI Series (#16) - Scikit-Learn - The Standard Library of ML
- Learn AI Series (#17) - Decision Trees - How Machines Make Decisions
- Learn AI Series (#18) - Random Forests - Wisdom of Crowds
- Learn AI Series (#19) - Gradient Boosting - The Kaggle Champion
- Learn AI Series (#20) - Support Vector Machines - Drawing the Perfect Boundary
- Learn AI Series (#21) - Mini Project - Predicting Crypto Market Regimes
- Learn AI Series (#22) - K-Means Clustering - Finding Groups
- Learn AI Series (#23) - Advanced Clustering - Beyond K-Means
- Learn AI Series (#24) - Dimensionality Reduction - PCA
- Learn AI Series (#25) - Advanced Dimensionality Reduction - t-SNE and UMAP
- Learn AI Series (#26) - Anomaly Detection - Finding What Doesn't Belong
- Learn AI Series (#27) - Recommendation Systems - "Users Like You Also Liked..."
- Learn AI Series (#28) - Time Series Fundamentals - When Order Matters
- Learn AI Series (#29) - Time Series Forecasting - Predicting What Comes Next
- Learn AI Series (#30) - Natural Language Processing - Text as Data
- Learn AI Series (#31) - Word Embeddings - Meaning in Numbers
- Learn AI Series (#32) - Bayesian Methods - Thinking in Probabilities
- Learn AI Series (#33) - Ensemble Methods Deep Dive - Stacking and Blending
- Learn AI Series (#34) - ML Engineering - From Notebook to Production
- Learn AI Series (#35) - Data Ethics and Bias in ML
- Learn AI Series (#36) - Mini Project - Complete ML Pipeline
- Learn AI Series (#37) - The Perceptron - Where It All Started
- Learn AI Series (#38) - Neural Networks From Scratch - Forward Pass
- Learn AI Series (#39) - Neural Networks From Scratch - Backpropagation
- Learn AI Series (#40) - Training Neural Networks - Practical Challenges
- Learn AI Series (#41) - Optimization Algorithms - SGD, Momentum, Adam
- Learn AI Series (#42) - PyTorch Fundamentals - Tensors and Autograd
- Learn AI Series (#43) - PyTorch Data and Training
- Learn AI Series (#44) - PyTorch nn.Module - Building Real Networks
- Learn AI Series (#45) - Convolutional Neural Networks - Theory
- Learn AI Series (#46) - CNNs in Practice - Classic to Modern Architectures
- Learn AI Series (#47) - CNN Applications - Detection, Segmentation, Style Transfer
- Learn AI Series (#48) - Recurrent Neural Networks - Sequences
- Learn AI Series (#49) - LSTM and GRU - Solving the Memory Problem
- Learn AI Series (#50) - Sequence-to-Sequence Models
- Learn AI Series (#51) - Attention Mechanisms
- Learn AI Series (#52) - The Transformer Architecture (Part 1)
- Learn AI Series (#53) - The Transformer Architecture (Part 2)
- Learn AI Series (#54) - Vision Transformers
- Learn AI Series (#55) - Generative Adversarial Networks
- Learn AI Series (#56) - Mini Project - Building a Transformer From Scratch
- Learn AI Series (#57) - Language Modeling - Predicting the Next Word
- Learn AI Series (#58) - GPT Architecture - Decoder-Only Transformers
- Learn AI Series (#59) - BERT and Encoder Models
- Learn AI Series (#60) - Training Large Language Models
- Learn AI Series (#61) - Instruction Tuning and Alignment
- Learn AI Series (#62) - Prompt Engineering - Getting the Most from LLMs
- Learn AI Series (#63) - Embeddings and Vector Search
- Learn AI Series (#64) - Retrieval-Augmented Generation (RAG) - Basics
- Learn AI Series (#65) - RAG - Advanced Techniques
- Learn AI Series (#66) - Working with LLM APIs
- Learn AI Series (#67) - Building AI Agents (Part 1) - Foundations
- Learn AI Series (#68) - Building AI Agents (Part 2) - Advanced Patterns
- Learn AI Series (#69) - Fine-Tuning Language Models
- Learn AI Series (#70) - Running Local Models
- Learn AI Series (#71) - Text Generation Techniques
- Learn AI Series (#72) - Tokenization Deep Dive
- Learn AI Series (#73) - LLM Evaluation
- Learn AI Series (#74) - The Hugging Face Ecosystem
- Learn AI Series (#75) - Multimodal Models - Text Meets Vision
- Learn AI Series (#76) - Mini Project - Your Own AI Assistant
- Learn AI Series (#77) - Image Processing Fundamentals
- Learn AI Series (#78) - Object Detection (Part 1) - Foundations
- Learn AI Series (#79) - Object Detection (Part 2) - Modern Approaches
- Learn AI Series (#80) - Image Segmentation
- Learn AI Series (#81) - Pose Estimation and Tracking
- Learn AI Series (#82) - Optical Character Recognition
- Learn AI Series (#83) - Video Understanding
- Learn AI Series (#84) - Generative Images - Diffusion Models (Part 1)
- Learn AI Series (#85) - Generative Images - Diffusion Models (Part 2)
- Learn AI Series (#86) - Image-to-Image and Editing
- Learn AI Series (#87) - 3D Vision
- Learn AI Series (#88) - Face Analysis
- Learn AI Series (#89) - Medical and Scientific Imaging
- Learn AI Series (#90) - Self-Supervised Learning for Vision
- Learn AI Series (#91) - Mini Project - Building a Visual AI System
- Learn AI Series (#92) - Audio Fundamentals for AI
- Learn AI Series (#93) - Speech Recognition
- Learn AI Series (#94) - Text-to-Speech (TTS)
- Learn AI Series (#95) - Audio Classification
- Learn AI Series (#96) - Music Generation
- Learn AI Series (#97) - Speaker Recognition and Diarization
- Learn AI Series (#98) - Natural Language Understanding for Voice
- Learn AI Series (#99) - Audio Enhancement
- Learn AI Series (#100) - Multimodal Audio-Visual Models
- Learn AI Series (#101) - Mini Project: Voice-Controlled AI Assistant
- Learn AI Series (#102) - What Is Reinforcement Learning?
- Learn AI Series (#103) - Multi-Armed Bandits
- Learn AI Series (#104) - Dynamic Programming
- Learn AI Series (#105) - Monte Carlo Methods
- Learn AI Series (#106) - Temporal Difference Learning
- Learn AI Series (#107) - Deep Q-Networks (DQN)
- Learn AI Series (#108) - Policy Gradient Methods
- Learn AI Series (#109) - Advanced Policy Optimization
- Learn AI Series (#110) - Model-Based Reinforcement Learning
- Learn AI Series (#111) - Multi-Agent Reinforcement Learning
- Learn AI Series (#112) - RL for Games
- Learn AI Series (#113) - RL for Real-World Applications
- Learn AI Series (#114) - Inverse Reinforcement Learning
- Learn AI Series (#115) - Offline Reinforcement Learning
- Learn AI Series (#116) - Mini Project: Training a Game-Playing AI
- Learn AI Series (#117) - ML System Design
- Learn AI Series (#118) - Data Engineering for AI
- Learn AI Series (#119) - Experiment Tracking and Reproducibility
- Learn AI Series (#120) - Model Optimization: Making Models Fast
- Learn AI Series (#121) - Model Serving Architecture
- Learn AI Series (#122) - Edge AI: Running Models on Devices
- Learn AI Series (#123) - Monitoring ML in Production
- Learn AI Series (#124) - CI/CD for Machine Learning
- Learn AI Series (#125) - GPU Programming Basics
- Learn AI Series (#126) - Distributed Training
- Learn AI Series (#127) - AI Security
- Learn AI Series (#128) - Privacy-Preserving AI
- Learn AI Series (#129) - AutoML and Neural Architecture Search
- Learn AI Series (#130) - Causal Inference and ML
- Learn AI Series (#131) - Graph Neural Networks
- Learn AI Series (#132) - AI for Structured Data
- Learn AI Series (#133) - Synthetic Data Generation
- Learn AI Series (#134) - AI Infrastructure Economics
- Learn AI Series (#135) - Building AI Teams and Processes
- Learn AI Series (#136) - Mini Project: Production AI Platform
- Learn AI Series (#137) - Foundation Models
- Learn AI Series (#138) - Multimodal AI
- Learn AI Series (#139) - AI for Code
- Learn AI Series (#140) - Scientific AI
- Learn AI Series (#141) - Robotics and Embodied AI (this post)
Learn AI Series (#141) - Robotics and Embodied AI
I ended last episode with a promise dressed up as a threat. We had spent #140 watching AI fold proteins, invent crystals and out-forecast supercomputers -- but always as a mind in a box, reading numbers and emitting numbers. And I asked what happens when you rip it OUT of the box and bolt it into something with motors, sensors, and a body that has to survive contact with the physical world.
That is today. Everything we've built across 140 episodes has lived in the digital world. Models process images, text and audio, but they never touch anything. They never bump a table, drop a glass, or lose their footing on a wet floor. Robotics is where AI meets physics, and physics -- unlike a validation set -- does not forgive sloppy abstractions. Let's dive right in ;-)
Solutions to episode #140's exercises
As always, we clear last time's homework before touching anything new. Episode #140 was Scientific AI, and the three tasks were about making our toy models a little more honest.
Exercise 1 -- Make the Evoformer admit how sure it is. The task: extend SimplifiedEvoformer so forward also returns a per-residue confidence vector, via a small linear head on the MSA output, averaged over the relatives and squashed with sigmoid. Here is the whole thing, confidence head and all.
import torch
import torch.nn as nn
class ConfidentEvoformer(nn.Module):
"""SimplifiedEvoformer (#140) + a per-residue confidence head."""
def __init__(self, seq_dim=256, pair_dim=128, n_heads=8):
super().__init__()
self.row_attn = nn.MultiheadAttention(seq_dim, n_heads, batch_first=True)
self.pair_proj = nn.Sequential(
nn.Linear(pair_dim, pair_dim), nn.ReLU(),
nn.Linear(pair_dim, pair_dim),
)
self.norm1 = nn.LayerNorm(seq_dim)
self.norm2 = nn.LayerNorm(pair_dim)
self.confidence = nn.Linear(seq_dim, 1) # the new bit
def forward(self, msa_repr, pair_repr):
batch, n_seq, seq_len, dim = msa_repr.shape
flat = msa_repr.reshape(batch * n_seq, seq_len, dim)
attn_out, _ = self.row_attn(flat, flat, flat)
msa_repr = self.norm1(msa_repr + attn_out.reshape(batch, n_seq, seq_len, dim))
pair_repr = self.norm2(pair_repr + self.pair_proj(pair_repr))
# average the confidence logits over the relatives, then squash to [0, 1]
conf_logits = self.confidence(msa_repr).squeeze(-1) # (batch, n_seq, seq_len)
confidence = torch.sigmoid(conf_logits.mean(dim=1)) # (batch, seq_len)
return msa_repr, pair_repr, confidence
model = ConfidentEvoformer()
msa = torch.randn(1, 4, 50, 256)
pair = torch.randn(1, 50, 50, 128)
_, _, conf = model(msa, pair)
print(f"confidence shape: {tuple(conf.shape)} (one number per residue)")
assert conf.shape == (1, 50)
The shape comes out (1, 50) -- exactly one confidence score per residue, which is the point. And WHY does a model that reports its own uncertainty beat one that only reports an answer? Because a scientist does not act on a prediction, they act on a prediction plus how much to trust it. AlphaFold's real confidence score (pLDDT) is not a nice-to-have -- it is the thing that tells a biologist which loops to believe and which to go verify in the lab. An answer with no error bar is a rumour.
Exercise 2 -- One GNN, two sciences. Feed MoleculeGNN a small-molecule graph and a crystal-cell graph of a DIFFERENT atom count, without touching the model, and confirm both spit out a single scalar. Then say what makes that possible.
class MoleculeGNN(nn.Module):
"""From #140: predict one bulk property from atoms + connectivity."""
def __init__(self, atom_features=32, hidden=64, output=1):
super().__init__()
self.atom_embed = nn.Linear(atom_features, hidden)
self.conv1 = nn.Linear(hidden, hidden)
self.conv2 = nn.Linear(hidden, hidden)
self.readout = nn.Linear(hidden, output)
def forward(self, atom_feats, adj):
h = torch.relu(self.atom_embed(atom_feats))
h = torch.relu(self.conv1(adj @ h))
h = torch.relu(self.conv2(adj @ h))
graph_repr = h.mean(dim=0, keepdim=True) # size-agnostic readout
return self.readout(graph_repr)
gnn = MoleculeGNN(atom_features=32)
# (a) a 9-atom molecule
mol_feats = torch.randn(9, 32)
mol_adj = torch.eye(9)
# (b) a 12-atom crystal cell -- DIFFERENT node count, SAME model
crys_feats = torch.randn(12, 32)
crys_adj = torch.eye(12)
print("molecule ->", gnn(mol_feats, mol_adj).shape) # (1, 1)
print("crystal ->", gnn(crys_feats, crys_adj).shape) # (1, 1)
Both return shape (1, 1) -- a single scalar -- despite one graph having 9 nodes and the other 12. The trick lives entirely in h.mean(dim=0): the readout AGGREGATES over however many nodes there are, so a 9-row tensor and a 12-row tensor both collapse to one fixed-size vector before the final linear layer. That is precisely why "atoms as nodes" generalises across chemistry and materials -- the model never hard-codes how many atoms it will see, it just averages messages over whatever graph you hand it. Change the molecule, change the crystal, the architecture does not blink.
Exercise 3 -- Draw the tool-versus-scientist line yourself. I asked you to pick one system and argue exactly where it sits. My answer, for AlphaFold: it is a tool, and a magnificent one, but it is NOT a scientist. It depends on a human to choose which protein matters, to frame why the structure is worth predicting, and -- crucially -- to interpret what the fold implies for disease or drug design. AlphaFold predicts that a chain folds a certain way with startling accuracy, but it has handed us no new physics of why proteins fold as they do. For it to cross into autonomous discovery it would need to do more than predict -- it would need to form a hypothesis about folding mechanism, design an experiment to test it, and revise its own theory from the result. "It would need to understand" is not the argument; the argument is that understanding would let it generate the next question, and prediction alone never does. Right -- homework settled. Now we add gravity ;-)
Why robotics is hard in a way software simply isn't
When your chatbot hallucinates, a user gets a wrong sentence. When your robot hallucinates, it drives off a staircase. The physical world piles on constraints that pure software never has to face, and it is worth naming them precisely before we reach for solutions.
Continuous state and action spaces. A robot arm has 6 or 7 joints, each with a continuous angle, velocity and torque. The state space is not a grid you can enumerate -- it is a manifold. Unlike Atari (#107), where you pick one of a handful of buttons, here "the set of actions" is uncountable.
Real-time or bust. A walking robot needs control signals at 100 to 1000 Hz. Your policy can NOT take 200ms to think -- by then the robot has already met the floor. This alone rules out a lot of the heavy inference we've been casually assuming for 140 episodes.
Partial observability. Sensors are noisy, occluded and limited. A camera cannot see behind the robot. A force sensor gives you local pressure, not the full state of the object you are grasping. The policy is always reasoning from a keyhole view.
Safety is not a config flag. You cannot explore recklessly the way you would in simulation. An arm swinging at full torque in a factory can injure or kill. "Just let it try random things and learn" is a sentence that ends careers here.
Non-stationarity. The real world drifts. Lighting shifts through the day, surfaces change friction, objects deform, a gripper wears. The policy has to GENERALISE, not memorise a frozen world.
Having said that, none of this is a reason to give up on learning-based robotics -- it is a reason to be honest about what "learning" has to survive.
The sim-to-real gap
Training robots in the real world is slow, expensive and occasionally on fire. Training in simulation is fast and free. The catch, and it is the central catch of the whole field: policies trained in simulation tend to FAIL in reality. This is the sim-to-real gap.
A simulated robot enjoys perfect joint angles, frictionless-or-exactly-specified contacts, instant sensor readings and deterministic physics. A real robot has backlash in its gears, sticky joints, camera latency, and a gripper that bends a hair under load. The policy that scored perfectly in sim has never seen ANY of this, so the first time reality whispers something off-distribution, it face-plants.
Domain randomization is the beautifully brutish fix: instead of straining to make the simulation match reality exactly, you make it match everything. During training you randomize the simulation parameters -- friction, masses, lighting, camera pose, sensor noise, actuator delay -- across a wide distribution. The policy has to work across all of them at once, so it is forced to learn behaviours that are robust rather than behaviours that exploit one specific fake world.
import numpy as np
class DomainRandomizer:
"""Randomize simulation parameters for sim-to-real transfer."""
def __init__(self):
self.ranges = {
'friction': (0.3, 1.5), # coefficient of friction
'mass_scale': (0.7, 1.3), # object mass multiplier
'actuator_noise': (0.0, 0.05), # noise added to actions
'obs_noise': (0.0, 0.02), # noise added to observations
'latency_steps': (0, 3), # sensor delay in timesteps
'gravity_z': (-10.5, -9.0), # gravity variation
}
def sample(self):
"""Sample one random environment configuration."""
params = {}
for key, (low, high) in self.ranges.items():
if isinstance(low, int) and isinstance(high, int):
params[key] = np.random.randint(low, high + 1)
else:
params[key] = np.random.uniform(low, high)
return params
def apply_obs_noise(self, obs, params):
noise = torch.randn_like(obs) * params['obs_noise']
return obs + noise
def apply_action_noise(self, action, params):
noise = torch.randn_like(action) * params['actuator_noise']
return action + noise
randomizer = DomainRandomizer()
for _ in range(3):
cfg = randomizer.sample()
print(f"friction={cfg['friction']:.2f} mass={cfg['mass_scale']:.2f} "
f"latency={cfg['latency_steps']} steps")
The insight worth tattooing on your arm: domain randomization turns sim-to-real into a GENERALIZATION problem, which is a problem we have spent this entire series learning to attack. Instead of asking "does this policy work in reality?" you ask "does this policy work in any environment drawn from a wide distribution?" -- and if the answer is yes, then reality is just one more environment it happens not to have seen. Nota bene: reality still has to fall INSIDE the randomization ranges. Randomize friction over too narrow a band and the real floor sits outside it, and you are right back where you started.
Robot learning paradigms
There are three main ways a robot learns to do a thing, and real systems mix them.
Imitation learning (learning from demonstrations). A human shows the robot what to do -- teleoperating an arm, say -- and the robot learns to replicate it. Treated as plain supervised learning (map observation to action), this is behavioral cloning, and it is as simple as it sounds.
class BehaviorCloningPolicy(nn.Module):
"""Learn to act by imitating expert demonstrations."""
def __init__(self, obs_dim, action_dim, hidden=256):
super().__init__()
self.net = nn.Sequential(
nn.Linear(obs_dim, hidden), nn.ReLU(),
nn.Linear(hidden, hidden), nn.ReLU(),
nn.Linear(hidden, action_dim),
nn.Tanh(), # actions bounded to [-1, 1]
)
def forward(self, obs):
return self.net(obs)
def train_behavior_cloning(policy, expert_obs, expert_actions, epochs=100):
optimizer = torch.optim.Adam(policy.parameters(), lr=3e-4)
dataset = torch.utils.data.TensorDataset(expert_obs, expert_actions)
loader = torch.utils.data.DataLoader(dataset, batch_size=256, shuffle=True)
for epoch in range(epochs):
total = 0.0
for obs_b, act_b in loader:
loss = nn.functional.mse_loss(policy(obs_b), act_b)
optimizer.zero_grad(); loss.backward(); optimizer.step()
total += loss.item()
if (epoch + 1) % 20 == 0:
print(f"epoch {epoch+1}, loss {total/len(loader):.4f}")
return policy
Behavioral cloning is simple but brittle, and the reason it breaks is worth understanding because it is subtle. It suffers from distribution shift -- sometimes called covariate shift or the compounding-error problem. The expert never made mistakes, so your training data contains ZERO examples of recovery. The moment the policy drifts a little off the expert's trajectory, it lands in a state it never trained on, makes a slightly worse prediction, drifts further, and the errors compound into a cascade. It is death by a thousand tiny deviations.
DAgger (Dataset Aggregation) patches exactly this: run the policy, collect the states it actually visits, ask the expert what they would have done in each of those states, add that to the dataset, retrain, repeat. You are teaching the model how to recover from its own drift. The price is an expert in the loop during training, which is not free.
Reinforcement learning (learning from rewards). The robot explores and learns from experience, the whole arc of episodes #102-116. For robotics, PPO (#109) and SAC (Soft Actor-Critic) dominate, because they cope well with continuous action spaces -- exactly the manifold-not-a-grid problem I flagged earlier.
Hybrid approaches are how serious robots actually get built: pretrain with imitation to get a sane starting policy (so RL is not exploring from pure noise), then fine-tune with RL to push past what the human demonstrator could do. Imitation bootstraps, RL refines. Here is the shape of that fine-tune, deliberately tiny so you can read it in one sitting:
def finetune_with_rl(policy, env_step, rollout_len=64, lr=1e-4):
"""Imitation bootstraps; RL refines. A minimal policy-gradient nudge
on top of a behavior-cloned policy (see PPO/#109 for the real thing)."""
optimizer = torch.optim.Adam(policy.parameters(), lr=lr)
obs = env_step(None) # reset -> initial observation
log_probs, rewards = [], []
for _ in range(rollout_len):
mean = policy(obs)
dist = torch.distributions.Normal(mean, 0.1) # explore a little
action = dist.sample()
log_probs.append(dist.log_prob(action).sum())
obs, reward = env_step(action.clamp(-1, 1))
rewards.append(reward)
returns = torch.tensor(rewards).flip(0).cumsum(0).flip(0) # reward-to-go
returns = (returns - returns.mean()) / (returns.std() + 1e-8)
loss = -(torch.stack(log_probs) * returns).mean() # REINFORCE objective
optimizer.zero_grad(); loss.backward(); optimizer.step()
return loss.item()
Notice the action.clamp(-1, 1) -- that is not cosmetic. In a body, an unclamped action is a joint asked to move past its physical stop. Even our toy loop respects the envelope, because the real one has to.
Dealing with the keyhole: observation history
Remember partial observability? A single camera frame does not tell you whether the ball is moving toward you or away. The standard trick is to feed the policy a short HISTORY of observations rather than a single snapshot, so velocity and intent become inferable. It is the same instinct behind sequence models (#48-49) -- give the network enough context to reconstruct what one frame hides.
class HistoryPolicy(nn.Module):
"""Stack the last k observations so the policy can infer motion."""
def __init__(self, obs_dim, action_dim, k=4, hidden=256):
super().__init__()
self.k = k
self.net = nn.Sequential(
nn.Linear(obs_dim * k, hidden), nn.ReLU(),
nn.Linear(hidden, action_dim), nn.Tanh(),
)
def forward(self, obs_window):
# obs_window: (batch, k, obs_dim) -> flatten the time axis
flat = obs_window.reshape(obs_window.shape[0], -1)
return self.net(flat)
policy = HistoryPolicy(obs_dim=12, action_dim=7, k=4)
window = torch.randn(2, 4, 12) # 2 robots, last 4 frames, 12-dim obs
print("actions ->", policy(window).shape) # (2, 7)
Language-conditioned robotics
Now the part that genuinely excites me. The most striking recent development is robots that follow NATURAL LANGUAGE. Instead of programming a specific behaviour, you tell the robot what you want in plain English -- "pick up the red cup" -- and it figures out the motor commands.
The architecture is usually a vision-language-action (VLA) model: a vision encoder chews the camera input, a language encoder chews the instruction, and a policy head fuses the two and emits motor commands. The clever move is to reuse a pretrained vision-language model -- CLIP-style perception, the thing we met in #75 and #138 -- as the perception backbone, and train only the action head on the (scarce, precious) robot data.
class VisionLanguageActionPolicy(nn.Module):
"""Simplified VLA: vision + language -> robot actions."""
def __init__(self, vision_dim=512, language_dim=512, action_dim=7):
super().__init__()
# In practice these would be PRETRAINED encoders (CLIP, etc.), frozen.
self.vision_encoder = nn.Sequential(nn.Linear(vision_dim, 256), nn.ReLU())
self.language_encoder = nn.Sequential(nn.Linear(language_dim, 256), nn.ReLU())
self.policy_head = nn.Sequential(
nn.Linear(512, 256), nn.ReLU(),
nn.Linear(256, 128), nn.ReLU(),
nn.Linear(128, action_dim), nn.Tanh(),
)
def forward(self, vision_features, language_features):
v = self.vision_encoder(vision_features)
l = self.language_encoder(language_features)
fused = torch.cat([v, l], dim=-1)
return self.policy_head(fused)
vla = VisionLanguageActionPolicy()
vision = torch.randn(1, 512) # a frame, already encoded
language = torch.randn(1, 512) # "pick up the red cup", already encoded
print("motor command ->", vla(vision, language).shape) # (1, 7)
Google's RT-2 and its cousins showed that a large vision-language model can GROUND language in physical action -- the model does not merely know what a "red cup" looks like, it knows how to reach for one. And the scale of the pretrained backbone matters enormously: bigger perception models generalise to instructions the robot never saw in training, which is exactly the foundation-model promise (#137) crossing over into meat-space.
Foundation models for robots (and the data wall)
The foundation-model pattern is reaching robotics: train one big model on diverse robot data -- many robots, many tasks, many environments -- then fine-tune or prompt it per task. Lovely idea. It slams straight into a wall.
The wall is DATA. Internet-scale text and images are lying around by the exabyte. Internet-scale robot-interaction data is NOT -- you cannot crawl the web for a million examples of a robot folding laundry, because nobody uploaded them, because they do not exist. Current approaches attack the shortage from several angles at once: simulation at massive scale, teleoperation datasets recorded by humans puppeting real arms, cross-embodiment transfer (train on data pooled from different robot bodies), and even using video-prediction models trained on YouTube as implicit world models.
The Open X-Embodiment project is the poster child: it pools demonstrations from 22 different robot platforms, and the resulting policy generalises across robot morphologies it was not trained on. That is a genuinely hopeful signal -- it hints that a robot foundation model does not need one standardised body, it can learn from the whole zoo. Whether that scales the way language did is, honestly, still an open question in 2026.
The hardware reality software people wave away
Software folk chronically underestimate how hard hardware constrains robot AI. A few realities that the cloud let us forget:
Sensors define perception. RGB cameras, depth cameras (Intel RealSense, Azure Kinect), LiDAR, IMUs, force/torque sensors, joint encoders -- each hands you a different slice of the world. Your sensor suite is the hard ceiling on what any policy can possibly perceive. No amount of clever network fixes a sensor that cannot see the thing.
Actuators define action. Electric motors (fast, precise, force-limited), hydraulics (powerful, slow, messy), pneumatic muscles (compliant, a pain to control). The actuator type sets your control bandwidth and force envelope before a single line of policy code is written.
Compute is on-board. For a mobile robot you cannot ship inference to a cloud GPU -- the round-trip latency would tip it over. Jetson modules, neural accelerators, custom silicon. Edge AI (#122) is not an optimisation in robotics, it is the entry fee. Everything we said about quantization and pruning back then becomes load-bearing here.
Safety is mechanical, not a checkbox. Collaborative robots (cobots) need force-limiting control, hardware e-stops, and workspace monitoring -- sensors, control theory and mechanical design working together. You do not try/except your way out of a two-hundred-kilo arm moving where a human is standing. Even in software, though, a learned policy's raw output should never reach a motor unfiltered -- you clamp it to the joint limits AND cap how fast it is allowed to change, so a spike in the network output cannot become a violent lurch:
def safe_command(prev_action, raw_action, joint_limits, max_delta=0.1):
"""Filter a policy's raw output before it ever touches a motor.
Clamp to physical limits, then limit the per-step CHANGE (rate limit)."""
low, high = joint_limits # tensors, one entry per joint
clamped = torch.clamp(raw_action, low, high) # respect the stops
delta = torch.clamp(clamped - prev_action, -max_delta, max_delta)
return prev_action + delta # bounded, smooth motion
prev = torch.zeros(7)
raw = torch.tensor([5.0, -9.0, 0.3, 0.0, 2.0, -0.4, 8.0]) # a wild network spike
limits = (torch.full((7,), -1.0), torch.full((7,), 1.0))
print("safe command ->", safe_command(prev, raw, limits).tolist())
This tiny guard is the software echo of the mechanical force-limiter: the network is allowed to be wrong, but it is NOT allowed to be wrong violently.
Exercises
Get your hands dirty before next time. Three tasks, climbing in difficulty:
Feel the compounding error. Take
BehaviorCloningPolicyand a trivial 1D "track the target" environment you write yourself (state = position, expert action = move toward 0). Train on expert rollouts that always START near 0, then evaluate from a starting position FAR from 0. Watch it wander. Then write one sentence explaining why the failure is worse the further you start from the training distribution -- and name the fix from this episode.Randomize until it transfers. Extend
DomainRandomizerwith arandomize_episode(env_params)method that returns a fresh config each episode, and run a loop of 1000 samples. Plot (or just bucket-count) the friction values to confirm they cover the full range. Then, in a comment, argue what happens to real-world transfer if yourfrictionrange is(0.9, 1.0)but the actual floor has friction0.4.Give the VLA a memory. Modify
VisionLanguageActionPolicyto accept a short history of vision frames (likeHistoryPolicydoes) instead of a single frame, keeping the language input as one instruction. Confirm the output is still a single action vector. Then explain, in a sentence, one manipulation task that is IMPOSSIBLE from a single frame but solvable with a 4-frame window.
We'll open next episode with full solutions, as always.
Quick recap
- Robotics is hard in ways software is not: continuous action manifolds, hard real-time control, partial observability, non-negotiable safety, and a world that keeps drifting under the policy's feet;
- the sim-to-real gap is the central problem -- policies perfect in simulation fail in reality because of physics mismatch, sensor noise and actuator imperfection;
- domain randomization bridges the gap by training across wide parameter distributions, converting sim-to-real into the generalization problem we already know how to fight (as long as reality falls inside your ranges);
- three learning paradigms: behavioral cloning (simple, but distribution shift makes it brittle), RL (PPO/SAC for continuous control), and the hybrid -- imitation to bootstrap, RL to refine -- that real systems ship;
- language-conditioned robotics uses vision-language-action models built on pretrained CLIP-style backbones (#75, #138) to turn plain-English instructions into motor commands;
- foundation models for robots hit a hard data wall; cross-embodiment pooling (Open X-Embodiment) and massive simulation are the current ladders out;
- hardware is not secondary -- sensors, actuators, on-board compute (edge AI, #122) and mechanical safety fundamentally bound what robot AI can do.
And here is the thread I want to leave dangling for next time. Everything today reacted to the world -- see a state, emit an action, see the next state, react again. Fast reflexes, no forethought. But the hardest problems, in a body or out of it, are not reflex problems -- they are the ones where the robot (or the agent) has to look several steps ahead, weigh options it has never tried, and commit to a PLAN before acting. What does it take to make a model that does not just react, but reasons about what to do next? That is where we head ;-)