Nelder–Mead, gradient descent, SQP z ograniczeniami, algorytmy ewolucyjne.
Wyobraź sobie, że jesteś w górach. Stoisz w nieznanym miejscu i chcesz zejść jak najniżej — do dna doliny. Nie masz mapy, ale masz dwie informacje: na jakiej wysokości teraz jesteś (możesz zmierzyć) i w którą stronę teren opada (możesz zobaczyć stopami). Co robisz? Idziesz w stronę spadku, krok po kroku, każdy krok kontrolując, czy jest niżej. Jeśli przypadkiem wszedłeś pod górę — cofasz się. To jest istota optymalizacji numerycznej.
Formalnie: mamy funkcję , która każdej wartości przypisuje liczbę („wysokość", „koszt", „błąd"). Szukamy , dla którego ta liczba jest najmniejsza. Tę wartość nazywamy argumentem minimum:
W jednym wymiarze wygląda to tak (zaczynamy z i schodzimy w dół):
W więcej niż jednym wymiarze (np. 2D) wizualizujemy zwykle linie konturowe funkcji — punkty o tej samej wartości. Algorytm zaczyna w punkcie startowym i przeskakuje zboczem ku centrum (minimum), prostopadle do konturów:
Dla funkcji jednej zmiennej możemy często znaleźć minimum analitycznie — przyrównujemy pochodną do zera () i rozwiązujemy równanie. Ale w praktyce funkcja jest skomplikowana albo wielowymiarowa, więc używamy iteracji numerycznej. W każdym kroku robimy mały ruch w stronę spadku:
to długość kroku (rozmiar pojedynczej iteracji). Za małe — wolna zbieżność. Za duże — ryzyko przeskoczenia minimum i oscylacji. Strategie wyboru to bogata część teorii optymalizacji (np. Armijo line search, którą zobaczysz niżej).
W IK chcemy znaleźć kąty przegubów , dla których końcówka robota trafia w zadaną pozę . Możemy to sformułować jako optymalizację: zdefiniujmy funkcję kosztu mierzącą, jak bardzo robot się myli:
(pierwsza składowa to kwadrat odległości pomiędzy aktualną a zadaną pozycją końcówki, druga — analogicznie dla orientacji). Wartość oznacza idealne trafienie w pozę. Każda inna konfiguracja daje . Zatem:
Rozwiązanie IK to znalezienie minimum funkcji kosztu. Tylko że teraz stało się 6-wymiarowym wektorem , a krajobraz „gór" stał się 6-wymiarowy. Reszta logiki jest taka sama jak w 1D — schodzimy w dół iteracyjnie.
Solvery Jakobianowe z modułu 3 też minimalizują funkcję kosztu — tylko bardzo specyficznym sposobem (linearyzacja przez Jakobian). Tu rozszerzamy spojrzenie:
Cena: zazwyczaj wolniejsze niż DLS dla pojedynczego zapytania IK (dziesiątki–setki ms vs ~ms). Ale dla problemów z bogatymi ograniczeniami lub redundancją robota są bezkonkurencyjne.
Po tym wstępie możemy zapisać IK ścisle:
— funkcja kosztu (podobna do tej z modułu 3, ale może być bogatsza). s.t. (subject to) wprowadza ograniczenia: twarde dla limitów przegubów oraz ograniczenia ogólne (np. kolizje).
W tym module standardowa funkcja kosztu wygląda tak:
Trzy człony, każdy mierzący inną „karę":
Zalety tego sformułowania:
Pomysł intuicyjny: wyobraź sobie, że nie znasz gradientu — nie wiesz, w którą stronę idzie spadek. Możesz tylko zmierzyć wysokość w kilku punktach. Co robisz?
Postaw na terenie trójkąt (a w przestrzeni -wymiarowej — figurę z wierzchołków, zwaną simpleksem). Zmierz wysokość każdego rogu. Najgorszy róg (najwyżej położony) — odbij przez środek pozostałych. Jeśli nowy róg jest niższy niż wszystkie poprzednie — świetnie, idź dalej w tym kierunku. Jeśli nie pomogło — ścisnij trójkąt do wewnątrz. Iteruj.
To jest klasyczna metoda Nelder–Mead (1965). W każdej iteracji simpleks przesuwa się i deformuje, „pełzając" w kierunku minimum. Cztery podstawowe operacje:
Zalety: nie wymaga gradientów, więc działa nawet jeśli funkcja kosztu jest nie-gładka (np. ma sztywne progi kolizyjne, „ścianki" z penalty). Implementacja jest trywialna (kilkadziesiąt linii kodu).
Wady: brak teoretycznych gwarancji zbieżności dla funkcji niewypukłych. Dla wymiarów staje się powolne (simpleks ma punktów do utrzymania). Dla naszych 6 wymiarów IK jest jeszcze rozsądny.
// pseudo-Nelder-Mead
simplex = [x0, x0+e1·step, x0+e2·step, ..., x0+en·step];
while not converged:
sort(simplex by f(x)) // posortuj
centroid = mean(simplex except worst) // środek bez najgorszego
reflected = centroid + α·(centroid - worst)
if f(reflected) < f(best): try expansion
elif f(reflected) < f(second_worst): replace worst
elif f(reflected) < f(worst): contract
else: shrink toward bestFunkcja kosztu: — paraboloida asymetryczna, minimum w . Simpleks startuje w lewym dolnym rogu z trzech wierzchołków. Naciśnij ▶ odtwórz i obserwuj jak trójkąt „pełza" w stronę zielonego minimum, deformując się przy każdej iteracji.
Najlepszy wierzchołek: f(-2.500, -1.000) = 18.1250
Trójkąt fioletowy to aktualny simpleks (3 wierzchołki w 2D, w n-D byłoby n+1). Każda iteracja zastępuje najgorszy wierzchołek (czerwony) nowym punktem, generowanym przez jedną z czterech operacji: reflection, expansion, contraction, shrink. Zielona kropka po prawej to minimum globalne x* = (2, 1).
Najlepszy wierzchołek (zielony) nigdy się nie pogarsza — jest najmocniejszą gwarancją Nelder-Meada. Czerwony (najgorszy) jest zastępowany w każdym kroku. Kolejna ramka pokazuje, którą operację zastosowano: reflection dominuje na początku (długie skoki), contraction przy końcu (precyzyjne dopasowanie).
Pomysł intuicyjny: jeśli MOŻESZ obliczyć gradient (kierunek najszybszego wzrostu), to po prostu idź w przeciwną stronę. Każdy krok zmniejsza wysokość — przynajmniej jeśli krok nie jest za duży.
Klasyczna metoda Gradient Descent:
to wektor pochodnych cząstkowych — mówi, jak bardzo każdy z 6 przegubów zmienia funkcję kosztu (w otoczeniu aktualnej konfiguracji). Dla naszej funkcji kosztu:
gdzie to jakobian (z modułu 3), to wektor błędu pozy, — diagonalna macierz wag (pozycja vs orientacja).
Problem długości kroku: stała wartość nie zawsze działa — w stromych dolinach wystarczy mały krok, na płaskich równinach trzeba dużych skoków. Warunek Armijo rozwiązuje to adaptacyjnie:
Słownie: po zrobieniu kroku nowa wartość kosztu ma być wyraźnie mniejsza niż gdyby gradient był liniowy. Jeśli nie jest — zmniejszamy (typowo połowicznie:, ) i próbujemy ponownie. Jeśli jest — akceptujemy krok.
Obserwacja teoretyczna: dla naszego kosztu IK z (tylko pozycja, brak orientacji) i bez limitów, GD z Armijo pokrywa się z Jacobian Transpose z modułu 3 — to jest dokładnie ten sam algorytm, opisany z innej perspektywy. To jest piękny moment łączący moduły 3 i 4: różne tradycje matematyczne, ten sam algorytm.
Ograniczenie: w dolinach o nierównym uwarunkowaniu (długie i wąskie, jak w pobliżu singularności) GD zygzakuje i jest powolny. Lepsze metody korzystają z drugiego rzędu (Hessianu) — patrz SQP niżej.
Ta sama funkcja kosztu, co poprzednio. Suwakiem regulujesz długość kroku . Czerwona strzałka pokazuje krok , niebieska linia — historię iteracji:
Spróbuj różnych wartości α: 0.05 = bardzo wolne ale stabilne; 0.30 = sensowne; 0.50+ = oscylacje (kierunek y ma steeper gradient).
Aktualnie: x = (-2.000, -2.000),f(x) = 26.0000
∇f = (-4.000, -12.000), ‖∇f‖ = 12.649
Czerwona strzałka pokazuje krok x_k+1 = x_k − α·∇f(x_k). Niebieski ślad — historia iteracji. Zauważ: dolinę (wąską oś) GD zwykle pokonuje zygzakowato, a długą oś — wolno. To klasyczna patologia GD na funkcjach o nierównej skali.
Eksperymentuj z :
Ten zygzak to powód, dla którego w prawdziwych zastosowaniach używamy metod adaptacyjnych (Adam — patrz moduł 5) albo metod drugiego rzędu (Newton, BFGS — patrz SQP niżej).
Pomysł intuicyjny: GD robi „liniowe" przybliżenie funkcji kosztu (gradient = pochodna pierwszego rzędu). Newton robi „kwadratowe" (gradient + Hessian = pochodne drugiego rzędu).SQP idzie krok dalej: w każdej iteracji budujelokalny problem kwadratowy z liniowymi ograniczeniami i rozwiązuje go ściśle.
Dla każdej iteracji:
Zalety:
Wady: kosztowne (rozwiązanie QP per iteracja to ), wymaga biblioteki (np. SciPy minimize(method='SLSQP') czy trust-constr).
W aplikacji uruchamiamy SQP w przeglądarce przez Pyodide (SciPy skompilowany do WebAssembly — patrz moduł 1, sekcja „Ten sam solver, dwa języki"). W tym module używamy lekkich solverów TS (Nelder-Mead, GD) bez SQP — SQP wymagałby integracji z Pyodide, która jest poza zakresem tego modułu, ale dostępna w przyszłych rozszerzeniach.
Ta sama funkcja kosztu, ale teraz dodajemy twarde ograniczenie: — obszar nad czerwoną prostą jest zakazany. Minimum bez ograniczenia () leży powyżej linii — czyli w obszarze niedopuszczalnym. Algorytm musi znaleźć minimum na granicy:
Aktualnie: x = (-2.000, -2.000),f(x) = 26.0000
Czerwona strzałka = pełny kierunek −∇f (jaki zrobiłby zwykły GD). Po napotkaniu czerwonej linii (granica ograniczenia) fioletowa strzałka = kierunek po rzutowaniu gradientu na styczną do ograniczenia. Tak właśnie SQP/projected gradient utrzymuje punkt w obszarze dopuszczalnym. Minimum z ograniczeniem: (1.5, 0.75), leży na granicy (różne od minimum bez ograniczenia (2, 1)).
Co dokładnie pokazuje wizualizacja (uproszczona wersja SQP — projected gradient):
Minimum z ograniczeniem leży w — nie w ! To pokazuje, że ograniczenia zmieniają nie tylko trajektorię, ale i samo rozwiązanie. W IK robota twarde limity przegubowe potrafią kompletnie zmienić wybraną gałąź rozwiązania.
Trzy solvery startują z aktualnej konfiguracji głównego kontrolera i zmierzają do tej samej pozy . Obserwuj wykresy zbieżności (pionowa = funkcja kosztu w skali log) i porównaj liczbę iteracji oraz czas. Możesz manipulować wagami funkcji kosztu — zobacz, jak zmienia to dynamikę.
| metoda | iteracje | residuum | czas [ms] | status |
|---|
Każdy z trzech solverów startuje z tego samego seeda i zmierza do tej samej pozy docelowej (czerwona kropka). Pełna linia pokazuje ścieżkę TCP po kolejnych iteracjach — widać wprost, jak szybko i jaką drogąsolver dochodzi do celu. Szary „duch” to robot w końcowej konfiguracji znalezionej przez metodę.
trust-krylov, trust-ncg.W module 5 porzucamy klasyczny cyfrowy przepis i patrzymy, co stanie się, gdy IK chcemy wyuczyć na danych — od naiwnego MLP (który słabo radzi sobie z wielomodalnością 8 rozwiązań) po nowoczesne Invertible Neural Networks, samplujące pełny posterior. W module 6 wracamy do benchmarku, który zmierzy wszystkie solvery z modułów 1–5 na tym samym zbiorze testowych poz.