Ccmmutty logo
Commutty IT
2
9 min read

遺伝的アルゴリズムの基礎1

https://cdn.magicode.io/media/notebox/b4e5f899-5339-4667-ae5e-fb9e515863e6.jpeg

参考リンク

専門用語のお品書き

  • 遺伝子(gene): 基礎となる最小構成要素
  • 染色体(choromosome): 遺伝子情報でできたグループ
  • 個体(individual): ひとつ、または複数の染色体で作られた解の候補
  • 集合(popuration): 解の候補である個体の集合体。世代のこと。

例題:OneMax問題

OneMax問題とは、0と1からなるn個の数列の和が最大となるような数列を探し出すこと。
[1,1,1,1,...]と全ての遺伝子が1になる染色体が答え。
これを遺伝子アルゴリズムを使って正解を導き出してみる。
ニューラルネットワークを用いれば複数の条件が絡んでくるもっと複雑な問題も解けるようになる

遺伝子アルゴリズムのワークフロー

  1. 解を表現する符号化方法を決定する。
  2. 個体の集合である、世代を作成する。
  3. 次世代を収納するために空の集合を用意する。
  4. 評価関数を使って、現世代の個体の適応度をそれぞれ計算する。
  5. 現世代から親を選び、次世代となる子孫を生成する。
  6. 次世代の数が指定の数になるまで5の操作を繰り返す。
  7. 次世代を現世代として3~6の手順を繰り返し、更に世代を進めていく
  8. 命題が収束したか判定し、最終世代の中で最も適応度の高い個体を「解」とする。

解説と関数の定義

事前準備

可読性を上げるためにpipやインポート文をこの事前準備の欄にまとめるよ
python
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import sys
from IPython.display import clear_output

1. 解を表現する符号化方法を決定する。

解は数字となり、最適解は最大数となる。
遺伝子構成を定義
  • 遺伝子(gene): 0 または 1
  • 染色体(choromosome): 遺伝子がGENES_LENGTH個の集合体
  • 個体(individual): 染色体ひとつ
パラメータ
  • GENES_LENGTH: 遺伝子長
  • POPURATION_NUMBER: ひとつの世代に収まる個体数
  • GENERATION_COUNT: 進化を繰り返す世代数
  • TOURNAMENT_SIZE: トーナメントでランダムに選ばれる個体数
  • MUTATION_PROB: 変異確率。通常、0.1~1%。多くても数%

2. 個体の集合である、世代を作成する。

遺伝子と染色体の作成!
python
def create_choromosome(GENES_LENGTH):
    """
    染色体クラス。0か1の情報遺伝子をGENES_LENGTH分持つ。

    input:
        GENES_LENGTH(int): 1染色体に入る遺伝子の数

    output:
        choromosome(np.array): 染色体

    note:
        今回の遺伝子情報は0と1だけなので遺伝子を計算する関数は省く。
    """
    return np.random.randint(0, 2, GENES_LENGTH)
個体関数の作成!
python
def create_individual(GENES_LENGTH):
    """
    個体を作成する関数。今回の個体は染色体がひとつなので、染色体=個体になっている
    """
    choromosome = create_choromosome(GENES_LENGTH)
    
    return choromosome
世代の作成!
python
def create_generation(POPURATIONS_NUMBER, GENES_LENGTH):
    """
    世代を作る関数。

    input:
        POPURATIONS_NUMBER: 1世代の個体数
        GENES_LENGTH(int): 遺伝子の長さ(n)
    
    output: 
        generation(list): 個体クラスのリスト
    """
    generation = []
    for i in range(POPURATIONS_NUMBER):
        individual = create_individual(GENES_LENGTH)
        generation.append(individual)

    return generation

4. 評価関数を使って、現世代の個体の適応度をそれぞれ計算する。

最大数が最適解になるので単純に遺伝子を足し算したものを適応度とする。
python
def calc_fitness(choromosome):
    """ 適応度を計算する。"""
    fitness = choromosome.sum() / len(choromosome)
    return fitness

5. 現世代から親を選び、次世代となる子孫を生成する。

1. 現世代から親となる個体を選択する。
選択では、適応度の高い個体をランダム性を加えつつ抽出します。
選択の例:
  • ルーレット方式: 適応度に応じて重みをつけて、ランダムに選ぶ(適応度が高いほど選ばれやすい)
  • トーナメント方式: ランダムに数個選び、その中で適応度が一番高いものを選ぶ
  • エリート主義: 世代の中で適応度上位~個をそのまま選ぶ
複雑な条件を含めずに最大数を求めるとわかっているのでこの場合はエリート主義が良いのだが、普通は答えが複雑な問題に対して遺伝子アルゴリズムを使うのでエリート主義はあまり使わないと思う
python
def select_roulette(generation):
    '''
    ルーレット方式の親選択

    input:
        generation(list)
    
    output:
        parents(np.array)
    '''
    weights = [calc_fitness(individual) for individual in generation]
    norm_weights = [fitness / sum(weights) for fitness in weights]
    
    idxs = [i for i in range(len(generation))]
    idx = np.random.choice(idxs, p=norm_weights)
    
    parent = generation[idx]
    return parent
    
def select_tournament(generation, TOURNAMENT_SIZE):
    '''
    トーナメント方式の親選択

    input:
        generation(list)
        TOURNAMENT_SIZE(int): ランダムに抽選される人数。
        size(int): outputする親の数。デフォルトでは2人
    
    output:
        oarrents([np.array,...])
    '''
    idxs = [i for i in range(len(generation))]
    random_idxs = np.random.choice(idxs, TOURNAMENT_SIZE, replace=False)
    random_parents = [generation[idx] for idx in random_idxs]
    parrent = max(random_parents, key=calc_fitness)
    
    return parrent
2. 親となる個体で交叉を行い、子孫を生成する。
選ばれた親の遺伝子の一部を入れ替える操作。生物の交配をモデル化したもの
  • 二点交叉: 2つの遺伝子をランダムに決定し、その間の遺伝子を入れ替える。
二点交叉にはヒッチハイキングという欠点があり、遺伝子の数が増えるほど最適解が得られる確率が低くなる。
  • 一様交叉: 各遺伝子のそれぞれを所定の確率(通常は1/2)で入れ替える。
遺伝子の数に左右されることなく最適解が求められる
この他にも色々な交叉の手法があって遺伝子情報の種類によっても変わってくる。
python
def crossover_two_point(mother,father):
    '''
    二点交叉をして子孫を返す。
    
    入力:
        混ぜ合わせたい個体のペア
    出力:
        交叉後の個体のペア
    '''
    size = len(mother)
    point_befor = np.random.randint(0, size)
    point_after = np.random.randint(0, size - 1)

    if point_after >= point_befor:
        point_after += 1
    else:
        point_befor, point_after = point_after, point_befor

    choromosome = father
    choromosome[point_befor:point_after] = mother[point_befor:point_after]
    child = choromosome

    return child

def crossover_uniform(mother,father):
    '''
    一様交叉をして子孫を返す。
    
    入力:
        混ぜ合わせたい個体のペア
    出力:
        交叉後の個体のペア
    '''
    choromosome = []
    for i in range(len(mother)):
        if np.random.random() < 0.5:
            choromosome.append(father[i])
        else:
            choromosome.append(mother[i])

    child = np.array(choromosome)

    return child
3. 子孫に対して確率的に突然変異を起こす。
個体の遺伝子の一部をランダムに変化させる。局所解に陥るのを防ぐ効果がある。
変異確率について
通常、0.1~1%。多くても数%に設定する。
高すぎるとランダム探索に近くなって収束しにくくなる。
種類
種類は沢山あるが、今回は置換を行う。
python
def mutation(child,MUTATION_PLOB):
    if np.random.rand() < MUTATION_PLOB*0.01:
        i = np.random.randint(0, len(child)-1)
        child[i] = int(not child[i])
    else:
        return child

    return child

遺伝的アルゴリズムの実装

python
# param
POPURATIONS_NUMBER = 100 #@param {type:"number"}
GENES_LENGTH = 100 #@param {type:"number"}
GENERATION_COUNT = 20 #@param {type:"number"}
TOURNAMENT_SIZE = 3 #@param {type:"number"}
MUTATION_PLOB = 1 #@param {type:"number"}


# 祖先となる世代を作成
generation = create_generation(POPURATIONS_NUMBER, GENES_LENGTH)
plot_max = []
plot_min = []

# ここから進化開始
for generation_number in range(GENERATION_COUNT):
    next_generation = []

    for i in range(len(generation)):
        # mother = select_roulette(generation)
        # father = select_roulette(generation)
        mother = select_tournament(generation, TOURNAMENT_SIZE)
        father = select_tournament(generation, TOURNAMENT_SIZE)
        
        # child = crossover_two_point(mother,father)
        child = crossover_uniform(mother,father)

        child = mutation(child,MUTATION_PLOB)

        next_generation.append(child)
    
    fitness = [calc_fitness(choromosome) for choromosome in generation]
    clear_output(wait=True)
    print("第",generation_number+1,"/",GENERATION_COUNT,"世代")
    print("適応度 : 最大",max(fitness),", 最小",min(fitness))
    plot_max.append(max(fitness))
    plot_min.append(min(fitness))

    generation = next_generation

plt.plot(plot_max)
plt.plot(plot_min)
plt.show()

第 20 / 20 世代 適応度 : 最大 1.0 , 最小 0.97
<Figure size 432x288 with 1 Axes>
課題
  • ルーレット選択だと早期収束が起きている。
  • 二点交叉だとヒッチハイキングが起こっている。
備考
  • 今回は廃止した交叉確率を実装したらもしかして早期収束は起こらなくなるのか?
  • 集合を10にしてみたらトーナメント選択でも早期収束が起きたので、一回の結果でアルゴリズムの優劣を決めつけてはダメ。他のパラメータ、他の問題で試してみる。

Discussion

コメントにはログインが必要です。