Пошук у просторі станів: BFS, DFS, UCS і A* з анімаціями
Основи штучного інтелекту наочно: що таке простір станів, як працюють BFS, DFS, UCS, жадібний пошук і A*, що таке евристика і як із цим пов’язане машинне навчання.
Серія Основи ШІ Частина 21 з 52
- Мапа штучного інтелекту: що є що і як усе це пов’язано
- Коротка історія штучного інтелекту: від Тюрінга до мовних моделей
- Дані, модель, тренування, інференс: основні поняття на одному прикладі
- Як читати математичний запис: символи, індекси, суми та квантори
- Множини й відношення: конструктор, з якого складаються означення в ШІ
- Функції й відображення: чому кожна модель у ШІ — це функція
- Логіка й доведення: правила, за якими міркують у ШІ
- Графи й дерева: структура, на якій тримаються пошук і нейронні мережі
- Лінійна алгебра для машинного навчання: вектори, матриці й чому ШІ передусім множить
- Похідні, градієнт і правило ланцюжка: як модель дізнається, у якому напрямку вчитися
- Градієнтний спуск: як навчається модель
- Що таке ймовірність: від кидка кубика до аксіом Колмогорова
- Умовна ймовірність і теорема Баєса: як змінити оцінку, коли дізнаєтеся щось нове
- Випадкова величина і функція розподілу: одна функція, яка каже про розподіл усе
- Функція щільності та функція маси ймовірності: чому неперервна величина має в точці ймовірність нуль
- Математичне сподівання, дисперсія і коваріація: де лежить розподіл, наскільки він широкий і що з чим пов’язано
- Огляд розподілів ймовірностей: Бернуллі, біноміальний, Пуассона, рівномірний, нормальний і експоненційний
- Закон великих чисел і центральна гранична теорема: чому середні поводяться так передбачувано
- Статистика й оцінювання параметрів: максимальна правдоподібність, MAP і довірчі інтервали
- Теорія інформації: ентропія, перехресна ентропія і KL-дивергенція — звідки беруться функції втрат
- Пошук у просторі станів: BFS, DFS, UCS і A* з анімаціями
- Ігри та суперник: мінімакс, альфа-бета відсікання і Monte Carlo tree search
- Задоволення обмежень: бектрекінг, поширення обмежень і AC-3 на судоку та мапі чеських країв
- Логіка й системи, засновані на знаннях: пряме і зворотне виведення, резолюція та експертні системи
- Планування: STRIPS, світ кубиків, евристики з релаксації та планування в AI-агентах
- Локальний пошук та імітація відпалу: комівояжер через обласні центри Чехії
- Генетичні алгоритми: популяція, схрещування, мутація і добір на рюкзаку для походу
- Еволюційні стратегії і генетичне програмування: від правила однієї п’ятої до закону Кеплера
- Оптимізація мурашиною колонією: як феромоновий слід знаходить короткий шлях
- Оптимізація роєм частинок: інерція, власний досвід і досвід рою
- Нейроеволюція: еволюція ваг і форми нейронних мереж та алгоритм NEAT
- Нечіткі множини й функції належності: як записати «трохи тепло»
- Нечітка логіка та ймовірність: чому «трохи тепло» не означає «імовірно тепло»
- Нечітка логіка: t-норми, імплікації та правила ЯКЩО–ТО
- Нечітке керування: Мамдані та Суґено від вентилятора до маятника на візку
- Нечітка кластеризація: алгоритм fuzzy c-means і точки між кластерами
- Нейро-нечіткі системи (ANFIS): нечіткі правила, які навчаються з даних
- Марковські ланцюги: матриця переходів, стаціонарний розподіл і PageRank
- Приховані марковські моделі: алгоритм Вітербі й нечесне казино
- Байєсівські мережі: чому трава мокра і що з цього випливає для дощувача
- Фільтр Калмана: як за неточними вимірюваннями оцінити, де авто і як швидко воно їде
- Види машинного навчання: з учителем, без учителя, з частковим залученням учителя та інші
- Промпти, контекст і RAG: як дати моделі правильну інформацію
- Варіаційні автокодувальники (VAE): нові зображення з неперервного латентного простору
- Генеративні змагальні мережі (GAN): фальшувальник проти детектива
- Дифузійні моделі: як із чистого шуму постає зображення
- Марковські процеси ухвалення рішень: стани, дії, винагороди й рівняння Беллмана
- Q-навчання і DQN: як навчитися цінності дій — від таблиці до нейронної мережі
- Policy gradient і PPO: навчання безпосередньо політики
- Як працює ШІ-агент: мовна модель у циклі з інструментами
- Програмування з AI-агентами: завдання, тести й перевірка
- Оцінювання і безпека систем ШІ: як вимірювати і як стежити
Повна програма серії Готово: 52 з 83 тем
- Як формально записати задачу як пошук шляху в графі станів.
- Чим відрізняються BFS, DFS, UCS, жадібний пошук і A* — і чому по суті це один-єдиний рядок коду.
- Що таке евристика, коли вона допустима і чому з нею A* знаходить найдешевший шлях із найменшими зусиллями.
Що варто знати заздалегідь: Мапа штучного інтелекту: що є що і як усе це пов’язано, Як читати математичний запис: символи, індекси, суми та квантори
Коли сьогодні говорять про штучний інтелект, більшість людей уявляє нейронні мережі та великі мовні моделі. Але задовго до того, як машини почали вчитися на даних, вони вміли шукати. Навігація на мапі, хід у шахах, план робота на складі — усе це пошук шляху в просторі можливостей. Пошук у просторі станів — перший розділ будь-якого підручника зі штучного інтелекту, і його ідеї ви зустрінете й у машинному навчанні: евристики, оптимізацію, пошук найкращого розв’язку.
Кожне поняття ви знайдете тут двічі: спершу точне математичне означення, а одразу під ним те саме простими словами. Усі анімації — це справжнє виконання алгоритму, нічого з цього не намальовано вручну.
1. Що таке простір станів
Перш ніж щось шукати, треба описати задачу так, щоб її зрозумів комп’ютер. Для цього слугує задача пошуку:
Означення — задача пошуку
Задача пошуку — це шістка P = (S, s0, A, T, G, c), де
- S — множина станів,
- s0 ∈ S — початковий стан,
- A(s) — множина дій, застосовних у стані s,
- T : S × A → S — функція переходу; стан s′ = T(s, a) — наступник стану s,
- G ⊆ S — множина цільових станів,
- c : S × A → ℝ+ — ціна дії, завжди додатна.
Стан — це «знімок світу» в один момент: де стоїть чоловічок у лабіринті, як лежать фішки головоломки. Дія — це хід, який ви можете зробити. Функція переходу каже, куди вас цей хід приведе. Ціль — те, куди ви хочете потрапити, а ціна каже, скільки вам коштує кожен хід: час, пальне, кроки.
Означення — шлях, розв’язок, оптимум
Шлях довжини k — це послідовність s0, a1, s1, …, ak, sk, де si = T(si−1, ai).
Ціна шляху — це сума цін дій: g = ∑i=1k c(si−1, ai).
Розв’язок — це шлях, що закінчується в цілі: sk ∈ G. Оптимальний розв’язок — розв’язок із найменшою ціною; її позначають C*.
Шлях — це список ходів, що йдуть один за одним. Розв’язок — шлях, який доходить до цілі. Оптимальний розв’язок — найдешевший з усіх, і саме його ми зазвичай хочемо знайти.
Стани, з’єднані діями, утворюють орієнтований граф: стани — це вершини, дії — ребра з вагою c. Шукати розв’язок означає знайти в графі шлях з s0 до якоїсь вершини з G. Класичний приклад — головоломка 8: фішки 1–8 у рамці 3 × 3 і одна порожня клітинка. Стан — це розташування фішок, дія — зсув порожньої клітинки, а ціль — впорядковані фішки:
Виглядає невинно, але головоломка 8 має 181 440 досяжних станів, а її старша сестра, головоломка 15, — понад десять трильйонів. Тому граф ніколи не будують повністю наперед. Алгоритм відкриває його поступово — з функції переходу щоразу обчислює лише наступників стану, на якому саме стоїть.
2. Задача: шлях лабіринтом
Усі алгоритми покажемо на одній і тій самій задачі. Маємо лабіринт 9 × 14 клітинок і хочемо дістатися від старту до цілі якомога дешевше. Спершу — що означає кожна клітинка:
- Вільна клітинка — вхід на неї коштує 1
- Болото — пройти можна, але вхід коштує 5
- Стіна — на неї ступити не можна
- S Старт (start) — початковий стан, клітинка (0, 0)
- G Ціль (goal) — клітинка (8, 13)
- Досліджена клітинка — алгоритм її розгорнув
- Знайдений шлях — результат алгоритму
Формальна постановка
Клітинку записуємо як пару (r, s) — рядок r ∈ {0, …, 8}, стовпець s ∈ {0, …, 13}. Позначимо Z множину стін і B множину клітинок із болотом.
- Стани: S = {(r, s) : (r, s) ∉ Z} — усі клітинки, крім стін.
- Початковий стан: s0 = (0, 0). Ціль: G = {(8, 13)}.
- Дії: A = {↑, →, ↓, ←}. Дія застосовна в стані, лише якщо веде на клітинку всередині сітки, яка не є стіною.
- Перехід: T((r, s), →) = (r, s + 1), аналогічно для інших напрямків.
- Ціна: c(x, a) = 5, якщо T(x, a) ∈ B, інакше 1 — платимо за клітинку, на яку ступаємо.
- Завдання: знайти розв’язок із найменшою ціною C*.
Чоловічок стоїть угорі ліворуч і хоче потрапити вниз праворуч. Ходить лише вгору, вниз, ліворуч і праворуч, крізь стіну не пройде, і кожен крок коштує йому 1 — крім кроку в болото, який коштує 5. Шукаємо шлях, що обійдеться йому найдешевше.
Болото лежить навмисно на найпрямішому маршруті. Побачимо, що не кожен алгоритм його помітить: одні рахують лише кроки, інші — справжню ціну.
3. Спільний кістяк усіх алгоритмів
Усі алгоритми в статті мають однаковий кістяк. Відрізняються вони лише одним рішенням.
Означення — межа, закриті стани, розгортання
Межа (frontier) F ⊆ S — множина станів, які алгоритм уже відкрив, але ще не розгорнув.
Закриті стани E ⊆ S — стани, які вже розгорнуто.
Розгорнути стан s означає обчислити всіх його наступників {T(s, a) : a ∈ A(s)} і ті, яких немає в E, додати до F.
Межа — це список місць, про які ви знаєте, що туди веде шлях, але ще туди не зазирнули. Закриті стани — місця, де ви вже побували: завдяки їм ви не ходите по колу. Розгорнути стан означає стати на нього й роззирнутися, куди можна йти далі.
frontier = [start]
closed = {}
поки frontier не порожня:
state = choose_from(frontier) # ← саме тут алгоритми відрізняються
якщо state — ціль: готово
додати state до closed
для кожного наступника state:
якщо він ще не закритий: додати його до frontier
Уся різниця між алгоритмами — в одному рядку: який стан із межі вибрати наступним. Черга, стек чи черга з пріоритетом за якимось числом. На цьому тримається все інше.
4. Пошук у ширину (BFS)
Означення — BFS
Межа F — це черга (FIFO): розгортається стан, який потрапив до F найраніше.
Наслідок: стани розгортаються в порядку глибини d(s) — кількості дій від s0. Тому BFS знаходить розв’язок із найменшою кількістю дій.
Якщо b — коефіцієнт розгалуження (найбільша кількість наступників стану), а d — глибина найменш глибокого розв’язку, BFS згенерує щонайбільше b + b2 + … + bd = O(bd) станів і стільки ж тримає в пам’яті.
Хто прийшов першим, той першим і йде — як черга в магазині. Спершу ви досліджуєте все на відстані одного кроку від старту, потім усе на відстані двох кроків, потім трьох. Це поширюється, як хвиля на воді, і тому перший шлях, що дістанеться цілі, має найменше кроків. Заковика: всю хвилю треба пам’ятати, а вона росте страшенно швидко.
У лабіринті хвилю добре видно — вона розтікається в усіх напрямках, доки не натрапить на ціль:
Але зверніть увагу, що шлях веде прямо через болото. BFS рахує дії, а не ціну — для нього крок болотом такий самий, як крок по сухому. Найкоротший шлях вийшов у 25 кроків, але з ціною 37. BFS оптимальний, лише коли всі дії мають однакову ціну.
from collections import deque
def bfs(start, goal, successors):
frontier = deque([start])
parent = {start: None}
while frontier:
state = frontier.popleft() # черга: перший увійшов — перший вийшов
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. Пошук у глибину (DFS)
Означення — DFS
Межа F — це стек (LIFO): розгортається стан, який потрапив до F останнім.
Якщо m — найбільша глибина простору станів, DFS тримає в пам’яті лише поточну гілку та її сусідні вузли: O(b · m) станів. Час у найгіршому випадку — O(bm).
DFS не оптимальний, а в нескінченному просторі навіть не повний — він може спускатися до нескінченності.
Як коли в лабіринті ви весь час ідете, тримаючись правою рукою за стіну: обираєте один коридор і йдете ним до кінця. Лише в глухому куті повертаєтеся до останнього розгалуження і пробуєте інший. Пам’яті потрібно мало — ви пам’ятаєте лише шлях, яким саме йдете. Але найкоротший шлях так знайдете хіба що випадково.
У дереві це видно за порядком: A, B, D, H, I — і лише потім E. У лабіринті:
Тут DFS пощастило, і він дістався до цілі, не дослідивши багато клітинок. Але шлях зайво довгий — 31 крок замість 25 і ціна 59.
Означення — ітеративне поглиблення (IDDFS)
Для ℓ = 0, 1, 2, … запусти DFS, який не розгортає стани, глибші за ℓ. Зупинись на першому знайденому розв’язку.
Стани на глибині i генеруються (d − i + 1) разів, разом ∑i=1d (d − i + 1) · bi = O(bd) — так само, як у BFS, але з пам’яттю лише O(b · d).
Ви запускаєте DFS, але забороняєте йому йти глибше ніж на один крок. Нічого не знайшов? Тоді на два кроки, потім на три… Верхні рівні ви проходите знову і знову, але більшість станів завжди лежить на найглибшому рівні, тож повторення майже нічого не коштує. Ви отримуєте найкоротший шлях, як у BFS, і малу пам’ять, як у DFS.
6. Пошук за ціною (UCS)
Означення — UCS
Для стану n позначимо g(n) ціну найдешевшого досі знайденого шляху з s0 до n.
Межа — це черга з пріоритетом, упорядкована за g: розгортається стан із найменшим g(n).
Якщо всі ціни дій ≥ ε > 0, UCS розгортає стани в неспадному порядку g, тому він повний і оптимальний. Кількість розгорнутих станів — O(b1 + ⌊C*/ε⌋).
Замість кроків ви рахуєте гроші. Ви завжди продовжуєте з місця, куди досі дісталися найдешевше. Коли дійдете до цілі, знаєте напевно, що дешевше не вийде — усе дешевше ви вже дослідили. У всіх графах із додатними цінами це робить відомий алгоритм Дейкстри; UCS — його версія, яка зупиняється в цілі.
UCS обійшов болото. Він знайшов шлях із такою самою кількістю кроків, як BFS, але з ціною 25 замість 37 — і він гарантовано найдешевший, тобто C* = 25. Платить за це тим, що шукає наосліп в усі боки: не знає, де ціль, тож поширюється і в протилежному напрямку.
7. Евристика: оцінка, куди йти до цілі
Алгоритми вище — неінформовані: про ціль вони знають лише те, що впізнають її, коли на неї ступлять. Інформовані алгоритми отримують на додачу оцінку, скільки ще лишилося.
Означення — евристика
Позначимо h*(n) ціну оптимального шляху зі стану n до найближчої цілі. Евристика — це функція h : S → ℝ≥0, яка оцінює h*(n), причому h(n) = 0 для n ∈ G.
h допустима (admissible), якщо ніколи не переоцінює: ∀n ∈ S : h(n) ≤ h*(n).
h узгоджена (consistent), якщо для кожного стану n, дії a і наступника n′ = T(n, a) виконується h(n) ≤ c(n, a) + h(n′). Кожна узгоджена евристика допустима.
Евристика — це обґрунтована здогадка, як коли в чужому місті ви знаєте, що вокзал «десь на півночі». Допустима означає оптимістична: вона ніколи не стверджує, що далі, ніж є насправді. Узгоджена означає, що оцінка після одного кроку не суперечить сама собі — вона не може покращитися більше, ніж ви за цей крок заплатили.
У нашому лабіринті напрошується манхеттенська відстань — кількість клітинок по горизонталі плюс по вертикалі, як коли ви йдете прямокутними вулицями Манхеттена:
h((r, s)) = |r − rG| + |s − sG| = |r − 8| + |s − 13|
Вона узгоджена (а отже, допустима): одна дія змінює h щонайбільше на 1 і коштує щонайменше 1.
Порахуйте, на скільки клітинок ціль нижче і на скільки правіше, і додайте. Стіни й болото шлях лише подовжують або здорожчують, але ніколи не скорочують — тож оцінка завжди оптимістична, саме так, як має бути.
8. Жадібний пошук
Означення — жадібний пошук
Межа — це черга з пріоритетом за f(n) = h(n): розгортається стан, який за оцінкою найближчий до цілі. Накопичену ціну g(n) не враховують.
Жадібний пошук не оптимальний; з контролем повторних станів він повний у скінченному просторі.
Ви завжди йдете туди, де здається найближче до цілі, і зовсім не зважаєте, скільки вам досі коштувала дорога. Це швидко прямує в правильному напрямку, але його легко обдурити — наприклад, болотом, яке лежить «у правильному напрямку».
Жадібний пошук досліджує мало клітинок, але йде туди, де здається ближче, і опиняється на довгому й дорогому шляху — 31 крок, ціна 59.
9. A*: найкраще з обох світів
Означення — A*
Межа — це черга з пріоритетом за f(n) = g(n) + h(n) — оцінкою ціни всього шляху від старту через n до цілі.
Теорема. Якщо h узгоджена, значення f уздовж кожного шляху не спадають, A* розгортає стани в неспадному порядку f, і перший розгорнутий цільовий стан лежить на оптимальному шляху з ціною C*. При цьому він розгортає кожен стан із f(n) < C* і жодного з f(n) > C*.
Для h ≡ 0 A* — те саме, що UCS; без g це був би жадібний пошук.
Для кожного місця A* додає, скільки ви вже заплатили і скільки, за оцінкою, ще заплатите. Завдяки першій частині він не спокушається дорогим скороченням, завдяки другій не гає часу на шляхи, що ведуть від цілі. Якщо оцінка оптимістична, він завжди знайде найдешевший шлях — і дослідить лише те, що дослідити мусить.
Та сама ціна 25, що й в UCS, але A* дослідив 37 клітинок замість 49. У маленькому лабіринті це різниця в кілька клітинок; на мапі з мільйонами перехресть — різниця між миттєвою відповіддю і хвилиною очікування. Тому A* (і його варіанти) ви знайдете в навігаторах, в іграх і в плануванні руху роботів.
import heapq
def a_star(start, goal, successors, h):
frontier = [(h(start), 0, start)] # (f, g, стан)
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 # застарілий запис, ми вже знаємо дешевший
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. Порівняння алгоритмів
Означення — властивості алгоритму
Алгоритм повний, якщо для кожної задачі, що має розв’язок, він повертає якийсь розв’язок.
Алгоритм оптимальний, якщо повернений розв’язок має ціну C*.
b — коефіцієнт розгалуження, d — глибина найменш глибокого розв’язку, m — найбільша глибина простору. O(·) показує, як час або пам’ять зростають із цими параметрами.
Повний = якщо шлях існує, знайде його. Оптимальний = знайде найдешевший. O(bd) означає «з кожним наступним кроком углиб робота множиться» — тому пам’ять має таке велике значення.
| Алгоритм | Повний | Оптимальний | Час | Пам’ять | У нашому лабіринті |
|---|---|---|---|---|---|
| BFS | так | за однакових цін | O(bd) | O(bd) | 75 клітинок, ціна 37 |
| DFS | ні* | ні | O(bm) | O(b·m) | 32 клітинки, ціна 59 |
| IDDFS | так | за однакових цін | O(bd) | O(b·d) | — |
| UCS | так | так | O(b1+⌊C*/ε⌋) | O(b1+⌊C*/ε⌋) | 49 клітинок, ціна 25 |
| Жадібний | ні* | ні | O(bm) | O(bm) | 33 клітинки, ціна 59 |
| A* | так | так** | залежить від евристики | залежить від евристики | 37 клітинок, ціна 25 |
* У скінченному просторі з контролем повторних станів розв’язок знаходять завжди. ** З узгодженою евристикою.
11. Локальний пошук: коли йдеться не про шлях
У деяких задачах шлях нас не цікавить — нам потрібен лише добрий кінцевий стан. Розклад занять, розміщення компонентів на платі, налаштування параметрів.
Означення — локальний пошук
Задано цільову функцію f : S → ℝ і окіл N(s) ⊆ S кожного стану. Шукаємо s* = argmaxs ∈ S f(s).
Стан s — локальний максимум, якщо f(s) ≥ f(s′) для всіх s′ ∈ N(s), і глобальний максимум, якщо це виконується для всіх s′ ∈ S.
Алгоритм сходження на пагорб: повторюй s ← argmaxs′ ∈ N(s) f(s′), доки f зростає.
Цільова функція каже, наскільки добрий розв’язок — що вище, то краще. Окіл — це розв’язки, які ви отримаєте невеликою зміною. Альпініст стоїть у тумані на пагорбі, бачить лише трохи навколо себе і йде завжди туди, де підйом. Коли довкола все йде вниз, він зупиняється — навіть якщо неподалік височіє значно вища гора.
Щоб не застрягти, використовують випадковий перезапуск (запускати альпініста з різних місць), генетичні алгоритми, які схрещують і мутують цілу популяцію розв’язків, або імітацію відпалу:
Імітація відпалу
Вибери випадкового сусіда s′ ∈ N(s) і обчисли Δ = f(s′) − f(s). Якщо Δ > 0, перейди до s′; інакше перейди з імовірністю eΔ/T, де «температура» T поступово спадає до нуля.
Спочатку альпініст часом дозволяє собі піти й униз — так він переходить із малої вершини на більшу. Чим довше шукає, тим обережнішим стає, аж поки врешті лише підіймається.
І тут коло замикається з машинним навчанням. Навчання нейронної мережі — це локальний пошук у просторі її ваг θ: градієнтний спуск повторює θ ← θ − η ∇L(θ) — крок розміром η у тому напрямку, в якому функція помилки L спадає найшвидше. Це альпініст, перевернутий догори ногами, який замість найвищої вершини шукає найнижчу долину, — і він стикається з тими самими проблемами: локальними мінімумами та плато.
12. Підсумок
- Задача пошуку — це шістка (S, s0, A, T, G, c). Розв’язок — це шлях у графі станів, оптимальний розв’язок — найдешевший із них.
- Алгоритми відрізняються лише тим, який стан вибирають із межі: черга (BFS), стек (DFS), найменше g (UCS), найменше h (жадібний), найменше g + h (A*).
- BFS знаходить найменше кроків, UCS — найнижчу ціну, A* — найнижчу ціну, досліджуючи при цьому найменше, якщо має узгоджену евристику.
- Коли йдеться не про шлях, а про результат, у гру вступає локальний пошук — а від нього лише крок до градієнтного спуску та машинного навчання.
Хочете помацати алгоритми власноруч? Попросіть ШІ-агента написати власну візуалізацію — а потім шукайте в коді той єдиний рядок, яким вони відрізняються.
Маєте іншу думку? Напишіть на support@coden-app.com.