import gymnasium as gym
#---#
import numpy as np
import collections
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import IPython14wk-2: (강화학습) – 4x4 Grid World q_table, Appedix B
1. 강의영상
2. Imports
3. q_table
q[s1,s2,a]는 상태 (s1,s2) 에서 행동 a의 “품질”을 의미한다.
- 직관적으로 푸는 방법 (손으로 푸는 다이나믹 프로그래밍)

A. 미래보상
- 언뜻생각하면 4x4 문제에서 q_table은 아래와 같이 생각하는게 합리적인듯 보인다.
q[s1,s2,a]= 상태 (s1,s2)에서 행동 a를 했을 경우 얻게되는 보상의 평균- \(q(s,a) = r(s,a) = \mathbb{E}[\text{Reward} | \text{State}=s, \text{Action}=a]\)
그렇지만 아래와 같이 생각하는게 더 합리적이다.
q[s1,s2,a]= 상태 (s1,s2)에서 행동 a를 했을 경우 얻게되는 보상의 평균 + 미래에 얻게되리라 기대하는 보상- \(q(s,a) = r(s,a) + r_{\text{future}}\)
단, 여기에서 미래에 얻게되리라 기대하는 보상은 최선의 선택을 한다는 전제하에 계산
- 미래에 얻게되리라 기대하는 보상은 어떻게 정의할 수 있을까?

# 예제1 – 상태 (2,2) 에서 “action=down” 을 했을때
- 즉시 얻게되는 보상과
- 미래에 얻게되리라 기대하는 보상
은 무엇인가? 이것을 바탕으로 \(s=(2,1)\), \(a=\text{down}\) 의 품질(Quality)는 어떻게 평가할 수 있는가?
(풀이?)
즉시 얻을 수 있다고 생각되는 보상은-1 이고 미래에 얻으리라 기대되는 보상은 100 점이다. 따라서 99점으로 평가하는게 합리적인듯하다. 수식으로 쓰면
\[q(s,a)=q(s_1,s_2,a)=q(2,2, \text{down}) = -1 + 100 = r(2,2,\text{down}) + \max_{a'}q(3,2,a')\]
와 같이 쓸 수 있겠다.
#
# 예제2 – 상태 (1,2) 에서 “action=down” 을 했을때
- 즉시 얻게되는 보상과
- 미래에 얻게되리라 기대하는 보상
은 무엇인가? 이것을 바탕으로 \(s=(1,2)\), \(a=\text{down}\) 의 품질(Quality)는 어떻게 평가할 수 있는가?
(풀이?)
즉시 얻을 수 있다고 생각되는 보상은-1 이고 미래에 얻으리라 기대되는 보상은 99점이다. 따라서 98점으로 평가하는게 합리적인듯하다. 수식으로 쓰면
\[q(s,a)=q(s_1,s_2,a)=q(1,2, \text{down}) = -1 + 99 = r(1,2,\text{down}) + \max_{a'}q(2,2,a')\]
와 같이 쓸 수 있겠다.
#
# 예제3 – 상태 (0,1) 에서 “action=right” 을 했을때
- 즉시 얻게되는 보상과
- 미래에 얻게되리라 기대하는 보상
은 무엇인가? 이것을 바탕으로 \(s=(0,1)\), \(a=\text{right}\) 의 품질(Quality)는 어떻게 평가할 수 있는가?
(풀이?)
앞의 예제들을 일반화하면 아래와 같은 수식을 쓸 수 있다.
\[q(0,1, \text{right}) = r(0,1,\text{right}) + \max_{a'}q(0,2,a')\]
따라서 만약에 \(\max_{a}q(0,2,a)\)의 값을 알고 있다면 이를 구할 수 있다.
#
- (아직 부족한) 깨달음: 모든 \((s,a)\)에 대하여 \(q(s,a)\)의 값은 아래와 같이 정의할 수 있겠다.
\[q(s,a) = r(s,a) + \max_{a'}q(s',a')\]
B. 감가율
# 예제1 – 당신은 지금 아무것도 쓰여 있지 않은 빈 종이 한 장을 가지고 있습니다. 이 종이에 쓸 수 있는 숫자는 오직 두 가지, 0 또는 1입니다. 어떤 숫자를 쓰느냐에 따라 보상이 달라지는데, 수많은 실험을 통해 0을 쓰면 아무 보상도 없고, 1을 쓰면 10만 원을 받을 수 있다는 사실이 확인되었습니다. 이 사실이 확인된 이후 이 빈 종이의 가치는 얼마일까요?
(1) 0원이다.
(2) 10만원이다.
(3) 5만원이다.
(4) 모르겠다.
#
# 예제2 – 당신 앞에는 빨간색 종이 한 장이 있습니다. 이 종이에는 0 또는 1 중 하나의 숫자를 쓸 수 있습니다. 만약 1을 쓰면 다음 단계인 주황색 종이 한 장을 받게 됩니다. 주황색 종이에도 똑같이 0 또는 1을 쓸 수 있고, 여기에 1을 쓰면 노란색 종이, 그다음은 초록색 종이, 그 다음은 파란색 종이, 그 다음은 남색 종이, 마지막으로는 보라색 종이를 순서대로 받습니다. 총 7단계(빨강 → 주황 → 노랑 → 초록 → 파랑 → 남색 → 보라색)를 거친 후, 보라색 종이에 1을 쓰면 비로소 10만 원의 현금 보상을 받을 수 있습니다. 단, 어느 단계에서든 0을 쓰면 아무 일도 일어나지 않고 그 즉시 게임이 종료됩니다. 즉, 그 이후로는 종이도 받을 수 없고 보상도 없습니다. 이 사실이 알려진 이후, 지금 당신이 들고 있는 ’빨간색 종이’의 가치는 얼마일까요?
(1) 0원이다.
(2) 10만 원이다.
(3) \(\frac{1}{2^6}\) x 10만원이다.
(4) 모르겠다.
#
- 직관: 아무리 보장된 보상이라고 해도, 미래에 주어지는 보상은 현재의 보상과 동급취급할 수 없다.
- 진짜 깨달음: 모든 \((s,a)\)에 대하여 \(q(s,a)\)의 값은 아래와 같이 정의하는게 합리적이다.
\[q(s,a) = r(s,a) + \gamma \max_{a'}q(s',a')\]
여기에서 \(\gamma\)는 0과 1사이의 값이며 감가율(discout factor)이라 부른다.
C. q_table update
- 지난시간코드
class GridWorld:
def __init__(self):
self.a2d = {
0: np.array([0,1]), # →
1: np.array([0,-1]), # ←
2: np.array([1,0]), # ↓
3: np.array([-1,0]) # ↑
}
self.state_space = gym.spaces.MultiDiscrete([4,4])
self.state = np.array([0,0])
self.reward = None
self.terminated = False
def step(self,action):
self.state = self.state + self.a2d[action]
s1,s2 = self.state
if (s1==3) and (s2==3):
self.reward = 100
self.terminated = True
elif self.state in self.state_space:
self.reward = -1
self.terminated = False
else:
self.reward = -10
self.terminated = True
# print(
# f"action = {action}\t"
# f"state = {self.state - self.a2d[action]} -> {self.state}\t"
# f"reward = {self.reward}\t"
# f"termiated = {self.terminated}"
# )
return self.state, self.reward, self.terminated
def reset(self):
self.state = np.array([0,0])
self.terminated = False
return self.state
class RandomAgent:
def __init__(self):
self.state = np.array([0,0])
self.action = None
self.reward = None
self.next_state = None
self.terminated = None
#---#
self.states = collections.deque(maxlen=500000)
self.actions = collections.deque(maxlen=500000)
self.rewards = collections.deque(maxlen=500000)
self.next_states = collections.deque(maxlen=500000)
self.terminations = collections.deque(maxlen=500000)
#---#
self.action_space = gym.spaces.Discrete(4)
self.n_experience = 0
def act(self):
self.action = self.action_space.sample()
def save_experience(self):
self.states.append(self.state)
self.actions.append(self.action)
self.rewards.append(self.reward)
self.next_states.append(self.next_state)
self.terminations.append(self.terminated)
self.n_experience = self.n_experience + 1
def learn(self):
passplayer = RandomAgent()
env = GridWorld()
scores = []
score = 0
#
for e in range(1,100000):
#---에피소드시작---#
while True:
# step1 -- 액션선택
player.act()
# step2 -- 환경반응
player.next_state, player.reward, player.terminated = env.step(player.action)
# step3 -- 경험기록 & 학습
player.save_experience()
player.learn()
# step4 --종료 조건 체크 & 후속 처리
if env.terminated:
score = score + player.reward
scores.append(score)
score = 0
player.state = env.reset()
break
else:
score = score + player.reward
player.state = player.next_state강의노트 수정 2025-06-12
노규호학생의 도움으로 예전강의의 오류를 발견하여 수정하였습니다.
# 수정전
...
if env.terminated:
...
else:
score = score + player.reward
scores.append(score)
player.state = player.next_state
# 수정후
if env.terminated:
...
else:
score = score + player.reward
# scores.append(score) ### <--- 여기를 주석처리해야함!!
player.state = player.next_state- 상황: player가 경험은 있는데, q_table을 만들줄 모름 (데이터는 있음, 학습이 안된상태)
player.n_experience328330
- 저번시간에 배운 q_table
q_table = np.zeros((4,4,4))
memory = zip(player.states, player.actions, player.rewards)
for (s1,s2), a, r in memory:
qhat = q_table[s1,s2,a] # 내가 생각했던갓
q = r # 실제값
diff = q-qhat # 차이
q_table[s1,s2,a] = q_table[s1,s2,a] + 0.01*diff# updatefor a in [0,1,2,3]:
print(f"action = {a}")
print(f"q[...,{a}] = {q_table[...,a].round(3)}")
print("---")action = 0
q[...,0] = [[ -1. -1. -1. -10. ]
[ -1. -1. -1. -10. ]
[ -1. -1. -1. -9.999]
[ -1. -1. 99.991 0. ]]
---
action = 1
q[...,1] = [[-10. -1. -1. -1.]
[-10. -1. -1. -1.]
[-10. -1. -1. -1.]
[-10. -1. -1. 0.]]
---
action = 2
q[...,2] = [[ -1. -1. -1. -1. ]
[ -1. -1. -1. -1. ]
[ -1. -1. -1. 99.993]
[-10. -10. -9.999 0. ]]
---
action = 3
q[...,3] = [[-10. -10. -10. -10.]
[ -1. -1. -1. -1.]
[ -1. -1. -1. -1.]
[ -1. -1. -1. 0.]]
---
- 이번시간에 배운 q_table: 아래를 이용한다.
\[q(s,a) = r(s,a) + \gamma \max_{a'}q(s',a')\]
사실 좀 더 실용적으로는(=코딩친화적으로는) 아래의 수식을 쓰는게 좋다.
\[q(s,a) = \begin{cases} r(s,a) & \text{if terminated} \\ r(s,a) + \gamma \max_{a'}q(s',a') & \text{else} \end{cases}\]
q_table = np.zeros((4,4,4))
memory = zip(player.states, player.actions, player.rewards, player.next_states, player.terminations)
for (s1,s2), a, r, (ss1,ss2), tmd in memory:
qhat = q_table[s1,s2,a] # 내가 생각했던값
if tmd:
q = r # 실제값
else:
future = q_table[ss1,ss2,:].max()
q = r + 0.95 * future
diff = q-qhat # 차이
q_table[s1,s2,a] = q_table[s1,s2,a] + 0.01*diff# updatefor a in [0,1,2,3]:
print(f"action = {a}")
print(f"q[...,{a}] = {q_table[...,a].round(3)}")
print("---")action = 0
q[...,0] = [[ 72.838 77.721 82.807 -10. ]
[ 77.725 82.87 88.276 -10. ]
[ 82.867 88.285 93.989 -9.999]
[ 88.229 93.978 99.991 0. ]]
---
action = 1
q[...,1] = [[-10. 68.195 72.833 77.683]
[-10. 72.836 77.721 82.849]
[-10. 77.717 82.862 88.186]
[-10. 82.723 88.128 0. ]]
---
action = 2
q[...,2] = [[ 72.838 77.724 82.867 88.241]
[ 77.721 82.869 88.286 93.982]
[ 82.801 88.272 93.985 99.993]
[-10. -10. -9.999 0. ]]
---
action = 3
q[...,3] = [[-10. -10. -10. -10. ]
[ 68.195 72.835 77.718 82.734]
[ 72.832 77.721 82.863 88.122]
[ 77.678 82.845 88.216 0. ]]
---
player.q_table = q_table- 이제 플레이어의 행동은?
player.q_tablearray([[[ 72.83771663, -10. , 72.83771538, -10. ],
[ 77.72139771, 68.19454652, 77.72442966, -10. ],
[ 82.80747929, 72.83310558, 82.86672465, -10. ],
[ -9.99998197, 77.6827334 , 88.24107054, -9.99998511]],
[[ 77.72454812, -10. , 77.72127368, 68.19457346],
[ 82.86950596, 72.83600446, 82.86912699, 72.83541752],
[ 88.27585546, 77.72068451, 88.28563001, 77.71797997],
[ -9.99999683, 82.84920596, 93.98163671, 82.73393175]],
[[ 82.8669733 , -10. , 82.8012108 , 72.83236834],
[ 88.28521264, 77.71722413, 88.27171754, 77.72136508],
[ 93.9886205 , 82.86197892, 93.98461924, 82.86278052],
[ -9.99904498, 88.18588483, 99.99321405, 88.12228859]],
[[ 88.22883855, -9.9999832 , -9.99997635, 77.67756866],
[ 93.97813902, 82.72305248, -9.99999792, 82.84474465],
[ 99.99082608, 88.12798376, -9.99917862, 88.21551322],
[ 0. , 0. , 0. , 0. ]]])
def act(player,s1,s2):
action = player.q_table[s1,s2,:].argmax()
return actionact(player,0,0)0
player.q_table[0,0,:]array([ 72.83771663, -10. , 72.83771538, -10. ])
4. Solve
- 추후 hw로 풀어보세요
Appendix B - 신경망관련용어
- 은근히 용어가 헷갈리는데, 뜻을 좀 살펴보자.
- ANN: 인공신경망
- MLP: 다층퍼셉트론 (레이어가 여러개 있어요)
- DNN: 깊은신경망, 심층신경망
- CNN: 합성곱신경망
- RNN: 순환신경망
# 예시1 – MLP, DNN
net = torch.nn.Sequential(
torch.nn.Linear(in_features=1,out_features=2),
torch.nn.ReLU(),
torch.nn.Linear(in_features=2,out_features=2),
torch.nn.ReLU(),
torch.nn.Linear(in_features=2,out_features=1),
torch.nn.Sigmoid()
)- ANN: O
- MLP: O
- DNN: O
- CNN: X (합성곱레이어가 없으므로)
- RNN: X (순환구조가 없으므로)
#
# 예시2 – MLP, Shallow Network
net = torch.nn.Sequential(
torch.nn.Linear(in_features=1,out_features=2),
torch.nn.ReLU(),
torch.nn.Linear(in_features=2,out_features=1),
torch.nn.Sigmoid()
)- ANN: O
- MLP: O
- DNN: X (깊은 신경망으로 생각하려면 더 많은 레이어가 필요함. 합의된 기준은 히든레이어 2장이상, 이걸 설명하기 위해서 얕은 신경망이란 용어도 씀)
- CNN: X (합성곱레이어가 없으므로)
- RNN: X (순환구조가 없으므로)
#
# 예시3 – MLP, DNN, Wide NN
net = torch.nn.Sequential(
torch.nn.Linear(in_features=1,out_features=1048576),
torch.nn.ReLU(),
torch.nn.Linear(in_features=1048576,out_features=1048576),
torch.nn.ReLU(),
torch.nn.Linear(in_features=1048576,out_features=1),
torch.nn.Sigmoid(),
)- ANN: O
- MLP: O
- DNN: O (깊긴한데 이정도면 모양이 깊다기 보다는 넓은 신경망임, 그래서 어떤 연구에서는 이걸 넓은 신경망이라 부르기도 함)
- CNN: X (합성곱레이어가 없으므로)
- RNN: X (순환구조가 없으므로)
# 예시4 – CNN
net = torch.nn.Sequential(
# Layer1
torch.nn.Conv2d(1, 64, kernel_size=4, stride=2, padding=1, bias=False),
torch.nn.LeakyReLU(0.2),
# Layer2
torch.nn.Conv2d(64, 128, kernel_size=4, stride=2, padding=1, bias=False),
torch.nn.BatchNorm2d(128),
torch.nn.LeakyReLU(0.2),
# Layer3
torch.nn.Conv2d(128, 256, kernel_size=4, stride=2, padding=1, bias=False),
torch.nn.BatchNorm2d(256),
torch.nn.LeakyReLU(0.2),
# Layer4
torch.nn.Conv2d(256, 512, kernel_size=4, stride=2, padding=1, bias=False),
torch.nn.BatchNorm2d(512),
torch.nn.LeakyReLU(0.2),
# Layer5
torch.nn.Conv2d(512, 1, kernel_size=4, stride=1, padding=0, bias=False),
torch.nn.Sigmoid(),
torch.nn.Flatten()
)- ANN: O
- MLP: X (합성곱연결이 포함되어있으므로, MLP가 아님, 완전연결만 포함해야 MLP임)
- DNN: O
- CNN: O (합성곱레이어를 포함하고 있으므로)
- RNN: X (순환구조가 없으므로)
#
# 예시5 – CNN
net = torch.nn.Sequential(
torch.nn.Conv2d(1,16,(5,5)),
torch.nn.ReLU(),
torch.nn.MaxPool2d((2,2)),
torch.nn.Flatten(),
torch.nn.Linear(2304,1),
torch.nn.Sigmoid()
)- ANN: O
- MLP: X
- DNN: X? (히든레이어가 1장이므로..)
- CNN: O (합성곱레이어를 포함하고 있으므로)
- RNN: X (순환구조가 없으므로)
근데 대부분의 문서에서는 CNN, RNN은 DNN의 한 종류로 설명하고 있어서요.. 이런 네트워크에서는 개념충돌이 옵니다.
#
# 예시6 – RNN
class Net(torch.nn.Module):
def __init__(self):
super().__init__()
self.rnn = torch.nn.RNN(4,2)
self.linr = torch.nn.Linear(2,2)
def forward(self,X):
h,_ = self.rnn(X)
netout = self.linr(h)
return netout
net = Net() - ANN: O
- MLP: X
- DNN: X? (히든레이어가 1장이므로..)
- CNN: X (합성곱레이어가 없으므로)
- RNN: O
이것도 비슷한 개념충돌
#
학부연구생 모집(2-3명): 관심 있으면 연락 주세요.
- 대상: 통계학에 관심있는 3-4학년
- 연구 내용: 다음을 포함한 통계학 관련 주제
- 다중척도 방법을 활용한 비유클리드 자료분석 (지오메트릭 딥러닝, 토폴로지컬 딥러닝)
- 혹은 학생이 원하는 연구주제
- 문의 및 지원: gbchoi0814@gmail.com
- 모집 기한: 6/25 까지