Agent wizyjno-językowy za 1500 zł — jak zbudować go na NumPy w weekend
W piątek wieczorem zespół z polskiego startupu robotycznego *RoboFlow* zablokował się na problemie: ich robot do sortowania paczek w magazynie gubił się przy…
# Agent wizyjno-językowy za 1500 zł — jak zbudować go na NumPy w weekend W piątek wieczorem zespół z polskiego startupu robotycznego *RoboFlow* zablokował się na problemie: ich robot do sortowania paczek w magazynie gubił się przy zmianie układu półek. Po trzech dniach testów z symulatorem Unity postanowili spróbować czegoś prostszego — grid world renderowany w NumPy. W poniedziałek rano agent już planował trasy bezpośrednio z pikseli, a koszt infrastruktury spadł z 12 tys. zł miesięcznie na serwerach AWS do 1500 zł na lokalnym GPU. ## Dlaczego proste symulacje grid world mogą przyspieszyć rozwój agentów embodied AI? ### NumPy-renderowane środowisko: 90% mniej obliczeń, 100% kontroli W *RoboFlow* renderowanie jednego klatki w Unity zajmowało 120 ms na serwerze z kartą A100. Po przejściu na NumPy czas spadł do 12 ms — dziesięciokrotne przyspieszenie [1]. Kluczowa różnica? NumPy operuje na macierzach liczbowych, a nie na fizycznych silnikach. To oznacza, że możesz symulować tysiące epizodów treningowych na jednym laptopie, zamiast płacić za chmurę. Przykład: środowisko grid world 10x10 pikseli to macierz NumPy o rozmiarze (10, 10, 3) — trzy kanały RGB. Każdy piksel to liczba z zakresu 0-255, a nie obiekt z kolizjami i oświetleniem. Dla agenta embodied AI to wystarczy — nie potrzebuje fotorealizmu, tylko spójnych obserwacji. ### RGB vs stany symboliczne: dlaczego piksele wygrywają w uczeniu przez wzmacnianie Tradycyjne podejście do uczenia przez wzmacnianie (RL) opiera się na stanach symbolicznych — np. współrzędnych (x, y) agenta i obiektów. Problem? W rzeczywistych środowiskach te stany trzeba ręcznie definiować, co jest pracochłonne i podatne na błędy. Obserwacje RGB rozwiązują ten problem. Agent widzi świat tak, jak kamera — jako ciągły strumień pikseli. To pozwala na: - **Uniwersalność**: ten sam model może działać w różnych środowiskach bez rekonfiguracji [2]. - **Skalowalność**: dodanie nowego obiektu do środowiska nie wymaga aktualizacji kodu — wystarczy zmienić piksele. - **Transfer wiedzy**: modele trenowane na pikselach łatwiej adaptują się do nowych zadań [4]. W testach Google DeepMind agenci trenowani na pikselach osiągali o 30% lepsze wyniki w zadaniach nawigacyjnych niż modele bazujące na stanach symbolicznych [2]. ### Case study: Jak DeepMind testuje algorytmy w grid world Zanim DeepMind wdrożyło algorytmy w robotach fizycznych, przetestowało je w symulowanych środowiskach grid world. W jednym z eksperymentów agenci musieli nawigować po labiryncie, ucząc się tylko z obserwacji RGB [1]. Kluczowe wnioski: - **Szybkie iteracje**: jeden epizod treningowy zajmował średnio 0,8 sekundy (vs 5 sekund w symulatorach 3D). - **Niskie koszty**: cały trening zmieścił się na jednym serwerze z 4 kartami RTX 3090 (koszt ~30 tys. zł). - **Replikowalność**: wyniki były spójne między uruchomieniami, co ułatwiło debugowanie. ## Jak przygotować środowisko grid world do treningu agenta wizyjno-językowego? ### Krok po kroku: Konfiguracja NumPy do renderowania RGB Zacznij od stworzenia klasy `GridWorld` w Pythonie. Będzie ona odpowiedzialna za: 1. Inicjalizację macierzy NumPy reprezentującej środowisko. 2. Renderowanie stanu jako obrazu RGB. 3. Obsługę akcji agenta (np. ruch w górę/dół/lewo/prawo). Przykładowy kod:
import numpy as np
class GridWorld:
def __init__(self, size=10):
self.size = size
self.grid = np.zeros((size, size, 3), dtype=np.uint8) # RGB
self.agent_pos = (0, 0)
self.goal_pos = (size-1, size-1)
def reset(self):
self.grid = np.zeros((self.size, self.size, 3), dtype=np.uint8)
self.agent_pos = (0, 0)
self._render()
return self.grid
def _render(self):
self.grid = np.zeros((self.size, self.size, 3), dtype=np.uint8)
# Agent (czerwony)
self.grid[self.agent_pos[0], self.agent_pos[1]] = [255, 0, 0]
# Cel (zielony)
self.grid[self.goal_pos[0], self.goal_pos[1]] = [0, 255, 0]
def step(self, action):
x, y = self.agent_pos
if action == 0: y = max(0, y-1) # Góra
elif action == 1: y = min(self.size-1, y+1) # Dół
elif action == 2: x = max(0, x-1) # Lewo
elif action == 3: x = min(self.size-1, x+1) # Prawo
self.agent_pos = (x, y)
self._render()
reward = 1.0 if self.agent_pos == self.goal_pos else 0.0
done = self.agent_pos == self.goal_pos
return self.grid, reward, done
### Dlaczego piksele są lepsze niż zmienne symboliczne?
W podejściu symbolicznym stan środowiska mógłby być reprezentowany jako krotka `(agent_x, agent_y, goal_x, goal_y)`. To działa, ale ma poważne ograniczenia:
- **Brak generalizacji**: model uczy się tylko dla konkretnych współrzędnych, a nie ogólnych wzorców.
- **Trudności w skalowaniu**: dodanie przeszkód wymaga aktualizacji logiki stanu.
- **Sztuczna reprezentacja**: w rzeczywistości robot nie ma dostępu do współrzędnych — widzi tylko piksele z kamery.
Piksele rozwiązują te problemy, ale wprowadzają nowe wyzwania:
- **Wysoka wymiarowość**: obraz 10x10x3 ma 300 wymiarów (vs 4 w podejściu symbolicznym).
- **Szum**: piksele mogą zawierać nieistotne informacje (np. kolor tła).
- **Zależność od rozdzielczości**: model trenowany na obrazach 10x10 może nie działać na 100x100.
Rozwiązaniem jest **latent world modeling** — o tym w następnej sekcji.
### Narzędzia open-source do wizualizacji i debugowania
1. **Matplotlib**: Najprostsze rozwiązanie do podglądu stanów środowiska.
```python
import matplotlib.pyplot as plt
plt.imshow(env.grid)
plt.show()
```
2. **TensorBoard**: Do logowania epizodów treningowych i wizualizacji metryk.
3. **Gymnasium** (dawniej Gym): Biblioteka do tworzenia środowisk RL. Możesz owinąć swoją klasę `GridWorld` w interfejs Gymnasium, aby korzystać z gotowych algorytmów RL.
```python
import gymnasium as gym
from gymnasium import spaces
class GridWorldEnv(gym.Env):
def __init__(self):
self.grid_world = GridWorld()
self.action_space = spaces.Discrete(4) # 4 kierunki
self.observation_space = spaces.Box(low=0, high=255,
shape=(10, 10, 3), dtype=np.uint8)
```
## Jak zbudować i wytrenować lekki model świata w latent space?
### Architektura modelu: percepcja + planowanie w jednym
Model świata w latent space składa się z dwóch części:
1. **Encoder**: Przekształca obserwacje RGB w wektor latent (np. 64-wymiarowy).
2. **Dynamics Model**: Przewiduje przyszłe stany latent na podstawie bieżącego stanu i akcji.
Przykładowa architektura w PyTorch:
import torch
import torch.nn as nn
class WorldModel(nn.Module):
def __init__(self, latent_dim=64):
super().__init__()
# Encoder: RGB -> latent
self.encoder = nn.Sequential(
nn.Conv2d(3, 16, kernel_size=3, stride=2), # (B, 16, 4, 4)
nn.ReLU(),
nn.Conv2d(16, 32, kernel_size=3, stride=2), # (B, 32, 1, 1)
nn.ReLU(),
nn.Flatten(),
nn.Linear(32, latent_dim)
)
# Dynamics: (latent, action) -> next_latent
self.dynamics = nn.Sequential(
nn.Linear(latent_dim + 4, 128), # 4 akcje (one-hot)
nn.ReLU(),
nn.Linear(128, latent_dim)
)
def forward(self, obs, action):
latent = self.encoder(obs)
action_onehot = torch.zeros(action.size(0), 4, device=action.device)
action_onehot.scatter_(1, action.unsqueeze(1), 1)
next_latent = self.dynamics(torch.cat([latent, action_onehot], dim=1))
return next_latent
### Dlaczego latent world modeling zmniejsza zapotrzebowanie na dane? Tradycyjne podejścia do uczenia przez wzmacnianie wymagają tysięcy epizodów, aby agent nauczył się skutecznych strategii. Latent world modeling redukuje to zapotrzebowanie na dwa sposoby: 1. **Kompresja informacji**: Encoder redukuje 300-wymiarowy obraz RGB do 64-wymiarowego wektora latent, zachowując tylko istotne informacje [2]. 2. **Generalizacja**: Dynamics model uczy się przewidywać przyszłe stany w przestrzeni latent, co pozwala na transfer wiedzy między podobnymi środowiskami. W eksperymentach Google AI modele trenowane w latent space osiągały zbliżone wyniki do tradycyjnych metod, używając **o 50% mniej danych treningowych** [6]. ### Przykładowy kod: Trening modelu predykcyjnego Aby wytrenować model świata, potrzebujesz zbioru danych z epizodów treningowych. Każdy epizod to sekwencja `(obserwacja, akcja, nagroda, następna_obserwacja)`.
def train_world_model(model, dataloader, epochs=10):
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.MSELoss()
for epoch in range(epochs):
for obs, action, next_obs in dataloader:
# Przekształć obserwacje do latent space
latent = model.encoder(obs)
next_latent_pred = model(obs, action)
next_latent_true = model.encoder(next_obs)
# Oblicz stratę
loss = criterion(next_latent_pred, next_latent_true)
# Aktualizuj wagi
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f"Epoch {epoch+1}, Loss: {loss.item():.4f}")
## Jak zaimplementować model predictive control (MPC) w agencie?
### Czym jest MPC i dlaczego sprawdza się w agentach embodied?
Model Predictive Control (MPC) to metoda planowania, która optymalizuje sekwencję akcji na podstawie predykcji przyszłych stanów. W kontekście agentów embodied AI działa to tak:
1. Agent obserwuje bieżący stan środowiska (obraz RGB).
2. MPC generuje sekwencję akcji (np. 5 kroków do przodu).
3. Dla każdej akcji model świata przewiduje przyszły stan latent.
4. MPC wybiera sekwencję akcji, która maksymalizuje sumaryczną nagrodę.
5. Agent wykonuje pierwszą akcję z sekwencji i powtarza proces.
Kluczowe zalety MPC:
- **Dynamiczne replanowanie**: Agent może dostosować plan w każdym kroku, reagując na zmiany w środowisku [3].
- **Optymalizacja horyzontu**: MPC bierze pod uwagę przyszłe konsekwencje akcji, a nie tylko natychmiastową nagrodę.
- **Efektywność**: W porównaniu z tradycyjnymi metodami RL, MPC redukuje liczbę błędów o 40% w zadaniach wymagających adaptacji [3].
### Krok po kroku: Integracja MPC z modelem świata
Implementacja MPC dla agenta wizyjno-językowego składa się z kilku kroków:
1. **Definicja funkcji kosztu**: Określa, jak dobrze sekwencja akcji prowadzi do celu.
```python
def cost_function(latent_states, goal_latent):
# Oblicz odległość między przewidywanymi stanami a celem
return torch.norm(latent_states - goal_latent, dim=1).mean()
```
2. **Generowanie sekwencji akcji**: Użyj algorytmu optymalizacji (np. random shooting) do znalezienia najlepszej sekwencji.
```python
def random_shooting(model, obs, horizon=5, samples=1000):
best_actions = None
best_cost = float('inf')
for _ in range(samples):
# Wygeneruj losową sekwencję akcji
actions = torch.randint(0, 4, (horizon,))
latent = model.encoder(obs)
latent_states = [latent]
# Przewiduj przyszłe stany
for action in actions:
latent = model(obs, action.unsqueeze(0))
latent_states.append(latent)
obs = torch.zeros_like(obs) # Uproszczenie: nie aktualizujemy obserwacji
# Oblicz koszt
cost = cost_function(torch.stack(latent_states), goal_latent)
if cost < best_cost:
best_cost = cost
best_actions = actions
return best_actions[0] # Zwróć pierwszą akcję z najlepszej sekwencji
```
3. **Integracja z agentem**: W każdym kroku agent używa MPC do wyboru akcji.
```python
def act(obs, model, goal_latent):
action = random_shooting(model, obs, horizon=5)
return action.item()
```
### Jak MPC pozwala agentowi na dynamiczne replanowanie?
W tradycyjnym uczeniu przez wzmacnianie agent uczy się polityki (np. sieci neuronowej), która mapuje stany na akcje. Problem? Polityka jest statyczna — nie dostosowuje się do zmian w środowisku.
MPC rozwiązuje ten problem, **ponownie planując sekwencję akcji w każdym kroku**. Przykład:
- Agent ma dotrzeć do celu w środowisku grid world.
- Po dwóch krokach napotyka przeszkodę (nowy obiekt w środowisku).
- MPC generuje nową sekwencję akcji, omijając przeszkodę.
W testach przeprowadzonych przez autorów artykułu [3], agenci z MPC osiągali **o 25% wyższą skuteczność** w środowiskach z dynamicznymi przeszkodami niż agenci z tradycyjnym RL.
## Jakie są ograniczenia i wyzwania tego podejścia?
### NumPy-renderowane środowiska: szybkie, ale ograniczone
Największą zaletą grid world renderowanego w NumPy jest **niski koszt obliczeniowy**. Problem? Takie środowiska mają poważne ograniczenia:
1. **Brak fizyki**: Nie można symulować kolizji, grawitacji czy tarcia. W *RoboFlow* oznaczało to, że agent uczył się omijać przeszkody, ale nie potrafił ich przesuwać.
2. **Ograniczona rozdzielczość**: NumPy nie radzi sobie z obrazami większymi niż ~100x100 pikseli bez znacznego spadku wydajności. Dla porównania, kamery w robotach przemysłowych często rejestrują obrazy 1280x720.
3. **Brak tekstur i oświetlenia**: Wszystkie obiekty muszą być reprezentowane jako jednolite kolory, co utrudnia transfer umiejętności do rzeczywistych środowisk [4].
Alternatywą są silniki 3D jak Unity lub Unreal Engine, ale ich wdrożenie wymaga **10-20 razy więcej zasobów obliczeniowych** [1].
### Latent world modeling: oszczędność danych, ale kosztem interpretowalności
Modele świata w latent space są efektywne, ale mają wady:
1. **Black box**: Przestrzeń latent jest trudna do interpretacji. W *RoboFlow* oznaczało to, że debugowanie błędów agenta wymagało dodatkowych narzędzi do wizualizacji (np. PCA lub t-SNE).
2. **Zależność od encodera**: Jeśli encoder nie nauczy się reprezentować istotnych cech środowiska (np. położenia celu), cały model będzie nieskuteczny.
3. **Ograniczone horyzonty**: Dynamics model może przewidywać stany tylko na kilka kroków do przodu. W długoterminowych zadaniach (np. budowanie wieży z klocków) MPC może nie znaleźć optymalnej sekwencji akcji.
W badaniach [2] agenci z latent world modeling radzili sobie gorzej w zadaniach wymagających **precyzji** (np. chwytanie małych obiektów) niż modele bazujące na surowych pikselach.
### Gdzie takie rozwiązanie sprawdzi się najlepiej?
Mimo ograniczeń, lekki agent wizyjno-językowy na NumPy i latent world modeling ma sens w:
1. **Prototypowaniu**: Szybkie testowanie pomysłów bez inwestycji w infrastrukturę.
2. **Edukacji**: Nauka podstaw embodied AI na uniwersytetach (np. na Politechnice Warszawskiej, gdzie studenci w ramach kursu "Robot Learning" implementują podobne rozwiązania [do uzupełnienia przez redakcję]).
3. **Zadaniach nawigacyjnych**: Sortowanie paczek, patrolowanie magazynów, nawigacja w prostych labiryntach.
4. **Środowiskach z ograniczonymi danymi**: Tam, gdzie nie ma tysięcy epizodów treningowych (np. w robotyce medycznej).
Nie sprawdzi się w:
- Zadaniach wymagających fizyki (np. chwytanie delikatnych obiektów).
- Środowiskach z wysoką rozdzielczością (np. autonomiczne pojazdy).
- Zadaniach długoterminowych (np. budowanie złożonych struktur).
## Co dalej? Jak rozbudować tego agenta o nowe funkcje?
### Dodanie obsługi języka naturalnego: krok po kroku
Aby agent rozumiał polecenia w języku naturalnym (np. "przynieś czerwoną paczkę"), potrzebujesz:
1. **Modelu językowego**: Np. DistilBERT do kodowania poleceń w wektory.
2. **Integracji z modelem świata**: Połącz wektor językowy z wektorem latent z encodera wizyjnego.
3. **Nowej funkcji kosztu**: Zamiast maksymalizować odległość do celu, agent powinien maksymalizować zgodność z poleceniem.
Przykładowy kod integracji:
from transformers import DistilBertModel, DistilBertTokenizer
class LanguageVisionAgent(nn.Module):
def __init__(self, latent_dim=64):
super().__init__()
self.vision_encoder = ... # Jak wcześniej
self.language_encoder = DistilBertModel.from_pretrained('distilbert-base-uncased')
self.tokenizer = DistilBertTokenizer.from_pret