28 KiB
Лабораторная работа 10¶
В качестве задачи оптимизации была выбрана классическая вариация задачи о рюкзаке: дан набор предметов, каждый с определенным весом и ценностью. Требуется определить, какие предметы взять с собой в рюкзак, чтобы их суммарная ценность была максимальной, а суммарный вес не превышал заданную грузоподъемность рюкзака. При этом каждый предмет можно взять только один раз или не брать вовсе (0/1).
Используем соответствующий датасет, в котором имеется большое число вариантов задачи с различными параметрами: https://www.kaggle.com/datasets/warcoder/knapsack-problem?select=knapsack_5_items.csv
import pandas as pd
df = pd.read_csv("..//..//static//csv//knapsack_5_items.csv")
pd.concat([df.head(5), df.tail(5)])
Структура хромосомы и тип данных гена¶
В данном случае хромосома будет представлять из себя список длины n (количество предметов в конкректной задаче), который представляет собой решение задачи рюкзака — то есть указывает, какие предметы включить в рюкзак.
Пример: [1, 0, 1, 0, 0]. В примере выбраны первый и третий предметы.
Ген же — это одно значение в хромосоме.
Тип данных: int.
Возможные значения:
- 1 — предмет в рюкзаке;
- 0 — предмет не в рюкзаке.
Реализация функции генерации начальной популяции и ее тест:¶
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)
Реализация фитнес-функции и ее тест:¶
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)
Реализация оператора кроссинговера и его тест:¶
# одноточечный кроссинговер
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])
Реализация двух операторов мутации и их тест:¶
# Мутация 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)
И наконец реализуем сам генетический алгоритм:¶
# Параметры алгоритма
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
Применим его для всех случаев из датасета:
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)])
По полученным результатам видно, что ответы алгоритма совпадают с теми ответами, которые уже имелись в наборе данных. Поэтому, можно сказать, что для таких условий задачи алгоритм работает успешно даже с 10 поколениями