Vision-Language-Action-Modelle für Robotik-Anwendungen feinabstimmen
Vision-Language-Action-Modelle, kurz VLA-Modelle, gelten als wichtiger Fortschritt im Bereich der verkörperten künstlichen Intelligenz. Sie vereinen visuelle Wahrnehmung, Sprachverständnis und Robotersteuerung in einer gemeinsamen Modellarchitektur. Dadurch können sie natürliche Sprachbefehle interpretieren und physische Aufgaben ausführen. Dieses Tutorial beschreibt den vollständigen Ablauf, um VLA-Modelle für individuelle Robotik-Anwendungen feinabzustimmen.
Was sind Vision-Language-Action-Modelle?
VLA-Modelle verbinden Computer Vision, Natural Language Processing und die Ausführung von Aktionen. Computer Vision ermöglicht es dem Modell, Bilder oder Videos auszuwerten. Die Sprachverarbeitung hilft dabei, Texte zu verstehen und zu erzeugen. Die Aktionsausführung sorgt dafür, dass das Modell mit Umgebungen, Robotern oder Systemen interagieren kann. Auf diese Weise können VLA-Modelle anhand visueller und sprachlicher Eingaben wahrnehmen, schlussfolgern und handeln.
Wenn Sie VLA-Modelle in der Praxis sehen möchten, können Sie sich die Google DeepMind Robotics Lab Tour mit Hannah Fry ansehen. Zu den bekannten VLA-Architekturen zählen OpenVLA, RT-2 (Robotic Transformer 2) und PaLM-E.
Wichtige Erkenntnisse
- VLA-Modelle führen Bildverarbeitung, Sprache und Handlungsausführung zusammen, indem sie Kamerabilder und natürliche Sprachbefehle direkt in Roboteraktionen überführen. Dies geschieht über eine durchgängige End-to-End-Modellstruktur.
- LoRA eignet sich für effizientes Fine-Tuning, da nur etwa 0,1-0,2 % der Modellparameter trainiert werden, anstatt das gesamte Modell anzupassen. Dadurch wird das Training auf einer einzelnen GPU mit 24 GB VRAM möglich.
Voraussetzungen
Hardware-Anforderungen
- Benötigt wird eine GPU mit mindestens 24 GB VRAM, beispielsweise eine RTX 3090, RTX 4090, A100 oder H100.
- Zusätzlich benötigen Sie Roboterhardware oder eine Simulationsumgebung.
- Für die visuelle Eingabe während der Inferenz sind Kameras erforderlich.
- Für Tests und Ausführung wird Roboterhardware oder eine Simulationsumgebung benötigt.
Schritt 0: GPU-Zugriff und Jupyter Notebook einrichten
Bereiten Sie einen GPU-fähigen Server oder eine entsprechende Workstation vor und verbinden Sie sich über Ihr Terminal per SSH damit.
Im Terminal:
# Core dependencies
pip install torch torchvision
pip install transformers accelerate
pip install datasets
pip install wandb # for experiment tracking
pip install jupyter lab
jupyter lab
Schritt 1: Die VLA-Architektur verstehen
Die wichtigsten Bestandteile eines VLA-Modells sind Vision Encoder, Language Encoder, Fusion Module und Action Decoder.
Der Vision Encoder verarbeitet Kamerabilder und wandelt sie in visuelle Einbettungen um. Der Language Encoder überführt Textanweisungen in sprachliche Einbettungen. Das Fusion Module kombiniert die visuellen und sprachlichen Informationen. Anschließend sagt der Action Decoder auf Basis dieser zusammengeführten Repräsentation die passenden Roboteraktionen voraus.
Schritt 2: Den Datensatz vorbereiten
Datenerfassung
Ihr Datensatz sollte Episoden mit den folgenden Bestandteilen enthalten:
- Bilder: RGB-Kameraaufnahmen für jeden Zeitschritt.
- Sprache: Natürlichsprachliche Beschreibungen der jeweiligen Aufgaben.
- Aktionen: Abfolgen von Roboteraktionen, zum Beispiel Gelenkpositionen, Geschwindigkeiten oder Endeffektor-Posen.
- Metadaten: Erfolgskennzeichnungen, Episodenlänge und ähnliche Informationen.
Beispiel für ein Datenformat
{
"episode_0": {
"images": [img_0, img_1, ..., img_T], # shape: (T, H, W, 3) where T is total frames
"language": "pick up the red block",
"actions": [act_0, act_1, ..., act_T], # shape: (T, action_dim)
"success": True
}
}
Organisation des Datensatzes
from datasets import Dataset, DatasetDict
import numpy as np
def create_vla_dataset(episodes):
"""Convert robot episodes to HuggingFace dataset format"""
data = {
"images": [],
"language": [],
"actions": [],
"episode_id": []
}
for ep_id, episode in enumerate(episodes):
for t in range(len(episode['images'])):
data['images'].append(episode['images'][t])
data['language'].append(episode['language'])
data['actions'].append(episode['actions'][t])
data['episode_id'].append(ep_id)
return Dataset.from_dict(data)
# Split into train/val
dataset = create_vla_dataset(your_episodes)
dataset = dataset.train_test_split(test_size=0.1)
Schritt 3: Das Basismodell einrichten
Ein vortrainiertes VLA-Modell laden
from transformers import AutoModel, AutoProcessor
import torch
# Load pre-trained VLA (example using OpenVLA)
model_name = "openvla/openvla-7b"
processor = AutoProcessor.from_pretrained(model_name)
model = AutoModel.from_pretrained(
model_name,
torch_dtype=torch.bfloat16,
device_map="auto"
)
# Freeze base model parameters (optional for LoRA)
for param in model.parameters():
param.requires_grad = False
Den Aktionsraum anpassen
Der Aktionsraum Ihres Roboters unterscheidet sich in der Regel von den Daten, die beim Vortraining verwendet wurden:
from torch import nn
class ActionHead(nn.Module):
def __init__(self, hidden_dim, action_dim):
super().__init__()
self.fc = nn.Sequential(
nn.Linear(hidden_dim, 512),
nn.ReLU(),
nn.Dropout(0.1),
nn.Linear(512, action_dim)
)
def forward(self, x):
return self.fc(x)
# Add custom action head
model.action_head = ActionHead(
hidden_dim=model.config.hidden_size,
action_dim=7 # e.g., 6-DOF arm + gripper
)
Schritt 4: LoRA für effizientes Fine-Tuning verwenden
Low-Rank Adaptation, kurz LoRA, macht das Fine-Tuning effizienter, indem die Anzahl der trainierbaren Parameter reduziert wird:
from peft import LoraConfig, get_peft_model
# Configure LoRA
lora_config = LoraConfig(
r=16, # rank
lora_alpha=32,
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
lora_dropout=0.1,
bias="none",
task_type="CAUSAL_LM"
)
# Apply LoRA to model
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
#Expected output: trainable params: 8.3M || all params: 7B || trainable%: 0.12%
Schritt 5: Daten verarbeiten und erweitern
Bildvorverarbeitung
from torchvision import transforms
image_transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
transforms.RandomHorizontalFlip(p=0.5),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
Normalisierung der Aktionen
def normalize_actions(actions, stats):
"""Normalize actions to [-1, 1] range"""
return (actions - stats['mean']) / (stats['std'] + 1e-8)
def denormalize_actions(normalized_actions, stats):
"""Convert back to original action space"""
return normalized_actions * (stats['std'] + 1e-8) + stats['mean']
# Calculate statistics from your dataset
action_stats = {
'mean': dataset['train']['actions'].mean(axis=0),
'std': dataset['train']['actions'].std(axis=0)
}
DataLoader einrichten
from torch.utils.data import DataLoader
def collate_fn(batch):
"""Custom collate function for VLA data"""
images = torch.stack([processor.image_processor(b['images'])
for b in batch])
text = processor.tokenizer([b['language'] for b in batch],
padding=True, return_tensors="pt")
actions = torch.tensor([b['actions'] for b in batch],
dtype=torch.float32)
return {
'pixel_values': images,
'input_ids': text['input_ids'],
'attention_mask': text['attention_mask'],
'actions': actions
}
train_loader = DataLoader(
dataset['train'],
batch_size=32,
shuffle=True,
collate_fn=collate_fn,
num_workers=4
)
Schritt 6: Die Trainingsschleife erstellen
Loss-Funktion
import torch.nn.functional as F
def vla_loss(predicted_actions, target_actions, reduction='mean'):
"""Action prediction loss (MSE for continuous actions)"""
mse_loss = F.mse_loss(predicted_actions, target_actions, reduction=reduction)
return mse_loss
# Alternative: Action chunking for temporal coherence
def chunked_action_loss(pred_chunks, target_chunks, chunk_size=10):
"""Predict multiple future actions at once"""
loss = 0
for i in range(chunk_size):
loss += F.mse_loss(pred_chunks[:, i], target_chunks[:, i])
return loss / chunk_size
Trainingsskript
from accelerate import Accelerator
from tqdm import tqdm
import wandb
# Initialize accelerator for distributed training
accelerator = Accelerator(mixed_precision='bf16')
# Setup
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.01)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=10000)
# Prepare for distributed training
model, optimizer, train_loader = accelerator.prepare(
model, optimizer, train_loader
)
# Initialize wandb
wandb.init(project="vla-finetuning", config={
"learning_rate": 1e-4,
"batch_size": 32,
"epochs": 10
})
# Training loop
global_step = 0
for epoch in range(10):
model.train()
epoch_loss = 0
for batch in tqdm(train_loader, desc=f"Epoch {epoch+1}"):
# Forward pass
outputs = model(
pixel_values=batch['pixel_values'],
input_ids=batch['input_ids'],
attention_mask=batch['attention_mask']
)
# Predict actions
predicted_actions = model.action_head(outputs.last_hidden_state[:, -1])
# Compute loss
loss = vla_loss(predicted_actions, batch['actions'])
# Backward pass
accelerator.backward(loss)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
scheduler.step()
optimizer.zero_grad()
# Logging
epoch_loss += loss.item()
global_step += 1
if global_step % 100 == 0:
wandb.log({
"loss": loss.item(),
"learning_rate": scheduler.get_last_lr()[0],
"epoch": epoch
})
print(f"Epoch {epoch+1} - Average Loss: {epoch_loss / len(train_loader):.4f}")
# Save checkpoint
if (epoch + 1) % 2 == 0:
accelerator.save_model(model, f"checkpoints/epoch_{epoch+1}")
Schritt 7: Das Modell evaluieren
Evaluierung in der Simulation
def evaluate_in_simulation(model, env, num_episodes=50):
"""Evaluate model in simulation environment"""
model.eval()
success_count = 0
with torch.no_grad():
for episode in range(num_episodes):
obs = env.reset()
instruction = env.get_task_instruction()
done = False
while not done:
# Process observation
image = torch.tensor(obs['image']).unsqueeze(0)
text_inputs = processor.tokenizer(instruction, return_tensors="pt")
# Predict action
outputs = model(
pixel_values=image.to(model.device),
input_ids=text_inputs['input_ids'].to(model.device)
)
action = model.action_head(outputs.last_hidden_state[:, -1])
# Denormalize and execute
action = denormalize_actions(action.cpu().numpy(), action_stats)
obs, reward, done, info = env.step(action[0])
if info['success']:
success_count += 1
success_rate = success_count / num_episodes
print(f"Success Rate: {success_rate*100:.1f}%")
return success_rate
Metriken für Behavioral Cloning
def evaluate_bc_metrics(model, val_loader):
"""Evaluate behavioural cloning performance"""
model.eval()
total_mse = 0
total_cosine_sim = 0
n_samples = 0
with torch.no_grad():
for batch in val_loader:
outputs = model(
pixel_values=batch['pixel_values'],
input_ids=batch['input_ids']
)
pred_actions = model.action_head(outputs.last_hidden_state[:, -1])
# MSE
mse = F.mse_loss(pred_actions, batch['actions'])
total_mse += mse.item() * len(batch['actions'])
# Cosine similarity
cosine_sim = F.cosine_similarity(pred_actions, batch['actions']).mean()
total_cosine_sim += cosine_sim.item() * len(batch['actions'])
n_samples += len(batch['actions'])
return {
'mse': total_mse / n_samples,
'cosine_similarity': total_cosine_sim / n_samples
}
Schritt 8: Das Modell bereitstellen
Modellexport
# Save fine-tuned model
model.save_pretrained("./finetuned_vla")
processor.save_pretrained("./finetuned_vla")
# For deployment, merge LoRA weights
from peft import PeftModel
base_model = AutoModel.from_pretrained(model_name)
merged_model = PeftModel.from_pretrained(base_model, "./finetuned_vla")
merged_model = merged_model.merge_and_unload()
merged_model.save_pretrained("./deployed_model")
Echtzeit-Inferenz
class VLAController:
def __init__(self, model_path):
self.model = AutoModel.from_pretrained(model_path)
self.processor = AutoProcessor.from_pretrained(model_path)
self.model.eval()
self.model.to('cuda')
@torch.inference_mode()
def predict_action(self, image, instruction):
"""Real-time action prediction"""
# Preprocess
inputs = self.processor(
images=image,
text=instruction,
return_tensors="pt"
).to('cuda')
# Predict
outputs = self.model(**inputs)
action = self.model.action_head(outputs.last_hidden_state[:, -1])
return action.cpu().numpy()[0]
# Usage
controller = VLAController("./deployed_model")
action = controller.predict_action(camera_image, "pick up the cup")
robot.execute_action(action)
Ressourcen
- OpenVLA
- RT-2 Paper und RT-2 Website: Dieses Projekt führte den Begriff VLA ein, also Vision-Language-Action-Modell.
- PEFT Library
- RoboMimic für die Arbeit mit Datensätzen
- OpenVLA: LeRobot Research Presentation #5 von Moo Jin Kim
Fazit
Das Fine-Tuning von VLA-Modellen ermöglicht es Robotern, spezialisierte Aufgaben mithilfe natürlicher Sprachsteuerung auszuführen. Dieses Tutorial hat gezeigt, wie vortrainierte Modelle an bestimmte Hardware-Setups und Aufgaben angepasst werden können, während gleichzeitig die Vorteile umfangreicher Vortrainingsdaten genutzt werden. Ein sinnvoller Ansatz besteht darin, zunächst in einer Simulation zu starten, das Modell durch schnelle Iterationen zu verbessern und anschließend schrittweise auf reale Roboterhardware zu wechseln, sobald die Leistung zuverlässiger wird.


