Moduł 04 · numeryczne

Optymalizacja

Nelder–Mead, gradient descent, SQP z ograniczeniami, algorytmy ewolucyjne.

Czym właściwie jest optymalizacja?

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ę f(x)f(x), która każdej wartości xx przypisuje liczbę („wysokość", „koszt", „błąd"). Szukamy xx^*, dla którego ta liczba jest najmniejsza. Tę wartość xx^* nazywamy argumentem minimum:

x=argminxf(x)x^* = \arg\min_x f(x)

W jednym wymiarze wygląda to tak (zaczynamy z x0x_0 i schodzimy w dół):

x (parametr)f(x) = koszt012345f(x) = (x − 2)² + 0.4x*, minimumx0x1x2x3x4Optymalizacja = znajdź x*,dla którego f(x*) jest najmniejsze.Krok GD: xₖ₊₁ = xₖ − α·f′(xₖ)(„idź w dół zbocza")

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:

Gradient descent w 2D z liniami konturowymi
Klasyczna ilustracja gradient descent w 2D. Linie konturowe pokazują wartości funkcji kosztu, ścieżka iteracji prowadzi prostopadle do konturów ku minimum globalnemu.
Źródło: Wikimedia Commons · autor: Zerodamage / Oleg Alexandrov · licencja: Public Domain

Dla funkcji jednej zmiennej możemy często znaleźć minimum analitycznie, przyrównujemy pochodną do zera (f(x)=0f'(x) = 0) 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:

xk+1=xkαf(xk)x_{k+1} = x_k - \alpha\,f'(x_k)

α\alpha to długość kroku (rozmiar pojedynczej iteracji). Za małe, wolna zbieżność. Za duże, ryzyko przeskoczenia minimum i oscylacji. Strategie wyboru α\alpha to bogata część teorii optymalizacji (np. Armijo line search, którą zobaczysz niżej).

A co to ma wspólnego z IK?

W IK chcemy znaleźć kąty przegubów qq, dla których końcówka robota trafia w zadaną pozę TT^*. Możemy to sformułować jako optymalizację: zdefiniujmy funkcję kosztu J(q)J(q) mierzącą, jak bardzo robot się myli:

J(q)=p(q)p2+R(q)RF2J(q) = \|\mathbf{p}(q) - \mathbf{p}^*\|^2 + \|R(q) - R^*\|^2_F

(pierwsza składowa to kwadrat odległości pomiędzy aktualną a zadaną pozycją końcówki, druga, analogicznie dla orientacji). Wartość J(q)=0J(q) = 0 oznacza idealne trafienie w pozę. Każda inna konfiguracja daje J(q)>0J(q) > 0. Zatem:

q=argminqJ(q)IKq^* = \arg\min_q J(q) \quad\Longleftrightarrow\quad \text{IK}

Rozwiązanie IK to znalezienie minimum funkcji kosztu. Tylko że teraz xx stało się 6-wymiarowym wektorem q=(q1,,q6)q = (q_1, \dots, q_6), a krajobraz „gór" stał się 6-wymiarowy. Reszta logiki jest taka sama jak w 1D, schodzimy w dół iteracyjnie.

Dlaczego osobny moduł, skoro moduł 3 też tym jest?

Solvery Jakobianowe z modułu 3 też minimalizują funkcję kosztu, tylko bardzo specyficznym sposobem (linearyzacja przez Jakobian). Tu rozszerzamy spojrzenie:

  • Funkcja kosztu może być bogatsza. Nie tylko błąd pozy, możemy dodać kary za przekroczenie limitów przegubów, kary za bliskość singularności, kary za kolizje, kary za odległość od „neutralnej" pozycji robota. Każdy taki cel to dodatkowy człon w J(q)J(q).
  • Algorytmy są bardziej elastyczne. Nelder-Mead nie wymaga gradientu, działa nawet na funkcjach nieróżniczkowalnych (np. binarne „w kolizji?"). SQP narzuca twarde ograniczenia (nie jako kary, lecz jako ścisłe warunki). Algorytmy ewolucyjne eksplorują globalnie, nie lokalnie.
  • Mniej wrażliwe na singularności. Solvery optymalizacyjne nie odwracają Jakobianu jawnie, więc problemy z uwarunkowaniem nie eskalują tak gwałtownie.

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.

IK jako problem optymalizacji, formalnie

Po tym wstępie możemy zapisać IK ścisle:

q=argminqQ  J(q)s.t.qiminqiqimax,  gk(q)0q^* = \arg\min_{q \in Q} \; J(q) \quad\text{s.t.}\quad q_i^\text{min} \le q_i \le q_i^\text{max},\; g_k(q) \le 0

J(q)J(q), 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 gk(q)0g_k(q) \le 0 (np. kolizje).

W tym module standardowa funkcja kosztu wygląda tak:

J(q)=wpp(q)p2+woω(q,R)2+wimax(0,  qiqimax)2+max(0,  qiminqi)2J(q) = w_p\,\|\mathbf{p}(q) - \mathbf{p}^*\|^2 + w_o\,\|\boldsymbol{\omega}(q, R^*)\|^2 + w_\ell \sum_i \max(0,\; q_i - q_i^\text{max})^2 + \max(0,\; q_i^\text{min} - q_i)^2

Trzy człony, każdy mierzący inną „karę":

  • wpp(q)p2w_p\,\|\mathbf{p}(q) - \mathbf{p}^*\|^2, kara za błąd pozycji końcówki (wagi wpw_p)
  • woω2w_o\,\|\boldsymbol{\omega}\|^2, kara za błąd orientacji (waga wow_o)
  • wimax()2w_\ell \sum_i \max(\dots)^2, kara „miękka" za przekroczenie limitów (zero gdy w zakresie)

Zalety tego sformułowania:

  • Miękko narzucamy ograniczenia przegubowe (jako kara kwadratowa), w odróżnieniu od twardego odcięcia, które potrafi „zablokować" solver na granicy.
  • Dodawać drugorzędne cele: odległość od singularności (regularyzacja γlogdet(JJ)-\gamma\,\log\det(JJ^\top)), bliskość neutralnej pozycji, unikanie kolizji (pole odstraszające).
  • Łączyć priorytety: wpwow_p \gg w_o oznacza „za wszelką cenę osiągnij pozycję, orientacja, jeśli się uda". To zasadniczo inna filozofia niż analityczna IK, która traktuje pozycję i orientację równoprawnie.
Krok 1

Nelder–Mead, szukamy minimum bez gradientu

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 nn-wymiarowej, figurę z n+1n+1 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:

  1. Posortuj wierzchołki po wartości kosztu.
  2. Reflection: odbij najgorszy przez centroid pozostałych: xr=xˉ+α(xˉxworst)\mathbf{x}_r = \bar{\mathbf{x}} + \alpha (\bar{\mathbf{x}} - \mathbf{x}_\text{worst}).
  3. Expansion: jeśli odbity był wyjątkowo dobry, pójdź dalej w tym kierunku.
  4. Contraction: jeśli odbity nie pomógł, kontraktuj w stronę centroida.
  5. Shrink: jeśli nic nie działa, zwiń cały simpleks ku najlepszemu wierzchołkowi.

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 n>10n > 10 staje się powolne (simpleks ma n+1n+1 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 best

Wizualizacja na żywo, Nelder-Mead w 2D

Funkcja kosztu: f(x,y)=0.5(x2)2+2(y1)2f(x, y) = 0.5(x-2)^2 + 2(y-1)^2, paraboloida asymetryczna, minimum w (2,1)(2, 1). 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.

iteracja 0/23Start
-2024-2024x* (minimum)

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).

Krok 2

Gradient descent, schodzenie po zboczu

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:

qk+1=qkαJ(qk)q_{k+1} = q_k - \alpha\,\nabla J(q_k)

J(q)\nabla J(q) 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:

J(q)=2JWe(q)\nabla J(q) = -2\,J^\top\,\mathbf{W}\,\mathbf{e}(q)

gdzie JJ to jakobian (z modułu 3), e(q)\mathbf{e}(q) to wektor błędu pozy, W\mathbf{W} , diagonalna macierz wag (pozycja vs orientacja).

Problem długości kroku: stała wartość α\alpha 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:

J(q+αd)J(q)+c1αJ(q)dJ(q + \alpha\,\mathbf{d}) \le J(q) + c_1\,\alpha\,\nabla J(q)^\top \mathbf{d}

Słownie: po zrobieniu kroku nowa wartość kosztu ma być wyraźnie mniejsza niż gdyby gradient był liniowy. Jeśli nie jest, zmniejszamy α\alpha (typowo połowicznie:αβα\alpha \leftarrow \beta \alpha, β=0,5\beta = 0{,}5) i próbujemy ponownie. Jeśli jest, akceptujemy krok.

Obserwacja teoretyczna: dla naszego kosztu IK zwo=0w_o = 0 (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.

Wizualizacja na żywo, gradient descent w 2D

Ta sama funkcja kosztu, co poprzednio. Suwakiem regulujesz długość kroku α\alpha. Czerwona strzałka pokazuje krok αf-\alpha\nabla f, niebieska linia, historię iteracji:

iteracja 0/17
krok α =0.30

Spróbuj różnych wartości α: 0.05 = bardzo wolne ale stabilne; 0.30 = sensowne; 0.50+ = oscylacje (kierunek y ma steeper gradient).

-2024-2024x* (minimum)

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 α\alpha:

  • α=0,05\alpha = 0{,}05, bardzo wolne, ale stabilne. Każdy krok mały, do minimum potrzeba dziesiątek iteracji.
  • α=0,30\alpha = 0{,}30, sensowny kompromis. Kilkanaście iteracji wystarczy.
  • α=0,50+\alpha = 0{,}50+, krok za duży! Zauważ jak ścieżka zygzakuje między ścianami doliny. Oś y ma steeper gradient (współczynnik 4), więc duży krok przeskakuje przez minimum w pionie. Klasyczny przykład wrażliwości GD na uwarunkowanie funkcji kosztu.

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).

Krok 3

SQP, Sequential Quadratic Programming

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:

  1. Aproksymuj Hessian Lagrangianu (BFGS lub dokładny).
  2. Rozwiąż lokalny problem kwadratowy (QP) ze zlinearyzowanymi ograniczeniami.
  3. Wykonaj krok z line search spełniającym warunki KKT (Karusha–Kuhna–Tuckera).

Zalety:

  • Twarde ograniczenia, limity qiminqiqimaxq_i^{\text{min}} \le q_i \le q_i^{\text{max}} są zachowywane ściśle, nie jako kary.
  • Wykorzystuje krzywiznę, szybciej zbiega niż GD na trudnych funkcjach.
  • Konwergencja kwadratowa blisko optimum, liczba dokładnych cyfr podwaja się z iteracją.

Wady: kosztowne (rozwiązanie QP per iteracja to O(n3)O(n^3)), 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.

Wizualizacja na żywo, projected gradient z aktywnym ograniczeniem

Ta sama funkcja kosztu, ale teraz dodajemy twarde ograniczenie: g(x,y)=y+0,5x1,50g(x, y) = y + 0{,}5x - 1{,}5 \le 0, obszar nad czerwoną prostą jest zakazany. Minimum bez ograniczenia (2,12, 1) leży powyżej linii, czyli w obszarze niedopuszczalnym. Algorytm musi znaleźć minimum na granicy:

iteracja 0/21
-2024-2024x* (minimum)g(x,y) = y + 0.5x − 1.5 = 0obszar niedopuszczalny (g > 0)

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):

  • Czerwona prosta, granica ograniczenia g(x,y)=0g(x,y) = 0. Czerwony pasek nad nią, obszar niedopuszczalny.
  • Zwykły GD (czerwona strzałka) chciałby iść prosto na(2,1)(2, 1), ale wszedłby w obszar zakazany.
  • Po napotkaniu granicy krok zostaje zrzutowany na styczną do ograniczenia (fioletowa strzałka), usuwamy składową gradientu prostopadłą do granicy. Algorytm „ślizga się" wzdłuż linii.
  • Pełny SQP robi dokładnie to samo, ale używa Hessianu zamiast samego gradientu (krok kwadratowy). Trajektoria byłaby krótsza, ale charakter „ślizgania się po granicy" identyczny.

Minimum z ograniczeniem leży w (1,5, 0,75)(1{,}5,\ 0{,}75), nie w (2,1)(2, 1)! 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.

Porównanie na żywo

Trzy solvery startują z aktualnej konfiguracji głównego kontrolera i zmierzają do tej samej pozy TT^*. 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ę.

Konfiguracja przegubów

θ₁2.8°
θ₂-77.7°
θ₃6.8°
θ₄8.5°
θ₅-19.3°
θ₆172.0°

Poza efektora (T₀⁶)

Pozycja [m]
x = 0.5000
y = 0.1500
z = 0.3000
Orientacja RPY [°]
R = 0.00
P = 90.00
Y = 0.00

Poza docelowa T*

Pozycja [m]
Orientacja RPY [°]

Wagi w funkcji kosztu

1e-81e-71e-61e-51e-41e-31e-21e-11e+000111iteracjekoszt J(q)
metodaiteracjeresiduumczas [ms]status

Trajektorie końcówki w trakcie iteracji

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ę.

Kiedy używać optymalizacji zamiast Jakobiana?

  • Twarde ograniczenia przegubowe / kolizyjne, SQP, trust-region lub metoda kar skutecznie je obsługują; czysty DLS ignoruje.
  • Drugorzędne cele, odległość od singularności, bliskość neutralnej konfiguracji, minimalizacja zużycia energii; wchodzą do kosztu jako dodatkowe człony.
  • Manipulatory redundantne (n>6n > 6), niezerową przestrzeń zerową jakobianu (null-space) można zagospodarować dodatkowym kryterium.
  • Nieróżniczkowalne cele, np. binarne „czy w kolizji z tym obiektem". Nelder-Mead, CMA-ES, PSO, Symulowane Wyżarzanie.
  • Globalna wyszukiwarka rozwiązań, algorytmy ewolucyjne z restartami potrafią znajdować alternatywne gałęzie niedostępne dla metod lokalnych.

Wybrane strategie zaawansowane (sygnalnie)

  • CMA-ES, Covariance Matrix Adaptation Evolution Strategy (Hansen & Ostermeier). Stochastyczna, skuteczna dla n100n \le 100.
  • Trust-region, zamiast linii poszukiwania, adaptacyjny region zaufania dla kroku Newtona. SciPy trust-krylov, trust-ncg.
  • Interior-point, dla twardych ograniczeń nierównościowych; stosowane w optymalizacji trajektorii.
  • ADMM / rozwiązania rozdzielone, gdy koszt ma strukturę separowalną (pozycja + orientacja + limity).

Co dalej

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.