遺伝的アルゴリズムで最適なドリンク成分を生成するツールを作成してみた

# 経緯説明

最近駅でポカリのイオンウォーターを飲んだとき、通常のポカリと少し味が違うと感じました。電車で考えて、一つのidea が出て、それが遺伝的アルゴリズムを使って最適なドリンク成分を生成することができるでは？
実際に試してみることにしました。

:::note alert
これは実際に生成したものをもとにドリンクを作っていないので、したこともないし中身の材料も不明なので、あくまで趣味範囲で遊んでたものをまとめて書いてた記事です。
参考用です！！！！
:::

# What is 美味しさ
お腹すいだね。
source from : http://www.foodyana.sakura.ne.jp/oishisa/knowledge/definition.htm

# 手順説明

まず、遺伝的アルゴリズムを用いてドリンク成分を最適化するための基本的な設定を行います。その後、Tkinterを使ってユーザーが評価基準と遺伝的アルゴリズムのパラメータを調整できるGUIを作成しました。
遺伝的アルゴリズム (GA) の説明
初期集団の生成

# What is GA 遺伝的アルゴリズム

どこでも資料あるので、以下はGPTの説明です。
遺伝的アルゴリズム (GA) は、自然界の進化の過程を模倣して最適化問題を解く手法です。以下はGAの主要なステップの説明です。

### 初期集団の生成

GAでは、まず初期集団をランダムに生成します。各個体は解候補であり、問題の異なる解の組み合わせを表しています。多様な解候補を持つ集団を形成することで、探索の幅を広げます。

### 評価

各個体の適応度（フィットネス）を評価関数を用いて計算します。評価関数は、問題固有の基準に基づいて各個体のスコアを計算します。このスコアが高い個体ほど、次世代に選ばれる確率が高くなります。

### 選択

適応度の高い個体を選択し、次世代の親として使用します。一般的な選択方法には、ルーレット選択、トーナメント選択、エリート選択などがあります。エリート選択では、適応度の高い個体の一部をそのまま次世代に引き継ぎます。

### 交叉（クロスオーバー）

親個体を組み合わせて新しい個体を生成します。交叉は、親の遺伝子を交換して子を作る操作で、一点交叉、二点交叉、均一交叉などの方法があります。これにより、親の特徴を受け継ぎつつ、新しい特徴を持つ子が生まれます。

### 突然変異

新しく生成された個体に対して、ランダムに遺伝子を変更する突然変異を適用します。これにより、集団内の遺伝的多様性を保ち、局所解に陥るのを防ぎます。

### 新しい集団の生成

交叉と突然変異によって生成された新しい個体を次世代の集団とします。これにより、集団全体が少しずつ進化していきます。

### 反復

上記のステップを所定の世代数まで繰り返します。各世代で最良の個体を記録し、最終的に最適な解を見つけ出します。


# このドリンク計画説明（今回のメイン）

今回のプロジェクトでは、遺伝的アルゴリズムを使用して最適なドリンク成分を見つけることを目指しています。具体的には、以下のようにGAの各ステップがこの計画に適用されています。

### 初期集団の生成

ドリンクの成分（電解質のバランス）をランダムに組み合わせた初期集団を生成します。

### 評価

各ドリンクの適応度を評価関数を用いて計算します。評価関数は、以下の要素に基づいています：
- 味覚プロファイル
- 栄養バランス
- 消費者の好み
- 飲料の効果

これらの要素はユーザーが設定する重みに基づいてスコア化されます。

### 選択

適応度の高いドリンク成分を選び、次世代の親として使用します。エリート選択を行い、上位20%の個体を次世代に引き継ぎます。

### 交叉（クロスオーバー）

親ドリンク成分を組み合わせて新しいドリンク成分を生成します。一点交叉を使用して、親の特徴を受け継ぐ新しい成分を作ります。

### 突然変異

新しく生成されたドリンク成分に対して、ランダムに成分のバランスを変更する突然変異を適用します。これにより、成分の多様性を保ち、最適解を見つけやすくします。

### 新しい集団の生成

交叉と突然変異によって生成された新しいドリンク成分を次世代の集団とします。

### 反復

上記のステップを所定の世代数まで繰り返し、最適なドリンク成分を見つけます。各世代で最良のドリンク成分を記録し、最終的に最も適応度の高い成分をユーザーに提供します。

このように、遺伝的アルゴリズムの各ステップを通じて、ユーザーが指定した評価基準に基づく最適なドリンク成分を見つけ出すことができます。
Tkinterを使って評価基準やアルゴリズムのパラメータを調整し、最適化プロセスを直感的に実行できます。

遺伝的アルゴリズムでは、まず初期集団をランダムに生成します。この集団は解候補の集合であり、それぞれがドリンクの異なる成分組み合わせを表しています。
評価

（美味しさ定義によると）
各個体（解候補）の適応度を評価関数に基づいて計算します。評価関数は以下の要素から構成されます：

    味覚プロファイル：ドリンクの味に関する要素。
    栄養バランス：ミネラルやビタミンなどの栄養素のバランス。
    消費者の好み：消費者の嗜好に基づく要素。
    飲料の効果：電解質バランスや水分補給効果などの機能性。

各要素にはユーザーが設定する重みがあり、これに基づいてスコアが計算されます。
選択

適応度の高い個体を選び、次世代の親として使用します。エリート選択を行い、上位20%をエリートとします。
交叉（クロスオーバー）

親個体を組み合わせて新しい個体を生成します。一点交叉を使用して、遺伝子を交換します。
突然変異

新しく生成された個体に対して、ランダムに遺伝子を変更する突然変異を適用します。これにより、解の多様性を確保します。
新しい集団の生成

交叉と突然変異によって生成された新しい個体を次世代の集団とします。
反復

上記のステップを所定の世代数まで繰り返します。

# Tkinter の説明
機能
    評価基準の選択と重み付け：
        味覚プロファイル、栄養バランス、消費者の好み、飲料の効果のチェックボックスとスライダーで評価基準とその重みを設定します。

    遺伝的アルゴリズムのパラメータ調整：
        初期集団のサイズ、交叉率、突然変異率をスライダーや入力フィールドで調整します。

    最適化結果の表示：
        最適化プロセス終了後、全世代で最良の評価値、最良の成分、および最良の世代を表示します。

UIの構成

    チェックボックスとスライダー：評価基準の有効化と重み付けの設定。
    入力フィールド：世代数の設定。
    スライダー：初期集団のサイズ、交叉率、突然変異率の設定。
    結果表示ラベル：最適化結果の表示。

操作手順

    評価基準を選択：必要な評価基準のチェックボックスをオンにし、スライダーで重みを設定します。
    アルゴリズムのパラメータを設定：初期集団のサイズ、交叉率、突然変異率を設定します。
    世代数を設定：世代数を入力フィールドに設定します。
    最適化開始：最適化開始ボタンをクリックして、最適化を開始します。
    結果確認：最適化プロセスが完了したら、結果表示ラベルに全世代で最良の評価値、最良の成分、および最良の世代が表示されます。

以上が、遺伝的アルゴリズムを使って最適なドリンク成分を生成するツールの全体像です。

# Code
```Python
import tkinter as tk
from tkinter import ttk
import numpy as np
import random

# 電解質の定義
electrolytes = {
    "Na+": 17.5,
    "K+": 5,
    "Ca2+": 1,
    "Mg2+": 0.5,
    "Cl-": 13.5,
    "citrate3-": 9,
    "lactate-": 1
}

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("ドリンク最適化ツール")
        
        # 各要素のチェックボックスと重みを設定
        self.check_vars = {
            "味覚プロファイル": tk.BooleanVar(),
            "栄養バランス": tk.BooleanVar(),
            "消費者の好み": tk.BooleanVar(),
            "飲料の効果": tk.BooleanVar(),
        }
        
        self.weights = {
            "味覚プロファイル": tk.DoubleVar(value=1.0),
            "栄養バランス": tk.DoubleVar(value=1.0),
            "消費者の好み": tk.DoubleVar(value=1.0),
            "飲料の効果": tk.DoubleVar(value=1.0),
        }

        self.generations_var = tk.IntVar(value=100)  # 世代数の初期値を設定
        self.population_size_var = tk.IntVar(value=50)  # 初期集団のサイズ
        self.crossover_rate_var = tk.DoubleVar(value=0.7)  # 交叉の確率
        self.mutation_rate_var = tk.DoubleVar(value=0.2)  # 突然変異の確率

        self.best_solution_overall = None  # 全世代を通じての最良の解
        self.best_score_overall = -float('inf')  # 全世代を通じての最良の評価値
        self.best_generation = 0  # 最良の評価値を持つ世代
        self.num_parameters = len(electrolytes)  # パラメータの数

        self.create_widgets()  # ウィジェットの作成

    def create_widgets(self):
        row = 0
        self.check_labels = {}
        self.scale_labels = {}
        for label, var in self.check_vars.items():
            # 各要素のチェックボックスとスライダーを作成
            ttk.Checkbutton(self, text=label, variable=var, command=self.update_check_labels).grid(row=row, column=0, sticky=tk.W)
            scale = ttk.Scale(self, from_=0, to=10, variable=self.weights[label], orient=tk.HORIZONTAL, command=lambda val, l=label: self.update_scale_label(l, val))
            scale.grid(row=row, column=1)
            self.check_labels[label] = ttk.Label(self, text=f"{label}: {var.get()}")
            self.check_labels[label].grid(row=row, column=2)
            self.scale_labels[label] = ttk.Label(self, text=f"{self.weights[label].get():.1f}")
            self.scale_labels[label].grid(row=row, column=3)
            row += 1

        # 世代数の入力フィールドを作成
        ttk.Label(self, text="世代数").grid(row=row, column=0, sticky=tk.W)
        ttk.Entry(self, textvariable=self.generations_var).grid(row=row, column=1)
        row += 1

        # 初期集団のサイズ設定
        ttk.Label(self, text="初期集団のサイズ").grid(row=row, column=0, sticky=tk.W)
        ttk.Entry(self, textvariable=self.population_size_var).grid(row=row, column=1)
        row += 1

        # 交叉の確率設定
        ttk.Label(self, text="交叉の確率").grid(row=row, column=0, sticky=tk.W)
        tk.Scale(self, from_=0, to=1, resolution=0.01, variable=self.crossover_rate_var, orient=tk.HORIZONTAL, command=lambda val: self.update_crossover_rate_label(val)).grid(row=row, column=1)
        self.crossover_rate_label = ttk.Label(self, text=f"{self.crossover_rate_var.get():.2f}")
        self.crossover_rate_label.grid(row=row, column=2)
        row += 1

        # 突然変異の確率設定
        ttk.Label(self, text="突然変異の確率").grid(row=row, column=0, sticky=tk.W)
        tk.Scale(self, from_=0, to=1, resolution=0.01, variable=self.mutation_rate_var, orient=tk.HORIZONTAL, command=lambda val: self.update_mutation_rate_label(val)).grid(row=row, column=1)
        self.mutation_rate_label = ttk.Label(self, text=f"{self.mutation_rate_var.get():.2f}")
        self.mutation_rate_label.grid(row=row, column=2)
        row += 1

        # パラメータ数の表示
        ttk.Label(self, text=f"パラメータ数: {self.num_parameters}").grid(row=row, column=0, sticky=tk.W)
        row += 1

        # 最適化開始ボタンを作成
        ttk.Button(self, text="最適化開始", command=self.start_optimization).grid(row=row, columnspan=2)
        row += 1

        # 最適化結果表示ラベルを追加
        self.result_label = ttk.Label(self, text="")
        self.result_label.grid(row=row, column=0, columnspan=4)
    
    def update_check_labels(self):
        # チェックボックスの状態に応じてラベルを更新
        for label, var in self.check_vars.items():
            self.check_labels[label].config(text=f"{label}: {var.get()}")

    def update_scale_label(self, label, value):
        # スライダーの値に応じてラベルを更新
        self.scale_labels[label].config(text=f"{float(value):.1f}")

    def update_crossover_rate_label(self, value):
        # 交叉の確率スライダーの値に応じてラベルを更新
        self.crossover_rate_label.config(text=f"{float(value):.2f}")

    def update_mutation_rate_label(self, value):
        # 突然変異の確率スライダーの値に応じてラベルを更新
        self.mutation_rate_label.config(text=f"{float(value):.2f}")

    def start_optimization(self):
        # 最適化の設定を取得
        self.selected_elements = {k: v.get() for k, v in self.check_vars.items()}
        self.selected_weights = {k: v.get() for k, v in self.weights.items()}
        generations = self.generations_var.get()
        population_size = self.population_size_var.get()
        crossover_rate = self.crossover_rate_var.get()
        mutation_rate = self.mutation_rate_var.get()
        print("選択された要素:", self.selected_elements)
        print("選択された重み:", self.selected_weights)
        print("世代数:", generations)
        print("初期集団のサイズ:", population_size)
        print("交叉の確率:", crossover_rate)
        print("突然変異の確率:", mutation_rate)
        self.generations = generations  # 世代数を設定
        self.population_size = population_size  # 初期集団のサイズを設定
        self.crossover_rate = crossover_rate  # 交叉の確率を設定
        self.mutation_rate = mutation_rate  # 突然変異の確率を設定
        self.best_score_overall = -float('inf')  # 最良の評価値の初期化
        self.best_solution_overall = None  # 最良の解の初期化
        self.best_generation = 0  # 最良の世代の初期化
        # 初期集団の生成
        self.population = [self.random_solution() for _ in range(self.population_size)]
        # 遺伝的アルゴリズムの実行
        for generation in range(generations):
            self.run_genetic_algorithm(generation)
        
        # 全世代を通じての最良の評価値と成分を表示
        result_text = (f"全世代を通じて最良の評価値: {self.best_score_overall}\n"
                       f"全世代を通じて最良の成分: {self.best_solution_overall}\n"
                       f"最良の評価値を持つ世代: 世代 {self.best_generation + 1}")
        self.result_label.config(text=result_text)
        print(result_text)

    def run_genetic_algorithm(self, generation):
        # 新しい世代の生成
        new_population = []
        while len(new_population) < len(self.population):
            parents = self.selection()  # 親の選択
            if random.random() < self.crossover_rate:
                child1, child2 = self.crossover(parents[0], parents[1])  # 交叉
            else:
                child1, child2 = parents[0], parents[1]
            if random.random() < self.mutation_rate:
                child1 = self.mutation(child1)  # 突然変異
            if random.random() < self.mutation_rate:
                child2 = self.mutation(child2)
            new_population.extend([child1, child2])
        self.population = new_population[:len(self.population)]

        # 現世代の評価
        scores = [self.evaluate_solution(individual) for individual in self.population]
        best_score = max(scores)
        best_solution = self.population[np.argmax(scores)]
        print(f"世代 {generation + 1}: 最良の評価値 = {best_score}, 最良の成分 = {best_solution}")
        
        # 全世代を通じての最良の評価値を更新
        if best_score > self.best_score_overall:
            self.best_score_overall = best_score
            self.best_solution_overall = best_solution
            self.best_generation = generation

    def random_solution(self):
        # ランダムな初期解を生成
        solution = {electrolyte: random.uniform(0.8, 1.2) * value for electrolyte, value in electrolytes.items()}
        return solution

    def selection(self):
        # エリート選択
        scores = [self.evaluate_solution(individual) for individual in self.population]
        sorted_population = [x for _, x in sorted(zip(scores, self.population), key=lambda pair: pair[0], reverse=True)]
        elites = sorted_population[:int(self.population_size * 0.2)]  # 上位20%をエリートとする
        remaining = sorted_population[int(self.population_size * 0.2):]
        return random.choices(elites, k=1) + random.choices(remaining, k=1)

    def crossover(self, parent1, parent2):
        # 一点交叉
        crossover_point = random.randint(1, len(parent1)-1)
        child1 = {**{k: parent1[k] for k in list(parent1)[:crossover_point]}, **{k: parent2[k] for k in list(parent2)[crossover_point:]}}
        child2 = {**{k: parent2[k] for k in list(parent2)[:crossover_point]}, **{k: parent1[k] for k in list(parent1)[crossover_point:]}}
        return child1, child2

    def mutation(self, individual):
        # 突然変異
        key = random.choice(list(individual.keys()))
        individual[key] = individual[key] * random.uniform(0.8, 1.2)
        return individual

    def evaluate_solution(self, solution):
        # 解の評価
        elements = self.selected_elements
        weights = self.selected_weights
        score = 0
        if elements.get("味覚プロファイル"):
            score += (solution["Cl-"] / electrolytes["Cl-"]) * weights["味覚プロファイル"] * 10
        if elements.get("栄養バランス"):
            balance = (solution["K+"] + solution["Ca2+"] + solution["Mg2+"]) / (electrolytes["K+"] + electrolytes["Ca2+"] + electrolytes["Mg2+"])
            score += balance * weights["栄養バランス"] * 10
        if elements.get("消費者の好み"):
            score += random.uniform(0, 1) * weights["消費者の好み"] * 10
        if elements.get("飲料の効果"):
            effect = (solution["Na+"] + solution["K+"] + solution["Cl-"]) / (electrolytes["Na+"] + electrolytes["K+"] + electrolytes["Cl-"])
            score += effect * weights["飲料の効果"] * 10
        return score

if __name__ == "__main__":
    app = App()
    app.mainloop()

```
# 実行結果の画像

![pokariGA01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3506235/00103b7c-11cd-e817-7989-e55ac5b716df.png)

# まとめ
今回も突然現れたIDEAなので、実際どれくらいの可用性は不明です。
最後まで読んでくれてありがとうございました。