Files
AIM-PIbd-31-Rodionov-I-A/lab_10/lab10.ipynb
2025-04-12 09:59:20 +04:00

28 KiB
Raw Blame History

Лабораторная работа 10

В качестве задачи оптимизации была выбрана классическая вариация задачи о рюкзаке: дан набор предметов, каждый с определенным весом и ценностью. Требуется определить, какие предметы взять с собой в рюкзак, чтобы их суммарная ценность была максимальной, а суммарный вес не превышал заданную грузоподъемность рюкзака. При этом каждый предмет можно взять только один раз или не брать вовсе (0/1).

Используем соответствующий датасет, в котором имеется большое число вариантов задачи с различными параметрами: https://www.kaggle.com/datasets/warcoder/knapsack-problem?select=knapsack_5_items.csv

In [70]:
import pandas as pd

df = pd.read_csv("..//..//static//csv//knapsack_5_items.csv")

pd.concat([df.head(5), df.tail(5)])
Out[70]:
<style scoped=""> .dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } </style>
Weights Prices Capacity Best picks Best price
0 [46 40 42 38 10] [12 19 19 15 8] 40 [0. 1. 0. 0. 0.] 19.0
1 [11 31 4 6 7] [ 2 8 18 16 3] 64 [1. 1. 1. 1. 1.] 47.0
2 [32 49 27 37 24] [19 16 16 4 1] 87 [1. 0. 1. 0. 1.] 36.0
3 [20 35 22 23 16] [19 17 19 9 1] 21 [1. 0. 0. 0. 0.] 19.0
4 [ 7 12 19 13 20] [10 11 18 15 5] 50 [0. 1. 1. 1. 0.] 44.0
9995 [18 12 11 49 32] [12 3 17 19 7] 41 [1. 1. 1. 0. 0.] 32.0
9996 [20 2 24 7 7] [17 12 4 3 8] 17 [0. 1. 0. 1. 1.] 23.0
9997 [43 43 5 15 23] [15 5 7 2 7] 62 [1. 0. 1. 0. 0.] 22.0
9998 [49 9 15 21 39] [11 15 3 12 19] 65 [0. 1. 1. 0. 1.] 37.0
9999 [25 36 42 19 39] [15 12 7 18 12] 79 [1. 0. 0. 1. 0.] 33.0

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

В данном случае хромосома будет представлять из себя список длины n (количество предметов в конкректной задаче), который представляет собой решение задачи рюкзака — то есть указывает, какие предметы включить в рюкзак.

Пример: [1, 0, 1, 0, 0]. В примере выбраны первый и третий предметы.

Ген же — это одно значение в хромосоме.

Тип данных: int.

Возможные значения:

  • 1 — предмет в рюкзаке;
  • 0 — предмет не в рюкзаке.

Реализация функции генерации начальной популяции и ее тест:

In [71]:
import random

def create_individual(elements_num): 
    # Генерирует случайную двоичную строку той же длины, что и список элементов
    return [random.randint(0, 1) for _ in range(elements_num)]

def create_population(elements_num, population_size):  
    return [create_individual(elements_num) for _ in range(population_size)]

create_population(5, 10)
Out[71]:
[[0, 1, 0, 0, 0],
 [1, 0, 0, 0, 1],
 [1, 0, 0, 0, 0],
 [1, 0, 1, 0, 1],
 [1, 1, 1, 1, 0],
 [0, 1, 0, 0, 1],
 [1, 0, 1, 1, 1],
 [1, 0, 0, 0, 1],
 [1, 0, 1, 1, 0],
 [1, 1, 0, 1, 1]]

Реализация фитнес-функции и ее тест:

In [72]:
def evaluate_fitness(individual, weights, prices, capacity):
    total_value = total_weight = 0
    for i in range(len(individual)):
        if individual[i] == 1:
            total_value += prices[i]
            total_weight += weights[i]
    # Если общий вес превышает вместимость ранца, устанавливается значение 0 (неверное решение)
    return total_value if total_weight <= capacity else 0

evaluate_fitness([0, 1, 1, 1, 0], [7, 12, 19, 13, 20], [10, 11, 18, 15, 5], 50)
Out[72]:
44

Реализация оператора кроссинговера и его тест:

In [73]:
# одноточечный кроссинговер
def crossover(parent1, parent2):
    point = random.randint(1, len(parent1) - 1)
    return (parent1[:point] + parent2[point:], parent2[:point] + parent1[point:])

crossover([0, 1, 1, 1, 0], [1, 0, 1, 0, 0])
Out[73]:
([0, 1, 1, 0, 0], [1, 0, 1, 1, 0])

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

In [74]:
# Мутация 1: побитовая замена
def mutate_flip_bits(individual, mutation_rate):
    for i in range(len(individual)):
        # Сработает с некоторой вероятностью
        if random.random() < mutation_rate:
            individual[i] = 1 - individual[i]

# Мутация 2: случайный свап двух генов
def mutate_swap_genes(individual, mutation_rate):
    if random.random() < mutation_rate:
        i, j = random.sample(range(len(individual)), 2)
        individual[i], individual[j] = individual[j], individual[i]

individual = [0, 1, 1, 1, 0]
print(individual)
mutate_flip_bits(individual, 0.5)
print("================")
print(individual)
print("================")
mutate_swap_genes(individual, 1)
print(individual)
[0, 1, 1, 1, 0]
================
[0, 1, 0, 1, 0]
================
[0, 0, 0, 1, 1]

И наконец реализуем сам генетический алгоритм:

In [79]:
# Параметры алгоритма
population_size = 100
num_generations = 10
mutation_rate = 0.1
mutation_strategy = 'flip'

# Выбор участников кроссинговера с помощью селекции на основе рулетки
def select_parents(population, weights, prices, capacity):
    fitness_values = [evaluate_fitness(ind, weights, prices, capacity) for ind in population]
    total_fitness = sum(fitness_values)
    if total_fitness == 0:
        return random.choice(population), random.choice(population)
    # чем выше значение фитнес-функции, тем больше шанс на выбор
    probabilities = [f / total_fitness for f in fitness_values]
    return random.choices(population, weights=probabilities, k=2)

def genetic_algorithm(weights, prices, capacity, population_size = 100, num_generations = 10, mutation_rate = 0.1, mutation_strategy='flip'):
    elements_num = len(weights)
    population = create_population(elements_num, population_size)

    for _ in range(num_generations):
        new_population = []
        for _ in range(population_size // 2):
            p1, p2 = select_parents(population, weights, prices, capacity)
            c1, c2 = crossover(p1, p2)
            if mutation_strategy == 'flip':
                mutate_flip_bits(c1, mutation_rate)
                mutate_flip_bits(c2, mutation_rate)
            elif mutation_strategy == 'swap':
                mutate_swap_genes(c1, mutation_rate)
                mutate_swap_genes(c2, mutation_rate)
            new_population.extend([c1, c2])
        population = new_population

    best = max(population, key=lambda ind: evaluate_fitness(ind, weights, prices, capacity))
    best_value = evaluate_fitness(best, weights, prices, capacity)
    return best, best_value

Применим его для всех случаев из датасета:

In [80]:
import ast
import re

picks = []
best_prices = []

def fix_list_string(s):
    # Удалить пробел сразу после [ и сразу перед ]
    s = re.sub(r'\[\s*', '[', s)
    s = re.sub(r'\s*\]', ']', s)
    # Заменить все группы пробелов на запятую
    s = re.sub(r'\s+', ',', s)
    return s

for _, row in df.iterrows():
    weights = ast.literal_eval(fix_list_string(row['Weights']))
    prices = ast.literal_eval(fix_list_string(row['Prices']))
    capacity = row['Capacity']

    best_individual, best_value = genetic_algorithm(weights, prices, capacity, population_size, num_generations, mutation_rate, mutation_strategy)
    
    picks.append(best_individual)
    best_prices.append(best_value)

df['algorithmPicks'] = picks
df['algorithmPrice'] = best_prices

pd.concat([df.head(5), df.tail(5)])
Out[80]:
<style scoped=""> .dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } </style>
Weights Prices Capacity Best picks Best price algorithmPicks algorithmPrice
0 [46 40 42 38 10] [12 19 19 15 8] 40 [0. 1. 0. 0. 0.] 19.0 [0, 1, 0, 0, 0] 19
1 [11 31 4 6 7] [ 2 8 18 16 3] 64 [1. 1. 1. 1. 1.] 47.0 [1, 1, 1, 1, 1] 47
2 [32 49 27 37 24] [19 16 16 4 1] 87 [1. 0. 1. 0. 1.] 36.0 [1, 0, 1, 0, 1] 36
3 [20 35 22 23 16] [19 17 19 9 1] 21 [1. 0. 0. 0. 0.] 19.0 [1, 0, 0, 0, 0] 19
4 [ 7 12 19 13 20] [10 11 18 15 5] 50 [0. 1. 1. 1. 0.] 44.0 [0, 1, 1, 1, 0] 44
9995 [18 12 11 49 32] [12 3 17 19 7] 41 [1. 1. 1. 0. 0.] 32.0 [1, 1, 1, 0, 0] 32
9996 [20 2 24 7 7] [17 12 4 3 8] 17 [0. 1. 0. 1. 1.] 23.0 [0, 1, 0, 1, 1] 23
9997 [43 43 5 15 23] [15 5 7 2 7] 62 [1. 0. 1. 0. 0.] 22.0 [1, 0, 1, 0, 0] 22
9998 [49 9 15 21 39] [11 15 3 12 19] 65 [0. 1. 1. 0. 1.] 37.0 [0, 1, 1, 0, 1] 37
9999 [25 36 42 19 39] [15 12 7 18 12] 79 [1. 0. 0. 1. 0.] 33.0 [1, 0, 0, 1, 0] 33

По полученным результатам видно, что ответы алгоритма совпадают с теми ответами, которые уже имелись в наборе данных. Поэтому, можно сказать, что для таких условий задачи алгоритм работает успешно даже с 10 поколениями