멀티 암드 밴딧
카지노에 놀러간 여러분의 앞에는 $n$개의 서로 다른 슬롯머신이 있습니다. 레버를 당기면 랜덤한 확률로 상금(보상)을 탈 수 있는데, 각각의 슬롯머신이 상금을 주는 확률과 상금의 크기가 서로 다릅니다. 여러분은 오늘 하루동안 이 슬롯머신이 상금을 주는 확률과 상금의 크기가 서로 다릅니다. 여러분은 오늘 하루동안 이 슬롯머신을 당겨서 최대한의 보상을 받고자 합니다.
에이전트는 한정된 플레이 횟수 속에서 자신이 알고있는 가장 좋은 슬롯머신을 계속 플레이해야할까? 아니면 더 좋은 슬롯머신이 있는지 미지의 환경을 탐색해야할까?
탐색과 활용
탐색(Exploration)
아직 잘 모르는 슬롯머신을 선택해 더 많은 정보를 얻는 과정이다. 더 좋은 슬롯머신을 발견할 가능성이 있다.
활용(Exploitation)
현재까지 알려진 정보를 바탕으로, 가장 좋은 보상을 줄것으로 보이는 슬롯머신을 선택하는 과정이다.
보상 추정하기
$1$번부터 $n$번까지의 슬롯머신 $i$에 대해 각각 $n$번씩 선택했다 해보자, i번 슬롯머신을 $n$번 당겨 $n$번의 보상을 받았을때 이 $i$번 슬롯머신 가치의 추정치는 보상의 평균이다.
환경 구성하기
import gymnasium as gym
import numpy as np
#라이브러리 불러오기
class BanditEnv(gym.Env):
    def __init__(self, num_bandits):
        self.num_bandits = num_bandits # 슬롯머신의 갯수
        self.action_space = list(range(num_bandits)) # 행동은 가능한 모든 슬롯머신
        self.observation_space = [0] # 상태는 의미 없음
    def reset(self):
        # 슬롯머신의 평균 보상의 크기를 미리 결정하는 코드
        # 각 슬롯머신에 대해, 랜덤한 평균 보상의 크기를 결정해 줌.
        # self.mean = np.random.normal(size=self.num_bandits) * 10
        # 아래는 항상 같은 크기의 보상을 정해둔 코드의 예시
        # self.mean = [9, 8, 7.5, 7, 8.5, 7, 6, 7.5, 8, 8.5]
        self.mean=np.random.normal(size=self.num_bandits) * 10
        return 0 # 상태 전환 없음. 의미 없음.
    def step(self, action):
        state = 0
        mean = self.mean[action] # 정해둔 평균값에서 선택한 슬롯머신에 대한 값 확인
        reward = mean + np.random.normal() # 보상은 그 슬롯머신의 평균값 + 랜덤한 노이즈
        done = False
        return state, reward, done, {}
슬롯 머신의 환경을 구현하는 코드이다. 클래스 BanditEnv를 만들고 num_bandits와 같은 데이터를 저장해주는데, 여기서 슬롯머신 문제의 특성상 상태와같은것은 필요 없기에 적절히 설정만 해둔다.
환경의 step부분은 먼저 정해둔 각 슬롯머신의 평균값에서 랜덤한 노이즈를 더해 구현한다.
직접 구현해보기
class MyPolicy:
    def __init__(self, num_bandits):
        self.num_bandits = num_bandits # 슬롯머신 개수 저장
        # 아래에 더 저장하고싶은정보 저장가능
        self.bandits_data=[0 for i in range(num_bandits)]
    def __call__(self, state, reward):
        global howMuch
        action = 0
        if np.random.random(1)[0] <= howMuch:
            action=np.random.randint(0, self.num_bandits)
            # print("random select")
            isRandom=True
        else:
            action=self.bandits_data.index(max(self.bandits_data))
            # print("best select")
            isRandom=False
        self.bandits_data[action]=(self.bandits_data[action] + reward)/2
        # print(" ".join(map(str, self.bandits_data)))
        return action, isRandom
직접 문제 해결을 시도한 코드이다. 탐색 과정을 확률을 이용해 강제로 진행하는 방식으로 진행하였고, 외부에서 지정한 확률에 따라 랜덤한 슬롯머신을 당기거나, 평균값이 최대인 슬롯머신의 레버를 당긴다.
bandits_data 리스트는 평균값을 저장하는데, bandits_data[action]=(self.bandits_data[action] + reward)/2에서 기존 평균값과 새로운 보상값을 평균내었다. 다만 이 방법은 정확한 평균값 수정이 아니었다.
env=BanditEnv(10)
state= env.reset()
agent=MyPolicy(10)
reward=0
n=20
howMuch=0.3
total_reward = []
len1, len2=[],[]
random_reward=[]
graph1=[]
graph2=[]
total_reward=0
    # howMuch=0.01*i
for t in range(n):
    action, isRandom = agent(state, reward)
    # action = 1
    # print("Chose action:", action)
    state, reward, done, _= env.step(action)
    # print("Reward:", reward)
    if isRandom: 
        random_reward.append(reward)
        len1.append(t)
    else: 
        total_reward+=reward
        len2.append(t)
    # print("="*20)
total_reward+=reward
print(env.mean[action])
print("최고의 슬롯머신의 보상 평균", max(env.mean))
print(total_reward)
9 최고의 슬롯머신의 보상 평균 9 131.73648839473503
직접 구현하려는 시도에서는 위와같이 운좋게 성공하는 경우가 있긴 했으나, 대다수 실패했다.

직접 만든 코드의 시각화인데, 정상적으로 진행되지 않은걸 볼수있다.
제대로된 구현
class MyPolicy:
    def __init__(self, num_bandits):
        self.num_bandits = num_bandits # 슬롯머신 개수 저장
        initial_q=100
        self.q = [initial_q for machine in range(num_bandits)]
        self.n = [0 for machine in range(num_bandits)]
    def __call__(self, state):
        action = np.argmax(self.q)
        return action
제대로된 구현에서는 비정상적으로 높은 값에서 뽑은 값과 평균내며, 확률 없이도 전체 탐색이 가능하도록 만들어져있다.
ε-greedy 알고리즘
env = BanditEnv(10)
state = env.reset()
agent = MyPolicy(10)
# 그동안의 선택 정보를 저장
history = [100 for i in range(env.num_bandits)]
num=[1 for i in range(env.num_bandits)]
total_reward = 0
for t in range(2000):
    if np.random.random(1)[0]<=0.01:
        action = action=np.random.randint(0, env.num_bandits)
    else:
        action = agent(state)
#   action = np.random.choice(env.num_bandits)
    state, reward, done, _ = env.step(action)
    # 기록 저장
    agent.n[action]+=1
    agent.q[action]+= (reward - agent.q[action])/ agent.n[action]
    total_reward+=reward
    # agent.q = ???
    # print(f"Action: {action}")
    # print(f"Reward: {reward}")
    total_reward += reward
print(f"Total Reward: {total_reward}")
ε-그리디 알고리즘은 기존 그리디 알고리즘이 탐색을 정상적으로 진행하지 않는다는 점을 보완하여, 특정한 확률에 따라 강제적으로 탐색을 실행하도록 만들어진 알고리즘이다. 이 코드에서는 단순하게 특정 확률로 다른 에이전트를 실행하는 방식으로 구현했지만, 에이전트 클래스 내부에서 확률에 따라 달라지게 만들수도 있다.
과제
- 정리하기
- 도식화한 내용 코드짜오기
- 배운 내용 교재 읽어보기
- 사전과제 안한 것 수행
- 강화학습으로 풀 수 있는 주제 고민해보고 코드로 구현가능하면 해보기