Files
AIM-PIbd-31-Alekseev-I-S/Lab_10/Lab10.ipynb
Иван Алексеев 70f14307ce кайфанули
2025-03-29 14:50:52 +04:00

200 KiB
Raw Blame History

Начало лабораторной работы, юхуу(данная лабораторная работа была создана исключительно в образовательных целях и не будет использоваться для личного применения)

Задача оптимизации с использованием простого генетического алгоритма

Формулировка задачи: дан датасет с картами-персонажами из игры Clash Royale. Требуется составить наиболее оптимальную колоду из 8 карт с приемлемами показателями элексира, требуемого для использования карт, едениц здоровья и наносимого урона.

В данном примере будут рассматриваться карты на 11-м уровне - "испытательном" уровне карт

In [ ]:
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)}")
             name  elixir  hitpoints11  damage11
0       Skeletons       1         81.0     243.0
1      Ice Spirit       1        230.0     110.0
2            Heal       1          0.0       0.0
3         Goblins       2        202.0     360.0
4   Spear Goblins       2        133.0     243.0
5             Zap       2          0.0     192.0
6            Bats       2         81.0     405.0
7    Fire Spirits       2        110.0     645.0
8  Giant Snowball       2          0.0     192.0
9       Ice Golem       2       1197.0      84.0
Всего карт: 94

Определение структуры хромосомы и типа данных гена

Хромосома будет представлять собой колоду из 8 карт (стандартный размер колоды в Clash Royale). Каждый ген - это индекс карты в нашем датасете

In [ ]:
# Определяем параметры генетического алгоритма
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)))

Функция генерации начальной популяции

In [81]:
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]}")
Пример начальной популяции:
Индивидуум 1: ['Musketeer', 'Furnace', 'Zappies', 'Baby Dragon', 'Skeletons', 'Inferno Tower', 'Minions', 'Fireball']
Индивидуум 2: ['Ram Rider', 'Executioner', 'Inferno Dragon', 'Minion Horde', 'Hunter', 'Dart Goblin', 'Spear Goblins', 'P.E.K.K.A']
Индивидуум 3: ['Mega Minion', 'Witch', 'Cannon Cart', 'Baby Dragon', 'Heal', 'Night Witch', 'Giant', 'Tesla']

Фитнес-функция

Наша цель - создать сбалансированную колоду. Определим фитнес как комбинацию:

  1. Средний эликсир (чем меньше, тем лучше)

  2. Суммарный урон (чем больше, тем лучше)

  3. Суммарное здоровье (чем больше, тем лучше)

In [82]:
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))
Тестирование фитнес-функции:
Колода: ['Musketeer', 'Furnace', 'Zappies', 'Baby Dragon', 'Skeletons', 'Inferno Tower', 'Minions', 'Fireball']
Fitness: 5809.655172413793

Оператор кроссинговера

Реализуем одноточечный кроссинговер

In [83]:
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])
Тестирование кроссинговера:
Родитель 1: ['Musketeer', 'Furnace', 'Zappies', 'Baby Dragon', 'Skeletons', 'Inferno Tower', 'Minions', 'Fireball']
Родитель 2: ['Ram Rider', 'Executioner', 'Inferno Dragon', 'Minion Horde', 'Hunter', 'Dart Goblin', 'Spear Goblins', 'P.E.K.K.A']
Ребенок 1: ['Musketeer', 'Furnace', 'Zappies', 'Minion Horde', 'Hunter', 'Dart Goblin', 'Spear Goblins', 'P.E.K.K.A']
Ребенок 2: ['Ram Rider', 'Executioner', 'Inferno Dragon', 'Baby Dragon', 'Skeletons', 'Inferno Tower', 'Minions', 'Fireball']

Операторы мутации

Реализуем два оператора мутации:

  1. Замена одной случайной карты

  2. Перемешивание колоды

In [84]:
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)])
Тестирование мутаций:
Оригинал: ['Musketeer', 'Furnace', 'Zappies', 'Baby Dragon', 'Skeletons', 'Inferno Tower', 'Minions', 'Fireball']
Swap мутация: ['Musketeer', 'Furnace', 'Zappies', 'Baby Dragon', 'Skeletons', 'Inferno Tower', 'Minions', 'Wizard']
Shuffle мутация: ['Musketeer', 'Furnace', 'Zappies', 'Baby Dragon', 'Skeletons', 'Inferno Tower', 'Minions', 'Fireball']

Различные визуализации для дальнейшего использования

In [90]:
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()

Реализация генетического алгоритма

In [91]:
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()
Запуск генетического алгоритма...
Поколение 1: Лучший fitness = 8902.00
Поколение 2: Лучший fitness = 8942.48
Поколение 3: Лучший fitness = 9572.00
Поколение 4: Лучший fitness = 9937.30
Поколение 5: Лучший fitness = 10542.72
Поколение 6: Лучший fitness = 11029.03
Поколение 7: Лучший fitness = 11202.13
Поколение 8: Лучший fitness = 11202.13
Поколение 9: Лучший fitness = 11312.55
Поколение 10: Лучший fitness = 11581.94
Поколение 11: Лучший fitness = 11581.94
Поколение 12: Лучший fitness = 11581.94
Поколение 13: Лучший fitness = 11741.63
Поколение 14: Лучший fitness = 11741.63
Поколение 15: Лучший fitness = 11741.63
Поколение 16: Лучший fitness = 11741.63
Поколение 17: Лучший fitness = 11741.63
Поколение 18: Лучший fitness = 11741.63
Поколение 19: Лучший fitness = 11741.63
Поколение 20: Лучший fitness = 11741.63
Поколение 21: Лучший fitness = 11741.63
Поколение 22: Лучший fitness = 11741.63
Поколение 23: Лучший fitness = 11741.63
Поколение 24: Лучший fitness = 11741.63
Поколение 25: Лучший fitness = 11741.63
Поколение 26: Лучший fitness = 11741.63
Поколение 27: Лучший fitness = 11741.63
Поколение 28: Лучший fitness = 11741.63
Поколение 29: Лучший fitness = 11741.63
Поколение 30: Лучший fitness = 11741.63
Поколение 31: Лучший fitness = 11741.63
Поколение 32: Лучший fitness = 11764.80
Поколение 33: Лучший fitness = 11764.80
Поколение 34: Лучший fitness = 11764.80
Поколение 35: Лучший fitness = 11764.80
Поколение 36: Лучший fitness = 11764.80
Поколение 37: Лучший fitness = 11764.80
Поколение 38: Лучший fitness = 11764.80
Поколение 39: Лучший fitness = 11764.80
Поколение 40: Лучший fitness = 11764.80
Поколение 41: Лучший fitness = 11764.80
Поколение 42: Лучший fitness = 11889.45
Поколение 43: Лучший fitness = 11889.45
Поколение 44: Лучший fitness = 11889.45
Поколение 45: Лучший fitness = 11889.45
Поколение 46: Лучший fitness = 11889.45
Поколение 47: Лучший fitness = 11889.45
Поколение 48: Лучший fitness = 11889.45
Поколение 49: Лучший fitness = 11889.45
Поколение 50: Лучший fitness = 11889.45
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
In [88]:
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}")
Лучшая найденная колода:
Ice Golem (Эликсир: 2, Урон: 84.0, Здоровье: 1197.0)
Giant (Эликсир: 5, Урон: 254.0, Здоровье: 3945.0)
Mirror (Эликсир: 0, Урон: 0.0, Здоровье: 0.0)
Furnace (Эликсир: 4, Урон: 2580.0, Здоровье: 1208.0)
Knight (Эликсир: 3, Урон: 202.0, Здоровье: 1669.0)
Elixir Golem (Эликсир: 3, Урон: 254.0, Здоровье: 1696.0)
Wall Breakers (Эликсир: 2, Урон: 962.0, Здоровье: 331.0)
Tombstone (Эликсир: 3, Урон: 1458.0, Здоровье: 508.0)

Fitness лучшей колоды: 11889.45