476 KiB
Лаб 10. Генетический алгоритм. Погнали
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import random
# Загружаем данные
df = pd.read_csv('..//static//csv//city.csv')
print(df.columns)
print("Список городов:")
df.head()
Генетический алгоритм
Я поставил цель реализовать генетический алгоритм для решения задачи коммивояжёра (TSP) для построения маршрутов между городами по России.
Важные характиристи:¶
- Координаты городов (Широта и долгота) для расчёта расстояния
- Название самих городов через которые будет идти путь
- Население и год основания, чтобы посещать исторически значимые и крупные города.
Найдем лучшие города, которые стоит посетить
# Преобразуем годы основания и население в числовой тип
df["foundation_year"] = pd.to_numeric(df["foundation_year"], errors="coerce")
df["population"] = pd.to_numeric(df["population"], errors="coerce")
# Задаём нужные округа
target_districts = ["Северо-Западный", "Центральный", "Приволжский", "Южный"]
# Фильтрация по году основания, населению и федеральному округу
historic_large_cities = df[
(df["foundation_year"] < 1800) &
(df["population"] > 500_000) &
(df["federal_district"].isin(target_districts))
].copy()
# Удалим дубликаты по названию города
historic_large_cities = historic_large_cities.drop_duplicates(subset=["region"])
# Сортировка по населению
historic_large_cities = historic_large_cities.sort_values("population", ascending=False)
# Сброс индекса
historic_large_cities = historic_large_cities.reset_index(drop=True)
# Вывод нужных столбцов
selected_columns = ["address", "city", "region", "country", "federal_district", "population", "foundation_year", "geo_lat", "geo_lon"]
print(f"Исторически значимые города из {target_districts} с населением более 500 000:")
display(historic_large_cities[selected_columns])
Отобразим на графике выборку городов
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 10))
# Построение точек: население делим для масштабирования
plt.scatter(
historic_large_cities["geo_lon"],
historic_large_cities["geo_lat"],
s=historic_large_cities["population"] / 100000,
alpha=0.6,
color='royalblue',
edgecolors='k'
)
# Подписи городов
for _, row in historic_large_cities.iterrows():
plt.text(
row["geo_lon"] + 0.3,
row["geo_lat"] + 0.3,
f"{row['address']} ({row['region']})",
fontsize=8,
ha="left",
va="center"
)
# Подписи и оформление
plt.title("Исторически крупные города с населением > 500 000", fontsize=14)
plt.xlabel("Долгота", fontsize=12)
plt.ylabel("Широта", fontsize=12)
plt.grid(True)
plt.tight_layout()
plt.show()
Создаю граф
import networkx as nx
from geopy.distance import geodesic
# Создаём пустой граф
G = nx.Graph()
# Добавляем узлы
for _, row in historic_large_cities.iterrows():
G.add_node(
row["address"],
region=row["region"],
country=row["country"],
population=row["population"],
year=row["foundation_year"],
pos=(row["geo_lon"], row["geo_lat"]) # Для визуализации
)
# Добавляем рёбра между всеми парами городов
cities = historic_large_cities[["address", "geo_lat", "geo_lon"]].values
for i in range(len(cities)):
for j in range(i + 1, len(cities)):
city1, lat1, lon1 = cities[i]
city2, lat2, lon2 = cities[j]
distance = geodesic((lat1, lon1), (lat2, lon2)).meters
G.add_edge(city1, city2, weight=distance)
print(f"Граф построен: {G.number_of_nodes()} узлов, {G.number_of_edges()} рёбер")
Теперь необходимо спроектировать хромосомы¶
Сделаю это используя кросинговер. А также нужно задать фитнес функцию. Которая будет оценивать насколько проложенный путь лучше, чем другой
cities_list = list(G.nodes)
n = len(cities_list)
distance_matrix = np.zeros((n, n))
for i in range(n):
for j in range(n):
if i != j:
try:
distance_matrix[i, j] = nx.shortest_path_length(
G, source=cities_list[i], target=cities_list[j], weight='weight'
)
except nx.NetworkXNoPath:
distance_matrix[i, j] = 1e6 # Штраф
import random
import networkx as nx
from typing import List, Optional
# Хромосома
class Chromosome:
# Инициализация решения
def __init__(self, chromosome: List[int], distance_matrix: np.ndarray):
if len(set(chromosome)) != len(chromosome):
raise ValueError("Хромосома содержит дубликаты")
self.chromosome = chromosome
self.distance_matrix = distance_matrix
self._total_distance: Optional[float] = None
self._fitness: Optional[float] = None
# Проверка хромосомы на соответствие
def valid_chromosome(self, chromosome: List[int], count_cities: int) -> bool:
""" Проверка типа данных """
if not isinstance(chromosome, list) or not all(isinstance(gene, int) for gene in chromosome):
return False
""" Проверка длины хромосомы """
if len(chromosome) != count_cities:
return False
""" Проверка уникальности генов """
if len(set(chromosome)) != count_cities:
return False
""" Проверка диапазона значений """
if any(gene < 0 or gene >= count_cities for gene in chromosome):
return False
return True
# Вычисляем общую длину маршрута
@property
def _fitness_total_distance(self) -> float:
if self._total_distance is None:
self._total_distance = 0.0
self._total_distance = sum(
self.distance_matrix[self.chromosome[i], self.chromosome[(i + 1) % len(self.chromosome)]]
for i in range(len(self.chromosome))
)
return self._total_distance
# Решаем и получаем значение fitness-функции
@property
def fitness(self) -> float:
"""Вычисляем как обратную величину к общей длине маршрута"""
if self._fitness is None:
self._fitness = 1 / self._fitness_total_distance
return self._fitness
# Эволюция популяции
@property
def evaluate(self) -> float:
return self.fitness
# Мутация
def mutate(self):
i, j = random.sample(range(len(self.chromosome)), 2)
self.chromosome[i], self.chromosome[j] = self.chromosome[j], self.chromosome[i]
self._fitness = None
self._total_distance = None
# Представление решения в виде строки
def __self__(self) -> str:
return f"Маршрут: {self.chromosome} | Длина: {self._total_distance:.2f} км"
Сгенерируем начальную популяцию¶
Я создал популяцию случайным образом, где каждая хромосома индекс города, которая каждый раз переставляется, что обеспечивает разнообразие начальных решений.
# Генерация начальной популяции
def generate_initial_population(population_size: int, count_cities: int) -> List[List[int]]:
"""Создает начальную популяцию случайных маршрутов."""
population = []
for _ in range(population_size):
chromosome = list(range(count_cities))
random.shuffle(chromosome)
population.append(chromosome)
return population
Задам кроссинговера¶
В данной задаче используется упорядоченный кроссовер (OX1), который сохраняет порядок городов из родительских маршрутов, избегая дублирований.
Алгоритм:
Выбирается случайный отрезок из первого родителя и копируется в потомка. Оставшиеся города заполняются из второго родителя, начиная с конца выбранного отрезка, в порядке их появления, но исключая уже скопированные города. Этот метод позволяет комбинировать удачные участки маршрутов, сохраняя при этом допустимость решения (каждый город посещается ровно один раз).
# Кроссинговер
def crossover(parent1: Chromosome, parent2: Chromosome) -> Chromosome:
"""Реализует упорядоченный одноточечный кроссинговер (OX1)."""
size = len(parent1.chromosome)
cut = random.randint(1, size - 2)
head = parent1.chromosome[:cut]
tail = [gene for gene in parent2.chromosome if gene not in head]
return Chromosome(head + tail, parent1.distance_matrix)
# Селекция (турнирный отбор)
def tournament_selection(population: List[Chromosome], tournament_size: int = 3) -> Chromosome:
"""Выбирает лучшую хромосому из случайной подгруппы."""
contenders = random.sample(population, tournament_size)
return max(contenders, key=lambda chrom: chrom.fitness)
Задам оператор мутации¶
Буду использовать два вида мутаций:
Обменная мутация (swap mutation) – случайно выбираются два гена (города) в хромосоме и меняются местами.
Мутация перемешиванием (scramble mutation) – случайный подотрезок хромосомы перемешивается, изменяя порядок городов внутри него.
# Обменная мутация
def swap_mutate(chromosome: List[int], mutation_rate: float) -> Chromosome:
"""Случайно меняет местами 2 города"""
if np.random.random() < mutation_rate:
genes = chromosome.chromosome
idx1, idx2 = np.random.choice(len(genes), 2, replace=False)
genes[idx1], genes[idx2] = genes[idx2], genes[idx1]
return chromosome
# Мутация перемешиванием
def scramble_mutation(chromosome: List[int], mutation_rate: float) -> Chromosome:
"""Мутация "перемешиванием" случайного отрезка"""
if np.random.random() < mutation_rate:
size: int = len(chromosome)
start, end = sorted(np.random.randint(0, size, 2))
segment: List[int] = chromosome[start:end]
np.random.shuffle(segment)
chromosome[start:end] = segment
return chromosome
Наконец сам генетический алгоритм¶
Основной эволюционный цикл Для каждого поколения выполняются следующие шаги:
- Оценка приспособленности Для каждой хромосомы вычисляется фитнес-функция как обратная величина к длине маршрута. Это делает более короткие маршруты более предпочтительными.
- Отбор элиты Лучшие elite_size решений переходят в следующее поколение без изменений, сохраняя уже найденные хорошие варианты.
- Формирование нового поколения Популяция дополняется потомками, созданными с помощью: Турнирного отбора (случайно выбираются несколько особей), Упорядоченного кроссовера (комбинация родительских маршрутов) и Мутаций - случайные изменения (обмен городов или перемешивание).
from typing import Callable, List
# Основной цикл Генетического Алгоритма
def genetic_algorithm(cities: pd.DataFrame, generations: int = 400, mutation_rate: float = 0.01, population_size: int = 100,
elite_size: int = 20, crossover_function: Callable = crossover, mutation_function: Callable = swap_mutate) -> Chromosome:
"""Запускает генетический алгоритм."""
# Создание матрицы расстояний
fitness_history = []
coords = cities[["geo_lat", "geo_lon"]].values
distance_matrix = np.zeros((len(coords), len(coords)))
for i in range(len(coords)):
for j in range(len(coords)):
if i != j:
distance_matrix[i][j] = haversine_distance(coords[i], coords[j])
# Генерация начальной популяции как объектов Chromosome
population: List[Chromosome] = [
Chromosome(chromo, distance_matrix)
for chromo in generate_initial_population(population_size, len(cities))
]
# Основной цикл эволюции
for generation in range(generations):
# Сортировка по приспособленности
population.sort(key=lambda x: x.fitness, reverse=True)
# Элита
new_population: List[Chromosome] = population[:elite_size]
# Генерация потомков
while len(new_population) < population_size:
parent1 = tournament_selection(population, tournament_size=3)
parent2 = tournament_selection(population, tournament_size=3)
child = crossover_function(parent1, parent2)
if random.random() < mutation_rate:
mutation_function(child, mutation_rate)
new_population.append(child)
population = new_population
best = population[0]
fitness_history.append(1 / best.fitness) # Длина маршрута в км или м
print(f"Поколение {generation + 1} | Лучшая длина маршрута: {1 / best.fitness:.2f} км")
return population[0], fitness_history
def haversine_distance(coordinate1: np.ndarray, coordinate2: np.ndarray) -> float:
"""Вычисляет расстояние между двумя точками на сфере (в км) по формуле гаверсинусов."""
lat1, lon1 = coordinate1
lat2, lon2 = coordinate2
# Конвертация градусов в радианы
lat1, lon1, lat2, lon2 = map(np.radians, [lat1, lon1, lat2, lon2])
# Разницы координат
dlat = lat2 - lat1
dlon = lon2 - lon1
# Формула гаверсинусов
a = np.sin(dlat/2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2)**2
c = 2 * np.arcsin(np.sqrt(a))
# Радиус Земли в километрах
r = 6371
return c * r
best, fitness_history = genetic_algorithm(
historic_large_cities,
population_size=100,
elite_size=20,
mutation_rate=0.05,
generations=500,
crossover_function=crossover,
mutation_function=swap_mutate
)
print(f"Лучший маршрут: {best.chromosome}")
print(f"Общая длина маршрута: {1 / best.fitness:.2f} км")
Нарисуем карсивый график¶
import matplotlib.cm as cm
plt.figure(figsize=(16, 12))
# Города с тенью для объёма
plt.scatter(
historic_large_cities["geo_lon"],
historic_large_cities["geo_lat"],
c="red", s=120, edgecolors='black', linewidth=0.7, alpha=0.9, zorder=5
)
# Цветовой градиент для маршрута (от начала к концу)
route_order = best.chromosome + [best.chromosome[0]]
route_lng = [historic_large_cities.iloc[i]["geo_lon"] for i in route_order]
route_lat = [historic_large_cities.iloc[i]["geo_lat"] for i in route_order]
colors = cm.viridis(np.linspace(0, 1, len(route_order)-1))
for i in range(len(route_order)-1):
plt.plot(
route_lng[i:i+2], route_lat[i:i+2],
color=colors[i], linewidth=3, alpha=0.8, zorder=4
)
# Номера и подписи городов с фоном
for i, city_idx in enumerate(best.chromosome):
city = historic_large_cities.iloc[city_idx]
plt.text(
city["geo_lon"], city["geo_lat"], str(i+1),
fontsize=12, ha="center", va="center",
bbox=dict(facecolor="white", alpha=0.8, boxstyle="circle"), zorder=6
)
plt.text(
city["geo_lon"] + 0.6, city["geo_lat"] + 0.3,
f'{city["address"]}',
fontsize=10, ha="left", va="center",
bbox=dict(facecolor="white", alpha=0.6, boxstyle="round,pad=0.3"), zorder=6
)
# Настройки графика
plt.title(
"Оптимальный маршрут коммивояжера через города России\n"
f"Общая длина маршрута: {best._total_distance:.2f} км",
fontsize=16, pad=20
)
plt.xlabel("Долгота", fontsize=14)
plt.ylabel("Широта", fontsize=14)
plt.grid(alpha=0.25)
plt.tight_layout()
# Легенда с кастомными маркерами
from matplotlib.lines import Line2D
legend_elements = [
Line2D([0], [0], marker='o', color='w', label='Города', markerfacecolor='red', markersize=12, markeredgecolor='black'),
Line2D([0], [0], color=cm.viridis(0.6), lw=3, label='Маршрут')
]
plt.legend(handles=legend_elements, loc='upper right', fontsize=12)
plt.show()
Можно вывести пусть в строке GA
def print_city_route(best_chromosome, city_df):
"""Печатает маршрут обхода городов с направлением.
best_chromosome: список индексов городов (например, best.chromosome)
city_df: DataFrame с данными городов (например, historic_large_cities)
"""
print("Маршрут коммивояжера:\n")
for i in range(len(best_chromosome)):
city_idx = best_chromosome[i]
city_name = city_df.iloc[city_idx]["address"]
print(f"{i + 1}. {city_name}")
if i < len(best_chromosome) - 1:
print("↓")
# Возвращение в начальный город
start_city = city_df.iloc[best_chromosome[0]]["address"]
print("↓")
print(f"{len(best_chromosome) + 1}. {start_city} (возвращение в начальный город)")
print_city_route(best.chromosome, historic_large_cities)