A star

개발/AI,ML,ALGORITHM 2023. 12. 27. 09:27
반응형

 

Q-Learning 알고리즘으로 구현하였던 길찾기를 A star 알고리즘으로 구현해 보았다.

그리드 구성은 이전과 동일하다.

#
# A* 
#

import numpy as np
import matplotlib.pyplot as plt
import heapq

# 그리드 월드 환경 설정
# 0: 빈공간, 1: 벽, 2: 목적지, (0,0): 시작위치
grid = np.array([[0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
                 [0, 0, 0, 1, 0, 0, 1, 0, 0, 0],
                 [0, 0, 0, 0, 1, 0, 0, 1, 0, 0],
                 [0, 1, 1, 0, 1, 0, 0, 0, 1, 0],
                 [0, 0, 0, 0, 0, 0, 0, 0, 1, 2]])

# 그리드 월드 환경 출력 함수
def plot_grid_world(start, goal, path=[]):
    plt.imshow(grid, cmap="gray", interpolation="none", origin="upper")
    plt.xticks([])
    plt.yticks([])
    
    if path:
        path = np.array(path)
        plt.plot(path[:, 1], path[:, 0], marker='o', markersize=4, linestyle='-', color='green')
        
        for i, pos in enumerate(path):
            plt.text(pos[1], pos[0], str(i + 1), ha="center", va="center", color="orange", fontsize=14)
    
    plt.text(start[1], start[0], "A", ha="center", va="center", color="red", fontsize=16)
    plt.text(goal[1], goal[0], "B", ha="center", va="center", color="green", fontsize=16)

# 휴리스틱 계산 함수
# A* 알고리즘을 이용하여 최적 경로 찾기
def heuristic(state, goal):
    return abs(state[0] - goal[0]) + abs(state[1] - goal[1])

# A* 알고리즘
def astar(grid, start, goal):
    frontier = [(0, start)]
    came_from = {}
    visited = set()
    
    # 'frontier'를 최소 힙(min heap)으로 변환
    #heapq.heapify(frontier)
    
    while frontier:
        current_cost, current_state = heapq.heappop(frontier)
        
        if current_state == goal:
            # 시작 지점부터 목표 지점까지의 경로를 재구성
            path = []
            while current_state in came_from:
                path.append(current_state)
                current_state = came_from[current_state]
            path.append(start)
            return path[::-1] # 리스트 역순으로 변환 
        
        # 현재 상태가 방문한 곳이라면 스킵.
        if current_state in visited:
            continue
        
        # 현재 상태 방문 리스트에 추가 
        visited.add(current_state)
        
        for next_state in get_neighbors(grid, current_state):
            if next_state not in visited:
                priority = current_cost + 1 + heuristic(next_state, goal)   # 우선순위 계산 
                heapq.heappush(frontier, (priority, next_state)) # 최소 우선순위를 가진 상태가 먼저 나오도록 정렬 
                came_from[next_state] = current_state # 현재 이동상태 기록 
    
    return None

# 현재 위치에서 이동 가능한 이웃 반환
def get_neighbors(grid, state):
    neighbors = []
    directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] 
    for dx, dy in directions:
        new_position = (state[0] + dx, state[1] + dy)
        if 0 <= new_position[0] < grid.shape[0] and 0 <= new_position[1] < grid.shape[1] and grid[new_position] != 1:
            neighbors.append(new_position)
    return neighbors

# 시작 위치 및 타겟 위치 설정
start_position = (0, 0)
target_position = tuple(np.argwhere(grid == 2)[0]) # grid값이 2인 곳이 목적지

# A* 알고리즘 실행
optimal_path = astar(grid, start_position, target_position)

# 결과 출력
print("최적 경로:", optimal_path)


# 최적 경로 및 순서 출력
plt.ion() # nteractive mode
plt.figure(3)
plt.clf()   # clear
plt.title(f"Optimal Path - Distance : {len(optimal_path)}")
plot_grid_world(start_position, target_position, optimal_path)
plt.show(block=True)

 

결과는 아래 그림과 같다.

 

 

2023.12.07 - [iOS] - XOR 연산을 이용한 문자열 비교

2023.11.03 - [AI,ML] - Gomoku(Five in a Row, Omok) (5/5) - 3x3 체크 추가

2023.10.29 - [AI,ML] - Gomoku(Five in a Row, Omok) (5/5) - 머신러닝으로 게임 구현

2023.10.29 - [AI,ML] - Gomoku(Five in a Row, Omok) (4/5) - 훈련 데이터 생성 및 학습

2023.10.28 - [AI,ML] - Gomoku(Five in a Row, Omok) (3/5) - 속도 최적화 2차 (RANDOM모드 추가)

2023.10.27 - [AI,ML] - Gomoku(Five in a Row, Omok) (2/5) - 속도 최적화 1차 (minimax 속도 개선)

2023.10.27 - [AI,ML] - Gomoku(Five in a Row, Omok) (1/5) - 기본 구현 (minimax, alpha-beta pruning)

2023.08.28 - [AI,ML] - Q-learning

 

반응형
블로그 이미지

SKY STORY

,
반응형

3x3 체크를 위해 아래와 같이 함수를 만들고 플레이어와 AI 둘다 막아보았다.
문제되는 부분이 있다면 댓글로 알려주길 바란다.

# 3x3 체크 
def check3x3(self, row, col):
    def check_direction_with_blank(dx, dy):
        scount = 1 # 같은 색상 갯수 
        bcount = 1 # 빈공간 포함 갯수
        r, c = row + dx, col + dy
        while 0 <= r < len(self.board) and 0 <= c < len(self.board[0]) and (self.board[r][c] == self.board[row][col] or self.board[r][c] == 0):
            bcount += 1
            if self.board[r][c] == self.board[row][col]:
                scount += 1
            r += dx
            c += dy
        return scount, bcount

    self.board[row][col] = self.turn_player
    
    count3x3 = 0
    directions = [(0, 1), (1, 0), (1, 1), (-1, 1)]
    for dx, dy in directions:
        sc1, bc1 = check_direction_with_blank( dx,  dy)
        sc2, bc2 = check_direction_with_blank(-dx, -dy)
        scount = sc1 + sc2 - 1
        bcount = bc1 + bc2 - 1
        if scound == 3 and bcound >= 5:
            count3x3 += 1
    self.board[row][col] = 0
    if check3x3 >= 2:
        return True
    return False
    
    
def find_empty_cells(self):
    empty_cells = []
    for row in range(BOARD_SIZE):
        for col in range(BOARD_SIZE):
            if self.board[row][col] == 0:
                if self.check3x3(row, col) == False: # 3x3 체크
                    empty_cells.append((row, col))
    random.shuffle(empty_cells)
    return empty_cells
    
def handle_events(self):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            self.game_over = True
        elif event.type == pygame.MOUSEBUTTONDOWN and not self.game_over and self.turn_player == PLAYER:
            x, y = pygame.mouse.get_pos()
            row = int(round((y - MARGIN) / CELL_SIZE))
            col = int(round((x - MARGIN) / CELL_SIZE))
            if 0 <= row < BOARD_SIZE and 0 <= col < BOARD_SIZE and self.board[row][col] == 0:
                if self.check3x3(row, col) == True: # 3x3 체크
                    return

                self.make_move(row, col, self.turn_player)
                
    			(이하 생략)

 
 
 
 
 
 
 
2023.10.27 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (1/5) - 기본 구현 (minimax, alpha-beta pruning)
2023.10.27 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (2/5) - 속도 최적화 1차 (minimax 속도 개선)
2023.10.28 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (3/5) - 속도 최적화 2차 (RANDOM모드 추가)
2023.10.29 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (4/5) - 훈련 데이터 생성 및 학습
2023.10.29 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (5/5) - 머신러닝으로 게임 구현
2023.11.03 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (5/5) - 3x3 체크 추가
 
 
 

반응형
블로그 이미지

SKY STORY

,
반응형

이제 머신러닝으로 게임을 구현해 보자.

학습 데이터가 5만 이상 되어야 하는데 2만개 정도만 학습 시켰다.

최적화 이동 처리 함수와 믹스해서 구현해 보았다.

3x3일 경우 막지 않았다.

나중에 그 부분은 업데이트 하도록 하겠다.

#
# FIAR
# Created by netcanis on 2023/10/29.
#
# minimax
# alpha beta pruning
# 속도 개선 
# 훈련데이터 생성하여 머신러닝.

import pygame
import random
import numpy as np
import sys
import os
import tensorflow as tf
from keras.models import load_model


BOARD_SIZE = 9  # (7,9,13,15,19)
CELL_SIZE = 40
MARGIN = 20
CLICK_RADIUS = CELL_SIZE // 2

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GRAY = (100, 100, 100)

NUM_ITEMS = (BOARD_SIZE * BOARD_SIZE)
PLAYER = 1
AI = -1

DIFFICULTY_LEVEL_AI = 4

PLAYER_MODE = "MANUAL"
AI_MODE = "ML"

H5_FILE_NAME = "fiar_model.h5"


class FIAR:
    def __init__(self):
        self.init_game()
    
    def init_game(self):
        self.episode = 0
        
        pygame.init()
        pygame.display.set_caption("FIAR")
        w = (BOARD_SIZE-1) * CELL_SIZE + 2 * MARGIN
        h = (BOARD_SIZE-1) * CELL_SIZE + 2 * MARGIN
        self.screen = pygame.display.set_mode((w, h))

        self.font = pygame.font.Font(None, CELL_SIZE-4)
        
    def reset_game(self):
        self.board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
        self.sequences = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
        self.sequence = 0
        self.game_over = False
        self.turn_player = PLAYER#random.choice([PLAYER, AI])

        self.draw_board(self.screen)
        pygame.display.flip()
        
        self.model = load_model(H5_FILE_NAME)
        
    def find_empty_cells(self):
        empty_cells = []
        for row in range(BOARD_SIZE):
            for col in range(BOARD_SIZE):
                if self.board[row][col] == 0:
                    empty_cells.append((row, col))
        random.shuffle(empty_cells)
        return empty_cells
    
    def find_adjacent_empty_cells(self):
        empty_cells = []
        empty_cells2 = []
        for row, col in self.find_empty_cells():
            for dr in [-1, 0, 1]:
                for dc in [-1, 0, 1]:
                    new_row, new_col = row + dr, col + dc
                    if 0 <= new_row < BOARD_SIZE and 0 <= new_col < BOARD_SIZE and self.board[new_row][new_col] != 0:
                        # 수를 놓았을 경우 연결된 수가 3개 이상이고 5개를 놓을 수 있는 자리일 경우만 배열에 추가.
                        self.board[row][col] = self.turn_player
                        c1, c2, _ = self.evaluate_position(self.board, row, col)
                        self.board[row][col] = -self.turn_player
                        c3, c4, _ = self.evaluate_position(self.board, row, col)
                        self.board[row][col] = 0
                        if (c1 >= 3 and c2 >= 5) or (c3 >= 3 and c4 >= 5):
                            empty_cells2.append((row, col))

                        empty_cells.append((row, col))
                        break
                    
        if len(empty_cells2) > 0:
            return empty_cells2
        
        return empty_cells
    
    def evaluate_position(self, board, row, col):
        def check_direction(dx, dy):
            count = 1
            r, c = row + dx, col + dy
            while 0 <= r < len(board) and 0 <= c < len(board[0]) and board[r][c] == board[row][col]:
                count += 1
                r += dx
                c += dy
            return count
        
        def check_direction_with_blank(dx, dy):
            count = 1 # 빈자리 포함 총자리  수   
            r, c = row + dx, col + dy
            while 0 <= r < len(board) and 0 <= c < len(board[0]) and (board[r][c] == board[row][col] or board[r][c] == 0):
                count += 1
                r += dx
                c += dy
            return count
        
        total_count1 = 0
        total_count2 = 0
        total_score = 0
        directions = [(0, 1), (1, 0), (1, 1), (-1, 1)]
        for dx, dy in directions:
            count1 = check_direction( dx,  dy)
            count2 = check_direction(-dx, -dy)
            dir_count1 = count1 + count2 - 1
            count3 = check_direction_with_blank( dx,  dy)
            count4 = check_direction_with_blank(-dx, -dy)
            dir_count2 = count3 + count4 - 1
            total_count1 = max(total_count1, dir_count1)
            total_count2 = max(total_count2, dir_count2)
            # 각 방향별로 연결된 돌의 수가 3자리 이상이고 5개이상 놓을 수 있는 자리라면 점수를 추가한다.
            # 즉, 연결되는 돌이 교차하는 위치라면 점수가 높게 나온다.
            if dir_count1 == 4 and dir_count2 >= 5:
                total_score += dir_count1 * 4
            elif dir_count1 >= 3 and dir_count2 >= 5:
                total_score += dir_count1 * 2
            elif dir_count1 >= 2 and dir_count2 >= 5:
                total_score += dir_count1
                
        return total_count1, total_count2, total_score
    
    def check_winner(self, board, row, col):
        if board[row][col] == 0:
            return False
        
        def check_direction(dx, dy):
            count = 1
            r, c = row + dx, col + dy
            while 0 <= r < len(board) and 0 <= c < len(board[0]) and board[r][c] == board[row][col]:
                count += 1
                r += dx
                c += dy
            return count

        directions = [(0, 1), (1, 0), (1, 1), (-1, 1)]
        for dx, dy in directions:
            count1 = check_direction(dx, dy)
            count2 = check_direction(-dx, -dy)
            total_count = count1 + count2 - 1
            if total_count == 5:
                return True

        return False
    
    def is_board_full(self, board):
        return all(cell != 0 for row in board for cell in row)

    def random_move(self, player):
        if self.game_over:
            return -1, -1
        
        best_move = None
        best_score = 0
        
        best_move = self.make_optimal_move(player)
        if best_move == None:
            if player == AI:
                empty_cells = self.find_adjacent_empty_cells()
                for row, col in empty_cells: 
                    self.board[row][col] = player
                    s1 = self.evaluate_position(self.board, row, col)[2]
                    if s1 > best_score:
                        best_score = s1
                        best_move = (row, col)
                    self.board[row][col] = -player
                    s2 = self.evaluate_position(self.board, row, col)[2]
                    if s2 > best_score:
                        best_score = s2
                        best_move = (row, col)
                    self.board[row][col] = 0
                
                if best_score == 0:
                    best_move = random.choice(self.find_empty_cells())
            else:
                best_move = random.choice(self.find_empty_cells())
        
        self.make_move(best_move[0], best_move[1], player)
        
        return best_move

    def minimax_move(self, player):
        if self.game_over:
            return -1, -1
        row, col = self.find_best_move(player)
        self.make_move(row, col, player)
        return row, col

    def make_move(self, row, col, player):
        if self.board[row][col] == 0:
            self.board[row][col] = player
            
            self.sequence += 1
            self.sequences[row][col] = self.sequence
            
            #print(f"[{self.sequence}] <{player}> {row},{col}")
            
            if self.check_winner(self.board, row, col):
                self.game_over = True
            elif self.is_board_full(self.board):
                self.game_over = True
                self.turn_player = 0
            else:
                self.turn_player *= -1

    def minimax(self, depth, is_maximizing, alpha, beta, row2, col2):
        if self.is_board_full(self.board):
            return 0
        
        if is_maximizing:
            if self.check_winner(self.board, row2, col2):
                return -(NUM_ITEMS - depth + 1)
        else:
            if self.check_winner(self.board, row2, col2):
                return (NUM_ITEMS - depth + 1)
        
        if depth >= DIFFICULTY_LEVEL_AI:
            return 0
        
        if is_maximizing:# AI
            best_score = -float('inf')
            for row, col in self.find_adjacent_empty_cells():
                self.board[row][col] = AI
                score = self.minimax(depth + 1, False, alpha, beta, row, col)
                self.board[row][col] = 0
                best_score = max(best_score, score)
                alpha = max(alpha, best_score)
                if beta <= alpha:
                    break
            return best_score
        else:
            best_score = float('inf')
            for row, col in self.find_adjacent_empty_cells():
                self.board[row][col] = PLAYER
                score = self.minimax(depth + 1, True, alpha, beta, row, col)
                self.board[row][col] = 0
                best_score = min(best_score, score)
                beta = min(beta, best_score)
                if beta <= alpha:
                    break
            return best_score
    
    def make_optimal_move(self, player):
        # 0. The first one is random.
        if self.sequence == 0:
            row = BOARD_SIZE // 2 + random.randint(-2, 2)
            col = BOARD_SIZE // 2 + random.randint(-2, 2)
            return (row, col)
        
        empty_cells = self.find_adjacent_empty_cells()

        # 1. ai turn: 5
        for row, col in empty_cells: 
            self.board[row][col] = player
            if self.check_winner(self.board, row, col):
                self.board[row][col] = 0
                return (row, col)
            self.board[row][col] = 0
            
        # 2. player turn: 5
        for row, col in empty_cells: 
            self.board[row][col] = -player
            if self.check_winner(self.board, row, col):
                self.board[row][col] = 0
                return (row, col)
            self.board[row][col] = 0
        
        # 3. The position that becomes 4 when placed
        for row, col in empty_cells: 
            self.board[row][col] = player
            c1, c2, _ = self.evaluate_position(self.board, row, col)
            self.board[row][col] = -player
            c3, c4, _ = self.evaluate_position(self.board, row, col)
            self.board[row][col] = 0
            if (c1 == 4 and c2 >= 5) or (c3 == 4 and c4 >= 5):
                return (row, col)
            
        # 4. The position that becomes 3 when placed
        for row, col in empty_cells: 
            self.board[row][col] = player
            c1, c2, _ = self.evaluate_position(self.board, row, col)
            self.board[row][col] = -player
            c3, c4, _ = self.evaluate_position(self.board, row, col)
            self.board[row][col] = 0
            if (c1 == 3 and c2 >= 5) or (c3 == 3 and c4 >= 5):
                return (row, col)
            
        return None
        
    def find_best_move(self, player):
        if AI_MODE == "ML":
            optimal_move = self.make_optimal_move(player)
            if optimal_move != None:
                print(f"optimal move! ({optimal_move[0]}, {optimal_move[1]})")
                return optimal_move
            
            move_index = self.predicts(self.board)
            row = move_index // BOARD_SIZE
            col = move_index % BOARD_SIZE
            best_move = (row, col)
            print(f"ML move! ({row}, {col})")
        elif AI_MODE == "RANDOM":
            best_move = self.random_move(player)
        else: # MINIMAX
            print(f"[{self.sequence+1}] <{player}> ...")
            optimal_move = self.make_optimal_move(player)
            if optimal_move != None:
                return optimal_move
        
            alpha = -float('inf')
            beta = float('inf')

            best_move = None
            best_score = -float('inf') if player == AI else float('inf')

            empty_cells = self.find_adjacent_empty_cells()
            for index, (row, col) in enumerate(empty_cells):
                self.board[row][col] = player
                is_maximizing = False if player == AI else True
                score = self.minimax(0, is_maximizing, alpha, beta, row, col)
                self.board[row][col] = 0
                
                if (player == AI and score > best_score) or (player == PLAYER and score < best_score):
                    best_score = score
                    best_move = (row, col)
                
                percentage = (index / len(empty_cells)) * 100
                print(f"    [{percentage:.1f}%] <{player}> {row},{col} -> {score}")
            
            print(f"    {best_move[0]},{best_move[1]} ({best_score})")
            
        return best_move
    
    def show_message(self, message, is_exit=False):
        popup = True
        while popup:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()

                if event.type == pygame.KEYDOWN:
                    if is_exit:
                        if event.key == pygame.K_y:
                            self.reset_game()
                            popup = False
                            return
                        elif event.key == pygame.K_n:
                            pygame.quit() 
                            sys.exit()
                    else:
                        popup = False
                        message = ""
                        self.draw_board(self.screen)
                        break
            
            text_lines = message.split('\n')
            for i, line in enumerate(text_lines):
                text = self.font.render(line, True, (128, 0, 128))
                text_rect = text.get_rect(topleft=(20, 20 + i * 20))
                self.screen.blit(text, text_rect)
            
            pygame.display.flip()
    
    def draw_board(self, screen):
        screen.fill(GRAY)
        for row in range(BOARD_SIZE):# draw horizontal lines
            pygame.draw.line(screen, BLACK, 
                            (0 * CELL_SIZE + MARGIN, row * CELL_SIZE + MARGIN), 
                            ((BOARD_SIZE-1) * CELL_SIZE + MARGIN, row * CELL_SIZE + MARGIN),
                            1)
            for col in range(BOARD_SIZE):# draw vertical lines
                if row == 0:
                    pygame.draw.line(screen, BLACK, 
                                    (col * CELL_SIZE + MARGIN, 0 * CELL_SIZE + MARGIN), 
                                    (col * CELL_SIZE + MARGIN, (BOARD_SIZE-1) * CELL_SIZE + MARGIN),
                                    1)

                x = col * CELL_SIZE + MARGIN
                y = row * CELL_SIZE + MARGIN

                if self.board[row][col] == PLAYER:
                    pygame.draw.circle(screen, BLACK, (x, y), CLICK_RADIUS)
                elif self.board[row][col] == AI:
                    pygame.draw.circle(screen, WHITE, (x, y), CLICK_RADIUS)
                
                seq_no = self.sequences[row][col] 
                if seq_no != 0:
                    if seq_no == self.sequence: 
                        pygame.draw.circle(screen, RED, (x, y), CLICK_RADIUS+1, 1)
                    
                    color = WHITE if self.board[row][col] == PLAYER else BLACK
                    text = self.font.render(f"{seq_no}", True, color)
                    text_rect = text.get_rect(center=(x, y))
                    screen.blit(text, text_rect)

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.game_over = True
            elif event.type == pygame.MOUSEBUTTONDOWN and not self.game_over and self.turn_player == PLAYER:
                x, y = pygame.mouse.get_pos()
                row = int(round((y - MARGIN) / CELL_SIZE))
                col = int(round((x - MARGIN) / CELL_SIZE))
                if 0 <= row < BOARD_SIZE and 0 <= col < BOARD_SIZE and self.board[row][col] == 0:
                    self.make_move(row, col, self.turn_player)
                    self.draw_board(self.screen)
                    pygame.display.flip()
                    
                    if self.turn_player == AI:
                        best_move = self.find_best_move(self.turn_player)
                        if best_move is not None:
                            row, col = best_move
                            self.make_move(row, col, self.turn_player)
                        
                        self.draw_board(self.screen)
                        pygame.display.flip()
                            
                    if self.game_over == True:
                        if self.turn_player == 0:
                            self.show_message("Game draw!")
                        else:
                            self.show_message(f"Game Over!\n{'Player' if self.turn_player == PLAYER else 'AI'} wins!")

    def init_ML(self):
        hidden_layer_count = 3 * NUM_ITEMS
        self.model = tf.keras.Sequential([
            tf.keras.layers.Dense(hidden_layer_count, activation='relu', input_shape=(NUM_ITEMS,)),
            tf.keras.layers.Dense(NUM_ITEMS, activation='softmax')
        ])
        self.model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

    def predicts(self, input_data):
        if isinstance(input_data, list):
            input_data = np.array(input_data)
        
        prediction = self.model.predict(input_data.reshape(1, -1))
        sorted_indices = np.argsort(prediction, axis=-1)[:, ::-1]
        index = 0
        for i in sorted_indices[0]:
            if input_data.shape == (NUM_ITEMS,):
                if input_data[i] == 0:
                    index = i
                    break
            elif input_data.shape == (BOARD_SIZE, BOARD_SIZE):
                row = i // BOARD_SIZE
                col = i % BOARD_SIZE
                if input_data[row][col] == 0:
                    index = i
                    break
        return index


    
if __name__ == "__main__":
    game = FIAR()
    game.init_ML()
    game.reset_game()
    
    while True:
        while not game.game_over:
            game.handle_events()
            
        game.show_message("Play Again? (y/n)", is_exit=True)

 

 

2023.10.27 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (1/5) - 기본 구현 (minimax, alpha-beta pruning)

2023.10.27 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (2/5) - 속도 최적화 1차 (minimax 속도 개선)

2023.10.28 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (3/5) - 속도 최적화 2차 (RANDOM모드 추가)

2023.10.29 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (4/5) - 훈련 데이터 생성 및 학습

2023.10.29 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (5/5) - 머신러닝으로 게임 구현

2023.11.03 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (5/5) - 3x3 체크 추가

 

 

반응형
블로그 이미지

SKY STORY

,
반응형

이제 머신러닝을 위한 대량의 훈련 데이터를 만들어보자.

플레이어와 AI는 모두 랜덤 모드로 수를 연산하도록 설정했다. 수를 놓을 때 3자리 이하인 부분에서는,

AI는 점수를 계산하여 높은 점수의 자리에 돌을 놓도록 했다. 플레이어는 3자리 이하인 부분에서 인접한 빈자리 중 랜덤한 위치에 돌을 놓도록 난이도에 약간의 차이를 두었다. 즉, 대부분 비기거나 AI가 좀더 잘 두도록 설정하였다.

#
# FIAR
# Created by netcanis on 2023/10/29.
#
# minimax
# alpha beta pruning
# 속도 개선 
# 훈련데이터 생성하여 머신러닝.

import pygame
import random
import numpy as np
import sys
import os
import tensorflow as tf
from keras.models import Sequential, load_model
import pandas as pd


BOARD_SIZE = 9  # (7,9,13,15,19)
CELL_SIZE = 40
MARGIN = 20
CLICK_RADIUS = CELL_SIZE // 2

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GRAY = (100, 100, 100)

NUM_ITEMS = (BOARD_SIZE * BOARD_SIZE)
PLAYER = 1
AI = -1

DIFFICULTY_LEVEL_AI = 4
DIFFICULTY_LEVEL_PLAYER = 2

AUTO_PLAY = True

if AUTO_PLAY == True:
    PLAYER_MODE = "RANDOM"#"MINIMAX"
    AI_MODE = "RANDOM"#"MINIMAX"
else:
    PLAYER_MODE = "MANUAL"
    AI_MODE = "RANDOM"#"ML"#"MINIMAX"
    
NUM_EPISODES = 20000
CSV_FILE_NAME = "fiar_training_data.csv"
H5_FILE_NAME = "fiar_model.h5"


class FIAR:
    def __init__(self):
        self.init_game()
    
    def init_game(self):
        self.episode = 0
        
        pygame.init()
        pygame.display.set_caption("FIAR")
        w = (BOARD_SIZE-1) * CELL_SIZE + 2 * MARGIN
        h = (BOARD_SIZE-1) * CELL_SIZE + 2 * MARGIN
        self.screen = pygame.display.set_mode((w, h))

        self.font = pygame.font.Font(None, CELL_SIZE-4)
        
    def reset_game(self):
        self.board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
        self.sequences = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
        self.sequence = 0
        self.game_over = False
        self.turn_player = random.choice([PLAYER, AI]) if AUTO_PLAY == True else PLAYER
        
        self.draw_board(self.screen)
        pygame.display.flip()
        
    def find_empty_cells(self):
        empty_cells = []
        for row in range(BOARD_SIZE):
            for col in range(BOARD_SIZE):
                if self.board[row][col] == 0:
                    empty_cells.append((row, col))
        random.shuffle(empty_cells)
        return empty_cells
    
    def find_adjacent_empty_cells(self):
        empty_cells = []
        empty_cells2 = []
        for row, col in self.find_empty_cells():
            for dr in [-1, 0, 1]:
                for dc in [-1, 0, 1]:
                    new_row, new_col = row + dr, col + dc
                    if 0 <= new_row < BOARD_SIZE and 0 <= new_col < BOARD_SIZE and self.board[new_row][new_col] != 0:
                        # 수를 놓았을 경우 연결된 수가 3개 이상이고 5개를 놓을 수 있는 자리일 경우만 배열에 추가.
                        self.board[row][col] = self.turn_player
                        c1, c2, _ = self.evaluate_position(self.board, row, col)
                        self.board[row][col] = -self.turn_player
                        c3, c4, _ = self.evaluate_position(self.board, row, col)
                        self.board[row][col] = 0
                        if (c1 >= 3 and c2 >= 5) or (c3 >= 3 and c4 >= 5):
                            empty_cells2.append((row, col))

                        empty_cells.append((row, col))
                        break
                    
        if len(empty_cells2) > 0:
            return empty_cells2
        
        return empty_cells
    
    def evaluate_position(self, board, row, col):
        def check_direction(dx, dy):
            count = 1
            r, c = row + dx, col + dy
            while 0 <= r < len(board) and 0 <= c < len(board[0]) and board[r][c] == board[row][col]:
                count += 1
                r += dx
                c += dy
            return count
        
        def check_direction_with_blank(dx, dy):
            count = 1
            r, c = row + dx, col + dy
            while 0 <= r < len(board) and 0 <= c < len(board[0]) and (board[r][c] == board[row][col] or board[r][c] == 0):
                count += 1
                r += dx
                c += dy
            return count
        
        total_count1 = 0
        total_count2 = 0
        total_score = 0
        directions = [(0, 1), (1, 0), (1, 1), (-1, 1)]
        for dx, dy in directions:
            count1 = check_direction( dx,  dy)
            count2 = check_direction(-dx, -dy)
            dir_count1 = count1 + count2 - 1
            count3 = check_direction_with_blank( dx,  dy)
            count4 = check_direction_with_blank(-dx, -dy)
            dir_count2 = count3 + count4 - 1
            total_count1 = max(total_count1, dir_count1)
            total_count2 = max(total_count2, dir_count2)
            # 각 방향별로 연결된 돌의 수가 3자리 이상이고 5개이상 놓을 수 있는 자리라면 점수를 추가한다.
            # 즉, 연결되는 돌이 교차하는 위치라면 점수가 높게 나온다.
            if dir_count1 == 4 and dir_count2 >= 5:
                total_score += dir_count1 * 4
            elif dir_count1 >= 3 and dir_count2 >= 5:
                total_score += dir_count1 * 2
            elif dir_count1 >= 2 and dir_count2 >= 5:
                total_score += dir_count1
                
        return total_count1, total_count2, total_score
    
    def check_winner(self, board, row, col):
        if board[row][col] == 0:
            return False
        
        def check_direction(dx, dy):
            count = 1
            r, c = row + dx, col + dy
            while 0 <= r < len(board) and 0 <= c < len(board[0]) and board[r][c] == board[row][col]:
                count += 1
                r += dx
                c += dy
            return count

        directions = [(0, 1), (1, 0), (1, 1), (-1, 1)]
        for dx, dy in directions:
            count1 = check_direction(dx, dy)
            count2 = check_direction(-dx, -dy)
            total_count = count1 + count2 - 1
            if total_count == 5:
                return True

        return False
    
    def is_board_full(self, board):
        return all(cell != 0 for row in board for cell in row)

    def random_move(self, player):
        if self.game_over:
            return -1, -1
        
        best_move = None
        best_score = 0
        
        best_move = self.make_optimal_move(player)
        if best_move == None:
            if player == AI:
                empty_cells = self.find_adjacent_empty_cells()
                for row, col in empty_cells: 
                    self.board[row][col] = player
                    s1 = self.evaluate_position(self.board, row, col)[2]
                    if s1 > best_score:
                        best_score = s1
                        best_move = (row, col)
                    self.board[row][col] = -player
                    s2 = self.evaluate_position(self.board, row, col)[2]
                    if s2 > best_score:
                        best_score = s2
                        best_move = (row, col)
                    self.board[row][col] = 0
                
                if best_score == 0:
                    best_move = random.choice(self.find_empty_cells())
            else:
                best_move = random.choice(self.find_empty_cells())
        
        self.make_move(best_move[0], best_move[1], player)
        
        return best_move

    def minimax_move(self, player):
        if self.game_over:
            return -1, -1
        row, col = self.find_best_move(player)
        self.make_move(row, col, player)
        return row, col

    def make_move(self, row, col, player):
        if self.board[row][col] == 0:
            self.board[row][col] = player
            
            self.sequence += 1
            self.sequences[row][col] = self.sequence
            
            #print(f"[{self.sequence}] <{player}> {row},{col}")
            
            if self.check_winner(self.board, row, col):
                self.game_over = True
            elif self.is_board_full(self.board):
                self.game_over = True
                self.turn_player = 0
            else:
                self.turn_player *= -1

    def minimax(self, depth, is_maximizing, alpha, beta, row2, col2):
        if self.is_board_full(self.board):
            return 0
        
        if is_maximizing:
            if self.check_winner(self.board, row2, col2):
                return -(NUM_ITEMS - depth + 1)
        else:
            if self.check_winner(self.board, row2, col2):
                return (NUM_ITEMS - depth + 1)
        
        if (self.turn_player == AI and depth >= DIFFICULTY_LEVEL_AI) or \
            (self.turn_player == PLAYER and depth >= DIFFICULTY_LEVEL_PLAYER):
            return 0
        
        if is_maximizing:# AI
            best_score = -float('inf')
            for row, col in self.find_adjacent_empty_cells():
                self.board[row][col] = AI
                score = self.minimax(depth + 1, False, alpha, beta, row, col)
                self.board[row][col] = 0
                best_score = max(best_score, score)
                alpha = max(alpha, best_score)
                if beta <= alpha:
                    break
            return best_score
        else:
            best_score = float('inf')
            for row, col in self.find_adjacent_empty_cells():
                self.board[row][col] = PLAYER
                score = self.minimax(depth + 1, True, alpha, beta, row, col)
                self.board[row][col] = 0
                best_score = min(best_score, score)
                beta = min(beta, best_score)
                if beta <= alpha:
                    break
            return best_score
    
    def make_optimal_move(self, player):
        # 0. The first one is random.
        if self.sequence == 0:
            row = BOARD_SIZE // 2 + random.randint(-2, 2)
            col = BOARD_SIZE // 2 + random.randint(-2, 2)
            return (row, col)
        
        empty_cells = self.find_adjacent_empty_cells()

        # 1. ai turn: 5
        for row, col in empty_cells: 
            self.board[row][col] = player
            if self.check_winner(self.board, row, col):
                self.board[row][col] = 0
                return (row, col)
            self.board[row][col] = 0
            
        # 2. player turn: 5
        for row, col in empty_cells: 
            self.board[row][col] = -player
            if self.check_winner(self.board, row, col):
                self.board[row][col] = 0
                return (row, col)
            self.board[row][col] = 0
        
        if (player == PLAYER and PLAYER_MODE == "RANDOM") or (player == AI and AI_MODE == "RANDOM"):
            # 3. The position that becomes 4 when placed
            for row, col in empty_cells: 
                self.board[row][col] = player
                c1, c2, _ = self.evaluate_position(self.board, row, col)
                self.board[row][col] = -player
                c3, c4, _ = self.evaluate_position(self.board, row, col)
                self.board[row][col] = 0
                if (c1 == 4 and c2 >= 5) or (c3 == 4 and c4 >= 5):
                    return (row, col)
                
            # 4. The position that becomes 3 when placed
            for row, col in empty_cells: 
                self.board[row][col] = player
                c1, c2, _ = self.evaluate_position(self.board, row, col)
                self.board[row][col] = -player
                c3, c4, _ = self.evaluate_position(self.board, row, col)
                self.board[row][col] = 0
                if (c1 == 3 and c2 >= 5) or (c3 == 3 and c4 >= 5):
                    return (row, col)
            
        return None
        
    def find_best_move(self, player):
        if AI_MODE == "RANDOM":
            best_move = self.random_move(player)
        else: # MINIMAX
            print(f"[{self.sequence+1}] <{player}> ...")
            optimal_move = self.make_optimal_move(player)
            if optimal_move != None:
                return optimal_move
        
            alpha = -float('inf')
            beta = float('inf')

            best_move = None
            best_score = -float('inf') if player == AI else float('inf')

            empty_cells = self.find_adjacent_empty_cells()
            for index, (row, col) in enumerate(empty_cells):
                self.board[row][col] = player
                is_maximizing = False if player == AI else True
                score = self.minimax(0, is_maximizing, alpha, beta, row, col)
                self.board[row][col] = 0
                
                if (player == AI and score > best_score) or (player == PLAYER and score < best_score):
                    best_score = score
                    best_move = (row, col)
                
                percentage = (index / len(empty_cells)) * 100
                print(f"    [{percentage:.1f}%] <{player}> {row},{col} -> {score}")
            
            print(f"    {best_move[0]},{best_move[1]} ({best_score})")
            
        return best_move
    
    def show_message(self, message, is_exit=False):
        popup = True
        while popup:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()

                if event.type == pygame.KEYDOWN:
                    if is_exit:
                        if event.key == pygame.K_y:
                            self.reset_game()
                            popup = False
                            return
                        elif event.key == pygame.K_n:
                            pygame.quit() 
                            sys.exit()
                    else:
                        popup = False
                        message = ""
                        self.draw_board(self.screen)
                        break
            
            text_lines = message.split('\n')
            for i, line in enumerate(text_lines):
                text = self.font.render(line, True, (128, 0, 128))
                text_rect = text.get_rect(topleft=(20, 20 + i * 20))
                self.screen.blit(text, text_rect)
            
            pygame.display.flip()
    
    def draw_board(self, screen):
        screen.fill(GRAY)
        for row in range(BOARD_SIZE):# draw horizontal lines
            pygame.draw.line(screen, BLACK, 
                            (0 * CELL_SIZE + MARGIN, row * CELL_SIZE + MARGIN), 
                            ((BOARD_SIZE-1) * CELL_SIZE + MARGIN, row * CELL_SIZE + MARGIN),
                            1)
            for col in range(BOARD_SIZE):# draw vertical lines
                if row == 0:
                    pygame.draw.line(screen, BLACK, 
                                    (col * CELL_SIZE + MARGIN, 0 * CELL_SIZE + MARGIN), 
                                    (col * CELL_SIZE + MARGIN, (BOARD_SIZE-1) * CELL_SIZE + MARGIN),
                                    1)

                x = col * CELL_SIZE + MARGIN
                y = row * CELL_SIZE + MARGIN

                if self.board[row][col] == PLAYER:
                    pygame.draw.circle(screen, BLACK, (x, y), CLICK_RADIUS)
                elif self.board[row][col] == AI:
                    pygame.draw.circle(screen, WHITE, (x, y), CLICK_RADIUS)
                
                seq_no = self.sequences[row][col] 
                if seq_no != 0:
                    if seq_no == self.sequence: 
                        pygame.draw.circle(screen, RED, (x, y), CLICK_RADIUS+1, 1)
                    
                    color = WHITE if self.board[row][col] == PLAYER else BLACK
                    text = self.font.render(f"{seq_no}", True, color)
                    text_rect = text.get_rect(center=(x, y))
                    screen.blit(text, text_rect)

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.game_over = True
            elif event.type == pygame.MOUSEBUTTONDOWN and not self.game_over and self.turn_player == PLAYER:
                x, y = pygame.mouse.get_pos()
                row = int(round((y - MARGIN) / CELL_SIZE))
                col = int(round((x - MARGIN) / CELL_SIZE))
                if 0 <= row < BOARD_SIZE and 0 <= col < BOARD_SIZE and self.board[row][col] == 0:
                    self.make_move(row, col, self.turn_player)
                    self.draw_board(self.screen)
                    pygame.display.flip()
                    
                    if self.turn_player == AI:
                        best_move = self.find_best_move(self.turn_player)
                        if best_move is not None:
                            row, col = best_move
                            self.make_move(row, col, self.turn_player)
                        
                        self.draw_board(self.screen)
                        pygame.display.flip()
                            
                    if self.game_over == True:
                        if self.turn_player == 0:
                            self.show_message("Game draw!")
                        else:
                            self.show_message(f"Game Over!\n{'Player' if self.turn_player == PLAYER else 'AI'} wins!")

    def init_ML(self):
        hidden_layer_count = 3 * NUM_ITEMS
        self.model = tf.keras.Sequential([
            tf.keras.layers.Dense(hidden_layer_count, activation='relu', input_shape=(NUM_ITEMS,)),
            tf.keras.layers.Dense(NUM_ITEMS, activation='softmax')
        ])
        self.model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
        
    def learning(self):
        if os.path.exists(CSV_FILE_NAME) == False:
            x_data, y_data = self.generate_training_data(NUM_EPISODES)

            self.save_data_to_csv(x_data, y_data, CSV_FILE_NAME)
            print(f"{CSV_FILE_NAME} 저장 완료")
        else:
            x_data, y_data = self.load_data_from_csv(CSV_FILE_NAME)
        
        self.model.fit(x_data, y_data, epochs=100, verbose=1)
        self.model.save(H5_FILE_NAME)
        
        test_results = self.model.evaluate(x_data, y_data)
        print(f"손실(Loss): {test_results[0]}")
        print(f"정확도(Accuracy): {test_results[1]}")
 
    def generate_training_data(self, num_games):
        x_data = []
        y_data = []
        
        while self.episode < num_games:
            self.reset_game()
            
            x = []
            y = []
            while True:
                row, col = self.get_next_move(self.turn_player)
                x.append(np.array(self.board).flatten())
                y.append(np.eye(NUM_ITEMS)[row * BOARD_SIZE + col])
                
                self.draw_board(self.screen)
                pygame.display.flip()
                
                if self.check_winner(self.board, row, col):
                    #print(f"{self.board}")
                    break
                if self.is_board_full(self.board):
                    #print(f"{self.board}")
                    break
                
            if self.turn_player != PLAYER:
                del x[-1]
                del y[0]
                x_data.extend(x)
                y_data.extend(y)
                self.episode += 1
                print(f"{self.episode}: {self.turn_player} win.")

        return np.array(x_data), np.array(y_data)

    def get_next_move(self, player):
        if (player == AI and AI_MODE == "MINIMAX") or (player == PLAYER and PLAYER_MODE == "MINIMAX"):
            return self.minimax_move(player)
        else:
            return self.random_move(player)
        
    def save_data_to_csv(self, x_data, y_data, file_name):
        x_data_flat = [x.flatten() for x in x_data]
        y_data_flat = [y.flatten() for y in y_data]
        data = {'x_data': x_data_flat, 'y_data': y_data_flat}
        df = pd.DataFrame(data)
        df.to_csv(file_name, index=False)

    def load_data_from_csv(self, file_name):
        df = pd.read_csv(file_name)
        x_data_flat = df['x_data'].apply(lambda x: np.fromstring(x[1:-1], sep=' '))
        y_data_flat = df['y_data'].apply(lambda x: np.fromstring(x[1:-1], sep=' '))
        x_data = np.array(x_data_flat.to_list())
        y_data = np.array(y_data_flat.to_list())
        return x_data, y_data
    
    
if __name__ == "__main__":
    game = FIAR()
    game.init_ML()
    game.learning()

 

2023.10.27 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (1/5) - 기본 구현 (minimax, alpha-beta pruning)

2023.10.27 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (2/5) - 속도 최적화 1차 (minimax 속도 개선)

2023.10.28 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (3/5) - 속도 최적화 2차 (RANDOM모드 추가)

2023.10.29 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (4/5) - 훈련 데이터 생성 및 학습

2023.10.29 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (5/5) - 머신러닝으로 게임 구현

2023.11.03 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (5/5) - 3x3 체크 추가

 

반응형
블로그 이미지

SKY STORY

,
반응형

minimax를 이용한 방식으로는 대량의 훈련데이터를 생성하기에 너무 느리므로
각 빈자리에 대한 점수를 계산하여 높은 점수 자리에 돌을 놓는 랜덤모드 추가해 보았다.
다소 기계적인 소심한 수를 두지만 속도만큼은 확실히 빠르다!
 
<빈 자리 반환 함수 개선사항>
- 수를 놓았을 때 연결된 수가 3개 이상이고 5개를 놓을 수 있는 자리일 경우만 반환.
- 수를 놓았을 때 연결된 수가 4개가 되는 경우 즉시 수를 놓도록 처리.
 
<랜덤 모드의 점수 계산 방법>
아래의 계산을 각 방향(좌->우, 상->하, 좌상->우하, 우상->좌하)으로 4번 누적된 값을 반환.
- 연결된 돌의 수가 4개이고 5개 이상 돌을 놓을 수 있는 자리: 16점
- 연결된 돌의 수가 3개이고 5개 이상 돌을 놓을 수 있는 자리:  6점
- 연결된 돌의 수가 2개이고 5개 이상 돌을 놓을 수 있는 자리:  2점
 
랜덤 모드는 수가 놓여진 주변 셀에 대해 위의 랜덤모드 점수를 구해 가장 높은 점수 위치에 수를 놓도록 하였다.

#
# FIAR
# Created by netcanis on 2023/10/28.
#
# minimax
# alpha beta pruning
# 속도 개선 2차 

import pygame
import random
import numpy as np
import sys


BOARD_SIZE = 9  # (7,9,13,15,19)
CELL_SIZE = 40
MARGIN = 20
CLICK_RADIUS = CELL_SIZE // 2

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GRAY = (100, 100, 100)

NUM_ITEMS = (BOARD_SIZE * BOARD_SIZE)
PLAYER = 1
AI = -1

AI_MODE = "RANDOM"#"MINIMAX"


DIFFICULTY_LEVEL = 3


class FIAR:
    def __init__(self):
        self.init_game()
    
    def init_game(self):
        pygame.init()
        pygame.display.set_caption("FIAR")
        w = (BOARD_SIZE-1) * CELL_SIZE + 2 * MARGIN
        h = (BOARD_SIZE-1) * CELL_SIZE + 2 * MARGIN
        self.screen = pygame.display.set_mode((w, h))

        self.font = pygame.font.Font(None, CELL_SIZE-4)

    def reset_game(self):
        self.board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
        self.sequences = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
        self.sequence = 0
        self.game_over = False
        self.turn_player = PLAYER
        
        self.draw_board(self.screen)
        pygame.display.flip()
         
    def find_empty_cells(self):
        empty_cells = []
        for row in range(BOARD_SIZE):
            for col in range(BOARD_SIZE):
                if self.board[row][col] == 0:
                    empty_cells.append((row, col))
        random.shuffle(empty_cells)
        return empty_cells
    
    def find_adjacent_empty_cells(self):
        empty_cells = []
        empty_cells2 = []
        for row, col in self.find_empty_cells():
            for dr in [-1, 0, 1]:
                for dc in [-1, 0, 1]:
                    new_row, new_col = row + dr, col + dc
                    if 0 <= new_row < BOARD_SIZE and 0 <= new_col < BOARD_SIZE and self.board[new_row][new_col] != 0:
                        # 수를 놓았을 경우 연결된 수가 3개 이상이고 5개를 놓을 수 있는 자리일 경우만 배열에 추가.
                        self.board[row][col] = self.turn_player
                        c1, c2, _ = self.evaluate_position(self.board, row, col)
                        self.board[row][col] = -self.turn_player
                        c3, c4, _ = self.evaluate_position(self.board, row, col)
                        self.board[row][col] = 0
                        if (c1 >= 3 and c2 >= 5) or (c3 >= 3 and c4 >= 5):
                            empty_cells2.append((row, col))

                        empty_cells.append((row, col))
                        break
                    
        if len(empty_cells2) > 0:
            return empty_cells2
        
        return empty_cells
    
    def evaluate_position(self, board, row, col):
        def check_direction(dx, dy):
            count = 1
            r, c = row + dx, col + dy
            while 0 <= r < len(board) and 0 <= c < len(board[0]) and board[r][c] == board[row][col]:
                count += 1
                r += dx
                c += dy
            return count
        
        def check_direction_with_blank(dx, dy):
            count = 1
            r, c = row + dx, col + dy
            while 0 <= r < len(board) and 0 <= c < len(board[0]) and (board[r][c] == board[row][col] or board[r][c] == 0):
                count += 1
                r += dx
                c += dy
            return count
        
        total_count1 = 0
        total_count2 = 0
        total_score = 0
        directions = [(0, 1), (1, 0), (1, 1), (-1, 1)]
        for dx, dy in directions:
            count1 = check_direction( dx,  dy)
            count2 = check_direction(-dx, -dy)
            dir_count1 = count1 + count2 - 1
            count3 = check_direction_with_blank( dx,  dy)
            count4 = check_direction_with_blank(-dx, -dy)
            dir_count2 = count3 + count4 - 1
            total_count1 = max(total_count1, dir_count1)
            total_count2 = max(total_count2, dir_count2)
            # 각 방향별로 연결된 돌의 수가 3개 이상이고 5개 이상 놓을 수 있는 자리라면 점수를 추가한다.
            # 즉, 연결되는 돌이 교차하는 위치라면 점수가 높게 나온다.
            if dir_count1 == 4 and dir_count2 >= 5:
                total_score += dir_count1 * 4
            elif dir_count1 >= 3 and dir_count2 >= 5:
                total_score += dir_count1 * 2
            elif dir_count1 >= 2 and dir_count2 >= 5:
                total_score += dir_count1
                
        return total_count1, total_count2, total_score
    
    def check_winner(self, board, row, col):
        if board[row][col] == 0:
            return False
        
        def check_direction(dx, dy):
            count = 1
            r, c = row + dx, col + dy
            while 0 <= r < len(board) and 0 <= c < len(board[0]) and board[r][c] == board[row][col]:
                count += 1
                r += dx
                c += dy
            return count

        directions = [(0, 1), (1, 0), (1, 1), (-1, 1)]
        for dx, dy in directions:
            count1 = check_direction(dx, dy)
            count2 = check_direction(-dx, -dy)
            total_count = count1 + count2 - 1
            if total_count == 5:
                return True

        return False
    
    def is_board_full(self, board):
        return all(cell != 0 for row in board for cell in row)

    def random_move(self, player):
        if self.game_over:
            return -1, -1
        
        optimal_move = self.make_optimal_move(player)
        if optimal_move != None:
            return optimal_move
        
        
        empty_cells = self.find_adjacent_empty_cells()
        
        best_move = (0,0)
        best_score = 0
        
        for row, col in empty_cells: 
            self.board[row][col] = player
            s1 = self.evaluate_position(self.board, row, col)[2]
            if s1 > best_score:
                best_score = s1
                best_move = (row, col)
            self.board[row][col] = -player
            s2 = self.evaluate_position(self.board, row, col)[2]
            if s2 > best_score:
                best_score = s2
                best_move = (row, col)
            self.board[row][col] = 0
        
        if best_score == 0:
            best_move = random.choice(self.find_empty_cells())
            
        return best_move

    def minimax_move(self, player):
        if self.game_over:
            return -1, -1
        row, col = self.find_best_move(player)
        self.make_move(row, col, player)
        return row, col

    def make_move(self, row, col, player):
        if self.board[row][col] == 0:
            self.board[row][col] = player
            
            self.sequence += 1
            self.sequences[row][col] = self.sequence
            
            print(f"[{self.sequence}] <{player}> {row},{col}")
            
            if self.check_winner(self.board, row, col):
                self.game_over = True
            elif self.is_board_full(self.board):
                self.game_over = True
                self.turn_player = 0
            else:
                self.turn_player *= -1

    def minimax(self, depth, is_maximizing, alpha, beta, row2, col2):
        if self.is_board_full(self.board):
            return 0
        
        if is_maximizing:
            if self.check_winner(self.board, row2, col2):
                return -(NUM_ITEMS - depth + 1)
        else:
            if self.check_winner(self.board, row2, col2):
                return (NUM_ITEMS - depth + 1)

        if depth >= DIFFICULTY_LEVEL:
            return 0
        
        if is_maximizing:# AI
            best_score = -float('inf')
            for row, col in self.find_adjacent_empty_cells():
                self.board[row][col] = AI
                score = self.minimax(depth + 1, False, alpha, beta, row, col)
                self.board[row][col] = 0
                best_score = max(best_score, score)
                alpha = max(alpha, best_score)
                if beta <= alpha:
                    break
            return best_score
        else:
            best_score = float('inf')
            for row, col in self.find_adjacent_empty_cells():
                self.board[row][col] = PLAYER
                score = self.minimax(depth + 1, True, alpha, beta, row, col)
                self.board[row][col] = 0
                best_score = min(best_score, score)
                beta = min(beta, best_score)
                if beta <= alpha:
                    break
            return best_score
    
    def make_optimal_move(self, player):
        # 0. The first one is random.
        if self.sequence == 0:
            row = BOARD_SIZE // 2 + random.randint(-2, 2)
            col = BOARD_SIZE // 2 + random.randint(-2, 2)
            return (row, col)
        
        empty_cells = self.find_adjacent_empty_cells()

        # 1. ai turn: 5
        for row, col in empty_cells: 
            self.board[row][col] = player
            if self.check_winner(self.board, row, col):
                self.board[row][col] = 0
                return (row, col)
            self.board[row][col] = 0
            
        # 2. player turn: 5
        for row, col in empty_cells: 
            self.board[row][col] = -player
            if self.check_winner(self.board, row, col):
                self.board[row][col] = 0
                return (row, col)
            self.board[row][col] = 0
        
        if AI_MODE == "RANDOM":
            # 3. ai turn: 4
            for row, col in empty_cells: 
                self.board[row][col] = player
                c1, c2, _ = self.evaluate_position(self.board, row, col)
                self.board[row][col] = 0
                if c1 == 4 and c2 >= 5:
                    return (row, col)
                
            # 4. player turn: 4
            for row, col in empty_cells: 
                self.board[row][col] = -player
                c1, c2, _ = self.evaluate_position(self.board, row, col)
                self.board[row][col] = 0
                if c1 == 4 and c2 >= 5:
                    return (row, col)
            
        return None
        
    def find_best_move(self, player):
        if AI_MODE == "RANDOM":
            best_move = self.random_move(player)
        else: # MINIMAX
            print(f"[{self.sequence+1}] <{player}> ...")
            optimal_move = self.make_optimal_move(player)
            if optimal_move != None:
                return optimal_move
        
            alpha = -float('inf')
            beta = float('inf')

            best_move = None
            best_score = -float('inf') if player == AI else float('inf')

            empty_cells = self.find_adjacent_empty_cells()
            for index, (row, col) in enumerate(empty_cells):
                self.board[row][col] = player
                is_maximizing = False if player == AI else True
                score = self.minimax(0, is_maximizing, alpha, beta, row, col)
                self.board[row][col] = 0
                
                if (player == AI and score > best_score) or (player == PLAYER and score < best_score):
                    best_score = score
                    best_move = (row, col)
                
                percentage = (index / len(empty_cells)) * 100
                print(f"    [{percentage:.1f}%] <{player}> {row},{col} -> {score}")
            
            print(f"    {best_move[0]},{best_move[1]} ({best_score})")
            
        return best_move
    
    def show_message(self, message, is_exit=False):
        popup = True
        while popup:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()

                if event.type == pygame.KEYDOWN:
                    if is_exit:
                        if event.key == pygame.K_y:
                            self.reset_game()
                            popup = False
                            return
                        elif event.key == pygame.K_n:
                            pygame.quit() 
                            sys.exit()
                    else:
                        popup = False
                        message = ""
                        self.draw_board(self.screen)
                        break
            
            text_lines = message.split('\n')
            for i, line in enumerate(text_lines):
                text = self.font.render(line, True, (128, 0, 128))
                text_rect = text.get_rect(topleft=(20, 20 + i * 20))
                self.screen.blit(text, text_rect)
            
            pygame.display.flip()
    
    def draw_board(self, screen):
        screen.fill(GRAY)
        for row in range(BOARD_SIZE):# draw horizontal lines
            pygame.draw.line(screen, BLACK, 
                            (0 * CELL_SIZE + MARGIN, row * CELL_SIZE + MARGIN), 
                            ((BOARD_SIZE-1) * CELL_SIZE + MARGIN, row * CELL_SIZE + MARGIN),
                            1)
            for col in range(BOARD_SIZE):# draw vertical lines
                if row == 0:
                    pygame.draw.line(screen, BLACK, 
                                    (col * CELL_SIZE + MARGIN, 0 * CELL_SIZE + MARGIN), 
                                    (col * CELL_SIZE + MARGIN, (BOARD_SIZE-1) * CELL_SIZE + MARGIN),
                                    1)

                x = col * CELL_SIZE + MARGIN
                y = row * CELL_SIZE + MARGIN

                if self.board[row][col] == PLAYER:
                    pygame.draw.circle(screen, BLACK, (x, y), CLICK_RADIUS)
                elif self.board[row][col] == AI:
                    pygame.draw.circle(screen, WHITE, (x, y), CLICK_RADIUS)
                
                seq_no = self.sequences[row][col] 
                if seq_no != 0:
                    if seq_no == self.sequence: 
                        pygame.draw.circle(screen, RED, (x, y), CLICK_RADIUS+1, 1)
                    
                    color = WHITE if self.board[row][col] == PLAYER else BLACK
                    text = self.font.render(f"{seq_no}", True, color)
                    text_rect = text.get_rect(center=(x, y))
                    screen.blit(text, text_rect)

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.game_over = True
            elif event.type == pygame.MOUSEBUTTONDOWN and not self.game_over and self.turn_player == PLAYER:
                x, y = pygame.mouse.get_pos()
                row = int(round((y - MARGIN) / CELL_SIZE))
                col = int(round((x - MARGIN) / CELL_SIZE))
                if 0 <= row < BOARD_SIZE and 0 <= col < BOARD_SIZE and self.board[row][col] == 0:
                    self.make_move(row, col, self.turn_player)
                    self.draw_board(self.screen)
                    pygame.display.flip()
                    
                    if self.turn_player == AI:
                        best_move = self.find_best_move(self.turn_player)
                        self.make_move(best_move[0], best_move[1], self.turn_player)
                        
                        self.draw_board(self.screen)
                        pygame.display.flip()
                            
                    if self.game_over == True:
                        if self.turn_player == 0:
                            self.show_message("Game draw!")
                        else:
                            self.show_message(f"Game Over!\n{'Player' if self.turn_player == PLAYER else 'AI'} wins!")


if __name__ == "__main__":
    game = FIAR()
    game.reset_game()
    
    while True:
        while not game.game_over:
            game.handle_events()
        
        game.show_message("Play Again? (y/n)", is_exit=True)

 
속도는 비교할 수 없을 정도로 빨라졌다.
기계적으로 막기에 급급해 보이는 소심한 수를 놓는건 단점이다.
하지만 대량으로 학습 데이터를 만들기에는 충분한 듯하다.

 
 

2023.10.27 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (1/5) - 기본 구현 (minimax, alpha-beta pruning)

2023.10.27 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (2/5) - 속도 최적화 1차 (minimax 속도 개선)

2023.10.28 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (3/5) - 속도 최적화 2차 (RANDOM모드 추가)

2023.10.29 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (4/5) - 훈련 데이터 생성 및 학습

2023.10.29 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (5/5) - 머신러닝으로 게임 구현

2023.11.03 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (5/5) - 3x3 체크 추가

 

반응형
블로그 이미지

SKY STORY

,
반응형

minimax 속도를 개선하기 위해 다음과 같은 변경사항을 적용하였다.

1. 빈 자리를 검색할 때, 이미 돌이 놓여진 위치 주변에 있는 빈 자리만 반환.

2. AI가 돌을 놓았을 때, 승리 조건이 만족되는 위치라면 즉시 돌을 놓는다.

3. 플레이어가 돌을 놓았을 때, 승리 조건이 만족되는 위치라면 AI가 그 위치에 돌을 즉시 놓는다.

 

1번 빈 자리 검색 시 아래 그림과 같이 돌이 놓여진 주변 빈자리만 검색하여 반환되도록 한다.

반환된 빈 자리는 불필요한 트리 탐색을 줄여 속도가 개선된다.

빈 자리 검색시 반환되는 위치

 

#
# FIAR
# Created by netcanis on 2023/10/07.
#
# minimax
# alpha beta pruning
# 속도 개선 

import pygame
import random
import numpy as np
import sys


BOARD_SIZE = 9  # (7,9,13,15,19)
CELL_SIZE = 40
MARGIN = 20
CLICK_RADIUS = CELL_SIZE // 2

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GRAY = (100, 100, 100)

NUM_ITEMS = (BOARD_SIZE * BOARD_SIZE)
PLAYER = 1
AI = -1

DIFFICULTY_LEVEL = 3


class FIAR:
    def __init__(self):
        self.init_game()
    
    def init_game(self):
        pygame.init()
        pygame.display.set_caption("FIAR")
        w = (BOARD_SIZE-1) * CELL_SIZE + 2 * MARGIN
        h = (BOARD_SIZE-1) * CELL_SIZE + 2 * MARGIN
        self.screen = pygame.display.set_mode((w, h))

        self.font = pygame.font.Font(None, CELL_SIZE-4)

    def reset_game(self):
        self.board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
        self.sequences = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
        self.sequence = 0
        self.game_over = False
        self.turn_player = PLAYER
        
        self.draw_board(self.screen)
        pygame.display.flip()
         
    def find_empty_cells(self):
        empty_cells = []
        for row in range(BOARD_SIZE):
            for col in range(BOARD_SIZE):
                if self.board[row][col] == 0:
                    empty_cells.append((row, col))
        random.shuffle(empty_cells)
        return empty_cells
    
    # 수가 놓여진 자리의 주변 빈 자리만 반환
    def find_adjacent_empty_cells(self):
        empty_cells = []
        for row, col in self.find_empty_cells():
            for dr in [-1, 0, 1]:
                for dc in [-1, 0, 1]:
                    new_row, new_col = row + dr, col + dc
                    if 0 <= new_row < BOARD_SIZE and 0 <= new_col < BOARD_SIZE and self.board[new_row][new_col] != 0:
                        empty_cells.append((row, col))
                        break
        return empty_cells
    
    def check_winner(self, board, row, col):
        def check_direction(dx, dy):
            count = 1
            r, c = row + dx, col + dy
            while 0 <= r < len(board) and 0 <= c < len(board[0]) and board[r][c] == board[row][col]:
                count += 1
                r += dx
                c += dy
            return count

        directions = [(0, 1), (1, 0), (1, 1), (-1, 1)]
        for dx, dy in directions:
            count1 = check_direction(dx, dy)
            count2 = check_direction(-dx, -dy)
            total_count = count1 + count2 - 1
            if total_count == 5:
                return True

        return False
    
    def is_board_full(self, board):
        return all(cell != 0 for row in board for cell in row)

    def random_move(self, player):
        if self.game_over:
            return -1, -1
        row, col = random.choice(self.find_adjacent_empty_cells())
        self.make_move(row, col, player)
        return row, col

    def minimax_move(self, player):
        if self.game_over:
            return -1, -1
        row, col = self.find_best_move(player)
        self.make_move(row, col, player)
        return row, col

    def make_move(self, row, col, player):
        if self.board[row][col] == 0:
            self.board[row][col] = player
            
            self.sequence += 1
            self.sequences[row][col] = self.sequence
            
            print(f"[{self.sequence}] <{player}> {row},{col}")
            
            if self.check_winner(self.board, row, col):
                self.game_over = True
            elif self.is_board_full(self.board):
                self.game_over = True
                self.turn_player = 0
            else:
                self.turn_player *= -1

    def minimax(self, depth, is_maximizing, alpha, beta, row2, col2):
        if self.is_board_full(self.board):
            return 0
        
        if is_maximizing:
            if self.check_winner(self.board, row2, col2):
                return -(NUM_ITEMS - depth + 1)
        else:
            if self.check_winner(self.board, row2, col2):
                return (NUM_ITEMS - depth + 1)

        if depth >= DIFFICULTY_LEVEL:
            return 0
        
        if is_maximizing:# AI
            best_score = -float('inf')
            for row, col in self.find_adjacent_empty_cells():
                self.board[row][col] = AI
                score = self.minimax(depth + 1, False, alpha, beta, row, col)
                self.board[row][col] = 0
                best_score = max(best_score, score)
                alpha = max(alpha, best_score)
                if beta <= alpha:
                    break
            return best_score
        else:
            best_score = float('inf')
            for row, col in self.find_adjacent_empty_cells():
                self.board[row][col] = PLAYER
                score = self.minimax(depth + 1, True, alpha, beta, row, col)
                self.board[row][col] = 0
                best_score = min(best_score, score)
                beta = min(beta, best_score)
                if beta <= alpha:
                    break
            return best_score
    
    # 아래 우선순위에 따라 즉시 처리할 자리는 minimax 알고리즘 없이 즉시 수를 놓아 속도 개선
    # 1.만약 AI가 놓으면 게임을 이기는 자리라면 즉시 수를 놓는다. 
    # 2.만약 Player가 놓으면 게임을 이기는 자리라면 AI는 그 곳에 수는 놓는다.
    def make_optimal_move(self, player):
        # 0. The first one is random.
        if self.sequence == 0:
            row = BOARD_SIZE // 2 + random.randint(-2, 2)
            col = BOARD_SIZE // 2 + random.randint(-2, 2)
            return (row, col)
        
        empty_cells = self.find_adjacent_empty_cells()

        # 1. ai turn: 5
        for row, col in empty_cells: 
            self.board[row][col] = player
            if self.check_winner(self.board, row, col):
                self.board[row][col] = 0
                return (row, col)
            self.board[row][col] = 0
            
        # 2. player turn: 5
        for row, col in empty_cells: 
            self.board[row][col] = -player
            if self.check_winner(self.board, row, col):
                self.board[row][col] = 0
                return (row, col)
            self.board[row][col] = 0
        
        return None
        
    def find_best_move(self, player):
        
        print(f"[{self.sequence+1}] <{player}> Calculating...")
        
        optimal_move = self.make_optimal_move(player)
        if optimal_move != None:
            return optimal_move
        
        alpha = -float('inf')
        beta = float('inf')

        best_move = None
        best_score = -float('inf') if player == AI else float('inf')

        empty_cells = self.find_adjacent_empty_cells()
        for index, (row, col) in enumerate(empty_cells):
            self.board[row][col] = player
            is_maximizing = False if player == AI else True
            score = self.minimax(0, is_maximizing, alpha, beta, row, col)
            self.board[row][col] = 0
            
            if (player == AI and score > best_score) or (player == PLAYER and score < best_score):
                best_score = score
                best_move = (row, col)
            
            percentage = (index / len(empty_cells)) * 100
            print(f"    [{percentage:.1f}%] <{player}> {row},{col} -> {score}")
            
        return best_move
    
    def show_message(self, message, is_exit=False):
        popup = True
        while popup:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()

                if event.type == pygame.KEYDOWN:
                    if is_exit:
                        if event.key == pygame.K_y:
                            self.reset_game()
                            popup = False
                            return
                        elif event.key == pygame.K_n:
                            pygame.quit() 
                            sys.exit()
                    else:
                        popup = False
                        break
            
            text_lines = message.split('\n')
            for i, line in enumerate(text_lines):
                text = self.font.render(line, True, (128, 0, 128))
                text_rect = text.get_rect(topleft=(20, 20 + i * 20))
                self.screen.blit(text, text_rect)
            
            pygame.display.flip()
    
    def draw_board(self, screen):
        screen.fill(GRAY)
        for row in range(BOARD_SIZE):# draw horizontal lines
            pygame.draw.line(screen, BLACK, 
                            (0 * CELL_SIZE + MARGIN, row * CELL_SIZE + MARGIN), 
                            ((BOARD_SIZE-1) * CELL_SIZE + MARGIN, row * CELL_SIZE + MARGIN),
                            1)
            for col in range(BOARD_SIZE):# draw vertical lines
                if row == 0:
                    pygame.draw.line(screen, BLACK, 
                                    (col * CELL_SIZE + MARGIN, 0 * CELL_SIZE + MARGIN), 
                                    (col * CELL_SIZE + MARGIN, (BOARD_SIZE-1) * CELL_SIZE + MARGIN),
                                    1)

                x = col * CELL_SIZE + MARGIN
                y = row * CELL_SIZE + MARGIN

                if self.board[row][col] == PLAYER:
                    pygame.draw.circle(screen, BLACK, (x, y), CLICK_RADIUS)
                elif self.board[row][col] == AI:
                    pygame.draw.circle(screen, WHITE, (x, y), CLICK_RADIUS)
                
                seq_no = self.sequences[row][col] 
                if seq_no != 0:
                    if seq_no == self.sequence: 
                        pygame.draw.circle(screen, RED, (x, y), CLICK_RADIUS+1, 1)
                    
                    color = WHITE if self.board[row][col] == PLAYER else BLACK
                    text = self.font.render(f"{seq_no}", True, color)
                    text_rect = text.get_rect(center=(x, y))
                    screen.blit(text, text_rect)

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.game_over = True
            elif event.type == pygame.MOUSEBUTTONDOWN and not self.game_over and self.turn_player == PLAYER:
                x, y = pygame.mouse.get_pos()
                row = int(round((y - MARGIN) / CELL_SIZE))
                col = int(round((x - MARGIN) / CELL_SIZE))
                if 0 <= row < BOARD_SIZE and 0 <= col < BOARD_SIZE and self.board[row][col] == 0:
                    self.make_move(row, col, self.turn_player)
                    self.draw_board(self.screen)
                    pygame.display.flip()
                    
                    if self.turn_player == AI:
                        best_move = self.find_best_move(self.turn_player)
                        if best_move is not None:
                            row, col = best_move
                            self.make_move(row, col, self.turn_player)
                        
                        self.draw_board(self.screen)
                        pygame.display.flip()
                            
                    if self.game_over == True:
                        if self.turn_player == 0:
                            self.show_message("Game draw!")
                        else:
                            self.show_message(f"Game Over!\n{'Player' if self.turn_player == PLAYER else 'AI'} wins!")

    
if __name__ == "__main__":
    game = FIAR()
    game.reset_game()
    
    while True:
        while not game.game_over:
            game.handle_events()
        
        game.show_message("Play Again? (y/n)", is_exit=True)

 

속도는 다소 빨라지긴 했다.

3x3을 만들면 이미 진 것을 예감한 것인지 AI는 그 때부터 엉뚱한 수를 놓는다.  

마치 포기한 것처럼...

 

2023.10.27 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (1/5) - 기본 구현 (minimax, alpha-beta pruning)

2023.10.27 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (2/5) - 속도 최적화 1차 (minimax 속도 개선)

2023.10.28 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (3/5) - 속도 최적화 2차 (RANDOM모드 추가)

2023.10.29 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (4/5) - 훈련 데이터 생성 및 학습

2023.10.29 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (5/5) - 머신러닝으로 게임 구현

2023.11.03 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (5/5) - 3x3 체크 추가

 

 

 

반응형
블로그 이미지

SKY STORY

,
반응형

tic tac toe 게임 머신러닝에 이어 오목 게임을 만들어 보자.

아래와 같은 순서로 총 5회로 구성하였다.

Gomoku(Five in a Row, Omok) (1/5) - 기본 구현 (minimax, alpha-beta pruning)

Gomoku(Five in a Row, Omok) (2/5) - 속도 최적화 1차 (minimax 속도 개선)

Gomoku(Five in a Row, Omok) (3/5) - 속도 최적화 2차 (RANDOM모드 추가)

Gomoku(Five in a Row, Omok) (4/5) - 훈련 데이터 생성 및 학습

Gomoku(Five in a Row, Omok) (5/5) - 머신러닝으로 게임 구현

 

9x9보드에서 minimax알고리즘을 사용하여 간단하게 구현해 보았다.

참고로 3x3을 막고있지 않다. 

DIFFICULTY_LEVEL 값을 높이면 좀더 잘 두지만 속도는 느려진다. 반대로 값을 낮추면  속도는 빨라지지만 잘 두지는 못한다.

가끔씩 엉뚱한 수를 두는 이유는 DIFFICULTY_LEVEL 제한으로 minimax트리검색이 중간에 중단되어 마지막으로 중단된 위치에 수를 놓기 때문이다. 해당 속도문제를 속도 최적화 1차, 속도 최적화 2차를 통해 개선하도록 하겠다.

# 필요한 모듈 임포트
import pygame
import random
import numpy as np
import sys

# 게임 보드 설정
BOARD_SIZE = 9  # 게임 보드 크기 (9x9)
CELL_SIZE = 40  # 셀 크기
MARGIN = 20  # 보드와 화면 경계의 여백
CLICK_RADIUS = CELL_SIZE // 2  # 클릭 시 지정된 반지름

# 게임 보드의 셀 상태
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GRAY = (100, 100, 100)

# 게임 변수 및 난이도 설정
NUM_ITEMS = (BOARD_SIZE * BOARD_SIZE)
PLAYER = 1
AI = -1

DIFFICULTY_LEVEL = 3  # 게임 난이도 (컴퓨터가 어려운 수를 계산하는 깊이)


# 게임 클래스 정의
class FIAR:
    def __init__(self):
        self.init_game()

    def init_game(self):
        # Pygame 초기화
        pygame.init()
        pygame.display.set_caption("FIAR")
        w = (BOARD_SIZE-1) * CELL_SIZE + 2 * MARGIN
        h = (BOARD_SIZE-1) * CELL_SIZE + 2 * MARGIN
        self.screen = pygame.display.set_mode((w, h))
        self.font = pygame.font.Font(None, CELL_SIZE-4)

    # 게임 초기화
    def reset_game(self):
        self.board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
        self.sequences = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
        self.sequence = 0
        self.game_over = False
        self.turn_player = PLAYER
        self.draw_board(self.screen)
        pygame.display.flip()

    # 빈 셀 찾기
    def find_empty_cells(self):
        empty_cells = []
        for row in range(BOARD_SIZE):
            for col in range(BOARD_SIZE):
                if self.board[row][col] == 0:
                    empty_cells.append((row, col))
        random.shuffle(empty_cells)
        return empty_cells

    # 승자 확인
    def check_winner(self, board, row, col):
        # 승자 판별 로직
        def check_direction(dx, dy):
            count = 1
            r, c = row + dx, col + dy
            while 0 <= r < len(board) and 0 <= c < len(board[0]) and board[r][c] == board[row][col]:
                count += 1
                r += dx
                c += dy
            return count

        directions = [(0, 1), (1, 0), (1, 1), (-1, 1)]
        for dx, dy in directions:
            count1 = check_direction(dx, dy)
            count2 = check_direction(-dx, -dy)
            total_count = count1 + count2 - 1
            if total_count == 5:
                return True
        return False

    # 게임 보드가 가득 찼는지 확인
    def is_board_full(self, board):
        return all(cell != 0 for row in board for cell in row)

    # 무작위로 수를 두는 함수
    def random_move(self, player):
        if self.game_over:
            return -1, -1
        row, col = random.choice(self.find_empty_cells())
        self.make_move(row, col, player)
        return row, col

    # 미니맥스 알고리즘을 사용해 수를 두는 함수
    def minimax_move(self, player):
        if self.game_over:
            return -1, -1
        row, col = self.find_best_move(player)
        self.make_move(row, col, player)
        return row, col

    # 수를 두는 함수
    def make_move(self, row, col, player):
        # 게임 보드에 수를 두고 승자를 확인하는 로직
        if self.board[row][col] == 0:
            self.board[row][col] = player
            self.sequence += 1
            self.sequences[row][col] = self.sequence
            if self.check_winner(self.board, row, col):
                self.game_over = True
            elif self.is_board_full(self.board):
                self.game_over = True
                self.turn_player = 0
            else:
                self.turn_player *= -1

    # 미니맥스 알고리즘
    def minimax(self, depth, is_maximizing, alpha, beta, row2, col2):
        # 미니맥스 알고리즘으로 최적의 수를 찾는 로직
        if self.is_board_full(self.board):
            return 0

        if is_maximizing:
            if self.check_winner(self.board, row2, col2):
                return -(NUM_ITEMS - depth + 1)
        else:
            if self.check_winner(self.board, row2, col2):
                return (NUM_ITEMS - depth + 1)

        if depth >= DIFFICULTY_LEVEL:
            return 0

        if is_maximizing:
            best_score = -float('inf')
            for row, col in self.find_empty_cells():
                self.board[row][col] = AI
                score = self.minimax(depth + 1, False, alpha, beta, row, col)
                self.board[row][col] = 0
                best_score = max(best_score, score)
                alpha = max(alpha, best_score)
                if beta <= alpha:
                    break
            return best_score
        else:
            best_score = float('inf')
            for row, col in self.find_empty_cells():
                self.board[row][col] = PLAYER
                score = self.minimax(depth + 1, True, alpha, beta, row, col)
                self.board[row][col] = 0
                best_score = min(best_score, score)
                beta = min(beta, best_score)
                if beta <= alpha:
                    break
            return best_score

    # 최적의 수를 찾는 함수
    def find_best_move(self, player):
        # 최적의 수를 찾는 함수
        if self.sequence == 0:
            row = BOARD_SIZE // 2 + random.randint(-2, 2)
            col = BOARD_SIZE // 2 + random.randint(-2, 2)
            return (row, col)

        alpha = -float('inf')
        beta = float('inf')
        best_move = None
        best_score = -float('inf') if player == AI else float('inf')
        empty_cells = self.find_empty_cells()

        for index, (row, col) in enumerate(empty_cells):
            self.board[row][col] = player
            is_maximizing = False if player == AI else True
            score = self.minimax(0, is_maximizing, alpha, beta, row, col)
            self.board[row][col] = 0

            if (player == AI and score > best_score) or (player == PLAYER and score < best_score):
                best_score = score
                best_move = (row, col)

            percentage = (index / len(empty_cells)) * 100
            print(f"    [{percentage:.1f}%] <{player}> {row},{col} -> {score}")

        return best_move

    # 화면에 메시지 표시
    def show_message(self, message, is_exit=False):
        popup = True
        while popup:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()

                if event.type == pygame.KEYDOWN:
                    if is_exit:
                        if event.key == pygame.K_y:
                            self.reset_game()
                            popup = False
                            return
                        elif event.key == pygame.K_n:
                            pygame.quit()
                            sys.exit()
                    else:
                        popup = False
                        break

            # 메시지 텍스트 표시
            text_lines = message.split('\n')
            for i, line in enumerate(text_lines):
                text = self.font.render(line, True, (0, 0, 0))
                text_rect = text.get_rect(topleft=(20, 20 + i * 20))
                self.screen.blit(text, text_rect)

            pygame.display.flip()

    # 게임 보드 그리기
    def draw_board(self, screen):
        screen.fill(GRAY)
        for row in range(BOARD_SIZE):
            pygame.draw.line(screen, BLACK,
                            (0 * CELL_SIZE + MARGIN, row * CELL_SIZE + MARGIN),
                            ((BOARD_SIZE-1) * CELL_SIZE + MARGIN, row * CELL_SIZE + MARGIN),
                            1)
            for col in range(BOARD_SIZE):
                if row == 0:
                    pygame.draw.line(screen, BLACK,
                                    (col * CELL_SIZE + MARGIN, 0 * CELL_SIZE + MARGIN),
                                    (col * CELL_SIZE + MARGIN, (BOARD_SIZE-1) * CELL_SIZE + MARGIN),
                                    1)

                x = col * CELL_SIZE + MARGIN
                y = row * CELL_SIZE + MARGIN

                if self.board[row][col] == PLAYER:
                    pygame.draw.circle(screen, BLACK, (x, y), CLICK_RADIUS)
                elif self.board[row][col] == AI:
                    pygame.draw.circle(screen, WHITE, (x, y), CLICK_RADIUS)

                seq_no = self.sequences[row][col]
                if seq_no != 0:
                    if seq_no == self.sequence:
                        pygame.draw.circle(screen, RED, (x, y), CLICK_RADIUS+1, 1)
                    color = WHITE if self.board[row][col] == PLAYER else BLACK
                    text = self.font.render(f"{seq_no}", True, color)
                    text_rect = text.get_rect(center=(x, y))
                    screen.blit(text, text_rect)

    # 이벤트 처리
    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.game_over = True
            elif event.type == pygame.MOUSEBUTTONDOWN and not self.game_over:
                x, y = pygame.mouse.get_pos()
                row = int(round((y - MARGIN) / CELL_SIZE))
                col = int(round((x - MARGIN) / CELL_SIZE))
                if 0 <= row < BOARD_SIZE and 0 <= col < BOARD_SIZE and self.board[row][col] == 0:
                    self.make_move(row, col, self.turn_player)
                    self.draw_board(self.screen)
                    pygame.display.flip()

                    if self.turn_player == AI:
                        best_move = self.find_best_move(self.turn_player)
                        if best_move is not None:
                            row, col = best_move
                            self.make_move(row, col, self.turn_player)
                            self.draw_board(self.screen)
                            pygame.display.flip()

                    if self.game_over == True:
                        if self.turn_player == 0:
                            self.show_message("Game draw!")
                        else:
                            self.show_message(f"Game Over!\n{'Player' if self.turn_player == PLAYER else 'AI'} wins!")

if __name__ == "__main__":
    game = FIAR()
    game.reset_game()

    while True:
        while not game.game_over:
            game.handle_events()
        
        game.show_message("Play Again? (y/n)", is_exit=True)

 

아래는 게임 실행화면이다.
게임 오버 상태에서 텍스트가 겹치는 오류가 있네.. -_-;;
중요한 문제 아니니 그건 나중에 수정하도록 하겠다.

 

2023.10.27 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (1/5) - 기본 구현 (minimax, alpha-beta pruning)

2023.10.27 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (2/5) - 속도 최적화 1차 (minimax 속도 개선)

2023.10.28 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (3/5) - 속도 최적화 2차 (RANDOM모드 추가)

2023.10.29 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (4/5) - 훈련 데이터 생성 및 학습

2023.10.29 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (5/5) - 머신러닝으로 게임 구현

2023.11.03 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (5/5) - 3x3 체크 추가

 

반응형
블로그 이미지

SKY STORY

,
반응형
# Tic Tac Toe (4/4)
# Created by netcanis on 2023/09/09.
#
# Minimax
# Alpha–beta pruning
# h5파일 로딩, 게임 GUI.
# 게임 테스트.

import tkinter as tk
from tkinter import messagebox
import random
import numpy as np
import tensorflow as tf
from keras.models import load_model

PLAYER = 1
AI = -1
H5_FILE_NAME = "ttt_model.h5"

class TTT:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("TTT")

        self.init_neural_network()
        self.start_game()

    def init_game(self):
        self.board = [[0 for _ in range(3)] for _ in range(3)]
        self.buttons = [[None for _ in range(3)] for _ in range(3)]
        self.sequence = 0
        self.game_over = False
        self.turn_player = random.choice([PLAYER, AI])
        
        for row in range(3):
            for col in range(3):
                self.buttons[row][col] = tk.Button(
                    self.window,
                    text=' ',
                    font=("Helvetica", 24),
                    height=1,
                    width=1,
                    command=lambda r=row, c=col: self.make_move(r, c, PLAYER),
                )
                self.buttons[row][col].grid(row=row, column=col)

    def find_empty_cells(self):
        empty_cells = []
        for row in range(3):
            for col in range(3):
                if self.board[row][col] == 0:
                    empty_cells.append((row, col))
        return empty_cells

    def check_winner(self, board, player):
        for row in board:
            if all(cell == player for cell in row):
                return True
        for col in range(3):
            if all(board[row][col] == player for row in range(3)):
                return True
        if all(board[i][i] == player for i in range(3)) or all(board[i][2 - i] == player for i in range(3)):
            return True
        return False

    def is_board_full(self, board):
        return all(cell != 0 for row in board for cell in row)

    def make_move(self, row, col, turn_player):
        if self.board[row][col] == 0:
            self.board[row][col] = turn_player
            self.updateBoardUI(row, col, turn_player)
            
            self.sequence += 1

            if self.check_winner(self.board, turn_player):
                self.game_over = True
            elif self.is_board_full(self.board):
                self.game_over = True
                self.turn_player = 0
            else:
                self.turn_player *= -1

    def wait_for_player_move(self):
        player_move_var = tk.IntVar()
        for row in range(3):
            for col in range(3):
                self.buttons[row][col]["command"] = lambda r=row, c=col: player_move_var.set(r * 3 + c)
        self.window.wait_variable(player_move_var)

        player_move = player_move_var.get()
        row = player_move // 3
        col = player_move % 3
        self.make_move(row, col, PLAYER)
    
    def wait_for_player_restart(self):
        response = messagebox.askyesno("Game Over", "Do you want to play again?")
        if response:
            self.start_game()
        else:
            self.window.quit()
            
    def updateBoardUI(self, row, col, turn_player):
        self.buttons[row][col]["text"] = 'O' if turn_player == 1 else 'X'
        self.buttons[row][col]["state"] = "disabled"
        self.window.update()

    def random_move(self, turn_player):
        if self.game_over == True:
            return -1, -1
        row, col = random.choice(self.find_empty_cells())
        self.make_move(row, col, turn_player)
        return row, col

    def init_neural_network(self):
        self.model = tf.keras.Sequential([
            tf.keras.layers.Dense(27, activation='relu', input_shape=(9,)),
            tf.keras.layers.Dense(9, activation='softmax')
        ])

        self.model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

    def predicts(self, input_data):
        if isinstance(input_data, list):
            input_data = np.array(input_data)

        prediction = self.model.predict(input_data.reshape(1, -1))
        sorted_indices = np.argsort(prediction, axis=-1)[:, ::-1]

        index = 0
        for i in sorted_indices[0]:
            if input_data.shape == (9,):
                if input_data[i] == 0:
                    index = i
                    break
            elif input_data.shape == (3, 3):
                row = i // 3
                col = i % 3
                if input_data[row][col] == 0:
                    index = i
                    break

        #max_value = prediction[0, index]
        return index

    def start_game(self):
        self.init_game()
        self.model = load_model(H5_FILE_NAME)
        
        while not self.game_over:
            if self.turn_player == AI:
                next_move = self.predicts(self.board)
                row = next_move // 3
                col = next_move % 3
                self.make_move(row, col, self.turn_player)
            else:
                self.wait_for_player_move()
        
        if self.turn_player == AI:
            messagebox.showinfo("Game Over", "AI wins!")
        elif self.turn_player == PLAYER:
            messagebox.showinfo("Game Over", "Player wins!")
        else:
            messagebox.showinfo("Game Over", "It's a draw!")
        
        self.wait_for_player_restart()
                    
    def run(self):
        self.window.mainloop()

if __name__ == "__main__":
    game = TTT()
    game.run()

 

 

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (1/4) - minimax

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (2/4) - alpha–beta pruning

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (3/4) - 머신러닝 훈련 데이터 생성

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (4/4) - 머신러닝을 이용한 게임 구현

 

 

 

 

반응형
블로그 이미지

SKY STORY

,
반응형

이제 학습 데이터를 생성하도록 한다.

플레이어는 랜덤, AI는 minimax 알고리즘을 이용하여 수를 두며

학습 데이터는 AI가 이기거나 비긴 데이터만 저장하도록 하였다.

# Tic Tac Toe (3/4)
# Created by netcanis on 2023/09/09.
#
# Minimax
# Alpha–beta pruning
# generate training data, csv파일 저장.
# 머신러닝, h5파일 저장.


import os
import tkinter as tk
import random
import numpy as np
import pandas as pd
import tensorflow as tf
from keras.models import Sequential, load_model

NUM_ITEMS = 9
PLAYER = 1
AI = -1

PLAYER_MODE = "RANDOM"
AI_MODE = "MINIMAX"

NUM_EPISODES = 50000
CSV_FILE_NAME = "ttt_training_data.csv"
H5_FILE_NAME = "ttt_model.h5"

class TTT:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("TTT")

        self.episode = 0

        self.init_ML()
        self.learning()

    def init_game(self):
        self.board = [[0 for _ in range(3)] for _ in range(3)]
        self.sequence = 0
        self.game_over = False
        self.turn_player = random.choice([PLAYER, AI])

    def find_empty_cells(self):
        empty_cells = []
        for row in range(3):
            for col in range(3):
                if self.board[row][col] == 0:
                    empty_cells.append((row, col))
        return empty_cells

    def check_winner(self, board, player):
        for row in board:
            if all(cell == player for cell in row):
                return True
        for col in range(3):
            if all(board[row][col] == player for row in range(3)):
                return True
        if all(board[i][i] == player for i in range(3)) or all(board[i][2 - i] == player for i in range(3)):
            return True
        return False

    def is_board_full(self, board):
        return all(cell != 0 for row in board for cell in row)

    def random_move(self, player):
        if self.game_over:
            return -1, -1
        row, col = random.choice(self.find_empty_cells())
        self.make_move(row, col, player)
        return row, col

    def minimax_move(self, player):
        if self.game_over:
            return -1, -1
        row, col = self.find_best_move(player)
        self.make_move(row, col, player)
        return row, col

    def make_move(self, row, col, player):
        if self.board[row][col] == 0:
            self.board[row][col] = player

            self.sequence += 1

            if self.check_winner(self.board, player):
                self.game_over = True
                print(f"Game Over! {'Player' if player == PLAYER else 'AI'} wins!")
            elif self.is_board_full(self.board):
                self.game_over = True
                self.turn_player = 0
                print("Game draw!")
            else:
                self.turn_player *= -1

    def find_best_move(self, player):
        if self.sequence <= 1:
            return random.choice(self.find_empty_cells())

        alpha = -float('inf')
        beta = float('inf')

        best_move = None
        if player == AI:
            best_score = -float('inf')
        else:
            best_score = float('inf')

        for row, col in self.find_empty_cells():
            self.board[row][col] = player
            if player == AI:
                score = self.minimax(0, False, alpha, beta)
            else:
                score = self.minimax(0, True, alpha, beta)
            self.board[row][col] = 0

            if (player == AI and score > best_score) or \
                    (player == PLAYER and score < best_score):
                best_score = score
                best_move = (row, col)

        return best_move

    def minimax(self, depth, is_maximizing, alpha, beta):
        if self.check_winner(self.board, AI):
            return (NUM_ITEMS + 1 - depth)

        if self.check_winner(self.board, PLAYER):
            return -(NUM_ITEMS + 1 - depth)

        if self.is_board_full(self.board):
            return 0

        if is_maximizing:
            best_score = -float('inf')
            for row, col in self.find_empty_cells():
                self.board[row][col] = AI
                score = self.minimax(depth + 1, False, alpha, beta)
                self.board[row][col] = 0
                best_score = max(best_score, score)
                alpha = max(alpha, best_score)
                if beta <= alpha:
                    break
            return best_score
        else:
            best_score = float('inf')
            for row, col in self.find_empty_cells():
                self.board[row][col] = PLAYER
                score = self.minimax(depth + 1, True, alpha, beta)
                self.board[row][col] = 0
                best_score = min(best_score, score)
                beta = min(beta, best_score)
                if beta <= alpha:
                    break
            return best_score

    def updateBoardUI(self, row, col, player):
        pass

    def save_data_to_csv(self, x_data, y_data, file_name):
        x_data_flat = [x.flatten() for x in x_data]
        y_data_flat = [y.flatten() for y in y_data]

        data = {'x_data': x_data_flat, 'y_data': y_data_flat}
        df = pd.DataFrame(data)

        df.to_csv(file_name, index=False)

    def load_data_from_csv(self, file_name):
        df = pd.read_csv(file_name)

        x_data_flat = df['x_data'].apply(lambda x: np.fromstring(x[1:-1], sep=' '))
        y_data_flat = df['y_data'].apply(lambda x: np.fromstring(x[1:-1], sep=' '))

        x_data = np.array(x_data_flat.to_list())
        y_data = np.array(y_data_flat.to_list())

        return x_data, y_data

    def generate_training_data(self, num_games):
        x_data = []
        y_data = []

        while self.episode < num_games:
            self.init_game()

            x = []
            y = []
            while True:
                row, col = self.get_next_move(self.turn_player)
                x.append(np.array(self.board).flatten())
                y.append(np.eye(9)[row * 3 + col])

                if self.check_winner(self.board, PLAYER):
                    break
                elif self.check_winner(self.board, AI):
                    break
                elif self.is_board_full(self.board):
                    break

            if self.turn_player != PLAYER:
                del x[-1]
                del y[0]
                x_data.extend(x)
                y_data.extend(y)
                self.episode += 1
                print(f"{self.episode}: {self.turn_player} win.")

        return np.array(x_data), np.array(y_data)

    def get_next_move(self, player):
        if (player == AI and AI_MODE == "MINIMAX") or (player == PLAYER and PLAYER_MODE == "MINIMAX"):
            return self.minimax_move(player)
        else:
            return self.random_move(player)

    def init_ML(self):
        # hidden layer : 27 - 각 위치당 3가지 상태(1,-1,0)이고 총 9개의 자리이므로 3x9=27
        self.model = tf.keras.Sequential([
            tf.keras.layers.Dense(27, activation='relu', input_shape=(9,)),
            tf.keras.layers.Dense(9, activation='softmax')
        ])

        self.model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

    def learning(self):
        if os.path.exists(CSV_FILE_NAME) == False:
            x_data, y_data = self.generate_training_data(NUM_EPISODES)

            self.save_data_to_csv(x_data, y_data, CSV_FILE_NAME)
            print(f"{CSV_FILE_NAME} 저장 완료")
        else:
            x_data, y_data = self.load_data_from_csv(CSV_FILE_NAME)

        self.model.fit(x_data, y_data, epochs=100, verbose=1)
        self.model.save(H5_FILE_NAME)

        test_results = self.model.evaluate(x_data, y_data)
        print(f"손실(Loss): {test_results[0]}")
        print(f"정확도(Accuracy): {test_results[1]}")

    def predicts(self, input_data):
        if isinstance(input_data, list):
            input_data = np.array(input_data)

        prediction = self.model.predict(input_data.reshape(1, -1))
        sorted_indices = np.argsort(prediction, axis=-1)[:, ::-1]

        index = 0
        for i in sorted_indices[0]:
            if input_data.shape == (9,):
                if input_data[i] == 0:
                    index = i
                    break
            elif input_data.shape == (3, 3):
                row = i // 3
                col = i % 3
                if input_data[row][col] == 0:
                    index = i
                    break

        #max_value = prediction[0, index]
        return index

    def run(self):
        self.window.mainloop()


if __name__ == "__main__":
    game = TTT()
    game.run()

 

 

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (1/4) - minimax

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (2/4) - alpha–beta pruning

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (3/4) - 머신러닝 훈련 데이터 생성

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (4/4) - 머신러닝을 이용한 게임 구현

 

반응형
블로그 이미지

SKY STORY

,
반응형

Tic-Tac-Toe와 같은 단순한 게임의 경우 minimax 알고리즘을 사용해도 속도에 그다지 민감하지 않지만

다른게임의 경우 속도가 너무 느려지므로 최적화 처리를 해야한다.

그 래서  alpha-beta pruning를 사용하여 최적화 하였다.

 

Alpha-beta pruning(알파-베타 가지치기)는 미니맥스 알고리즘(Minimax algorithm)과 함께 사용되는 게임 트리 탐색 알고리즘이다. 주로 체스, 오셀로, 틱택토 등의 턴 기반 보드 게임을 포함한 다양한 게임에서 최적의 수를 계산하기 위해 사용된다.

Alpha-beta pruning은 미니맥스 알고리즘의 성능을 향상시키기 위해 개발되었다. 미니맥스 알고리즘은 게임 트리를 완전히 탐색하여 모든 가능한 움직임을 고려하지만 이것은 많은 게임 상황에서 매우 비실용적일 수 있으며, 계산 복잡성이 지나치게 높을 수 있다.

Alpha-beta pruning은 게임 트리의 탐색을 중지하거나 일부 노드를 스킵함으로써 계산 시간을 크게 절약한다. 이것은 최소한의 계산만으로도 최선의 움직임을 찾을 수 있게 해준다. 알파와 베타라는 두 개의 값, 일반적으로 부모 노드에서 현재 노드로 이동할 때 알파(α)와 현재 노드에서 부모 노드로 이동할 때 베타(β)라는 값으로 표시된다.

<알파-베타 가지치기의 작동 방식>

1. 미니맥스 알고리즘과 마찬가지로 게임 트리를 깊이 우선 탐색.
2. 최대 플레이어 노드(현재 플레이어가 최대인 노드)에서 시작하고, 가능한 모든 자식 노드를 탐색. 이 때 알파 값을 최대로 업데이트.
3. 최소 플레이어 노드(상대 플레이어)로 이동하고, 가능한 모든 자식 노드를 탐색. 이 때 베타 값을 최소로 업데이트.
4. 알파 값과 베타 값 사이에 관계를 검사하여 가지치기를 수행. 만약 알파가 베타보다 크거나 같으면 현재 노드의 형제 노드를 스킵.
5. 이 과정을 루트 노드까지 반복하며 최선의 움직임을 결정.

# Tic Tac Toe (2/4)
# Created by netcanis on 2023/09/09.
#
# Minimax
# Alpha–beta pruning


import tkinter as tk
import random
from tkinter import messagebox

NUM_ITEMS = 9
PLAYER = 1
AI = -1

class TTT:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("TTT")
        
        self.board = [[0 for _ in range(3)] for _ in range(3)]
        self.buttons = [[None for _ in range(3)] for _ in range(3)]
        
        self.sequence = 0
        
        for row in range(3):
            for col in range(3):
                self.buttons[row][col] = tk.Button(
                    self.window,
                    text=' ',
                    font=("Helvetica", 24),
                    height=1,
                    width=1,
                    command=lambda r=row, c=col: self.make_human_move(r, c),
                )
                self.buttons[row][col].grid(row=row, column=col)
        
    def find_empty_cells(self):
        return [(row, col) for row in range(3) for col in range(3) if self.board[row][col] == 0]
    
    def check_winner(self, player):
        for row in self.board:
            if all(cell == player for cell in row):
                return True
        for col in range(3):
            if all(self.board[row][col] == player for row in range(3)):
                return True
        if all(self.board[i][i] == player for i in range(3)) or all(self.board[i][2 - i] == player for i in range(3)):
            return True
        return False
    
    def is_board_full(self):
        return all(cell != 0 for row in self.board for cell in row)
    
    def updateBoardUI(self, row, col, turn_player):
        self.buttons[row][col]["text"] = 'X' if turn_player == PLAYER else 'O'
        self.buttons[row][col]["state"] = "disabled"
        self.window.update()
        
    def make_human_move(self, row, col):
        if self.board[row][col] == 0:
            self.board[row][col] = PLAYER
            self.updateBoardUI(row, col, PLAYER)
            self.sequence += 1
            
            if self.check_winner(PLAYER):
                messagebox.showinfo("Game Over", "Player wins!")
                self.window.quit()
            
            if self.is_board_full():
                messagebox.showinfo("Game Over", "It's a draw!")
                self.window.quit()
            
            self.make_computer_move()
    
    def make_computer_move(self):
        best_move = self.find_best_move()
        if best_move is not None:
            row, col = best_move
            self.board[row][col] = AI
            self.updateBoardUI(row, col, AI)
            self.sequence += 1
            
            if self.check_winner(AI):
                messagebox.showinfo("Game Over", "AI wins!")
                self.window.quit()

            if self.is_board_full():
                messagebox.showinfo("Game Over", "It's a draw!")
                self.window.quit()
    
    def find_best_move(self):
        if self.sequence <= 1:
            return random.choice(self.find_empty_cells())
        
        alpha = -float('inf')
        beta = float('inf')
        best_move = None
        best_score = -float('inf')
        
        for row, col in self.find_empty_cells():
            self.board[row][col] = AI
            score = self.minimax(0, False, alpha, beta)
            self.board[row][col] = 0
            
            if score > best_score:
                best_score = score
                best_move = (row, col)
        
        return best_move
    
    def minimax(self, depth, is_maximizing, alpha, beta):
        if self.check_winner(AI):
            return (NUM_ITEMS + 1 - depth)
                    
        if self.check_winner(PLAYER):
            return -(NUM_ITEMS + 1 - depth)

        if self.is_board_full():
            return 0
        
        if is_maximizing:
            best_score = -float('inf')
            for row, col in self.find_empty_cells():
                self.board[row][col] = AI
                score = self.minimax(depth + 1, False, alpha, beta)
                self.board[row][col] = 0
                best_score = max(best_score, score)
                alpha = max(alpha, best_score)
                if beta <= alpha:
                    break
            return best_score
        else:
            best_score = float('inf')
            for row, col in self.find_empty_cells():
                self.board[row][col] = PLAYER
                score = self.minimax(depth + 1, True, alpha, beta)
                self.board[row][col] = 0
                best_score = min(best_score, score)
                beta = min(beta, best_score)
                if beta <= alpha:
                    break
            return best_score

    def run(self):
        self.window.mainloop()

if __name__ == "__main__":
    game = TTT()
    game.run()

 

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (1/4) - minimax

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (2/4) - alpha–beta pruning

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (3/4) - 머신러닝 훈련 데이터 생성

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (4/4) - 머신러닝을 이용한 게임 구현

 

 

반응형
블로그 이미지

SKY STORY

,
반응형

신경망 알고리즘을 이용하여 tic-tac-toe게임을 구현해 보고자 한다. 

전체 4회에 걸쳐 업로드 할 예정이며 순서는 다음과 같다.

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (1/4) - minimax

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (2/4) - alpha–beta pruning

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (3/4) - 머신러닝 훈련 데이터 생성

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (4/4) - 머신러닝을 이용한 게임 구현

 

아래 코드는 AI의 수를 계산하기 위해 minimax알고리즘을 사용하였다.

Minimax는 게임 이론 및 인공 지능 분야에서 사용되는 의사결정 알고리즘 중 하나이다. 주로 두 플레이어 간의 게임에서 최적의 움직임을 찾는 데 사용된다. Minimax 알고리즘은 상대방과 자신의 이익 또는 손해를 고려하여 다음 움직임을 결정하는 방법을 제공한다.

<Minimax 알고리즘의 작동 방식>

1. 게임 트리 생성: 현재 게임 상태에서 가능한 모든 움직임을 고려하여 게임 트리를 생성한다. 트리의 각 레벨은 게임의 한 턴을 나타내고, 각 노드는 해당 턴에서 가능한 상태를 나타낸다.

2. 평가 함수: 게임 트리의 말단 노드(게임 종료 상태)에 도달하면 각 상태를 평가한다. 이 평가는 게임에서 어떤 플레이어가 이길 확률 또는 게임 상태의 가치를 나타내는 값이다.

3. Minimax 알고리즘: 게임 트리를 위에서 아래로 (루트에서 리프로) 탐색하면서, 각 레벨에서 최대화(Maximize) 노드와 최소화(Minimize) 노드를 번갈아 가며 선택한다. 최대화 노드에서는 가능한 움직임 중에서 가장 큰 가치를 선택하고, 최소화 노드에서는 가능한 움직임 중에서 가장 작은 가치를 선택한다.

4. 최선의 움직임 결정: 루트 노드까지 도달하면 최종적으로 최선의 움직임을 결정한다. 최대화 노드에서 최대 가치를 가진 움직임이 최적의 움직임이 된다.

# Tic Tac Toe (1/4)
# Created by netcanis on 2023/09/09.
#
# Minimax


import tkinter as tk
import random
from tkinter import messagebox

NUM_ITEMS = 9
PLAYER = 1
AI = -1

class TTT:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("TTT")
        
        self.board = [[0 for _ in range(3)] for _ in range(3)]
        self.buttons = [[None for _ in range(3)] for _ in range(3)]
        
        self.sequence = 0
        
        for row in range(3):
            for col in range(3):
                self.buttons[row][col] = tk.Button(
                    self.window,
                    text=' ',
                    font=("Helvetica", 24),
                    height=1,
                    width=1,
                    command=lambda r=row, c=col: self.make_human_move(r, c),
                )
                self.buttons[row][col].grid(row=row, column=col)
        
    def find_empty_cells(self):
        return [(row, col) for row in range(3) for col in range(3) if self.board[row][col] == 0]
    
    def check_winner(self, player):
        for row in self.board:
            if all(cell == player for cell in row):
                return True
        for col in range(3):
            if all(self.board[row][col] == player for row in range(3)):
                return True
        if all(self.board[i][i] == player for i in range(3)) or all(self.board[i][2 - i] == player for i in range(3)):
            return True
        return False
    
    def is_board_full(self):
        return all(cell != 0 for row in self.board for cell in row)
    
    def updateBoardUI(self, row, col, turn_player):
        self.buttons[row][col]["text"] = 'X' if turn_player == PLAYER else 'O'
        self.buttons[row][col]["state"] = "disabled"
        self.window.update()
        
    def make_human_move(self, row, col):
        if self.board[row][col] == 0:
            self.board[row][col] = PLAYER
            self.updateBoardUI(row, col, PLAYER)
            self.sequence += 1
            
            if self.check_winner(PLAYER):
                messagebox.showinfo("Game Over", "Player wins!")
                self.window.quit()
            
            if self.is_board_full():
                messagebox.showinfo("Game Over", "It's a draw!")
                self.window.quit()
            
            self.make_computer_move()
    
    def make_computer_move(self):
        best_move = self.find_best_move()
        if best_move is not None:
            row, col = best_move
            self.board[row][col] = AI
            self.updateBoardUI(row, col, AI)
            self.sequence += 1
            
            if self.check_winner(AI):
                messagebox.showinfo("Game Over", "AI wins!")
                self.window.quit()

            if self.is_board_full():
                messagebox.showinfo("Game Over", "It's a draw!")
                self.window.quit()
    
    def find_best_move(self):
        if self.sequence <= 1:
            return random.choice(self.find_empty_cells())
        
        best_move = None
        best_score = -float('inf')
        
        for row, col in self.find_empty_cells():
            self.board[row][col] = AI
            score = self.minimax(0, False)
            self.board[row][col] = 0
            
            if score > best_score:
                best_score = score
                best_move = (row, col)
        
        return best_move
        
    def minimax(self, depth, is_maximizing):
        if self.check_winner(AI):
            return NUM_ITEMS + 1 - depth
                    
        if self.check_winner(PLAYER):
            return -(NUM_ITEMS + 1 - depth)

        if self.is_board_full():
            return 0
        
        if is_maximizing:
            best_score = -float('inf')
            for row, col in self.find_empty_cells():
                self.board[row][col] = AI
                score = self.minimax(depth + 1, False)
                self.board[row][col] = 0
                best_score = max(best_score, score)
            return best_score
        else:
            best_score = float('inf')
            for row, col in self.find_empty_cells():
                self.board[row][col] = PLAYER
                score = self.minimax(depth + 1, True)
                self.board[row][col] = 0
                best_score = min(best_score, score)
            return best_score

    def run(self):
        self.window.mainloop()

if __name__ == "__main__":
    game = TTT()
    game.run()

 

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (1/4) - minimax

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (2/4) - alpha–beta pruning

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (3/4) - 머신러닝 훈련 데이터 생성

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (4/4) - 머신러닝을 이용한 게임 구현

 

반응형

'개발 > AI,ML,ALGORITHM' 카테고리의 다른 글

Tic-Tac-Toe 게임 제작 (3/4) - 머신러닝 훈련 데이터 생성  (0) 2023.09.12
Tic-Tac-Toe 게임 제작 (2/4) - alpha–beta pruning  (0) 2023.09.12
Simple Neural Network XOR  (0) 2023.08.29
SARSA  (0) 2023.08.28
Q-learning  (0) 2023.08.28
블로그 이미지

SKY STORY

,
반응형

 

# neural network xor
#
# Created by netcanis on 2023/08/22.
#

import numpy as np
from graphviz import Digraph


'''
Input 0 -- w[0] -- Hidden 0 
          \       /  \      
           \     /   w2[0]  
           w[1] /       \    
              \/         Output
              /\        /
           w[2] \    w2[1]
            /    \    /
           /      \  /
Input 1 -- w[3] -- Hidden 1
'''


# XOR 학습 데이터
x_data = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y_data = np.array([[0], [1], [1], [0]])

# 신경망 파라미터 초기화
input_size = 2
hidden_size = 2
output_size = 1
learning_rate = 0.1

# 가중치와 편향 초기화 - 평균이 0이고 표준편자가 1인 랜덤 값으로 행렬을 생성한다. (-1.0 ~ 1.0)
w1 = np.random.randn(input_size, hidden_size)   # 2x2 행렬
b1 = np.random.randn(hidden_size)               # 1차원 배열 (첫번째 은닉층의 편향 벡터. 편향은 각 은닉층 뉴런의 활성화 값을 조정하는 역할.)
w2 = np.random.randn(hidden_size, output_size)  # 2x1 행렬
b2 = np.random.randn(output_size)               # 1차원 배열 (출력층의 편향 벡터. 출력층의 뉴런들을 편향하여 최종 예측 값을 만듬.)

# 활성화 함수 (시그모이드)
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

# 활성화 함수의 미분
def sigmoid_derivative(x):
    return x * (1 - x)

# 학습 시작
epochs = 10000
for epoch in range(epochs):
    # 순전파 (Forward Propagation)
    z1 = np.dot(x_data, w1) + b1
    a1 = sigmoid(z1)
    z2 = np.dot(a1, w2) + b2
    a2 = sigmoid(z2)

    # 오차 계산
    error = y_data - a2

    # 역전파 (Backpropagation)
    delta2 = error * sigmoid_derivative(a2)
    delta1 = np.dot(delta2, w2.T) * sigmoid_derivative(a1)

    # 가중치 업데이트
    w2 += np.dot(a1.T, delta2) * learning_rate
    w1 += np.dot(x_data.T, delta1) * learning_rate

# 예측 함수
def predict(input_data):
    z1 = np.dot(input_data, w1) + b1
    a1 = sigmoid(z1)
    z2 = np.dot(a1, w2) + b2
    a2 = sigmoid(z2)
    return int(np.round(a2)[0])




# -----------------------------------------------------------------------

# TEST

# 학습 완료 후 예측
input_example = np.array([0, 1])
prediction = predict(input_example)
print("Input:", input_example)
print("Prediction:", prediction)



# 레이어 구조도 출력 

# 그래프 생성
graph = Digraph(format='png', engine='dot')
graph.node('Input0', 'Input 0')
graph.node('Input1', 'Input 1')
graph.node('Hidden0', 'Hidden 0')
graph.node('Hidden1', 'Hidden 1')
graph.node('Output', 'Output')
graph.node('Bias1', 'Bias 1')
graph.node('Bias2', 'Bias 2')

# 그래프 속성 설정
graph.attr(rankdir='LR')  # 좌에서 우로 방향 설정

# 입력 노드와 히든 노드 연결
graph.edge('Input0', 'Hidden0', label='w1[0]')
graph.edge('Input0', 'Hidden1', label='w1[1]')
graph.edge('Input1', 'Hidden0', label='w1[2]')
graph.edge('Input1', 'Hidden1', label='w1[3]')

# 히든 노드와 출력 노드 연결
graph.edge('Hidden0', 'Output', label='w2[0]')
graph.edge('Hidden1', 'Output', label='w2[1]')

# 바이어스 노드와 히든 노드 연결
graph.edge('Bias1', 'Hidden0', label='b1[0]')
graph.edge('Bias1', 'Hidden1', label='b1[1]')
graph.edge('Bias2', 'Output', label='b2[0]')


# 그래프 시각화 및 출력
graph.view()

 

 

 

반응형

'개발 > AI,ML,ALGORITHM' 카테고리의 다른 글

Tic-Tac-Toe 게임 제작 (2/4) - alpha–beta pruning  (0) 2023.09.12
Tic-Tac-Toe 게임 제작 (1/4) - minimax  (0) 2023.09.12
SARSA  (0) 2023.08.28
Q-learning  (0) 2023.08.28
MNIST - TensorFlowLite  (0) 2023.07.19
블로그 이미지

SKY STORY

,

SARSA

개발/AI,ML,ALGORITHM 2023. 8. 28. 09:14
반응형

 

# SARSA
# 
# Created by netcanis on 2023/08/22.
#
# 5x10 크기의 그리드 월드 환경에서 SARSA 알고리즘을 실행하는 간단한 예시입니다. 
# 에이전트는 상, 하, 좌, 우로 움직이며 목표 지점에 도달하는 최적의 경로를 학습
# 매 에피소드마다 이동 경로 및 순서를 실시간으로 출력
# 에피소드 100의 배수마다 이동경로 및 순서 출력
# 경로 횟수가 적을수록 높은 보상
# 에피소드가 증가할수록 탐험보다 활용을 선택하도록 개선.
#
# SARSA (State-Action-Reward-State-Action)는 강화학습의 한 종류로, 상태와 동작의 시퀀스를 고려하여 학습하는 알고리즘입니다. 
# 이 알고리즘은 Q-learning과 비슷하지만, Q-learning이 다음 상태의 최대 Q 값을 사용하는 반면에 SARSA는 다음 상태에서 실제로 
# 선택한 동작에 대한 Q 값을 사용합니다. 이로 인해 더 안정적으로 학습하고, 더 정확한 제어를 할 수 있는 특징이 있습니다.


import numpy as np
import matplotlib.pyplot as plt


# 그리드 월드 환경 설정
# 0: 빈공간, 1: 벽, 2: 목적지, (0,0): 시작위치
grid = np.array([[0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
                 [0, 0, 0, 1, 0, 0, 1, 0, 0, 0],
                 [0, 0, 0, 0, 1, 0, 0, 1, 0, 0],
                 [0, 1, 1, 0, 1, 0, 0, 0, 1, 0],
                 [0, 0, 0, 0, 0, 0, 0, 0, 1, 2]])

# 환경 설정
NUM_STATES = np.prod(grid.shape)  # 상태 공간 크기 (5x6=30)
NUM_ACTIONS = 4  # 상, 하, 좌, 우

# hyperparameter
EXPLORATION_PROB = 0.2  # 탐험율 - exploration(탐험)과 exploitation(활용) 사이의 균형을 조절.
LEARNING_RATE = 0.1     # 학습률 - 경사 하강법 등 최적화 알고리즘에서 가중치 업데이트에 사용되는 학습률.
DISCOUNT_FACTOR = 0.99  # 할인율 - 강화 학습에서 미래 보상을 현재 보상보다 얼마나 중요하게 고려할지를 조절하는 요소.
NUM_EPISODES = 1500     # 강화 학습에서 에피소드 수

# Q 함수 초기화
Q = np.zeros((NUM_STATES, NUM_ACTIONS))

# 상태 인덱스 계산 함수
def state_index(state):
    return state[0] * grid.shape[1] + state[1]

# 그래프 초기화
plt.ion()
fig, ax = plt.subplots()
episode_rewards = []

# 시작 위치 설정
start_position = (0, 0)
print("start_position :", start_position) # (0, 0)

# 타겟 위치 설정
target_position = np.argwhere(grid == 2)[0]
print("target_position :", target_position) # (4, 9)


# 그리드 월드 환경 출력 함수
def plot_grid_world(state, path=[]):
    plt.imshow(grid, cmap="gray", interpolation="none", origin="upper")
    plt.xticks([])
    plt.yticks([])
    
    if path:
        path = np.array(path)
        plt.plot(path[:, 1], path[:, 0], marker='o', markersize=4, linestyle='-', color='green')
        
        for i, pos in enumerate(path):
            plt.text(pos[1], pos[0], str(i + 1), ha="center", va="center", color="orange", fontsize=14)
    
    plt.text(state[1], state[0], "A", ha="center", va="center", color="red", fontsize=16)
    plt.text(grid.shape[1] - 1, grid.shape[0] - 1, "B", ha="center", va="center", color="green", fontsize=16)


def choose_action(state, epsilon):
    if np.random.rand() < epsilon:
        return np.random.choice(NUM_ACTIONS)
    return np.argmax(Q[state_index(state)])


# SARSA 알고리즘
for episode in range(NUM_EPISODES):
    print(f"Episode: {episode + 1}/{NUM_EPISODES}")
    
    state = tuple(start_position)
    total_reward = 0
    path_taken = []
    
    # Decaying exploration
    # 에피소드가 증가할 수록 탐험하는 비율을 쇠퇴시킨다.(원래 탐험율의 절반으로 줄인다)
    epsilon = EXPLORATION_PROB + (EXPLORATION_PROB / 2 - EXPLORATION_PROB) * (1.0 - episode / NUM_EPISODES)
    
    # 첫 이동 선택
    action = choose_action(state, epsilon)
    
    while state != tuple(target_position):

        # max와 min 함수는 에이전트가 그리드 월드 환경 내에서 벗어나지 않도록 하기 위해 사용
        if action == 0:   # 상 : 현재 행을 1 감소시킴 (위쪽으로 이동)
            next_state = (max(state[0] - 1, 0), state[1])
        elif action == 1: # 하 : 현재 행을 1 증가시킴 (아래쪽으로 이동)
            next_state = (min(state[0] + 1, grid.shape[0] - 1), state[1])
        elif action == 2: # 좌 : 현재 열을 1 감소시킴 (왼쪽으로 이동)
            next_state = (state[0], max(state[1] - 1, 0))
        elif action == 3: # 우 : 현재 열을 1 증가시킴 (오른쪽으로 이동)
            next_state = (state[0], min(state[1] + 1, grid.shape[1] - 1))
        

        # 보상
        # reward = -1 if grid[next_state] == 1 else 1 if grid[next_state] == 2 else 0
        if grid[next_state] == 1: # 벽을 만났을 때 보상 설정
            reward = -(NUM_STATES * 100)
        else: # 경로 횟수가 증가될 수록 횟수 만큼 - 보상 (즉 경로 횟수가 적을 수록 보상이 크다)
            reward = -(len(path_taken) + 1)
        
        
        # 다음 이동 선택
        next_action = choose_action(next_state, epsilon)
        
        
        # SARSA
        a1 = Q[state_index(state), action]
        a2 = Q[state_index(next_state), next_action]
        Q[state_index(state)][action] = a1 + LEARNING_RATE * (reward + DISCOUNT_FACTOR * a2 - a1)
        
        
        total_reward += reward
        path_taken.append(state)
        state = next_state
        action = next_action
        
        
        # 에피소드 다음 배수마다 이동경로 및 순서 출력
        if (episode + 1) % 100 == 0:
            plt.figure(2)
            plt.clf() # Matplotlib의 현재 활성화된 그림 창을 지우기
            plt.title(f"Episode : {episode + 1}")
            plot_grid_world(state, path_taken)
            plt.pause(0.01)
            
    episode_rewards.append(total_reward)

    # 에피소드 10의 배수마다 이동경로 및 순서 출력
    if (episode + 1) % 10 == 0:
        plt.figure(1)
        plt.plot(episode_rewards)
        plt.xlabel("Episode")
        plt.ylabel("Total Reward")
        plt.title("Total Reward per Episode")
        plt.show()
        plt.pause(0.01)
    


#----------------------------------------------------------------------------
#
# 결과 출력 
#

# 학습 완료 후 최적 경로 및 순서 출력
state = tuple(start_position) # 시작 위치
total_reward = 0
best_trajectory = [state]  # 최적 경로 및 순서를 저장할 리스트

# 최적 경로 추적
# max와 min 함수는 에이전트가 그리드 월드 환경 내에서 벗어나지 않도록 하기 위해 사용
while state != tuple(target_position):
    action = np.argmax(Q[state_index(state)])
    if action == 0:   # 상 : 현재 행을 1 감소시킴 (위쪽으로 이동)
        state = (max(state[0] - 1, 0), state[1])
    elif action == 1: # 하 : 현재 행을 1 증가시킴 (아래쪽으로 이동)
        state = (min(state[0] + 1, grid.shape[0] - 1), state[1])
    elif action == 2: # 좌 : 현재 열을 1 감소시킴 (왼쪽으로 이동)
        state = (state[0], max(state[1] - 1, 0))
    elif action == 3: # 우 : 현재 열을 1 증가시킴 (오른쪽으로 이동)
        state = (state[0], min(state[1] + 1, grid.shape[1] - 1))
    
    best_trajectory.append(state)
    
    # 보상
    # reward = -1 if grid[next_state] != 1 else -100
    if grid[next_state] == 1: # 벽을 만났을 때 보상 설정
        reward = -(NUM_STATES * 100)
    else: # 경로 횟수가 증가될 수록 횟수 만큼 - 보상 (즉 경로 횟수가 적을 수록 보상이 크다)
        reward = -(len(path_taken) + 1)
    
    total_reward += reward    
    


# 최적 경로 및 순서 출력
plt.figure(3)  # 자동으로 크기 조정됨, 새로운 창으로 출력 
plt.clf() # Matplotlib의 현재 활성화된 그림 창을 지우기
plt.title(f"Total Reward : {total_reward}, Best Trajectory Length : {len(best_trajectory)}")
plot_grid_world(state, best_trajectory)
plt.show(block=True)  # 그래프를 출력한 후 버튼을 누를 때까지 대기        


# 결과 로그 출력 
print("Learned Q-values:")
print(Q)

 

 

반응형

'개발 > AI,ML,ALGORITHM' 카테고리의 다른 글

Tic-Tac-Toe 게임 제작 (1/4) - minimax  (0) 2023.09.12
Simple Neural Network XOR  (0) 2023.08.29
Q-learning  (0) 2023.08.28
MNIST - TensorFlowLite  (0) 2023.07.19
MNIST - Keras  (0) 2023.07.19
블로그 이미지

SKY STORY

,

Q-learning

개발/AI,ML,ALGORITHM 2023. 8. 28. 09:09
반응형
# Q-learning 알고리즘 
#
# Created by netcanis on 2023/08/22.
#
# 5x10 크기의 그리드 월드 환경에서 Q-learning 알고리즘을 실행하는 간단한 예시입니다. 
# 에이전트는 상, 하, 좌, 우로 움직이며 목표 지점에 도달하는 최적의 경로를 학습
# 매 에피소드마다 이동 경로 및 순서를 실시간으로 출력
# 에피소드 100의 배수마다 이동경로 및 순서 출력
# 경로 횟수가 적을수록 높은 보상
# 에피소드가 증가할수록 탐험보다 활용을 선택하도록 개선.


import numpy as np
import matplotlib.pyplot as plt

# 그리드 월드 환경 설정
# 0: 빈공간, 1: 벽, 2: 목적지, (0,0)): 시작위치
grid = np.array([[0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
                 [0, 0, 0, 1, 0, 0, 1, 0, 0, 0],
                 [0, 0, 0, 0, 1, 0, 0, 1, 0, 0],
                 [0, 1, 1, 0, 1, 0, 0, 0, 1, 0],
                 [0, 0, 0, 0, 0, 0, 0, 0, 1, 2]])


# Q-learning 매개변수
NUM_STATES = np.prod(grid.shape)  # 상태 공간 크기 (5x6=30)
NUM_ACTIONS = 4  # 상, 하, 좌, 우

# hyperparameter
EXPLORATION_PROB = 0.2  # 탐험율 - exploration(탐험)과 exploitation(활용) 사이의 균형을 조절.
LEARNING_RATE = 0.1     # 학습률 - 경사 하강법 등 최적화 알고리즘에서 가중치 업데이트에 사용되는 학습률.
DISCOUNT_FACTOR = 0.99  # 할인율 - 강화 학습에서 미래 보상을 현재 보상보다 얼마나 중요하게 고려할지를 조절하는 요소.
NUM_EPISODES = 1500     # 강화 학습에서 에피소드 수

# Q 함수 초기화
Q = np.zeros((NUM_STATES, NUM_ACTIONS))

# 상태 인덱스 계산 함수
def state_index(state):
    return state[0] * grid.shape[1] + state[1]

# 그래프 초기화
plt.ion()
fig, ax = plt.subplots()
episode_rewards = []

# 시작 위치 설정
start_position = (0, 0)
print("begin_position :", start_position) # (0, 0)

# 타겟 위치 설정
target_position = np.argwhere(grid == 2)[0]
print("target_position :", target_position) # (4, 9)


# 그리드 월드 환경 출력 함수
def plot_grid_world(state, path=[]):
    plt.imshow(grid, cmap="gray", interpolation="none", origin="upper")
    plt.xticks([])
    plt.yticks([])
    
    if path:
        path = np.array(path)
        plt.plot(path[:, 1], path[:, 0], marker='o', markersize=4, linestyle='-', color='green')
        
        for i, pos in enumerate(path):
            plt.text(pos[1], pos[0], str(i + 1), ha="center", va="center", color="orange", fontsize=14)
    
    plt.text(state[1], state[0], "A", ha="center", va="center", color="red", fontsize=16)
    plt.text(grid.shape[1] - 1, grid.shape[0] - 1, "B", ha="center", va="center", color="green", fontsize=16)

        
def choose_action(state, epsilon):
    if np.random.rand() < epsilon:
        return np.random.choice(NUM_ACTIONS)
    return np.argmax(Q[state_index(state)])


# Q-learning 알고리즘
for episode in range(NUM_EPISODES):
    print(f"Episode: {episode + 1}/{NUM_EPISODES}")
    
    state = tuple(start_position)
    total_reward = 0
    path_taken = []
    
    # Decaying exploration
    # 에피소드가 증가할 수록 탐험하는 비율을 쇠퇴시킨다.(원래 탐험율의 절반으로 줄인다)
    epsilon = EXPLORATION_PROB + (EXPLORATION_PROB / 2 - EXPLORATION_PROB) * (1.0 - episode / NUM_EPISODES)

    while state != tuple(target_position):
        
        action = choose_action(state, epsilon)
      
        # max와 min 함수는 에이전트가 그리드 월드 환경 내에서 벗어나지 않도록 하기 위해 사용
        if action == 0:   # 상 : 현재 행을 1 감소시킴 (위쪽으로 이동)
            next_state = (max(state[0] - 1, 0), state[1])
        elif action == 1: # 하 : 현재 행을 1 증가시킴 (아래쪽으로 이동)
            next_state = (min(state[0] + 1, grid.shape[0] - 1), state[1])
        elif action == 2: # 좌 : 현재 열을 1 감소시킴 (왼쪽으로 이동)
            next_state = (state[0], max(state[1] - 1, 0))
        elif action == 3: # 우 : 현재 열을 1 증가시킴 (오른쪽으로 이동)
            next_state = (state[0], min(state[1] + 1, grid.shape[1] - 1))
        
        
        # 보상
        # reward = -1 if grid[next_state] != 1 else -100
        if grid[next_state] == 1: # 벽을 만났을 때 보상 설정
            reward = -(NUM_STATES * 100)
        else: # 경로 횟수가 증가될 수록 횟수 만큼 - 보상 (즉 경로 횟수가 적을 수록 보상이 크다)
            reward = -(len(path_taken) + 1)
        
        
        # Q                                                                             ccc                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              -learning        
        a1 = Q[state_index(state), action]
        a2 = np.max(Q[state_index(next_state)])
        q1 = (1 - LEARNING_RATE) * a1
        q2 = LEARNING_RATE * (reward + DISCOUNT_FACTOR * a2)
        Q[state_index(state), action] = q1 + q2
        
        total_reward += reward
        path_taken.append(state)
        state = next_state
        
        
        # 에피소드 다음 배수마다 이동경로 및 순서 출력
        if (episode + 1) % 100 == 0:
            plt.figure(2)
            plt.clf() # Matplotlib의 현재 활성화된 그림 창을 지우기
            plt.title(f"Episode : {episode + 1}")
            plot_grid_world(state, path_taken)
            plt.pause(0.01)

    episode_rewards.append(total_reward)

    # 에피소드 다음 배수마다 이동경로 및 순서 출력
    if (episode + 1) % 10 == 0:
        plt.figure(1)
        plt.plot(episode_rewards)
        plt.xlabel("Episode")
        plt.ylabel("Total Reward")
        plt.title("Total Reward per Episode")
        plt.show()
        plt.pause(0.01)



#----------------------------------------------------------------------------
#
# 결과 출력 
#

# 학습 완료 후 최적 경로 및 순서 출력
state = tuple(start_position) # 시작 위치
total_reward = 0
best_trajectory = [state]  # 최적 경로 및 순서를 저장할 리스트

# 최적 경로 추적
# max와 min 함수는 에이전트가 그리드 월드 환경 내에서 벗어나지 않도록 하기 위해 사용
while state != tuple(target_position):
    action = np.argmax(Q[state_index(state)])
    
    if action == 0:   # 상 : 현재 행을 1 감소시킴 (위쪽으로 이동)
        state = (max(state[0] - 1, 0), state[1])
    elif action == 1: # 하 : 현재 행을 1 증가시킴 (아래쪽으로 이동)
        state = (min(state[0] + 1, grid.shape[0] - 1), state[1])
    elif action == 2: # 좌 : 현재 열을 1 감소시킴 (왼쪽으로 이동)
        state = (state[0], max(state[1] - 1, 0))
    elif action == 3: # 우 : 현재 열을 1 증가시킴 (오른쪽으로 이동)
        state = (state[0], min(state[1] + 1, grid.shape[1] - 1))
    
    best_trajectory.append(state)
    
    # 보상
    # reward = -1 if grid[next_state] != 1 else -100
    if grid[next_state] == 1: # 벽을 만났을 때 보상 설정
        reward = -(NUM_STATES * 100)
    else: # 경로 횟수가 증가될 수록 횟수 만큼 - 보상 (즉 경로 횟수가 적을 수록 보상이 크다)
        reward = -(len(path_taken) + 1)
    
    total_reward += reward    
    

# 최적 경로 및 순서 출력
plt.figure(3)  # 자동으로 크기 조정됨, 새로운 창으로 출력 
plt.clf() # Matplotlib의 현재 활성화된 그림 창을 지우기
plt.title(f"Total Reward : {total_reward}, Best Trajectory Length : {len(best_trajectory)}")
plot_grid_world(state, best_trajectory)
plt.show(block=True)  # 그래프를 출력한 후 버튼을 누를 때까지 대기        


# 결과 로그 출력 
print("Learned Q-values:")
print(Q)

 

 

 

반응형

'개발 > AI,ML,ALGORITHM' 카테고리의 다른 글

Simple Neural Network XOR  (0) 2023.08.29
SARSA  (0) 2023.08.28
MNIST - TensorFlowLite  (0) 2023.07.19
MNIST - Keras  (0) 2023.07.19
MNIST - RandomForestClassifier  (0) 2023.07.19
블로그 이미지

SKY STORY

,
반응형

TensorFlow Lite는 구글이 개발한 TensorFlow의 경량 버전으로, 모바일 기기 및 임베디드 시스템에서 딥러닝 모델을 실행하기 위해 최적화된 라이브러리입니다. TensorFlow Lite는 딥러닝 모델의 크기와 성능을 최적화하여 모바일 기기에서도 효율적으로 동작할 수 있도록 합니다.

< TensorFlow Lite 특징 >

경량화: TensorFlow Lite는 모바일 기기와 임베디드 시스템의 제한된 자원을 고려하여 딥러닝 모델의 크기와 연산량을 최적화합니다. 이를 통해 모바일 환경에서도 효율적으로 모델을 실행할 수 있습니다.
하드웨어 가속: TensorFlow Lite는 하드웨어 가속을 지원하여 모바일 기기의 GPU, DSP 등을 활용하여 딥러닝 연산을 가속화할 수 있습니다. 이로 인해 모델의 추론 속도가 향상됩니다.
모바일 지원: TensorFlow Lite는 Android 및 iOS와 같은 주요 모바일 플랫폼에서 사용할 수 있도록 지원합니다. 또한, 임베디드 보드와 같은 다양한 장치에서도 실행 가능합니다.
모델 컨버터: TensorFlow Lite는 TensorFlow 모델을 TensorFlow Lite 모델로 변환하는 컨버터를 제공합니다. 이를 통해 기존에 훈련된 TensorFlow 모델을 모바일에서 실행 가능한 형식으로 변환할 수 있습니다.
지원되는 모델 형식: TensorFlow Lite는 다양한 모델 형식을 지원합니다. TensorFlow 모델을 변환하여 사용할 수 있으며, TensorFlow 모델 최적화 도구를 사용하여 모델 크기를 최소화할 수도 있습니다.
On-device 추론: TensorFlow Lite는 모바일 기기에서 모델을 로드하고 추론을 직접 수행할 수 있습니다. 이를 통해 모바일 애플리케이션에서 실시간으로 딥러닝 모델을 활용할 수 있습니다.
Custom 모델 지원: TensorFlow Lite는 사용자 정의 연산자를 지원하므로 사용자가 원하는 연산자를 직접 구현하고 모델에 통합할 수 있습니다.
TensorFlow Lite는 모바일 애플리케이션에서 딥러닝을 활용하고자 하는 개발자들에게 매우 유용한 도구이며, 경량화된 딥러닝 모델을 사용하여 모바일 기기에서 실시간 추론을 구현하는 데에 적합합니다. 또한, TensorFlow Lite는 TensorFlow와 통합되어 있어 TensorFlow에서 훈련한 모델을 간편하게 모바일 환경에서 실행할 수 있도록 지원합니다.

 

//
//  MNIST - TENSORFLOWLITE
//
//  Created by netcanis on 2023/07/20.
//

import tensorflow as tf
import dataset_loader
import model_tester


# Load MNIST data
training_images, training_labels, test_images, test_labels = dataset_loader.load_dataset("data/MNIST")

# Reshape the data
# 배열의 차원을 변경하여 크기를 자동으로 변경한다. (-1은 해당 차원의 크기를 자동으로 조정하라는 뜻)
training_images = training_images.reshape(-1, 28, 28, 1)
test_images = test_images.reshape(-1, 28, 28, 1)

# Print the image shapes
# reshape - Training Images shape: (60000, 28, 28, 1)
# reshape - Test Images shape: (10000, 28, 28, 1)
print("reshape - Training Images shape:", training_images.shape)
print("reshape - Test Images shape:", test_images.shape)

# Normalize the pixel values
training_images = training_images / 255.0
test_images = test_images / 255.0

# Assign images and labels to x_train, y_train, x_test, y_test
x_train, y_train = training_images, training_labels
x_test, y_test = test_images, test_labels




#
# TensorFlow Lite 모델 저장 (tflite)
#

# TensorFlow Keras model
keras_model = tf.keras.Sequential([
    tf.keras.layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(64, activation='relu'),
    tf.keras.layers.Dense(10, activation='softmax') # 0~9 총 10개 클래스
])

# Train the model
keras_model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
keras_model.fit(x_train, y_train, epochs=5, validation_data=(x_test, y_test))

# Convert the model to TensorFlow Lite format
converter = tf.lite.TFLiteConverter.from_keras_model(keras_model)
tflite_model = converter.convert()

# Save the TensorFlow Lite model
with open('tflite_mnist_model.tflite', 'wb') as file:
    file.write(tflite_model)

print("Complete Save the TensorFlow Lite model.")




#
# TEST
#

model_file = "tflite_mnist_model.tflite"
model_tester.test_model("data/MNIST", model_file)

# Error rate: 1.44%

 

2023.07.19 - [AI] - MNIST 데이터셋 다운로드

2023.07.19 - [AI] - MNIST 데이터셋을 이미지 파일로 복원

2023.07.19 - [AI] - MNIST 데이터셋 로더

2023.07.19 - [AI] - MNIST 모델 테스터

2023.07.19 - [AI] - MINST - SVC(Support Vector Classifier)

2023.07.19 - [AI] - MNIST - RandomForestClassifier

2023.07.19 - [AI] - MNIST - Keras

2023.07.19 - [AI] - MNIST - TensorFlowLite

 

 

 

반응형

'개발 > AI,ML,ALGORITHM' 카테고리의 다른 글

SARSA  (0) 2023.08.28
Q-learning  (0) 2023.08.28
MNIST - Keras  (0) 2023.07.19
MNIST - RandomForestClassifier  (0) 2023.07.19
MINST - SVC(Support Vector Classifier)  (0) 2023.07.19
블로그 이미지

SKY STORY

,