초보자부터 전문가까지, 자신만의 AI 게임을 만들 수 있는 최종 워크플로우
'추천 주제 보기' 버튼을 눌러 영감을 얻고, 상세 기획안을 요청하는 프롬프트를 복사하세요.
You are a creative game designer. Based on the theme "[여기에 선택한 게임 주제와 간단한 설명을 입력하세요.]", please develop a detailed and engaging game plan with these four sections: Game Concept, Player Objective, Controls, Win/Loss Conditions. Write in Korean.
1단계에서 생성된 기획안을 아래 입력란에 붙여넣으면, AI 웹 게임 생성을 위한 프롬프트가 자동으로 완성됩니다.
You are an elite game developer, a master of Matter.js, and an expert in creating robust, AI-ready web games. Your task is to write the complete code for a single, self-contained `index.html` file based on the provided game plan.
**Game Logic Plan:**
[여기에 1단계에서 생성된 게임 기획안을 붙여넣으세요.]
**CRITICAL JavaScript Implementation Requirements:**
1. **Game Modes & State:** Must support `USER`, `AI`, `TRAINING` modes. Use `sessionStorage` to remember the current mode after a refresh, automatically re-entering `TRAINING` mode if it was active. Provide clear UI indicators for the current mode.
2. **Reinforcement Learning Deadlock Prevention:**
* In `USER` and `AI` modes, the Matter.js `Runner` must be enabled (`runner.enabled = true`) for smooth, real-time gameplay.
* **Crucially, upon entering `TRAINING` mode, the automatic `Runner` MUST be disabled (`runner.enabled = false`).**
* In `TRAINING` mode, the physics simulation must only advance a single step via a manual call to `Engine.update(engine, 1000 / 60)` from within the `stepGameForTraining` function. This gives the Python RL agent absolute control over the game loop.
3. **WebSocket Interface & Strict RL Protocol:**
* Implement `connectWebSocket()` to connect to `ws://localhost:8765`.
* The interface must handle `reset` and `action` commands.
* **Strict RL Protocol:**
* The `resetForTraining()` function must ONLY send back the initial observation packet: `{observation: getObservation()}`.
* The `stepGameForTraining(action)` function must send the complete data packet: `{observation, reward, done}`.
4. **Robust Asynchronous Model Loading & State Handling:**
* The ONNX model must be loaded asynchronously via a dedicated `loadOnnxModel` function.
* Implement a state flag (e.g., `isModelReady = false`). This flag must be set to `true` **only after** the `ort.InferenceSession` object is created AND its internal metadata (e.g., `ortSession.inputNames`) is confirmed to be fully available.
* The AI decision-making loop (`setInterval`) **must not be initiated** by default. It should only be started by the `setGameMode('AI')` function, and **only if `isModelReady` is already `true`**.
* If `setGameMode('AI')` is called *before* the model is ready, `loadOnnxModel` has the final responsibility to check the current game mode upon completion and start the AI decision loop itself. This dual-check mechanism robustly handles all race conditions.
* The UI must clearly indicate the model's status (`Loading...`, `Loaded`, `Failed`).
5. **Decoupled AI Inference Loop:**
* For the real-time `AI` mode, model inference (the "brain") must be decoupled from the physics loop (the "body").
* Inference should run on a fixed, slower interval (e.g., `setInterval` at 20Hz / 50ms) to make decisions.
* The physics engine (`Runner`) should run at maximum speed. On every physics tick, it must apply the single, most recent action decided upon by the "brain" (e.g., `latestAiAction`).
6. **Strict Environment Consistency:**
* The physics of the `TRAINING` environment and the `AI` inference environment must be identical.
* The mathematical formula and all constants used to convert the model's `action` output into a physical `Force` **must be 100% identical** across `stepGameForTraining` and the `AI` mode's logic in `gameLoop`.
* The `USER` mode may use different force constants for better playability, but this must not affect the other two modes.
7. **Robust Physics & World Design:**
* Use Matter.js collision categories to prevent unintended physical interactions.
* Game resets must be stable, using a state flag (e.g., `isRoundOver`).
* The game world should be significantly larger than the visible screen, with the camera tracking the player. Implement a "world wrap" (teleporting from one end to the other) to allow for infinite movement.
8. **Core RL Functions & State Management:**
* Implement `getObservation()`, `resetForTraining()`, and `stepGameForTraining(action)`.
* **Meaningful Resets:** The `resetGame()` function must always initialize the environment in a **non-trivial, randomized state** that requires immediate action.
* **High-Throughput Training:** In `TRAINING` mode, reaching a terminal state (`done: true`) must trigger an **immediate** reset.
// --- For 2-Player / Self-Play Games ONLY ---
/*
9. **Dual ONNX Model Support:**
* The game must load `model.onnx` and `opponent.onnx`. Use a cache-busting query (`?v=` + Date.now()) when loading `opponent.onnx`.
* In `TRAINING` mode, the opponent character must be controlled by the `opponent.onnx` model.
*/
You are an expert reinforcement learning engineer. Your task is to modify the provided HTML game code to make it compatible with a Python-based training environment. You MUST inject the following features without breaking the original game's functionality.
You are an expert AI integration specialist and a master of JavaScript game engineering. Your task is to intelligently modify an existing single-file HTML game to make it a robust environment for reinforcement learning. You must analyze the provided game code and inject the necessary logic without breaking the original gameplay.
**CRITICAL JavaScript Implementation Requirements:**
1. **Game Modes & State:** Must support `USER`, `AI`, `TRAINING` modes. Use `sessionStorage` to remember the current mode after a refresh, automatically re-entering `TRAINING` mode if it was active. Provide clear UI indicators for the current mode.
2. **Reinforcement Learning Deadlock Prevention:**
* In `USER` and `AI` modes, the Matter.js `Runner` must be enabled (`runner.enabled = true`) for smooth, real-time gameplay.
* **Crucially, upon entering `TRAINING` mode, the automatic `Runner` MUST be disabled (`runner.enabled = false`).**
* In `TRAINING` mode, the physics simulation must only advance a single step via a manual call to `Engine.update(engine, 1000 / 60)` from within the `stepGameForTraining` function. This gives the Python RL agent absolute control over the game loop.
3. **WebSocket Interface & Strict RL Protocol:**
* Implement `connectWebSocket()` to connect to `ws://localhost:8765`.
* The interface must handle `reset` and `action` commands.
* **Strict RL Protocol:**
* The `resetForTraining()` function must ONLY send back the initial observation packet: `{observation: getObservation()}`.
* The `stepGameForTraining(action)` function must send the complete data packet: `{observation, reward, done}`.
4. **Robust Asynchronous Model Loading & State Handling:**
* The ONNX model must be loaded asynchronously via a dedicated `loadOnnxModel` function.
* Implement a state flag (e.g., `isModelReady = false`). This flag must be set to `true` **only after** the `ort.InferenceSession` object is created AND its internal metadata (e.g., `ortSession.inputNames`) is confirmed to be fully available.
* The AI decision-making loop (`setInterval`) **must not be initiated** by default. It should only be started by the `setGameMode('AI')` function, and **only if `isModelReady` is already `true`**.
* If `setGameMode('AI')` is called *before* the model is ready, `loadOnnxModel` has the final responsibility to check the current game mode upon completion and start the AI decision loop itself. This dual-check mechanism robustly handles all race conditions.
* The UI must clearly indicate the model's status (`Loading...`, `Loaded`, `Failed`).
5. **Decoupled AI Inference Loop:**
* For the real-time `AI` mode, model inference (the "brain") must be decoupled from the physics loop (the "body").
* Inference should run on a fixed, slower interval (e.g., `setInterval` at 20Hz / 50ms) to make decisions.
* The physics engine (`Runner`) should run at maximum speed. On every physics tick, it must apply the single, most recent action decided upon by the "brain" (e.g., `latestAiAction`).
6. **Strict Environment Consistency:**
* The physics of the `TRAINING` environment and the `AI` inference environment must be identical.
* The mathematical formula and all constants used to convert the model's `action` output into a physical `Force` **must be 100% identical** across `stepGameForTraining` and the `AI` mode's logic in `gameLoop`.
* The `USER` mode may use different force constants for better playability, but this must not affect the other two modes.
7. **Robust Physics & World Design:**
* Use Matter.js collision categories to prevent unintended physical interactions.
* Game resets must be stable, using a state flag (e.g., `isRoundOver`).
* The game world should be significantly larger than the visible screen, with the camera tracking the player. Implement a "world wrap" (teleporting from one end to the other) to allow for infinite movement.
8. **Core RL Functions & State Management:**
* Implement `getObservation()`, `resetForTraining()`, and `stepGameForTraining(action)`.
* **Meaningful Resets:** The `resetGame()` function must always initialize the environment in a **non-trivial, randomized state** that requires immediate action.
* **High-Throughput Training:** In `TRAINING` mode, reaching a terminal state (`done: true`) must trigger an **immediate** reset.
// --- For 2-Player / Self-Play Games ONLY ---
/*
9. **Dual ONNX Model Support:**
* The game must load `model.onnx` and `opponent.onnx`. Use a cache-busting query (`?v=` + Date.now()) when loading `opponent.onnx`.
* In `TRAINING` mode, the opponent character must be controlled by the `opponent.onnx` model.
*/
**Original Game Code:**
[여기에 사용자의 기존 게임 코드를 붙여넣으세요.]
import gymnasium as gym
from gymnasium import spaces
import numpy as np
import asyncio
import websockets
import json
import logging
import threading
import queue
# Websockets 라이브러리의 상세 로그 비활성화
logging.getLogger("websockets").setLevel(logging.ERROR)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
class ServerThread(threading.Thread):
"""
백그라운드에서 웹소켓 서버를 실행하고 메인 스레드와 큐를 통해 통신하는
독립적인 스레드입니다.
"""
def __init__(self):
super().__init__()
self.loop = asyncio.new_event_loop()
self.command_queue = queue.Queue()
self.observation_queue = queue.Queue()
self.daemon = True # 메인 스레드가 종료되면 함께 종료
# --- FIX ---
# 최신 websockets 라이브러리(v10.0+)는 핸들러에 'path' 인자를 전달하지 않습니다.
# 따라서 함수 정의에서 'path'를 제거해야 합니다.
async def handler(self, websocket):
logging.info(f"새로운 클라이언트 연결됨: {websocket.remote_address}")
consumer_task = asyncio.ensure_future(self.consumer(websocket))
producer_task = asyncio.ensure_future(self.producer(websocket))
done, pending = await asyncio.wait(
[consumer_task, producer_task],
return_when=asyncio.FIRST_COMPLETED,
)
for task in pending:
task.cancel()
logging.info(f"클라이언트 연결 종료: {websocket.remote_address}")
async def consumer(self, websocket):
"""메인 스레드로부터 명령을 받아 클라이언트로 전송"""
while True:
try:
command = await self.loop.run_in_executor(None, self.command_queue.get)
await websocket.send(json.dumps(command))
except (websockets.exceptions.ConnectionClosed, asyncio.CancelledError):
break
async def producer(self, websocket):
"""클라이언트로부터 관측 데이터를 받아 메인 스레드로 전송"""
while True:
try:
message = await websocket.recv()
data = json.loads(message)
if "observation" in data:
obs = np.array(data["observation"], dtype=np.float64)
reward = data.get("reward", 0)
done = data.get("done", False)
self.observation_queue.put((obs, reward, done))
except (websockets.exceptions.ConnectionClosed, asyncio.CancelledError):
break
def run(self):
asyncio.set_event_loop(self.loop)
async def start_server():
async with websockets.serve(self.handler, "localhost", 8765):
await asyncio.Future()
try:
# run_until_complete() 대신 run()을 사용하여 서버를 실행합니다.
# 이 방식은 스레드 종료 시 더 깔끔하게 처리됩니다.
self.loop.run_until_complete(start_server())
finally:
self.loop.close()
def stop(self):
if self.loop.is_running():
self.loop.call_soon_threadsafe(self.loop.stop)
logging.info("웹소켓 서버 종료를 시도합니다.")
class WebSocketEnv(gym.Env):
def __init__(self, observation_shape, action_space_config):
super(WebSocketEnv, self).__init__()
self.observation_space = spaces.Box(low=-np.inf, high=np.inf, shape=observation_shape, dtype=np.float64)
if action_space_config['type'] == 'discrete': self.action_space = spaces.Discrete(action_space_config['n'])
elif action_space_config['type'] == 'continuous': self.action_space = spaces.Box(low=np.array(action_space_config['low']), high=np.array(action_space_config['high']), dtype=np.float64)
self.server_thread = ServerThread()
self.server_thread.start()
print("첫 클라이언트의 연결 및 데이터 수신을 기다립니다...")
def reset(self, seed=None, options=None):
super().reset(seed=seed)
with self.server_thread.observation_queue.mutex:
self.server_thread.observation_queue.queue.clear()
self.server_thread.command_queue.put({"command": "reset"})
obs, _, _ = self.server_thread.observation_queue.get()
return obs, {}
def step(self, action):
action_to_send = action.tolist() if isinstance(action, np.ndarray) else float(action)
self.server_thread.command_queue.put({"command": "action", "action": action_to_send})
obs, reward, done = self.server_thread.observation_queue.get()
return obs, reward, done, False, {}
def render(self, mode='human'): pass
def close(self):
self.server_thread.stop()
print("웹소켓 서버가 종료되었습니다.")
# train_simple.py
import os, time, torch, webbrowser, threading
import http.server, socketserver
from stable_baselines3 import PPO
from stable_baselines3.common.callbacks import CheckpointCallback, ProgressBarCallback, CallbackList
from stable_baselines3.common.logger import configure
from websocket_env import WebSocketEnv
# --- 중요 설정 ---
# 1. 관찰 공간 (Observation Space) - AI의 '눈'
OBSERVATION_SHAPE = (4,)
# OBSERVATION_SHAPE = (1, 84, 84) # CnnPolicy를 위한 이미지 관찰 예시
# 2. 행동 공간 (Action Space) - AI의 '손'
# ACTION_SPACE_CONFIG = {'type': 'discrete', 'n': 5}
ACTION_SPACE_CONFIG = {'type': 'continuous', 'low': [-2.0], 'high': [2.0]} # 1차원 연속 행동 예시
# ACTION_SPACE_CONFIG = {'type': 'continuous', 'low': [-1.0, -1.0, -1.0, 0.0], 'high': [1.0, 1.0, 1.0, 1.0]} # 2차원 연속 행동 예시
# -----------------
LOG_DIR, TOTAL_TIMESTEPS, PORT = "training_logs", 50_000_000, 8000
os.makedirs(LOG_DIR, exist_ok=True)
class WebServerThread(threading.Thread):
def __init__(self, port): super().__init__(); self.port, self.server, self.daemon = port, None, True
def run(self): self.server = socketserver.TCPServer(("", self.port), http.server.SimpleHTTPRequestHandler); self.server.serve_forever()
def stop(self):
if self.server: self.server.shutdown()
def find_latest_run_and_checkpoint():
if not os.path.isdir(LOG_DIR): return None, None
runs = sorted([d for d in os.listdir(LOG_DIR) if os.path.isdir(os.path.join(LOG_DIR, d))], reverse=True)
for run in runs:
run_path = os.path.join(LOG_DIR, run)
checkpoints_path = os.path.join(run_path, "checkpoints")
if not os.path.isdir(checkpoints_path): continue
checkpoints = [f for f in os.listdir(checkpoints_path) if f.startswith("rl_model_") and f.endswith(".zip")]
if checkpoints:
latest_checkpoint = max(checkpoints, key=lambda f: int(f.split("_")[2].replace(".zip", "")))
return run, os.path.join(checkpoints_path, latest_checkpoint)
return None, None
def main():
web_server = WebServerThread(PORT)
web_server.start()
time.sleep(1)
webbrowser.open(f'http://localhost:{PORT}')
latest_run_name, latest_checkpoint = find_latest_run_and_checkpoint()
TENSORBOARD_LOG_NAME = latest_run_name if latest_run_name else f"PPO_{int(time.time())}"
run_path = os.path.join(LOG_DIR, TENSORBOARD_LOG_NAME)
checkpoint_path = os.path.join(run_path, "checkpoints")
os.makedirs(checkpoint_path, exist_ok=True)
env = WebSocketEnv(observation_shape=OBSERVATION_SHAPE, action_space_config=ACTION_SPACE_CONFIG)
custom_logger = configure(run_path, ["stdout", "tensorboard"])
checkpoint_callback = CheckpointCallback(save_freq=20000, save_path=checkpoint_path, name_prefix="rl_model")
progress_callback = ProgressBarCallback()
callback_list = CallbackList([checkpoint_callback, progress_callback])
policy_type = "CnnPolicy" if len(OBSERVATION_SHAPE) == 3 else "MlpPolicy"
if latest_checkpoint:
print(f"체크포인트에서 학습을 재개합니다: {latest_checkpoint}")
model = PPO.load(latest_checkpoint, env=env, verbose=0, device='auto')
else:
print(f"{policy_type} 정책으로 새로운 학습 세션을 시작합니다.")
model = PPO(policy_type, env, verbose=0, device='auto') # verbose=0 for clean progress bar
model.set_logger(custom_logger)
final_model_path = ""
try:
model.learn(total_timesteps=TOTAL_TIMESTEPS, callback=callback_list, reset_num_timesteps=not bool(latest_checkpoint))
final_model_path = os.path.join(run_path, "final_model.zip")
model.save(final_model_path)
print(f"\n최종 모델 저장 완료: {final_model_path}")
finally:
env.close()
web_server.stop()
if __name__ == '__main__':
main()
# train_selfplay.py
# train_selfplay.py
import os
import time
import torch
import webbrowser
import threading
import http.server
import socketserver
import shutil
from stable_baselines3 import PPO
from stable_baselines3.common.callbacks import ProgressBarCallback
from stable_baselines3.common.logger import configure
from websocket_env import WebSocketEnv
# --- 중요 설정 ---
# 1. 관찰 공간 (Observation Space) - AI의 '눈'
# (예: 플레이어1 위치,속도 + 플레이어2 위치,속도 + 공 위치,속도 등)
OBSERVATION_SHAPE = (10,)
# 2. 행동 공간 (Action Space) - AI의 '손'
# (예: 위, 아래, 가만히 있기)
ACTION_SPACE_CONFIG = {'type': 'discrete', 'n': 3}
# 3. 셀프 플레이 설정
TOTAL_TRAINING_ITERATIONS = 20 # 총 학습 반복 횟수
STEPS_PER_ITERATION = 50_000 # 한 반복 당 학습할 타임스텝 수 (이 값마다 상대방 모델이 업데이트됨)
TOTAL_TIMESTEPS = TOTAL_TRAINING_ITERATIONS * STEPS_PER_ITERATION
# 4. 기타 설정
LOG_DIR = "training_logs_selfplay"
PORT = 8000
# -----------------
os.makedirs(LOG_DIR, exist_ok=True)
class WebServerThread(threading.Thread):
"""백그라운드에서 로컬 웹 서버를 실행하는 스레드"""
def __init__(self, port):
super().__init__()
self.port = port
self.server = None
self.daemon = True
def run(self):
handler = http.server.SimpleHTTPRequestHandler
self.server = socketserver.TCPServer(("", self.port), handler)
print(f"로컬 서버가 http://localhost:{self.port} 에서 시작되었습니다.")
self.server.serve_forever()
def stop(self):
if self.server:
self.server.shutdown()
print("로컬 서버가 종료되었습니다.")
def update_opponent_model(model_zip_path, observation_shape):
"""
주어진 PPO 모델(.zip)을 'opponent.onnx'로 변환하여 루트 폴더에 저장합니다.
JS 게임은 이 파일을 상대방 AI로 사용합니다.
"""
if not os.path.exists(model_zip_path):
print(f"경고: 상대방 모델로 업데이트할 '{model_zip_path}' 파일을 찾을 수 없습니다. 건너뜁니다.")
return
print(f"\n--- 상대방 모델 업데이트 시작 ---")
print(f"'{model_zip_path}' 모델을 ONNX로 변환합니다...")
try:
model = PPO.load(model_zip_path, device='cpu')
model.policy.eval()
# ONNX 변환을 위한 더미 입력 데이터 생성
# (배치 크기 1을 추가하여 [1, obs_dim] 형태로 만듦)
dummy_input = torch.randn((1, *observation_shape), dtype=torch.float32)
output_path = "opponent.onnx"
torch.onnx.export(
model.policy,
dummy_input,
output_path,
opset_version=11,
input_names=['input'],
output_names=['output'],
dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}}
)
print(f" 성공! '{output_path}' 파일이 업데이트 되었습니다.")
print(f"다음 학습부터 에이전트는 더 강해진 자신과 대결합니다.")
print(f"---------------------------------\n")
except Exception as e:
print(f" 상대방 모델 업데이트 중 오류 발생: {e}")
def find_latest_model_and_opponent():
"""가장 최근에 학습된 모델과 상대방 모델을 찾습니다."""
if not os.path.isdir(LOG_DIR):
return None, None
# 가장 최근의 학습 세션 폴더 찾기
runs = sorted([d for d in os.listdir(LOG_DIR) if d.startswith("PPO_SelfPlay")], reverse=True)
if not runs:
return None, None
latest_run_path = os.path.join(LOG_DIR, runs[0])
# 해당 세션의 최종 모델과 최종 상대방 모델 찾기
latest_model = os.path.join(latest_run_path, "latest_model.zip")
latest_opponent = os.path.join(latest_run_path, "latest_opponent.onnx")
if os.path.exists(latest_model):
# 프로젝트 루트에 이전 상대방 모델 복사
if os.path.exists(latest_opponent):
shutil.copyfile(latest_opponent, "opponent.onnx")
return latest_model, latest_opponent
return None, None
def main():
# 1. 웹서버 시작 및 브라우저 열기
web_server = WebServerThread(PORT)
web_server.start()
time.sleep(1)
webbrowser.open(f'http://localhost:{PORT}/?mode=TRAINING') # TRAINING 모드로 바로 진입
# 2. 학습 환경 및 모델 설정
env = WebSocketEnv(observation_shape=OBSERVATION_SHAPE, action_space_config=ACTION_SPACE_CONFIG)
TENSORBOARD_LOG_NAME = f"PPO_SelfPlay_{int(time.time())}"
run_path = os.path.join(LOG_DIR, TENSORBOARD_LOG_NAME)
os.makedirs(run_path, exist_ok=True)
custom_logger = configure(run_path, ["stdout", "tensorboard"])
# 이전 학습 상태 불러오기
latest_model_path, _ = find_latest_model_and_opponent()
if latest_model_path:
print(f"이전 학습 모델에서 훈련을 재개합니다: {latest_model_path}")
model = PPO.load(latest_model_path, env=env, device='auto')
else:
print("새로운 셀프 플레이 학습 세션을 시작합니다.")
# 처음에는 상대가 없으므로, JS에서 랜덤 또는 기본 AI로 작동해야 함
if os.path.exists("opponent.onnx"):
print("기존 'opponent.onnx' 파일을 사용합니다.")
else:
print("경고: 'opponent.onnx' 파일이 없습니다. JS 환경에서 상대방이 멈춰있을 수 있습니다.")
model = PPO("MlpPolicy", env, verbose=0, device='auto')
model.set_logger(custom_logger)
try:
# 3. 셀프 플레이 학습 루프
for i in range(TOTAL_TRAINING_ITERATIONS):
print(f"=============== 셀프 플레이 반복 [{i+1}/{TOTAL_TRAINING_ITERATIONS}] ===============")
# reset_num_timesteps=False로 설정하여 학습을 이어나감
model.learn(total_timesteps=STEPS_PER_ITERATION, callback=ProgressBarCallback(), reset_num_timesteps=False)
# 현재까지 학습된 모델을 저장
current_model_path = os.path.join(run_path, "latest_model.zip")
model.save(current_model_path)
# 저장된 모델을 opponent.onnx로 변환 및 복사
update_opponent_model(current_model_path, OBSERVATION_SHAPE)
# 만약을 위해 변환된 onnx 파일도 백업
shutil.copyfile("opponent.onnx", os.path.join(run_path, "latest_opponent.onnx"))
print("\n\n 모든 셀프 플레이 학습이 완료되었습니다! ")
final_model_path = os.path.join(run_path, "final_model.zip")
model.save(final_model_path)
print(f"최종 모델이 '{final_model_path}'에 저장되었습니다.")
print(f"최종 상대방 모델은 프로젝트 루트의 'opponent.onnx' 입니다.")
print("이제 index.html을 열고 AI 모드로 최종 결과를 확인하세요.")
except KeyboardInterrupt:
print("\n학습이 중단되었습니다. 현재까지의 진행 상황을 저장합니다.")
model.save(os.path.join(run_path, "interrupted_model.zip"))
finally:
env.close()
web_server.stop()
if __name__ == '__main__':
main()
이제 모든 준비가 끝났습니다. 학습의 최종 결과물인 `model.onnx`가 얼마나 똑똑해졌는지 확인해볼 시간입니다.
training_logs/PPO_숫자/ 폴더에서 `final_model.zip` 파일을 찾습니다.MODEL_DIR="경로"의 경로에 붙여넣어 수정합니다. (이때 파일 이름은 지우고 폴더 경로만 붙여넣기)python -m http.server 명령어로 로컬 서버를 실행합니다.import torch
import torch.nn as nn
from stable_baselines3 import PPO
import numpy as np
import os
# --- 설정 ---
MODEL_DIR = "model" # 모델이 저장된 폴더 경로
MODEL_FILENAME = "final_model.zip"
MODEL_PATH = os.path.join(MODEL_DIR, MODEL_FILENAME)
# 변환된 ONNX 모델을 저장할 경로와 파일명
# index.html 파일이 있는 루트 폴더에 저장해야 AI 모드가 인식할 수 있습니다.
ONNX_OUTPUT_PATH = "model.onnx"
# train.py에 정의된 환경의 관찰 공간 모양
# (배치 크기 1을 추가하여 [1, 20] 형태로 만들어야 합니다)
OBSERVATION_SHAPE = (1, 20)
def convert_to_onnx():
"""
Stable-Baselines3 PPO 모델(.zip)을 ONNX 형식으로 변환합니다.
"""
print(f"'{MODEL_PATH}'에서 학습된 모델을 불러옵니다...")
# 모델 파일이 존재하는지 확인
if not os.path.exists(MODEL_PATH):
print(f"오류: 모델 파일 '{MODEL_PATH}'을 찾을 수 없습니다.")
print("먼저 train.py를 실행하여 모델을 학습시키고 저장해야 합니다.")
return
# Stable-Baselines3 모델 불러오기
# PyTorch를 사용하도록 device='cpu'로 명시하는 것이 안정적입니다.
try:
model = PPO.load(MODEL_PATH, device='cpu')
except Exception as e:
print(f"모델을 불러오는 중 오류가 발생했습니다: {e}")
return
print("모델을 성공적으로 불러왔습니다.")
# ONNX 변환을 위해 모델을 평가 모드(evaluation mode)로 설정합니다.
# 이 과정은 드롭아웃(dropout)이나 배치 정규화(batch normalization) 등의
# 학습용 레이어를 비활성화하여 일관된 출력을 보장합니다.
model.policy.eval()
# ONNX 변환을 위한 더미 입력 데이터를 생성합니다.
# 이 데이터는 모델의 구조를 추적하는 데 사용되며, 실제 입력값은 중요하지 않습니다.
dummy_input = torch.randn(OBSERVATION_SHAPE, dtype=torch.float32)
print(f"모델을 ONNX 형식으로 변환합니다. 저장 위치: '{ONNX_OUTPUT_PATH}'")
try:
# torch.onnx.export 함수를 사용하여 모델을 변환합니다.
torch.onnx.export(
model.policy, # 변환할 모델의 정책 네트워크
dummy_input, # 모델 구조 추적을 위한 더미 입력
ONNX_OUTPUT_PATH, # 저장될 ONNX 파일 경로
opset_version=11, # ONNX 버전 (11은 호환성이 좋음)
input_names=['input'], # ONNX 모델의 입력 레이어 이름
output_names=['output'], # ONNX 모델의 출력 레이어 이름
dynamic_axes={'input': {0: 'batch_size'}, # 배치 크기가 변할 수 있도록 설정
'output': {0: 'batch_size'}}
)
print("-" * 50)
print(" ONNX 변환이 성공적으로 완료되었습니다!")
print(f"이제 'index.html' 파일을 브라우저에서 열고 'AI 모드'를 테스트할 수 있습니다.")
print("-" * 50)
except Exception as e:
print(f"ONNX 변환 중 오류가 발생했습니다: {e}")
if __name__ == '__main__':
convert_to_onnx()
최종 단계입니다. 완성된 프로젝트를 VS Code에서 바로 GitHub에 올리고, GitHub Pages를 통해 전 세계에 배포합니다.
Git이 설치되어 있지 않다면 git-scm.com에서 다운로드하여 설치합니다. 설치 후, 터미널을 열어 아래 명령어를 입력하여 사용자 정보를 설정합니다 (최초 한 번만 필요).
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
VS Code 왼쪽 하단의 사람 모양 아이콘(계정)을 클릭하고, 'GitHub으로 로그인(Sign in with GitHub)'을 선택하여 브라우저의 안내에 따라 계정을 연동합니다.
VS Code 왼쪽의 '소스 제어' 탭(가지 모양 아이콘)으로 이동하여 '리포지토리 초기화(Initialize Repository)' 버튼을 클릭합니다. 그 후, 상단의 메시지 입력란에 "Initial commit"과 같은 첫 커밋 메시지를 작성한 후 ✓ 커밋 버튼을 눌러 프로젝트 파일(`index.html`, `model.onnx` 등)을 커밋합니다.
'소스 제어' 탭에 나타나는 'GitHub에 게시(Publish to GitHub)' 버튼을 클릭합니다. VS Code가 GitHub에 저장소를 만드는 과정을 안내합니다. 저장소 이름을 정하고, 반드시 '공개(Public)' 저장소로 설정해야 GitHub Pages를 무료로 사용할 수 있습니다.
게시가 완료되면 GitHub 사이트에서 방금 생성한 저장소로 이동합니다. 'Settings' → 'Pages' 탭으로 이동하여 'Source'를 'Deploy from a branch'로, 브랜치를 'main' (또는 'master')으로 선택하고 'Save'를 클릭합니다.
잠시 후 페이지 상단에 녹색으로 게시 주소(https://<사용자명>.github.io/<저장소명>/)가 나타납니다. 이 주소로 접속하여 게임을 확인하고 전 세계와 공유하세요!
이 가이드를 통해 최종적으로 완성된 CartPole 게임의 파일입니다. 학습 파일은 제공되지 않았습니다. 직접 학습시켜보세요. 전체 코드를 직접 확인하고 싶거나, 자신의 프로젝트와 비교하며 참고하고 싶을 때 아래 버튼을 통해 다운로드하세요.