Ursprünglich veröffentlicht auf Medium im Dezember 2025. Dies ist eine Neuauflage mit geringfügigen redaktionellen Änderungen; die kanonische URL verweist auf das Medium-Original.
Am Ende dieses Leitfadens werden Sie Folgendes verstehen:
Ich verwende IR-SIM (Intelligent Robot Simulator), eine schlanke 2D-Simulationsbibliothek, die sich perfekt zum Lernen eignet. Keine 3D-Grafikkarte erforderlich, keine komplexe Physik-Engine – nur reine, verständliche Robotik.
Die meisten Radroboter, die Sie kennen – von Roombas bis hin zu AGVs in Lagerhäusern – nutzen ein sogenanntes Differentialantriebssystem. Es ist wunderbar einfach: zwei Räder, jedes mit einem eigenen Motor. Indem man die Geschwindigkeit jedes Rades unabhängig voneinander steuert, kann man den Roboter vorwärts, rückwärts, in Kurven oder auf der Stelle drehen lassen.
Stellen Sie sich das wie einen Panzer vor, oder wie wenn Sie schon einmal einen Einkaufswagen gelenkt haben, indem Sie auf einer Seite stärker gedrückt haben.
Mathematisch funktioniert das so:
Aus diesen beiden Werten berechnest du die Geschwindigkeiten der einzelnen Räder und umgekehrt. Die Position des Roboters wird wie folgt aktualisiert:
x_new = x + v × cos(θ) × dt
y_new = y + v × sin(θ) × dt
θ_new = θ + ω × dt
Dabei ist θ die Ausrichtung des Roboters (in welche Richtung er zeigt) und dt der Zeitschritt.
Das Verständnis des Differentialantriebs ist entscheidend, da es definiert, was der Roboter kann und nicht kann:
Diese Einschränkung bestimmt alles andere – von der Art und Weise, wie wir Steuerungen entwerfen, bis hin zu den Aktionen, die unser RL-Agent ausführen kann.
IR-SIM ist eine Python-Bibliothek, die 2D-Roboterumgebungen simuliert. Sie übernimmt die Physik und Visualisierung und bietet eine übersichtliche Schnittstelle für die Steuerung.
# Erstellen einer virtuellen Umgebung python3 -m venv venv source venv/bin/activate # Abhängigkeiten installieren pip install ir-sim gymnasium stable-baselines3 numpy matplotlib pyyaml
Das Robotik-Äquivalent zu „Hello, World“:
import os os.environ['MPLBACKEND'] = 'TkAgg' # Wichtig für macOS from irsim.world import World import matplotlib.pyplot as plt world_config = { 'world_name': 'my_first_robot', 'height': 10, 'width': 10, 'step_time': 0.1, 'sample_time': 0.1, 'offset': [0, 0], 'control_mode': 'keyboard' } world = World(**world_config) robot_config = { 'type': 'diff', # Differentialantrieb 'shape': 'circle', 'radius': 0.3, 'state': [2, 2, 0], # x, y, theta 'vel_min': [-1, -1], 'vel_max': [1, 1], 'goal': [8, 8] } world.add_robot(robot_config) obstacle_config = { 'type': 'obstacle', 'shape': 'circle', 'center': [5, 5], 'radius': 0.5 } world.add_obstacle(obstacle_config) plt.ion() for i in range(200): world.step() plt.pause(0.05)
Führe dies aus und du solltest einen kreisförmigen Roboter und ein Hindernis sehen. Der Roboter bewegt sich noch nicht – wir müssen ihm sagen, was er tun soll.
Vor dem Zeitalter der KI lösten Robotiker das Problem der Navigation mit cleveren Regelalgorithmen. Implementieren wir den einfachsten davon: die proportionale Regelung in Richtung eines Ziels.
Stellen Sie sich vor, Sie gehen über ein Feld auf einen Freund zu. Dabei tun Sie ganz natürlich Folgendes:
Das ist im Wesentlichen das, was die „Go-to-Goal“-Steuerung tut:
import numpy as np class GoToGoalController: def __init__(self, k_linear=0.5, k_angular=2.0): self.k_linear = k_linear # Vorwärtsgeschwindigkeitsverstärkung self.k_angular = k_angular # Drehgeschwindigkeitsverstärkung def compute_velocity(self, robot_state, goal): """ robot_state: [x, y, theta] Ziel: [Ziel_x, Ziel_y] Rückgabewerte: [lineare_Geschwindigkeit, Winkelgeschwindigkeit] """ x, y, theta = Roboter_Zustand Ziel_x, Ziel_y = Ziel dx = Ziel_x - x dy = Ziel_y - y Abstand = np.sqrt(dx**2 + dy**2) Winkel_zum_Ziel = np.arctan2(dy, dx) Kursfehler = Winkel_zum_Ziel - theta Kursfehler = np.arctan2(np.sin(Kursfehler), np.cos(Kursfehler)) if distance < 0.3: return [0.0, 0.0] linear_vel = np.clip(self.k_linear * distance, 0.0, 1.0) angular_vel = np.clip(self.k_angular * heading_error, -1.0, 1.0) return [linear_vel, angular_vel]
Das funktioniert wunderbar – bis ein Hindernis im Weg steht. Der Roboter fährt direkt hinein, da er nur das Ziel kennt.
Die einfachste Lösung: Wenn sich etwas in der Nähe und vor dir befindet, wende dich ab.
def compute_velocity_with_avoidance(self, robot_state, goal, obstacles): x, y, theta = robot_state for obs in obstacles: obs_x, obs_y = obs['center'] obs_radius = obs['radius'] dist = np.sqrt((obs_x - x)**2 + (obs_y - y)**2) - obs_radius - 0.3 angle_to_obs = np.arctan2(obs_y - y, obs_x - x) relative_angle = abs(angle_to_obs - theta) if dist < 0.5 and relative_angle < np.pi / 3: if angle_to_obs - theta > 0: return [0.1, -0.8] # nach rechts abbiegen return [0.1, 0.8] # nach links abbiegen return self.compute_velocity(robot_state, goal)
Das ist reaktive Navigation – einfach, schnell, aber begrenzt. Der Roboter kann in Ecken stecken bleiben und plant nicht voraus.
Was wäre, wenn wir statt abrupter „Wenn-Dann“-Regeln ein sanftes Kraftfeld schaffen würden, das den Roboter auf natürliche Weise lenkt?
Stell dir vor, das Ziel sei ein Magnet, der den Roboter anzieht, und Hindernisse seien Magnete, die ihn abstoßen. Der Roboter folgt der resultierenden Kraft.
Anziehungskraft (Ziel):
F_att = k_att × (goal_position − robot_position)
Abstoßendes Potential (Hindernisse):
F_rep = k_rep × (1/distance − 1/threshold) × direction_away
Der Roboter bewegt sich in Richtung der Gesamtkraft.
Potentialfelder sehen auf dem Papier elegant aus, haben aber ein bekanntes Problem: lokale Minima. Stellen Sie sich zwei Hindernisse vor, die in gleichem Abstand auf beiden Seiten des Ziels liegen. Die abstoßenden Kräfte heben sich gegenseitig auf und der Roboter bleibt stecken.
Hier kommen lernbasierte Ansätze ins Spiel – sie können potenziell kreative Lösungen finden, die handgefertigte Algorithmen übersehen.
Jetzt wird es interessant. Was wäre, wenn wir den Roboter durch Versuch und Irrtum lernen lassen würden, statt Regeln zu schreiben?
Verstärkendes Lernen besteht aus vier Komponenten:
Der Agent führt Aktionen aus, beobachtet die Ergebnisse und passt seine Strategie an, um die Gesamtbelohnung zu maximieren. Es ist wie das Trainieren eines Hundes mit Leckerlis – nur dass der „Hund“ ein neuronales Netzwerk ist und die „Leckerlis“ positive Zahlen sind.
Um moderne RL-Bibliotheken wie Stable-Baselines3 zu nutzen, bündeln wir IR-SIM in einer Gymnasium-Umgebung:
import gymnasium as gym from gymnasium import spaces import numpy as np class DiffDriveNavEnv(gym.Env): def __init__(self): super().__init__() self.observation_space = spaces.Box( low=-np.inf, high=np.inf, shape=(7,), dtype=np.float32 ) self.action_space = spaces.Box( low=np.array([0.0, -1.0]), # nur vorwärts! high=np.array([1.0, 1.0]), dtype=np.float32 ) def step(self, action): self.world.set_robot_velocity(action) self.world.step() obs = self._get_observation() reward = self._compute_reward(obs) terminated = self._check_goal_reached() truncated = self._check_collision() or self.steps > 1000 return obs, reward, terminated, truncated, {}
Ich habe 7 Werte ausgewählt, die dem Roboter Situationsbewusstsein verleihen:
def _get_observation(self): dx = self.goal[0] - self.robot.x dy = self.goal[1] - self.robot.y dist = np.sqrt(dx**2 + dy**2) dx_norm = dx / max(dist, 0.01) dy_norm = dy / max(dist, 0.01) angle_to_goal = np.arctan2(dy, dx) dtheta = angle_to_goal - self.robot.theta dtheta = np.arctan2(np.sin(dtheta), np.cos(dtheta)) v = self.robot.linear_velocity w = self.robot.angular_velocity min_obstacle_dist = self._get_min_obstacle_distance() return np.array([ dx_norm, dy_norm, dtheta, v, w, min_obstacle_dist, dist ], dtype=np.float32)
Hier ist das schmutzige Geheimnis des RL: Die Belohnungsfunktion ist alles. Eine schlechte Belohnung führt zu bizarrem, unerwartetem Verhalten. Eine gute Belohnung führt zu eleganten Lösungen.
Ich dachte, ich wäre clever:
def _compute_reward_v1(self, obs): reward = 0 reward += 100 / (obs[6] + 0.1) # umgekehrte Entfernung reward -= 0.01 # Zeitstrafe if obs[5] < 0.5: reward -= 10 * (0.5 - obs[5]) # Strafe für Nähe zum Hindernis reward += 0.5 * abs(obs[3]) # Bonus für jede Bewegung reward += 1.0 * (1 - abs(obs[2]) / np.pi) # Bonus für Ausrichtung auf das Ziel if self._check_collision(): Belohnung -= 200 # Strafe für Kollision if obs[6] < 0.3: Belohnung += 50 # Bonus für Zielerreichung return Belohnung
Ergebnis: Der Roboter drehte sich im Kreis. Warum?
Bei so vielen Belohnungstermen war der Agent verwirrt:
Nachdem ich Artikel über RL-Navigation gelesen hatte, vereinfachte ich das System drastisch:
def _compute_reward_v2(self, obs): progress = self.prev_distance - obs[6] self.prev_distance = obs[6] reward = 0 reward += 10.0 * max(0.0, progress) # primär: Belohnung für Vorwärtsbewegung angular_vel = abs(obs[4]) if angular_vel > 0.8: reward -= 5.0 * (angular_vel - 0.8) # Abzug: übermäßiges Drehen if obs[5] < 0.3: reward -= 5.0 # Abzug: zu nah an Hindernissen elif obs[5] < 0.5: reward -= 1.0 if obs[6] < 0.3: reward += 100.0 # Zielbonus return reward
Wichtige Erkenntnisse:
Es gab noch eine weitere entscheidende Änderung: Bewegung nur nach vorne.
Mein ursprünglicher Aktionsraum erlaubte [-1, 1] für die lineare Geschwindigkeit. Der Roboter konnte rückwärts fahren. Das Problem: Er lernte, sich unendlich weit von Hindernissen zurückzuziehen, anstatt um sie herum zu navigieren.
Durch die Beschränkung der linearen Geschwindigkeit auf [0, 1] musste der Roboter vorwärts fahren und einen Weg finden. Er konnte nicht durch Rückzug entkommen.
Wenn die Umgebung korrekt eingerichtet ist, ist das Training überraschend unkompliziert:
from stable_baselines3 import PPO from stable_baselines3.common.callbacks import CheckpointCallback env = DiffDriveNavEnv(randomize=True) model = PPO( "MlpPolicy", env, learning_rate=3e-4, n_steps=2048, batch_size=64, n_epochs=10, gamma=0.99, verbose=1, tensorboard_log="./logs/tensorboard/" ) model.learn( total_timesteps=500_000, callback=CheckpointCallback( save_freq=20_000, save_path="./models/ppo/checkpoints/" ) ) model.save("./models/ppo/best_model")
Proximal Policy Optimization (PPO) ist das Arbeitspferd des modernen RL. Es ist:
Es funktioniert, indem es kleine, vorsichtige Schritte im Policy-Raum unternimmt – es ändert nie zu viel auf einmal.
Auf meinem M1-Mac dauern 500.000 Zeitschritte etwa 45 Minuten. Die Verwendung von MPS (Apples GPU-Beschleunigung) beschleunigt den Vorgang erheblich:
import torch def get_device(): if torch.backends.mps.is_available(): return "mps" # Apple Silicon GPU if torch.cuda.is_available(): return "cuda" # NVIDIA GPU return "cpu"
Nachdem die Belohnungsfunktion und der Aktionsraum festgelegt wurden, sieht eine erfolgreiche Navigation wie folgt aus:
Der Weg ist bemerkenswert direkt – der Agent hat gelernt, effiziente Routen durch Hindernisfelder zu finden, ohne explizite Wegplanungsalgorithmen.
Eines der interessantesten Experimente war der Einsatz aller fünf Steuerungsansätze in denselben Szenarien:
In diesem Szenario übertreffen die klassischen Steuerungen PPO tatsächlich bei der Schrittzahl. Potential Field ist mit 100 Schritten besonders effizient. Aber sehen Sie sich an, was in einem anspruchsvolleren Szenario passiert:
Das ist die eigentliche Erkenntnis: PPO ist robuster. Wenn die Hinderniskonfiguration knifflig wird, versagen klassische Steuerungsalgorithmen – Go-to-Goal kollidiert, Potential Field bleibt in einem lokalen Minimum stecken. PPO findet einen Weg hindurch.
Beachte, wie SAC (Soft Actor-Critic) das klassische Drehverhalten zeigt, das ich zuvor erwähnt habe – dieses kleine spiralförmige Muster ist genau das, was passiert, wenn die Belohnungsfunktion übermäßige Drehung nicht bestraft. (Die SAC- und TD3-Läufe wurden noch mit dem alten Belohnungssystem trainiert.)
Go-to-Goal: schnell, einfach, interpretierbar. Scheitert bei komplexen Hindernislayouts.
Potential Field: glatte Pfade, keine Feinabstimmung. Lokales Minimum-Falle, kann zeitlich auslaufen.
PPO: robust, bewältigt komplexe Szenarien. Benötigt Training, weniger interpretierbar.
SAC (musste mit der neuen Belohnung neu trainiert werden): theoretisch sample-effizient; hatte Schwierigkeiten mit dieser Aufgabe.
TD3 (musste mit der neuen Belohnung neu trainiert werden): gut für kontinuierliche Steuerung; gab zu schnell auf.
Ein weiteres Konzept, das es wert ist, visualisiert zu werden: Odometrie-Drift.
In der realen Welt schätzen Roboter ihre Position durch Zählen der Radumdrehungen. Kleine Fehler summieren sich – Räder rutschen, Messungen sind nicht perfekt, und schon bald glaubt der Roboter, er befinde sich an einem Ort, an dem er nicht ist.
In dieser Simulation habe ich den Messwerten der Radausleser kleine zufällige Störsignale hinzugefügt. Selbst bei winzigen Fehlern pro Schritt weicht die geschätzte Position merklich vom tatsächlichen Weg ab. Deshalb benötigen echte Roboter zusätzliche Sensoren (Kameras, LIDAR, GPS), um ihre Positionsschätzungen zu korrigieren – ein Thema für einen anderen Beitrag.
Nach wochenlangem Debuggen, Neu-Trainieren und Kopfzerbrechen:
Implementiere vor dem Trainieren von RL klassische Lösungen. Sie validieren deine Simulation, liefern Basisdaten und ihr Verhalten beeinflusst das RL-Design (zum Beispiel die „Forward-Only“-Einschränkung).
Wenn RL versagt, widerstehen Sie dem Drang, Hyperparameter willkürlich anzupassen. Stattdessen:
Komplexe Belohnungsfunktionen mit vielen Termen verwirren den Agenten. Fortschrittsbasierte Belohnungen mit klaren, konsistenten Signalen schneiden besser ab als aufwendige Formulierungen mit mehreren Zielen.
Die Einschränkung des Aktionsraums entsprechend dem gewünschten Verhalten verhindert, dass der Agent kontraproduktive Strategien lernt. Wenn Ihr Roboter nicht rückwärts fahren soll, lassen Sie keine negative lineare Geschwindigkeit zu.
Das wertvollste Ergebnis ist nicht das endgültige Modell – es ist der Weg dorthin. Diagnoseskripte und Änderungsprotokolle helfen Ihnen zu verstehen, was schiefgelaufen ist und warum die Korrekturen funktioniert haben.
Möchten Sie es ausprobieren?
# Klonen und einrichten git clone https://github.com/padawanabhi/robot_sim.git cd robot_sim python3 -m venv venv && source venv/bin/activate pip install -r requirements.txt # Simulation testen python scripts/01_test_simulation.py # RL-Agent trainieren python scripts/06_train_rl.py --algorithm ppo --timesteps 500000 # Trainiertes Modell auswerten python scripts/07_evaluate_rl.py --algorithm ppo --episodes 5 # Alle Steuerungen vergleichen python scripts/09_compare_controllers.py
tensorboard --logdir logs/tensorboard # http://localhost:6006 öffnen
Dieses Projekt hat Türen zu mehreren spannenden Richtungen geöffnet:
Robotik muss nicht einschüchternd sein. Mit einem einfachen 2D-Simulator und etwas Ausdauer können Sie:
Die wertvollste Lektion betraf nicht Roboter oder RL – es ging um systematische Problemlösung. Wenn sich Ihr Agent im Kreis dreht, geben Sie nicht dem Algorithmus die Schuld. Sie vertiefen sich in die Beobachtungen, überprüfen die Physik und vereinfachen, bis Sie den Fehler finden.
Das ist die wahre Fähigkeit. Und sie lässt sich weit über die Robotik hinaus anwenden.
Wenn Sie die bisherigen Beiträge verfolgt haben, greift der 3D-Folgebeitrag – Von der Flachwelt zur Physik dieselben Ideen in PyBullet auf, mit Sensoren, Navigationsalgorithmen und PPO-Training an einem 3D-Roboter mit Differentialantrieb.