2024.07.09 - [프로그래밍/강화학습 (RL)] - 틱택토 강화학습 (Tik-Tak-Toe RL) - [정보과학융합탐구 - 7월 과제]
저번 7월 과제를 끝으로 더 이상 정융탐 과제를 할 필요는 없지만 저번에 시간이 없었어서 연구를 좀 찝찝하게 끝내기도 했고, (쓸데없이 제가 꼭 분석해보겠다고 해서) 추가적으로 확인해볼 사항들이 좀 있었기에 기존 연구를 좀 수정하고 분석해보는 시간을 가져보았습니다.
이번에 해본 연구는 크게 '첫 수를 둔 칸 별 승률 확인', '칸 별 첫 수를 둔 횟수 확인', '에이전트와 직접 플레이' 정도로 나눌 수 있습니다.
첫 수를 둔 칸 별 승률 확인 & 칸 별 첫 수를 둔 횟수 확인
먼저 첫 수를 둔 칸 별의 승률들과 칸 별 첫 수를 둔 횟수를 확인해볼 것입니다. 해당 기능들을 위해서는 run_q_learning.py 파일을 수정해야합니다. run_q_learning.py 파일의 Run_Q_Learning 함수를 수정해 episode를 반복할 때 첫 수를 둔 위치를 기록하고, agent.first_move_wins라는 딕셔너리를 이용해 해당 episode의 결과가 승인지 아닌지를 저장하고, 첫 수를 둔 위치의 횟수를 계산하였습니다.
수정된 Run_Q_Learning 함수의 코드는 다음과 같습니다.
def Run_Q_Learning(agent, env, num_episode, p_mode="N"):
history = []
for episode in range(num_episode):
state = env.reset()
final_reward, n_moves = 0.0, 0
first_move = None
while True:
action = agent.choose_action(state)
if n_moves == 0:
first_move = action # 첫 수 기록
next_state, reward, done = env.move(action)
agent.learn(Transition(state, action, reward, next_state, done))
env.print_board()
state = next_state
n_moves += 1
if done:
final_reward = reward
if first_move is not None:
agent.first_move_wins[first_move]["total"] += 1
if reward == env.reward_case["win"]:
agent.first_move_wins[first_move]["win"] += 1
break
history.append(final_reward)
if episode % 1 == 0:
print('에피소드 %d: 보상 %.1f #이동 %d' % (episode, final_reward, n_moves))
return history
이후 그래프를 그리는 plot_graphs라는 함수를 수정해 기존의 그래프(episode 별 보상, episode 별 승률)들과 새로 만든 그래프가 한 화면에 들어올 수 있게 수정하였습니다. 그래프를 그릴 때 딕셔너리에서 요소들을 받아와 승률을 계산하게 하였고, 칸 별 승률과 첫 수 선택 횟수는 모두 히트맵으로 표현해주었습니다.
plot_graphs 함수는 다음과 같습니다.
def plot_graphs(history, agent, env):
win_prob = win_prob_history(history, env)
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
axes[0, 0].plot(history, 'b.')
axes[0, 0].set_xlabel('Episodes')
axes[0, 0].set_ylabel('Final Rewards')
axes[1, 0].plot(win_prob)
axes[1, 0].set_xlabel('Episodes')
axes[1, 0].set_ylabel('Winning Probability')
win_rates = np.zeros((env.cell, env.cell))
for i in range(env.cell ** 2):
total = agent.first_move_wins[i]["total"]
wins = agent.first_move_wins[i]["win"]
if total > 0:
win_rates[i // env.cell][i % env.cell] = wins / total
else:
win_rates[i // env.cell][i % env.cell] = 0
im = axes[0, 1].imshow(win_rates, cmap='coolwarm', interpolation='nearest')
fig.colorbar(im, ax=axes[0, 1], label='Winning Rate')
axes[0, 1].set_title('First Move Winning Rate Heatmap')
axes[0, 1].set_xlabel('Column')
axes[0, 1].set_ylabel('Row')
for i in range(env.cell):
for j in range(env.cell):
axes[0, 1].text(j, i, f'{win_rates[i, j]:.2f}', ha='center', va='center', color='black')
first_move_freq = np.zeros((env.cell, env.cell))
for i in range(env.cell ** 2):
first_move_freq[i // env.cell][i % env.cell] = agent.first_move_wins[i]["total"]
im2 = axes[1, 1].imshow(first_move_freq, cmap='Blues', interpolation='nearest')
fig.colorbar(im2, ax=axes[1, 1], label='Frequency')
axes[1, 1].set_title('First Move Frequency Heatmap')
axes[1, 1].set_xlabel('Column')
axes[1, 1].set_ylabel('Row')
for i in range(env.cell):
for j in range(env.cell):
axes[1, 1].text(j, i, int(first_move_freq[i, j]), ha='center', va='center', color='black')
plt.tight_layout()
plt.show()
이제 해당 코드를 실행해 얻은 결과를 보여드리겠습니다.
왼쪽의 두 그래프는 7월달에 출력해본 그래프이고, 7월에 출력해봤을 때와 형태가 똑같음을 알 수 있습니다. 주목해야할 것은 새로 추가된 오른쪽의 두 그래프인데, 오른쪽 위 그래프가 첫 수를 둔 칸 별 승률을 나타낸 것이고, 오른쪽 아래 그래프가 첫 수를 둔 칸 별 횟수를 나타낸 것입니다. 참고로 전체 승률은 95.14%가 나왔습니다.
인상적인 점은 가운데 위 칸을 무려 9000번 정도 선택하고, 나머지 칸들은 150번 남짓밖에 선택하지 않았다는 것입니다. 또, 승률도 가운데 위 칸은 0.97이라는 압도적인 수치인 반면에 나머지 칸들은 0.7~0.8 정도의 비교적 저조한 승률을 나타냈다는 것입니다.
이 상황을 이해하기 위해 제가 생각한 시나리오는
1. agent가 모든 칸에 적절히 첫 수를 둬본 결과 가운데 위 칸의 승률이 제일 높아 가운데 위 칸에만 두기 시작했다.
2. 우연히 agent가 가운데 위 칸에서 몇 번 이겨서 승률이 높아졌고, 이에 무지성으로 가운데 위에만 두기 시작했다.
이렇게 2가지입니다.
1번 시나리오대로라면 agent가 가운데 칸의 승률이 0.97로 가장 높다는 것을 스스로 알아내고 그곳에 첫 수를 둔 것이기 때문에 더할 나위 없이 좋은 결과이겠지만, 다른 칸들에 겨우 150번 남짓 둬보고 가운데 위 칸의 승률이 제일 높다고 판단하기는 어려울 것이라 생각했습니다.
따라서 저는 agent가 어쩌다 가운데 위 칸에서 몇 번 이기는 바람에 가운데 위 칸의 승률이 제일 높아졌고, 승률이 높은 쪽으로 행동하는 agent가 가운데 위에만 첫 수를 둔 것이라고 생각했습니다. 이러한 상황이 벌어지면 다른 칸들의 승률이 높을지 낮을지 정확히 판단할 수 없다는 문제가 발생할 수 있기에, 이를 해결하기 위해 모든 칸에 돌아가면서 한 번씩 첫 수를 두게 코드를 수정해보았습니다.
모든 칸에 같은 횟수만큼 첫 수를 두게 한 결과는 다음과 같습니다.
가장 먼저 눈에 띄는 것은 승률이 확연히 낮아졌다는 것입니다. 이전 실행 결과의 전체 승률이 95% 정도인 것에 비해 모든 칸을 동일하게 선택했을 경우 최종 승률이 58.52% 밖에 되지 않습니다. 이를 통해 이전 실행에서 agent가 어느 정도는 승률이 높은 쪽으로 선택했다는 것을 알 수 있습니다.
또, 칸 별 승률을 확인하면 이번에는 가운데의 승률이 약 0.7로 승률이 0.5~0.6인 다른 곳보다 높다는 것을 알 수 있습니다. 사실 우리가 일반적으로 생각하는 건 당연히 가운데에 놔야 승률이 높다고 생각하는데, 앞서는 이러한 결과가 나오지 않았지만 무조건 한 번씩 돌아가며 선택하게하자 이런 결과가 나온 것도 상당히 흥미로운 것 같습니다.
어찌되었든 모든 칸을 동일하게 선택했을 경우 승률이 낮아지는 것은 맞기에 승률이 높은 쪽으로 알아서 선택하게 하는 앞선 실행이 좀 더 효과적이었다고 판단할 수 있을 것 같습니다.
에이전트와 직접 플레이
다음으로 해볼 일은 에이전트와 직접 플레이가 가능하게 하는 것입니다. 백날천날 개발해봤자 컴퓨터에서 혼자 돌아가다가 끝나면 무슨 소용이겠습니까. 의문의 조력자(GPT-4o)에게 조언을 구한 결과 q-value들을 저장해두면 학습된 에이전트를 저장하는 것과 같은 효과라고 합니다. q-value들은 어떤 행동을 했을 때 어떤 보상이 올지를 예측하게 해주기 때문이죠.
따라서 run_q_learning.py 파일을 수정하여 pickle이라는 라이브러리를 이용할 수 있도록 q_value들을 저장하였습니다. 해당 코드는 다음과 같습니다.
if __name__ == '__main__':
env = TicTacToe("1p", "N")
agent = Agent(env)
history = Run_Q_Learning(agent, env, 10000)
# Q-Value 저장
agent.save_q_table('q_table.pkl')
total_wins = sum(1 for reward in history if reward == env.reward_case["win"])
total_episodes = len(history)
win_rate = total_wins / total_episodes
print(f'총 승률: {win_rate:.2%}')
plot_graphs(history, agent, env) # 그래프 그리기
이후 play_with_agent라는 파일을 새로 만들어 저장해둔 pkl 파일을 pickle 라이브러리를 이용해 불러오고, 게임을 진행할 수 있게 해주었습니다. play_with_agent 파일의 코드는 다음과 같습니다.
from TTT_Env import TicTacToe
from agent import Agent
import pickle
def play_with_agent(agent, env):
state = env.reset()
env.print_board()
while True:
if env.player == 1:
print("당신의 차례입니다. 착수할 위치를 입력하세요 (1-9): ")
action = int(input()) - 1
else:
action = agent.choose_action(state)
print(f"에이전트의 차례입니다. 에이전트가 {action + 1} 위치에 착수했습니다.")
next_state, reward, done = env.move(action)
env.print_board()
state = next_state
if done:
if reward == env.reward_case["win"]:
if env.player == -1:
print("당신이 이겼습니다!")
else:
print("에이전트가 이겼습니다!")
elif reward == env.reward_case["draw"]:
print("무승부입니다!")
break
if __name__ == '__main__':
env = TicTacToe("1p", "Y")
agent = Agent(env)
# Q-값 불러오기
agent.load_q_table('q_table.pkl')
play_with_agent(agent, env)
이후 대망의 시험을 해보았습니다.
Human vs AI ! (Tik-Tak-Toe)
"인간과 AI의 맞대결" 지금까지 인간은 수많은 종목에서 AI와 대결해왔습니다. 처음에는 기계가 어떻게 인간을 이기냐고 비웃음만 가득했죠.
그러나 1997년 IBM의 딥블루가 세계 체스 챔피언을 상대로 승리하는 말도 안 되는 일이 일어났고, 이후 AI의 폭주는 점점 심해져 절대 AI가 이길 수 없다던 바둑에서마저 세계 최고의 바둑 기사 이세돌(3D)가 알파고에게 4대1로 완벽히 패배하는 지경에까지 와버렸습니다.
이 모든 끔찍한 광경들을 우리 인간들은 그저 목도할 수 밖에 없었습니다. 그러나 오늘, 드디어 잔혹한 AI와의 피터지는 복수전이 펼쳐집니다. 무려 틱택토로 말이죠.
이번 대결은 총 3번의 대국으로 이루어집니다. 시작은 인간 플레이어가 먼저 하며, AI가 인간을 상대로 얼마나 잘 싸우는지를 확인해볼 것입니다. 다음은 대국 장면입니다.
- 대국 1
- 대국 2
- 대국 3
큰일입니다....ㅋㅋ 인간이 쳐발랐습니다. 틱택토는 가뜩이나 쉬운 게임이라 AI가 하기도 쉬운데 제가 대놓고 공격을 해도 막기는 커녕 이상한 곳에다만 둡니다. 이런 애가 승률 95%라니 기가 차는군요.
학습이 잘 안 된건지 모델 저장을 잘못한 건지 감도 안 잡힙니다. 일단 오늘은 앞에 여러 그래프들을 이용해 비교했다는 것에 의의를 두고... 여기까지만 해보는 게 좋을 것 같습니다. 나중에 에러를 또 고쳐보든가 해야겠네요
'프로그래밍 > 강화학습 (RL)' 카테고리의 다른 글
틱택토 강화학습 (Tik-Tak-Toe RL) - [정보과학융합탐구 - 7월 과제] (0) | 2024.07.09 |
---|---|
틱택토 강화학습 (Tik-Tak-Toe RL) - [정보과학융합탐구 - 6월 과제] (0) | 2024.06.29 |
틱택토 강화학습 (Tik-Tak-Toe RL) - [정보과학융합탐구 - 5월 과제] (4) | 2024.05.31 |
틱택토 강화학습 (Tik-Tak-Toe RL) - [정보과학융합탐구 - 4월 과제] (2) | 2024.04.13 |
틱택토 강화학습 (Tik-Tak-Toe RL) - [정보과학융합탐구 - 3월 과제] (1) | 2024.03.28 |