Files
AIM-PIbd-31-Potapov-N-S/lab_10/lab10.ipynb

55 KiB
Raw Blame History

Лабортароная работа №10. Генетический алгоритм

Задача оптимизации: подбор оптимального рациона на день (завтрак, обед и ужин) по содержанию белков, жиров и углеводов (БЖУ).

In [1]:
import random
import numpy as np
from pprint import pprint
from deap import base, creator, tools, algorithms
import matplotlib.pyplot as plt

Список с продуктами и информацией по содержанию в них БЖУ (а также цене и предположительной разовой порции)

In [2]:
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},
]

Указываем целевые значения и дополнительные списки с несочетаемыми продуктами

In [3]:
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]

Особь (хромосома) у нас будет являться списком списков (приемов пищи). Внутри списоков приемов пищи будут находиться гены - продукты питания.

In [4]:
# Создаем структуру особи
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)
In [5]:
# пример создания случайного приема пищи
create_meal("breakfast")
Out[5]:
[{'name': 'Сыр твердый',
  'calories': 340,
  'protein': 26.3,
  'fat': 26.1,
  'carbs': 0,
  'price': 126,
  'category': 'dairy',
  'portion': 50},
 {'name': 'Перловка',
  'calories': 317,
  'protein': 10.4,
  'fat': 1.3,
  'carbs': 67,
  'price': 3,
  'category': 'grains',
  'portion': 100},
 {'name': 'Индейка',
  'calories': 135,
  'protein': 29,
  'fat': 1.7,
  'carbs': 0,
  'price': 70,
  'category': 'meat',
  'portion': 150}]

В фитнес-функции будем высчитывать общее содержание БЖУ для всех приемов пищи за день и высчитывать отклонение от целевых параметров. Также будем учитывать цену продуктов питания и содержание в одном приеме пищи несочетаемых продуктов (например, и рыба и мясо в одном приеме пищи, или молочная продукция и овощи). Так как у нас есть заданные оптимальные параметры БЖУ, то нам будет удобнее взять эти параметры за нулевую точку и отталкиваться от нее - будем искать минимум отклонения от целевых значений.

In [6]:
# Фитнес-функция
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)
In [7]:
# пример генерации хромосомы (рациона на день)
daily_meals = create_individual()
pprint(daily_meals)
[[{'calories': 60,
   'carbs': 4.7,
   'category': 'dairy',
   'fat': 3.2,
   'name': 'Молоко',
   'portion': 200,
   'price': 9.5,
   'protein': 3},
  {'calories': 191,
   'carbs': 0,
   'category': 'fish',
   'fat': 13.2,
   'name': 'Скумбрия',
   'portion': 150,
   'price': 106,
   'protein': 18},
  {'calories': 47,
   'carbs': 9.8,
   'category': 'fruits',
   'fat': 0.4,
   'name': 'Яблоки',
   'portion': 150,
   'price': 18,
   'protein': 0.4}],
 [{'calories': 90,
   'carbs': 3.5,
   'category': 'dairy',
   'fat': 0.5,
   'name': 'Творог 0.5%',
   'portion': 150,
   'price': 38,
   'protein': 18},
  {'calories': 242,
   'carbs': 0,
   'category': 'meat',
   'fat': 17,
   'name': 'Свинина',
   'portion': 120,
   'price': 34,
   'protein': 21},
  {'calories': 96,
   'carbs': 21,
   'category': 'fruits',
   'fat': 0.5,
   'name': 'Бананы',
   'portion': 120,
   'price': 19,
   'protein': 1.5},
  {'calories': 28,
   'carbs': 4.7,
   'category': 'vegetables',
   'fat': 0.2,
   'name': 'Капуста белокочаная',
   'portion': 200,
   'price': 7,
   'protein': 1.8}],
 [{'calories': 34,
   'carbs': 7.0,
   'category': 'vegetables',
   'fat': 0.4,
   'name': 'Брокколи',
   'portion': 200,
   'price': 40,
   'protein': 2.8},
  {'calories': 18,
   'carbs': 2.7,
   'category': 'vegetables',
   'fat': 0.2,
   'name': 'Помидоры',
   'portion': 200,
   'price': 40,
   'protein': 0.9},
  {'calories': 43,
   'carbs': 8.1,
   'category': 'fruits',
   'fat': 0.2,
   'name': 'Апельсины',
   'portion': 150,
   'price': 15,
   'protein': 0.9}]]
In [8]:
# расчет фитнес-функции
val = evaluate(daily_meals)
val
Out[8]:
(14453.0,)

Опишем функции мутации, будем с определенной вероятностью менять ген (продукт) в приеме пищи

In [9]:
# функция мутации с учетом несочетаемых продуктов
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 лучших индивида.

In [10]:
toolbox.register("mate", tools.cxTwoPoint)
toolbox.register("select", tools.selTournament, tournsize=3)

Запускаем работу алгоритма и указываем количество особей в популяции 100 штук. Количество поколений - 100.

In [11]:
# Запуск алгоритма
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
)
gen	nevals	min   
0  	100   	1444.7
1  	79    	1349.9
2  	76    	703.8 
3  	83    	703.8 
4  	81    	703.8 
5  	79    	703.8 
6  	71    	703.8 
7  	76    	703.8 
8  	73    	703.8 
9  	78    	703.8 
10 	75    	703.8 
11 	82    	703.8 
12 	73    	703.8 
13 	80    	586   
14 	69    	586   
15 	87    	586   
16 	76    	586   
17 	82    	586   
18 	80    	586   
19 	70    	586   
20 	84    	586   
21 	79    	586   
22 	71    	586   
23 	79    	586   
24 	73    	586   
25 	83    	586   
26 	72    	586   
27 	82    	586   
28 	86    	586   
29 	82    	586   
30 	78    	586   
31 	78    	586   
32 	68    	586   
33 	71    	586   
34 	82    	586   
35 	70    	586   
36 	76    	586   
37 	87    	586   
38 	76    	586   
39 	75    	586   
40 	86    	586   
41 	89    	586   
42 	82    	586   
43 	77    	586   
44 	80    	586   
45 	82    	586   
46 	82    	551   
47 	80    	551   
48 	81    	551   
49 	83    	551   
50 	77    	551   
51 	79    	551   
52 	78    	551   
53 	76    	551   
54 	79    	551   
55 	83    	551   
56 	81    	551   
57 	80    	551   
58 	79    	551   
59 	85    	551   
60 	86    	551   
61 	83    	551   
62 	74    	551   
63 	77    	442.3 
64 	78    	442.3 
65 	78    	442.3 
66 	89    	442.3 
67 	84    	442.3 
68 	83    	442.3 
69 	78    	442.3 
70 	76    	442.3 
71 	78    	442.3 
72 	84    	442.3 
73 	81    	442.3 
74 	76    	442.3 
75 	84    	442.3 
76 	72    	442.3 
77 	85    	442.3 
78 	83    	442.3 
79 	82    	442.3 
80 	80    	442.3 
81 	80    	442.3 
82 	80    	442.3 
83 	76    	442.3 
84 	74    	442.3 
85 	78    	442.3 
86 	79    	442.3 
87 	73    	442.3 
88 	78    	442.3 
89 	82    	442.3 
90 	74    	442.3 
91 	75    	442.3 
92 	78    	442.3 
93 	87    	442.3 
94 	82    	442.3 
95 	76    	442.3 
96 	75    	442.3 
97 	83    	442.3 
98 	71    	442.3 
99 	87    	442.3 
100	86    	442.3 

Вывод лучшего решения

In [12]:
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} руб.")
=== Оптимальный рацион на день ===

Завтрак:
  Перловка: 100 г (317 ккал, 3.0 руб.)
  Помидоры: 200 г (36 ккал, 80.0 руб.)
  Капуста белокочаная: 200 г (56 ккал, 14.0 руб.)
  Итого: 409 ккал, 15.8 г белков, 2.1 г жиров, 81.8 г углеводов, 97.0 руб.

Обед:
  Курица (грудка): 150 г (248 ккал, 45.0 руб.)
  Яблоки: 150 г (70 ккал, 27.0 руб.)
  Овсянка: 80 г (282 ккал, 3.6 руб.)
  Морковь: 150 г (62 ккал, 1.4 руб.)
  Итого: 661 ккал, 58.3 г белков, 11.3 г жиров, 79.1 г углеводов, 76.9 руб.

Ужин:
  Рис: 100 г (325 ккал, 6.8 руб.)
  Масло сливочное: 10 г (66 ккал, 8.6 руб.)
  Апельсины: 150 г (64 ккал, 22.5 руб.)
  Итого: 456 ккал, 8.3 г белков, 8.6 г жиров, 84.5 г углеводов, 37.9 руб.

=== Суточная сумма ===
• Калории: 1526 (цель: 2000)
• Белки: 82.4 г (цель: 75)
• Жиры: 21.9 г (цель: 67)
• Углеводы: 245.4 г (цель: 275)
• Стоимость: 211.8 руб.

График сходимости

In [13]:
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()
No description has been provided for this image