지난 포스팅 이후 오래도 걸렸네요.
이것저것 바꿔보고 고쳐보며 돌려보던 끝에 드디어 학습이 되기 시작해서 학습 끝까지 돌리는 동안 글을 적어볼까 합니다.
그동안 거의 코드단을 안뜯어본 곳이 없을 정도로 샅샅이 수색하고 용의자를 찾아나섰는데, 결과적으로는 여러 가지 요인들이 맞게 수정되어 이제 학습이 되기 시작한 것 같아요.
일단 오델로 환경에서 20만번 iteration을 돌았고, 현재 학습 스탯은 이렇습니다.
 |
| [Fig 1] evaluation result |
요게 가장 최근 evaluation 결과인데요, 랜덤 봇을 상대로 10전 전승을 거두었습니다. (리워드 양수)
아직 수렴이 완벽하게 되지는 않아서 가끔 한두판 지긴 합니다만 시간 지나면 나아질 것 같습니다.
아래와 같이 텐서보드를 보면 학습 상황을 좀 더 정확히 알 수 있습니다.
 |
| [Fig 2] evaluation history |
이건 모델의 리워드 히스토리인데요, 1000번 학습마다 랜덤 봇을 상대로 배틀을 한 리워드를 기록한 것입니다.
약 5만번 학습 이후부터 본격적으로 학습이 되기 시작해 변동하면서 리워드가 계속 느는 모습을 볼 수 있습니다. 증가세가 좀 더뎌지긴 했습니다만 자연스러운 현상이라고 생각합니다.
대략 20~25 정도가 최종 수렴위치가 아닐까 싶습니다. 이에 관해서는 아래에 후술하겠습니다.
 |
| [Fig 3] training loss history |
위는 매 100 iter 마다의 training loss를 기록한 것입니다. 아직 예측오차가 감소중인 것으로 보아 모델 성장의 여지가 좀 남아있어 보입니다. 아마 여기서 수렴하고 나면 디테일한 수정을 바꿔서 마지널한 개선을 노려볼 것 같아요.
초반에 예측오차가 확 감소했다가 점차 늘었다가 다시 떨어지는 것은 모델이 self-play를 통해 데이터를 계속 생성하기 때문입니다. 매 학습마다 데이터셋에서 512개의 샘플을 뽑아 학습에 사용하기 때문에, 초반에는 데이터 샘플이 적어서 같은 샘플이 계속 학습에 사용되어 예측오차가 적습니다. unseen 데이터가 생기는 속도 vs 모델의 학습속도 차이로 위와 같은 그래프가 나타나게 되는데, 물론 replay buffer size 등 파라미터 세팅에 따라 개형이 좀 달라질 수 있습니다.
매 25 iter 마다 8번의 self-play를 진행하니 대략 6만4천 게임 정도가 진행되었겠네요. 이렇게 보니 생각보다 적은 게임수로 꽤 높은 성적을 거두는 것 같아 신기하기도 합니다.
아무튼 현재 학습 상황을 요약하면
랜덤봇은 높은 확률로 이긴다 + 아직 성장의 여지가 많이 남아있다
정도겠습니다.
이제 그럼 이번 포스팅의 진짜 목적인, 모델의 업그레이드 내역을 좀 기록해보겠습니다.
우선, Muzero 모델의 작동원리를 간단하게 살펴봅시다.
Muzero 모델의 학습은 크게 다음과 같은 두 단계로 이루어집니다.
1. self-play => data 생성
2. 데이터셋에서 샘플 랜덤 추출 + train
각 단계에서 모델의 추론 과정은 다음과 같은 단계로 구성됩니다.
1. 보드 상태를 받아서 모델이 이해하는 latent space로의 변환
2. 변환된 latent space 내에서 가상의 액션을 두면서 보드 변화 시뮬레이션
3. 시뮬레이션 된 값 중 최선의 액션 선택
아주 간단하게 정리하면 위 정도로 요약되겠습니다.
업그레이드/버그픽스 내역은 대부분
2. 변환된 latent space 내에서 가상의 액션을 두면서 보드 변화 시뮬레이션
이 항목에서 이루어졌는데요, 하나씩 짚어보겠습니다.
매 업그레이드가 옵션화된게 아니라 무조건 추가되면서 진행된 것이기에 확실하게 알 수 있는 것은 '현재 세팅에서는 학습이 이루어진다' 정도이고, 정확히 어떤 업데이트가 주효했는지는 확실하게 알 수 없습니다. 업데이트가 이루어질 때마다 성능이 개선된 게 아니라 아예 작동하지 않다가 이제 막 작동하기 시작한 거라서요.
다만 제 추측으로 주효했던 업데이트는 크게 4가지이고, 하나는 버그픽스 / 나머지 셋은 업그레이드 & 파라미터수정 입니다.
1. MCTS 서치 과정에서의 버그픽스
저번 수정에서 legal_action이 존재하지 않을 때 턴이 스킵되는 과정에서의 버그를 수정했는데요, 결론적으로는 이것만으로 부족했고 MCTS 서치할 때에도 이게 반영되지 않는 문제가 있었습니다.
현재 모델은 보드를 받아서 가능성 높은 가상의 시나리오를 50개 생성합니다. 그런데 이 가상의 시나리오를 생성하는 과정에서 턴이 스킵되는 것이 반영되지 않아서, A가 액션하고 또 A가 액션하는 경우 이때 받는 리워드를 A에게 지급해야하는데 시나리오상으로 B에게 지급해버리는 문제가 발생했습니다.
처음에는 트리서치 내부에서 수정을 하려고 했으나, 트리의 각 노드에 필요 정보를 기록 / 체크 / 수정해주는게 생각보다 복잡한 문제여서 조금 다른 방식으로 해결했습니다.
아예 오델로 Env를 수정하는 방식으로 바꾸었는데요, 애초에 턴이 스킵되는 개념을 삭제함으로써 트리서치 내부에서 무조건 착수 플레이어가 바뀐다고 가정해도 이상없도록 만들었습니다.
legal_action이 존재하지 않을 경우에만 활성화되는 패스라는 액션을 추가하여 총 액션 스페이스를 64 + 1 = 65개, game max length를 60 고정이 아니라 패스가 여러번 나와도 지장없도록 80 정도로 늘려주었습니다.
턴스킵은 오델로 플레이 과정에서 꽤 빈번하게 일어나는 일이라 이 버그는 학습에 치명적입니다. 이걸 발견하고 나서 아 이제 뭔가 학습이 되겠구나 하는 기대를 많이 품었던 것 같네요ㅋㅋ
2. ssl_loss 추가
Muzero 모델은 추론 과정에서 총 3개의 뉴럴넷을 사용합니다.
a. 보드를 latent space 상으로 옮겨주는 representation network
b. 가상 시뮬레이션 과정에서 액션과 보드 상태를 시뮬레이션해주는 dynamics network
c. 현재 보드의 latent space를 보고 policy와 value를 뱉어내는 prediction network
train 과정에서는 위 3개의 network 학습이 모두 잘 이루어져야 하는데, dynamics와 prediction network는 필수고, representaion network는 나머지 두 네트워크의 학습 과정에서 자연스럽게 학습되기도 하고, 아예 supervised로 값을 넣어줄 수도 있습니다.
저는 학습 과정에서 여러 파라미터를 디폴트 세팅값을 사용하고 있었는데, 디폴트 세팅이 representation network의 타겟을 직접 넣어주는게 아니라 나머지 두 네트워크를 backpropagate하는 과정에서 자연스럽게 피팅되도록 되어있더라구요..
이걸 lightzero-muzero에서는 self-supervised learning loss라고 부르는데, 이 가중치 값이 0으로 세팅되어 있었습니다.
ssl_loss를 피드백 과정에 추가하면 연산이 꽤 늘어나기 때문에 학습 시간이 늘어납니다. 그래서 간단한 모델의 경우 이 값을 0으로 사용해도 무방하지만, 오델로는 꽤 복잡한 문제이기 때문에 ssl_loss를 피팅하지 않으면 학습이 원활하지 않을 수 있다고 생각해 이 값을 올려주었습니다.
3. 리워드 지급 방식의 변경 + support vector scale 수정
lightzero에서 현재 제공하는 보드게임은 connect4 (4목과 비슷)와 틱택토입니다. 저도 오델로를 만들 때 이 보드게임들의 env를 참조하였는데요, 이 두 env는 게임이 종료되기 전까지는 모든 액션에 0의 리워드를, 게임 종료시 승리측에 1, 패배측에 -1, 비긴 경우 0을 지급합니다.
다만 저는 이 세팅이 조금 이상하다고 느낀게, 이렇게 할 거였으면 dynamics network의 reward 차원을 3으로 세팅하는게 맞기 때문입니다.
muzero에서는 안정적인 학습을 위해 phi_transform이라는 피쳐가 사용되는데, 이는 리워드를 마치 one-hot encoding 혹은 softmax와 비슷하게 categorical로 치환하여 학습하는 방법입니다.
만약 리워드가 1이면 [0 0 1], -1이면 [1 0 0] 이런 식으로 치환하여 뱉도록 하는 방법인데요, 이처럼 스칼라값을 타겟으로 하지 않고 categorical로 바꾸어 뉴럴넷을 학습시키는 것은 딥러닝에서 다양한 형태로 널리 차용되는 방식입니다.
그런데 의아한 점은 보드게임의 학습 콘피그에 이 세팅값이 21로 들어가있다는 점이었습니다. 어차피 리워드가 -1 ~ 1이라서 무조건 3차원 안에 들어가는데, 굳이 이 세팅값을 늘릴 이유가 있나 하는 의문이 있었습니다. 물론 뭐 더 크게 넣는다고 해서 학습이 되지 않는 것은 아니지만요.
아마 저자가 lightzero를 다양한 모델을 하나의 프레임워크 위에서 작동하도록 하는 과정에서 콘피그 세팅이나 모델의 정합성 등이 고려가 제대로 되지 않은 것 같아요.
무튼 그것과 별개로, 풀어야 하는 문제가 해당 게임들보다는 훨씬 복잡하기 때문에 학습을 좀 빨리 안정화시키고자 리워드를 [최종 돌의 개수 - 32]로 수정하였습니다.
[Fig 1] 에서 평균 리워드가 10인데요, 이 말인 즉슨 평균을 내면 랜덤 봇 대상으로 42 : 22로 승리했다는 것을 알 수 있습니다.
오델로는 실력 차이와 돌 개수 차이가 완전 정비례하는 것은 아닐뿐더러 아무리 잘해도 50 중반을 넘어가기가 어렵습니다. 대략 47~8 이후부터는 내가 얼마나 잘하느냐가 아니라 상대가 얼마나 못하느냐가 최종 스코어에 더 영향을 많이 미치는 것 같아요.
따라서 지금 생각으로는 랜덤 봇 대상으로 50 중반 정도 꾸준히 먹으면 어느 정도 통달했다고 볼 수 있을 것 같아서 처음에 20~25 정도를 최종 수렴위치로 잡은 것입니다. 물론 이는 감을 잡기 위한 대략적인 수치이고 최종 모델을 선정할 때는 iteration별 모델별로 게임을 진행해서 elo rating을 측정하던가 할 것 같아요.
4. Replay buffer sizeup
이건 마이너한 수정에 가깝기는 한데, self-play한 데이터를 저장하는 버퍼의 사이즈를 늘려주었습니다.
사실 기존에 쓰던 수치는 너무 작고, 새로 세팅한 수치는 너무 큰 것 같아서 다음 학습 때는 그 중간 정도로 사용할 생각입니다.
모델이 self-play를 통해 모은 게임 데이터는 replay buffer라는 저장소에 쌓이게 되는데, 무한정 저장하지는 않고 일정 사이즈를 정해두고 최신 데이터만 남기고 예전 것들을 지워버립니다.
이렇게 하는 이유는 너무 오래된 데이터는 성능이 떨어지는 예전 모델의 policy 및 value를 타겟으로 삼고 있어서 오히려 학습에 방해가 되기 때문입니다.
그렇다고 이 사이즈를 너무 작게 설정하면, 모델이 충분한 데이터를 학습하지 못해 안정적으로 수렴하지 않고 계속 빙빙 돌 가능성이 있습니다.
기존에 사용하던 값은 총 20만 transition -> 약 3400 게임 정도를 저장할 수 있는 버퍼인데, 이게 너무 부족했던 것 같습니다.
그래서 어차피 학습이 아예 안되는거 리플레이 버퍼를 용의선상에서 지우려고 일단 6백만 transition -> 약 10만 게임 정도로 세팅해두었는데 지금 학습 양상을 보니 좀 더 줄이는게 맞지 않나 싶네요.
느낌으로는 한 3만 ~ 4만 게임 정도를 저장하는 것이 좋아보입니다.
Summary
아직 완벽하게 학습된 모델은 아니지만, 그래도 계속 고생하고 있던 미해결 문제의 실마리를 풀었습니다.
이제 작동하는 베이스라인을 확보했으니, 일정 수준까지 성능을 올려보는 것은 그리 어렵지 않아보입니다.
다만 학습에 너무 시간이 오래 걸려 마음껏 테스트해보지 못하는 것은 좀 아쉽네요.
이제 랜덤 봇은 얼추 이기는 것 같으니, 다음 목표는 저를 이기는 것으로 해야겠습니다.
그럼 다음 포스팅으로 돌아오겠습니다.