Rで 遺伝的アルゴリズム を試みます。
1. 遺伝的アルゴリズムとは?
遺伝的アルゴリズムとは、生物の「進化」の仕組みを模倣した問題解決手法(アルゴリズム)です。特に、非常に多くの選択肢の中から最適な答え(最適解)を見つけ出す「最適化問題」を解くのに利用されます。
一言でいうと、「優秀な個体を生き残らせ、交配させることで、世代を重ねるごとに、より優れた解の集団へと進化させていく」というアプローチです。
2. 生物の進化との対応(アナロジー)
このアルゴリズムを理解する鍵は、生物の進化の仕組みと対比させることです。
生物の進化 | 遺伝的アルゴリズム | 説明 |
---|---|---|
遺伝子 (Gene) | 遺伝子 (Gene) | 解を構成する最小単位(例:0か1のビット) |
染色体 (Chromosome) | 染色体 (Chromosome) | 遺伝子の集まり。一つの「解の候補」を表す。 |
個体 (Individual) | 個体 (Individual) | 染色体と同じ。一つの「解の候補」。 |
生物の集団 (Population) | 集団 (Population) | 複数の個体(解の候補)の集まり。 |
環境への適応度 (Fitness) | 適応度 (Fitness) | その解がどれだけ優れているかを示す評価値。 |
自然淘汰 (Natural Selection) | 選択 (Selection) | 適応度の高い(優秀な)個体を次世代に残すために選ぶこと。 |
交配 (Crossover) | 交叉 (Crossover) | 2つの親個体の遺伝子を組み合わせて、新しい子個体を作ること。 |
突然変異 (Mutation) | 突然変異 (Mutation) | 子個体の遺伝子をランダムに少しだけ変化させること。 |
世代 (Generation) | 世代 (Generation) | アルゴリズムの1回の繰り返し(ループ)のこと。 |
3. 遺伝的アルゴリズムの流れ
遺伝的アルゴリズムは、以下のステップを繰り返すことで、徐々に解を良くしていきます。
ステップ1:初期集団の生成(Initialization)
まず、ランダムに多数の「個体(解の候補)」を生成します。これが第一世代の集団となります。
ステップ2:適応度の評価(Evaluation)
各個体が、問題の解としてどれだけ優れているかを「適応度関数」という物差しで評価し、点数をつけます。
ステップ3:選択(Selection)
適応度の高い(優秀な)個体を、次世代の親として選択します。優秀な個体ほど選ばれる確率が高くなります。(例:ルーレット選択、トーナメント選択など)
ステップ4:交叉(Crossover)
選択された親のペアから、それぞれの遺伝子(染色体)の一部を交換して、新しい「子」の個体を作ります。これにより、親の優れた特徴を組み合わせた、新しい解の候補が生まれます。
親A: [1, 1, 1, | 0, 0, 0]
親B: [0, 0, 0, | 1, 1, 1]
↓ 交叉
子C: [1, 1, 1, | 1, 1, 1] // 親Aの前半と親Bの後半を組み合わせ
子D: [0, 0, 0, | 0, 0, 0] // 親Bの前半と親Aの後半を組み合わせ
ステップ5:突然変異(Mutation)
生成された子の遺伝子を、ごく低い確率でランダムに変化させます。これにより、集団が画一的になるのを防ぎ、探索の多様性を維持します。(局所解に陥るのを防ぐ効果があります)
変化前の子: [1, 1, 0, 1, 1, 0]
↓ 突然変異
変化後の子: [1, 1, 0, 0, 1, 0] // 4番目の遺伝子が1から0に変化
ステップ6:世代交代
こうして作られた新しい個体で、古い世代の集団を置き換えます。
上記のステップ2~6を、決められた世代数に達するか、満足のいく解が見つかるまで繰り返します。 最終的に、その時点で最も適応度が高い個体が、この問題の「最適解(に近い解)」となります。
4. 具体例:ナップサック問題
遺伝的アルゴリズムがどう使われるか、簡単な例で見てみましょう。
【問題】 容量15kgのナップサックがあります。価値と重さが異なる5つの品物から、ナップサックに入る範囲で、合計価値が最大になるように品物を選びます。
品物 | 価値(万円) | 重さ(kg) |
---|---|---|
A | 1 | 1 |
B | 4 | 12 |
C | 8 | 4 |
D | 9 | 2 |
E | 3 | 1 |
【遺伝的アルゴリズムでの表現】
- 遺伝子: 各品物を「入れる(1)」か「入れない(0)」かで表現します。
- 染色体: 5つの品物の組み合わせを表す、長さ5の0/1の列です。 例:
[1, 0, 1, 1, 0]
→ 品物A, C, D を入れる - 適応度関数:
- 染色体から合計の重さと価値を計算する。
- もし合計の重さが15kgを超えていたら、適応度は0(ペナルティ)。
- 超えていなければ、適応度は「合計価値」そのもの。
【実行イメージ】
- 初期集団:
[1,1,1,1,1]
,[0,0,1,1,0]
,[1,0,1,0,1]
などをランダムに多数生成。 - 評価:
-
[1,1,1,1,1]
→ 重さ20kg > 15kg → 適応度0 -
[0,0,1,1,0]
→ 重さ6kg, 価値17万円 → 適応度17 -
[1,0,1,0,1]
→ 重さ6kg, 価値12万円 → 適応度12
-
- 選択: 適応度の高い
[0,0,1,1,0]
などが親として選ばれやすくなる。 - 交叉・突然変異: 選ばれた親から新しい組み合わせ(例:
[1,0,1,1,1]
など)が作られる。 - 世代交代: これを繰り返すと、徐々に「重すぎず、価値が高い」組み合わせ(例:
[0,1,0,1,1]
重さ15kg, 価値16万円 や[1,0,1,1,1]
重さ8kg, 価値21万円など)が生き残り、最終的に最適解[1,0,1,1,1]
が見つかる可能性が高まります。
5. メリットとデメリット
メリット
- 幅広い問題に適用可能: 微分不可能な関数や、複雑でルールが明確でない問題にも使えます。
- 局所解からの脱出: 突然変異などにより、部分的に良いだけの解(局所解)に陥りにくく、全体として最も良い解(大域的最適解)を見つけやすいです。
- 実装が比較的容易: アルゴリズムの基本的な考え方がシンプルです。
デメリット
- 必ず最適解が見つかる保証はない: あくまで確率的な探索なので、最適解が見つからないこともあります。
- 計算コストが高い: 多数の個体を評価するため、計算時間がかかることがあります。
- パラメータ設定が難しい: 集団のサイズ、交叉率、突然変異率などのパラメータを適切に設定しないと、良い結果が得られないことがあります。
6. シミュレーションのシナリオ:『究極のラーメンレシピ探求!』
あなたは新進気鋭のラーメン店の店主です。 お店の看板メニューとなる「究極の一杯」を開発するため、様々な食材の組み合わせを試すことにしました。
しかし、闇雲に試すだけでは時間もお金もかかってしまいます。そこであなたは、生物の進化の仕組みを応用した「遺伝的アルゴリズム」を使って、最高のレシピを効率的に見つけ出すことを思いつきました。
【ルール】
レシピの構成要素(遺伝子):
ラーメンは「麺」、「スープ」、「肉」、「トッピング」の4つの要素で構成されます。それぞれの要素にはいくつかの選択肢があります。
レシピ(個体):
4つの要素の組み合わせが、一つの「ラーメンレシピ」となります。
例:
[細麺, とんこつスープ, 豚バラチャーシュー, 味玉]
評価(適応度):
各食材には、お客さんの「満足度ポイント」と「原価(コスト)」が設定されています。
- 予算: 1杯あたりの原価は700円まで。
- 評価方法:
- 原価が700円を超えたレシピは「失格(満足度0)」となります。
- 予算内のレシピは、食材の「合計満足度ポイント」が評価点となります。
目標:
予算内で、お客さんの満足度が最も高くなる「究極のラーメンレシピ」を見つけ出すこと。
【進化のプロセス】
- 初期集団: まずは、ランダムにたくさんのレシピ(第一世代)を作ります。
- 選択: 満足度の高いレシピが、次世代のレシピの「親」として選ばれやすくなります。
- 交叉: 選ばれた2つの「親レシピ」の良いところを組み合わせて、新しい「子レシピ」を作ります。(例:Aの麺とBのスープを組み合わせる)
- 突然変異: ごく稀に、レシピの一部が全く新しい食材に変わることがあります。これにより、思いもよらない美味しい組み合わせが生まれるかもしれません。
この「選択 → 交叉 → 突然変異」を何世代も繰り返すことで、レシピはどんどん洗練され、究極の一杯へと進化していきます。さあ、シミュレーションを始めましょう!
7. R言語によるシミュレーションコード
library(dplyr)
set.seed(20250716)
# ===================================================================
# 1. シナリオ設定
# ===================================================================
cat("ようこそ、ラーメン店主!これから遺伝的アルゴリズムで究極のレシピを探します。\n\n")
# --- 食材リストの定義 ---
# 各食材に「満足度」と「コスト(円)」を設定します
# 1:麺, 2:スープ, 3:肉, 4:トッピング
<- data.frame(
ingredients category_id = c(1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4),
item_id = c(1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4),
name = c(
"細麺", "中太麺", "ちぢれ麺", "極太麺",
"醤油スープ", "味噌スープ", "とんこつスープ", "塩スープ",
"豚バラチャーシュー", "鶏チャーシュー", "角煮", "肉なし",
"味玉", "メンマ", "たっぷりネギ", "のり"
),satisfaction = c(70, 80, 85, 90, 80, 85, 100, 75, 100, 80, 120, 0, 80, 40, 60, 20),
cost = c(100, 110, 120, 130, 150, 160, 200, 140, 200, 160, 250, 0, 100, 50, 80, 30)
)
# --- GAのパラメータ設定 ---
<- 700 # 予算(円)
BUDGET <- 50 # 集団のサイズ(一度に考えるレシピの数)
POPULATION_SIZE <- 30 # 世代数
GENERATIONS <- 0.1 # エリート選択率(優秀なレシピをそのまま残す割合)
ELITISM_RATE <- 0.05 # 突然変異の確率
MUTATION_RATE
# ===================================================================
# 2. 遺伝的アルゴリズムの関数定義
# ===================================================================
# --- レシピ(個体)から詳細情報を取得する関数 ---
<- function(recipe) {
get_recipe_details # recipeは [1, 2, 3, 4] のような、各カテゴリのitem_idを持つベクトル
<- ingredients %>%
details filter((category_id == 1 & item_id == recipe[1]) |
== 2 & item_id == recipe[2]) |
(category_id == 3 & item_id == recipe[3]) |
(category_id == 4 & item_id == recipe[4]))
(category_id return(details)
}
# --- 適応度(満足度)を計算する関数 ---
<- function(recipe) {
calculate_fitness <- get_recipe_details(recipe)
details <- sum(details$cost)
total_cost <- sum(details$satisfaction)
total_satisfaction
# 予算オーバーのレシピは評価0(失格)
if (total_cost > BUDGET) {
return(0)
else {
} return(total_satisfaction)
}
}
# --- 初期集団(最初のレシピ集団)を生成する関数 ---
<- function(size) {
create_initial_population <- list()
population for (i in 1:size) {
# 各カテゴリからランダムに1つずつ食材を選ぶ
<- c(
recipe sample(1:4, 1), # 麺
sample(1:4, 1), # スープ
sample(1:4, 1), # 肉
sample(1:4, 1) # トッピング
)<- recipe
population[[i]]
}return(population)
}
# --- 親を選択する関数(ルーレット選択) ---
<- function(population, fitness_scores) {
select_parents <- sum(fitness_scores)
total_fitness if (total_fitness == 0) {
# 全員失格の場合はランダムに選択
return(sample(population, 2, replace = TRUE))
}
# 適応度に基づいて選択確率を決定
<- fitness_scores / total_fitness
selection_probs
# 確率に基づいて親を2人(2レシピ)選ぶ
<- sample(1:length(population), 2, replace = TRUE, prob = selection_probs)
parents_indices return(population[parents_indices])
}
# --- 交叉(2つのレシピから新しいレシピを作る)関数 ---
<- function(parent1, parent2) {
crossover # 交叉点(どこでレシピを区切るか)をランダムに決定
<- sample(1:3, 1)
crossover_point
# 親1の前半と親2の後半を組み合わせて子1を作る
<- c(parent1[1:crossover_point], parent2[(crossover_point + 1):4])
child1
# 親2の前半と親1の後半を組み合わせて子2を作る
<- c(parent2[1:crossover_point], parent1[(crossover_point + 1):4])
child2
return(list(child1, child2))
}
# --- 突然変異(レシピの一部をランダムに変える)関数 ---
<- function(recipe, mutation_rate) {
mutate <- recipe
mutated_recipe for (i in 1:length(recipe)) {
if (runif(1) < mutation_rate) {
# 突然変異が発生!同じカテゴリの別の食材にランダムで変更
<- sample(1:4, 1)
mutated_recipe[i]
}
}return(mutated_recipe)
}
# ===================================================================
# 3. シミュレーション実行
# ===================================================================
# --- 1. 初期集団の生成 ---
cat("ステップ1: まずはランダムにレシピをたくさん作ります(初期集団の生成)。\n\n")
<- create_initial_population(POPULATION_SIZE)
population
# --- 生成された初期集団の概要を表示 ---
cat(sprintf("%d 個のランダムな初期レシピが生成されました。\n", length(population)))
cat("その中から、最初の3つのレシピ例を見てみましょう:\n\n")
for (i in 1:min(3, length(population))) {
<- population[[i]]
recipe <- get_recipe_details(recipe)
details <- calculate_fitness(recipe)
fitness <- sum(details$cost)
cost
cat(sprintf("レシピ %d: [%s]\n", i, paste(details$name, collapse = ", ")))
cat(sprintf(" -> 満足度: %d, コスト: %d 円", fitness, cost))
if (cost > BUDGET) {
cat(" (予算オーバーのため失格!)\n\n")
else {
} cat("\n\n")
}
}
# --- 2. 世代の進化ループ ---
cat("ステップ2: これから世代交代を繰り返し、レシピをどんどん進化させます!\n\n")
for (gen in 1:GENERATIONS) {
# --- 適応度の計算 ---
<- sapply(population, calculate_fitness)
fitness_scores
# --- 新しい世代の準備 ---
<- list()
new_population
# --- エリート選択: 最も優秀なレシピはそのまま次世代へ ---
<- floor(POPULATION_SIZE * ELITISM_RATE)
elite_count if (elite_count > 0 && sum(fitness_scores) > 0) {
<- order(fitness_scores, decreasing = TRUE)[1:elite_count]
elite_indices <- population[elite_indices]
elites <- c(new_population, elites)
new_population
}
# --- 交叉と突然変異で新しい子レシピを生成 ---
while (length(new_population) < POPULATION_SIZE) {
# 親を選択
<- select_parents(population, fitness_scores)
parents
# 交叉
<- crossover(parents[[1]], parents[[2]])
children
# 突然変異
<- mutate(children[[1]], MUTATION_RATE)
child1 <- mutate(children[[2]], MUTATION_RATE)
child2
length(new_population) + 1]] <- child1
new_population[[if (length(new_population) < POPULATION_SIZE) {
length(new_population) + 1]] <- child2
new_population[[
}
}
# --- 世代交代 ---
<- new_population
population
# --- 世代ごとの結果を表示 ---
<- sapply(population, calculate_fitness)
current_fitness <- max(current_fitness)
best_fitness <- mean(current_fitness)
avg_fitness
cat(sprintf("第 %2d 世代 | 最高の満足度: %3.0f | 平均満足度: %5.1f\n", gen, best_fitness, avg_fitness))
}
cat("\n")
# ===================================================================
# 4. 最終結果の発表
# ===================================================================
cat("ステップ3: 究極のラーメンレシピが決定しました!\n\n")
<- sapply(population, calculate_fitness)
final_fitness_scores <- which.max(final_fitness_scores)
best_index <- population[[best_index]]
best_recipe <- get_recipe_details(best_recipe)
best_recipe_details
cat("【究極のラーメン レシピ】\n")
print(best_recipe_details[, c("name", "satisfaction", "cost")])
cat("\n")
cat(sprintf("合計満足度: %d\n", sum(best_recipe_details$satisfaction)))
cat(sprintf("合計コスト: %d 円 (予算 %d 円)\n", sum(best_recipe_details$cost), BUDGET))
ようこそ、ラーメン店主!これから遺伝的アルゴリズムで究極のレシピを探します。
ステップ1: まずはランダムにレシピをたくさん作ります(初期集団の生成)。
50 個のランダムな初期レシピが生成されました。
その中から、最初の3つのレシピ例を見てみましょう:
レシピ 1: [ちぢれ麺, 醤油スープ, 豚バラチャーシュー, メンマ]
-> 満足度: 305, コスト: 520 円
レシピ 2: [細麺, とんこつスープ, 鶏チャーシュー, メンマ]
-> 満足度: 290, コスト: 510 円
レシピ 3: [中太麺, 塩スープ, 鶏チャーシュー, たっぷりネギ]
-> 満足度: 295, コスト: 490 円
ステップ2: これから世代交代を繰り返し、レシピをどんどん進化させます!
第 1 世代 | 最高の満足度: 385 | 平均満足度: 307.3
第 2 世代 | 最高の満足度: 390 | 平均満足度: 322.8
第 3 世代 | 最高の満足度: 390 | 平均満足度: 336.9
第 4 世代 | 最高の満足度: 390 | 平均満足度: 342.1
第 5 世代 | 最高の満足度: 390 | 平均満足度: 344.0
第 6 世代 | 最高の満足度: 390 | 平均満足度: 348.6
第 7 世代 | 最高の満足度: 390 | 平均満足度: 355.5
第 8 世代 | 最高の満足度: 390 | 平均満足度: 360.1
第 9 世代 | 最高の満足度: 390 | 平均満足度: 360.5
第 10 世代 | 最高の満足度: 390 | 平均満足度: 369.8
第 11 世代 | 最高の満足度: 390 | 平均満足度: 373.7
第 12 世代 | 最高の満足度: 390 | 平均満足度: 370.3
第 13 世代 | 最高の満足度: 390 | 平均満足度: 372.1
第 14 世代 | 最高の満足度: 390 | 平均満足度: 370.3
第 15 世代 | 最高の満足度: 390 | 平均満足度: 367.4
第 16 世代 | 最高の満足度: 390 | 平均満足度: 364.6
第 17 世代 | 最高の満足度: 390 | 平均満足度: 365.5
第 18 世代 | 最高の満足度: 390 | 平均満足度: 367.6
第 19 世代 | 最高の満足度: 390 | 平均満足度: 372.1
第 20 世代 | 最高の満足度: 390 | 平均満足度: 366.4
第 21 世代 | 最高の満足度: 390 | 平均満足度: 367.3
第 22 世代 | 最高の満足度: 390 | 平均満足度: 370.6
第 23 世代 | 最高の満足度: 390 | 平均満足度: 367.6
第 24 世代 | 最高の満足度: 390 | 平均満足度: 364.7
第 25 世代 | 最高の満足度: 390 | 平均満足度: 360.8
第 26 世代 | 最高の満足度: 390 | 平均満足度: 362.1
第 27 世代 | 最高の満足度: 390 | 平均満足度: 366.2
第 28 世代 | 最高の満足度: 390 | 平均満足度: 375.0
第 29 世代 | 最高の満足度: 390 | 平均満足度: 373.1
第 30 世代 | 最高の満足度: 390 | 平均満足度: 375.7
ステップ3: 究極のラーメンレシピが決定しました!
【究極のラーメン レシピ】
name satisfaction cost
1 極太麺 90 130
2 とんこつスープ 100 200
3 角煮 120 250
4 味玉 80 100
合計満足度: 390
合計コスト: 680 円 (予算 700 円)
以上です。