강화학습 기초 4 - 큐러닝

김민수·2025년 1월 5일
0

Q-Learning은 강화학습의 대표적인 알고리즘 중 하나로, 환경과의 상호작용을 통해 최적의 정책을 학습하는 방법입니다.

[이미지 출처] https://www.researchgate.net/figure/Q-Learning-vs-Deep-Q-Learning_fig1_351884746

핵심 개념

  • Q-함수: 상태-행동 가치 함수라고도 불리며 상태(s)에서 행동(a)을 취했을 때의 예상 가치를 리턴하는 함수. 벨만 방정식을 기반으로 Q값을 갱신함

  • 벨만 방정식: Q(s,a) = r + γ * max(Q(s',a'))

    • Q(sts_t,ata_t) : 현재 상태 sts_t에서 행동ata_t를 했을 때 기대되는 누적 보상
    • r: 즉각적인 보상
    • γ: 할인 계수
    • s': 다음 상태
    • a': 다음 상태에서 가능한 모든 행동
  • Q-함수 업데이트 식:

    • Q(st,at)(1α)Q(st,at)+α(r+γmaxaQ(st+1,a)Q(st,at))Q(s_t, a_t) \leftarrow (1-\alpha)Q(s_t, a_t) + \alpha \big(r + \gamma \max_{a'} Q(s_{t+1}, a') - Q(s_t, a_t)\big)
    • α\alpha: 학습률 (0~1 사이의 값으로, 새로운 정보의 반영 정도를 조절)
    • 업데이트는 현재 Q(st,at)Q(s_t, a_t) 값을 보상과 다음 상태의 최댓값을 반영하여 조금씩 개선하는 방식으로 진행됩니다.
  • ε-greedy 정책: 탐험(exploration)과 활용(exploitation)의 균형을 위한 정책

    • Exploration (탐험): 아직 시도해보지 않은 행동을 선택하여 더 나은 보상을 찾는 과정.새로운 정보를 얻을 수 있지만, 단기적으로는 보상이 낮을 수 있음.

    • Exploitation (활용): 이미 알고 있는 정보에서 가장 높은 보상을 줄 것이라고 예상되는 행동을 선택. 현재의 지식으로 최대 보상을 얻으려는 접근. ε-Greedy 정책은 이 두 과정을 아래와 같은 방식으로 혼합합니다:

    • 확률 ε로 랜덤하게 행동을 선택(탐험).확률 (1−𝜀)로 현재 가장 높은 보상을 줄 것으로 예상되는 행동 선택(활용)

알고리즘 단계

  1. Q-테이블 초기화
  2. 현재 상태 관찰
  3. ε-greedy 정책에 따라 행동 선택
  4. 행동 수행 및 보상 획득
  5. Q-함수 업데이트
  6. 새로운 상태로 이동
  7. 2-6 단계 반복

장단점

장점:

  • 모델 없이 학습 가능 (모델 프리 접근법)
  • 학습 종료 시 수렴성 보장

단점:

  • 큰 상태 공간에서는 비효율적
  • 연속적인 상태나 행동 처리에 제한
      1. 메모리 요구량 증가 : 큰 상태 공간에서는 Q-테이블의 크기가 기하급수적으로 증가합니다. 각 상태-행동 쌍에 대한 Q-값을 저장해야 하므로, 상태의 수가 증가할수록 필요한 메모리가 급격히 늘어납니다.
      1. 학습 시간 증가 : 상태 공간이 커질수록 모든 상태-행동 쌍을 충분히 탐색하고 Q-값을 업데이트하는 데 필요한 시간이 크게 증가합니다. 이는 학습 속도를 현저히 저하시킵니다.
      1. 탐색-활용 딜레마 : 큰 상태 공간에서는 모든 상태를 충분히 탐색하기 어려워집니다. 이로 인해 최적 정책을 찾는 데 필요한 탐색과 학습된 정책을 활용하는 것 사이의 균형을 맞추기가 더욱 어려워집니다.

코드 설명

제공된 코드는 Q-Learning을 구현한 예제입니다. Env 클래스는 그리드 월드 환경을, QLearningAgent 클래스는 Q-Learning 에이전트를 구현합니다.

주요 함수 설명

  1. Env 클래스:

    • __init__: 환경 초기화
    • step: 행동 수행 및 다음 상태, 보상 반환
    • reset: 환경 초기화
  2. QLearningAgent 클래스:

    • learn: Q-함수 업데이트
    • get_action: ε-greedy 정책에 따른 행동 선택

코드 실행 과정

  1. 환경과 에이전트 초기화
  2. 에피소드 반복:
    • 상태 초기화
    • 행동 선택 및 수행
    • Q-함수 업데이트
    • 새로운 상태로 이동
    • Q-값 출력
  3. 1000 에피소드 동안 반복

이 코드를 통해 에이전트는 그리드 월드에서 장애물을 피해 목표에 도달하는 최적 경로를 학습합니다.

# 필요한 라이브러리 임포트
import time  # 딜레이를 추가하거나 시간 측정을 위해 사용
import numpy as np  # 배열 및 수학적 계산에 사용
import tkinter as tk  # GUI 환경을 구성하기 위한 라이브러리
from PIL import ImageTk, Image  # 이미지를 tkinter에서 사용하기 위해 변환

# 랜덤 시드 설정 (재현 가능성 보장)
np.random.seed(1)

# tkinter에서 사용할 이미지 객체
PhotoImage = ImageTk.PhotoImage

# 그리드월드 크기 설정
UNIT = 100  # 각 셀의 크기 (픽셀)
HEIGHT = 5  # 그리드월드의 세로 크기 (셀 단위)
WIDTH = 5  # 그리드월드의 가로 크기 (셀 단위)


class Env(tk.Tk):
    def __init__(self):
        # tkinter의 Tk 클래스를 초기화
        super(Env, self).__init__()
        # 환경에서 사용할 행동들 (상, 하, 좌, 우)
        self.action_space = ['u', 'd', 'l', 'r']
        self.n_actions = len(self.action_space)  # 행동의 개수
        self.title('Q Learning')  # 윈도우 제목
        self.geometry('{0}x{1}'.format(HEIGHT * UNIT, HEIGHT * UNIT))  # 윈도우 크기
        self.shapes = self.load_images()  # 사용할 이미지 로드
        self.canvas = self._build_canvas()  # 캔버스 초기화
        self.texts = []  # 그리드 안에 텍스트 표시를 위한 리스트

    def _build_canvas(self):
        # 캔버스 생성 (그리드 및 이미지 추가)
        canvas = tk.Canvas(self, bg='white',
                           height=HEIGHT * UNIT,
                           width=WIDTH * UNIT)
        # 세로선 그리기
        for c in range(0, WIDTH * UNIT, UNIT):
            x0, y0, x1, y1 = c, 0, c, HEIGHT * UNIT
            canvas.create_line(x0, y0, x1, y1)
        # 가로선 그리기
        for r in range(0, HEIGHT * UNIT, UNIT):
            x0, y0, x1, y1 = 0, r, HEIGHT * UNIT, r
            canvas.create_line(x0, y0, x1, y1)

        # 캔버스에 이미지 추가
        self.rectangle = canvas.create_image(50, 50, image=self.shapes[0])  # 빨간 네모 (에이전트)
        self.triangle1 = canvas.create_image(250, 150, image=self.shapes[1])  # 장애물1
        self.triangle2 = canvas.create_image(150, 250, image=self.shapes[1])  # 장애물2
        self.circle = canvas.create_image(250, 250, image=self.shapes[2])  # 목표 지점

        canvas.pack()  # 캔버스를 tkinter 윈도우에 추가
        return canvas

    def load_images(self):
        # 이미지를 로드하고 크기를 조정하여 반환
        rectangle = PhotoImage(
            Image.open("../img/rectangle.png").resize((65, 65)))
        triangle = PhotoImage(
            Image.open("../img/triangle.png").resize((65, 65)))
        circle = PhotoImage(
            Image.open("../img/circle.png").resize((65, 65)))

        return rectangle, triangle, circle  # 로드된 이미지 반환

    def text_value(self, row, col, contents, action, font='Helvetica', size=10,
                   style='normal', anchor="nw"):
        # 그리드 안에 Q 값 표시
        if action == 0:  # 상
            origin_x, origin_y = 7, 42
        elif action == 1:  # 하
            origin_x, origin_y = 85, 42
        elif action == 2:  # 좌
            origin_x, origin_y = 42, 5
        else:  # 우
            origin_x, origin_y = 42, 77

        # 텍스트의 좌표 계산
        x, y = origin_y + (UNIT * col), origin_x + (UNIT * row)
        font = (font, str(size), style)
        text = self.canvas.create_text(x, y, fill="black", text=contents,
                                       font=font, anchor=anchor)
        return self.texts.append(text)

    def print_value_all(self, q_table):
        # 현재 Q 테이블 값을 캔버스에 표시
        for i in self.texts:
            self.canvas.delete(i)
        self.texts.clear()  # 이전 텍스트 지우기
        for i in range(HEIGHT):
            for j in range(WIDTH):
                for action in range(0, 4):
                    state = [i, j]
                    if str(state) in q_table.keys():
                        temp = q_table[str(state)][action]
                        self.text_value(j, i, round(temp, 2), action)

    def coords_to_state(self, coords):
        # 캔버스 좌표를 그리드월드의 상태로 변환
        x = int((coords[0] - 50) / 100)
        y = int((coords[1] - 50) / 100)
        return [x, y]

    def state_to_coords(self, state):
        # 그리드월드 상태를 캔버스 좌표로 변환
        x = int(state[0] * 100 + 50)
        y = int(state[1] * 100 + 50)
        return [x, y]

    def reset(self):
        # 환경을 초기 상태로 리셋
        self.update()
        time.sleep(0.5)  # 딜레이 추가
        x, y = self.canvas.coords(self.rectangle)  # 에이전트의 현재 좌표
        self.canvas.move(self.rectangle, UNIT / 2 - x, UNIT / 2 - y)  # 초기 위치로 이동
        self.render()  # 환경 시각화
        return self.coords_to_state(self.canvas.coords(self.rectangle))  # 초기 상태 반환

    def step(self, action):
        # 주어진 행동에 따라 환경의 상태를 변화시키고 보상을 반환
        state = self.canvas.coords(self.rectangle)  # 현재 에이전트 좌표
        base_action = np.array([0, 0])  # 이동 방향 초기화
        self.render()  # 환경 시각화

        # 행동에 따른 이동 방향 결정
        if action == 0:  # 상
            if state[1] > UNIT:
                base_action[1] -= UNIT
        elif action == 1:  # 하
            if state[1] < (HEIGHT - 1) * UNIT:
                base_action[1] += UNIT
        elif action == 2:  # 좌
            if state[0] > UNIT:
                base_action[0] -= UNIT
        elif action == 3:  # 우
            if state[0] < (WIDTH - 1) * UNIT:
                base_action[0] += UNIT

        # 에이전트 이동
        self.canvas.move(self.rectangle, base_action[0], base_action[1])
        self.canvas.tag_raise(self.rectangle)  # 에이전트를 맨 위로 배치
        next_state = self.canvas.coords(self.rectangle)  # 다음 상태

        # 보상 함수
        if next_state == self.canvas.coords(self.circle):  # 목표 도달
            reward = 100
            done = True
        elif next_state in [self.canvas.coords(self.triangle1),
                            self.canvas.coords(self.triangle2)]:  # 장애물 도달
            reward = -100
            done = True
        else:  # 이동만 한 경우
            reward = 0
            done = False

        next_state = self.coords_to_state(next_state)  # 좌표를 상태로 변환
        return next_state, reward, done  # 다음 상태, 보상, 종료 여부 반환

    def render(self):
        # 환경 시각화 및 딜레이 추가
        time.sleep(0.03)
        self.update()

실행파일

import numpy as np
import random
from environment import Env  # 그리드 월드 환경을 정의하는 사용자 정의 클래스
from collections import defaultdict  # 기본값이 있는 딕셔너리 생성에 사용

class QLearningAgent:
    def __init__(self, actions):
        # Q-Learning 에이전트를 초기화
        # 행동(actions): [0, 1, 2, 3] 순서대로 상, 하, 좌, 우를 의미
        self.actions = actions  # 에이전트가 선택할 수 있는 행동 리스트
        self.learning_rate = 0.01  # 학습률 α: 새 정보 반영 정도
        self.discount_factor = 0.9  # 할인 계수 γ: 미래 보상의 중요도
        self.epsilon = 0.9  # 탐험 확률 ε: 무작위 행동 선택 비율
        # Q 테이블 초기화: 상태별로 [0.0, 0.0, 0.0, 0.0] 초기값을 가지는 딕셔너리
        self.q_table = defaultdict(lambda: [0.0, 0.0, 0.0, 0.0])

    # <s, a, r, s'> 샘플로부터 Q-함수를 업데이트
    def learn(self, state, action, reward, next_state):
        # 현재 상태(state)와 행동(action)에 대한 Q 값
        q_1 = self.q_table[state][action]
        # 벨만 최적 방정식 기반으로 업데이트 대상 Q 값 계산
        # reward + γ * max(Q(s', a')): 현재 보상과 다음 상태에서의 최대 Q 값
        q_2 = reward + self.discount_factor * max(self.q_table[next_state])
        # Q 값 업데이트: 기존 Q 값에 학습률을 곱한 TD 오차를 더함
        self.q_table[state][action] += self.learning_rate * (q_2 - q_1)

    # Q-테이블 기반의 ε-탐욕 정책으로 행동 선택
    def get_action(self, state):
        if np.random.rand() < self.epsilon:
            # 탐험(Exploration): 무작위 행동 선택
            action = np.random.choice(self.actions)
        else:
            # 활용(Exploitation): Q-테이블에서 가장 높은 값을 가진 행동 선택
            state_action = self.q_table[state]
            action = self.arg_max(state_action)
        return action

    @staticmethod
    def arg_max(state_action):
        # Q 값이 최대인 행동을 반환
        max_index_list = []  # 최대 Q 값을 가진 행동들의 인덱스 리스트
        max_value = state_action[0]  # 첫 번째 값을 초기 최대값으로 설정
        for index, value in enumerate(state_action):
            if value > max_value:
                # 새로운 최대값 발견 시 리스트 초기화 후 추가
                max_index_list.clear()
                max_value = value
                max_index_list.append(index)
            elif value == max_value:
                # 최대값과 같은 값을 가진 행동 추가
                max_index_list.append(index)
        return random.choice(max_index_list)  # 최대값 행동 중 무작위로 선택

if __name__ == "__main__":
    env = Env()  # 사용자 정의 환경 초기화
    agent = QLearningAgent(actions=list(range(env.n_actions)))  # 에이전트 초기화

    for episode in range(1000):  # 1000개의 에피소드 동안 학습 반복
        state = env.reset()  # 환경 초기화 및 초기 상태 반환

        while True:  # 한 에피소드 동안 반복
            env.render()  # 환경 시각화 (현재 상태 출력)

            # 현재 상태(state)에 따른 행동(action) 선택
            action = agent.get_action(str(state))
            # 행동 수행 후 다음 상태(next_state), 보상(reward), 종료 여부(done) 반환
            next_state, reward, done = env.step(action)

            # Q-함수 업데이트: <s, a, r, s'> 샘플로 학습
            agent.learn(str(state), action, reward, str(next_state))
            state = next_state  # 상태 업데이트

            # 모든 상태-행동에 대한 Q 값을 화면에 표시
            env.print_value_all(agent.q_table)

            if done:  # 에피소드 종료 조건
                break
profile
인공지능을 공부하고 가르치는 김민수 강사입니다. 공부한 내용 및 수업 자료가 업로드 됩니다.

0개의 댓글