Przeszukiwanie przestrzeni stanów: BFS, DFS, UCS i A* z animacjami
Podstawy sztucznej inteligencji w przystępnej formie: czym jest przestrzeń stanów, jak działają BFS, DFS, UCS, przeszukiwanie zachłanne i A*, czym jest heurystyka i jak to się łączy z uczeniem maszynowym.
Seria Podstawy AI Część 21 z 52
- Mapa sztucznej inteligencji: co jest czym i jak to do siebie pasuje
- Krótka historia sztucznej inteligencji: od Turinga do modeli językowych
- Dane, model, trening, inferencja: podstawowe pojęcia na jednym przykładzie
- Jak czytać zapis matematyczny: symbole, indeksy, sumy i kwantyfikatory
- Zbiory i relacje: klocki, z których składają się definicje w AI
- Funkcje i odwzorowania: dlaczego każdy model AI jest funkcją
- Logika i dowody: reguły, według których rozumuje AI
- Grafy i drzewa: struktura, na której opierają się przeszukiwanie i sieci neuronowe
- Algebra liniowa dla uczenia maszynowego: wektory, macierze i dlaczego AI głównie mnoży
- Pochodne, gradient i reguła łańcuchowa: jak model ustala, w którą stronę się uczyć
- Spadek gradientu: jak uczy się model
- Czym jest prawdopodobieństwo: od rzutu kostką do aksjomatów Kołmogorowa
- Prawdopodobieństwo warunkowe i twierdzenie Bayesa: jak zmienić ocenę, gdy dowiesz się czegoś nowego
- Zmienna losowa i dystrybuanta: jedna funkcja, która mówi o rozkładzie wszystko
- Funkcja gęstości i funkcja prawdopodobieństwa: dlaczego zmienna ciągła ma w punkcie prawdopodobieństwo zero
- Wartość oczekiwana, wariancja i kowariancja: gdzie leży rozkład, jak jest szeroki i co z czym się wiąże
- Przegląd rozkładów prawdopodobieństwa: Bernoulliego, dwumianowy, Poissona, jednostajny, normalny i wykładniczy
- Prawo wielkich liczb i centralne twierdzenie graniczne: dlaczego średnie zachowują się tak przewidywalnie
- Statystyka i estymacja parametrów: największa wiarygodność, MAP i przedziały ufności
- Teoria informacji: entropia, entropia krzyżowa i dywergencja KL — skąd się biorą funkcje straty
- Przeszukiwanie przestrzeni stanów: BFS, DFS, UCS i A* z animacjami
- Gry i przeciwnik: minimax, przycinanie alfa-beta i Monte Carlo tree search
- Spełnianie ograniczeń: backtracking, propagacja ograniczeń i AC-3 na sudoku i mapie czeskich krajów
- Logika i systemy z bazą wiedzy: wnioskowanie w przód i wstecz, rezolucja i systemy ekspertowe
- Planowanie: STRIPS, świat klocków, heurystyki z relaksacji i planowanie w agentach AI
- Przeszukiwanie lokalne i symulowane wyżarzanie: komiwojażer przez czeskie miasta wojewódzkie
- Algorytmy genetyczne: populacja, krzyżowanie, mutacja i selekcja na plecaku na wycieczkę
- Strategie ewolucyjne i programowanie genetyczne: od reguły jednej piątej do prawa Keplera
- Optymalizacja kolonią mrówek: jak ślad feromonowy znajduje krótką drogę
- Optymalizacja rojem cząstek: bezwładność, własne doświadczenie i doświadczenie roju
- Neuroewolucja: ewolucja wag i kształtu sieci neuronowych oraz algorytm NEAT
- Zbiory rozmyte i funkcje przynależności: jak zapisać „trochę ciepło”
- Logika rozmyta a prawdopodobieństwo: dlaczego „trochę ciepło” nie znaczy „prawdopodobnie ciepło”
- Logika rozmyta: t-normy, implikacje i reguły JEŻELI–TO
- Sterowanie rozmyte: Mamdani i Sugeno od wentylatora po wahadło na wózku
- Klasteryzacja rozmyta: algorytm fuzzy c-means i punkty między skupieniami
- Systemy neuro-rozmyte (ANFIS): reguły rozmyte, które uczą się z danych
- Łańcuchy Markowa: macierz przejścia, rozkład stacjonarny i PageRank
- Ukryte modele Markowa: algorytm Viterbiego i nieuczciwe kasyno
- Sieci bayesowskie: dlaczego trawa jest mokra i co z tego wynika dla zraszacza
- Filtr Kalmana: jak z niedokładnych pomiarów oszacować, gdzie jest samochód i jak szybko jedzie
- Rodzaje uczenia maszynowego: nadzorowane, nienadzorowane, półnadzorowane i inne
- Prompty, kontekst i RAG: jak dać modelowi właściwe informacje
- Wariacyjne autoenkodery (VAE): nowe obrazki z ciągłej przestrzeni latentnej
- Generatywne sieci przeciwstawne (GAN): fałszerz kontra detektyw
- Modele dyfuzyjne: jak z czystego szumu powstaje obraz
- Procesy decyzyjne Markowa: stany, akcje, nagrody i równania Bellmana
- Q-learning i DQN: uczenie się wartości akcji od tabeli po sieć neuronową
- Policy gradient i PPO: uczenie się bezpośrednio polityki
- Jak działa agent AI: model językowy w pętli z narzędziami
- Programowanie z agentami AI: zadanie, testy i kontrola
- Ocena i bezpieczeństwo systemów AI: jak mierzyć i jak pilnować
Pełny program serii Gotowe: 52 z 83 tematów
- Jak formalnie zapisać zadanie jako szukanie ścieżki w grafie stanów.
- Czym różnią się BFS, DFS, UCS, przeszukiwanie zachłanne i A* — i dlaczego w gruncie rzeczy to jedna linijka kodu.
- Czym jest heurystyka, kiedy jest dopuszczalna i dlaczego dzięki niej A* znajduje najtańszą ścieżkę najmniejszym nakładem pracy.
Warto wiedzieć wcześniej: Mapa sztucznej inteligencji: co jest czym i jak to do siebie pasuje, Jak czytać zapis matematyczny: symbole, indeksy, sumy i kwantyfikatory
Kiedy dziś mówi się o sztucznej inteligencji, większość ludzi myśli o sieciach neuronowych i dużych modelach językowych. Jednak na długo przedtem, zanim maszyny zaczęły uczyć się z danych, potrafiły szukać. Nawigacja na mapie, ruch w szachach, plan robota w magazynie — wszystko to jest szukaniem drogi w przestrzeni możliwości. Przeszukiwanie przestrzeni stanów to pierwszy rozdział każdego podręcznika sztucznej inteligencji, a jego idee spotkasz też w uczeniu maszynowym: heurystyki, optymalizację, szukanie najlepszego rozwiązania.
Każde pojęcie znajdziesz tu dwa razy: najpierw ścisłą definicję matematyczną, a tuż pod nią to samo ludzkim językiem. Wszystkie animacje to prawdziwe działanie algorytmu — nic z tego nie jest rysowane ręcznie.
1. Czym jest przestrzeń stanów
Zanim zaczniemy czegokolwiek szukać, musimy opisać problem tak, żeby zrozumiał go komputer. Do tego służy problem przeszukiwania:
Definicja — problem przeszukiwania
Problem przeszukiwania to szóstka P = (S, s0, A, T, G, c), gdzie
- S jest zbiorem stanów,
- s0 ∈ S jest stanem początkowym,
- A(s) jest zbiorem akcji dostępnych w stanie s,
- T : S × A → S jest funkcją przejścia; stan s′ = T(s, a) jest następnikiem stanu s,
- G ⊆ S jest zbiorem stanów docelowych,
- c : S × A → ℝ+ jest kosztem akcji, zawsze dodatnim.
Stan to „migawka świata” w jednej chwili — gdzie stoi ludzik w labiryncie, jak leżą klocki łamigłówki. Akcja to ruch, który możesz wykonać. Funkcja przejścia mówi, dokąd ten ruch cię zaprowadzi. Cel to miejsce, do którego chcesz dotrzeć, a koszt mówi, ile kosztuje cię każdy ruch — czas, paliwo, kroki.
Definicja — ścieżka, rozwiązanie, optimum
Ścieżka długości k to ciąg s0, a1, s1, …, ak, sk, gdzie si = T(si−1, ai).
Koszt ścieżki to suma kosztów akcji: g = ∑i=1k c(si−1, ai).
Rozwiązanie to ścieżka, która kończy się w celu: sk ∈ G. Rozwiązanie optymalne to rozwiązanie o najmniejszym koszcie; ten koszt oznaczamy C*.
Ścieżka to lista ruchów wykonywanych po kolei. Rozwiązanie to ścieżka, która dociera do celu. Rozwiązanie optymalne to najtańsze ze wszystkich — i właśnie to zwykle chcemy znaleźć.
Stany połączone akcjami tworzą graf skierowany: stany są wierzchołkami, akcje krawędziami o wadze c. Szukać rozwiązania znaczy więc znaleźć w grafie ścieżkę z s0 do któregoś wierzchołka z G. Klasyczny przykład to łamigłówka 8 (ósemka): klocki 1–8 w ramce 3 × 3 i jedno puste miejsce. Stan to układ klocków, akcja to przesunięcie pustego miejsca, a cel to uporządkowane klocki:
Wygląda to niewinnie, ale łamigłówka 8 ma 181 440 osiągalnych stanów, a jej starszy brat, łamigłówka 15 (piętnastka), ponad dziesięć bilionów. Dlatego nigdy nie budujemy całego grafu z góry. Algorytm odkrywa go stopniowo — z funkcji przejścia oblicza zawsze tylko następników stanu, na którym akurat stoi.
2. Zadanie: droga przez labirynt
Wszystkie algorytmy pokażemy na tym samym zadaniu. Mamy labirynt 9 × 14 pól i chcemy dostać się ze startu do celu jak najtaniej. Najpierw, co oznacza które pole:
- Wolne pole — wejście na nie kosztuje 1
- Błoto — da się przez nie przejść, ale wejście kosztuje 5
- Ściana — nie można na nią wejść
- S Start — stan początkowy, pole (0, 0)
- G Cel (goal) — pole (8, 13)
- Zbadane pole — algorytm je rozwinął
- Znaleziona ścieżka — wynik algorytmu
Formalne sformułowanie zadania
Pole zapisujemy jako parę (r, s) — wiersz r ∈ {0, …, 8}, kolumna s ∈ {0, …, 13}. Niech Z oznacza zbiór ścian, a B zbiór pól z błotem.
- Stany: S = {(r, s) : (r, s) ∉ Z} — wszystkie pola oprócz ścian.
- Stan początkowy: s0 = (0, 0). Cel: G = {(8, 13)}.
- Akcje: A = {↑, →, ↓, ←}. Akcja jest dostępna w danym stanie tylko wtedy, gdy prowadzi na pole wewnątrz siatki, które nie jest ścianą.
- Przejście: T((r, s), →) = (r, s + 1), analogicznie dla pozostałych kierunków.
- Koszt: c(x, a) = 5, jeśli T(x, a) ∈ B, w przeciwnym razie 1 — płaci się za pole, na które wchodzimy.
- Zadanie: znaleźć rozwiązanie o najmniejszym koszcie C*.
Ludzik stoi w lewym górnym rogu i chce dotrzeć do prawego dolnego. Chodzi tylko w górę, w dół, w lewo i w prawo, przez ścianę nie przejdzie, a każdy krok kosztuje go 1 — oprócz kroku w błoto, który kosztuje 5. Szukamy ścieżki, która będzie go kosztować najmniej.
Błoto leży celowo na najprostszej trasie. Zobaczymy, że nie każdy algorytm je zauważy: niektóre liczą tylko kroki, inne rzeczywisty koszt.
3. Wspólny szkielet wszystkich algorytmów
Wszystkie algorytmy w tym artykule mają ten sam szkielet. Różnią się tylko jedną decyzją.
Definicja — brzeg, stany zamknięte, rozwinięcie
Brzeg (frontier) F ⊆ S to zbiór stanów, które algorytm już odkrył, ale jeszcze nie rozwinął.
Stany zamknięte E ⊆ S to stany już rozwinięte.
Rozwinąć stan s znaczy obliczyć wszystkich jego następników {T(s, a) : a ∈ A(s)} i tych, którzy nie są w E, dodać do F.
Brzeg to lista miejsc, o których wiesz, że prowadzi tam droga, ale jeszcze tam nie zajrzałeś. Stany zamknięte to miejsca, w których już byłeś — dzięki nim nie chodzisz w kółko. Rozwinąć stan znaczy stanąć na nim i rozejrzeć się, dokąd można iść dalej.
brzeg = [start]
zamkniete = {}
dopóki brzeg nie jest pusty:
stan = wybierz_z(brzeg) # ← tu algorytmy się różnią
jeśli stan jest celem: koniec
dodaj stan do zamkniete
dla każdego następnika stanu:
jeśli jeszcze nie jest zamknięty: dodaj go do brzegu
Cała różnica między algorytmami leży w jednej linijce: który stan z brzegu wybierzemy jako następny. Kolejka, stos albo kolejka priorytetowa według jakiejś liczby. Na tym opiera się cała reszta.
4. Przeszukiwanie wszerz (BFS)
Definicja — BFS
Brzeg F jest kolejką (FIFO): rozwija się stan, który trafił do F najwcześniej.
Wniosek: stany są rozwijane w kolejności głębokości d(s) — liczby akcji od s0. BFS znajduje więc rozwiązanie z najmniejszą liczbą akcji.
Jeśli b jest współczynnikiem rozgałęzienia (największą liczbą następników stanu), a d głębokością najpłytszego rozwiązania, BFS wygeneruje co najwyżej b + b2 + … + bd = O(bd) stanów i tyle samo trzyma w pamięci.
Kto pierwszy przyszedł, ten pierwszy obsłużony — jak kolejka w sklepie. Najpierw badasz wszystko jeden krok od startu, potem wszystko dwa kroki, potem trzy. Rozchodzi się to jak fala na wodzie, i dlatego pierwsza ścieżka, która dotrze do celu, ma najmniej kroków. Haczyk: całą falę musisz pamiętać, a ona rośnie strasznie szybko.
W labiryncie falę dobrze widać — rozlewa się we wszystkich kierunkach, aż natrafi na cel:
Zauważ jednak, że ścieżka prowadzi prosto przez błoto. BFS liczy akcje, nie koszt — dla niego krok w błoto jest taki sam jak krok po suchym. Najkrótsza ścieżka wyszła więc na 25 kroków, ale koszt 37. BFS jest optymalny tylko wtedy, gdy wszystkie akcje kosztują tyle samo.
from collections import deque
def bfs(start, goal, successors):
frontier = deque([start])
parent = {start: None}
while frontier:
state = frontier.popleft() # kolejka: pierwszy wchodzi, pierwszy wychodzi
if state == goal:
return path(parent, goal)
for nxt in successors(state):
if nxt not in parent:
parent[nxt] = state
frontier.append(nxt)
return None
def path(parent, state):
result = []
while state is not None:
result.append(state)
state = parent[state]
return result[::-1]
5. Przeszukiwanie w głąb (DFS)
Definicja — DFS
Brzeg F jest stosem (LIFO): rozwija się stan, który trafił do F najpóźniej.
Jeśli m jest największą głębokością przestrzeni stanów, DFS trzyma w pamięci tylko bieżącą gałąź i jej rodzeństwo: O(b · m) stanów. Czas w najgorszym przypadku wynosi O(bm).
DFS nie jest optymalny, a w nieskończonej przestrzeni nie jest nawet zupełny — może schodzić w dół w nieskończoność.
Jak wtedy, gdy w labiryncie idziesz cały czas z prawą ręką przy ścianie: wybierasz jeden korytarz i idziesz nim aż do końca. Dopiero w ślepym zaułku wracasz do ostatniego rozwidlenia i próbujesz innej drogi. Pamięci potrzeba mało — pamiętasz tylko ścieżkę, którą właśnie idziesz. Ale najkrótszą ścieżkę znajdziesz tak tylko przypadkiem.
W drzewie widać to po kolejności: A, B, D, H, I — i dopiero potem E. W labiryncie:
Tutaj DFS miał szczęście i dotarł do celu, nie badając wielu pól. Ścieżka jest jednak niepotrzebnie długa — 31 kroków zamiast 25 i koszt 59.
Definicja — iteracyjne pogłębianie (IDDFS)
Dla ℓ = 0, 1, 2, … uruchom DFS, który nie rozwija stanów głębszych niż ℓ. Zakończ przy pierwszym znalezionym rozwiązaniu.
Stany na głębokości i są generowane (d − i + 1) razy, łącznie ∑i=1d (d − i + 1) · bi = O(bd) — tak samo jak BFS, ale z pamięcią tylko O(b · d).
Uruchamiasz DFS, ale zabraniasz mu schodzić głębiej niż o jeden krok. Nic nie znalazł? To o dwa kroki, potem o trzy… Górne piętra przechodzisz raz za razem, ale większość stanów zawsze leży na tym najgłębszym, więc powtarzanie prawie nic nie kosztuje. Dostajesz najkrótszą ścieżkę jak w BFS i małą pamięć jak w DFS.
6. Przeszukiwanie według kosztu (UCS)
Definicja — UCS
Dla stanu n oznaczmy przez g(n) koszt najtańszej dotąd znalezionej ścieżki z s0 do n.
Brzeg jest kolejką priorytetową uporządkowaną według g: rozwija się stan o najmniejszym g(n).
Jeśli wszystkie koszty akcji są ≥ ε > 0, UCS rozwija stany w niemalejącej kolejności g, dlatego jest zupełny i optymalny. Liczba rozwiniętych stanów wynosi O(b1 + ⌊C*/ε⌋).
Zamiast kroków liczysz pieniądze. Zawsze kontynuujesz z miejsca, do którego dotąd dotarłeś najtaniej. Kiedy dojdziesz do celu, wiesz na pewno, że taniej się nie da — wszystko tańsze już zbadałeś. We wszystkich grafach z dodatnimi kosztami robi to znany algorytm Dijkstry; UCS to jego wersja, która zatrzymuje się w celu.
UCS ominął błoto. Znalazł ścieżkę o tej samej liczbie kroków co BFS, ale z kosztem 25 zamiast 37 — i jest ona na pewno najtańsza, czyli C* = 25. Płaci za to tym, że przeszukuje na ślepo we wszystkich kierunkach: nie wie, gdzie leży cel, więc rozchodzi się także w stronę od niego.
7. Heurystyka: oszacowanie, którędy do celu
Powyższe algorytmy są niepoinformowane — o celu wiedzą tylko tyle, że go rozpoznają, kiedy na niego wejdą. Algorytmy poinformowane dostają dodatkowo oszacowanie, ile jeszcze zostało.
Definicja — heurystyka
Oznaczmy przez h*(n) koszt optymalnej ścieżki ze stanu n do najbliższego celu. Heurystyka to funkcja h : S → ℝ≥0, która szacuje h*(n), przy czym h(n) = 0 dla n ∈ G.
h jest dopuszczalna (admissible), jeśli nigdy nie przeszacowuje: ∀n ∈ S : h(n) ≤ h*(n).
h jest spójna (konsystentna), jeśli dla każdego stanu n, akcji a i następnika n′ = T(n, a) zachodzi h(n) ≤ c(n, a) + h(n′). Każda spójna heurystyka jest dopuszczalna.
Heurystyka to rozsądne oszacowanie — jak wtedy, gdy w obcym mieście wiesz, że dworzec jest „gdzieś na północy”. Dopuszczalna znaczy optymistyczna: nigdy nie twierdzi, że jest dalej, niż jest naprawdę. Spójna znaczy, że oszacowanie po jednym kroku sobie nie przeczy — nie może się poprawić bardziej, niż zapłaciłeś za ten krok.
W naszym labiryncie nasuwa się odległość manhattańska — liczba pól w poziomie plus w pionie, jakbyś szedł prostopadłymi ulicami Manhattanu:
h((r, s)) = |r − rG| + |s − sG| = |r − 8| + |s − 13|
Jest spójna (a więc dopuszczalna): jedna akcja zmienia h co najwyżej o 1 i kosztuje co najmniej 1.
Liczysz, o ile pól cel jest niżej i o ile bardziej w prawo, i dodajesz to. Ściany i błoto mogą ścieżkę tylko wydłużyć albo podrożyć, nigdy skrócić — więc oszacowanie jest zawsze optymistyczne, dokładnie tak, jak powinno.
8. Przeszukiwanie zachłanne
Definicja — przeszukiwanie zachłanne
Brzeg jest kolejką priorytetową według f(n) = h(n): rozwija się stan, który jest według oszacowania najbliżej celu. Dotychczasowy koszt g(n) nie jest brany pod uwagę.
Przeszukiwanie zachłanne nie jest optymalne; z pilnowaniem powtórzonych stanów jest w skończonej przestrzeni zupełne.
Idziesz zawsze tam, gdzie wygląda najbliżej do celu, i w ogóle nie interesuje cię, ile ścieżka dotąd kosztowała. Szybko zmierza we właściwym kierunku, ale łatwo daje się nabrać — na przykład na błoto, które leży „we właściwym kierunku”.
Przeszukiwanie zachłanne bada mało pól, ale idzie tam, gdzie wygląda bliżej, i kończy na długiej i drogiej ścieżce — 31 kroków, koszt 59.
9. A*: to, co najlepsze z obu światów
Definicja — A*
Brzeg jest kolejką priorytetową według f(n) = g(n) + h(n) — oszacowania kosztu całej ścieżki od startu przez n do celu.
Twierdzenie. Jeśli h jest spójna, wartości f wzdłuż każdej ścieżki nie maleją, A* rozwija stany w niemalejącej kolejności f, a pierwszy rozwinięty stan docelowy leży na optymalnej ścieżce o koszcie C*. Rozwija przy tym każdy stan z f(n) < C* i żadnego z f(n) > C*.
Dla h ≡ 0 A* jest tym samym co UCS; bez g byłoby to przeszukiwanie zachłanne.
A* przy każdym miejscu sumuje, ile już zapłaciłeś i ile według oszacowania jeszcze zapłacisz. Dzięki pierwszej części nie da się skusić drogim skrótem, dzięki drugiej nie traci czasu na ścieżki, które prowadzą od celu. Kiedy oszacowanie jest optymistyczne, zawsze znajdzie najtańszą ścieżkę — i zbada przy tym tylko to, co zbadać musi.
Ten sam koszt 25 co UCS, ale A* zbadał 37 pól zamiast 49. W małym labiryncie to różnica kilku pól; na mapie z milionami skrzyżowań to różnica między natychmiastową odpowiedzią a minutą czekania. Dlatego A* (i jego warianty) znajdziesz w nawigacjach, w grach i w planowaniu ruchu robotów.
import heapq
def a_star(start, goal, successors, h):
frontier = [(h(start), 0, start)] # (f, g, stan)
g = {start: 0}
parent = {start: None}
while frontier:
_, cost, state = heapq.heappop(frontier)
if state == goal:
return path(parent, goal), cost
if cost > g[state]:
continue # nieaktualny wpis, znamy już tańszy
for nxt, step in successors(state):
new = cost + step
if new < g.get(nxt, float("inf")):
g[nxt] = new
parent[nxt] = state
heapq.heappush(frontier, (new + h(nxt), new, nxt))
return None
10. Porównanie algorytmów
Definicja — właściwości algorytmu
Algorytm jest zupełny, jeśli dla każdego problemu, który ma rozwiązanie, zwraca jakieś rozwiązanie.
Algorytm jest optymalny, jeśli zwrócone rozwiązanie ma koszt C*.
b — współczynnik rozgałęzienia, d — głębokość najpłytszego rozwiązania, m — największa głębokość przestrzeni. O(·) określa, jak czas lub pamięć rosną wraz z tymi parametrami.
Zupełny = jeśli ścieżka istnieje, znajdzie ją. Optymalny = znajdzie najtańszą. O(bd) znaczy „z każdym kolejnym krokiem w głąb praca się mnoży” — dlatego pamięć ma tak duże znaczenie.
| Algorytm | Zupełny | Optymalny | Czas | Pamięć | W naszym labiryncie |
|---|---|---|---|---|---|
| BFS | tak | przy równych kosztach | O(bd) | O(bd) | 75 pól, koszt 37 |
| DFS | nie* | nie | O(bm) | O(b·m) | 32 pola, koszt 59 |
| IDDFS | tak | przy równych kosztach | O(bd) | O(b·d) | — |
| UCS | tak | tak | O(b1+⌊C*/ε⌋) | O(b1+⌊C*/ε⌋) | 49 pól, koszt 25 |
| Zachłanne | nie* | nie | O(bm) | O(bm) | 33 pola, koszt 59 |
| A* | tak | tak** | zależnie od heurystyki | zależnie od heurystyki | 37 pól, koszt 25 |
* W skończonej przestrzeni z pilnowaniem powtórzonych stanów zawsze znajdą rozwiązanie. ** Ze spójną heurystyką.
11. Przeszukiwanie lokalne: kiedy nie chodzi o ścieżkę
W niektórych problemach ścieżka nas nie interesuje — chcemy tylko dobrego stanu końcowego. Planu lekcji, rozmieszczenia elementów na płytce, ustawienia parametrów.
Definicja — przeszukiwanie lokalne
Dana jest funkcja celu f : S → ℝ i sąsiedztwo N(s) ⊆ S każdego stanu. Szukamy s* = argmaxs ∈ S f(s).
Stan s jest maksimum lokalnym, jeśli f(s) ≥ f(s′) dla wszystkich s′ ∈ N(s), i maksimum globalnym, jeśli zachodzi to dla wszystkich s′ ∈ S.
Algorytm wspinaczkowy: powtarzaj s ← argmaxs′ ∈ N(s) f(s′), dopóki f rośnie.
Funkcja celu mówi, jak dobre jest rozwiązanie — im wyżej, tym lepiej. Sąsiedztwo to rozwiązania, które dostajesz przez małą zmianę. Wspinacz stoi we mgle na wzgórzu, widzi tylko kawałek wokół siebie i idzie zawsze tam, gdzie jest pod górę. Kiedy wszędzie dookoła teren opada, zatrzymuje się — nawet jeśli nieopodal stoi dużo wyższa góra.
Przeciw utknięciu stosuje się losowy restart (wypuszczenie wspinacza z różnych miejsc), algorytmy genetyczne, które krzyżują i mutują całą populację rozwiązań, albo symulowane wyżarzanie:
Symulowane wyżarzanie
Wybierz losowego sąsiada s′ ∈ N(s) i oblicz Δ = f(s′) − f(s). Jeśli Δ > 0, przejdź do s′; w przeciwnym razie przejdź z prawdopodobieństwem eΔ/T, gdzie „temperatura” T stopniowo spada do zera.
Na początku wspinacz od czasu do czasu pozwala sobie zejść też w dół — dzięki temu przedostaje się z małego szczytu na większy. Im dłużej szuka, tym jest ostrożniejszy, aż w końcu już tylko się wspina.
I tu koło zamyka się z uczeniem maszynowym. Trenowanie sieci neuronowej to przeszukiwanie lokalne w przestrzeni jej wag θ: spadek gradientu powtarza θ ← θ − η ∇L(θ) — krok o wielkości η w tym kierunku, w którym funkcja straty L spada najszybciej. To wspinacz odwrócony do góry nogami, który zamiast najwyższego szczytu szuka najniższej doliny — i zmaga się z tymi samymi problemami: z minimami lokalnymi i płaskowyżami.
12. Podsumowanie
- Problem przeszukiwania to szóstka (S, s0, A, T, G, c). Rozwiązanie to ścieżka w grafie stanów, rozwiązanie optymalne — najtańsze z nich.
- Algorytmy różnią się tylko tym, który stan wybierają z brzegu: kolejka (BFS), stos (DFS), najmniejsze g (UCS), najmniejsze h (zachłanne), najmniejsze g + h (A*).
- BFS znajduje najmniej kroków, UCS najniższy koszt, A* najniższy koszt, badając przy tym najmniej — o ile ma spójną heurystykę.
- Kiedy nie chodzi o ścieżkę, lecz o wynik, wkracza przeszukiwanie lokalne — a stąd już tylko krok do spadku gradientu i uczenia maszynowego.
Chcesz wypróbować algorytmy na własnej skórze? Poproś agenta AI o napisanie własnej wizualizacji — a potem poszukaj w kodzie tej jednej linijki, którą się od siebie różnią.
Masz inne zdanie? Napisz na support@coden-app.com.