Skip to main content

MAB

import gymnasium as gym
import numpy as np
import tqdm

class MultiArmedBandit(gym.Env):
def __init__(self, p_dist, r_dist):
self.p_dist = np.array(p_dist) # 확률
self.r_dist = np.array(r_dist) # 보상
self.action_space = gym.spaces.Discrete(len(p_dist))
self.observation_space = gym.spaces.Discrete(1)

def step(self, action):
p = self.p_dist[action]
r = np.random.binomial(1, p) * self.r_dist[action]
return action, r, False, False, {}

평균의 계산법

평균 계산법은 이렇게 계산하거든요

어떻게 하냐면 R이 여러 개가 있다

R1, R2 여기서 보상이란 뜻이죠

보상이 여러 개가 있으면 이걸 다 더해가지고 n으로 나누면 그게 평균이 되죠

이게 우리가 알고 있는 평균 계산법인데 그러면 이 식을 잘 보시면 Vn은 n분의 R1 더하기 Rn 까지인데 Rn-1 더하기 Rn 이렇게 되겠죠

여기를 과로로 이렇게 묶어주면 R1부터 Rn-1까지를 더하면 그거는 이걸 n-1으로 나누면 Vn-1이니까 반대로 말하면 이거는 Vn-1에다가 n-1을 곱한 거랑 똑같아요

더하기 Rn이랑 똑같습니다

그러면은 우리가 이제 어떻게 할 수 있냐면 이걸 내릴 수가 있죠

그러면 이걸 쭉 풀어서 전개를 해주면 결국에는 이런 형태의 식으로 정리가 됩니다

그래서 이제 강학습에서는 이 계산법을 더 많이 써요

어차피 이거나 이거나 수학적으로는 똑같은데 이쪽을 더 좋아하는데는 몇 가지 이유가 있습니다

첫 번째는 합계를 구하면 이 합계의 숫자가 굉장히 클 거 아니에요

근데 컴퓨터의 특성과는 다르거든요

합계의 숫자가 굉장히 클 거 아니에요

컴퓨터의 특성상 숫자가 굉장히 커지면 계산이 이상해지는 현상이 있습니다

어떤 현상이 있냐면 우리가 예를 들어서 1 더하기 2 하면 3이 되죠

그러면 100 더하기 2 하면 102가 됩니다

여기다 소수점을 좀 붙일게요

100.0에다가 2를 더하면 102.0이 돼요

0을 좀 많이 해주면 어떻게 되냐면 10에 17승 이러고 갑자기 뒷자리가 없어져 버립니다

이거는 왜 그러냐면 지금 150쪽이죠

우리가 앞에서 그냥 설명 안 하고 넘어갔던 것 중에 64쪽에 부동소수점의 실수라는 게 있는데 우리가 컴퓨터에서 어떤 실수를 표현을 할 때 어떻게 하냐면 여기 만약에 32비트를 쓴다

그럼 이 첫 번째 비트는 부호를 나타냅니다

비트는 컴퓨터에서 이진수로 나타내는 건데 여기가 부호, 플러스, 마이너스 이진수로 나타낼 수 있겠죠

여기가 지수부예요

그래서 10에 n승 할 때 n을 이진수 8자리로 표현합니다

이진수 8자리면 최고 2에 256승까지 표현할 수 있겠죠

여기가 가수부예요

그래서 이거 곱하기 10에 n승 아니 10에 n승이란다

2에 n승 그다음에 플러스, 마이너스 이렇게 표현을 하는데 문제는 숫자가 커지면 이게 너무 커지니까 작은 숫자하고 더하면 서로 자릿수를 맞출 수가 없어요

그래서 어떻게 하냐면 작은 숫자를 버려버립니다

그래서 작은 숫자를 버리면 문제가 뭐냐면 예를 들어 합계를 계속 누적을 시켜 나가다가 합계가 어느 정도 커지고 나가면 여기다가 추가를 해줘도 합계가 더 이상 변하지 않아요

그래서 오류가 생기기 시작합니다

합계가 너무 커지면 오버플로우 문제가 생긴다는 게 이런 얘긴데 그래서 합계는 가능하면 안 구하려고 해요

그래서 그게 첫 번째 이유고 이건 단순히 수치적인 이유고 또 하나는 이렇게 수식을 써놓고 가만히 보다 보면 여기에 뭔가 깊은 뜻이 있는 걸로 느껴져요

무슨 깊은 뜻이 있느냐 이 vn-1은 우리가 알고 있던 기존의 가치죠

기존의 평균값이고 그러면 여기 뒤의 값을 지우고 이렇게 보면 기존의 값이 새로운 값이 됩니다

기존에 우리가 가지고 있던 새로운 추정치가 되는데 뭐가 붙냐 하면 이게 붙는 거예요

뭐가 붙느냐 뭐가 붙느냐 이거는 이번에 받은 보상이죠

이번에 받은 보상 이거는 우리가 기존에 알고 있던 가치 기존에 알던 가치 그래서 이 차이를 구해서 일종의 오차인데 이 차이가 플러스면 어떻게 됩니까?

기존의 가치에 더해주는 거죠

만약에 이게 마이너스면 기존의 가치에서 빼줍니다

그래서 이거를 직관적인 예로 바꿔보면 예를 들면 여러분이 단골 식당이 있는데 이 식당은 한 이 정도 맛은 내가 보장하는 식당이라고 알고 있었어요

예를 들면 한 90점 정도 되는 식당이에요

그래서 이 식당의 v는 원래 90점인데 v가 90점인데 이번에 가서 먹어보니까 영 못해 한 80점밖에 안 되는 것 같아

그러면 바로 80점으로 깎자니 그동안 정이 있단 말이에요

여러분 단골이에요

하루 맛없다고 밝기를 끊지는 않단 말이에요

그렇지만 마음속으로 점수가 좀 깎이죠

오늘은 좀 별로인데 그러면 예를 들면 점수를 깎겠죠

그래서 여러분이 오랫동안 여기에 단골이었으면 이 m분의 1이 굉장히 작겠죠 그러니까 점수를 조금만 깎습니다

그러면 사장님 오늘 음식이 좀 맛이 없네요

그리고 다음에 또 가겠죠

근데 여러분이 그 식당에 별로 많이 안 가봤어요

두세 번밖에 안 가봤습니다

그럼

2분의 1, 3분의 1 이러니까 한 번 맛없으면 점수가 확 깎이겠죠

그래서 단골이면 조금 봐주고 별로 많이 안 가봤으면 안 되겠네

이러면서 안 가겠죠

그리고 여러분이 처음 간 식당이다

그럼 처음 간 식당이면 1분의 1이니까 그날 먹어본 게 그 식당의 가치가 되어버립니다

그래서 평균 수학적으로 보면 그냥 다 더해서 n으로 나누는 거랑 똑같은데 이렇게 식으로 나누고 보니까 해석이 그럴듯 하단 말이에요

그래서 이제 강학습을 할 때 이렇게 많이 씁니다

두 가지 의미에서 계산상으로도 더 좋은 점이 있고 그 다음에 뭔가

이게 뭔가 우리한테 심오한 통찰을 준단 말이에요

이게 그냥 평균이 괜히 평균이 아니구나

그래서 결국 우리가 새로운 보상이 뭔가 우리가 기존에 알던 가치보다 높다

뭔가

내가 알던 것보다 괜찮은데 그러면 가치를 업해주고 새로운 보상이 내가 알던 것보다 못하다

그러면 가치를 다운 시켜주고 그럼 이 짓을 계속 하다가 우리가 단골이 돼서 n이 좀 쌓이면 한두 번은 사소한 거는 좀 봐주는 거죠 크게 영향을 미치는데 그것도 차이가 커서 내가 그래도 한 90점 되는데 80점이면 봐주겠지만 내가 이식당 100번 갔다 그럼

한 10점 빠져도 백분의 1을 하니까 0.1점만 깎는데 못 먹는 게 나왔어요

마이너스 100점 이러면 그건 용서가 안 되는 거죠

그래서 이런 식으로 해석을 한다

지수 이동 평균

그래서 기본적으로 평균 공식을 보면 영향력을 1만 분의 1만큼만 줘가지고 내가 많이 해보면은 더 이상 뭐 별로 업데이트를 잘 안 한다고 하면 업데이트를 잘 안 한단 말이에요

근데 그러면 후반으로 갈수록 영향을 잘 안 받게 되는데 이게 상황이 급격하게 변할 때는 좀 문제가 생깁니다

과거의 경험에 갇히는 그런 문제가 생깁니다

그래서 그거를 피하는 방법이 뭐냐면 원래는 여기가 m분의 1을 하게 되는데 그럼 n이 커지면 이제 더 이상 새로운 경험이 반영이 안 되는 거죠

여기가 새로운 경험인데 n이 커지면은 결국에는 n이 커지면 여기가 거의 0이 되잖아요

계속 옛날에 하던 소리 또 하고 또 하고 이렇게 되는 거니까 아 그렇게 하지 말고 여기를 어떤 a라는 상수로 고정을 해버리자 이런 방법이 나온다

이걸 이제 지수이동평균이라고 불러요

그래서 왜 지수이동평균이냐면 이 rn은 a만큼 반영이 되는 거죠

v의 a만큼 반영이 되는데 그러면은 이거를 식을 다시 잘 보시면 이걸 이렇게 정리를 다시 해주면 vn-1 빼기 a vn-1 더하기 arn 이렇게 됩니다

그죠?

이 쪽에다 써주면 그러면 얘네를 이렇게 묶어주면은 결국에는 1-a vn-1 더하기 arn 이렇게 되는데 새로운 v는 그 얘기는 뭐냐면 예를 들어서 우리가 a를 10%로 강제로 고정을 했어요

그럼 여기는 90%라는 얘기거든요

그럼 무조건 과거의 가치는 90%만 반영이 되고 새로운 가치가 10%가 반영이 됩니다

근데 그 다음 스텝에서는 그 다음 거가 10%가 반영되니까 또 과거의 거는 90%가 반영되죠

그래서 내가 아까 받은 거는 10% 반영이 되는데 이미 과거가 됐으니까 9%밖에 안 남아요

지금께 10% 그 더 전 거는 다시 9%에 9%니까 8. 몇 % 이렇게 해서 과거로 갈수록 반영 비율이 급격하게 떨어집니다

그리고 항상 현재는 10% 고정값을 차지하는 거예요

그래서 기존의 데이터가 남긴 영향은 점점 사라지는데 굉장히 지수적으로 사라지기 때문에 옛ㄱ날 기억이 많이 남기고 그 기억이 언제쯤인데 아직도 하고 있어요

그거는 반영 비율이 다 사라져서 없어져요

그리고 항상 최신 데이터가 더 많이 반영이 됩니다

그래서 A를 키워주면 예를 들면 A가 20%다

그러면 항상 최신 데이터가 20% 과거 데이터는 80%만 반영을 하니까 더 많이 반영을 하겠죠

그래서 이렇게 하면은 어떤 새로운 변화에 더 민감하게 할 수가 있어요

아닌 게 더 좋을 수도 있습니다

환경이 안정적이다 그러면은 이렇게 지수적으로 반영이 되니까 지수 이동 평균이 별로 안 좋은 선택이고 원래처럼 1분의 1으로 하는 게 좋은데 환경이 우리가 계속 변한다고 생각하면 이렇게 하시는 것이 계속 민감하게 따라갈 수 있는 방법이 되는 거죠

입실론 퍼스트

보통 우리가 간다는 사람들은 그러면은 몇 가지가 있는데 뭐가 잘 되는지 모르니까 일단 뭐 예를 들면 A 100번, B 100번, C 100번 이렇게 땡겨본단 말이야

그래서 B가 좀 잘 터지는 거 같은데 하면은 남은 전제산을 B에다가 갖다 넣는 거죠 보면 설득력 있는 방법이죠

아니면은 우리가 홈페이지에 어떤 디자인이나 광고가 있는데 A 안이 광고가 잘 될지 B 안이 광고가 잘 될지 잘 모르겠어요

그러면은 처음에는 뭐 예를 들면 1만 명 이렇게 정해가지고 1만 명의 고객 중에 5천명한테는 A 안을 보여주고 B 안은 5천명은 B 안을 보여줘가지고 어느 쪽이 더 광고가 클릭이 많이 되나

그래서 B가 더 많이 되더라 그러면 앞으로는 계속 B를 보여주면 되죠

그래서 이제 입실론 퍼스트를 한번 구현을 해보면 뭐가 이제 되게 복잡한데 일단 한 줄 한 줄 보면 입실론은 우리가 탐색할 비율입니다 탐색 비율 그래서 지금 N 에피소드가 1000인데 이거는 총 예산 같은 거라고요

총 1000번을 할 거예요

총 1000번을 할 건데 에피소드를 0.1 하면은 10%는 탐색을 한다는 거니까 100번 탐색하고 그다음에 900번 활용을 하겠다

이 얘기입니다

100번 탐색 900번 활용 만약에 여기를 0.2로 한다 그럼

20% 탐색을 하니까 200번 탐색 800번 활용 이렇게 됩니다

이해 되시죠?

class EpsilonFirstAgent:
def __init__(self, env, epsilon, n_episodes=1000):
self.env = env
self.epsilon = epsilon
self.n_episodes = n_episodes
self.Q = np.zeros(env.action_space.n)
self.N = np.zeros(env.action_space.n, dtype=int)
self.Q_history = np.empty((n_episodes, env.action_space.n))
self.returns = np.empty(n_episodes)
self.actions = np.empty(n_episodes, dtype=int)

def select_action(self, episode):
n_explore = int(self.n_episodes * self.epsilon)
if episode < n_explore:
return self.env.action_space.sample() # 탐색
else:
return np.argmax(self.Q) # 활용

def update_estimates(self, action, reward):
self.N[action] += 1
self.Q[action] = self.Q[action] + (reward - self.Q[action]) / self.N[action]

def run(self):
for e in tqdm.trange(self.n_episodes):
action = self.select_action(e)
_, reward, done, _, _ = self.env.step(action)
self.update_estimates(action, reward)
self.Q_history[e] = self.Q
self.returns[e] = reward
self.actions[e] = action
if done:
self.env.reset()
return self.returns, self.Q_history, self.actions

이동 평균

def moving_average(x, window=100):
return np.convolve(x, np.ones(window), 'valid') / window

입실론 퍼스트 실험

env = MultiArmedBandit(p_dist=[0.3, 0.1], r_dist=[0.1, 0.2])
agent = EpsilonFirstAgent(env, epsilon=0.1)
returns, Q_history, actions = agent.run()

# 시각화
import matplotlib.pyplot as plt
plt.plot(moving_average(returns), label='returns')
plt.plot(moving_average(actions), label='actions')
plt.legend()
output
100%|██████████| 1000/1000 [00:00<00:00, 117392.15it/s]

<matplotlib.legend.Legend at 0x12e66d064a0>
<Figure size 640x480 with 1 Axes>

# 행동 가치 추정치의 변화
plt.plot(Q_history[:, 0], label='0')
plt.plot(Q_history[:, 1], label='1')
plt.legend()
output
<matplotlib.legend.Legend at 0x12e689063e0>
<Figure size 640x480 with 1 Axes>

그러니까 우리가 이게 한 0.03 정도가 기대값으로 가지는 슬롯 머신이면 우리가 계속 그 슬롯 머신을 당기면 당연히 0.03 정도로 수렴을 하게 됩니다

근데 이제 약간 문제가 있는데 뭐가 문제냐면 이 주황색 얘는 0.02로 수렴을 해야 되거든요

수렴을 안 합니다

왜까요?

안 하니까요 그죠

아까 얘기 드렸듯이 많이 하면 수렴을 하게 되는데 얘한테 이제 기회가 없어요 100번 안에 수렴 못하면 얘는 끝입니다

다시는 얘한테 기회가 안 가요

그래서 여기서 지금 보시면 이렇게 직선이 돼버리는데 이거는 여기 마지막으로 추정한 거에서 더 이상 업데이트가 안 되기 때문에 그래서 이제 더 이상 변화가 없죠

문제점

그래서 이제 입실론 이 퍼스트가 좋은 정책 좋은 방법인데 문제가 있습니다

뭐가 문제냐면 만약에 우리가 상황이 이때까지 전이 함수가 안 바뀐다고 가정을 했는데 만약에 전이 함수가 바뀐다면 또는 보상이 바뀐다면 어떻게 됩니까?

만약에 과거에는 이 1번이 별로 안 좋았는데 만약에 얘가 좋아진다면 어떻게 돼요?

얘한테는 기회가 안 가니까 얘는 자기를 증명할 기회가 없는 거죠

여러분이 어떤 식당이 있는데 그 식당이 굉장히 맛있게 주황장도 바꾸고 심기일전에서 메뉴도 바꾸고 다 했어요

여러분이 안 가면 아무 소용 없잖아요

영원히 여러분한테 그 식당에는 여러분한테 서빙할 기회가 없는 겁니다

가끔 가줘야 돼요

그죠?

입실론 퍼스트의 문제 중에 입실론 퍼스트의 문제 중에 하나는 우리가 예를 들면 100번을 탐색하기로 정했는데 이 100번이 충분하냐

이거야

이게 확실히 의문이 드는 거죠

예를 들면 아까도 얘기했지만 여러분들이 결혼을 하기 전에 여러 사람을 만나보고 내가 누구랑 결혼할지 정하겠다 몇 명을 만나봐야 될까요?

예를 들면 소개팅이나 선을 보신다면 몇 번을 해야 충분히 만나봤다고 할 수 있을까요?

물론 혼자 사는 선택지도 있겠지만 그 다음에 더 문제는 뭔가 시간에 따라서 변화하는 상황 우리가 전이 함수 자체가 여기서 전이라기보다는 보상 함수죠

보상 함수가 바뀌면 어떻게 되냐

이거야

여름에 실험한 게 겨울에 안 통한다면 광고 같은 경우가 그렇죠

광고가 아무리 효과가 좋은 광고라도 천년 만 년 효과가 있지는 않을 겁니다 일정 기간 지나면 광고가 효과가 떨어지니까 광고를 바꿔줘야겠죠

입실론 탐욕법

똑같이 입실론인데 여기서 입실론 어떤 일정 비율을 말해요 그래서 입실론 퍼스트는 이 앞에 입실론만큼 하고 나머지는 계속 활용을 하는 방식이라면 입실론 탐욕법은 가끔씩 합니다 가끔씩 하는데 랜덤하게 어떨 때는 하고 어떨 때는 안 하고 근데 그 비율이 입실론이다

이 얘기야

예를 들면 우리가 입실론 10%다 라고 했으면 예를 들면 매일매일 우리가 0에서 1까지 난수를 생성해서 여기 사이에 들어가면 그날은 탐색하는 날 아니면 활용하는 날 예를 들면 여러분들이 식사를 하시는데 보통 때는 10번에 9번은 여러분이 잘 아는 맛집에 가는데 10번에 한 번쯤은 새로운 식당에 도전을 해보는 거예요

우리도 많이 이렇게 하죠

가끔씩 새로운 데 가볼까?

이러면서 새로운 데 가보잖아요

그래서 이렇게 가끔 가다 한 번씩 무작위로 탐색을 해보는 방법을 입실론 탐욕법이라고 합니다

입실론이라는 이름이 붙는 이유는 이 일정 비율 근데 왜 하필이면 그리스 문자가 입실론이냐면 보통 수학에서 입실론이라고 하면 작은 숫자를 나타낼 때 쓰거든요

그러니까 자주 안 한다는 거죠 가끔씩 이런 얘기입니다

입실론은 결국 가끔 탐욕법은 왜 탐욕법이냐?

활용을 하니까 탐욕법이라는 이름은 뭐냐면 주로 활용을 하는데 가끔 탐색도 한다

가끔 탐색 주로 활용 이런 뜻이에요

class EpsilonGreedyAgent(EpsilonFirstAgent):
def select_action(self, episode):
if np.random.rand() < self.epsilon:
return self.env.action_space.sample() # 탐색
else:
return np.argmax(self.Q) # 활용

입실론 그리디 알고리즘은 입실론 퍼스트 알고리즘하고 대강 똑같습니다 상속을 받아서 쓰는데 뭐가 달라지냐면 Select Action이 달라져요

다른 부분 같이 추정하거나 이런 거는 똑같습니다

어차피 몇 번 해보고 하는 거니까 뭐가 달라지면 아까 Select Action 다시 보시면 아까 Select Action은 n번을 정해서 n번까지는 무조건 탐색 그걸 넘어서면 무조건 활용 이렇게 되는데 입실론 탐욕법에서는 그냥 랜덤 넘버를 하나 만들어서 이 랜덤 넘버가 우리가 정한 입실론보다 작으면 탐색 아니면 활용 이렇게 합니다

그래서 계속 탐색을 하는 거죠

agent = EpsilonGreedyAgent(env, epsilon=0.1)
returns, Q_history, actions = agent.run()

# 시각화
plt.plot(moving_average(returns), label='returns')
plt.plot(moving_average(actions), label='actions')
plt.legend()
output
100%|██████████| 1000/1000 [00:00<00:00, 132819.41it/s]

<matplotlib.legend.Legend at 0x12e6892b670>
<Figure size 640x480 with 1 Axes>

# 행동 가치 추정치의 변화
plt.plot(Q_history[:, 0], label='0')
plt.plot(Q_history[:, 1], label='1')
plt.legend()
output
<matplotlib.legend.Legend at 0x12e689c35b0>
<Figure size 640x480 with 1 Axes>

그래서 가끔씩 탐색을 하니까 오르락 내리락 오르락 내리락 이러고 리워드도 0.03으로 수렴하지 않고 살짝 오르락 내리락 오르락 내리락 하는 현상이 생깁니다

왜냐하면 자꾸 탐색을 하니까 계속 똑같은 데만 슬롯머시만 당기면 수렴을 할 텐데 우리가 이것도 해보고 저것도 해보고 하니까 잘 수렴을 안 하겠죠

보시면 파란색은 0.03으로 수렴을 하는데 주황색은 자주 안 하니까 얘는 기회가 별로 자주 오지는 않거든요

가끔 한번씩 해보는 겁니다

감쇠 입실론 탐욕법 (선형적)

근데 입실론 퍼스트는 초기에 탐색을 하고 후반에는 탐색을 너무 안 해서 문제니까 우리가 초반에는 탐색을 하다가 서서히 탐색을 줄이는 이런 식으로 하면 좋을 것 같거든요

그래서 그거를 합친 게 감쇄 입실론 탐욕법입니다

쇄는 서서히 줄인다는 건데 뭘 줄이냐면 입실론을 줄인다

이런 거 그래서 입실론을 초반에는 크게 후반에는 조금 더 줄인다는 거 후반에는 조금 더 크게 후반에는 조금 작게 탐색을 계속 하긴 하지만 서서히 줄인다는 거죠

그래서 입실론을 어떤 속도로 줄이느냐 여러 가지 방법이 있는데 m분의 1로 줄이는 방법도 있고요 사인함수 형태로 줄이는 방법도 있습니다

그래서 입실론을 늘렸다가 줄였다가 늘렸다가 줄였다가 늘렸다가 줄였다가 이러는 방법도 있어요

그러면 어떻게 됩니까?

이때는 탐색 많이 하고 이때는 안 하다가 이때는 많이 하고 이때는 안 하다가 주기적으로 뭐가 바뀔 때 봄, 여름, 가을, 겨울에 따라 바뀐다

그러면 계절 초에 많이 해보고 예를 들면 여름이면 6, 7월에 좀 해보고 알겠다 싶으면 8, 9월에는 좀 덜 했다가 가을 넘어가면서 10월에 좀 해봤다가 이러면 되겠죠

아니면 같은 폭으로 일정하게 줄이는 것도 있고 같은 비율로 줄인다는 거는 이렇게 줄어들겠죠

거부가 그래서 여러 가지 방식으로 줄일 수 있는데 이거는 뭐가 좋다기보다는 상황, 상황마다 좀 달라요

그래서 딱 뭐가 좋다고 하기 힘든데 이거는 실제로 해보시고 더 결과가 좋은 거를 왜냐하면 상황마다 너무 다른데 어떤 상황에 뭐가 맞는지는 약간 예상이 어렵거든요

다 해보셔야 됩니다

예를 들면 우리가 어느 정도 예상이 가능한 경우도 있는데 예를 들면 계절적인 거면 계절마다 이렇게 해보시면 되는데 근데 아무리 계절적이라도 또 예를 들면 우리가 공장 같은 걸 운영하고 공장 같은 걸 운영을 하는데 봄, 여름, 가을, 겨울마다 뭐가 좀 바뀔 거니까 그거에 맞춰서 세팅 같은 걸 좀 바꿔야 되지만 그런 것도 한 10년, 20년 해보면 대충 이때쯤 되면 봄이면 이렇게 되겠네

이런 게 좀 감이 오잖아요

그러면 사실 그냥 사인으로 하는 게 아니라 사인으로 하는데 초반에는 크게 하다가 서서히 이렇게 사인 이런 식으로 줄여나가겠죠

이게 전략이 하나가 있는 게 아니라 상황상화마다 여러 가지가 있고 실제로 입실론을 다양한 방법으로 조정을 해보면서 최적치를 찾아야 됩니다

158쪽에 모이 담금질이라는 말이 나오는데요

이거는 저도 단조 이런 건 잘 모르는데 혹시 여기 잘 아시는 분 아마 있으실 거 같은데 우리가 철 같은 거의 강도를 조절할 때 담금질을 하잖아요

뜨겁게 했다가 서서히 어떻게 식히는데 온도를 어떻게 잘 조절하면 철의 성질 같은 거를, 재료의 성질을 바꿀 수 있잖아요

제가 잘 모르기 때문에 저도 잘 모르니까 대충 넘어가면 그런 물리적인 그런 현상이 있는데 그게 어쨌든 우리가 원하는 결정 구조 같은 게 있는데 뜨거워지면 입자들이 막 자유로워졌다가 어떤 온도를 이렇게 식히면 특정 결정 구조가 되는 거잖아요

여기에서 아이디어를 따와서 마치 우리가 철 온도를 높였다가 서서히 낮추는데 온도 낮추는 속도에 따라서 결정 구조가 형성이 되듯이 우리가 원하는 어떤...

어떤 알고리즘의 결과가 있는데 그거를 지금 탐색을 확 늘렸다가 서서히 낮춰가지고 우리가 원하는 어떤 상태로 수렴을 하게 하는 거예요

class LinearlyDecayingEpsilonGreedyAgent(EpsilonGreedyAgent):
def __init__(self, env, initial_epsilon, min_epsilon, decay_rate,
n_episodes=1000):
super().__init__(env, initial_epsilon, n_episodes)
self.initial_epsilon = initial_epsilon
self.min_epsilon = min_epsilon
self.decay_rate = decay_rate
self.epsilon = initial_epsilon
self.total_steps = 0

def decay_epsilon(self):
self.epsilon = max(self.min_epsilon, self.epsilon - self.decay_rate)

def select_action(self, episode):
self.decay_epsilon()
self.total_steps += 1
if np.random.rand() < self.epsilon:
return self.env.action_space.sample() # 탐색
else:
return np.argmax(self.Q) # 활용


agent = LinearlyDecayingEpsilonGreedyAgent(env, initial_epsilon=0.8, min_epsilon=0.1, decay_rate=0.0035)
returns, Q_history, actions = agent.run()

# 시각화
plt.plot(moving_average(returns), label='returns')
plt.plot(moving_average(actions), label='actions')
plt.legend()
output
100%|██████████| 1000/1000 [00:00<00:00, 79926.52it/s]


<matplotlib.legend.Legend at 0x12e6880eaa0>
<Figure size 640x480 with 1 Axes>

# 행동 가치 추정치의 변화
plt.plot(Q_history[:, 0], label='0')
plt.plot(Q_history[:, 1], label='1')
plt.legend()
output
<matplotlib.legend.Legend at 0x12e6887dd50>
<Figure size 640x480 with 1 Axes>

그래서 입실론 퍼스트처럼 처음에 탐색을 많이 하는데 뒤로 갈수록 탐색 비율이 줄어드니까 점점점점 줄어들어가지고 이쪽 후반으로 가면 탐색을 별로 안 하게 되는 거죠

별로 안 하지만 그래도 10%의 경우에는 탐색을 하니까 오르락내리락하는 게 있고요 그 다음에 행동가치의 추정치 변화를 보면 이때는 탐색을 많이 하니까 둘 다 많이 해보는 거예요

감쇠 입실론 탐욕법 (지수적)

class ExponentialyDecayingEpsilonGreedyAgent(LinearlyDecayingEpsilonGreedyAgent):
def decay_epsilon(self):
self.epsilon = max(self.min_epsilon, self.epsilon * (1-self.decay_rate))

낙관적 초기화

그 다음에 이제 마지막 방법이 있는데 낙관적 초기화라는 방법이 있습니다 낙관적 초기화는 뭐냐면 정말 별거 아닌 아이디어인데 굉장히 좋은 아이디어에요

뭐냐면 처음에 우리가 이제 가치를 선정을 할 때 가치의 초기 값을 얼마로 주냐면 코드를 보시면 0으로 설정하게 돼 있어요

근데 이렇게 하면 어떻게 되냐면 우리가 이제 A 안이 있고 B 안이 있는데 A를 선택할 때 가치의 초기값을 얼마나 줄 거냐 하면 코드를 보시면 0으로 설정하게 돼 있어요

근데 이렇게 하면 어떻게 되냐면 B 안이 있는데 둘 다 0으로 시작하잖아요

근데 A를 해봤더니 괜찮아요

그러면 아까 공식을 보면 VN-를 하니까 이게 0이니까 조금만 좋아도 얘가 확 올라가거든요

30 이렇게 올라간단 말이에요

그러면 문제가 뭐냐면 우리가 입실론 탐욕법이잖아요

항상 얘만 합니다

얘는 언제 기회가 오냐면 10%의 경우에만 기회가 와요

문제가 뭐냐면 어쩌다가 우연히 A가 몇 번만 좋으면 B는 10번에 한 번이나 기회가 올까?

이렇지 기회가 안 갑니다

문제가 뭐냐면 탐색이 원활하게 잘 안 되는 거예요 초반부터 너무 우리가 입실론을 꽤 크게 주더라도 그거를 상쇄하려면 입실론 한 50%는 줘야 상쇄가 되지 어지간해서는 B한테 기회가 안 가는 거죠

굉장히 우연한 작은 차이였는데 초반에 우연히 A가 좀 높았다는 이유로 A가 계속하게 되는 거죠

그래서 우리가 이제 MAB 문제에서도 초반에 어떤 우연 때문에 한쪽은 계속 탐색을 하고 한쪽은 탐색을 안 하면은 어떤 문제가 생기냐면 우리가 통계, 결국 이게 통계잖아요

그럼

A는 많이 해보면 A는 확실하게 추정이 됩니다

가치가 추정이 잘 되는데 B는 자주 안 해보니까 추정이 잘 안 돼요

그럼 추정이 잘 안 되면 뭐냐면 자기 가치가 이만큼 있는데 실제로는 많이 해봤으면 여기까지 갈 텐데 많이 안 해봐서 여기에 있을 수도 있는 거죠

그래서 낙관적 초기화라는 방법이 나오는데요

낙관적 초기화는 뭐냐면 그래서 처음에 가치를 0으로 주지 말라는 거예요

처음에 낙관적으로 주라는 거예요

그래서 초기값을 0으로 주지 말고 넉넉하게 100 이런 식으로 넉넉하게 주면 얘는 어떻게 되냐면 우리가 0으로 할 때랑 어떤 차이가 생기느냐

우리가 예를 들면 A가 있고 B가 있는데 실제 가치는 A는 30점이고 B는 한 20점이라고 쳐보겠습니다

그럼 우리가 0점에서 시작하면 0점에서 시작하는데 우연히 B를 한번 해봤어요

근데 B가 20점이라서 20점으로 올라가 버립니다

얘는 A는 0점에 있죠

그래서 A한테는 기회가 안 가고 계속 B만 하는 거예요

20점 20점 20점 그래서 10번에 한번쯤 가끔 B가 기회가 갔는데 우리가 100% 30점 항상 받는 게 아니잖아요

B가 기회가 갔다 왔는데 놓쳤네

그럼 다시 또 B만 계속 하는 거예요

B만 20점 20점 그러면 A는 10번에 한 번만 기회가 오니까 자기를 입증할 기회가 너무 늦게 옵니다

그래서 0점부터 시작하면 이게 되게 A한테 불리한 상황이 될 수 있어요

그냥 우연 때문에 근데 만약에 100점에서 시작하면 100점에서 시작하면 어떻게 되냐면 100점에서부터 시작했으니까 어떻게 돼?

점수가 거꾸로 내려가는 방식이잖아요

그래서 기회를 많이 받으면 오히려 약간 불리하게 됩니다

왜냐하면 처음에 또 똑같이 B가 우연히 해봤어요

그럼 어떻게 됩니까?

100점에서 시작했는데 얘는 점수가 확 깎이고 시작하는 거죠

20점 그러면 어떻게 되냐면 A한테 기회가 갑니다

A는 점수가 높으니까 자기를 입증할 기회가 있죠

그리고 만약에 그럼 반대로 되면 어떻게 되냐?

아니 그러면 반대로 되면 더 나쁜 거 아니에요?

라고 할 수 있는데 반대로 되면 예를 들면 A를 했는데 그럼 이번에 A가 점수가 30점으로 깎이고 시작하겠죠

그럼 이제 B한테 기회가 갈 겁니다

B는 아직 안 해봤으니까 초기 값으로 100점이에요

근데 B한테 기회가 많이 간다는 얘기는 B도 결국 많이 해서 자기 가치로 수렴한다는 얘기죠

그래서 결국에는 공평하게 기회가 가게 됩니다

그래서 우리가 탐색을 조금 하면 초기 가치가 많이 반영되어 있으니까 그냥 초기 값을 넉넉하게 줘서 모두한테 기회를 공평하게 줘라

이런 거에요

class OptimisticInitializationAgent(EpsilonGreedyAgent):
def __init__(self, env, optimistic_estimate, initial_count, n_episodes=1000):
super().__init__(env, 0, n_episodes)
self.optimistic_estimate = optimistic_estimate
self.initial_count = initial_count
self.Q = np.full(env.action_space.n, self.optimistic_estimate)
self.N = np.full(env.action_space.n, self.initial_count, dtype=int)
# Q는 낙관적 추정치로, N은 초기 값으로 채움
agent = OptimisticInitializationAgent(env, optimistic_estimate=0.05, initial_count=20)
returns, Q_history, actions = agent.run()
output
100%|██████████| 1000/1000 [00:00<00:00, 20664.14it/s]

plt.plot(moving_average(returns), label='returns')
plt.plot(moving_average(actions), label='actions')
plt.legend()
output
<matplotlib.legend.Legend at 0x12e68a4b010>
<Figure size 640x480 with 1 Axes>

plt.plot(Q_history[:, 0], label='0')
plt.plot(Q_history[:, 1], label='1')
plt.legend()
output
<matplotlib.legend.Legend at 0x12e68ab2830>
<Figure size 640x480 with 1 Axes>

여기서 시작을 해가지고 그러면 0과 1의 추정치인데 그러면 0과 1의 추정치인데 그 다음에 0하고 1의 추정치인데 둘 다 높은 데서 시작을 하는 거죠

여기서 시작을 해가지고 아까는 앞에 보시면 다 0에서 시작하거든요

0에서 시작해서 팍 위로 튀었다가 서서 이제 자기 걸로 수렴을 하는데 낙관적 초기화를 하면은 반대로 위에서부터 시작해서 서서 이제 자기 수치로 내려옵니다

자기 수치로 내려오니까 서서히 두 개가 다 내려와서 안정적으로 수렴을 하는 것을 볼 수 있죠

소프트맥스 전략

그래서 다른 대안들이 여러 가지 나오는데 그 중에 하나가 소프트맥스 전략입니다

소프트맥스 함수는 머신러닝 공부해 보신 분들은 많이 아시겠지만 쉽게 말하면 이런 거예요

처음 들으시는 분들도 이야기 쉽게 얘기해 보자면 우리가 예를 들어서 가치가 10점짜리가 있고 20점짜리가 있으면 한 가지 생각해 볼 수 있는 좀 설득력 있는 방법은 점수 비율대로 1대2 정도로 탐색을 해 보면 괜찮겠죠

이거는 두 배로 하고 만약에 10대 40이다

그럼 한 1대 4 정도 정도로 탐색하면 되겠죠

자기 가치 비율대로 하면 괜찮은 방법이 될 겁니다

근데 문제는 이런 식으로 하면 한쪽이 마이너스 하면 약간 비율 정하기가 곤란해요

그래서 그냥 숫자를 그대로 하지 않고 여기다 지수 함수를 씌웁니다

그래서 이렇게 exp 이렇게 쓰면 뭐냐면 exp-10 하면 2에 10승 이런 뜻이고 exp-20 하면 2에 20이라는 뜻입니다

아 플러스죠

지수 함수는 함수 생긴 모양이 이렇게 생겼기 때문에 여기가 0이고 마이너스가 들어오든 플러스가 들어오든 항상 어쨌든 결과가 양의 실수가 되거든요

그래서 지수 함수를 씌운 다음에 그 비율을 구하면 자기 가치의 비율대로 1대1로 대응 되는 건 아니지만 가치가 큰 거는 많이 하고 가치가 적은 거는 조금 하고 이런 식으로 되게 됩니다

그래서 가치가 클수록 더 많이 하겠죠

지수 함수니까 그래서 가치에 따라서 확률을 결정하는 방식으로 해서 그럼 두 개가 하나는 이만큼이고 하나는 이만큼이다 그러면 두 개가 차이가 많이 안 나니까 얘도 하면서 얘도 좀 하겠죠

근데 만약에 하나는 이만큼인데 다른 하나가 이만큼이다

그러면 가치 차이가 많이 나니까 얘를 압도적으로 많이 하고 얘는 아주 가끔만 하게 될 겁니다

그래서 탐색의 비율이 고정이 된 게 아니라 두 개가 비슷하면 더 탐색을 많이 하고 두 개가 차이가 많이 나면 탐색을 좀 덜 하는 거죠

예를 들면 여러분들이 A랑 사귈까

B랑 사귈까 아니면 A랑 결혼할까 B랑 결혼할까

고민이 되는데 A가 훨씬 좋아요

A가 훨씬 사람도 좋고 취향도, 외모도 내 취향이고 여러분은 A랑 더 자주 만나시겠죠

가끔 B가 연락 오면 A랑 B랑 한번 만나고 너무 비도덕쟁인가?

그럴 수 있는데 A랑 B랑 고민인데 둘 다 괜찮아

그러면 몰래 양쪽 다 만나겠죠

얘가 좀 이상한 거 같은데 어쨌든 넘어갑시다 가끔 말해놓고 후회할 때가 된대요

어쨌든 그래서 비율을 정해놓지 않고 두 개의 가치 차이에 따라서 확률이 달라지는 전략입니다

그래서 여기 보시면 그냥 EXP에 Q 이렇게 하면 되는데 여기 밑에 TAU로 나누거든요

분모에 TAU가 있는데 이거는 보통 온도라고 부릅니다

온도라고 부르는데 그 이유는 이게 원래 연력학에 볼치만 분포해서 나왔기 때문에 이 시기 연력학에서 이 자리가 온도가 들어가는 자리라서 그래요

그래서 얘가 연력학은 그렇다 치고 소프트맥스 전략에서 해주는 역할은 뭐냐 이 TAU가 커지면 어떻게 되냐면 예를 들면 TAU가 무한대로 간다 무한대분의 1이니까 여기가 다 0이 되겠죠

그러면 EXP 0 하면 1이 되니까 결국에는 그냥 m분의 1로 하는 거가 됩니다

그래서 TAU가 무한대로 가면 결국 모든 행동을 m분의 1로 하는 것과 똑같게 돼요

그래서 온도가 높으면 높을수록 모든 행동의 확률이 비슷비슷해집니다

골고루 하는 거고 온도를 낮추면 가치가 높은 행동을 더 많이 하게 됩니다

그래서 온도가 이를 때는 자기 가치대로 하는데 온도가 높아지면 자기 가치보다 더 높은 걸 더 많이 하게 됩니다

높은 건 더 많이 하고 적은 건 적게 하고 그래서 이것도 이제 앞에서 우리가 담금질 얘기할 때 얘기했듯이 처음에는 온도를 높여줬다가 온도를 서서히 낮춰주면 점점 점점 이제 가치가 높은 행동으로 수렴을 하게 되는 거죠

또는 이제 약간 기억하기 쉽게 하시려면 저는 어떤 비유를 드냐면 우리가 술을 먹으면 온몸이 후끈후끈 하면서 평소에 안 하던 행동도 좀 해보는 거죠 소리도 지르고 노래도 부르고 어디 물에도 뛰어들고 그러다가 술 깨고 정신이 차분해지면 머리가 차가워지면서 늘 하던 점잖은 행동만 하게 됩니다

그렇게 기억을 하시면 술 먹고 내가 머리가 뜨겁다

그러면 안 하던 행동도 좀 하고 평소에 안 하자는 짓을 하는 거죠 정신이 차가워지면 아주 날카롭게 어떤 하나만 하겠죠

그런 느낌으로 기억을 하시면 아 좀 외우기 쉬워요

그 다음에 이 소프트맥스 전략은 여기를 추정을 원래 Q를 추정을 해야 되는데 아예 Q를 추정하지 않는 방법도 있습니다

H라는 것을 보통 이걸 선호도라고 부르는데요 그냥 특정 행동의 선호도를 추정을 해요

그리고 이 가치를 추정하는 과정을 생략을 하고 매 단계마다 선호도를 이 공식에 따라서 수정을 합니다

그래서 어떻게 수정하냐면 이것도 보시면 약간 비슷한데 이게 이제 새로운 선호도고 이게 이제 기존의 선호도죠

기존의 선호도 그러면은 이번에 받은 보상이 있을 거예요

이번에 받은 보상인데 그 평균적인 보상이 있습니다

이거는 특정 행동의 평균이 아니라 지금까지 내가 받았던 이 행동을 했건 저 행동을 했건 지금까지 내가 했던 모든 보상의 평균이에요

모든 보상의 평균 V랑은 좀 다르죠

V는 왜냐하면 특정한 행동의 가치인데 이거는 그냥 행동 따지지 않아요

그래서 이번에 내가 받은 어떤 특정 행동을 해서 받은 보상이 있습니다

그래서 만약에 이 두 개의 차이가 플러스면은 이 선호도를 높여주고 마이너스면은 선호도를 낮추는 거예요

앞에 사실 가치를 추정을 안 한다고 했지만 뭐 공식 자체는 비슷해요

그 다음에 여기 하나가 더 붙는데 1-π가 붙죠

그럼 뭐냐면 내가 기존의 이 행동을 많이 하고 있었어요

그러면 이 차이 1배기 하면 이 차이가 작아지면 수정을 해주는 폭이 작습니다

내가 평소에 늘 하던 행동인데 이번에 보상을 좀 더 받거나 덜 받거나 이러면 그거는 선호도의 반영을 별로 안 하는데 내가 평소에 안 하던 걸 했는데 보상을 크게 받았다 그러면 확 늘어납니다

또 반대로 내가 원래 안 하던 걸 했는데 뭔가 큰 처벌을 받았다

그럼 선호도가 확 떨어집니다

예를 들면 여러분 도박 중독 같은 거 보면은 안 하다가 하면은 확 빠지잖아요

약간 그런 느낌으로 그리고 이제 나머지는 요공식으로 반영을 합니다

그래서 고르게 다른 거의 선호도를 빼줘요

그래서 이제 요공식이 어디서 나왔냐면 여기 요공식이 어디서 나왔냐면 경사상승법이라고 해서 요거는 우리가 보상의 기대값인데 보상의 기대값 보상의 기대값을 이렇게 생겼겠죠

그래서 우리가 행동을 이렇게 할 때 요 행동을 취할 때 이때 보상이 보상의 기대값이 제일 최고가 되는 어떤 행동이 있을 거예요

그러면 우리가 지금 행동을 이렇게 하고 있으면 기울기를 계산을 해서 이게 위분을 한다는 거거든요

이 기울기를 계산해서 기울기가 이쪽으로 갈수록 커지면 우리가 행동을 여기를 하고 있다가도 A'를 하고 있다가도 A로 이동을 해야겠죠

만약에 기울기가 이렇게 되어 있다

그럼 반대로 기울기 방향이 요 방향이니까 여기에서 A'를 하고 있다가도 이렇게 가서 해야 될 겁니다

그래서 요걸 경사상승법이라고 합니다

내가 지금 기울기가 이쪽 방향이냐 저쪽 방향이냐를 봐서 기울기가 올라가는 방향으로 행동을 수정을 해주는 거예요

그래서 이걸 위분을 해서 전개를 하면 요식이 나옵니다

그래서 보상의 기대값이 커지는 방향으로 선호도를 계속 줄이고 늘리고 이런덴데 그러면은 우리가 가치를 추정하지 않아도 행동을 바꿀 수가 있죠

그래서 여러분들 중에 인공신경망이나 딥러닝 이런 거를 아시는 분들은 딥러닝 할 때 경사하강법이라는 걸 보통 써서 딥러닝은 보통 일반적인 딥러닝 할 때는 어떻게 하냐면 예측오차를 줄이려고 하거든요

그래서 경사하강법이라는 걸 씁니다

근데 강화학습에서 딥러닝에다가 강화학습을 섞는데 우리가 소프트백스 전략을 쓴다 그러면 별거가 아닌데 보상을 높여야겠죠

그럼 어떻게 하면 되냐면 그냥 경사상승법을 쓰면 돼요

간단하죠

사실은 이렇게 하면은 그냥 지도학습이랑 비슷한 방식으로 크게 차이 안 나게 강화학습을 할 수가 있습니다

이 방법은 우리가 나중에 알아볼 건데 그래서 4일차 정도에 다룰 건데 그래서 이런 식으로 하는 거를 우리가 정책을 경사상승법을 쓴다고 해서 정책경사 이렇게 부릅니다

그래서 이게 약간 양대산맥 같은 거 중에 하나예요

근데 일단은 약간 맛보기로 이런 식으로 할 수 있다

이런 거고 그래서 이것도 구현을 해보면 온도를 설정을 하고요 온도를 서서히 낮춰가는 거죠 온도 조절하는 거는 우리가 앞에서 입실론 그리드에서 감쇄 입실론 그리드 하면서 배웠습니다 온도는 이렇게 변동을 하는 거고 그다음에 행동을 선택할 때 셀렉트 액션을 할 때 어떻게 하냐면 일단 온도를 우리가 Q를 구했으면 Q를 온도를 이용해서 나눠주고 그다음에 그냥 지수함수에도 넣어야 되는데

지수함수에 너무 큰 값을 넣으면은 이 숫자가 좀 이상해지거든요

그래서 여기다가 일괄적으로 제일 큰 최대 값을 빼줍니다

근데 이렇게 하면 해도 사실 계산상으로는 변화가 없어요

예를 들면 exp4하고 exp3하고 비교를 하나 양쪽에다 4 빼면은 exp0하고 exp-1하고 비교하는 거죠

계산을 해보면 이렇게 계산하나 이렇게 계산하는 똑같거든요

안 믿어지시겠지만 똑같습니다

보여드릴까요?

못 믿으시는 분이 있을까봐 잘 생각해보시면 똑같을 수밖에 없는데 mp.exp 해서 우리가 4가 있고 3이 있으면 이렇게 되는 거죠

그러면 이제 73% 4쪽이 73%가 되고 3쪽이 27%가 되겠죠

이렇게 해도 되고 둘 다 4를 빼서 이렇게 해도 똑같습니다

위에처럼 계산해도 되고 아래처럼 계산해도 되는데 컴퓨터에서 계산할 때 항상 숫자가 너무 크면 문제가 생겨요

그래서 숫자를 0 근처로 줄여주면 계산이 훨씬 안정적으로 되거든요

그래서 여기 빼주는 거는 계산의 안정성을 위해서 해주는 건데 사실 우리가 별로 그렇게 알 필요 없는 디테일이긴 합니다

근데 겸사겸사 알아 두시면 될 거 같고요 그래서 이 q를 지수함수에 넣어서 합계로 나눠주면 확률이 되는 거고 그래서 랜덤 초이스 이거는 우리가 준 확률에 따라서 이 액션들 중에 하나를 골라라

이런 얘기입니다

그 다음에 q를 여기서 아까 얘기 드린 겸사상승법을 안 쓰고 그냥 q 자체를 직접 추정을 하기 때문에 입실론 그리디를 상속을 받아서 q 자체를 직접 추정을 합니다 앞에서 한 것처럼 선호도를 조정하는 거는 숙제로 한번 해보세요 해보고 싶으신 분들은 그렇게 어렵진 않겠죠

공식 다 있으니까 이 공식대로 하시면 됩니다

그래서 이 방법을 해보면 소프트맥스 전략 여기 실험하는 코드도 있죠

어쨌든 보시면 액션은 처음에는 둘을 반반 하다가 나중에 시간 지나면 0으로 수렴하는 것을 볼 수가 있고요 그 다음에 두 개의 같이 추정치에는 둘 다 이렇게 시간이 지나면 얘는 0.3으로 수렴하고 얘는 0.01로 수렴하는데 얘를 거의 하고 얘는 잘 안 하는 거죠 확률이 이제 이쪽으로 많이 치우쳐서 그렇습니다

근데 우리가 여기 보시면 소프트맥스 에이전트의 기본 온도가 처음에 10.0에서 시작해서 점점 온도가 감소해 가지고 10에 마이너스 32升까지 낮추거든요 온도가 거의 0이 되는데 온도가 0이 되면 여기 보시면 탐색을 거의 안 합니다

온도가 아주 차가워졌기 때문에 아주 차갑게, 그렇게 3번이 좋군

냉철하게 여러분 술 먹고 도박한다고 에헤이 이러면서 이것도 하고 저것도 할 거 아니에요

그래서 여기 이제 민 템퍼러처를 여기 소프트맥스 에이전트에서 처음에 초기 하실 때 민 템퍼러처를 1.0 이렇게 주신다

그러면 0번하고 1번하고 골고루 합니다

1번도 좀 해요

왜냐하면 0번은 이쪽이 아니라 이쪽이구나 보시면 거의 반반을 하게 됩니다

왜냐하면 두 개의 확률 차이가 별로 차이가 크게 나지 않거든요

거의 비슷하게 하는데 약간 더 0을 많이 하는 정도로 하는 거죠

그래서 온도에 따라서 최종 온도가 1이면 우리가 가치가 그렇게 차이 안 나면 두 개를 비슷하게 하는데 온도를 많이 낮춰주면 아까 저런 온도를 낮춰주면 지금 보시면 액션이 여기서 훅 꺾이죠 온도를 낮춰주면 조금만 차이가 나도 그냥 안 해 버리는 거예요

그래서 온도를 잘 조절을 해서 우리가 탐색을 얼마나 하게 할 건지 원하는 대로 조정을 할 수 있습니다

class SoftMaxAgent(EpsilonGreedyAgent):
def __init__(self, env, init_temp=10.0, min_temp=1e-32,
decay_ratio=0.02, n_episodes=1000):
super().__init__(env, epsilon=0.0, n_episodes=n_episodes)
self.temp = init_temp
self.min_temp = min_temp
self.temp_history = np.empty(n_episodes)
self.decay_ratio = decay_ratio

def select_action(self, episode):
self.temp = max(self.temp * (1 - self.decay_ratio), self.min_temp)
self.temp_history[episode] = self.temp
scaled_Q = self.Q / self.temp
norm_Q = scaled_Q - np.max(scaled_Q) # 계산의 안정성을 위해
exp_Q = np.exp(norm_Q)
probs = exp_Q / np.sum(exp_Q)
action = np.random.choice(np.arange(len(probs)), size=1, p=probs)[0]
return action

agent = SoftMaxAgent(env)
returns, Q_history, actions = agent.run()
output
100%|██████████| 1000/1000 [00:00<00:00, 36188.68it/s]

plt.plot(moving_average(returns), label='returns')
plt.plot(moving_average(actions), label='actions')
plt.legend()
output
<matplotlib.legend.Legend at 0x12e68adbb20>
<Figure size 640x480 with 1 Axes>

plt.plot(Q_history[:, 0], label='0')
plt.plot(Q_history[:, 1], label='1')
plt.legend()
output
<matplotlib.legend.Legend at 0x12e68bde0b0>
<Figure size 640x480 with 1 Axes>

UCB

이제 신뢰상한 전략이라고 있는데 보통 줄여서 UCB라고 많이 합니다

UCB 일단 여러분 신뢰 구간이라는 말은 아마 들었을 거예요

통계 좀 아시는 분들은 뭔지 아실 거고 모르셔도 뭔가 학교 다닐 때 배운 적이 있는 거 같은데 이런 생각이 드실 수 있는데 그래도 잘 모르겠다 그러면 오차범위라고 생각하시면 됩니다 오차범위는 오차범위인데 오차범위는 오차범위가 오차범위는 오차범위인데 약간 측정 오차가 아니라 우리가 가지고 있는 어떤 불확실성을 말해요 측정 오차하고 뭐가 다르냐면 측정 오차는 그냥 기계가 정밀도가 떨어져서 생기는 거고 통계에서 신뢰 구간 또는 오차범위는 우리가 정보가 부족하기 때문에 잘 몰라서 생기는 겁니다

아리까리한데?

잘 모르겠네

그러면 우리가 지금 가치를 보면 우리는 딱 지금 하나로 추정을 하거든요

가치를 보면 가치가 이만큼인 거 같아

이렇게 추정을 하는데 사실은 우리가 잘 모른단 말이에요

우리는 추정치는 추정칠 뿐이기 때문에 이거보다 조금 더 높을 가능성도 있고 이거보다 사실 좀 더 낮을 가능성도 있습니다

그게 이제 우리의 신뢰 구간이 되는 거죠

그러면 약간 낙관적 초기화와 비슷한 논리로 우리가 잘 모르겠는데 그래도 한 이만큼까지도 될 수 있을 거 같거든요

아직 증거는 없지만 지금은 우리가 이 정도로 추정을 했는데 조금 뭐 한 이 정도까지도 되지 않을까 싶어요

그러면 이거를 얘 대표 값으로 주는 거죠

그래서 얘 실제 현재 가치가 아니라 얘의 어떤 우리 흔히 말하는 포텐셔를 보는 거죠

얘가 지금 지금까지 보여준 건 이 가치가 맞아요

지금까지 보여준 건 이 가치가 맞지만 얘는 여기까지도 될 거 같아

그래서 그 일종의 오차범위 또는 신뢰 구간을 해가지고 그 신뢰 구간에 컴피던스 바운드의 위쪽 어퍼 위쪽을 봐주는 건다 우리 뭐 예를 들면 야구 같은 거 좋아하시는 분들 야구 신인을 뽑으면 그런 거 보잖아요

지금까지는 얘가 보여준 거 한 이 정도지만 얘 잘 되면 여기까지 올라갈 거 같다 하면 이런 신인도 뽑을 수 있잖아요

그죠 팀에서 약간 그런 논리 그래서 이렇게 하면 약간 낙관적 초기화와 비슷한 결과를 가져오게 됩니다

왜냐하면 우리가 별로 안 해봤어요 적게 행동을 하면 많이 안 해봤으니까 샘플이 작죠

그럼 샘플이 작으니까 어떤 불확실성이 큽니다

얘는 그 잘 몰라요

우리가 잘 되면 여기까지 갈 수도 있는데 사실은 꽝일 수도 있는 거죠

꽝일 수도 있는데 우리가 이제 위쪽을 고르기 때문에 결국에는 얘가 선택될 수가 있습니다

그래서 우리가 두 개의 행동이 있는데 둘 다 가치는 똑같아요

지금까지 보여준 가치는 똑같지만 이쪽은 많이 해 봐가지고 잘 돼봤자 여기서 더 잘 돼봤자 여기고 안 돼봤자 여기에요

예를 들면 우리가 야구 선수인데 이 선수는 한 10년 정도 뛰었어요

그래서 이 정도 가치가 있는데 이 선수에 대해서는 이미 우리가 다 알아요

뭐 걔가 뭐 잘 해봤자지

근데 얘는 신인이에요

가치가 비슷해요

그럼 얘는 더 잘 될 수도 있겠죠

아니면 더 못 할 수도 있고 그러면은 누구한테 기회를 줘봐야 되겠냐

이거죠

우리가 이제 기회를 줘볼 수 있다면 얘한테 한번 기회를 줘봐야겠죠

얘가 가능성이 있으니까 지금까지 보여준 가치는 똑같아도 그래서 좀 아직까지 안 해본 쪽을 더 많이 탐색하게 해주는 그런 효과가 있습니다

그래서 이거를 이제 신뢰 상한 전략이라고 하고 알파고도 이 전략을 씁니다

알파고도 바둑을 배울 때 자꾸 새로운 수를 돋아야 되잖아요

그러면은 내가 어떤 수를 새로 시도를 해볼 거냐 할 때 이 신뢰 상한 전략을 씁니다

그래서 굉장히 약간 강학습계에서는 좀 뭐랄까

이렇게 얘기한 적은 없는데 먹어주는 전략입니다

약간 그 유명한 전략이에요

그래서 여기 이제 공식을 보시면 여기 오차 범위 공식인데 여기 밑에 n이 있어 가지고 n이 커지면은 n분의 1이니까 여기가 전체는 줄어들겠죠

그래서 오차 범위가 줄어들고 n이 작으면 n분의 1이니까 n이 작으면 전체는 커져서 플러스가 커집니다

원래는 신뢰 구간 할 때 플러스 마이너스 이렇게 하는데 우리는 플러스 쪽만 생각을 하는 거죠

그래서 잘 될 가능성을 보고 그쪽으로 가게 됩니다

그래서 신뢰 상한 전략 UCB를 구현을 한 걸 보면 역시 입실론 그리디를 상속을 하고요

왜냐하면 가치 추정은 똑같아요

가치는 똑같이 추정을 하는데 어디만 달라지냐면 이 Select Action 여기가 달라집니다

그래서 입실론은 안 쓰고요 입실론으로는 하지 않고 UCB를 계산을 하는데 아까 그 UCB 공식 이 앞에 있는 공식 이 공식대로 u를 계산을 해 가지고 이 q가 이때까지 보여준 가치고 실제 가치 여기가 실제 가치라고 하니까 좀 이상한데 지금까지 추정된 가치고 거기다가 더하기 u만큼 해줍니다

소프트맥스하고 다르게 이거는 확률적으로 하진 않아요

그 대신에 가능성이 있는 애한테 한번 해보자 그래서 일종의 가능성을 계산해서 가능성 큰 애를 선택을 하는 그런 방법입니다

UCB는 시간이 지나면 결국에는 둘러 많이 해보니까 그냥 가치가 높은 쪽으로 하게 되겠죠

그래서 이것도 해보면 UCB 그래서 보면 그 액션을 거의 반반으로 이렇게 하고 있는데요 추정되는 가치를 보면 추정되는 가치는 이렇게 벌어지고 있습니다 점점 벌어지고 있는데 그래도 거의 반반을 하고 있어요

왜냐하면 아직까지는 이만큼 벌어졌지만 얘가 좀 포텐셜이 있는 거예요

얘는 어쨌든 기회가 좀 적게 가긴 하거든요

기회가 적게 가긴 하지만 어쨌든 얘 포텐셜이 얘를 많이 하다가 어느 정도 많이 하고 나면 이거 너무 많이 해가지고 어떤 현상이 생기냐면 지금 제가 오차범위를 표시를 안 해놨는데 카피 이미지 이제 어떤 느낌으로 가는 거냐면 우리가 파란색을 많이 해보잖아요

그럼 파란색을 많이 하면 파란색이 처음에 오차범위가 이렇게 크다가 시간이 지나면 어떻게 되냐면 오차범위가 점점 파란색을 많이 해서 오차범위가 이렇게 줄어듭니다

줄어들면 주황색은 많이 안 해봤으니까 예를 들면 오차범위가 이만큼 있었단 말이에요

근데 주황색은 안 해보니까 오차범위가 계속 일정한데 오차범위가 이렇게 가는데 파란색이 처음에는 오차범위가 파란색도 오차범위가 이만큼 있다가 점점 해보면서 오차범위가 이렇게 줄어들겠죠

파란색이 줄어들다가 이렇게 크로스하면 주황색을 한번 해봅니다

주황색도 한번 해봤으니까 주황색도 오차범위가 줄어든단 말이에요

그러니까 오차범위 실제 가치뿐만 아니라 오차범위까지 작용을 해가지고 엎치락 뒤치락 하면서 파란색을 더 많이 하면 파란색 오차범위는 또 줄어드는데 그러면 얘가 더 줄어들면 또 다시 뒤집히니까 다시 얘한테 또 기회가 가는 거죠

그런 식으로 해서 서로 엎치락 뒤치락 하면서 기회가 가는 거죠

그래서 약간 자연스럽게 파란색을 많이 해보면 주황색 한 때도 한번씩 기회가 가게 됩니다

아주 차이가 많이 나지 않나요

class UpperConfidenceBoundAgent(EpsilonGreedyAgent):
def __init__(self, env, c=2, n_episodes=1000):
super().__init__(env, epsilon=0.0, n_episodes=n_episodes)
# epsilon은 사용되지 않으므로 0으로 설정
self.c = c
self.total_steps = 0

def select_action(self, episode):
if episode < len(self.Q):
return episode # 처음에는 각 행동을 한 번씩 선택
else:
# UCB 계산
U = self.c * np.sqrt(np.log(episode + 1) / (self.N + 1e-5))
return np.argmax(self.Q + U) # UCB를 적용하여 행동 선택
agent = UpperConfidenceBoundAgent(env)
returns, Q_history, actions = agent.run()
output
100%|██████████| 1000/1000 [00:00<00:00, 76588.71it/s]

plt.plot(moving_average(returns), label='returns')
plt.plot(moving_average(actions), label='actions')
plt.legend()
output
<matplotlib.legend.Legend at 0x12e68c47340>
<Figure size 640x480 with 1 Axes>

plt.plot(Q_history[:, 0], label='0')
plt.plot(Q_history[:, 1], label='1')
plt.legend()
output
<matplotlib.legend.Legend at 0x12e68cbf880>
<Figure size 640x480 with 1 Axes>

톰슨 샘플링

그 다음에 UCB는 기본적으로 무작위성이 없거든요

그냥 가장 포텐셜이 있는 가능성이 있는 쪽에다가 해보는 거라서 약간 이것도 해보고 저것도 해보고 싶은데 어떻게 하냐

그런 이유 때문에 만들어진 게 톰슨 샘플링이라는 방법입니다

톰슨 샘플링은 어떻게 하냐면 지금까지는 가치를 추정을 했어요

그렇게 하지 말고 그 다음에 UCB를 한번 해보고 싶었죠

지금까지는 가치를 추정을 했어요

그렇게 하지 말고 가치의 어떤 분포를 추정을 하는 겁니다

그래서 약간 오차범이랑 비슷한데 결국에는 한 이 정도 되는 것 같긴 한데 이 분포 안에 있을 것 같아

이런 식으로 추정을 하는 거예요

그러면은 우리가 지금 여기 있는지 여기 있는지 여기 있는지 모르잖아요

랜덤하게 골라보는 거예요

하면 여기는 확률이 높으니까 여기서는 자주 나오고 이런 데에는 확률이 작으니까 이런 데에서는 자주 안 나오게 됩니다

그림이 너무 밑에 있나요?

다시 여기 우리가 지금 여기 이제 이런 볼록한 분포를 보면 여기 꼭지점은 확률이 높기 때문에 여기서는 이제 자주 나와요 자주 나오면 그 다음에 여기 끝머리로 가면은 자주 안 나옵니다

자주 안 나와

그 다음에 이렇게 뾰족한 거 있잖아요

뾰족한 거 이거는 여기에서 자주 나오고 여기에서는 자주 안 나와

그러면 어떤 현상이 생기냐면 우리가 이제 여기서 랜덤하게 뽑으면 이 뾰족한 애가 어쨌든 지금 보시면 상대적으로 더 높은 데 있잖아요

높은 데 있으니까 자주 높은 값이 나오긴 하는데 얘는 넓직한 애는 막 이런 데서도 나오지만 뾰족한 애는 이런 데서는 거의 안 나온단 말이에요

그래서 얘가 더 높은 데서 나오긴 하는데 또 반대로 이런 쪽 극단으로 가면은 얘는 또 잘 나오지 않는 거죠

얘가 또 자주 나오는 거죠

가끔 이 넓적한 애가 여기서 뽑혀 나오고 이 뾰족한 애는 이 범위 안에서 어쨌든 나오겠죠

그러면 고를 때는 이 넓적한 애가 가치가 더 높게 됩니다

그래서 매번 매번 가치의 추정치를 다시 뽑아요

그래서 추정치가 더 높게 나온 쪽을 선택하는 방법입니다

랜덤하게 어떤 현상이 생기냐면 일단 우리가 전반적으로 두 개가 이렇게 있는데 둘 다 분포가 하나는 이렇게 있고 하나는 분포가 이렇게 있다

그래서 이게 A고 이게 B면 대체로 어쨌든 B가 가치가 더 높은 쪽이 많이 뽑혀 나올 테니까 대부분 B를 더 많이 하게 됩니다

약간 소프트맥스랑 비슷하죠

그 다음에 A랑 B랑 비슷한데 A는 이렇게 되어 있고 B는 이렇게 뾰족하게 되어 있다 그러면은 이럴 때는 A를 하게 되고 이럴 때는 B를, 이럴 때는 A를 더 많이 할 수도 있겠죠

그렇지만은 이런 데서 A는 뽑히고 B는 여기서 나오면 B를 하게 될 겁니다

근데 이제 언제 좁아지느냐 이거는 신뢰 구간하고 비슷하게 많이 해보면은 이렇게 좁아져요

많이 해보면 좁아지고 많이 해봤는데 실제로 가치가 별로더라

그러면은 이런 데로 내려가는 거죠

이런 데로 내려가고 많이 봤는데 좋더라 그럼

이런 데로 올라갑니다

처음에 넓적한 데서 시작을 해서 많이 할수록 좁아지는데 좁아지면서 자기 실제 가치가 여기면 이 점으로 점점 좁아지는 거고 이렇게 됩니다

그래서 약간 UCB의 특징도 있고 소프트맥스의 특징도 있고 이런 방법이에요

가장 최신 방법이라고 할 수 있고요 이게 알려지기로는 구글에서 광고 선택할 때 구글이 대부분 광고로 돈을 버는데 구글이 광고를 뭘 내보는 일까를 결정할 때 이걸 그대로 쓰지는 않겠죠

톰슨 샘플링에 기반한 방법을 쓰고 있다고 합니다

그래서 이 방법도 구현을 보면 구현 자체는 그렇게 복잡하지는 않아요

생각보다 굉장히 단순한데 우리가 두 가지 값을 설정을 합니다 알파랑 베타 알파랑 베타를 설정하고요 보시면 지금 알파는 1이고 베타는 0인데 여기서 보시면 표준 편차를 정하는데 표준 편차를 알파 분해 N 더하기 베타 만큼 해요

그러면 알파가 1이고 베타가 0이면 어떻게 되냐면 정규분포는 평균하고 표준 편차에 의해서 모양이 결정되거든요

이 공식을 보시면 결국 표준 편차를 N분의 1로 하겠다

이거예요

그 다음에 이 정규분포에 노멀에서 평균을 어디에 설정하냐면 이때까지 추정된 가치를 평균으로 하겠다

이거예요

어떻게 되냐면 우리가 추정한 Q가 여기 있으면 표준 편차는 N분의 1로 설정을 하게 됩니다

그러면 사실상 UCB랑 거의 비슷하거든요

UCB랑 거의 비슷한데 UCB는 루트가 더 들어가는 거죠

UCB는 루트가 더 들어간 거고 루트가 빠진 거 빼고는 사실 비슷합니다

근데 UCB랑 다른 점은 UCB는 상한 값 자체를 쓰는 거고 톰슨 샘플링은 상한 값 자체를 쓰는 거고요 그래서 UCB를 평균으로 하겠다

이거예요

UCB는 상한 값 자체를 쓰는 거고 톰슨 샘플링은 요 사이에서 뾱!

하나 뽑는 거에요

그 차이입니다

그래서 여기서 보시면 랜덤 점 노멀하면 정규분포에서 하나를 뽑아라

이렇게 하는 거에요

그래서 뽑힌 값이 제일 큰 거를 고르게 돼요

코드는 의외로 되게 짧죠

아까 논리는 되게 복잡하죠

그래서 이런 방식으로 하는 게 톰슨 샘플링입니다

이것도 한번 해보면 그래서 서서히 보시면 탐색이 줄어드는 거를 보실 수가 있고요

왜냐하면 표준편차가 M분의 1로 줄어들기 때문에 그 다음에 가치가 추정되는 걸 보면 이런 식으로 탐색을 많이 하게 되면 수렴은 둘 다 잘 하게 됩니다

왜냐하면 기회가 둘 다 공평하게 가니까 근데 톰슨 샘플링에서 이것도 우리가 조금 생각을 해보시면 탐색을 얼마나 할까를 다양하게 결정을 할 수 있는데 정규분포 같은 경우에 모양을 좁게 만들려면 어떻게 돼요?

표준편차를 작게 만들면 되죠

그래서 알파를 작게 주시면 탐색을 좀 덜 하겠죠

아니면 베타를 좀 크게 주거나 이런 식으로 알파나 베타를 조절하시면 탐색하는 정도를 바꿀 수 있고 아니면 여기도 약간 온도처럼 해서 그냥 시간이 지나면 저절로 알파가 올라가게 이런 식으로 해서 감쇄를 하실 수도 있습니다

아니면 알파가 줄어들게 해야 되죠

시간 지나면 지금 그런 게 없는데 알파를 디케잉 시켜가지고 시간 지나면 표준편차가 서서히 0이 되도록 만들어주면 나중에 가면 탐색을 안 하게 되겠죠

그런 식으로 여러분들 이제 응용을 할 수도 있어요

class ThompsonSamplingAgent(EpsilonGreedyAgent):
def __init__(self, env, alpha=1, beta=0, n_episodes=1000):
super().__init__(env, epsilon=0.0, n_episodes=n_episodes)
self.alpha = alpha
self.beta = beta

def select_action(self, episode):
# 평균이 Q이고 표준편차가 sd인 정규분포에서 값을 하나씩 추출
sd = self.alpha / (np.sqrt(self.N) + self.beta)
samples = np.random.normal(loc=self.Q, scale=sd)
return np.argmax(samples) # 가장 큰 값이 나온 밴딧을 선택
agent = ThompsonSamplingAgent(env)
returns, Q_history, actions = agent.run()
output
  0%|          | 0/1000 [00:00<?, ?it/s]C:\Users\eupho\AppData\Local\Temp\ipykernel_48804\1819746816.py:9: RuntimeWarning: divide by zero encountered in divide
sd = self.alpha / (np.sqrt(self.N) + self.beta)
100%|██████████| 1000/1000 [00:00<00:00, 55341.86it/s]

plt.plot(moving_average(returns), label='returns')
plt.plot(moving_average(actions), label='actions')
plt.legend()
output
<matplotlib.legend.Legend at 0x12e68d42950>
<Figure size 640x480 with 1 Axes>

plt.plot(Q_history[:, 0], label='0')
plt.plot(Q_history[:, 1], label='1')
plt.legend()
output
<matplotlib.legend.Legend at 0x12e68dbb580>
<Figure size 640x480 with 1 Axes>

퀴즈