Unity/Unity ML-Agents

유니티 ML-Agents Episode 종료 및 시작에 대한 고찰

pnltoen 2022. 6. 30.
반응형

Unity ML-Agents

Episode 종료 및 시작에 대한 고찰


ML-Agents는 강화학습을 위해 에이전트가 행동을 통해 에피소드를 진행하고, 특정 조건이 되면 종료 되면서 학습을 진행합니다. 이를 위해 제가 사용하는 메커니즘은 총 3가지입니다.

 

가장 이상적인 방법 - EndEpisode(), OnEpisodeBegin()

 

ML-Agents 예제에서 가장 많이 사용하는 방법입니다. 다음과 같이 특정 조건(미션 실패시) EndEpisode()를 호출하고 OnEpisodeBegin()을 호출하여 환경을 리셋시킵니다. (아래 예제는 3D_Ball입니다)

 

   public override void OnActionReceived(ActionBuffers actionBuffers)
    {
        var actionZ = 2f * Mathf.Clamp(actionBuffers.ContinuousActions[0], -1f, 1f);
        var actionX = 2f * Mathf.Clamp(actionBuffers.ContinuousActions[1], -1f, 1f);

        if ((gameObject.transform.rotation.z < 0.25f && actionZ > 0f) ||
            (gameObject.transform.rotation.z > -0.25f && actionZ < 0f))
        {
            gameObject.transform.Rotate(new Vector3(0, 0, 1), actionZ);
        }

        if ((gameObject.transform.rotation.x < 0.25f && actionX > 0f) ||
            (gameObject.transform.rotation.x > -0.25f && actionX < 0f))
        {
            gameObject.transform.Rotate(new Vector3(1, 0, 0), actionX);
        }
        if ((ball.transform.position.y - gameObject.transform.position.y) < -2f ||
            Mathf.Abs(ball.transform.position.x - gameObject.transform.position.x) > 3f ||
            Mathf.Abs(ball.transform.position.z - gameObject.transform.position.z) > 3f)
        {
            SetReward(-1f);
            EndEpisode(); //에피소드 종료
        }
        else
        {
            SetReward(0.1f);
        }
    }

    public override void OnEpisodeBegin() //환경 리셋
    {
        Debug.Log("OnEpisodeBegin");
        gameObject.transform.rotation = new Quaternion(0f, 0f, 0f, 0f);
        gameObject.transform.Rotate(new Vector3(1, 0, 0), Random.Range(-10f, 10f));
        gameObject.transform.Rotate(new Vector3(0, 0, 1), Random.Range(-10f, 10f));
        m_BallRb.velocity = new Vector3(0f, 0f, 0f);
        ball.transform.position = new Vector3(Random.Range(-1.5f, 1.5f), 4f, Random.Range(-1.5f, 1.5f))
            + gameObject.transform.position;
        //Reset the parameters when the Agent is reset.
        SetResetParameters();
    }

 

Void OnEpisodeBeigin()을 보시면 게임 오브젝트를 초기화해주는 것을 볼 수 있습니다. 위의 예시가 학습 효율 측면에서도 가장 빠르고 널리 알려진 방법입니다. 하지만 만약 환경에 너무 많은 오브젝트가 존재하여 모든 오브젝트를 코드로 리셋시키는 것이 힘들경우 위의 방법은 사용하기가 매우 번거롭습니다.

P.S. EndEpisode 대신 EpisodeInterrupted();를 사용하기도 합니다. 이 경우에는 에이전트의 잘못된 행동(Fault)로 에피소드가 종료된 경우 (예 : MaxSteps) 사용합니다. 전체적인 매커니즘은 환경을 리셋시켜준다는 점에서 EndEpisode()와 동일하지만 학습에 조금 다른 영향을 미칩니다.

 

UnityEngine.SceneManagement를 사용하는 방법

 

주의 : 이 방법은 매우 비효율적입니다.

 

위 방법은 아마 기존 Unity로 게임을 개발하는 분들께서 익숙한 방법이라고 생각됩니다. 간단히 설명드리자면 기존에 저장한 Scene 자체를 불러옴으로써, 환경을 리셋하는 방법입니다.

따라서 OnepisodeBegin() 함수를 작성할 필요가 없고, 복잡한 환경에서 쉽게 강화학습을 적용할 수 있습니다.

이 방법을 사용할 경우, 위에 첨부된 코드와 달리 코드 자체가 매우 단순해집니다.

 

    public override void OnActionReceived(ActionBuffers actionBuffers)
    {
        var actionZ = 2f * Mathf.Clamp(actionBuffers.ContinuousActions[0], -1f, 1f);
        var actionX = 2f * Mathf.Clamp(actionBuffers.ContinuousActions[1], -1f, 1f);

        if ((gameObject.transform.rotation.z < 0.25f && actionZ > 0f) ||
            (gameObject.transform.rotation.z > -0.25f && actionZ < 0f))
        {
            gameObject.transform.Rotate(new Vector3(0, 0, 1), actionZ);
        }

        if ((gameObject.transform.rotation.x < 0.25f && actionX > 0f) ||
            (gameObject.transform.rotation.x > -0.25f && actionX < 0f))
        {
            gameObject.transform.Rotate(new Vector3(1, 0, 0), actionX);
        }
        if ((ball.transform.position.y - gameObject.transform.position.y) < -2f ||
            Mathf.Abs(ball.transform.position.x - gameObject.transform.position.x) > 3f ||
            Mathf.Abs(ball.transform.position.z - gameObject.transform.position.z) > 3f)
        {
            SetReward(-1f);
            EndEpisode();
            ResetAgent(); //추가
        }
        else
        {
            SetReward(0.1f); 
        }
    }

    public void ResetAgent() //추가
    {
        Debug.Log("RESET!!! : ");
        SceneManager.LoadSceneAsync(sceneName); //LoadScene과 LoadSceneAsync 존재
    }

여기까지보면 오... 이 방법이 되게 편해보이는데? 뭐가 비효율적이라는 거지? 할 수 있지만 위 방법은 아래와 같은 단점을 갖습니다.

 

Basic/Scripts/BasicController.cs

코드 내에도 명시되어 있듯이 위 방법은 매우 비효율적입니다. 그 이유에 대해 생각 및 몇가지 실험을 해보았습니다.

첫째 MultiEnviroment Training을 사용하기 힘듭니다. ML-Agents에서 사용할 수있는 2가지 병렬학습 중 하나인 MultiEnvironment Training을 사용할 수 없습니다. 이상적인 방법의 경우 각각의 에이전트가 하나의 환경을 구성하면서 미션에 실패할 경우 Reset이 되는 매커니즘을 갖고 있는데, 이에 반해 SceneManagement의 경우 씬 자체를 리셋시키는 방법으로써 위 방법을 사용하기가 힘듭니다.

(물론 전체 Agent가 미션에 실패할 경우 Reset 시키는 매커니즘을 구현할 수 있겠지만... --num-envs를 활용하는 편이 더 간단하고 효율이 좋아보입니다)

이 방법에서도 Scene을 불러오는 2가지 방법이 있습니다. 하나는 LoadScene이고 또 다른 하나는 LoadSceneAsync 입니다. 원래 이 부분을 "결론만 말씀드리자면 LoadSceneAsync (비동기) 방법을 사용하는게 속도면에서 낫습니다" 라고 포스팅 계획을 잡았는데... TensorBoard를 통한 모니터링 결과 생각과 다른 면이 있어서 결론은 적지 않겠습니다. 강화학습이 환경에 따라 너무 다르기 때문에 아래 내용은 3D_Ball 환경에서는 이렇게 되는구나 하고 넘어가주시면 감사하겠습니다.

환경을 리셋하는 Sync, Async 부분만 다르고 나머지 셋팅은 동일합니다.

 

3DBall_Async

 

3DBALL_Sync

 

환경을 학습할 때 동기(Sync), 비동기(Async) 차이에 따른 영상을 준비하였습니다. 보시면 누구나 Async는 엄청 빠르네...? Async로 해야겠다!!!라고 생각하실 수 있지만 실제 학습 되는 시간을 확인해보니 조금 다른 결과가 나타났습니다.

 

3D_BALL/TensorBoard

 

Sync의 경우 200k 횟수에 100점을 달성하였고 그 이후에는 안정적으로 100점을 받으면서 학습을 진행하였습니다. 그 와 반면 Async의 경우 300k에 만점에 도달하였습니다. 간단한 환경에서도 이정도의 유의미한 차이가 나타나서... 해당 방법을 쓰실 때 동기로 학습을 진행하실지 또는 비동기로 진행하실지는 본인 Custom 환경에 맞춰서 설정해야할 것 같습니다 (학습 시간도 Sync가 더 빠릅니다)

결론 : 유니티 예제에 있는 방법을 사용하는게 가장 좋다. (EndEpisode() & OnEpisodeBegin) 다만 이 방법에서 코드를 작성하기 힘들 경우... Sync를 추천한다!

반응형

댓글