Mímisbrunnr知恵の泉

← 数理最適化 一覧

🎓 レベル:標準 | 重要度:A(必須)

📎 前提:局所探索と近傍 | 関連:粒子群最適化・群知能

要点(BLUF)

概念 ── 進化を模倣する

これまでの手法は1つの解を動かした。GA は 複数の解(集団)を同時に持ち、世代交代で進化させる。生物の進化(適者生存・交配・突然変異)からの類推:

graph TD
  A["初期集団 (ランダムな解の集まり)"] --> B["適応度を評価"]
  B --> C["選択: 良い個体を残す"]
  C --> D["交叉: 2個体を組み合わせ子を作る"]
  D --> E["突然変異: 一部の遺伝子を反転"]
  E --> F["次世代の集団"]
  F --> B

3つの進化操作

  1. 選択(selection):適応度に応じて親を選ぶ。トーナメント選択(数個体から最良を選ぶ)・ルーレット選択など。良い解を残しつつ多様性も保つ。
  2. 交叉(crossover):2つの親の遺伝子を組み合わせて子を作る。一点交叉(ある位置で前後を入れ替え)など。良い部分構造(ビルディングブロック)を継承・結合する。
  3. 突然変異(mutation):低確率で遺伝子をランダムに変える。多様性を補給し、局所最適への早期収束を防ぐ。

Pythonでナップサックを解く

15品目の 0/1 ナップサックを GA で解き、厳密解(PuLP、代表的な組合せ問題)と比べる。

import numpy as np

rng_data = np.random.default_rng(42)
n_items = 15
values  = rng_data.integers(5, 50, n_items)     # 各品目の価値
weights = rng_data.integers(1, 20, n_items)      # 各品目の重さ
capacity = int(weights.sum() * 0.5)              # 容量

def fitness(ind):
    total_w = (ind * weights).sum()
    if total_w > capacity:
        return 0                                 # 容量超過は失格(適応度0)
    return (ind * values).sum()

def genetic_algorithm(pop_size=60, generations=120, p_mut=0.05, seed=1):
    rng = np.random.default_rng(seed)
    pop = rng.integers(0, 2, (pop_size, n_items))    # 初期集団
    for gen in range(generations):
        fits = np.array([fitness(ind) for ind in pop])
        # 選択: トーナメント(2個体で適応度の高い方を親に)
        selected = []
        for _ in range(pop_size):
            a, b = rng.integers(0, pop_size, 2)
            selected.append(pop[a] if fits[a] >= fits[b] else pop[b])
        pop = np.array(selected)
        # 交叉: 一点交叉(確率0.8)
        for k in range(0, pop_size - 1, 2):
            if rng.random() < 0.8:
                pt = rng.integers(1, n_items)
                pop[k, pt:], pop[k+1, pt:] = pop[k+1, pt:].copy(), pop[k, pt:].copy()
        # 突然変異: 各遺伝子を確率p_mutで反転
        mask = rng.random((pop_size, n_items)) < p_mut
        pop = np.where(mask, 1 - pop, pop)
    return max(fitness(ind) for ind in pop)

ga_best = genetic_algorithm()
print(f"容量={capacity}, 品目数={n_items}")
print(f"GA最良   = {ga_best}")
print(f"厳密最適 = 338 (PuLPで別途計算)")

実行結果:

容量=82, 品目数=15
GA最良   = 338
厳密最適 = 338 (PuLPで別途計算)

GA は厳密最適 338 に 到達(達成率 100%)。15品目なら 215=327682^{15}=32768 通りで厳密も容易だが、品目が数百〜数千になると厳密は重くなる一方、GA は集団サイズと世代数を増やすだけで 良い解を実用時間で返す。これが GA の実務的価値。

数式の直観的意味

GA が局所最適に強い理由は 集団の多様性にある。単一解の探索(山登り・SA・タブー)は1点が罠に落ちると抜けにくいが、GA は集団のあちこちが別々の谷を探るので、全滅しにくい。交叉の役割は「異なる谷で見つかった良い部分(ビルディングブロック)を組み合わせて、より良い解を合成する」こと ── これは局所探索には無い、集団ならではの能力。スキーマ定理は「適応度が平均以上で短く低次のスキーマ(部分パターン)は世代ごとに指数的に増える」と述べ、なぜ良い部分構造が広がるかを説明する。突然変異は多様性の供給源で、交叉だけでは到達できない遺伝子を導入し、早期収束(全個体が同じになり進化が止まる)を防ぐ。探索(突然変異・多様性)と活用(選択・交叉)のバランスが、SA の温度(焼きなまし法)と同じ役割を果たす。

⚠️ よくある誤解・落とし穴

関連ノート