728x90

뱀게임을 진행중인 상황

유전학습을 통해 뱀게임을 진행하는 코드가 있어서 가지고 왔습니다.

 

일단 필수 패키지는 

- 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를 실행하시면 작업이 진행됩니다.

 

- 폰트의 문제는

font3270.otf
0.07MB

위의 폰트를 다운받아주시면 해결이 가능하십니다.

위의 파일들은 모두 같은 디렉토리에 있어야만 진행이 가능하다는점 유의하시길 바랍니다.

 

더욱 자세한 설명은 밑의 유튜브 주소에서 확인이 가능하십니다.

www.youtube.com/watch?v=C4WH5b-EidU&t=6s&ab_channel=%EB%B9%B5%ED%98%95%EC%9D%98%EA%B0%9C%EB%B0%9C%EB%8F%84%EC%83%81%EA%B5%AD

728x90

+ Recent posts