Мурашиний алгоритм
Задача комівояджера
Постановка задачі
Задано орієнтований граф . Ребро графа характеризуються довжиною . Задано початкову вершину і цільову (кінцеву, фінальну) вершину .
Необхідно знайти найкоротший шлях із до .
Неформальний опис алгоритму
Розглянемо популяцію з мурах, які протягом ітерацій намагаються знайти найкоротший шлях (наприклад, шлях доставки листя до мурашника).
Уявімо, що на кожній ітерації кожна мурашка проходить якийсь шлях, залишаючи на своєму шляху феромени, і керуючись вже наявними із попередніх ітерацій фероменами для вибору шляху.
Код
Представлення графу у пам’яті програми:
g = {
A: {B: Edge(2), C: Edge(3), D: Edge(6)},
B: {E: Edge(4), F: Edge(5)},
C: {E: Edge(2), F: Edge(3)},
D: {E: Edge(5), F: Edge(2)},
E: {G: Edge(2)},
F: {G: Edge(1)},
G: {},
}
де ребро моделюєтсья наступним класом:
class Edge:
def __init__(self, length):
self.length, self.feroment, self.delta = length, 1, 0
Як бачимо, у ребра є довжина (length
), на ньому є певна інтенсивність фероменів (feroment
), і необхідне оновлення фероментів (delta
).
Ось так цей граф виглядає:
Клас, який моделює мурашку:
- конструктор:
class Ant:
def __init__(self, start, target):
self.tabu_list = []
self.vertice = start
self.target = target
self.path_length = 0
self.path = []
self.alive = True
Як ми бачимо з коду, кожна мурашка пам’ятає список вже пройдених вершин (tabu_list
), знає у якій вершині вона зараз знаходиться (vertice
), знає, куди їй треба йти (target
), підтримує у пам’яті загальну довжину пройденого шляху (path_length
), сам цей шлях (path
) і знає, чи вона «жива» (alive
). Мурашка вважається «мертвою», якщо вона не змогла дістатися мурашника.
- метод для вибору напрямку кроку:
def step(self):
# абсолютні характеристики привабливості дозволених напрямків
pre_probability = {
to : (g[self.vertice][to].feroment + \
1 / g[self.vertice][to].length)
for to in (set(g[self.vertice].keys()) - set(self.tabu_list))
}
# якщо мурашка не може нікуди йти то вона "мертва"
if not pre_probability:
self.alive = False
return
# нормалізуємо абсолютні привабливості до відносних
sum_pre_probability = sum(pre_probability.values())
probability = {
to : pre_probability[to] / sum_pre_probability
for to in pre_probability
}
# вибираємо напрямок кроку
choose_from, choice_probability = [], []
for t in probability:
choose_from.append(t)
choice_probability.append(probability[t])
step_to = choice(choose_from, p=choice_probability)
# опрацьовуємо крок
self.path_length += g[self.vertice][step_to].length
self.path.append((self.vertice, step_to))
self.tabu_list.append(self.vertice)
self.vertice = step_to
- метод для пошуку цілісного шляху:
def solve(self):
while self.vertice != self.target and self.alive:
self.step()
if self.alive:
for f, t in self.path:
g[f][t].delta += .05 * g[f][t].length / self.path_length**2
Програма-драйвер:
n, m = 1000, 1000
for i in range(m):
for j in range(n):
ant = Ant(start=START, target=END)
ant.solve()
for f in g:
for t in g[f]:
g[f][t].feroment, g[f][t].delta = \
.7 * g[f][t].feroment + g[f][t].delta, 0
Графіки
Інтенсивність фероменів від ітерації:
-
10 ітерацій:
-
100 ітерацій:
-
1000 ітерацій:
Швидкодія
Середній час виконання ітерації — 0.179 секунди, або 3 хвилини на 1000 ітерацій.
Зауважимо, що алгоритм багатоагентний і ідеально паралелиться, тому насправді нас цікавить час виконання однією мурахою однієї ітерації.
Нескладно зрозуміти, що час виконання однієї мурахо-ітерації мізерний, а саме 0.000179 секунди, тобто одна мурашка може виконати понад 5000 ітерацій за одну секунду.
На більшому графі (, ) швидкодія передбачувано знизиться, але все одно складе принаймні 100 ітерацій на секунду.
Тобто, маючи 16 логічних процесорів (а саме стільки їх у моєму ноутбуці) і розпаралеливши алгоритм можна розв’язати у 50 разів складнішу (і вже цілком реалістичну) задачу десь за 30 хвилин.
Непоганий результат, враховуючи що сама постановка задачі NP-повна.
Задача про ранець
Постановка задачі
Розглянемо відому задачу про ранець: є типів предметів, причому предметів -го типу рівно штук.
Предмет -го типу характеризується значеннями своєї корисності та ваги .
Окрім цього є ранець який вміщує довільну кількість предметів сумарною вагою не більше .
Необхідно вибрати підмножину заданих предметів яку можна розмістити у ранці, з максимальною сумарною корисністю.
Неформальний опис алгоритму
Розглянемо популяцію з мурах, які протягом ітерацій намагаються спакувати свій «ранець» (наприклад, підготувати запаси на зиму, які необхідно розмістити в обмеженому просторі мурашника).
Уявімо, що на кожній ітерації кожна мурашка пакує свій уявний ранець ходячи туди-сюди до потрібних їй предметів, залишаючи на своєму шляху феромени, і керуючись вже наявними із попередніх ітерацій фероменами для вибору шляху.
Звіт
Доступний до завантаження за посиланням
Код
-
Функція generate solution: python script
-
Функція calculate feroments: python script
-
Програма-драйвер: python script