유전 알고리즘 파이썬 구현 - yujeon algolijeum paisseon guhyeon

인공지능 수업에서 유전자 알고리즘에 대한 과제를 하게 되었다.

다윈의 진화론에 의하면, 지구가 처음 생성되고 지금까지 환경에 적응한 생물만이 살아남고, 또한 진화했다. 유전 알고리즘은 생물체가 환경에 적응하며 진화하는 모습을 모방하여, 최적해를 찾아내는 검색 방법이다. GA는 수학적으로 명확하게 이해 혹은 풀기 힘든 문제에도 적용이 가능하기 때문에, 이 경우 매우 유용하게 사용된다.
염색체(Chromosome) 는 생물학적으로 유전자들이 뭉쳐져 포장된 것이다. GA에서의 염색체는 하나의 개체 값을 의미한다.
유전자(gene) 는 생물학적으로나 GA적으로나 염색체의 요소로, 유전 정보를 나타낸다.
자손(offspring, child) 는 부모(Parent) 유전자로부터 생성된 자식 유전자이며, 이전 세대와 비슷한 (완전히 동일하지는 않은) 유전 정보를 갖게 된다.
적합도(fitness) 어떤 염색체가 최적해에 얼마나 가까운지 나타내는 고유 값으로, 적합도의 값으로 염색체의 개체선별을 하게 된다.

어떤 문제에 유전 알고리즘을 적용하기 위해서는 그 문제에 대해 아래와 같이 5개의 연산을 정의해야 한다.

보통 1번 연산 후, 2~5번 연산을 반복하는 식으로 진행된다. 각 연산에 대한 설명은 실습과 함께 진행하겠다.

저번 다층 퍼셉트론 실습에서, C/C++ 로 제로코드부터 시작하여 코딩하였었는데, 그 당시의 고통이 너무 커서… 이번엔 비교적 간단한 예제를 파이썬으로 실습을 진행할 계획이다.
해결할 문제는 숫자야구 게임이다. 숫자야구 게임 규칙은 다음과 같다.

실습에서는 N은 6, 즉 임의의 6자리 숫자를 가지고 실험하였으며, 염색체를 10개를 두고 실험하였다.
fitness 함수는 스트라이크를 5점, 볼을 1점으로 환산하여 적합도를 계산하였다. 선택 방식은 룰렛방식으로, 각 염색체 중 적합도가 가장 높은 염색체를 선택하여 유전자를 교환하였다.

  • 초기 염색체를 생성하는 연산

    1 2 3 4 5 6 7 8 9 10 def _generate_parent(length, geneSet, get_fitness): chromosome_list = [] for i in range(0, 10): genes = [] while len(genes) < length: sampleSize = min(length - len(genes), len(geneSet)) genes.extend(random.sample(geneSet, sampleSize)) fitness = get_fitness(genes) chromosome_list.append(Chromosome(genes, fitness)) return chromosome_list

    정답과 같은 길이의 염색체를 10개정도 생성하여, 리스트에 저장한다. 이때 유전자의 배열은 0부터 9까지 사이 숫자의 랜덤 값이다.

  • 적합도를 계산하는 연산

    1 2 3 4 5 6 7 8 9 def get_fitness(guess, target): fitness = 0 for expected, actual in zip(target, guess): if expected == actual: fitness += 5 elif actual in target: fitness += 1 return fitness

    숫자와 그 위치까지 동일하다면 스트라이크이므로 5점, 만약 위치는 같지 않고 숫자만 같다면 볼이므로 1점으로 환산하여 계산한다.

  • 적합도를 기준으로 염색체를 선택하는 연산

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 def _generate_child(parent_list, geneSet, get_fitness): child_list = [] fitness_percent_list = [] fitness_accum_list = [] fitness_sum = 0 for parent in parent_list: fitness_sum += parent.Fitness for parent in parent_list: fitness_percent_list.append(parent.Fitness / fitness_sum) fitness_sum = 0 for fitness_percent in fitness_percent_list: fitness_sum += fitness_percent fitness_accum_list.append(fitness_sum) # Selection for i in range(0, 10): rand = random.random() before = 0 for j in range(0, len(fitness_accum_list)): if rand > before and rand <= fitness_accum_list[j]: child_list.append(copy.deepcopy(parent_list[j])) break before = fitness_accum_list[j] # --- 생략 ---

    부모들로부터 계산된 적합도 값을 모두 더한 후, 백분율로 환산하여 축적하여 저장한다. 이후 난수를 돌려서 걸린 염색체를 자식 염색체로 지정한다. 이게 바로 룰렛 휠 방식이다.

  • 선택된 염색체들로부터 자손을 생성하는 연산

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def _generate_child(parent_list, geneSet, get_fitness): # --- 생략 --- # Crossover crossover_rate = 0.20 selected = None for i in range(0, len(child_list)): rand = random.random() if rand < crossover_rate: if selected is None: selected = i else: child_list[selected].Genes[2:], child_list[i].Genes[2:] = \ child_list[i].Genes[2:], child_list[selected].Genes[2:] selected = None # update child_list[i].Fitness = get_fitness(child_list[i].Genes) # --- 생략 ---

    선택한 염색체 중 랜덤으로 선택하여 유전자를 교배 시킨다. Single Point Crossover 방식으로, 유전자, 즉 숫자의 일부를 두 유전자끼리 교환시킨다. Crossover Point는 임의로 지정하였다.

  • 돌연변이 연산

    1 2 3 4 5 6 7 8 9 10 11 12 def _generate_child(parent_list, geneSet, get_fitness): # --- 생략 --- # mutate mutate_rate = 0.2 for i in range(0, len(child_list)): rand = random.random() if rand < mutate_rate: child = _mutate(child_list[i], geneSet, get_fitness) del child_list[i] child_list.append(child) return child_list

    기존의 염색체들로만 교배를 한다면 지역 최적점, 즉 원하는 값에 도달할 수가 없기 때문에, 광역 최적점에 도달하기 위해서는 돌연변이 연산을 섞어줘야 한다. 일정 확률로 돌연변이를 발생 시킨 후, 유전자의 일부를 치환하였다.

  • 결과는 다음과 같다.

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 target : 841532 820653 1/3 8 0:00:00.000997 491670 1/1 6 0:00:00.000997 763810 0/3 3 0:00:00.000997 642183 1/4 9 0:00:00.000997 702563 1/2 7 0:00:00.000997 721048 1/3 8 0:00:00.000997 978064 0/2 2 0:00:00.000997 543081 1/4 9 0:00:00.000997 841530 5/0 25 0:00:00.000997 493157 0/4 4 0:00:00.000997 average fitness : 8.1 new maximum fitness : 9.1 new maximum fitness : 13.6 new maximum fitness : 17.2 ...중략... new maximum fitness : 29.2 new maximum fitness : 29.6 new maximum fitness : 30.0 841532 6/0 30 0:00:00.038933 841532 6/0 30 0:00:00.038933 841532 6/0 30 0:00:00.038933 841532 6/0 30 0:00:00.038933 841532 6/0 30 0:00:00.038933 841532 6/0 30 0:00:00.038933 841532 6/0 30 0:00:00.038933 841532 6/0 30 0:00:00.038933 841532 6/0 30 0:00:00.038933 841532 6/0 30 0:00:00.038933 average fitness : 30.0

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 target : 495367 450791 1/3 8 0:00:00 315478 1/3 8 0:00:00 068147 1/2 7 0:00:00 598437 2/3 13 0:00:00 385704 1/3 8 0:00:00 623759 0/5 5 0:00:00 253147 1/3 8 0:00:00 548967 2/3 13 0:00:00 067142 0/3 3 0:00:00 431768 2/2 12 0:00:00 average fitness : 8.5 new maximum fitness : 9.5 new maximum fitness : 11.0 ...중략... new maximum fitness : 29.1 new maximum fitness : 30.0 495367 6/0 30 0:00:00.025973 495367 6/0 30 0:00:00.025973 495367 6/0 30 0:00:00.025973 495367 6/0 30 0:00:00.025973 495367 6/0 30 0:00:00.025973 495367 6/0 30 0:00:00.025973 495367 6/0 30 0:00:00.025973 495367 6/0 30 0:00:00.025973 495367 6/0 30 0:00:00.025973 495367 6/0 30 0:00:00.025973 average fitness : 30.0

    위와 같이 원하는 결과가 잘 나오는 것을 확인할 수 있었다.

    저번 다층 퍼셉트론 구현할 때 사실 엄청난 좌절감을 느꼈었다. 이번에도 혹시나 실패하여 좌절감이 더해지지는 않을 까 걱정 했었는데, 다행히도 난이도가 그렇게 어렵지 않은 실습이었고, 오픈소스를 참고할 수가 있어 오히려 자신감이 붙은 느낌이다. 이 교육용 오픈소스가 보기 쉽게 되어있어서, 유전자 알고리즘을 이해하는 데 많은 도움이 되었다. 하지만 Crossover 없이 Mutate로만 구현되어 있었고, 염색체도 하나밖에 쓰지 않아서, 해당 부분은 내가 직접 구현했다. 덕분에 유전자 알고리즘을 한층 더 이해하기 수월했던 것 같다.

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 import datetime import random import unittest import copy def _generate_parent(length, geneSet, get_fitness): chromosome_list = [] for i in range(0, 10): genes = [] while len(genes) < length: sampleSize = min(length - len(genes), len(geneSet)) genes.extend(random.sample(geneSet, sampleSize)) fitness = get_fitness(genes) chromosome_list.append(Chromosome(genes, fitness)) return chromosome_list def _mutate(parent, geneSet, get_fitness): childGenes = parent.Genes[:] index = random.randrange(0, len(parent.Genes)) newGene, alternate = random.sample(geneSet, 2) childGenes[index] = alternate if newGene == childGenes[index] else newGene fitness = get_fitness(childGenes) return Chromosome(childGenes, fitness) def _generate_child(parent_list, geneSet, get_fitness): child_list = [] fitness_percent_list = [] fitness_accum_list = [] fitness_sum = 0 for parent in parent_list: fitness_sum += parent.Fitness for parent in parent_list: fitness_percent_list.append(parent.Fitness / fitness_sum) fitness_sum = 0 for fitness_percent in fitness_percent_list: fitness_sum += fitness_percent fitness_accum_list.append(fitness_sum) # Selection for i in range(0, 10): rand = random.random() before = 0 for j in range(0, len(fitness_accum_list)): if rand > before and rand <= fitness_accum_list[j]: child_list.append(copy.deepcopy(parent_list[j])) break before = fitness_accum_list[j] # Crossover crossover_rate = 0.20 selected = None for i in range(0, len(child_list)): rand = random.random() if rand < crossover_rate: if selected is None: selected = i else: child_list[selected].Genes[2:], child_list[i].Genes[2:] = \ child_list[i].Genes[2:], child_list[selected].Genes[2:] selected = None # update child_list[i].Fitness = get_fitness(child_list[i].Genes) # mutate mutate_rate = 0.2 for i in range(0, len(child_list)): rand = random.random() if rand < mutate_rate: child = _mutate(child_list[i], geneSet, get_fitness) del child_list[i] child_list.append(child) return child_list def get_best(get_fitness, targetLen, optimalFitness, geneSet, display): random.seed() # 1. Generate Parent bestParentList = _generate_parent(targetLen, geneSet, get_fitness) display(bestParentList) gen_count = 0 maximum_average = 0 while True: gen_count += 1 #print("generation : {}".format(gen_count)) child_list = _generate_child(bestParentList, geneSet, get_fitness) fitness_sum = 0 for child in child_list: fitness_sum += child.Fitness average = fitness_sum / 10 if average > maximum_average: print("new maximum fitness : {}".format(average)) bestParentList = child_list maximum_average = average if average >= optimalFitness: return child_list class Chromosome: def __init__(self, genes, fitness): self.Genes = genes self.Fitness = fitness def get_fitness(guess, target): fitness = 0 for expected, actual in zip(target, guess): if expected == actual: fitness += 5 elif actual in target: fitness += 1 return fitness def display(candidate, target, startTime): timeDiff = datetime.datetime.now() - startTime strike = 0 ball = 0 for expected, actual in zip(target, candidate.Genes): if expected == actual: strike += 1 elif actual in target: ball += 1 if strike == 0 and ball == 0: result = "out" else: result = "{}/{}".format(strike, ball) print("{}\t{}\t{}\t{}".format( ''.join(candidate.Genes), result, candidate.Fitness, timeDiff)) def display_list(candidate_list, target, startTime): fitness_sum = 0 for candidate in candidate_list: display(candidate, target, startTime) fitness_sum += candidate.Fitness print("average fitness : {}".format(fitness_sum / len(candidate_list))) def pick_baseball_num(length, is_duplicate_allowed): if is_duplicate_allowed is True or length > 10: return ''.join(random.choice("0123456789") for _ in range(length)) baseball_list = [] num = random.randrange(0, 10) for i in range(length): while str(num) in baseball_list: num = random.randrange(0, 10) baseball_list.append(str(num)) return ''.join(baseball_list) class BaseballGames(unittest.TestCase): geneset = "0123456789" def test_Baseball(self): length = 6 target = pick_baseball_num(length, False) print("target : {}".format(target)) self.guess_baseball(target) def guess_baseball(self, target): startTime = datetime.datetime.now() def fnGetFitness(genes): return get_fitness(genes, target) def fnDisplay(candidate_list): display_list(candidate_list, target, startTime) optimalFitness = len(target) * 5 child_list = get_best(fnGetFitness, len(target), optimalFitness, self.geneset, fnDisplay) fnDisplay(child_list) if __name__ == '__main__': unittest.main()

    주관적인 생각이 많이 들어간 포스트입니다. 지적은 언제나 환영입니다!

    Toplist

    최신 우편물

    태그