Rで遺伝的アルゴリズム

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)
A11
B412
C84
D92
E31

【遺伝的アルゴリズムでの表現】

  • 遺伝子: 各品物を「入れる(1)」か「入れない(0)」かで表現します。
  • 染色体: 5つの品物の組み合わせを表す、長さ5の0/1の列です。 例:[1, 0, 1, 1, 0] → 品物A, C, D を入れる
  • 適応度関数:

    1. 染色体から合計の重さと価値を計算する。
    2. もし合計の重さが15kgを超えていたら、適応度は0(ペナルティ)。
    3. 超えていなければ、適応度は「合計価値」そのもの。

【実行イメージ】

  1. 初期集団: [1,1,1,1,1], [0,0,1,1,0], [1,0,1,0,1] などをランダムに多数生成。
  2. 評価:

    • [1,1,1,1,1] → 重さ20kg > 15kg → 適応度0
    • [0,0,1,1,0] → 重さ6kg, 価値17万円 → 適応度17
    • [1,0,1,0,1] → 重さ6kg, 価値12万円 → 適応度12
  3. 選択: 適応度の高い [0,0,1,1,0] などが親として選ばれやすくなる。
  4. 交叉・突然変異: 選ばれた親から新しい組み合わせ(例:[1,0,1,1,1]など)が作られる。
  5. 世代交代: これを繰り返すと、徐々に「重すぎず、価値が高い」組み合わせ(例:[0,1,0,1,1] 重さ15kg, 価値16万円 や [1,0,1,1,1] 重さ8kg, 価値21万円など)が生き残り、最終的に最適解 [1,0,1,1,1] が見つかる可能性が高まります。

5. メリットとデメリット

メリット

  • 幅広い問題に適用可能: 微分不可能な関数や、複雑でルールが明確でない問題にも使えます。
  • 局所解からの脱出: 突然変異などにより、部分的に良いだけの解(局所解)に陥りにくく、全体として最も良い解(大域的最適解)を見つけやすいです。
  • 実装が比較的容易: アルゴリズムの基本的な考え方がシンプルです。

デメリット

  • 必ず最適解が見つかる保証はない: あくまで確率的な探索なので、最適解が見つからないこともあります。
  • 計算コストが高い: 多数の個体を評価するため、計算時間がかかることがあります。
  • パラメータ設定が難しい: 集団のサイズ、交叉率、突然変異率などのパラメータを適切に設定しないと、良い結果が得られないことがあります。

6. シミュレーションのシナリオ:『究極のラーメンレシピ探求!』

あなたは新進気鋭のラーメン店の店主です。 お店の看板メニューとなる「究極の一杯」を開発するため、様々な食材の組み合わせを試すことにしました。

しかし、闇雲に試すだけでは時間もお金もかかってしまいます。そこであなたは、生物の進化の仕組みを応用した「遺伝的アルゴリズム」を使って、最高のレシピを効率的に見つけ出すことを思いつきました。

【ルール】

  1. レシピの構成要素(遺伝子):

    ラーメンは「麺」、「スープ」、「肉」、「トッピング」の4つの要素で構成されます。それぞれの要素にはいくつかの選択肢があります。

  2. レシピ(個体):

    4つの要素の組み合わせが、一つの「ラーメンレシピ」となります。

    例:[細麺, とんこつスープ, 豚バラチャーシュー, 味玉]

  3. 評価(適応度):

    各食材には、お客さんの「満足度ポイント」と「原価(コスト)」が設定されています。

    • 予算: 1杯あたりの原価は700円まで。
    • 評価方法:

      • 原価が700円を超えたレシピは「失格(満足度0)」となります。
      • 予算内のレシピは、食材の「合計満足度ポイント」が評価点となります。
  4. 目標:

    予算内で、お客さんの満足度が最も高くなる「究極のラーメンレシピ」を見つけ出すこと。

【進化のプロセス】

  • 初期集団: まずは、ランダムにたくさんのレシピ(第一世代)を作ります。
  • 選択: 満足度の高いレシピが、次世代のレシピの「親」として選ばれやすくなります。
  • 交叉: 選ばれた2つの「親レシピ」の良いところを組み合わせて、新しい「子レシピ」を作ります。(例:Aの麺とBのスープを組み合わせる)
  • 突然変異: ごく稀に、レシピの一部が全く新しい食材に変わることがあります。これにより、思いもよらない美味しい組み合わせが生まれるかもしれません。

この「選択 → 交叉 → 突然変異」を何世代も繰り返すことで、レシピはどんどん洗練され、究極の一杯へと進化していきます。さあ、シミュレーションを始めましょう!

7. R言語によるシミュレーションコード

library(dplyr)

set.seed(20250716)

# ===================================================================
# 1. シナリオ設定
# ===================================================================
cat("ようこそ、ラーメン店主!これから遺伝的アルゴリズムで究極のレシピを探します。\n\n")

# --- 食材リストの定義 ---
# 各食材に「満足度」と「コスト(円)」を設定します
# 1:麺, 2:スープ, 3:肉, 4:トッピング
ingredients <- data.frame(
  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のパラメータ設定 ---
BUDGET <- 700 # 予算(円)
POPULATION_SIZE <- 50 # 集団のサイズ(一度に考えるレシピの数)
GENERATIONS <- 30 # 世代数
ELITISM_RATE <- 0.1 # エリート選択率(優秀なレシピをそのまま残す割合)
MUTATION_RATE <- 0.05 # 突然変異の確率

# ===================================================================
# 2. 遺伝的アルゴリズムの関数定義
# ===================================================================

# --- レシピ(個体)から詳細情報を取得する関数 ---
get_recipe_details <- function(recipe) {
  # recipeは [1, 2, 3, 4] のような、各カテゴリのitem_idを持つベクトル
  details <- ingredients %>%
    filter((category_id == 1 & item_id == recipe[1]) |
      (category_id == 2 & item_id == recipe[2]) |
      (category_id == 3 & item_id == recipe[3]) |
      (category_id == 4 & item_id == recipe[4]))
  return(details)
}

# --- 適応度(満足度)を計算する関数 ---
calculate_fitness <- function(recipe) {
  details <- get_recipe_details(recipe)
  total_cost <- sum(details$cost)
  total_satisfaction <- sum(details$satisfaction)

  # 予算オーバーのレシピは評価0(失格)
  if (total_cost > BUDGET) {
    return(0)
  } else {
    return(total_satisfaction)
  }
}

# --- 初期集団(最初のレシピ集団)を生成する関数 ---
create_initial_population <- function(size) {
  population <- list()
  for (i in 1:size) {
    # 各カテゴリからランダムに1つずつ食材を選ぶ
    recipe <- c(
      sample(1:4, 1), # 麺
      sample(1:4, 1), # スープ
      sample(1:4, 1), # 肉
      sample(1:4, 1) # トッピング
    )
    population[[i]] <- recipe
  }
  return(population)
}

# --- 親を選択する関数(ルーレット選択) ---
select_parents <- function(population, fitness_scores) {
  total_fitness <- sum(fitness_scores)
  if (total_fitness == 0) {
    # 全員失格の場合はランダムに選択
    return(sample(population, 2, replace = TRUE))
  }

  # 適応度に基づいて選択確率を決定
  selection_probs <- fitness_scores / total_fitness

  # 確率に基づいて親を2人(2レシピ)選ぶ
  parents_indices <- sample(1:length(population), 2, replace = TRUE, prob = selection_probs)
  return(population[parents_indices])
}


# --- 交叉(2つのレシピから新しいレシピを作る)関数 ---
crossover <- function(parent1, parent2) {
  # 交叉点(どこでレシピを区切るか)をランダムに決定
  crossover_point <- sample(1:3, 1)

  # 親1の前半と親2の後半を組み合わせて子1を作る
  child1 <- c(parent1[1:crossover_point], parent2[(crossover_point + 1):4])

  # 親2の前半と親1の後半を組み合わせて子2を作る
  child2 <- c(parent2[1:crossover_point], parent1[(crossover_point + 1):4])

  return(list(child1, child2))
}

# --- 突然変異(レシピの一部をランダムに変える)関数 ---
mutate <- function(recipe, mutation_rate) {
  mutated_recipe <- recipe
  for (i in 1:length(recipe)) {
    if (runif(1) < mutation_rate) {
      # 突然変異が発生!同じカテゴリの別の食材にランダムで変更
      mutated_recipe[i] <- sample(1:4, 1)
    }
  }
  return(mutated_recipe)
}


# ===================================================================
# 3. シミュレーション実行
# ===================================================================

# --- 1. 初期集団の生成 ---
cat("ステップ1: まずはランダムにレシピをたくさん作ります(初期集団の生成)。\n\n")
population <- create_initial_population(POPULATION_SIZE)

# --- 生成された初期集団の概要を表示 ---
cat(sprintf("%d 個のランダムな初期レシピが生成されました。\n", length(population)))
cat("その中から、最初の3つのレシピ例を見てみましょう:\n\n")
for (i in 1:min(3, length(population))) {
  recipe <- population[[i]]
  details <- get_recipe_details(recipe)
  fitness <- calculate_fitness(recipe)
  cost <- sum(details$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) {
  # --- 適応度の計算 ---
  fitness_scores <- sapply(population, calculate_fitness)

  # --- 新しい世代の準備 ---
  new_population <- list()

  # --- エリート選択: 最も優秀なレシピはそのまま次世代へ ---
  elite_count <- floor(POPULATION_SIZE * ELITISM_RATE)
  if (elite_count > 0 && sum(fitness_scores) > 0) {
    elite_indices <- order(fitness_scores, decreasing = TRUE)[1:elite_count]
    elites <- population[elite_indices]
    new_population <- c(new_population, elites)
  }

  # --- 交叉と突然変異で新しい子レシピを生成 ---
  while (length(new_population) < POPULATION_SIZE) {
    # 親を選択
    parents <- select_parents(population, fitness_scores)

    # 交叉
    children <- crossover(parents[[1]], parents[[2]])

    # 突然変異
    child1 <- mutate(children[[1]], MUTATION_RATE)
    child2 <- mutate(children[[2]], MUTATION_RATE)

    new_population[[length(new_population) + 1]] <- child1
    if (length(new_population) < POPULATION_SIZE) {
      new_population[[length(new_population) + 1]] <- child2
    }
  }

  # --- 世代交代 ---
  population <- new_population

  # --- 世代ごとの結果を表示 ---
  current_fitness <- sapply(population, calculate_fitness)
  best_fitness <- max(current_fitness)
  avg_fitness <- mean(current_fitness)

  cat(sprintf("第 %2d 世代 | 最高の満足度: %3.0f | 平均満足度: %5.1f\n", gen, best_fitness, avg_fitness))
}

cat("\n")

# ===================================================================
# 4. 最終結果の発表
# ===================================================================
cat("ステップ3: 究極のラーメンレシピが決定しました!\n\n")

final_fitness_scores <- sapply(population, calculate_fitness)
best_index <- which.max(final_fitness_scores)
best_recipe <- population[[best_index]]
best_recipe_details <- get_recipe_details(best_recipe)

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 円)

以上です。