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-함수 업데이트 식:
ε-greedy 정책: 탐험(exploration)과 활용(exploitation)의 균형을 위한 정책
Exploration (탐험): 아직 시도해보지 않은 행동을 선택하여 더 나은 보상을 찾는 과정.새로운 정보를 얻을 수 있지만, 단기적으로는 보상이 낮을 수 있음.
Exploitation (활용): 이미 알고 있는 정보에서 가장 높은 보상을 줄 것이라고 예상되는 행동을 선택. 현재의 지식으로 최대 보상을 얻으려는 접근. ε-Greedy 정책은 이 두 과정을 아래와 같은 방식으로 혼합합니다:
확률 ε로 랜덤하게 행동을 선택(탐험).확률 (1−𝜀)로 현재 가장 높은 보상을 줄 것으로 예상되는 행동 선택(활용)
제공된 코드는 Q-Learning을 구현한 예제입니다. Env
클래스는 그리드 월드 환경을, QLearningAgent
클래스는 Q-Learning 에이전트를 구현합니다.
Env
클래스:
__init__
: 환경 초기화step
: 행동 수행 및 다음 상태, 보상 반환reset
: 환경 초기화QLearningAgent
클래스:
learn
: Q-함수 업데이트get_action
: ε-greedy 정책에 따른 행동 선택이 코드를 통해 에이전트는 그리드 월드에서 장애물을 피해 목표에 도달하는 최적 경로를 학습합니다.
# 필요한 라이브러리 임포트
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