|
Расставляем корабли в игре Морской бой
Архив проекта: ga_8.zip
На этом занятии
мы рассмотрим еще один пример использования генетического алгоритма для
расстановки кораблей в игре «Морской бой». Наша цель разместить один
четырехпалубный, два трехпалубных, три двухпалубных и четыре однопалубных
кораблей на игровом поле так, чтобы они не соприкасались между собой:
Вначале нам
нужно определиться с форматом данных в хромосомах. Я закодировал их, следующим
образом:
Здесь x, y – положение
корабля (целое число от 1 до 10); p – ориентация
корабля (0 – по горизонтали; 1 – по вертикали). Общая длина хромосомы
составляет 10*3 = 30 генов.
Идея вычисления приспособленности особи
Следующим важным
шагом является вычисление значения функции приспособленности каждого
индивидуума. Идея расчета в нашем алгоритме будет следующей. Вначале сформируем
матрицу, в которой нули будут описывать игровое поле, а граничные значения 1000
– величину штрафа за расположение корабля за пределами игрового поля:
Например, добавляя
маску для четырехпалубного корабля, расположенного так, как показано на рисунке
ниже, мы получим значения в пределах игрового поля не более единицы, а за его
пределами – более 1000:
Если, затем,
добавить трехпалубный корабль с наложением на четырехпалубный, то в области
игрового поля будем получать величины больше единицы (см. второй рисунок).
Таким образом,
логика вычисления значения приспособленности особи, следующая. В игровой
области суммируем все величины больше 1, а за его пределами – все значения
более 1000. Общая сумма и будет определять приспособленность индивидуума. Можно
заметить, если корабли располагаются по правилам игры, то значение
приспособленности равно нулю. И возрастает, при увеличении числа ошибочных
расстановок. То есть, в процессе оптимизации нам нужно будет минимизировать
функцию принадлежности.
Реализация алгоритма на Python
Давайте теперь
реализуем этот алгоритм на Python с использованием
пакета DEAP. Вначале
выполним импорт необходимых пакетов и модулей:
from deap import base, algorithms
from deap import creator
from deap import tools
import algelitism
from graph_show import show_graph
import random
import matplotlib.pyplot as plt
import numpy as np
Затем, определим
глобальные параметры алгоритма и задачи:
POLE_SIZE = 10
SHIPS = 10
LENGTH_CHROM = 3*SHIPS # длина хромосомы, подлежащей оптимизации
# константы генетического алгоритма
POPULATION_SIZE = 50 # количество индивидуумов в популяции
P_CROSSOVER = 0.9 # вероятность скрещивания
P_MUTATION = 0.2 # вероятность мутации индивидуума
MAX_GENERATIONS = 50 # максимальное количество поколений
HALL_OF_FAME_SIZE = 1
hof = tools.HallOfFame(HALL_OF_FAME_SIZE)
RANDOM_SEED = 42
random.seed(RANDOM_SEED)
Следующим шагом
определим класс FitnessMin для работы со значением
приспособленности и класс Individual представления индивидуумов в
популяции:
creator.create("FitnessMin", base.Fitness, weights=(-1.0,))
creator.create("Individual", list, fitness=creator.FitnessMin)
Здесь все
стандартно и вес определен как -1, означающий, что нас будет интересовать
минимум функции приспособленности.
Затем, определим
функцию randomShip, которая будет
создавать начальных индивидуумов со случайными значениями генов:
def randomShip(total):
ships = []
for n in range(total):
ships.extend([random.randint(1, POLE_SIZE), random.randint(1, POLE_SIZE), random.randint(0, 1)])
return creator.Individual(ships)
Параметр total – это общее
число кораблей. В цикле для каждого корабля создаются три случайных значения:
координаты x, y и ориентация p.
Далее, мы
регистрируем функцию randomShip для создания индивидуума,
функцию populationCreator – для создания
популяции и создаем начальную популяцию размером POPULATION_SIZE:
toolbox = base.Toolbox()
toolbox.register("randomShip", randomShip, SHIPS)
toolbox.register("populationCreator", tools.initRepeat, list, toolbox.randomShip)
population = toolbox.populationCreator(n=POPULATION_SIZE)
Самое сложное в
этом алгоритме оказалось реализовать вычисление значения приспособленности
особи. У меня получилась вот такая функция с применением списков пакета numpy:
def shipsFitness(individual):
type_ship = [4, 3, 3, 2, 2, 2, 1, 1, 1, 1]
inf = 1000
P0 = np.zeros((POLE_SIZE, POLE_SIZE))
P = np.ones((POLE_SIZE+6, POLE_SIZE+6))*inf
P[1:POLE_SIZE+1, 1:POLE_SIZE+1] = P0
th = 0.2
h = np.ones((3, 6)) * th
ship_one = np.ones((1, 4))
v = np.ones((6, 3)) * th
for *ship, t in zip(*[iter(individual)] * 3, type_ship):
if ship[-1] == 0:
sh = np.copy(h[:, :t+2])
sh[1, 1:t+1] = ship_one[0, :t]
P[ship[0] - 1:ship[0] + 2, ship[1]-1:ship[1] + t+1] += sh
else:
sh = np.copy(v[:t+2, :])
sh[1:t+1, 1] = ship_one[0, :t]
P[ship[0]-1:ship[0] + t+1, ship[1] - 1:ship[1] + 2] += sh
s = np.sum(P[np.bitwise_and(P > 1, P < inf)])
s += np.sum(P[P > inf+th*4])
return s, # кортеж
В целом, здесь
все просто. Сначала создаем поле P с нулевыми значениями в области
размещения кораблей и величинами 1000 – за пределами игрового поля. Далее идут
вспомогательные матрицы h и v для формирования
масок кораблей, которые, затем, будут добавляться в игровое поле для
формирования числовых значений в соответствии с идеей вычисления
приспособленности особи. В цикле мы перебираем по три значения из хромосомы
индивидуума и по одному значению из списка type_ship. Этот список говорит,
какой тип корабля в данном случае добавляется в игровое поле. Затем, добавляем
маску корабля в соответствии с его ориентацией (вертикальная или
горизонтальная). После расстановки всех кораблей, мы подсчитываем сумму
величин, которые больше 1 и меньше 1000 – это для ошибок в пределах игрового
поля и сумму величин, которые больше 1000 + 0,2∙4 – это величина штрафа
при размещении кораблей за пределами игрового поля. Общая сумма, как раз, и
является значением приспособленности особи.
Дальше, проще.
Объявим еще одну функцию для выполнения мутации хромосомы:
def mutShips(individual, indpb):
for i in range(len(individual)):
if random.random() < indpb:
individual[i] = random.randint(0, 1) if (i+1) % 3 == 0 else random.randint(1, POLE_SIZE)
return individual,
Здесь координаты
могут меняться случайно в пределах от 1 до 10, а ориентация принимает значение
0 или 1.
Затем, регистрируем
все эти функции для выполнения в главном цикле ГА пакета DEAP:
toolbox.register("evaluate", shipsFitness)
toolbox.register("select", tools.selTournament, tournsize=3)
toolbox.register("mate", tools.cxTwoPoint)
toolbox.register("mutate", mutShips, indpb=1.0/LENGTH_CHROM)
stats = tools.Statistics(lambda ind: ind.fitness.values)
stats.register("min", np.min)
stats.register("avg", np.mean)
В качестве
отбора используется турнирный отбор, а в качестве скрещивания – двухточечное
скрещивание. Также определен экземпляр класса Statistics для сбора
статистики работы ГА.
Наконец,
запускаем алгоритм с помощью нашей функции eaSimpleElitism(), которая
реализует принцип элитизма – сохранения лучших индивидуумов в каждом поколении.
И в конце, считываем статистику, отображаем лучшего индивидуума и графики:
population, logbook = algelitism.eaSimpleElitism(population, toolbox,
cxpb=P_CROSSOVER,
mutpb=P_MUTATION,
ngen=MAX_GENERATIONS,
halloffame=hof,
stats=stats,
verbose=True)
maxFitnessValues, meanFitnessValues = logbook.select("min", "avg")
best = hof.items[0]
print(best)
plt.plot(maxFitnessValues, color='red')
plt.plot(meanFitnessValues, color='green')
plt.xlabel('Поколение')
plt.ylabel('Макс/средняя приспособленность')
plt.title('Зависимость максимальной и средней приспособленности от поколения')
plt.show()
После запуска
программы увидим, следующий график:
Как видим,
решение находится мгновенно. И это не удивительно, так как на поле 10х10
расставить корабли очень просто. Давайте усложним задачу и уменьшим поле до
размеров 7х7:
Здесь лучшего
решения достигнуто не было. Но, если увеличить размер популяции до 200 особей:
то решение
находится на 17-й итерации.
Отображение расположения кораблей
Графики кривых –
это, конечно, хорошо, но куда лучше было бы увидеть полученную расстановку. Для
этого в модуль graph_show.py я добавил
следующую функцию:
v_ship = np.array([0, 1, 2, 3])
h_ship = np.array([0, 0, 0, 0])
type_ship=[4, 3, 3, 2, 2, 2, 1, 1, 1, 1]
colors=['g', 'b', 'm', 'y']
def show_ships(ax, best, pole_size):
rect = Rectangle((0, 0), pole_size+1, pole_size+1, fill=None, edgecolor='r')
t_n = 0
for i in range(0, len(best), 3):
x = best[i]
y = best[i+1]
r = best[i+2]
t = type_ship[t_n]
t_n += 1
if r == 1:
ax.plot(v_ship[:t] + x, h_ship[:t] + y, ' sb', markersize=18, alpha=0.8, markerfacecolor=colors[t-1])
else:
ax.plot(h_ship[:t] + x, v_ship[:t] + y, ' sb', markersize=18, alpha=0.8, markerfacecolor=colors[t-1])
ax.add_patch(rect)
Далее, мы ее
импортируем в основном модуле программы:
from graph_show import show_graph, show_ships
И будем вызывать
на каждой итерации работы ГА, чтобы видеть лучший вариант расстановки. Вначале
определим уже знакомую нам функцию:
def show(ax):
ax.clear()
show_ships(ax, hof.items[0], POLE_SIZE)
plt.draw()
plt.gcf().canvas.flush_events()
Включим
интерактивный режим отображения данных, сформируем окно и координатные оси, а
также установим размер окна и границы осей:
plt.ion()
fig, ax = plt.subplots()
fig.set_size_inches(6, 6)
ax.set_xlim(-2, POLE_SIZE+3)
ax.set_ylim(-2, POLE_SIZE+3)
В функции eaSimpleElitism() укажем
параметр callback с функцией show и аргументом ax:
population, logbook = algelitism.eaSimpleElitism(population, toolbox,
cxpb=P_CROSSOVER,
mutpb=P_MUTATION,
ngen=MAX_GENERATIONS,
halloffame=hof,
stats=stats,
callback=(show, (ax, )),
verbose=True)
После выполнения
ГА выключим интерактивный режим и оставим окно с последним вариантом
расстановки кораблей на экране:
После выполнения
программы увидим следующий конечный результат:
Конечно, это
всего лишь пример того, как можно реализовать эту задачу с помощью ГА. Надеюсь,
после примеров с расстановкой кораблей и определением кратчайшего маршрута в
графе, вы стали лучше понимать, как конструируются генетические алгоритмы для
решения тех или иных задач.
|