https://codebastardarch.pages.dev/ 의 작업을 보고 인상받아 직접 코드로 한번 구현해 봤다. 아쉬운점은 이게 제대로 짜여진 코드가 맞는지 나도 알 수 없는 부분들이 있어서 일단은 그냥 써본다
1 Q-learning recap
옵시디언을 쓰면 내용들을 링크하는게 가능하지만 그래도 정리차원에서 어느정도는 재반복으로 관련내용을 쓰기로 했다. Q-learning은 off-policy, TD based, Value based method 이며, 계산 과정에서 내부적으로 Q-table을 사용한다. Q-table에는 각 state,action pair의 value(action value)가 저장된다. 이를 이용해 Q-function을 학습시키면 된다.
가령 원래 렉쳐의 경우를 가져와 다시 설명하면 이렇다. 위 그림에서 쥐가 있을 수 있는 위치(타일)는 6개다. 그렇다는 것은 state는 6개로 정의 할 수 있다. 각 위치에서 취할 수 있는 행동은 위,아래,좌,우 4가지가 된다. 이 상태를 그림으로 표현해보면 아래와 같아진다.
각 위치에서 움직일 수 있는 위치가 4가지기 때문에 위처럼 4개의 타일로 쪼개서 그림으로 표현한다. 그리고 빨간색과 초록색은 terminate state를 뜻한다. 게임으로 치면 져서 게임을 종료하던, 엔딩을 봐서 종료하던 그런 상황을 의미한다. 위 그림을 테이블로 만들면 Q-table이 된다. 다행히도 2차원으로 표현 가능하다. 위 state를 언롤하듯 x축으로 만들면 아래 테이블이 된다.
코딩 스타일에 따라 위 테이블에 접근하기 위한 row를 tuple 형태로 하거나(왜냐면 원래는 x,y위치로 볼 수도 있으니까) 0~#of state-1로 하기도 한다. 전자나 후자나 나름의 고충은 존재한다. 내가 본 예제들은 후자를 주로 따르니 후자를 기준으로 했다.
2 Pseudo code
여기서는 두개의 Pseudo code를 언급할 필요가 있다. 하나는 수학적으로 정의된 Q-learning이다. 이게 근간이 되어야 뭘 할 수 있다. 그리고 이 코드르 직접 쓰기위해 내가 어떤 흐름으로 했는지 보여주는 Pseudocode 두가지를 소개한다.
2.1 이론 수학
이론적 설명은 8 A Q-Learning example에 해놨으니 여기에는 그림만 놓는다.
2.2 내 코드
# 넘파이, 랜덤, tqdm 라이브러리를 로드함
# 글로벌 변수 설정
## state의 수
## action의 수
## Dead state
## winning state
# reward table을 정의 함
# q-table을 정의 함
# randomAction 기능을 정의함
## 액션은 랜덤으로 하나 선택하게 됨
## 그러나 현재 상태를 기준으로 액션을 취했을때, 테이블 바깥으로 벗어나면 다시 새로운 랜덤을 정의해야함
## 랜덤 액션을 리턴함
# greedyAction 정의
## Q-table에서 해당 state중 가장 높은 action value의 action return
# epsilongreedy
## epsilon factor를 정의함
## Epsilon factor와 eps를 비교해 action 선택
# step
## 해당 state에서 action을 되는지 안되는지를 알려줌. 가령 모서리 부분에서는 바깥으로 나가면 안되는 경우들을 방지해주는 여할
## 0,1,2,3 -> up,right,down,left(시계방향)으로 정의함
## action이 0이고 현재 state에서 위로 올라갈때 index가 0보다 크거나 같으면 -> update state
## action이 1이고 오른쪽이 table의 바깥이 아니라면 -> update
## action이 2이고 업데이트 위치가 max state보다 작으면 -> update
## action이 3이고 업데이트 위치가 왼쪽 바깥이 아니면 -> update
## 모두 해당 안되면 제자리에
# 학습
## 최대 에피소드
## 디케이 레이트(입실론이 줄어드는 비율)
## 러닝레이트
## 감마(할인율)
## 입실론
## 최대 스탭(한 에피소드에서 최대로 탐색 가능한 스탭
## 종료 위치
## 각 에피소드에서
## - 임의 위치에서 시작
## - 해당 위치부터 최대 스탭까지
## -- 만약 현재 위치가 종료위치면 종료
## -- 액션은 입실론 그리디
## -- 새 스테이트는 step function을 토앻
## -- 리워드는 리워드 테이블에 새 스테이트를 넣어서
## -- TD update
## -- 현재 스테이트 = 새 스테이트
## - 입실론 디케잉
## Q-table 리턴
허깅페이스에서 제공하는 예제의 Q-table 결과물과 비교했을 때 위 방식이 맞는거 같다. 아주 간단한 수학이지만 그래도 좀 꼼꼼함이 필요한 것은 step 부분이었는데 관련한 그림으로 설명해보면 아래와 같다.
가령 내가 3*5 = 15 만큼의 state를 정의했다고 가정해보자. 그러면 이를 위에서 언급했듯 unroll하면 위 그림처럼 0,1,...,14의 형태가 된다. 이 상태에서 어떤 경우에 table 바깥으로 벗어나게 되는지 정의하는 것이 핵심이다. 위 그림 중 4위치를 중심으로 문제를 설명해보면 다음과 같다. unroll된 list를 보고 판단했을때, 4의 위치에서 오른쪽으로 이동 가능해 보인다. 그러나 2차원 테이블로 보면 그게 불가능 하다는 것을 볼 수 있다
따라서 unroll된 테이블을 2차원으로 해석해서 오른쪽으로 이동가능한지 불가능한지 판별하는 로직이 필요하다. 이를 위해서는 table의 폭을 이용해 각 값의 나머지(remainder)를 구함으로서 해결할 수 있다. 가령 위 예시에서 테이블의 폭은 5가 되며 각 나머지를 구하면 다음과 같다.
이 방법을 통해 어느 한 state에서 왼쪽 오른쪽으로 이동 가능한지 판별 할 수 있다. 지금 생각해보니 이 방식을 이용해 row를 판별하는 방법도 가능하지만 굳이 이렇게 까지 안해도 up,down은 판별이 가능하다
3 최종 코드
import numpy as np
import random
from tqdm import tqdm
STATE_ROWS = 4
STATE_COLS = 4
ACTIONS = 4
DEAD_STATE = 7
WIN_STATE = 4
REWARD_TABLE = np.zeros(STATE_COLS*STATE_ROWS)
REWARD_TABLE[3] = 1
REWARD_TABLE[WIN_STATE] = 10
REWARD_TABLE[DEAD_STATE] = -10
REWARD_TABLE[9] = 1
Q_TABLE = np.zeros((STATE_COLS*STATE_ROWS,ACTIONS))
def randomAction(state):
action = random.randint(0,ACTIONS-1)
if step(state,action) == state:
action = randomAction(state)
return action
def greedyAction(state):
action = np.argmax(Q_TABLE[state][:])
return action
def epsGreedy(state:int,eps:int)->int:
epsFactor = random.uniform(0,1)
if epsFactor <= eps:
action = randomAction(state)
else:
action = greedyAction(state)
return action
# actino 0,1,2,3 -> up,right,down,left
def step(state:int,action:int)-> int:
if action == 0:
if state-STATE_COLS >= 0:
return state - STATE_COLS
if action == 1:
if state%STATE_COLS != STATE_COLS-1:
return state + 1
if action == 2:
if state + STATE_COLS < STATE_COLS*STATE_ROWS:
return state + STATE_COLS
if action == 3:
if state%STATE_COLS != 0:
return state - 1
return state
print(step(3,3))
def train():
max_episodes = 10000
decay_rate = 0.999
learning_rate = 0.7
gamma = 0.90
eps = 1.0
max_step = 15
terminal_state = {WIN_STATE,DEAD_STATE}
for i in tqdm(range(max_episodes)):
state = random.randint(0,STATE_ROWS*STATE_COLS-1)
for j in range(max_step):
if state in terminal_state:
break
action = epsGreedy(state,eps)
new_state = step(state,action)
reward = REWARD_TABLE[new_state]
Q_TABLE[state][action] = Q_TABLE[state][action] +\
learning_rate*(reward + gamma *\
np.max(Q_TABLE[new_state])
- Q_TABLE[state][action])
state = new_state
eps *= decay_rate
return Q_TABLE
3.1 return 결과물
reward table
[[ 0. 0. 0. 1.]
[ 10. 0. 0. -10.]
[ 0. 1. 0. 0.]
[ 0. 0. 0. 0.]]
Q-table
[[ 0. 8.1 10. 0. ]
[ 0. 7.29 9. 9. ]
[ 0. 7.561 8.1 8.1 ]
[ 0. 0. -10. 7.29 ]
[ 0. 0. 0. 0. ]
[ 8.1 8.1 9.1 10. ]
[ 7.29 -10. 8.19 9. ]
[ 0. 0. 0. 0. ]
[ 10. 9.1 8.1 0. ]
[ 9. 8.19 8.19 9. ]
[ 8.1 7.371 7.371 9.1 ]
[-10. 0. 6.6339 8.19 ]
[ 9. 8.19 0. 0. ]
[ 9.1 7.371 0. 8.1 ]
[ 8.19 6.6339 0. 8.19 ]
[ 7.371 0. 0. 7.371 ]]
위 스크립트를 돌리면 아마도 다음과 같은 결과물을 얻을 수 있을 것이다. 결과물을 해석해보면 Q-learning이 제대로 된 것 같다. 몇가지를 sample로 보면 아래와 같다
위 빨간색 에서는 아래로갈때 (action=3) 가장 큰 보상을 받을 수 있다. 바로 10의 reward를 받을 수 있기 때문이다. 다른 자잘한 reward(1)을 합친 후 가는 방향도 고려해 볼 수 있지만, 할인율을 적용해 봤을때, 바로 10의 reward를 받고 끝내는 것이 가장 큰 보상을 받는 바법이다.
파란색의 경우 위로 이동하면 -10을 받고 끝나게 되는 부분이 잘 포현되어 있다.
초록색은 위로 갔을때 9.1의 보상을 받게 되는 것을 표시하고 있다. 그 이유는 바로 받는 보상 1과 state중 가장 큰 보상을 받는 () 값에 할인율을 곱한 8.1이기 때문이다.
4 후기
내용이 슬슬 어려워지는 점도 있지만 그래도 전체적인 내용이 잘 정리되어 있어서 풀기 편했다. 다시한번 라이브 코딩을 해준 https://codebastardarch.pages.dev/ 님께 감사를 표하며 글을 마친다.