2024년 12월 31일 화요일

[Playground] Lightzero Training 3

 어느덧 2024년이 저물어갑니다.

올해 안에 완성시키고 싶었는데 해를 넘어가게 되었네요.

저번에 랜덤 봇을 확실하게 이기고 나서 거의 다 풀었다고 생각했는데 아직 미완인 것 같습니다.

저번 모델로는 학습이 덜 되어서, 모델을 조금 더 두껍게 하고 돌려보고 있습니다.

residual block을 3개 쌓던 모델에서 이제 5개 쌓은 모델로 돌려보고 있는데요, 25만번 조금 넘게 학습이 돌았는데 현재 스탯은 다음과 같습니다.

랜덤 봇 상대 reward

total loss


랜덤 봇 상대로 한 점수도 뉘엿뉘엿 올라가고는 있는데 애초에 타겟했던 20을 넘기지는 못했습니다.

residual block 3개 쌓았을 때는 total_loss가 이미 수렴해버린 상태에서도 성능이 잘 나오지 않아서 블록을 좀 더 두껍게 쌓고 돌려보고 있습니다.

다만 이것도 슬슬 total_loss가 수렴해가는 느낌이 드는데 아직 성능이 그리 좋지 못한 것을 보면 좀 아쉽긴 하네요.

다시 디버깅을 해야하는데, 학습 스탯들을 보면서 어디가 문제인지 진단을 좀 해보려고 합니다.

아직까지는 정확한 원인은 모르겠는데 뭔가 미심쩍은 부분은 있습니다.

collector stat & iteration result

self-play를 하면서 데이터를 수집하고, 그걸 바탕으로 학습을 하게 되는데 collector 스탯을 보면 reward_max는 19인데 반해 reward_min은 -2입니다. 선 플레이어 기준으로 리워드가 계산되기 때문에, 이는 선공이 압도적으로 많이 이기고 있다는 뜻이 됩니다. (mean도 9 언저리죠)

학습이 잘 되고 있다면, reward_std가 줄면서 선공과 후공 중 유리한 쪽으로 살짝 bias가 있는 점수가 나오고, 결국 어느 정도로 수렴하는게 자연스러울 것 같은데 시나리오별로 결과 편차도 크고 선공이 훨씬 득점을 많이 하고 있는 양상입니다.

아직 학습 수렴 단계가 아니어서 속단하기는 이르지만 만약 이번에도 학습이 잘 되지 않는다면 여기부터 뜯어볼 계획입니다. collector에서 후공이 action하는 매커니즘이 뭔가 꼬여서 제대로 된 곳에 돌을 놓지 못한다면 선공의 학습 또한 제대로 이루어질 수 없기 때문입니다.

학습은 계속 맥미니로 돌리고 있는데 모델이 두꺼워지니까 확실히 한계가 느껴집니다..

모델이 두꺼워져서 거의 11일 넘게 돌려놓았는데 아직도 수렴 판정을 못하겠네요..ㅋㅋ

맥미니 올해 참 고생많았는데 새해에는 좀 더 빠릿빠릿한 친구를 들여와서 일을 시켜야하겠습니다.

최근에는 MusicGen이나 MERT 등등 음악 생성 및 임베딩 관련된 모델들을 살펴보고 있는데, 조만간 페이퍼 리뷰든 모델 리뷰든 업데이트를 해보겠습니다.

2025년도 화이팅!

2024년 12월 10일 화요일

[Playground] Ligthzero Training 2

 지난 포스팅 이후 오래도 걸렸네요.

이것저것 바꿔보고 고쳐보며 돌려보던 끝에 드디어 학습이 되기 시작해서 학습 끝까지 돌리는 동안 글을 적어볼까 합니다.

그동안 거의 코드단을 안뜯어본 곳이 없을 정도로 샅샅이 수색하고 용의자를 찾아나섰는데, 결과적으로는 여러 가지 요인들이 맞게 수정되어 이제 학습이 되기 시작한 것 같아요.

일단 오델로 환경에서 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

아직 완벽하게 학습된 모델은 아니지만, 그래도 계속 고생하고 있던 미해결 문제의 실마리를 풀었습니다. 

이제 작동하는 베이스라인을 확보했으니, 일정 수준까지 성능을 올려보는 것은 그리 어렵지 않아보입니다.

다만 학습에 너무 시간이 오래 걸려 마음껏 테스트해보지 못하는 것은 좀 아쉽네요.

이제 랜덤 봇은 얼추 이기는 것 같으니, 다음 목표는 저를 이기는 것으로 해야겠습니다.

그럼 다음 포스팅으로 돌아오겠습니다.

2024년 10월 2일 수요일

[Playground] Lightzero training 1

 저번에 이런저런 실험을 통해 빠르고 효율적으로 학습을 할 수 있는 환경을 세팅해두었으니, 이제 제대로 학습을 시켜볼 차례다.

저번에 오델로를 돌릴 수 있도록 env를 만들어주고, 기타 cython 코드들 빌드 후 잘 돌아가는 것도 확인했었는데 뭔가 결과가 계속 이상하게 나와서 점검이 필요하다는 생각은 계속 하고있었다.


이상하다고 진단한 이유는 랜덤 액션을 취하는 봇 상대로 무승부가 나오기 힘든데 무승부의 비중이 너무 높은것 + 승리 비중이 너무 작은것이다.

승리 비중은 그럴 수 있다 쳐도 (모델이 계속 한쪽의 액션만 취하도록 뱉으면 그럴 수 있다고 생각함) 랜덤 상대로 무승부가 이렇게 자주 나올 수 있나 싶어서 점검을 해주었다.

결론은 env 자체 (step시 보드 변경 / legal_action 서치 / winner 및 terminate 여부 판정)에는 문제가 없는데 이걸 품고 돌리는 training단 코드에 이상한 부분이 있었다.

lightzero에 포함된 다른 보드게임들 코드를 참조하여 env만 새로 짜주었는데, 이게 룰이 다르다보니 오델로를 플레이할 때 수정해주어야 하는 부분이 있었다.

틱택토 / connect4 등 코드를 수정해서 새로 짜주었는데 오델로는 이 게임들과 다른 점이 2가지 있어서 모체코드에서 수정이 필요했던 것이다...

1. 틱택토 / connect4의 경우 착수한 플레이어만 승리가 가능하기에, 착수 시 승패를 판정하고 만약 승리 조건을 달성하지 못했는데 legal_action이 없는 경우 무승부 판정

=> 가장 큰 차이는 여기에서 기인했다. 오델로는 대체로 64칸이 꽉 찼을 때 게임이 종료되기에 마지막 수를 착수한 플레이어도 패배할 수 있다. 기존 코드에서는 착수시 착수 플레이어의 승리 여부를 체크한 뒤 착수 플레이어가 승리하지 못했으면 무승부로 판정해서 자꾸 플레이어2가 패배한 경우 training 단에서 무승부라고 판단해서 리워드를 주고 있던 것이었다..

2. 틱택토 / connect4의 경우 게임이 종료되지 않았으면 legal_action이 있어서 턴이 넘어가는 경우가 없음

=> 오델로는 한 플레이어가 legal_action이 없으면 상대 턴으로 넘어가기 때문에, evaluation 과정에서 단순히 턴 기준으로 플레이어를 판단하도록 짜여져 있는 코드가 있었는데, 이 부분의 수정이 필요했다.

이렇게 수정하고 돌려보니, 이제 적어도 evaluation 단에는 문제가 없는 것을 확인했다.

그리고 설레는 마음으로 학습을 돌렸는데...

윽 아직 뭔가 디버깅할 부분이 남아있는 것 같다 - 랜덤 봇 상대로도 잘 이기지 못하는 모습이다

evaluation에는 이제 문제가 없는걸 확인했으니 training 단을 뜯어봐야 할 것 같다.


한 7~8000번까지는 오르는 모습이어서 기대하고 잤는데 웬걸, 그냥 우연이었다.

legal_action이 없으면 턴이 넘어가는 조건이 기존 코드와 호환되지 않는 가장 큰 이유인 것 같은데, 자세한 내용은 좀 더 뜯어봐야 할 것 같다.

같이 오델로 봇을 만드는 친구가 있는데, 10월 15일에 중간고사 끝나면 배틀을 하기로 했으니 due가 생겨서 좀 더 열심히 하게 되는 것 같다ㅋㅋㅋ