메뉴
HN
Hacker News 14일 전

슈퍼 마리오 JEPA 세계 모델: 구현과 한계점

IMP
7/10
핵심 요약

얀 르쿤이 제안한 JEPA 아키텍처를 슈퍼 마리오 브라더스 게임 환경에 맞춰 처음부터 직접 구현하고 학습시킨 기술 사례입니다. 모델이 단기적인 미래 예측은 훌륭하게 수행했지만, 장기적인 목표 달성이나 복잡한 장애물 통과라는 본질적인 게임 진행에는 실패하여 예측과 진행의 차이를 보여줍니다. 게임 내 행동 제어를 위해 Adaptive LayerNorm(AdaLN-Zero)을 적용하는 등 모델 설계의 기술적 디테일을 깊이 있게 다루고 있습니다.

번역된 본문

‹ 프로젝트로 돌아가기 LeMario: JEPA 모델로 학습시킨 슈퍼 마리오 브라더스 벤자민 바이 (Benjamin Bai) · 2026년 7월 · LeWorldModel 논문 · 깃허브(GitHub) · X(구 트위터)

저는 픽셀과 행동(액션)으로부터 세계의 동역학(world dynamics)을 학습하는 작은 결합 임베딩 예측 아키텍처(JEPA)인 LeWorldModel을 재현해 보고 싶었습니다. 원래 논문에서는 이를 Push-T 과제의 보상 없는 계획(Reward-free planning)에 사용했습니다. 하지만 저는 비디오 게임을 사랑했고, 동시에 얀 르쿤(Yann LeCun)의 JEPA 아키텍처에 대해 더 깊이 이해하고 싶었기 때문에, 전체 아키텍처를 처음부터 직접 작성하여 슈퍼 마리오 브라더스(Super Mario Bros)로 학습시키기로 결정했습니다.

이 모델은 제가 처음에 중요하다고 생각했던 모든 테스트를 통과했습니다. 이 모델은 평가용으로 제외된 에피소드(held-out episodes)로 일반화되었고, 행동(액션)을 사용했으며, 강력한 베이스라인(baseline)보다 5단계 더 먼 미래를 잘 예측했습니다. 단순한 보상 없는 계획(Raw reward-free planning)만으로도 마리오를 근처의 이미지 목표지점으로 이동시킬 수 있었고, 목표물의 2~5픽셀 이내에 도달하게 할 수 있었습니다. :D

잠깐 동안은 이 모델이 게임하는 법을 배운 것처럼 보였습니다. 그러다 제가 목표를 레벨의 더 먼 곳으로 옮겼습니다... 마리오는 첫 번째 주요 장애물을 안정적으로 넘거나 먼 거리의 단일 목표 이미지 쪽으로 탐색할 수 없었습니다. 모델은 '게임을 예측하는 법'을 학습했을 뿐, 게임을 '진행하는 법'을 학습한 것은 아니었습니다. D:

이 글은 기술 연재이자 사후 분석(postmortem)입니다. 제가 무엇을 만들었고, 어떻게 테스트했으며, 어떤 실수를 저질렀고, 점진적으로 실제 문제를 드러낸 실험들은 무엇이었는지 다룹니다. (대부분의 교훈들은 돌이켜 보면 너무나 당연해 보이네요 T^T)

전체 아키텍처 각 수식을 개별적으로 소개하기 전에, 전체 기계가 어떻게 생겼는지 한눈에 보는 것이 이해에 도움이 됩니다:

초록색 경로부터 시작해 보겠습니다. 각 학습 샘플에는 4개의 마리오 프레임이 포함됩니다. 비전 인코더(Vision Encoder)는 모든 프레임을 192개의 숫자로 이루어진 '잠재 표현(latent)'이라고 불리는 형태로 압축합니다: z_t = E_θ(x_t), z_t ∈ R^192 잠재 표현(latent)은 스크린샷에 대한 모델만의 고유한 설명이라고 생각하시면 됩니다.

빨간색 경로에는 컨트롤러 입력이 포함됩니다. 각 관측(observation) 쌍은 5개의 에뮬레이터 프레임으로 구분되며, 모든 프레임에는 6가지의 가능한 버튼 상태가 포함됩니다: frames: [batch, 4, 3, 224, 224] actions: [batch, 4, 5, 6] # 왼쪽, 오른쪽, 위, 아래, A, B

액션 인코더(Action Encoder)는 5 x 6 버튼 시퀀스를 192개의 숫자로 이루어진 또 다른 벡터로 압축합니다. 그런 다음 프레임과 액션 잠재 표현(latent)은 인과적 예측기(Causal Predictor)로 전달됩니다. 이 예측기의 역할은 "이전 프레임의 생김새와 눌린 버튼이 주어졌을 때, 다음 프레임의 잠재 표현은 어떤 모습이어야 하는가?"라는 질문에 대답하는 것입니다.

예측기에는 6개의 트랜스포머 블록(Transformer block)이 포함되어 있습니다. 각 프레임은 이전 프레임들에 주의(attention)를 기울이게 됩니다. 하지만, 여기에 액션은 어떻게 주입할 수 있을까요? 행동(액션)은 Adaptive LayerNorm Zero (AdaLN-Zero)를 통해 이러한 트랜스포머 블록에 들어옵니다. 단순히 프레임 벡터에 액션 벡터를 붙이는 대신, AdaLN-Zero는 각 액션을 세 가지 종류의 제어 신호로 변환합니다:

• Shift (이동): 프레임 특징(feature)에 액션에 따른 오프셋(offset)을 더합니다. • Scale (스케일): 특정 특징들의 영향력을 키우거나 줄입니다. • Gate (게이트): 트랜스포머가 현재 상태를 얼마나 강하게 업데이트할지 제어합니다.

이것들이 트랜스포머 블록에 어떤 영향을 미칠까요? 일반적으로 우리가 아는 일반적인 어텐션(attention)은 각 프레임에 이전 문맥을 제공한 다음, 피드포워드(Feedforward, MLP)를 통과시켜 해당 정보를 종합합니다. 하지만 AdaLN은 어텐션과 피드포워드 두 단계 모두를 액션에 따라 수정합니다. 예를 들어, '점프'라는 액션은 수직 이동과 관련된 잠재 특징(latent feature)의 스케일을 키우고 점프를 예측하는 데 덜 중요한 특징의 스케일은 줄일 수 있습니다. Shift는 정규화된 특징들을 다른 액션 종속적인 기준선(baseline)으로 이동시킵니다. 마지막으로, 게이트(gate)는 어텐션이나 MLP의 업데이트가 예측된 상태에 얼마나 강하게 영향을 미칠지 결정합니다.

이러한 제어 신호들은 어텐션 및 MLP 브랜치에 대해 각각 개별적으로 생성되므로, 블록에는 총 6개의 값이 주어집니다. 즉, 각 브랜치에 대해 shift, scale, gate가 하나씩 주어집니다. 이름에 포함된 "Zero"는 이 값들의 가중치가 0에서 시작한다는 의미입니다. 따라서 예측기는 초기에 무작위 액션 효과 없이 시작하여 훈련 과정을 통해 어떤 게이트를 열어야 할지 점차 학습하게 됩니다.

이제 학습 중에 6개의 트랜스포머 블록을 거친 후에는, 작은 투영 헤드(projection head)가 3개의 사전 값을(pre-) 생성합니다...

원문 보기
원문 보기 (영어)
‹ back to projects LeMario: Super Mario Bros trained on a JEPA Model Benjamin Bai · July 2026 · LeWorldModel paper · GitHub · X I wanted to reproduce LeWorldModel , a small Joint-Embedding Predictive Architecture (JEPA) that learns world dynamics from pixels and actions. The original paper used it for reward-free planning in Push-T. But, since I loved video games, and at the same time wanted to learn more deeply about LeCun's JEPA architecture, I decided to write the whole architecture from scratch and train it on Super Mario Bros. The model passed every test I initially thought mattered. It generalized to held-out episodes, used the actions, and predicted five-step futures better than strong baselines. Raw reward-free planning could move Mario toward nearby image goals and finish within two and five pixels of the targets. :D For a moment, it looked like the model had learned to play. Then I moved the goal farther into the level... Mario could not reliably jump over the first major obstacle or navigate toward a single distant goal image. The model had learned to predict the game, but that did not mean it had learned how to make progress through it. D: This post is both a technical walkthrough and a postmortem, what I built, how I tested it, the mistakes I made, and the experiments that gradually exposed the real problem. (Most of the lessons seem obvious in hindsight T^T ) The whole architecture Before introducing each equation separately, it helps to see the whole machine at once: Let’s start with the green path. Each training sample contains four Mario frames. The vision encoder compresses every frame into a 192-number representation called a latent ( z z z ): z t = E θ ( x t ) , z t ∈ R 192 z_t = E_\theta(x_t), \qquad z_t \in \mathbb{R}^{192} z t ​ = E θ ​ ( x t ​ ) , z t ​ ∈ R 192 You can think of a latent as the model’s private description of a screenshot. The red path contains the controller inputs. Each pair of observations is separated by five emulator frames, and every frame contains six possible button states: frames: [batch, 4, 3, 224, 224] actions: [batch, 4, 5, 6] # Left, Right, Up, Down, A, B The action encoder compresses each 5 × 6 button sequence into another 192-number vector. The frame and action latents are then piped into the causal predictor . Its job is to answer: Given what the previous frames looked like and which buttons were pressed, what should the next frame’s latent look like? The predictor contains six transformer blocks. Where each of the frames will attend to the previous frames. But however, how would we inject action? The actions enter these transformer blocks through Adaptive LayerNorm Zero (AdaLN-Zero). Rather than simply attaching the action vector to the frame vector, AdaLN-Zero turns each action into three kinds of controls: Shift: adds an action-dependent offset to the frame features Scale: turns particular features up or down. Gate: controls how strongly the transformer updates the current state. Now how do these affect the transformer block? Usually our normal attention would give each frame its previous context then pass it through the feedforward (MLP) to synthesize that information, however AdaLN modifies both stages according to the action. For example, a jump action might scale up latent features related to vertical motion and scale down features that matter less for predicting the jump. Shift moves the normalized features toward a different action-dependent baseline. Finally, the gate decides how strongly the attention or MLP update should affect the predicted state. These controls are produced separately for the attention and MLP branches, giving the block six values in total: a shift, scale, and gate for each branch. The “Zero” just means their weights begin at zero, so the predictor starts without random action effects and gradually learns which gates to open during training. Now, during training after the six transformer blocks, a small projection head produces three predicted future latents: z ^ 1 , z ^ 2 , z ^ 3 \hat z_1,\hat z_2,\hat z_3 z ^ 1 ​ , z ^ 2 ​ , z ^ 3 ​ These are compared with the latents produced by the three real next frames: L pred = MSE ⁡ ( [ z ^ 1 , z ^ 2 , z ^ 3 ] , [ z 1 , z 2 , z 3 ] ) \mathcal L_{\text{pred}} = \operatorname{MSE} \left( [\hat z_1,\hat z_2,\hat z_3], [z_1,z_2,z_3] \right) L pred ​ = MSE ( [ z ^ 1 ​ , z ^ 2 ​ , z ^ 3 ​ ] , [ z 1 ​ , z 2 ​ , z 3 ​ ] ) Now we just want to lower this loss to 0... but clearly there is one easy way for the model to cheat, we can just make all the latent vectors the same! Prediction would become perfect because Mario, a pipe, a new world would all look identical (representation collapse). So to prevent it from cheating, we use SIGReg 1 ! It prevents this collapse by encouraging the real frame latents to remain varied and informative. So our new loss function becomes: L = L pred + 0.1 L SIGReg \mathcal L = \mathcal L_{\text{pred}} + 0.1\mathcal L_{\text{SIGReg}} L = L pred ​ + 0.1 L SIGReg ​ So that's the whole architecture! Now moving on to the actual results. But did it Actually Learn? LeMario trained on 737,134 frames from 280 episodes across 32 Mario levels. To verify that the model has learned we couldn't just look for a lower loss. Lower loss was not enough to prove it had learned dynamics, nearby frames often look so similar that predicting “nothing changes” is a strong baseline. On held-out episodes, I compared LeMario with that persistence baseline and with the real frame history paired with shuffled actions: Method One-step error Five-step error LeMario 0.013773 0.077717 Predict no change 0.014472 0.142473 Shuffle the actions 0.016555 0.114648 Shuffling the actions raised one-step error by 20.2%. Across five recursive steps, LeMario beat persistence by 45.5%, while shuffled actions were 47.5% worse. The farther it predicted, the more the buttons mattered. LeMario had learned short-horizon Mario dynamics conditioned on the player’s actions! Letting it touch the controller Now for the fun part. Once the model can imagine futures, we can search through those futures and let it choose what Mario should do. Searching through imagination Now to turn the model that only predicts a frame ahead given an action, into something that could predict multiple actions and steps into the future, I use the Cross-Entropy Method! Given a current image x t x_t x t ​ and goal image x g x_g x g ​ , the encoder produces z t z_t z t ​ and z g z_g z g ​ . CEM then: Samples hundreds of action sequences. Rolls each sequence forward through LeMario. Scores the predicted final latent against z g z_g z g ​ . Keeps the best candidates. Resamples around them and repeats. CEM found action sequences with predicted goal distances far below random candidates. I had a model that could imagine, an optimizer that could search its imagination, and a goal image that required no reward engineering. It was magnificent. Then Mario barely moved I began with a tiny goal. Mario started at x=40 ; the goal frame showed him at x=72 . Raw JEPA+CEM ended at x=44 . In layman's terms, it sucked. At this point I did not know which part had failed. Maybe the predictor was wrong, maybe CEM was broken, or maybe the encoder had ignored Mario entirely. I needed to start with the simplest question, did those 192 numbers even contain Mario’s position? What is inside 192 numbers? The latent is only 192 numbers. I needed a way to ask what information was inside without changing the encoder. Did it forget Mario? I froze the JEPA and trained a small probe 2 to recover Mario’s emulator coordinates from its latent. Because the encoder could not change, any position the probe recovered had to be information the JEPA had already learned. horizontal position: MAE = 9.30 px, R² = 0.997 vertical position: MAE = 21.62 px, R² = 0.188 The probe worked! Mario’s horizontal position was almost perfectly recoverable. Vertical state was much weaker, but the encoder had clearly learne