200 KiB
Начало лабораторной работы, юхуу(данная лабораторная работа была создана исключительно в образовательных целях и не будет использоваться для личного применения)¶
Задача оптимизации с использованием простого генетического алгоритма¶
Формулировка задачи: дан датасет с картами-персонажами из игры Clash Royale. Требуется составить наиболее оптимальную колоду из 8 карт с приемлемами показателями элексира, требуемого для использования карт, едениц здоровья и наносимого урона.
В данном примере будут рассматриваться карты на 11-м уровне - "испытательном" уровне карт
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import random
from tqdm import tqdm
# Загрузка данных
data = pd.read_csv(".//static//csv//cardsInfo.csv")
# Оставляем только нужные столбцы
df = data[['name', 'elixir', 'hitpoints11', 'damage11']].copy()
# Удаляем дубликаты и пропущенные значения
df = df.drop_duplicates(subset=['name']).dropna()
# Проверяем данные
print(df.head(10))
print(f"Всего карт: {len(df)}")
Определение структуры хромосомы и типа данных гена¶
Хромосома будет представлять собой колоду из 8 карт (стандартный размер колоды в Clash Royale). Каждый ген - это индекс карты в нашем датасете
# Определяем параметры генетического алгоритма
DECK_SIZE = 8
POPULATION_SIZE = 100
GENERATIONS = 50
MUTATION_RATE = 0.5
CROSSOVER_RATE = 0.8
TOURNAMENT_SIZE = 5
ELITISM_COUNT = 2 # сколько лучших особей текущего поколения автоматически переходят в следующее
# поколение без изменений (без скрещивания и мутации)
# Получаем список всех карт
all_cards = df['name'].tolist()
card_indices = list(range(len(all_cards)))
Функция генерации начальной популяции¶
def generate_individual():
"""Генерация одной колоды (индивидуума)"""
return random.sample(card_indices, DECK_SIZE)
def generate_population(size=POPULATION_SIZE):
"""Генерация начальной популяции"""
return [generate_individual() for _ in range(size)]
# Тестируем генерацию популяции
population = generate_population(5)
print("Пример начальной популяции:")
for i, ind in enumerate(population[:3]):
print(f"Индивидуум {i+1}: {[all_cards[idx] for idx in ind]}")
Фитнес-функция¶
Наша цель - создать сбалансированную колоду. Определим фитнес как комбинацию:
Средний эликсир (чем меньше, тем лучше)
Суммарный урон (чем больше, тем лучше)
Суммарное здоровье (чем больше, тем лучше)
def calculate_fitness(individual):
"""Вычисление fitness для одного индивидуума"""
total_elixir = 0
total_damage = 0
total_hp = 0
for card_idx in individual:
card = df.iloc[card_idx]
total_elixir += card['elixir']
total_damage += card['damage11']
total_hp += card['hitpoints11']
avg_elixir = total_elixir / DECK_SIZE
fitness = (total_damage * 0.4 + total_hp * 0.4) / (avg_elixir * 0.2)
return fitness
# Тестируем фитнес-функцию
test_individual = population[0]
print("\nТестирование фитнес-функции:")
print("Колода:", [all_cards[idx] for idx in test_individual])
print("Fitness:", calculate_fitness(test_individual))
Оператор кроссинговера¶
Реализуем одноточечный кроссинговер
def crossover(parent1, parent2):
"""Одноточечный кроссинговер"""
if random.random() > CROSSOVER_RATE:
return parent1.copy(), parent2.copy()
point = random.randint(1, DECK_SIZE-1)
child1 = parent1[:point] + parent2[point:]
child2 = parent2[:point] + parent1[point:]
# Убедимся, что в колоде нет дубликатов
child1 = list(dict.fromkeys(child1))
child2 = list(dict.fromkeys(child2))
# Дополняем колоду случайными картами, если нужно
while len(child1) < DECK_SIZE:
new_card = random.choice(card_indices)
if new_card not in child1:
child1.append(new_card)
while len(child2) < DECK_SIZE:
new_card = random.choice(card_indices)
if new_card not in child2:
child2.append(new_card)
return child1, child2
# Тестируем кроссинговер
parent1 = population[0]
parent2 = population[1]
child1, child2 = crossover(parent1, parent2)
print("\nТестирование кроссинговера:")
print("Родитель 1:", [all_cards[idx] for idx in parent1])
print("Родитель 2:", [all_cards[idx] for idx in parent2])
print("Ребенок 1:", [all_cards[idx] for idx in child1])
print("Ребенок 2:", [all_cards[idx] for idx in child2])
Операторы мутации¶
Реализуем два оператора мутации:
Замена одной случайной карты
Перемешивание колоды
def mutation_swap(individual):
"""Мутация: замена одной случайной карты"""
if random.random() > MUTATION_RATE:
return individual
idx_to_replace = random.randint(0, DECK_SIZE-1)
new_card = random.choice(card_indices)
# Убедимся, что новая карта не дублируется
while new_card in individual:
new_card = random.choice(card_indices)
new_individual = individual.copy()
new_individual[idx_to_replace] = new_card
return new_individual
def mutation_shuffle(individual):
"""Мутация: перемешивание колоды"""
if random.random() > MUTATION_RATE:
return individual
new_individual = individual.copy()
random.shuffle(new_individual)
return new_individual
# Тестируем мутации
test_individual = population[0]
print("\nТестирование мутаций:")
print("Оригинал:", [all_cards[idx] for idx in test_individual])
print("Swap мутация:", [all_cards[idx] for idx in mutation_swap(test_individual)])
print("Shuffle мутация:", [all_cards[idx] for idx in mutation_shuffle(test_individual)])
Различные визуализации для дальнейшего использования¶
def plot_elixir_distribution(best_individual):
elixirs = [df.iloc[card_idx]['elixir'] for card_idx in best_individual]
plt.figure(figsize=(8, 5))
plt.hist(elixirs, bins=np.arange(1, 10)-0.5, edgecolor='black', rwidth=0.8)
plt.xlabel('Стоимость эликсира')
plt.ylabel('Количество карт')
plt.title('Распределение стоимости карт в колоде')
plt.xticks(range(1, 10))
plt.grid(axis='y')
plt.show()
def plot_damage_vs_hp(best_individual):
damage = sum(df.iloc[card_idx]['damage11'] for card_idx in best_individual)
hp = sum(df.iloc[card_idx]['hitpoints11'] for card_idx in best_individual)
plt.figure(figsize=(6, 6))
plt.bar(['Урон', 'Здоровье'], [damage, hp], color=['red', 'green'])
plt.ylabel('Суммарное значение')
plt.title('Баланс урона и здоровья в колоде')
plt.grid(axis='y')
plt.show()
def plot_diversity(population_diversity):
plt.figure(figsize=(10, 5))
plt.plot(population_diversity, color='purple')
plt.xlabel('Поколение')
plt.ylabel('Разнообразие')
plt.title('Динамика разнообразия популяции')
plt.grid(True)
plt.show()
from math import pi
def plot_radar_chart(best_individual):
# Вычисляем характеристики
total_damage = sum(df.iloc[card_idx]['damage11'] for card_idx in best_individual)
total_hp = sum(df.iloc[card_idx]['hitpoints11'] for card_idx in best_individual)
avg_elixir = sum(df.iloc[card_idx]['elixir'] for card_idx in best_individual) / DECK_SIZE
elixir_variance = np.var([df.iloc[card_idx]['elixir'] for card_idx in best_individual])
categories = ['Урон', 'Здоровье', 'Ср. эликсир', 'Баланс эликсира']
values = [total_damage/1000, total_hp/1000, 10-avg_elixir, 10*(1-elixir_variance)]
N = len(categories)
angles = [n / float(N) * 2 * pi for n in range(N)]
angles += angles[:1]
fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(111, polar=True)
values += values[:1]
ax.plot(angles, values, linewidth=1, linestyle='solid')
ax.fill(angles, values, 'b', alpha=0.1)
ax.set_xticks(angles[:-1])
ax.set_xticklabels(categories)
ax.set_title('Радар-диаграмма характеристик колоды', y=1.1)
plt.show()
Реализация генетического алгоритма¶
def tournament_selection(population, fitnesses, k=TOURNAMENT_SIZE):
"""Турнирная селекция"""
selected = random.sample(list(zip(population, fitnesses)), k)
selected.sort(key=lambda x: x[1], reverse=True)
return selected[0][0]
def genetic_algorithm():
"""Основной генетический алгоритм"""
# Генерация начальной популяции
population = generate_population()
best_individual = None
best_fitness = -float('inf')
for generation in range(GENERATIONS):
# Вычисление fitness для всех индивидуумов
fitnesses = [calculate_fitness(ind) for ind in population]
# Проверка на лучшего индивидуума
current_best = max(fitnesses)
current_best_idx = fitnesses.index(current_best)
if current_best > best_fitness:
best_fitness = current_best
best_individual = population[current_best_idx]
print(f"Поколение {generation+1}: Лучший fitness = {current_best:.2f}")
# Создание нового поколения
new_population = []
# Элитизм: сохраняем лучших
elite_indices = np.argsort(fitnesses)[-ELITISM_COUNT:]
for idx in elite_indices:
new_population.append(population[idx])
# Генерация потомков
while len(new_population) < POPULATION_SIZE:
# Селекция
parent1 = tournament_selection(population, fitnesses)
parent2 = tournament_selection(population, fitnesses)
# Кроссинговер
child1, child2 = crossover(parent1, parent2)
# Мутация
child1 = mutation_swap(child1)
child2 = mutation_shuffle(child2)
new_population.extend([child1, child2])
# Обрезаем, если получилось больше (из-за четного размера)
population = new_population[:POPULATION_SIZE]
plot_elixir_distribution(best_individual)
plot_damage_vs_hp(best_individual)
plot_radar_chart(best_individual)
# Возвращаем лучшего индивидуума
return best_individual, best_fitness
# Запуск алгоритма
print("\nЗапуск генетического алгоритма...")
best_deck, best_score = genetic_algorithm()
print("\nЛучшая найденная колода:")
for card_idx in best_deck:
card = df.iloc[card_idx]
print(f"{card['name']} (Эликсир: {card['elixir']}, Урон: {card['damage11']}, Здоровье: {card['hitpoints11']})")
print(f"\nFitness лучшей колоды: {best_score:.2f}")