55 KiB
Лабортароная работа №10. Генетический алгоритм¶
Задача оптимизации: подбор оптимального рациона на день (завтрак, обед и ужин) по содержанию белков, жиров и углеводов (БЖУ).
import random
import numpy as np
from pprint import pprint
from deap import base, creator, tools, algorithms
import matplotlib.pyplot as plt
Список с продуктами и информацией по содержанию в них БЖУ (а также цене и предположительной разовой порции)
products = [
{"name": "Курица (грудка)", "calories": 165, "protein": 31, "fat": 3.6, "carbs": 0, "price": 30, "category": "meat", "portion": 150},
{"name": "Говядина", "calories": 250, "protein": 26, "fat": 15, "carbs": 0, "price": 93, "category": "meat", "portion": 120},
{"name": "Индейка", "calories": 135, "protein": 29, "fat": 1.7, "carbs": 0, "price": 70, "category": "meat", "portion": 150},
{"name": "Свинина", "calories": 242, "protein": 21, "fat": 17, "carbs": 0, "price": 34, "category": "meat", "portion": 120},
{"name": "Минтай", "calories": 72, "protein": 16, "fat": 0.9, "carbs": 0, "price": 36, "category": "fish", "portion": 180},
{"name": "Треска", "calories": 69, "protein": 16, "fat": 0.6, "carbs": 0, "price": 110, "category": "fish", "portion": 180},
{"name": "Скумбрия", "calories": 191, "protein": 18, "fat": 13.2, "carbs": 0, "price": 106, "category": "fish", "portion": 150},
{"name": "Рис", "calories": 325, "protein": 6.9, "fat": 1, "carbs": 72.2, "price": 6.8, "category": "grains", "portion": 100},
{"name": "Гречка", "calories": 330, "protein": 13, "fat": 2.5, "carbs": 68, "price": 3.5, "category": "grains", "portion": 100},
{"name": "Овсянка", "calories": 352, "protein": 12.3, "fat": 6.2, "carbs": 61.8, "price": 4.5, "category": "grains", "portion": 80},
{"name": "Перловка", "calories": 317, "protein": 10.4, "fat": 1.3, "carbs": 67, "price": 3, "category": "grains", "portion": 100},
{"name": "Брокколи", "calories": 34, "protein": 2.8, "fat": 0.4, "carbs": 7.0, "price": 40, "category": "vegetables", "portion": 200},
{"name": "Морковь", "calories": 41, "protein": 0.9, "fat": 0.2, "carbs": 10, "price": 0.9, "category": "vegetables", "portion": 150},
{"name": "Картофель", "calories": 77, "protein": 2, "fat": 0.1, "carbs": 17, "price": 11, "category": "grains", "portion": 150},
{"name": "Капуста белокочаная", "calories": 28, "protein": 1.8, "fat": 0.2, "carbs": 4.7, "price": 7, "category": "vegetables", "portion": 200},
{"name": "Помидоры", "calories": 18, "protein": 0.9, "fat": 0.2, "carbs": 2.7, "price": 40, "category": "vegetables", "portion": 200},
{"name": "Огурцы", "calories": 15, "protein": 0.7, "fat": 0.1, "carbs": 3.1, "price": 38, "category": "vegetables", "portion": 200},
{"name": "Яблоки", "calories": 47, "protein": 0.4, "fat": 0.4, "carbs": 9.8, "price": 18, "category": "fruits", "portion": 150},
{"name": "Бананы", "calories": 96, "protein": 1.5, "fat": 0.5, "carbs": 21, "price": 19, "category": "fruits", "portion": 120},
{"name": "Апельсины", "calories": 43, "protein": 0.9, "fat": 0.2, "carbs": 8.1, "price": 15, "category": "fruits", "portion": 150},
{"name": "Творог 5%", "calories": 120, "protein": 16, "fat": 5, "carbs": 3, "price": 54, "category": "dairy", "portion": 150},
{"name": "Творог 0.5%", "calories": 90, "protein": 18, "fat": 0.5, "carbs": 3.5, "price": 38, "category": "dairy", "portion": 150},
{"name": "Молоко", "calories": 60, "protein": 3, "fat": 3.2, "carbs": 4.7, "price": 9.5, "category": "dairy", "portion": 200},
{"name": "Сыр твердый", "calories": 340, "protein": 26.3, "fat": 26.1, "carbs": 0, "price": 126, "category": "dairy", "portion": 50},
{"name": "Йогурт 5%", "calories": 120, "protein": 5.7, "fat": 4.8, "carbs": 13.9, "price": 38, "category": "dairy", "portion": 125},
{"name": "Яйца (1 шт. ~60г)", "calories": 157, "protein": 12.7, "fat": 11.5, "carbs": 0.7, "price": 15.8, "category": "eggs", "portion": 60},
{"name": "Масло сливочное", "calories": 662, "protein": 1, "fat": 72.5, "carbs": 1.4, "price": 86, "category": "fats", "portion": 10},
{"name": "Масло подсолнечное", "calories": 899, "protein": 0, "fat": 100, "carbs": 0, "price": 16, "category": "fats", "portion": 10},
]
Указываем целевые значения и дополнительные списки с несочетаемыми продуктами
TARGET_CALORIES = 2000
TARGET_PROTEIN = 75
TARGET_FAT = 67
TARGET_CARBS = 275
MEAL_PRODUCTS = {
"breakfast": 3,
"lunch": 4,
"dinner": 3,
} # Кол-во продуктов на прием пищи
# Категории, которые не должны повторяться в одном приеме пищи
EXCLUSIVE_CATEGORIES = ["meat", "fish", "grains", "dairy"]
# Категории, которые не должны встречаться в одном приеме пищи
INCOMPATIBLE_CATEGORIES = [
["meat", "fish"],
["fish", "dairy"],
["vegetables", "dairy"],
["fats", "dairy"],
]
INCOMPATIBLE_CATEGORIES = [set(x) for x in INCOMPATIBLE_CATEGORIES]
Особь (хромосома) у нас будет являться списком списков (приемов пищи). Внутри списоков приемов пищи будут находиться гены - продукты питания.
# Создаем структуру особи
creator.create("FitnessMin", base.Fitness, weights=(-1.0,)) # Минимизируем функцию
creator.create("Individual", list, fitness=creator.FitnessMin)
toolbox = base.Toolbox()
# Генерация случайного приема пищи с учетом ограничений
def create_meal(meal_type):
meal = []
categories_in_meal = set()
for _ in range(MEAL_PRODUCTS[meal_type]):
# Фильтруем продукты, чтобы не было конфликтов категорий
available_products = [
p
for p in products
if (
p["category"] not in EXCLUSIVE_CATEGORIES
or p["category"] not in categories_in_meal
)
]
if not available_products:
break
product = random.choice(available_products)
meal.append(product)
if product["category"] in EXCLUSIVE_CATEGORIES:
categories_in_meal.add(product["category"])
return meal
# Создание особи: [завтрак, обед, ужин]
def create_individual():
individual = []
used_products = set()
for meal_type in ["breakfast", "lunch", "dinner"]:
while True:
meal = create_meal(meal_type)
meal_product_names = {p["name"] for p in meal}
# Проверяем, что продукты не повторяются в течение дня
if not meal_product_names & used_products:
individual.append(meal)
used_products.update(meal_product_names)
break
return individual
toolbox.register("individual", tools.initIterate, creator.Individual, create_individual)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
# пример создания случайного приема пищи
create_meal("breakfast")
В фитнес-функции будем высчитывать общее содержание БЖУ для всех приемов пищи за день и высчитывать отклонение от целевых параметров. Также будем учитывать цену продуктов питания и содержание в одном приеме пищи несочетаемых продуктов (например, и рыба и мясо в одном приеме пищи, или молочная продукция и овощи). Так как у нас есть заданные оптимальные параметры БЖУ, то нам будет удобнее взять эти параметры за нулевую точку и отталкиваться от нее - будем искать минимум отклонения от целевых значений.
# Фитнес-функция
def evaluate(individual):
total_calories = 0
total_protein = 0
total_fat = 0
total_carbs = 0
total_price = 0
penalty = 0
# Собираем все продукты дня
all_products = []
for meal in individual:
all_products.extend(meal)
# Проверка на уникальность продуктов
if len([p["name"] for p in all_products]) != len(set([p["name"] for p in all_products])):
penalty += 1000
# Проверка на конфликты категорий в приемах пищи
for meal in individual:
categories = set()
all_categories_set = set()
for p in meal:
all_categories_set.add(p["category"])
if p["category"] in EXCLUSIVE_CATEGORIES:
if p["category"] in categories:
penalty += 500 # Штраф за повторную категорию
categories.add(p["category"])
for incompatible_cats in INCOMPATIBLE_CATEGORIES:
if len(incompatible_cats & all_categories_set) == len(incompatible_cats):
penalty += 1000 # штраф за сочетание несочетаемых продуктов
# Расчет нутриентов
for product in all_products:
total_calories += product["calories"]
total_protein += product["protein"]
total_fat += product["fat"]
total_carbs += product["carbs"]
total_price += product["price"]
# Штраф за отклонение от норм
calorie_penalty = abs(total_calories - TARGET_CALORIES) * 10
protein_penalty = abs(total_protein - TARGET_PROTEIN) * 5
fat_penalty = abs(total_fat - TARGET_FAT) * 5
carbs_penalty = abs(total_carbs - TARGET_CARBS) * 2
fitness = total_price + (calorie_penalty + protein_penalty + fat_penalty + carbs_penalty) + penalty
return (fitness,)
toolbox.register("evaluate", evaluate)
# пример генерации хромосомы (рациона на день)
daily_meals = create_individual()
pprint(daily_meals)
# расчет фитнес-функции
val = evaluate(daily_meals)
val
Опишем функции мутации, будем с определенной вероятностью менять ген (продукт) в приеме пищи
# функция мутации с учетом несочетаемых продуктов
def mutate_individual(individual, indpb):
for i, meal in enumerate(individual):
if random.random() < indpb:
# Создаем новый прием пищи
meal_type = ["breakfast", "lunch", "dinner"][i]
new_meal = create_meal(meal_type)
# Проверяем на уникальность продуктов
other_meals = [m for j, m in enumerate(individual) if j != i]
other_products = [p for m in other_meals for p in m]
new_product_names = {p["name"] for p in new_meal}
if not new_product_names & {p["name"] for p in other_products}:
individual[i] = new_meal
return individual,
# функция мутации без учета сочетаемости
def mutate_individual2(individual, indpb):
for i, meal in enumerate(individual):
if random.random() < indpb:
# Создаем новый прием пищи
meal_type = ["breakfast", "lunch", "dinner"][i]
new_meal = create_meal(meal_type)
individual[i] = new_meal
return individual,
toolbox.register("mutate", mutate_individual2, indpb=0.3)
Функции кроссинговера и селекции возьмем стандартные, которые предлагает библиотека.
В кроссинговере происходит обмен приемами пищи, а в селекции будем отбирать 3 лучших индивида.
toolbox.register("mate", tools.cxTwoPoint)
toolbox.register("select", tools.selTournament, tournsize=3)
Запускаем работу алгоритма и указываем количество особей в популяции 100 штук. Количество поколений - 100.
# Запуск алгоритма
population = toolbox.population(n=100)
stats = tools.Statistics(lambda ind: ind.fitness.values)
stats.register("min", np.min)
result, logbook = algorithms.eaSimple(
population, toolbox, cxpb=0.7, mutpb=0.3, ngen=100, stats=stats, verbose=True
)
Вывод лучшего решения
best_individual = tools.selBest(result, k=1)[0]
print("\n=== Оптимальный рацион на день ===")
meal_names = ["Завтрак", "Обед", "Ужин"]
total_cost = 0
total_calories = 0
total_protein = 0
total_fat = 0
total_carbs = 0
for i, meal in enumerate(best_individual):
print(f"\n{meal_names[i]}:")
meal_calories = 0
meal_protein = 0
meal_fat = 0
meal_carbs = 0
meal_cost = 0
for product in meal:
portion = product["portion"]
calories = (portion / 100) * product["calories"]
protein = (portion / 100) * product["protein"]
fat = (portion / 100) * product["fat"]
carbs = (portion / 100) * product["carbs"]
cost = (portion / 100) * product["price"]
print(
f" {product['name']}: {portion:.0f} г ({calories:.0f} ккал, {cost:.1f} руб.)"
)
meal_calories += calories
meal_protein += protein
meal_fat += fat
meal_carbs += carbs
meal_cost += cost
total_cost += meal_cost
total_calories += meal_calories
total_protein += meal_protein
total_fat += meal_fat
total_carbs += meal_carbs
print(
f" Итого: {meal_calories:.0f} ккал, {meal_protein:.1f} г белков, {meal_fat:.1f} г жиров, {meal_carbs:.1f} г углеводов, {meal_cost:.1f} руб."
)
print("\n=== Суточная сумма ===")
print(f"• Калории: {total_calories:.0f} (цель: {TARGET_CALORIES})")
print(f"• Белки: {total_protein:.1f} г (цель: {TARGET_PROTEIN})")
print(f"• Жиры: {total_fat:.1f} г (цель: {TARGET_FAT})")
print(f"• Углеводы: {total_carbs:.1f} г (цель: {TARGET_CARBS})")
print(f"• Стоимость: {total_cost:.1f} руб.")
График сходимости
gen = logbook.select("gen")
fit_min = logbook.select("min")
plt.plot(gen, fit_min, "b-", label="Минимальная ошибка")
plt.xlabel("Поколение")
plt.ylabel("Фитнес-функция")
plt.title("Оптимизация рациона на день")
plt.legend()
plt.show()