{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 使用强化学习训练平衡摆杆\n",
    "\n",
    "本笔记本是 [AI for Beginners Curriculum](http://aka.ms/ai-beginners) 的一部分，灵感来源于 [官方 PyTorch 教程](https://pytorch.org/tutorials/intermediate/reinforcement_q_learning.html) 和 [这个 Cartpole PyTorch 实现](https://github.com/yc930401/Actor-Critic-pytorch)。\n",
    "\n",
    "在这个示例中，我们将使用强化学习（RL）训练一个模型，使其能够在水平轨道上移动的推车上平衡摆杆。我们将使用 [OpenAI Gym](https://www.gymlibrary.ml/) 环境来模拟摆杆。\n",
    "\n",
    "> **注意**: 你可以在本地运行本课程的代码（例如，通过 Visual Studio Code），此时模拟器会在新窗口中打开。如果在线运行代码，你可能需要对代码进行一些调整，具体说明见 [这里](https://towardsdatascience.com/rendering-openai-gym-envs-on-binder-and-google-colab-536f99391cc7)。\n",
    "\n",
    "我们将从确保 Gym 已安装开始：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import sys\n",
    "!{sys.executable} -m pip install gym"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在让我们创建 CartPole 环境，并了解如何对其进行操作。一个环境具有以下属性：\n",
    "\n",
    "* **动作空间** 是我们在模拟的每一步中可以执行的所有可能动作的集合  \n",
    "* **观察空间** 是我们能够获取的所有可能观察值的集合  \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import gym\n",
    "\n",
    "env = gym.make(\"CartPole-v1\")\n",
    "\n",
    "print(f\"Action space: {env.action_space}\")\n",
    "print(f\"Observation space: {env.observation_space}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "让我们看看模拟是如何运行的。以下循环会运行模拟，直到 `env.step` 不返回终止标志 `done`。我们将使用 `env.action_space.sample()` 随机选择动作，这意味着实验可能会非常快地失败（当小车的速度、位置或角度超出某些限制时，CartPole 环境会终止）。\n",
    "\n",
    "> 模拟会在新窗口中打开。你可以多次运行代码，观察它的表现。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "env.reset()\n",
    "\n",
    "done = False\n",
    "total_reward = 0\n",
    "while not done:\n",
    "   env.render()\n",
    "   obs, rew, done, info = env.step(env.action_space.sample())\n",
    "   total_reward += rew\n",
    "   print(f\"{obs} -> {rew}\")\n",
    "print(f\"Total reward: {total_reward}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "你会注意到观测值包含4个数字，它们是：\n",
    "- 小车的位置\n",
    "- 小车的速度\n",
    "- 杆子的角度\n",
    "- 杆子的旋转速度\n",
    "\n",
    "`rew` 是我们在每一步中获得的奖励。在 CartPole 环境中，每进行一步模拟都会获得1分奖励，目标是最大化总奖励，也就是让小车平衡杆子而不倒下的时间尽可能长。\n",
    "\n",
    "在强化学习中，我们的目标是训练一个**策略** $\\pi$，它会根据每个状态 $s$ 告诉我们应该采取的动作 $a$，本质上就是 $a = \\pi(s)$。\n",
    "\n",
    "如果你想要一个概率性的解决方案，可以将策略视为为每个动作返回一组概率，也就是说 $\\pi(a|s)$ 表示在状态 $s$ 下采取动作 $a$ 的概率。\n",
    "\n",
    "## 策略梯度方法\n",
    "\n",
    "在最简单的强化学习算法中，称为**策略梯度**，我们将训练一个神经网络来预测下一步的动作。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import torch\n",
    "\n",
    "num_inputs = 4\n",
    "num_actions = 2\n",
    "\n",
    "model = torch.nn.Sequential(\n",
    "    torch.nn.Linear(num_inputs, 128, bias=False, dtype=torch.float32),\n",
    "    torch.nn.ReLU(),\n",
    "    torch.nn.Linear(128, num_actions, bias = False, dtype=torch.float32),\n",
    "    torch.nn.Softmax(dim=1)\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们将通过运行许多实验来训练网络，并在每次运行后更新我们的网络。让我们定义一个函数来运行实验并返回结果（所谓的**轨迹**）——所有状态、动作（及其推荐概率）和奖励：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def run_episode(max_steps_per_episode = 10000,render=False):    \n",
    "    states, actions, probs, rewards = [],[],[],[]\n",
    "    state = env.reset()\n",
    "    for _ in range(max_steps_per_episode):\n",
    "        if render:\n",
    "            env.render()\n",
    "        action_probs = model(torch.from_numpy(np.expand_dims(state,0)))[0]\n",
    "        action = np.random.choice(num_actions, p=np.squeeze(action_probs.detach().numpy()))\n",
    "        nstate, reward, done, info = env.step(action)\n",
    "        if done:\n",
    "            break\n",
    "        states.append(state)\n",
    "        actions.append(action)\n",
    "        probs.append(action_probs.detach().numpy())\n",
    "        rewards.append(reward)\n",
    "        state = nstate\n",
    "    return np.vstack(states), np.vstack(actions), np.vstack(probs), np.vstack(rewards)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "您可以运行一个未训练网络的情节，并观察到总奖励（即情节长度）非常低：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s, a, p, r = run_episode()\n",
    "print(f\"Total reward: {np.sum(r)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "政策梯度算法的一个棘手方面是使用**折扣奖励**。其思想是我们在游戏的每一步计算总奖励的向量，并在此过程中使用某个系数 $gamma$ 对早期奖励进行折扣。我们还对结果向量进行归一化，因为我们将使用它作为权重来影响我们的训练：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "eps = 0.0001\n",
    "\n",
    "def discounted_rewards(rewards,gamma=0.99,normalize=True):\n",
    "    ret = []\n",
    "    s = 0\n",
    "    for r in rewards[::-1]:\n",
    "        s = r + gamma * s\n",
    "        ret.insert(0, s)\n",
    "    if normalize:\n",
    "        ret = (ret-np.mean(ret))/(np.std(ret)+eps)\n",
    "    return ret"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在开始实际训练！我们将运行300次实验，每次实验中我们将执行以下步骤：\n",
    "\n",
    "1. 运行实验并收集轨迹数据。\n",
    "2. 计算所采取的动作与预测概率之间的差异（`gradients`）。差异越小，说明我们越确信采取了正确的动作。\n",
    "3. 计算折扣奖励，并将梯度乘以折扣奖励——这样可以确保高奖励的步骤对最终结果的影响比低奖励的步骤更大。\n",
    "4. 我们的神经网络的目标动作部分来自运行期间的预测概率，部分来自计算出的梯度。我们将使用`alpha`参数来确定梯度和奖励在多大程度上被考虑——这被称为强化学习算法的*学习率*。\n",
    "5. 最后，我们基于状态和目标动作训练网络，并重复这一过程。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "optimizer = torch.optim.Adam(model.parameters(), lr=0.01)\n",
    "\n",
    "def train_on_batch(x, y):\n",
    "    x = torch.from_numpy(x)\n",
    "    y = torch.from_numpy(y)\n",
    "    optimizer.zero_grad()\n",
    "    predictions = model(x)\n",
    "    loss = -torch.mean(torch.log(predictions) * y)\n",
    "    loss.backward()\n",
    "    optimizer.step()\n",
    "    return loss"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "alpha = 1e-4\n",
    "\n",
    "history = []\n",
    "for epoch in range(300):\n",
    "    states, actions, probs, rewards = run_episode()\n",
    "    one_hot_actions = np.eye(2)[actions.T][0]\n",
    "    gradients = one_hot_actions-probs\n",
    "    dr = discounted_rewards(rewards)\n",
    "    gradients *= dr\n",
    "    target = alpha*np.vstack([gradients])+probs\n",
    "    train_on_batch(states,target)\n",
    "    history.append(np.sum(rewards))\n",
    "    if epoch%100==0:\n",
    "        print(f\"{epoch} -> {np.sum(rewards)}\")\n",
    "\n",
    "plt.plot(history)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在让我们运行带有渲染的剧集来查看结果：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_ = run_episode(render=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "希望你能看到，现在杆子已经可以很好地保持平衡了！\n",
    "\n",
    "## Actor-Critic 模型\n",
    "\n",
    "Actor-Critic 模型是策略梯度的进一步发展，在该模型中，我们构建一个神经网络来同时学习策略和估计奖励。这个网络将有两个输出（或者你可以将其视为两个独立的网络）：\n",
    "* **Actor** 将通过给出状态概率分布来推荐采取的行动，就像在策略梯度模型中一样。\n",
    "* **Critic** 将估算这些行动可能带来的奖励。它会返回在给定状态下未来的总估计奖励。\n",
    "\n",
    "让我们定义这样一个模型：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from itertools import count\n",
    "import torch.nn.functional as F"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
    "env = gym.make(\"CartPole-v1\")\n",
    "\n",
    "state_size = env.observation_space.shape[0]\n",
    "action_size = env.action_space.n\n",
    "lr = 0.0001\n",
    "\n",
    "class Actor(torch.nn.Module):\n",
    "    def __init__(self, state_size, action_size):\n",
    "        super(Actor, self).__init__()\n",
    "        self.state_size = state_size\n",
    "        self.action_size = action_size\n",
    "        self.linear1 = torch.nn.Linear(self.state_size, 128)\n",
    "        self.linear2 = torch.nn.Linear(128, 256)\n",
    "        self.linear3 = torch.nn.Linear(256, self.action_size)\n",
    "\n",
    "    def forward(self, state):\n",
    "        output = F.relu(self.linear1(state))\n",
    "        output = F.relu(self.linear2(output))\n",
    "        output = self.linear3(output)\n",
    "        distribution = torch.distributions.Categorical(F.softmax(output, dim=-1))\n",
    "        return distribution\n",
    "\n",
    "\n",
    "class Critic(torch.nn.Module):\n",
    "    def __init__(self, state_size, action_size):\n",
    "        super(Critic, self).__init__()\n",
    "        self.state_size = state_size\n",
    "        self.action_size = action_size\n",
    "        self.linear1 = torch.nn.Linear(self.state_size, 128)\n",
    "        self.linear2 = torch.nn.Linear(128, 256)\n",
    "        self.linear3 = torch.nn.Linear(256, 1)\n",
    "\n",
    "    def forward(self, state):\n",
    "        output = F.relu(self.linear1(state))\n",
    "        output = F.relu(self.linear2(output))\n",
    "        value = self.linear3(output)\n",
    "        return value"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们需要稍微修改我们的 `discounted_rewards` 和 `run_episode` 函数：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def discounted_rewards(next_value, rewards, masks, gamma=0.99):\n",
    "    R = next_value\n",
    "    returns = []\n",
    "    for step in reversed(range(len(rewards))):\n",
    "        R = rewards[step] + gamma * R * masks[step]\n",
    "        returns.insert(0, R)\n",
    "    return returns\n",
    "\n",
    "def run_episode(actor, critic, n_iters):\n",
    "    optimizerA = torch.optim.Adam(actor.parameters())\n",
    "    optimizerC = torch.optim.Adam(critic.parameters())\n",
    "    for iter in range(n_iters):\n",
    "        state = env.reset()\n",
    "        log_probs = []\n",
    "        values = []\n",
    "        rewards = []\n",
    "        masks = []\n",
    "        entropy = 0\n",
    "        env.reset()\n",
    "\n",
    "        for i in count():\n",
    "            env.render()\n",
    "            state = torch.FloatTensor(state).to(device)\n",
    "            dist, value = actor(state), critic(state)\n",
    "\n",
    "            action = dist.sample()\n",
    "            next_state, reward, done, _ = env.step(action.cpu().numpy())\n",
    "\n",
    "            log_prob = dist.log_prob(action).unsqueeze(0)\n",
    "            entropy += dist.entropy().mean()\n",
    "\n",
    "            log_probs.append(log_prob)\n",
    "            values.append(value)\n",
    "            rewards.append(torch.tensor([reward], dtype=torch.float, device=device))\n",
    "            masks.append(torch.tensor([1-done], dtype=torch.float, device=device))\n",
    "\n",
    "            state = next_state\n",
    "\n",
    "            if done:\n",
    "                print('Iteration: {}, Score: {}'.format(iter, i))\n",
    "                break\n",
    "\n",
    "\n",
    "        next_state = torch.FloatTensor(next_state).to(device)\n",
    "        next_value = critic(next_state)\n",
    "        returns = discounted_rewards(next_value, rewards, masks)\n",
    "\n",
    "        log_probs = torch.cat(log_probs)\n",
    "        returns = torch.cat(returns).detach()\n",
    "        values = torch.cat(values)\n",
    "\n",
    "        advantage = returns - values\n",
    "\n",
    "        actor_loss = -(log_probs * advantage.detach()).mean()\n",
    "        critic_loss = advantage.pow(2).mean()\n",
    "\n",
    "        optimizerA.zero_grad()\n",
    "        optimizerC.zero_grad()\n",
    "        actor_loss.backward()\n",
    "        critic_loss.backward()\n",
    "        optimizerA.step()\n",
    "        optimizerC.step()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在我们将运行主要训练循环。我们将通过计算适当的损失函数并更新网络参数来使用手动网络训练过程：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "actor = Actor(state_size, action_size).to(device)\n",
    "critic = Critic(state_size, action_size).to(device)\n",
    "run_episode(actor, critic, n_iters=100)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "env.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 要点\n",
    "\n",
    "在这个演示中，我们看到了两种强化学习算法：简单的策略梯度算法和更复杂的演员-评论家算法。可以看到，这些算法使用的是状态、动作和奖励的抽象概念，因此它们可以应用于非常不同的环境。\n",
    "\n",
    "强化学习使我们能够仅通过观察最终奖励来学习解决问题的最佳策略。不需要标注数据集这一点使我们可以多次重复模拟来优化模型。然而，强化学习领域仍然存在许多挑战，如果你决定更多地关注这个有趣的人工智能领域，你将会进一步了解这些挑战。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n---\n\n**免责声明**：  \n本文档使用AI翻译服务 [Co-op Translator](https://github.com/Azure/co-op-translator) 进行翻译。尽管我们努力确保翻译的准确性，但请注意，自动翻译可能包含错误或不准确之处。原始语言的文档应被视为权威来源。对于重要信息，建议使用专业人工翻译。我们不对因使用此翻译而产生的任何误解或误读承担责任。\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3.10.4 64-bit",
   "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.10.4"
  },
  "orig_nbformat": 4,
  "vscode": {
   "interpreter": {
    "hash": "916dbcbb3f70747c44a77c7bcd40155683ae19c65e1c03b4aa3499c5328201f1"
   }
  },
  "coopTranslator": {
   "original_hash": "04f8d9978cd11281d81dd037cbf6ce20",
   "translation_date": "2025-08-31T09:26:08+00:00",
   "source_file": "lessons/6-Other/22-DeepRL/CartPole-RL-PyTorch.ipynb",
   "language_code": "zh"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}