{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 강화학습 20줄 — 막대 세우기\n", "### 배우기 전 vs 배운 뒤\n", "\n", "**Connect AI LAB · 굿나잇 AI 1편 실습**\n", "\n", "정답을 알려 주지 않습니다. **잘하면 점수, 못하면 끝.** 그것만으로 AI가 스스로 배우는 걸 눈으로 봅니다.\n", "\n", "문제는 「카트 위의 막대를 쓰러뜨리지 않고 버티기」입니다. 카트를 왼쪽·오른쪽으로 밀 수 있고, 한 번 버틸 때마다 1점입니다. 최고는 500점.\n", "\n", "위에서부터 ▶ 를 누르기만 하면 됩니다. 5분이면 끝납니다." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# 1 · 준비\n", "\n", "| 줄 | 하는 일 |\n", "|---|---|\n", "| `pip install` | 훈련장(gymnasium)과 학습 도구(stable-baselines3)를 설치합니다 |\n", "| `gym.make(\"CartPole-v1\")` | 「막대 세우기」 훈련장을 엽니다. 이게 **시뮬레이션**입니다 |" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!pip -q install \"gymnasium[classic-control]\" stable-baselines3\n", "\n", "import gymnasium as gym, numpy as np\n", "훈련장 = gym.make(\"CartPole-v1\")\n", "print(\"훈련장 준비 완료 — 관찰:\", 훈련장.observation_space.shape[0], \"개 숫자 · 행동:\", 훈련장.action_space.n, \"가지(왼쪽/오른쪽)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# 2 · 배우기 전\n", "\n", "아무렇게나 밀어 봅니다. 몇 점이나 버틸까요.\n", "\n", "| 줄 | 하는 일 |\n", "|---|---|\n", "| `점수(정책)` | 정책(어떻게 행동할지)을 받아 5판 돌리고 평균 점수를 냅니다 |\n", "| `action_space.sample()` | 왼쪽·오른쪽을 동전 던지듯 고릅니다 |" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def 점수(정책, 판수=5):\n", " 결과 = []\n", " for _ in range(판수):\n", " 관찰, _ = 훈련장.reset(); 합 = 0; 끝 = False\n", " while not 끝:\n", " 행동 = 정책(관찰)\n", " 관찰, 보상, 종료, 시간초과, _ = 훈련장.step(행동)\n", " 합 += 보상; 끝 = 종료 or 시간초과\n", " 결과.append(합)\n", " return np.mean(결과)\n", "\n", "무작위 = 점수(lambda 관찰: 훈련장.action_space.sample())\n", "print(f\"배우기 전(무작위): 평균 {무작위:.0f}점 버팀 (최고 500)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# 3 · 배우기\n", "\n", "정답을 안 줍니다. **버티면 점수, 넘어지면 끝**만 알려 줍니다. 2만 번 시도합니다.\n", "\n", "| 줄 | 하는 일 |\n", "|---|---|\n", "| `PPO(\"MlpPolicy\", 훈련장)` | 작은 두뇌(신경망)를 만들고 강화학습 방법(PPO)을 붙입니다 |\n", "| `.learn(20_000)` | 훈련장에서 2만 번 행동하며 **보상이 큰 쪽으로** 두뇌를 고칩니다 |" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from stable_baselines3 import PPO\n", "\n", "두뇌 = PPO(\"MlpPolicy\", 훈련장, verbose=0, seed=0)\n", "두뇌.learn(20_000) # ← 2만 번 시도. 코랩에서 약 30초~1분\n", "print(\"학습 끝\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# 4 · 배운 뒤" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "배운뒤 = 점수(lambda 관찰: int(두뇌.predict(관찰, deterministic=True)[0]))\n", "print(f\"배우기 전 {무작위:.0f}점 → 배운 뒤 {배운뒤:.0f}점 (최고 500)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "보통 **20점 → 500점**이 나옵니다. 정답을 한 번도 안 알려 줬는데도요.\n", "\n", "이게 강화학습입니다. 그리고 이 「훈련장」이 세일즈포스 복제본이 되면 업무 에이전트가, 로봇 시뮬레이터가 되면 피지컬 AI가 됩니다. **훈련장을 만드는 사람에게 돈이 가는 이유**입니다.\n", "\n", "---\n", "# 5 · 눈으로 보기 (선택)\n", "\n", "배운 두뇌가 막대를 세우는 장면을 그림 8장으로 봅니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "보기 = gym.make(\"CartPole-v1\", render_mode=\"rgb_array\")\n", "관찰, _ = 보기.reset(seed=0); 장면 = []\n", "for t in range(200):\n", " 행동 = int(두뇌.predict(관찰, deterministic=True)[0])\n", " 관찰, _, 종료, 시간초과, _ = 보기.step(행동)\n", " if t % 25 == 0: 장면.append(보기.render())\n", " if 종료 or 시간초과: break\n", "fig, axes = plt.subplots(1, len(장면), figsize=(3*len(장면), 2.4))\n", "for ax, 그림, i in zip(np.atleast_1d(axes), 장면, range(0, 200, 25)):\n", " ax.imshow(그림); ax.set_title(f\"{i}번째\"); ax.axis(\"off\")\n", "plt.tight_layout(); plt.show()\n", "print(f\"{t+1}번 버텼습니다\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "### 더 해 보기\n", "- `learn(20_000)` 을 `2_000` 으로 줄이면? 덜 배운 두뇌가 몇 점 나오는지 보세요.\n", "- `CartPole-v1` 을 `LunarLander-v3` 로 바꾸면 달 착륙선입니다. (`pip install \"gymnasium[box2d]\"` 필요)\n", "- 훈련장 수천 개가 모여 있는 곳: https://app.primeintellect.ai/dashboard/environments\n", "\n", "*Connect AI LAB · AI CITY BUILDERS · 교재 https://www.aicitybuilders.com/gn1*" ] }, { "cell_type": "markdown", "id": "42fb7cc2", "metadata": {}, "source": [ "---\n", "# 6 · 더 해 보기 실습\n", "\n", "교재에서 안내된 2가지 실험을 코랩에서 바로 실행해 봅니다." ] }, { "cell_type": "markdown", "id": "06241f05", "metadata": {}, "source": [ "### 6-1 · 덜 배운 두뇌 (2,000번 시도)\n", "\n", "2만 번 대신 **2,000번**만 배운 두뇌는 얼마나 버틸까요?\n", "학습량이 부족할 때의 점수와 충분히 배운 두뇌(20,000번)의 점수를 비교해 봅니다." ] }, { "cell_type": "code", "execution_count": null, "id": "3ae1146e", "metadata": {}, "outputs": [], "source": [ "# 덜 배운 두뇌 생성 및 2,000번 학습 (약 3~5초 소요)\n", "덜배운두뇌 = PPO(\"MlpPolicy\", 훈련장, verbose=0, seed=0)\n", "덜배운두뇌.learn(2_000)\n", "\n", "덜배운뒤 = 점수(lambda 관찰: int(덜배운두뇌.predict(관찰, deterministic=True)[0]))\n", "\n", "print(\"=\" * 45)\n", "print(f\"1. 배우기 전 (무작위): 평균 {무작위:.0f}점\")\n", "print(f\"2. 덜 배운 뒤 (2,000번): 평균 {덜배운뒤:.0f}점\")\n", "print(f\"3. 다 배운 뒤 (20,000번): 평균 {배운뒤:.0f}점 (최고 500점)\")\n", "print(\"=\" * 45)" ] }, { "cell_type": "markdown", "id": "bb2d3698", "metadata": {}, "source": [ "---\n", "### 6-2 · 새로운 훈련장: 달 착륙선 (`LunarLander-v3`)\n", "\n", "카트폴과 똑같은 강화학습 방식으로 다른 환경도 풀 수 있습니다.\n", "로켓 추진체를 제어해 두 깃발 사이의 착륙 패드에 안전하게 내려앉아야 합니다.\n", "- **필요 패키지**: `gymnasium[box2d]`\n", "- **성공 기준**: 200점 이상이면 안정 착륙 성공" ] }, { "cell_type": "code", "execution_count": null, "id": "b8299d00", "metadata": {}, "outputs": [], "source": [ "# 1) 달 착륙선 패키지 설치\n", "!pip -q install \"gymnasium[box2d]\"\n", "\n", "import gymnasium as gym\n", "\n", "# 2) 달 착륙선 훈련장 열기\n", "달훈련장 = gym.make(\"LunarLander-v3\")\n", "print(\"달 착륙선 훈련장 준비 완료!\")\n", "print(\"관찰값:\", 달훈련장.observation_space.shape[0], \"개 (위치, 속도, 각도 등)\")\n", "print(\"행동:\", 달훈련장.action_space.n, \"가지 (0: 대기, 1: 좌측엔진, 2: 메인엔진, 3: 우측엔진)\")" ] }, { "cell_type": "code", "execution_count": null, "id": "1ee01491", "metadata": {}, "outputs": [], "source": [ "# 3) 달 착륙선 점수 측정 함수\n", "def 달착륙_점수(정책, 판수=3):\n", " 결과 = []\n", " for _ in range(판수):\n", " 관찰, _ = 달훈련장.reset()\n", " 합 = 0; 끝 = False\n", " while not 끝:\n", " 행동 = 정책(관찰)\n", " 관찰, 보상, 종료, 시간초과, _ = 달훈련장.step(행동)\n", " 합 += 보상\n", " 끝 = 종료 or 시간초과\n", " 결과.append(합)\n", " return np.mean(결과)\n", "\n", "# 배우기 전 (무작위 조작) 점수 - 보통 마이너스 점수(추락)\n", "달_무작위 = 달착륙_점수(lambda 관찰: 달훈련장.action_space.sample())\n", "print(f\"배우기 전(무작위) 달 착륙선: 평균 {달_무작위:.1f}점 (추락)\")\n", "\n", "# 4) 달 착륙선 두뇌 학습 (코랩 기준 약 1~2분 소요, 40,000 스텝)\n", "from stable_baselines3 import PPO\n", "\n", "달두뇌 = PPO(\"MlpPolicy\", 달훈련장, verbose=0, seed=0)\n", "print(\"\\n달 착륙선 학습 시작 (40,000 스텝)... (약 1분)\")\n", "달두뇌.learn(40_000)\n", "print(\"학습 완료!\")\n", "\n", "# 배운 뒤 점수 확인\n", "달_배운뒤 = 달착륙_점수(lambda 관찰: int(달두뇌.predict(관찰, deterministic=True)[0]))\n", "print(\"=\" * 45)\n", "print(f\"달 착륙선 배우기 전: {달_무작위:.1f}점 → 배운 뒤: {달_배운뒤:.1f}점\")\n", "print(\"=\" * 45)" ] }, { "cell_type": "markdown", "id": "f2bdfc0a", "metadata": {}, "source": [ "### 6-3 · 달 착륙선 눈으로 보기 (선택)\n", "\n", "배운 달 착륙선 AI가 실제로 어떻게 내려앉는지 프레임으로 확인합니다." ] }, { "cell_type": "code", "execution_count": null, "id": "9e60c412", "metadata": {}, "outputs": [], "source": [ "달보기 = gym.make(\"LunarLander-v3\", render_mode=\"rgb_array\")\n", "관찰, _ = 달보기.reset(seed=0)\n", "달장면 = []\n", "\n", "for t in range(300):\n", " 행동 = int(달두뇌.predict(관찰, deterministic=True)[0])\n", " 관찰, _, 종료, 시간초과, _ = 달보기.step(행동)\n", " if t % 35 == 0:\n", " 달장면.append(달보기.render())\n", " if 종료 or 시간초과:\n", " 달장면.append(달보기.render())\n", " break\n", "\n", "fig, axes = plt.subplots(1, len(달장면), figsize=(3 * len(달장면), 2.4))\n", "for ax, 그림, idx in zip(np.atleast_1d(axes), 달장면, range(len(달장면))):\n", " ax.imshow(그림)\n", " ax.set_title(f\"#{idx+1}\")\n", " ax.axis(\"off\")\n", "plt.tight_layout()\n", "plt.show()\n", "print(f\"총 {t+1}스텝 진행\")" ] } ], "metadata": { "colab": { "name": "cartpole_rl.ipynb" }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }