{ "cells": [ { "cell_type": "markdown", "id": "9ab9a8f7-1d63-4bd5-9ccf-cf9b7ba51bd5", "metadata": {}, "source": [ "\n", "
\n", "
\n", " \"Department\n", "
\n", "
\n", "

CS-866 Deep Reinforcement Learning

\n", " \n", "

Notebook: Policy-Based RL: REINFORCE (Mountain Car)

\n", "

Instructor: Nazar Khan   |   Semester: Fall 2025

\n", "
\n", "
\n", " \"University\n", "
\n", "
\n", "\n", "---\n", "\n", "**This notebook** is a self-contained teaching and evaluation resource for the REINFORCE policy-gradient algorithm implemented on `MontainCarContinuous-v0`. It includes: theory, fully-commented PyTorch code, training with TensorBoard logging, recording (MP4/GIF), visualizations, and an assignment + rubric for students." ] }, { "cell_type": "code", "execution_count": 1, "id": "0f6266bc-cd04-48fb-8622-03ffd8f08158", "metadata": {}, "outputs": [], "source": [ "# REINFORCE on MountainCarContinuous-v0\n", "# produces both GIF and MP4, uses Gymnasium API (reset -> obs, info).\n", "#\n", "# Requirements: gymnasium, torch, numpy, matplotlib, imageio, tqdm\n", "# Optional (for MP4): ffmpeg installed on system (imageio uses ffmpeg)\n", "\n", "import gymnasium as gym\n", "import numpy as np\n", "import torch\n", "import torch.nn as nn\n", "import torch.optim as optim\n", "from collections import deque\n", "import imageio\n", "import matplotlib.pyplot as plt\n", "from IPython.display import HTML, display\n", "from tqdm import trange\n", "import os\n", "import math\n", "import tempfile" ] }, { "cell_type": "code", "execution_count": 7, "id": "8379b5be-ab76-435c-baf6-2ccb8b2464a5", "metadata": {}, "outputs": [], "source": [ "# -----------------------------\n", "# 0) Short explanation\n", "# -----------------------------\n", "# We're teaching a little \"car\" how to drive up a hill to reach a flag.\n", "# The car can push left or right with a continuous force (a number between -1 and 1).\n", "# REINFORCE is a method where the car tries many times and learns from the\n", "# whole trip (episode) how good its choices were.\n", "#\n", "# We use a neural network to pick the push (action) as a random number\n", "# from a small bell-shaped curve (Gaussian). The network learns to shift\n", "# the bell-curve so better actions happen more often.\n", "#\n", "# We'll record videos (GIF + MP4) so you can watch the car learn." ] }, { "cell_type": "code", "execution_count": 8, "id": "30fda3f2-093f-4c40-9f7f-ae7fd60f113c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Environment: MountainCarContinuous-v0\n", "State dimension: 2, Action dimension: 1\n", "Action bounds: [-1.0, 1.0] (we will clip actions to this)\n" ] } ], "source": [ "# -----------------------------\n", "# 1) Environment setup\n", "# -----------------------------\n", "env_id = \"MountainCarContinuous-v0\"\n", "\n", "# Create the environment with rgb frames available for rendering\n", "env = gym.make(env_id, render_mode=\"rgb_array\") # gymnasium returns frames via env.render()\n", "\n", "# Get dimensions and bounds\n", "state_dim = env.observation_space.shape[0] # normally 2: position and velocity\n", "action_dim = env.action_space.shape[0] # continuous action dimension (should be 1 here)\n", "action_low = float(env.action_space.low[0])\n", "action_high = float(env.action_space.high[0])\n", "\n", "print(f\"Environment: {env_id}\")\n", "print(f\"State dimension: {state_dim}, Action dimension: {action_dim}\")\n", "print(f\"Action bounds: [{action_low}, {action_high}] (we will clip actions to this)\")" ] }, { "cell_type": "code", "execution_count": 9, "id": "a8552925-639b-411a-ac29-a155964403e8", "metadata": {}, "outputs": [], "source": [ "# -----------------------------\n", "# 2) Policy network (Gaussian policy)\n", "# -----------------------------\n", "# The network will produce the mean of the gaussian. We keep a small learnable\n", "# log-standard-deviation so the network can tune how exploratory it is.\n", "\n", "class PolicyNet(nn.Module):\n", " def __init__(self, state_dim, action_dim, hidden=128):\n", " super().__init__()\n", " # Simple 2-layer MLP\n", " self.fc1 = nn.Linear(state_dim, hidden)\n", " self.fc2 = nn.Linear(hidden, hidden)\n", " self.mean = nn.Linear(hidden, action_dim)\n", " # We'll use a separate learnable parameter for log std (shared per action dim)\n", " # Initialized to small negative value so initial std is modest.\n", " self.log_std = nn.Parameter(torch.ones(action_dim) * -0.5)\n", "\n", " def forward(self, x):\n", " # x is a torch tensor of shape (state_dim,)\n", " x = torch.tanh(self.fc1(x))\n", " x = torch.tanh(self.fc2(x))\n", " mean = self.mean(x) # shape: (action_dim,)\n", " std = torch.exp(self.log_std) # shape: (action_dim,)\n", " return mean, std\n", "\n", "# Instantiate policy and optimizer\n", "policy = PolicyNet(state_dim, action_dim, hidden=128)\n", "optimizer = optim.Adam(policy.parameters(), lr=1e-3)" ] }, { "cell_type": "code", "execution_count": 10, "id": "21bbb676-54f5-49b5-946e-429eee2d7a41", "metadata": {}, "outputs": [], "source": [ "# -----------------------------\n", "# 3) Helper functions: selecting actions & computing returns\n", "# -----------------------------\n", "def select_action(state_np):\n", " \"\"\"\n", " state_np: numpy array of state (shape (state_dim,))\n", " Returns: clipped_action (numpy), log_prob (torch scalar)\n", " \"\"\"\n", " # Convert numpy -> torch\n", " state = torch.tensor(state_np, dtype=torch.float32)\n", " mean, std = policy(state) # both torch tensors\n", " # Create a normal distribution per action dimension\n", " dist = torch.distributions.Normal(mean, std)\n", " action = dist.sample() # sample an action (torch)\n", " action_clipped = torch.clamp(action, action_low, action_high)\n", " # Sum log_prob across action dims to get scalar log probability\n", " log_prob = dist.log_prob(action).sum()\n", " return action_clipped.detach().numpy(), log_prob\n", "\n", "def compute_returns(rewards, gamma=0.99):\n", " \"\"\"\n", " Given a list of rewards for an episode, compute discounted returns G_t.\n", " Then normalize returns (important for stable REINFORCE learning).\n", " Returns: torch tensor of shape (len(rewards),)\n", " \"\"\"\n", " returns = []\n", " G = 0.0\n", " for r in reversed(rewards):\n", " G = r + gamma * G\n", " returns.insert(0, G)\n", " returns = torch.tensor(returns, dtype=torch.float32)\n", " # Normalize (subtract mean, divide by std) to reduce variance\n", " if returns.std().item() > 1e-8:\n", " returns = (returns - returns.mean()) / (returns.std() + 1e-8)\n", " else:\n", " returns = returns - returns.mean()\n", " return returns" ] }, { "cell_type": "code", "execution_count": 7, "id": "dcae85d4-9047-483f-aec3-7398762b1056", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Training episodes: 100%|ā–ˆ| 2000/2000 [23:29<00:00, 1.42it/s, ep_reward=-1.01, r\n" ] } ], "source": [ "# -----------------------------\n", "# 4) Training loop with early stopping & recording\n", "# -----------------------------\n", "# Hyperparameters (you can tweak them)\n", "num_episodes = 2000 # upper bound on episodes\n", "gamma = 0.99 # discount factor\n", "render_every = 200 # record a single frame every N episodes to create animation steps\n", "record_last_episodes = 5 # record a few full episodes after training to make better video\n", "early_stop_window = 100 # window for computing recent average reward\n", "early_stop_threshold = 90.0 # average reward threshold to consider solved (common for this env)\n", "# Explanation: MountainCarContinuous gives reward ~100 for success, so ~90 is good.\n", "\n", "# Logging structures\n", "episode_rewards = []\n", "recent_buffer = deque(maxlen=early_stop_window)\n", "frames_for_gif = [] # will store a handful of frames captured periodically\n", "frames_final_episodes = [] # will store full frames for final example episodes (for video)\n", "scale = 0.25 # scale for reducing frame size to save RAM\n", "\n", "# We'll use tqdm for nicer progress bar\n", "pbar = trange(num_episodes, desc=\"Training episodes\")\n", "\n", "# For stability, set a manual seed (optional)\n", "torch.manual_seed(42)\n", "np.random.seed(42)\n", "\n", "solved_episode = None\n", "\n", "for episode in pbar:\n", " # Gymnasium reset returns (obs, info)\n", " state, _ = env.reset()\n", " done = False\n", " rewards = []\n", " log_probs = []\n", "\n", " # Run one full episode (until terminated or truncated)\n", " while not done:\n", " # Optionally capture a frame every so often to make a slow-motion GIF of learning\n", " if (episode + 1) % render_every == 0:\n", " try:\n", " frame = env.render() # rgb array\n", " frame = cv2.resize(frame, (0, 0), fx=scale, fy=scale, interpolation=cv2.INTER_AREA)\n", " frames_for_gif.append(frame)\n", " except Exception:\n", " pass # rendering might fail in headless environments; it's optional\n", "\n", " action, log_prob = select_action(state) # get action & its log probability\n", " next_state, reward, terminated, truncated, _ = env.step(action)\n", " done = bool(terminated or truncated)\n", "\n", " rewards.append(float(reward))\n", " log_probs.append(log_prob)\n", "\n", " state = next_state\n", "\n", " # Compute returns and policy loss\n", " returns = compute_returns(rewards, gamma=gamma) # torch tensor\n", " # Multiply -log_prob * return for each timestep (we minimize negative objective)\n", " policy_loss_terms = []\n", " for lp, G in zip(log_probs, returns):\n", " # -log_prob * G (we use negative because optimizers minimize)\n", " policy_loss_terms.append(-lp * G)\n", " policy_loss = torch.stack(policy_loss_terms).sum()\n", "\n", " # Gradient step\n", " optimizer.zero_grad()\n", " policy_loss.backward()\n", " optimizer.step()\n", "\n", " # Logging & early stopping check\n", " ep_reward = sum(rewards)\n", " episode_rewards.append(ep_reward)\n", " recent_buffer.append(ep_reward)\n", "\n", " # Update progress bar\n", " pbar.set_postfix({\"ep_reward\": f\"{ep_reward:.2f}\",\n", " \"recent_mean\": f\"{np.mean(recent_buffer):.2f}\" if len(recent_buffer)>0 else \"N/A\"})\n", "\n", " # Early stopping condition: average of last early_stop_window episodes above threshold\n", " if len(recent_buffer) == early_stop_window:\n", " recent_mean = float(np.mean(recent_buffer))\n", " if recent_mean >= early_stop_threshold:\n", " solved_episode = episode + 1\n", " print(f\"\\nšŸŽ‰ Early stopping at episode {solved_episode}: recent mean = {recent_mean:.2f}\")\n", " break\n", "\n", "# If training ended early, record a few full episodes to show final behavior\n", "# Record record_last_episodes full episodes (frames) for a nicer final video\n", "for i in range(record_last_episodes):\n", " s, _ = env.reset()\n", " done = False\n", " episode_frames = []\n", " while not done:\n", " # collect frames for video\n", " try:\n", " frame = env.render()\n", " frame = cv2.resize(frame, (0, 0), fx=scale, fy=scale, interpolation=cv2.INTER_AREA)\n", " episode_frames.append(frame)\n", " except Exception:\n", " pass\n", " a, _ = select_action(s)\n", " s, r, terminated, truncated, _ = env.step(a)\n", " done = bool(terminated or truncated)\n", " frames_final_episodes.extend(episode_frames)\n", "\n", "# If no final frames captured (headless), we still might have periodic frames_for_gif\n", "if len(frames_final_episodes) == 0 and len(frames_for_gif) == 0:\n", " print(\"Warning: no frames were recorded (rendering may be disabled in this environment).\")\n", "\n", "# Close env\n", "env.close()" ] }, { "cell_type": "code", "execution_count": 11, "id": "4c965996-60d1-48bd-b801-e8df37b91232", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "IMAGEIO FFMPEG_WRITER WARNING: input image is not divisible by macro_block_size=16, resizing from (600, 400) to (608, 400) to ensure video compatibility with most codecs and players. To prevent resizing, make your input image divisible by the macro_block_size or set the macro_block_size to 1 (risking incompatibility).\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Could not save progress GIF: Unable to allocate 6.70 GiB for an array with shape (9990, 400, 600, 3) and data type uint8\n", "Could not save final GIF: Unable to allocate 3.35 GiB for an array with shape (4995, 400, 600, 3) and data type uint8\n", "Could not save MP4 (ffmpeg may be missing). Error: [Errno 12] Cannot allocate memory\n", "Final GIF is available if created above.\n" ] } ], "source": [ "# -----------------------------\n", "# 5) Save animations: GIF + MP4 (if possible)\n", "# -----------------------------\n", "# We'll attempt to save:\n", "# - \"mountaincar_reinforce_progress.gif\": short GIF made from periodic frames_for_gif\n", "# - \"mountaincar_reinforce_final.mp4\": MP4 made from final full episode frames (if available)\n", "#\n", "# If ffmpeg is unavailable, MP4 saving may fail; GIF saving with imageio should still work.\n", "\n", "out_dir = \"reinforce_videos\"\n", "os.makedirs(out_dir, exist_ok=True)\n", "\n", "# Helper to scale frames (optional): ensure frames are uint8 arrays\n", "def _to_uint8(frame):\n", " # frame might already be uint8; if floats, scale to 0..255\n", " if frame.dtype == np.uint8:\n", " return frame\n", " f = np.clip(frame, 0.0, 1.0)\n", " f = (255.0 * f).astype(np.uint8)\n", " return f\n", "\n", "# 5a) Save short progress GIF if we have periodic frames\n", "gif_path = os.path.join(out_dir, \"mountaincar_reinforce_progress.gif\")\n", "if len(frames_for_gif) > 0:\n", " try:\n", " frames_uint8 = [_to_uint8(f) for f in frames_for_gif]\n", " # Make the GIF (speed up by setting fps)\n", " imageio.mimsave(gif_path, frames_uint8, fps=15)\n", " print(f\"Saved progress GIF to: {gif_path} (frames used: {len(frames_uint8)})\")\n", " except Exception as e:\n", " print(\"Could not save progress GIF:\", e)\n", "else:\n", " print(\"No periodic frames collected for progress GIF.\")\n", "\n", "# 5b) Save final episodes MP4/GIF using frames_final_episodes if available\n", "final_gif_path = os.path.join(out_dir, \"mountaincar_reinforce_final.gif\")\n", "final_mp4_path = os.path.join(out_dir, \"mountaincar_reinforce_final.mp4\")\n", "if len(frames_final_episodes) > 0:\n", " frames_uint8_final = [_to_uint8(f) for f in frames_final_episodes]\n", " # Save final GIF\n", " try:\n", " imageio.mimsave(final_gif_path, frames_uint8_final, fps=30)\n", " print(f\"Saved final GIF to: {final_gif_path} (frames: {len(frames_uint8_final)})\")\n", " except Exception as e:\n", " print(\"Could not save final GIF:\", e)\n", "\n", " # Try saving MP4 (may require ffmpeg)\n", " try:\n", " # imageio's ffmpeg writer uses plugin \"ffmpeg\"\n", " imageio.mimsave(final_mp4_path, frames_uint8_final, fps=30, codec=\"libx264\")\n", " print(f\"Saved final MP4 to: {final_mp4_path}\")\n", " except Exception as e:\n", " print(\"Could not save MP4 (ffmpeg may be missing). Error:\", e)\n", " print(\"Final GIF is available if created above.\")\n", "else:\n", " print(\"No full-episode frames collected for final video.\")" ] }, { "cell_type": "code", "execution_count": 9, "id": "2b2c6835-b093-4826-8adf-6a7dd415ea24", "metadata": {}, "outputs": [ { "data": { "image/png": "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3P392nt+zfNMnTa1wfbd/ZsL+/e1V1paytXZV++VfK+pa29narEWEREREWkB5pxr6zrssdGjR7tPP/20Tc7dVvPUt1eKV3IUr+QoXslRvJKjeCVPMUuO4pWctoqXmX3mnBvd2Da1WIuIiIiItAAl1iIiIiIiLUCJtYiIiIhIC1BiLSIiIiLSApRYi4iIiIi0ACXWIiIiIiItQIm1iIiIiEgLUGItIiIiItIClFiLiIiIiLQAJdYiIiIiIi1AibWIiIiISAtQYi0iIiIi0gKUWIuIiIiItIA2TazN7AdmNtfM5pjZE2aWYWYdzewNM1vsP3doyzqKiIiIiCSizRJrM+sJfBcY7ZwbBgSBC4BbgLecc4OAt/xlEREREZF9Wlt3BUkBMs0sBcgC1gJnAo/42x8BzmqbqomIiIiIJM6cc213crPvAf8DVAGvO+cuMrNS51xBXJmtzrkG3UHM7GrgaoCuXbseNn369L1U6/oqKirIyclpk3O3R4pXchSv5CheyVG8kqN4JU8xS47ilZy2itfEiRM/c86Nbmxbyt6uTB2/7/SZQD+gFHjazC5OdH/n3MPAwwCjR492EyZMaIVa7tqMGTNoq3O3R4pXchSv5CheyVG8kqN4JU8xS47ilZx9MV5t2RXkeGC5c26Tcy4EPAscCWwws+4A/vPGNqyjiIiIiEhC2jKx/ho4wsyyzMyAScB84EXgMr/MZcALbVQ/EREREZGEtVlXEOfcJ2b2DPA5EAa+wOvakQM8ZWbfxku+z2urOoqIiIiIJKrNEmsA59wdwB07ra7Ba70WEREREWk32nq4PRERERGR/YISaxERERGRFqDEWkRERESkBSixFhERERFpAUqsRURERERagBJrEREREZEWoMRaRERERKQFKLEWEREREWkBSqxFRERERFqAEmsRERERkRagxFpEREREpAUosRYRERERaQFKrEVEREREWoASaxERERGRFqDEWkRERESkBSixFhERERFpAUqsRURERERagBJrEREREZEWoMRaRERERKQFKLEWEREREWkBSqxFRERERFqAEmsRERERkRagxFpEREREpAUosRYRERERaQEpzW00s3HAxcDRQHegCpgDvAw85pwra/UaioiIiIi0A022WJvZq8CVwL+Ak/ES6yHAT4AM4AUzO2NvVFJEREREZF/XXIv1Jc65zTutqwA+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HZsIZsKECdx2220ce+yxPPDAA7HyS5cuZdSoUbHlxYsXc9hhhzU47m9/+1uGDBlCcXExF1xwAStWrOChhx7i/vvv56ijjuL9999n5cqVTJo0ieLiYiZNmsTXX38NwJQpU7j22ms5+uijOeigg3jppZcaHP+WW27h/fffZ+TIkdx///0ArF27lpNPPplBgwbxox/9KFY2JyeH22+/nbFjx/LRRx/x2GOPcfjhhzNy5EiuueYaIpEIkUiEKVOmMGzYMIYPHx47JsDTTz/N4YcfzkEHHcT7778PeOM+X3755QwfPpxDDz2Ud955p0EdS0pKOPHEEzn00EO55pprGkyyU+fxxx/nzDPPBLxh74444giysrJISUnh2GOPjb3/F154gcsuuwyAyy67jOeff77BsWbOnMnAgQPp378/aWlpXHDBBbGJf/r06UNJSQnr169vtB7JaPObFw8UscRambWIiEijJkyb0GDd+UPP5/ox11MZquTUxxvOAjhl5BSmjJzC5srNnPvUufW2zZgyY5fnrKqqYuTIkbHlW2+9lcmTJ/Od73yH22+/HYBLLrmEl156idNPPx2A0tJS3n33XWDHTIsDBgwgPz+fWbNmMXLkSP76178yZcqUBue79957Wb58Oenp6ZSWllJQUMC1115LTk4O11xzDbm5uZx++ulceumlXHbZZfzlL3/hu9/9bixZXLFiBe+++y5Lly5l4sSJLFmyhIyMjHrHv++++2JJ57Rp05g1axZffPEF6enpDB48mBtvvJFevXqxfft2hg0bxl133cX8+fP5xS9+wYcffkhqairXX389jz/+OEOHDmXNmjXMmTMn9t7rhMNhZs6cySuvvMKdd97Jm2++yYMPPgjAV199xYIFCzjxxBNZtGhRvRjceeedjB8/nttvv52XX36Zhx9+uNHP5sMPP+SPf/wjAMOGDePHP/4xJSUlZGZm8sorr1BcXAzAhg0b6N69OwDdu3dn48aNDY61Zs0aevXqFVsuKirik08+iS2PGjWKDz/8kHPOOafRuiRKLdZ7SYY/KoipzVpERGSfsXNXkMmTJwPwzjvvMHbsWIYPH87bb7/N3LlzY/vUldnZlVdeyV//+lcikQhPPvkk3/rWtxqUKS4u5qKLLuKxxx5rckrwjz76KLbvJZdcwgcffBDbdv755xMIBBg0aBD9+/dnwYIFu3yP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", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "Attempting to display saved media (if any). If files are not shown, check the 'reinforce_videos' folder.\n", "No media displayed inline. Look in the 'reinforce_videos' folder for created files.\n" ] } ], "source": [ "# -----------------------------\n", "# 6) Plot training curves and show animations inline (if possible)\n", "# -----------------------------\n", "# Plot the reward per episode and a moving average.\n", "plt.figure(figsize=(12,5))\n", "plt.plot(episode_rewards, alpha=0.6, label=\"Episode reward\")\n", "# moving average\n", "window = 50\n", "if len(episode_rewards) >= window:\n", " mov_avg = np.convolve(episode_rewards, np.ones(window)/window, mode=\"valid\")\n", " plt.plot(range(window-1, window-1+len(mov_avg)), mov_avg, linewidth=3, label=f\"Moving avg ({window})\")\n", "plt.axhline(early_stop_threshold, color=\"green\", linestyle=\"--\", label=f\"Early stop threshold ({early_stop_threshold})\")\n", "plt.xlabel(\"Episode\")\n", "plt.ylabel(\"Total reward (sum per episode)\")\n", "plt.title(\"REINFORCE training on MountainCarContinuous-v0\")\n", "plt.legend()\n", "plt.grid()\n", "plt.show()\n", "\n", "# Try to display final GIF and MP4 inline (Jupyter)\n", "def try_display(path):\n", " if not os.path.exists(path):\n", " return False\n", " ext = os.path.splitext(path)[1].lower()\n", " if ext == \".gif\":\n", " display(HTML(f'\"gif\"'))\n", " return True\n", " if ext == \".mp4\":\n", " # HTML5 video tag for mp4\n", " display(HTML(f\"\"\"\n", " \n", " \"\"\"))\n", " return True\n", " return False\n", "\n", "print(\"\\nAttempting to display saved media (if any). If files are not shown, check the 'reinforce_videos' folder.\")\n", "shown = False\n", "# Prefer final mp4 then final gif then progress gif\n", "if try_display(final_mp4_path):\n", " shown = True\n", "elif try_display(final_gif_path):\n", " shown = True\n", "elif try_display(gif_path):\n", " shown = True\n", "\n", "if not shown:\n", " print(\"No media displayed inline. Look in the 'reinforce_videos' folder for created files.\")" ] }, { "cell_type": "code", "execution_count": 10, "id": "f587b3b4-f944-4d8e-b37e-59d0319b4e28", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Done running training cell.\n", "Training finished without meeting early-stop threshold. You can:\n", "- Increase num_episodes\n", "- Try lowering the learning rate or increasing network size\n", "- Use a baseline (actor-critic) for faster learning\n", "\n", "Files are in the folder: /home/nazar/teaching/CS866/notebooks/Reinforce/reinforce_videos\n", "If MP4 saving failed, ensure ffmpeg is installed on your system (e.g., 'sudo apt install ffmpeg' on Ubuntu).\n" ] } ], "source": [ "# -----------------------------\n", "# 7) Short tips & next steps (printed for the user)\n", "# -----------------------------\n", "print(\"\\nDone running training cell.\")\n", "if solved_episode is not None:\n", " print(f\"Environment considered solved at episode {solved_episode} (avg >= {early_stop_threshold}).\")\n", "else:\n", " print(\"Training finished without meeting early-stop threshold. You can:\")\n", " print(\"- Increase num_episodes\")\n", " print(\"- Try lowering the learning rate or increasing network size\")\n", " print(\"- Use a baseline (actor-critic) for faster learning\")\n", "\n", "print(\"\\nFiles are in the folder:\", os.path.abspath(out_dir))\n", "print(\"If MP4 saving failed, ensure ffmpeg is installed on your system (e.g., 'sudo apt install ffmpeg' on Ubuntu).\")" ] }, { "cell_type": "code", "execution_count": null, "id": "e366ee88-087a-49ab-b44f-659faa460b27", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.0" } }, "nbformat": 4, "nbformat_minor": 5 }