유전학습을 통해 뱀게임을 진행하는 코드가 있어서 가지고 왔습니다.
일단 필수 패키지는
- Python
- numpy
- pygame
을 설치해주시면 됩니다.
뱀게임에 간략히 소개해드리면 초기에 조그마한 뱀이 존재를 하고 먹이를 먹을때마다 크기가 커지게 됩니다.
위 설명과 같이 간략한 게임이며, 이것을 유전학습을 통해 점점 인공지능이 최적의 경로를 찾도록 해보겠습니다.
뱀의 경우는 밑의 이미지 처럼 3방향으로만 움직일 수 있습니다.
고로 뱀의 이동경로에 해당 가중치값을 센서를 달아 주기로 하였습니다.
만약에 5칸안에 장애물(벽)이 없을 경우 1을 반환하도록 센서를 설계한 것 입니다.
뱀이 먹이를 찾는 것은 정면에 있을때와 왼쪽, 오른쪽에 있을 때 각각의 위치에서 1을 반환하도록 하였습니다.
먹이의 배열은 (앞,왼,오) 순으로 이루어져 있습니다.
고로 input값은 위의 이미지와 같이 장애물 값 3개와 먹이 감지 3개로 이루어지게 됩니다.
여기의 예는 Crossover라는 기법이 사용되었고, 코드를 수정하여 여러 Hidden Layers를 구성해서 완성도를 높이셔도 됩니다.
이제 유전학습에 대한 구성은 완성이 되었고, 이제 유전학습을 통해 적용되는 점수(Fitness)값은 코드상으로
- 먹이를 향해 가까워질때 +1.0
- 먹이로부터 멀어질 때 -1.5
- 먹이를 먹었을때 +10
점이 부여되도록 했습니다 먹이로부터 멀어질 때 더많은 점수를 떨어뜨리는 이유는 그리하지않으면 뱀이 같은 자리를 안정을 위해 계속 돌게됩니다.
그것을 방지하고자 이리 점수를 적용하였습니다.
#snake.py
import pygame
import os, random
import numpy as np
FPS = 60
SCREEN_SIZE = 30
PIXEL_SIZE = 20
LINE_WIDTH = 1
DIRECTIONS = np.array([
(0, -1), # UP
(1, 0), # RIGHT
(0, 1), # DOWN
(-1, 0) # LEFT
])
class Snake():
snake, fruit = None, None
def __init__(self, s, genome):
self.genome = genome
self.s = s
self.score = 0
self.snake = np.array([[15, 26], [15, 27], [15, 28], [15, 29]])
self.direction = 0 # UP
self.place_fruit()
self.timer = 0
self.last_fruit_time = 0
# fitness
self.fitness = 0.
self.last_dist = np.inf
def place_fruit(self, coord=None):
if coord:
self.fruit = np.array(coord)
return
while True:
x = random.randint(0, SCREEN_SIZE-1)
y = random.randint(0, SCREEN_SIZE-1)
if list([x, y]) not in self.snake.tolist():
break
self.fruit = np.array([x, y])
def step(self, direction):
old_head = self.snake[0]
movement = DIRECTIONS[direction]
new_head = old_head + movement
if (
new_head[0] < 0 or
new_head[0] >= SCREEN_SIZE or
new_head[1] < 0 or
new_head[1] >= SCREEN_SIZE or
new_head.tolist() in self.snake.tolist()
):
# self.fitness -= FPS/2
return False
# eat fruit
if all(new_head == self.fruit):
self.last_fruit_time = self.timer
self.score += 1
self.fitness += 10
self.place_fruit()
else:
tail = self.snake[-1]
self.snake = self.snake[:-1, :]
self.snake = np.concatenate([[new_head], self.snake], axis=0)
return True
def get_inputs(self):
head = self.snake[0]
result = [1., 1., 1., 0., 0., 0.]
# check forward, left, right
possible_dirs = [
DIRECTIONS[self.direction], # straight forward
DIRECTIONS[(self.direction + 3) % 4], # left
DIRECTIONS[(self.direction + 1) % 4] # right
]
# 0 - 1 ... danger - safe
for i, p_dir in enumerate(possible_dirs):
# sensor range = 5
for j in range(5):
guess_head = head + p_dir * (j + 1)
if (
guess_head[0] < 0 or
guess_head[0] >= SCREEN_SIZE or
guess_head[1] < 0 or
guess_head[1] >= SCREEN_SIZE or
guess_head.tolist() in self.snake.tolist()
):
result[i] = j * 0.2
break
# finding fruit
# heading straight forward to fruit
if np.any(head == self.fruit) and np.sum(head * possible_dirs[0]) <= np.sum(self.fruit * possible_dirs[0]):
result[3] = 1
# fruit is on the left side
if np.sum(head * possible_dirs[1]) < np.sum(self.fruit * possible_dirs[1]):
result[4] = 1
# fruit is on the right side
# if np.sum(head * possible_dirs[2]) < np.sum(self.fruit * possible_dirs[2]):
else:
result[5] = 1
return np.array(result)
def run(self):
self.fitness = 0
prev_key = pygame.K_UP
#font = pygame.font.Font('/Users/brad/Library/Fonts/3270Medium.otf', 20)
font = pygame.font.Font('/home/vlsi2141/20152167/genetic_snake-master/font3270.otf', 20)
font.set_bold(True)
appleimage = pygame.Surface((PIXEL_SIZE, PIXEL_SIZE))
appleimage.fill((0, 255, 0))
img = pygame.Surface((PIXEL_SIZE, PIXEL_SIZE))
img.fill((255, 0, 0))
clock = pygame.time.Clock()
while True:
self.timer += 0.1
if self.fitness < -FPS/2 or self.timer - self.last_fruit_time > 0.1 * FPS * 5:
# self.fitness -= FPS/2
print('Terminate!')
break
clock.tick(FPS)
for e in pygame.event.get():
if e.type == pygame.QUIT:
pygame.quit()
elif e.type == pygame.KEYDOWN:
# QUIT
if e.key == pygame.K_ESCAPE:
pygame.quit()
exit()
# PAUSE
if e.key == pygame.K_SPACE:
pause = True
while pause:
for ee in pygame.event.get():
if ee.type == pygame.QUIT:
pygame.quit()
elif ee.type == pygame.KEYDOWN:
if ee.key == pygame.K_SPACE:
pause = False
if __name__ == '__main__':
# CONTROLLER
if prev_key != pygame.K_DOWN and e.key == pygame.K_UP:
self.direction = 0
prev_key = e.key
elif prev_key != pygame.K_LEFT and e.key == pygame.K_RIGHT:
self.direction = 1
prev_key = e.key
elif prev_key != pygame.K_UP and e.key == pygame.K_DOWN:
self.direction = 2
prev_key = e.key
elif prev_key != pygame.K_RIGHT and e.key == pygame.K_LEFT:
self.direction = 3
prev_key = e.key
# action
if __name__ != '__main__':
inputs = self.get_inputs()
outputs = self.genome.forward(inputs)
outputs = np.argmax(outputs)
if outputs == 0: # straight
pass
elif outputs == 1: # left
self.direction = (self.direction + 3) % 4
elif outputs == 2: # right
self.direction = (self.direction + 1) % 4
if not self.step(self.direction):
break
# compute fitness
current_dist = np.linalg.norm(self.snake[0] - self.fruit)
if self.last_dist > current_dist:
self.fitness += 1.
else:
self.fitness -= 1.5
self.last_dist = current_dist
self.s.fill((0, 0, 0))
pygame.draw.rect(self.s, (255,255,255), [0,0,SCREEN_SIZE*PIXEL_SIZE,LINE_WIDTH])
pygame.draw.rect(self.s, (255,255,255), [0,SCREEN_SIZE*PIXEL_SIZE-LINE_WIDTH,SCREEN_SIZE*PIXEL_SIZE,LINE_WIDTH])
pygame.draw.rect(self.s, (255,255,255), [0,0,LINE_WIDTH,SCREEN_SIZE*PIXEL_SIZE])
pygame.draw.rect(self.s, (255,255,255), [SCREEN_SIZE*PIXEL_SIZE-LINE_WIDTH,0,LINE_WIDTH,SCREEN_SIZE*PIXEL_SIZE+LINE_WIDTH])
for bit in self.snake:
self.s.blit(img, (bit[0] * PIXEL_SIZE, bit[1] * PIXEL_SIZE))
self.s.blit(appleimage, (self.fruit[0] * PIXEL_SIZE, self.fruit[1] * PIXEL_SIZE))
score_ts = font.render(str(self.score), False, (255, 255, 255))
self.s.blit(score_ts, (5, 5))
pygame.display.update()
return self.fitness, self.score
if __name__ == '__main__':
pygame.init()
pygame.font.init()
s = pygame.display.set_mode((SCREEN_SIZE * PIXEL_SIZE, SCREEN_SIZE * PIXEL_SIZE))
pygame.display.set_caption('Snake')
while True:
snake = Snake(s, genome=None)
fitness, score = snake.run()
print('Fitness: %s, Score: %s' % (fitness, score))
뱀게임 코드는 위와 같습니다.
#genome.py
import numpy as np
class Genome():
def __init__(self):
self.fitness = 0
hidden_layer = 10
self.w1 = np.random.randn(6, hidden_layer)
self.w2 = np.random.randn(hidden_layer, 20)
self.w3 = np.random.randn(20, hidden_layer)
self.w4 = np.random.randn(hidden_layer, 3)
def forward(self, inputs):
net = np.matmul(inputs, self.w1)
net = self.relu(net)
net = np.matmul(net, self.w2)
net = self.relu(net)
net = np.matmul(net, self.w3)
net = self.relu(net)
net = np.matmul(net, self.w4)
net = self.softmax(net)
return net
def relu(self, x):
return x * (x >= 0)
def softmax(self, x):
return np.exp(x) / np.sum(np.exp(x), axis=0)
def leaky_relu(self, x):
return np.where(x > 0, x, x * 0.01)
유전학습을 위한 코드는 위와 같습니다.
#evolution.py
import pygame, random
import numpy as np
from copy import deepcopy
from snake import Snake, SCREEN_SIZE, PIXEL_SIZE
from genome import Genome
N_POPULATION = 10
N_BEST = 5
N_CHILDREN = 5
PROB_MUTATION = 0.4
pygame.init()
pygame.font.init()
s = pygame.display.set_mode((SCREEN_SIZE * PIXEL_SIZE, SCREEN_SIZE * PIXEL_SIZE))
pygame.display.set_caption('Snake')
# generate 1st population
genomes = [Genome() for _ in range(N_POPULATION)]
best_genomes = None
n_gen = 0
while True:
n_gen += 1
for i, genome in enumerate(genomes):
snake = Snake(s, genome=genome)
fitness, score = snake.run()
genome.fitness = fitness
# print('Generation #%s, Genome #%s, Fitness: %s, Score: %s' % (n_gen, i, fitness, score))
if best_genomes is not None:
genomes.extend(best_genomes)
genomes.sort(key=lambda x: x.fitness, reverse=True)
print('===== Generaton #%s\tBest Fitness %s =====' % (n_gen, genomes[0].fitness))
# print(genomes[0].w1, genomes[0].w2)
best_genomes = deepcopy(genomes[:N_BEST])
# crossover
for i in range(N_CHILDREN):
new_genome = deepcopy(best_genomes[0])
a_genome = random.choice(best_genomes)
b_genome = random.choice(best_genomes)
cut = random.randint(0, new_genome.w1.shape[1])
new_genome.w1[i, :cut] = a_genome.w1[i, :cut]
new_genome.w1[i, cut:] = b_genome.w1[i, cut:]
cut = random.randint(0, new_genome.w2.shape[1])
new_genome.w2[i, :cut] = a_genome.w2[i, :cut]
new_genome.w2[i, cut:] = b_genome.w2[i, cut:]
cut = random.randint(0, new_genome.w3.shape[1])
new_genome.w3[i, :cut] = a_genome.w3[i, :cut]
new_genome.w3[i, cut:] = b_genome.w3[i, cut:]
cut = random.randint(0, new_genome.w4.shape[1])
new_genome.w4[i, :cut] = a_genome.w4[i, :cut]
new_genome.w4[i, cut:] = b_genome.w4[i, cut:]
best_genomes.append(new_genome)
# mutation
genomes = []
for i in range(int(N_POPULATION / (N_BEST + N_CHILDREN))):
for bg in best_genomes:
new_genome = deepcopy(bg)
mean = 20
stddev = 10
if random.uniform(0, 1) < PROB_MUTATION:
new_genome.w1 += new_genome.w1 * np.random.normal(mean, stddev, size=(6, 10)) / 100 * np.random.randint(-1, 2, (6, 10))
if random.uniform(0, 1) < PROB_MUTATION:
new_genome.w2 += new_genome.w2 * np.random.normal(mean, stddev, size=(10, 20)) / 100 * np.random.randint(-1, 2, (10, 20))
if random.uniform(0, 1) < PROB_MUTATION:
new_genome.w3 += new_genome.w3 * np.random.normal(mean, stddev, size=(20, 10)) / 100 * np.random.randint(-1, 2, (20, 10))
if random.uniform(0, 1) < PROB_MUTATION:
new_genome.w4 += new_genome.w4 * np.random.normal(mean, stddev, size=(10, 3)) / 100 * np.random.randint(-1, 2, (10, 3))
genomes.append(new_genome)
위의 코드들을 따로 다 파이썬파일로 저장하시고 에볼루션.py를 실행하시면 작업이 진행됩니다.
- 폰트의 문제는
위의 폰트를 다운받아주시면 해결이 가능하십니다.
위의 파일들은 모두 같은 디렉토리에 있어야만 진행이 가능하다는점 유의하시길 바랍니다.
더욱 자세한 설명은 밑의 유튜브 주소에서 확인이 가능하십니다.
'코딩 > python - 프로젝트' 카테고리의 다른 글
[python]thread(쓰레드)를 이용한 파일소켓통신 프로젝트 (0) | 2020.11.16 |
---|