# Section 0 问题描述与完成项目流程

## 1. 问题描述

</img>

在该项目中，你将使用强化学习算法，实现一个自动走迷宫机器人。

1. 如上图所示，智能机器人显示在右上角。在我们的迷宫中，有陷阱（红色炸弹）及终点（蓝色的目标点）两种情景。机器人要尽量避开陷阱、尽快到达目的地。
2. 小车可执行的动作包括：向上走 `u`、向右走 `r`、向下走 `d`、向左走 `l`。
3. 执行不同的动作后，根据不同的情况会获得不同的奖励，具体而言，有以下几种情况。
    - 撞到墙壁：-10
    - 走到终点：50
    - 走到陷阱：-30
    - 其余情况：-0.1
4. 我们需要通过修改 `robot.py` 中的代码，来实现一个 Q Learning 机器人，实现上述的目标。

## 2. 完成项目流程

1. 配置环境，使用 `envirnment.yml` 文件配置名为 `robot-env` 的 conda 环境，具体而言，你只需转到当前的目录，在命令行/终端中运行如下代码，稍作等待即可。
```
conda env create -f envirnment.yml
```
安装完毕后，在命令行/终端中运行 `source activate robot-env`（Mac/Linux 系统）或 `activate robot-env`（Windows 系统）激活该环境。

2. 阅读 `main.ipynb` 中的指导完成项目，并根据指导修改对应的代码，生成、观察结果。
3. 导出代码与报告，上传文件，提交审阅并优化。
---

---

# Section 1 算法理解

## 1. 1 强化学习总览

强化学习作为机器学习算法的一种，其模式也是让智能体在“训练”中学到“经验”，以实现给定的任务。但不同于监督学习与非监督学习，在强化学习的框架中，我们更侧重通过智能体与环境的**交互**来学习。通常在监督学习和非监督学习任务中，智能体往往需要通过给定的训练集，辅之以既定的训练目标（如最小化损失函数），通过给定的学习算法来实现这一目标。然而在强化学习中，智能体则是通过其与环境交互得到的奖励进行学习。这个环境可以是虚拟的（如虚拟的迷宫），也可以是真实的（自动驾驶汽车在真实道路上收集数据）。

在强化学习中有五个核心组成部分，它们分别是：**环境（Environment）**、**智能体（Agent）**、**状态（State）**、**动作（Action）**和**奖励（Reward）**。在某一时间节点 $t$：

- 智能体在从环境中感知其所处的状态 $s_t$
- 智能体根据某些准则选择动作 $a_t$
- 环境根据智能体选择的动作，向智能体反馈奖励 $r_{t+1}$

通过合理的学习算法，智能体将在这样的问题设置下，成功学到一个在状态 $s_t$ 选择动作 $a_t$ 的策略 $\pi (s_t) = a_t$。

---

**问题 1**：请参照如上的定义，描述出 “机器人走迷宫这个问题” 中强化学习五个组成部分对应的实际对象：

- **环境** : 迷宫环境
- **状态** : 撞到墙壁，踩到陷阱，到达终点，其余情况

- **动作** : 前后左右
- **奖励** : -30，-10，-0.1，+50

$$T(s^{'}, a, s) = P(s^{'}|a,s)$$

---

## 1.2 计算 Q 值

在我们的项目中，我们要实现基于 Q-Learning 的强化学习算法。Q-Learning 是一个值迭代（Value Iteration）算法。与策略迭代（Policy Iteration）算法不同，值迭代算法会计算每个”状态“或是”状态-动作“的值（Value）或是效用（Utility），然后在执行动作的时候，会设法最大化这个值。因此，对每个状态值的准确估计，是我们值迭代算法的核心。通常我们会考虑**最大化动作的长期奖励**，即不仅考虑当前动作带来的奖励，还会考虑动作长远的奖励。

在 Q-Learning 算法中，我们把这个长期奖励记为 Q 值，我们会考虑每个 ”状态-动作“ 的 Q 值，具体而言，它的计算公式为：

$$
q(s_{t},a) = R_{t+1} + \gamma \times\max_a q(a,s_{t+1})
$$

也就是对于当前的“状态-动作” $(s_{t},a)$，我们考虑执行动作 $a$ 后环境给我们的奖励 $R_{t+1}$，以及执行动作 $a$ 到达 $s_{t+1}$后，执行任意动作能够获得的最大的Q值 $\max_a q(a,s_{t+1})$，$\gamma$ 为折扣因子。

不过一般地，我们使用更为保守地更新 Q 表的方法，即引入松弛变量 $alpha$，按如下的公式进行更新，使得 Q 表的迭代变化更为平缓。

$$
q(s_{t},a) = (1-\alpha) \times q(s_{t},a) + \alpha \times(R_{t+1} + \gamma \times\max_a q(a,s_{t+1}))
$$

---
</img>

**问题 2**：根据已知条件求 $q(s_{t},a)$，在如下模板代码中的空格填入对应的数字即可。

已知：如上图，机器人位于 $s_1$，行动为 `u`，行动获得的奖励与题目的默认设置相同。在 $s_2$ 中执行各动作的 Q 值为：`u`: -24，`r`: -13，`d`: -0.29、`l`: +40，$\gamma$ 取0.9。

$$
\begin{align}
q(s_{t},a) & = R_{t+1} + \gamma \times\max_a q(a,s_{t+1}) \\
 & =(0.1) + (0.9)*(40) \\
 & =(36.1)
\end{align}
$$

---

## 1.3 如何选择动作

在强化学习中，「探索-利用」问题是非常重要的问题。具体来说，根据上面的定义，我们会尽可能地让机器人在每次选择最优的决策，来最大化长期奖励。但是这样做有如下的弊端：
1. 在初步的学习中，我们的 Q 值会不准确，如果在这个时候都按照 Q 值来选择，那么会造成错误。
2. 学习一段时间后，机器人的路线会相对固定，则机器人无法对环境进行有效的探索。

因此我们需要一种办法，来解决如上的问题，增加机器人的探索。由此我们考虑使用 epsilon-greedy 算法，即在小车选择动作的时候，以一部分的概率随机选择动作，以一部分的概率按照最优的 Q 值选择动作。同时，这个选择随机动作的概率应当随着训练的过程逐步减小。

---

**问题 3**：在如下的代码块中，实现 epsilon-greedy 算法的逻辑，并运行测试代码。

```python
import random
import numpy as np

actions = ['u','r','d','l']
qline = {'u':1.2, 'r':-2.1, 'd':-24.5, 'l':27}
epsilon = 0.3 # 以0.3的概率进行随机选择

def choose_action(actions,qline,epsilon):

    action = None
    if random.random()<epsilon: # 以某一概率
        action=np.random.choice(actions) # 实现对动作的随机选择
    else:
        action=max(qline,key=qline.get) # 否则选择具有最大 Q 值的动作
    return action
```

```python
action=choose_action(actions,qline,epsilon)
print(action)
```

    u

---

---

# Section 2 代码实现

## 2.1. `Maze` 类理解

我们首先引入了迷宫类 `Maze`，这是一个非常强大的函数，它能够根据你的要求随机创建一个迷宫，或者根据指定的文件，读入一个迷宫地图信息。

1. 使用 `Maze("file_name")` 根据指定文件创建迷宫，或者使用 `Maze(maze_size=(height,width))` 来随机生成一个迷宫。
2. 使用 `trap_number` 参数，在创建迷宫的时候，设定迷宫中陷阱的数量。
3. 直接键入迷宫变量的名字按回车，展示迷宫图像（如 `g=Maze("xx.txt")`，那么直接输入 `g` 即可。
4. 建议生成的迷宫尺寸，长在 6~12 之间，宽在 10～12 之间。

---

**问题 4**：在如下的代码块中，创建你的迷宫并展示。

```python
from Maze import Maze
%matplotlib inline
%config InlineBackend.figure_format = 'retina'

## todo: 创建迷宫并展示
mario=Maze(maze_size=(8,12),trap_number=2)
mario
```

---

你可能已经注意到，在迷宫中我们已经默认放置了一个机器人。实际上，我们为迷宫配置了相应的 API，来帮助机器人的移动与感知。其中你随后会使用的两个 API 为 `maze.sense_robot()` 及 `maze.move_robot()`。

1. `maze.sense_robot()` 为一个无参数的函数，输出机器人在迷宫中目前的位置。
2. `maze.move_robot(direction)` 对输入的移动方向，移动机器人，并返回对应动作的奖励值。

---

**问题 5**：随机移动机器人，并记录下获得的奖励，展示出机器人最后的位置。

```python
rewards = []

## 循环、随机移动机器人10次，记录下奖励
for i in range(10):
    action=np.random.choice(actions)
    rewards.append(mario.move_robot(action))

## 输出机器人最后的位置
print(mario.sense_robot())
print(rewards)

## 打印迷宫，观察机器人位置
```

    (0, 8)
    [-10.0, -30.0, -0.1, -0.1, -0.1, -10.0, -0.1, -0.1, -10.0, -0.1]

## 2.2. `Robot` 类实现

`Robot` 类是我们需要重点实现的部分。在这个类中，我们需要实现诸多功能，以使得我们成功实现一个强化学习智能体。总体来说，之前我们是人为地在环境中移动了机器人，但是现在通过实现 `Robot` 这个类，机器人将会自己移动。通过实现学习函数，`Robot` 类将会学习到如何选择最优的动作，并且更新强化学习中对应的参数。

首先 `Robot` 有多个输入，其中 `alpha=0.5, gamma=0.9, epsilon0=0.5` 表征强化学习相关的各个参数的默认值，这些在之前你已经了解到，`Maze` 应为机器人所在迷宫对象。

随后观察 `Robot.update` 函数，它指明了在每次执行动作时，`Robot` 需要执行的程序。按照这些程序，各个函数的功能也就明了了。

最后你需要实现 `Robot.py` 代码中的8段代码，他们都在代码中以 `#TODO` 进行标注，你能轻松地找到他们。

---

**问题 6**：实现 `Robot.py` 中的8段代码，并运行如下代码检查效果（记得将 `maze` 变量修改为你创建迷宫的变量名）。

```python
from Robot import Robot
robot = Robot(mario) # 记得将 maze 变量修改为你创建迷宫的变量名
robot.set_status(learning=True,testing=False)
print(robot.update())

mario
```

---

## 2.3 用 `Runner` 类训练 Robot

在实现了上述内容之后，我们就可以开始对我们 `Robot` 进行训练并调参了。我们为你准备了又一个非常棒的类 `Runner`，来实现整个训练过程及可视化。使用如下的代码，你可以成功对机器人进行训练。并且你会在当前文件夹中生成一个名为 `filename` 的视频，记录了整个训练的过程。通过观察该视频，你能够发现训练过程中的问题，并且优化你的代码及参数。

---

**问题 7**：尝试利用下列代码训练机器人，并进行调参。可选的参数包括：

- 训练参数
    - 训练次数 `epoch`
- 机器人参数：
    - `epsilon0` (epsilon 初值)
    - `epsilon`衰减（可以是线性、指数衰减，可以调整衰减的速度），你需要在 Robot.py 中调整
    - `alpha`
    - `gamma`
- 迷宫参数:
    - 迷宫大小
    - 迷宫中陷阱的数量

```python
## 可选的参数：
epoch = 500

epsilon0 = 0.05
alpha = 0.5
gamma = 0.5

maze_size = (8,12)
trap_number = 2
```

```python
from Runner import Runner

g = Maze(maze_size=maze_size,trap_number=trap_number)
r = Robot(g,alpha=alpha, epsilon0=epsilon0, gamma=gamma)
r.set_status(learning=True)

runner = Runner(r, g)
runner.run_training(epoch, display_direction=True)
#runner.generate_movie(filename = "final1.mp4") # 你可以注释该行代码，加快运行速度，不过你就无法观察到视频了。
```

---

使用 `runner.plot_results()` 函数，能够打印机器人在训练过程中的一些参数信息。

- Success Times 代表机器人在训练过程中成功的累计次数，这应当是一个累积递增的图像。
- Accumulated Rewards 代表机器人在每次训练 epoch 中，获得的累积奖励的值，这应当是一个逐步递增的图像。
- Running Times per Epoch 代表在每次训练 epoch 中，小车训练的次数（到达终点就会停止该 epoch 转入下次训练），这应当是一个逐步递减的图像。

---

**问题 8**：使用 `runner.plot_results()` 输出训练结果，根据该结果对你的机器人进行分析。

- 指出你选用的参数如何，选用参数的原因。
- 建议你比较不同参数下机器人的训练的情况。
- 训练的结果是否满意，有何改进的计划。

```python
runner.plot_results()
```

```python
epoch = 500

epsilon0 = 0.05
#alpha = 0.5
gamma = 0.5

maze_size = (8,12)
trap_number = 2
for alpha in np.linspace(0.1,0.9,9):
    g = Maze(maze_size=maze_size,trap_number=trap_number)
    r = Robot(g,alpha=alpha, epsilon0=epsilon0, gamma=gamma)
    r.set_status(learning=True)

    runner = Runner(r, g)
    runner.run_training(epoch, display_direction=True)
    runner.plot_results()

```

(回答区)

---

**问题 9：** 请将如下的文件打包，提交文件给审阅者。

- `robot.py`
- `robot_maze.ipynb`
- 由 `robot_maze.ipynb` 导出的 `robot_maze.html`


# Portfolio Choice with Risky Housing

```python
# This cell does some preliminary set up

# Packages
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
# This is a jupytext paired notebook that autogenerates BufferStockTheory.py
# which can be executed from a terminal command line via "ipython BufferStockTheory.py"
# But a terminal does not permit inline figures, so we need to test jupyter vs terminal
# Google "how can I check if code is executed in the ipython notebook"
from IPython import get_ipython  # In case it was run from python instead of ipython

# Import relevenat HARK tools
from ConsPortfolioHousingModel import PortfolioRentalHousingType

# If the ipython process contains 'terminal' assume not in a notebook
def in_ipynb():
    try:
        if "terminal" in str(type(get_ipython())):
            return False
        else:
            return True
    except NameError:
        return False

# Determine whether to make the figures inline (for spyder or jupyter)
# vs whatever is the automatic setting that will apply if run from the terminal
if in_ipynb():
    # %matplotlib inline generates a syntax error when run from the shell
    # so do this instead
    get_ipython().run_line_magic("matplotlib", "inline")
else:
    get_ipython().run_line_magic("matplotlib", "auto")
```

### The base model

\begin{equation}
	\begin{gathered}
		v^R_{t}(m_{t}) =  \max_{x_{t}, \varphi_{t}}u(x_t) + \beta \mathbb{E} \Gamma_{t+1}^{1-\rho} v^R_{t+1}(m_{t+1}) \\
		\text{s.t.} \quad a_t = m_t - x_t, \quad x_t, a_t \ge 0 \\
		c_t = (1-\alpha) x_t, \quad h_t = \alpha x_t \\
		m_{t+1} = \theta_{t+1} + a_t(\varphi_t R_{t+1} + (1-\varphi_t)R_f)/\Gamma_{t+1}
	\end{gathered}
\end{equation}

### Calibration

We replicate the previously defined parameter values and transition processes for state variables and shocks, and format them into the structures required by HARK.

This is done in the external file [Calibration/params.py](https://github.com/matthew-zahn/CGMPort/blob/master/CGMPort-Shared/Code/Python/Calibration/params.py), which we now import.

```python
# Calibrate the model in line with the information above
import sys, os

# The following code navigates to another directory where a python script with the parameters for the model is saved.
sys.path.append(os.path.realpath("Calibration/"))
# Loading the parameters from the Calibration/params.py script
from params import dict_portfolio, time_params, det_income, age_plot_params
```

All of the model's parameters are contained in the structure <tt>dict_portfolio<tt>, which can now be passed to HARK's <tt>PortfolioConsumerType<tt> to build and solve a representation of our problem.

```python
# add RntHseShare to model, assuming households spend 30%
# of expenditures on rental housing
dict_portfolio["RntHseShare"] = 0.30

from ConsPortfolioHousingModel import init_portfolio_housing
init_portfolio_housing["RntHseShare"] = 0.30
# Solve the model with the given parameters
agent = PortfolioRentalHousingType(**init_portfolio_housing)
agent.solve()
```

```python
# Define a normalization factor
norm_factor = det_income * np.exp(0)
```

### Key Results

#### The optimal risky asset share

The plot below shows the policy function for the risky portfolio share as a function of wealth at different ages.

The optimal risky share is decreasing in wealth. The authors argue this is due to the fact that, at low levels of wealth, relatively safe human wealth represents a higher fraction of the consumer's wealth, so he shifts his tradeable wealth towards riskier alternatives.

Analyzing the policy rule by age also shows that the risky share increases from young to middle age, and decreases from middle to old age. This is consistent with the previous interpretation: shares trace the humped shape of labor earnings.

These estimates are different from what is produced in the original paper, which are also reproduced below. Generally, the policy functions do not share the same curvature, which leads to greater reductions in the optimal portfolio share at lower levels of wealth.

```python
# Plot portfolio rule
eevalgrid = np.linspace(0, 300, 100)
plt.figure()
# In the last period of life you consume everything
# so portfolio choice is irrelevant

# Ages
ages = age_plot_params
age_born = time_params["Age_born"]
for a in ages:
    plt.plot(
        eevalgrid,
        agent.solution[a - age_born].ShareFuncAdj(
            eevalgrid / norm_factor[a - age_born]
        ),
        label="Age = %i" % (a * 5 + 25),
    )
plt.xlabel("Wealth")
plt.ylabel("Risky portfolio share")
plt.title("Risky Portfolio Policy Function")
plt.legend()
plt.grid()

if not in_ipynb():
    plt.show(block=False)
    plt.pause(1)
else:
    plt.show(block=True)
```

We present the author's originally reported policy functions for comparison

#### Consumption behavior

The plot below shows the policy functions for consumption, rental housing, and total expenditures as a function of wealth at different ages.

At all age levels consumption increases with wealth. The consumption function also appears to shift upwards as life progresses.

Our consumption policy functions again do not match those of the original paper, which are also reproduced below. Consumption also appears to increase with age in our policy functions that does not come through in the results presented in the paper.

```python
# Plot consumption function
plt.figure()
ages = age_plot_params
for a in ages:
    plt.plot(
        eevalgrid,
        agent.solution[a - age_born].cFuncAdj(eevalgrid / norm_factor[a - age_born])
        * norm_factor[a - age_born],
        label="Age = %i" % (a * 5 + 25),
    )
plt.xlabel("Wealth")
plt.ylabel("Consumption")
plt.title("Consumption Policy Function")
plt.legend()
plt.grid()

if not in_ipynb():
    plt.show(block=False)
    plt.pause(1)
else:
    plt.show(block=True)
```

```python
# Plot rental housing function
plt.figure()
ages = age_plot_params
for a in ages:
    plt.plot(
        eevalgrid,
        agent.solution[a - age_born].hFuncAdj(eevalgrid / norm_factor[a - age_born])
        * norm_factor[a - age_born],
        label="Age = %i" % (a * 5 + 25),
    )
plt.xlabel("Wealth")
plt.ylabel("Rental Housing")
plt.title("Housing Policy Function")
plt.legend()
plt.grid()

if not in_ipynb():
    plt.show(block=False)
    plt.pause(1)
else:
    plt.show(block=True)
```

```python
# Plot total expenditures function
plt.figure()
ages = age_plot_params
for a in ages:
    plt.plot(
        eevalgrid,
        agent.solution[a - age_born].TotalExpAdj(eevalgrid / norm_factor[a - age_born])
        * norm_factor[a - age_born],
        label="Age = %i" % (a * 5 + 25),
    )
plt.xlabel("Wealth")
plt.ylabel("Total Expenditures")
plt.title("Expenditures Policy Function")
plt.legend()
plt.grid()

if not in_ipynb():
    plt.show(block=False)
    plt.pause(1)
else:
    plt.show(block=True)
```

We again present the author's originally reported policy functions for comparison

### Simulations

Using the policy functions obtained from solving the model we present a series of simulations to highlight features of the model.

We first run a few simulations to verify the quality of our calibration.

The figures below show simulated levels of permanent income and risky portfolio shares for 5 agents over their life spans. We can see the model generates a heterogeneous permanent income distribution. Interestingly, all of these agents tend to follow the same general pattern for investing in the risky asset. Early in life, all of their portfolios are invested in the risky asset. This declines as the agent ages and converges to approximately 35% once they reach retirement.

```python
# Set up simulation parameters

# Number of agents and periods in the simulation.
agent.AgentCount = 50  # Number of instances of the class to be simulated.
# Since agents can die, they are replaced by a new agent whenever they do.

# Number of periods to be simulated
agent.T_sim = 80

# Set up the variables we want to keep track of.
agent.track_vars = ["aNrm", "cNrm", "pLvl", "t_age", "Share", "mNrm"]

# Run the simulations
agent.initialize_sim()
agent.simulate()

# Present diagnostic plots.
plt.figure()
plt.plot(agent.history["t_age"] + time_params["Age_born"], agent.history["pLvl"], ".")
plt.xlabel("Age")
plt.ylabel("Permanent income")
plt.title("Simulated Income Paths")
plt.grid()

if not in_ipynb():
    plt.show(block=False)
    plt.pause(1)
else:
    plt.show(block=True)

plt.figure()
plt.plot(agent.history["t_age"] + time_params["Age_born"], agent.history["Share"], ".")
plt.xlabel("Age")
plt.ylabel("Risky share")
plt.title("Simulated Risky Portfolio Shares")
plt.grid()

if not in_ipynb():
    plt.show(block=False)
    plt.pause(1)
else:
    plt.show(block=True)
```

#### The average life cycle patterns

We now increase the number of simulations to examine and compare the behavior of the mean values of variables of interest at different ages, conditional on survival. In each case we present the original plots from the paper for reference.

The plot below illustrates the average dynamics of permanent income, consumption, and market resources across all of the simulated agents. The plot follows the general pattern observed in the original paper. However, our results show that the agents are accumulating significantly more market resources.

```python
# Number of agents and periods in the simulation.
agent.AgentCount = 50  # Number of instances of the class to be simulated.
# Since agents can die, they are replaced by a new agent whenever they do.

# Number of periods to be simulated
agent.T_sim = 15 * 50

# Run the simulations
agent.initialize_sim()
agent.simulate()

raw_data = {
    "Age": agent.history["t_age"].flatten() + time_params["Age_born"] - 1,
    "pIncome": agent.history["pLvl"].flatten(),
    "rShare": agent.history["Share"].flatten(),
    "nrmM": agent.history["mNrm"].flatten(),
    "nrmC": agent.history["cNrm"].flatten(),
}

Data = pd.DataFrame(raw_data)
Data["Cons"] = Data.nrmC * Data.pIncome
Data["M"] = Data.nrmM * Data.pIncome

# Find the mean of each variable at every age
AgeMeans = Data.groupby(["Age"]).mean().reset_index()
```

    C:\Users\alujan\anaconda3\envs\PortfolioChoiceWithRiskyHousing-Latest\lib\site-packages\HARK\core.py:865: RuntimeWarning: invalid value encountered in add
      self.history[var_name] = np.empty((self.T_sim, self.AgentCount)) + np.nan

```python
plt.figure()
plt.plot(AgeMeans.Age, AgeMeans.pIncome, label="Income")
plt.plot(AgeMeans.Age, AgeMeans.M, label="Market resources")
plt.plot(AgeMeans.Age, AgeMeans.Cons, label="Consumption")
plt.legend()
plt.xlabel("Age")
plt.title("Variable Means Conditional on Survival")
plt.grid()

if not in_ipynb():
    plt.show(block=False)
    plt.pause(1)
else:
    plt.show(block=True)
```

```python
# Find age percentiles
AgePC5 = Data.groupby(["Age"]).quantile(0.05).reset_index()
AgePC95 = Data.groupby(["Age"]).quantile(0.95).reset_index()

# plot till death - 1
age_1 = time_params["Age_death"] - time_params["Age_born"]

plt.figure()
plt.ylim([0, 1.1])
plt.plot(AgeMeans.Age[:age_1], AgeMeans.rShare[:age_1], label="Mean")
plt.plot(AgePC5.Age[:age_1], AgePC5.rShare[:age_1], "--r", label="Perc. 5")
plt.plot(AgePC95.Age[:age_1], AgePC95.rShare[:age_1], "--g", label="Perc. 95")
plt.legend()

plt.xlabel("Age")
plt.ylabel("Risky Share")
plt.title("Risky Portfolio Share Mean Conditional on Survival")
plt.grid()

if not in_ipynb():
    plt.show(block=False)
    plt.pause(1)
else:
    plt.show(block=True)
```

```python
ratio = (np.log(np.array(agent.RiskyAvg)) - np.log(agent.Rfree)) / (
            agent.CRRA * np.array(agent.RiskyStd) ** 2)
ratio = np.append(ratio, 0.0)

AgeMeans["nrmH"] = agent.hNrm
AgeMeans["RiskyAvg"] = agent.RiskyAvg + [0]
AgeMeans["RiskyStd"] = agent.RiskyStd + [0]
AgeMeans["Share"] = ratio * (1 + AgeMeans["nrmH"] / AgeMeans["nrmM"])
AgeMeans
```

```python
agent.RiskyStdTrue
```

```python

```


# Announcements
- Problem 3 posted on D2L, due on 9/21/2020. Submission of notebooks encouraged(one per problem set, each problem labeled)
- Solutions to Monday's breakout room exercise posted on D2L in ``Lecture 9``
- Markdown cheat sheet posted on D2L in ``Course Notes``

# Intro to Ordinary Differential Equations - Euler's method

Based on notes and notebooks by Niels Henrik Aase, Thorvald Ballestad, Vasilis Paschalidis and Jon Andreas Støvneng

## Introduction: ODEs in Physics

Physics equations of motion require the solution of ordinary
differential equations (ODE). While the vast majority of equations in
Physics are partial differential equations, ordinary differential
equations pop-up in scenarios where a high degree of symmetry and/or
stationarity (time-independence) is imposed (e.g., stationary and
spherically symmetric). For example, in Newtonian gravity the
equation for the gravitational potential is

$$
\nabla^2 \Phi = 4\pi G \rho,
$$
but in spherical symmetry this becomes an ODE

$$
\frac{1}{r^2}\frac{d}{dr}\left(r^2\frac{d}{dr} \Phi\right) = 4\pi G \rho.
$$

The Newtonian equation of motion is also a second-order ODE in time (acceleration=force/mass),
$$
\frac{d^2 x}{dt^2} = F/m
$$
For a very complex density distribution or complicated
(time-dependent) forces, these last equations are not easily solved
analytically. So, in such cases we need to solve the ODEs numerically.
There are multiple ways one can numerically solve ODEs, and we will
review here some of these methods including the Euler, and Runge-Kutta
methods. We will also discuss the distinction between initial value
and boundary value problems.

## Theory

### Initial value problems
An initial value problem (IVP) for ODEs is described by the following
equations
$$
\frac{d}{dt} y(t)=g(y(t), t), a \leq t \leq b,\;\; \mbox{ Initial conditions: } y(a)=\alpha
$$
for a single ODE, and
$$
\frac{d}{dt} \vec y=\vec G(y(t), t), a \leq t \leq b,\;\; \mbox{ Initial conditions: } \vec y(a)=\vec\alpha,
$$
where
$$
\vec y =(y_1, y_2, \ldots y_n), \ \vec G =(g_1(\vec y(t), t), g_2(\vec y(t), t) \ldots g_n(\vec y(t), t)).
$$
for a system of coupled ODEs.

Before we decide to code any differential equation in a computer it is
always important to be able to answer the following questions:
- (1) Does the ODE have a solution?
- (2) Is the ODE solution unique for a given initial conditions?
- (3) Does the solution depend continuously on the initial conditions?

All three questions point to the concept of
well-posedness of the IVP. *If the answer is NO to any of these 3
questions, the IVP is not well-posed, and it is redundant to try to
solve the ODE as an IVP, as the numerical algorithm will fail.* In this
first section we will introduce the mathematical background for
answering the above questions.

#### Definitions
- We say that $g(y,t)$ satisfies a **Lipschitz condition** (or is Lipschitz continuous)
  in the variable $y$ on a set $D \subset \mathbb{R}^2$, if a constant $L>0$
  exists with $|g(y_1,t)-g(y_2,t)| \leq L |y_1-y_2|$.

  For example, $d(y,t)=ty$, $D=\{(y,t) | -3\leq y \leq
  4, 1\leq t\leq 2 \}$, then for $(y_1,t)$, $(y_2,t)$ we have $|g(y_1,t)-g(y_2,t)| =
  |t| |y_1-y_2| \leq 2|y_1-y_2|$. Thus, $L=2$ and $g(y,t)=ty$
  is Lipschitz continuous.

-  A set $D\subset \mathbb{R}$ is called **convex**, if for $(y_1,t_1)$ and $(y_2,t_2) \in D$, then
  $[(1-\lambda) y_1 + \lambda y_2,(1-\lambda) t_1 + \lambda t_2] \in D \ \forall \lambda \in [0,1]$

#### Theorem 1:
If $g(y,t)$ is defined on a convex set $D \subset
\mathbb{R}^2$, and there exists a postive $L>0$ such that $\bigg\lvert
\frac{\partial}{\partial y} g(y,t)\bigg\lvert \leq L$ $\forall (t,y)\in D$, then $g$
is Lipschitz continuous on $D$, with
Lipschitz constant $L$.

#### Theorem 2:
If $D = \{(y,t)\ |-\infty
  < y < \infty,\ a \leq t \leq b\}$, and $g(y,t)$ is continuous on $D$, and $g(y,t)$
satisfies a Lipschitz condition on $D$ in $y$, then the IVP
$$
\frac{dy}{dt}=g(y,t), a \leq t \leq b, \mbox{ Initial conditions: } y(a)=\alpha
$$
has a _unique_ solution $y(t)$ for $a\leq t\leq b$.

For example, the IVP
$$\frac{dy}{dt}=g(t,y)=1+t\sin(yt), 0 \leq t \leq 2, \mbox{ Initial conditions: } y(a)=\alpha
$$
satisfies
$$
\frac{\partial}{\partial y}g(y,t)=t^2 \cos(yt) \leq 4,
$$
Thus, by theorems 1 and 2, the IVP with $g(y,t)=1+t\sin(t y)$ has a unique solution.

#### Definition: Well-posedness of the IVP
The IVP
$$\frac{dy}{dt}=g(y,t), a \leq t \leq b, \mbox{ Initial conditions: } y(a)=\alpha
$$
is __well-posed__ if
- A unique solution $y(t)$ exists
- There exist constants $\epsilon_0>0$, $k>0$ such that $\forall
  \epsilon >0$ with $\epsilon_0 > \epsilon >0$, whenever $\delta(t)$
  is continuous with $|\delta(t)| < \epsilon$ $\forall t \in [a,b]$,
  and $\delta_0 < \epsilon$, the IVP
$$    \frac{dz}{dt}=g(z,t)+\delta(t), a \leq t \leq b, \mbox{ Initial conditions: } z(a)=\alpha+\delta_0 \label{pert}\tag{1}
$$  has a unique solution $z(t)$ that satisfies
$$    |z(t)-y(t)| < k \epsilon_0, \ \forall t \in [a,b].\label{stab}\tag{2}$$

#### Theorem 3
Assume $D = \{(y,t)\ |-\infty < y < \infty,\ a \leq t \leq b\}$, where $g(y,t)$ is continuous and is
Lipschitz continuous in $y$ on $D$, then the IVP
$$\frac{dy}{dt}=g(y,t), a \leq t \leq b, \mbox{ Initial conditions: } y(a)=\alpha$$
is well-posed.

Well-posedness is a fundamental property of initial value problems for
ordinary and partial differential equations. If a differential
equation does not admit a well-posed initial value problem, then we
cannot integrate it numerically. This is because, well-posedness is
intimately related with the "stability" of the solution as stated in
Eq.\ref{stab}. This equation implies that the perturbed initial
value problem must have a solution that is very close to the
unperturbed one, i.e., the absolute difference between the solutions
$z(t)$ and $y(t)$ is bounded. If a small perturbation either in
initial conditions or in the right-hand-side of Eq.\ref{pert} leads
the difference between the solution $z(t)$ and $y(t)$ to grow without
bound, then this problem is ill-posed and cannot be integrated in a
computer. The reason is that computers will always have finite error,
i.e., will always apply small perturbations in the form of either
truncation or round-off error. More specifically well-posedness
implies continuous dependence on the initial data.

## Getting started

Let's start by giving a concrete example on what kind of problem we wish to solve. Consider the first order differential equation
$$
    \frac{d}{dt} y(t) = g(y(t), t),
    \label{genODE}\tag{3}
$$
where $y(t)$ is the function we want to calculate, and $g(y(t), t)$ is a function that can depend on $y(t)$, but it also might have explicit time dependence. If we have the initial condition $y(0) = y_0$, how do we solve \eqref{genODE}? Analytically there exists a multitude of different schemes to solve these kinds of equations, all with their own limitations and areas of use.

However, all these analytical schemes depend on the __existence__ of an analytical solution. By solving equation \eqref{genODE} numerically we are not restricted to these kinds of ODEs, and thus we have opened a Pandora's box of new ODEs that we can solve!

## Theory

### Discretization

One of the downsides by using numerical schemes to solve ODEs is that we have to *discretize* our time variables. This means that the time variable $t$, only can take a set of predetermined discrete time values, called grid points, it is no longer a continuous variable. We define this set of possible time values to be on the form

$$
t_n = t_0 + nh, \quad \mathrm{with} \quad n = 0, 1, 2,..., N,
$$

where $t_0$ often the time value where we know our initial condition, and $h$ is the size between adjacent discrete time values. The relation between $N$ and $h$ is given by

\begin{equation}
h = \frac{t_N - t_0}{N},
\label{hN}\tag{4}
\end{equation}

where $N + 1$ is the number of discrete time points we have in our simulation (where the + 1 appears due to our choice of zero-indexing) , while $t_N$ denote the largest time value we have in our simulation. You might think of $h$ as the coarseness of our time variable; the smaller $h$, the more grid points we need to cover the same time interval. For instance, if we want to discretize the interval $[0,1]$ such that we have grid points every 0.01 seconds, i.e. the distance between grid points is 0.01 (recall that this is our definition of $h$), we use equation \eqref{hN}, to deduce that we need $N=100$. In general, our numerical approximation will be better if we choose a small $h$. Note as the size of $h$ decreases, the number of discrete time values between $t_0$ and $t_N$ increases. We pay for the increased level of precision by increasing the number of calculations needed, thus increasing the runtime of our program. It is highly encouraged to reflect over what the necessary coarseness of our discretized time variable before solving a problem.

### Euler's method

There are several ways to derive Euler's method. Here we present a proof which revolve around Taylor series, thus basic calculus is required to follow the proof. Any "nice" (We do not elaborate what this requires) function $y(t)$ can be written as

$$
y(t) = \sum_{n=0}^{\infty} \frac{y^{(n)}(t_0)}{n!}(t-t_0)^n, = y(t_0) + \frac{y'(t_0)}{1!}(t-t_0) + \frac{y''(t_0)}{2!}(t-t_0)^2 + ...
\label{Taylor}\tag{5}
$$

where $t_0$ is an arbitrary value. We say that we expand $y(t)$ around $t_0$. Now we use Taylor's theorem and truncate the series at the first order

$$
y(t_0 + h) = y(t_0) + \frac{y'(t_0)}{1!}h + \frac{1}{2!}h^2 y''(\tau),
\label{Taylor_trunc}\tag{6}
$$

for some $\tau \in [t_0, t_0 + h]$. Reshuffling equation \eqref{Taylor_trunc}, and solving for $y'(t_0)$ we get

$$
y'(t_0) = \frac{y(t_0 + h)- y(t_0)}{h} + \mathcal{O}(h),
\label{deriv}\tag{7}
$$

where we use *[big O notation](https://en.wikipedia.org/wiki/Big_O_notation)*. Now we use the essential idea of Euler's method, from \eqref{genODE}, we know the exact expression of $y'(t_0)$ because we know that $y'(t) = g(y(t), t)$! Inserting this into \eqref{deriv} and solving for $y(t_0 + h)$ we get

$$
y(t_0 + h) = y(t_0) + h g(y(t_0), t_0) + \mathcal{O}(h^2).
\label{next_step}\tag{8}
\$$

Thus, by equation \eqref{next_step} we have an estimate for what the value of $y$ is at our first grid point $t= t_0 + h$! By choosing $h$ small enough (and hoping that the $ \mathcal{O}$ term is not too large), our estimate will be quite precise. Neglecting the $ \mathcal{O}$ term, we write

\begin{equation}
y(t_0 + h) \approx y(t_0) + h g(y(t_0), t_0).
\label{next_step_ish}
\end{equation}

If we have our initial condition at $t_0$ and denote this value as $y_0$, we can use Euler's method to find an approximation of $y$ at $t_1 = t_0 +h$. $y$ at $t_1$ is denoted as $y_1$. This approximation can be calculated by the formula

$$
y_1 = y_0 + hg(y_0).
$$

Now, to find the population size $y_2$ at $t_2 = t_1 + h = t_0 + 2h$, we use the same formula, but with $y_1$ instead of $y_0$

$$
y_2 = y_1 + h g(y_1).
$$

The most general form of Euler's method is written as

\begin{equation}
y_{n+1} = y_n + h g(y_n).
\label{Euler}
\end{equation}

### Example: Population growth
Now we will illustrate how to implement Euler's method by using the the growth of a population of bacteria as example:
Bacteria reproduce by binary fission. This is a process where a single bacterial cell splits in two, creating two bacterial cells. The two new bacterial cells, can again split into two, thus creating four bacterial cells. In reality, a single bacterial cell can only split in two a certain amount of times, but in our project we model our growth rate as if this was not the case.

We can then write our differential equation as

\begin{equation}
\frac{dy}{dt} = ky(t),
\label{ODE}\tag{9}
\end{equation}

where $y(t)$ is the population $y$ at time $t$, and $k$ is some constant of proportionality. If we use days as time unit, and assume that the population size at $t=1$ is double the population size at the $t=0$, then
$$
y(1)=2y(0),
$$
and $k=\mathrm{ln}(2)$. We can solve equation \eqref{ODE} analytically to obtain $y(t) = 2^t$, so we have something to compare our numerical results to.

```python
# Importing the necessary libraries
import numpy as np # NumPy is used to generate arrays and to perform some mathematical operations
import matplotlib.pyplot as plt # Used for plotting results

```

```python
def step_Euler(y, h, f):
    """Performs a single step of Euler's method.

    Parameters:
            y: Numerical approximation of y at time t
            h: Step size
            f: RHS of our ODE (RHS = Right hand side). Can be any function that only has y as a variable.
        Returns:
            next_y: Numerical approximation of y at time t+h
    """
    next_y = y + h * f(y)
    return next_y

def full_Euler(h, f, y_0 = 1, start_t = 0, end_t = 1):
    """ A full numerical aproximation of an ODE in a set time interval. Performs consecutive Euler steps
    with step size h from start time until the end time. Also takes into account the initial values of the ODE

    Parameters:
            h: Step size
            f: RHS of our ODE
            y_0 : Initial condition for y at t = start_t
            start_t : The time at the initial condtion, t_0
            end_t : The end of the interval where the Euler method is perfomed, t_N
        Returns:
            y_list: Numerical approximation of y at times t_list
            t_list: Evenly spaced discrete list of time with spacing h.
                    Starting time = start_t, and end time = end_t
    """
    # Number of discretisation steps
    N = int((end_t - start_t) / h)
    # Following the notation in the theory, we have N+1 discrete time values linearly spaced
    t_list = np.linspace(start_t, end_t, N + 1)

    # Initialise array to store y-values
    y_list = np.zeros(N + 1)
    # Assign initial condition to first element
    y_list[0] = y_0

    # Assign the rest of the array using N Euler_steps
    for i in range(0, N):
        y_list[i + 1] = step_Euler(y_list[i], h, f)
    return y_list, t_list
```

Now that we have our functions defined, we only need to define our RHS (Right hand side) of our differential equation, which we in the theory part denoted as $g(y)$.

```python
def g(y):
    """Defines the right hand side of our differential equation. In our case of bacterial growth, g(y) = k*y

    Parameters:
            y: Numerical approximation of y at time t
        Returns:
            growth_rate: Current population size multiplied with a constant of proportionality.
            In this case this is equal to ln(2)
    """
    growth_rate = np.log(2)*y
    return growth_rate

# Now we can find the the numerical results from Euler's method
# and compare them to the analytical solution

# Input parameters
y_0 = 1  # Initial population size, i.e. a single bacteria
h = 0.01 # Step size
t_0 = 0  # We define the time at our initial observation as 0
t_N = 10 # 10 days after our initial observation of a single bacteria

# Calculating results from Euler and plotting them
y_list, t_list = full_Euler(h, g, y_0, t_0, t_N)
plt.plot(t_list, y_list, label="Numerical", linewidth=1)

# Plotting the analytical solution derived earlier
plt.plot(t_list,np.power(2, t_list), label="Analytical", linewidth=1)

# Making the plot look nice
plt.legend()
plt.title("The population size of a bacterial colony as a function of time")
plt.xlabel(r'$t$ [days]')
plt.ylabel(r'$y$ [# bacteria]')
plt.show()

# Let's see how far off our numerical approximation is after 5 days.

last_analytical = np.power(2,t_list[-1]) # Extracting the last element of the analytical solution
last_numerical = y_list[-1] # Extracting the last element of the numerical solution

print("After 10 days, our numerical approximation of bacterias is off by: %.2f" %(last_analytical - last_numerical))
```

We see that our model fares quite well with a step size $h=0.0
1$, as it only deviates from the analytical solution by 24 bacteria cells, or 2.4 %. Using a smaller $h$ would yield a smaller error, and this is the next theoretical aspect we will consider.

### Local and Global truncation errors

In the previous theory section, we showed the following relations

\begin{equation}
y(t_0 + h) = y(t_0) + h g(y(t_0), t_0) + \mathcal{O}(h^2)
\\
y(t_0 + h) \approx y(t_0) + h g(y(t_0), t_0).
\label{Euler_approx}\tag{10}
\end{equation}

The first equation is __exact__, while the other is an approximation. We have already used the method of Taylor-expanding a function and truncating it at a certain order to study the convergence rate of different integration algorithms. For ODEs, it becomes important to differentiate between the __local__ and __global__ truncation error, analogous to the trunction error per integration step, and trunctaion error over the full interval.

The __local__ truncation error describes the error we make after each time step. If we have the initial condition $y_0$, we can use our numerical scheme to find an approximation of what $y(t_0 + h)$ ought to be, denoted $y_{approx}(t_0+h)$. This is what we do in the second line above. If we then compare this approximation to the __exact__ solution (i.e. the first line), denoted $y_{exact}(t_0 + h)$, we can find the local truncation error at the first time step, denoted $\tau_1$

\begin{equation}
\tau_1 = \mid y_{exact}(t_0 + h) - y_{approx}(t_0 + h) \mid.
\label{local_trunc_error}\tag{11}
\end{equation}

Using equation \eqref{Euler_approx}, we see that $\tau_1 = \mathcal{O}(h^2)$. Thus our local truncation error is of the second order, meaning that if use a $h$ that is only half of the original $h$, the local truncation error would be one forth of the original size.

Analogous to the local truncation error, we have the __global__ truncation error. At each time step of our numerical simulation, we will have an approximation of $y(t)$ at $t=t_n$, denoted $y_{approx}(t_n)$. The global truncation error, $e_n$. is defined as

\begin{equation}
e_n = \mid y_{exact}(t_n) - y_{approx}(t_n) \mid.
\label{global_trunc_error}\tag{12}
\end{equation}

Equation \eqref{global_trunc_error} describes how far off our numerical scheme is to the exact solution. Note that the global truncation error is not the sum of all the local truncation errors, but rather the accumulation of errors made in each step (i.e. the sum of the local truncation errors __if__ they were defined without the absolute value). Here we present a somewhat heuristic approach to find $e_n$. If we know our local truncation error is of the order $\mathcal{O}(h^2)$, we know that for each time step we get the error $ah^2$, where $a$ is just some constant. In order to get to $t_N$, we need to make $N$ steps, and the number of steps needed is inversely proportional to $h$. Thus the accumulated error $e_N = ah^2 \frac{1}{h} = a h$. Hence, our conclusion is that the global truncation error for the Euler's method is of the order $\mathcal{O}(h)$. Note that this relation holds in general. If a ODE scheme has local truncation error $\mathcal{O}(h^{p+1})$, the global truncation error is $\mathcal{O}(h^{p})$.

Now, let's put the theory derived here into practice!

```python
from prettytable import PrettyTable  # This is imported solely to get the output on a nice format

def trunc_errors(f, base=2, h_max_log=-1, h_min_log=-6, y_0=1, start_t=0, end_t=2):
    """A full numerical approximation of an ODE in a set time interval. Performs consecutive Euler steps
    with step size h from start time until the end time. Also takes into account the initial values of the ODE.
    Returns both the local and global truncation error for each step size.

    Parameters:
            f: RHS of our ODE
            base: The base of our logspace, our h_list is created by taking: base **(h_list_log)
            h_min_log: Our smallest element in our h_list is defined by: base**(h_min_log)
            h_max_log: Our largest element in our h_list is defined by: base**(h_max_log)
            y_0 : Initial condition for y at t = start_t
            start_t : The time at the initial condition, t_0
            end_t : The end of the interval where the Euler method is performed, t_N
        Returns:
            t: Table containing the time step size, the global truncation error and the local truncation error
    """
    K = h_max_log - h_min_log + 1
    h_list = np.logspace(h_min_log, h_max_log, K, base=base)  # Crates an array that is evenly spaced on a logscale
    t = PrettyTable(['h', 'Global truncation error', 'Local truncation error'])
    for i in range(len(h_list)):
        y_list, t_list = full_Euler(h_list[i], g, y_0, start_t, end_t)  # Runs Euler Algorithm with a given h
        analytic_list = np.power(2, t_list)
        # Want to format our output nicely, thus we need to add h, Global trunc error
        # and Local trunc error (for the first time step) to our row
        t.add_row([h_list[i], np.abs(y_list[-1] - analytic_list[-1]), np.abs(y_list[1] - analytic_list[1])])
    t.sortby = "h"  # Formatting the table
    t.reversesort = True
    print(t)
    return t

t = trunc_errors(g, 2,h_min_log=-8)

```

    +------------+-------------------------+------------------------+
    |     h      | Global truncation error | Local truncation error |
    +------------+-------------------------+------------------------+
    |    0.5     |    0.7120865983468994   |  0.06763997209312245   |
    |    0.25    |   0.40885068263221713   |  0.015920319862734678  |
    |   0.125    |   0.22086218920020384   |  0.003864335095264515  |
    |   0.0625   |   0.11506573325497316   | 0.0009520836424172785  |
    |  0.03125   |   0.05876725013275852   | 0.00023629926161827797 |
    |  0.015625  |   0.029702418263456654  | 5.886135545130067e-05  |
    | 0.0078125  |   0.014932231644601224  | 1.4688764678139066e-05 |
    | 0.00390625 |   0.007486540203270664  |  3.66887614022815e-06  |
    +------------+-------------------------+------------------------+

Clearly, the global truncation error is of order $\mathcal{O}(h)$ as each time we cut $h$ in half, the corresponding truncation error is also cut in half, while the local truncation error is one forth of the previous.

The order of the global truncation error is what defines the __order__ of a numerical method to solve ODEs. We say that Euler's method is a first-order method. There exists a multitude of different ways to solve ODEs, where Euler's method is the simplest. For numerical work that is more sensitive to errors (for example long simulations of planetary motions), the additional work of implementing a higher order method is well worth the investment, as the number of required time steps is often way lower.

### Instability

We will now demonstrate a simple theoretical example that illustrates how Euler's method may break down, i.e. the numerical solution starts to deviate from the exact solution in dramatic ways. Usually, this happens when the numerical solution grows large in magnitude while the exact solution remains small.

Let's look at the ODE

$$
\frac{dy}{dt} = -y \quad \mathrm{with} \quad y(0) = 1.
$$

Trivially, this has the exact solution $y(t) = \mathrm{e}^{-t}$. However if look what happens with a single step in Euler's method

\begin{equation}
y_{n+1} = y_n + h g(y_n) = y_n - h y_n = (1-h) y_n.
\end{equation}

Observe that for $h=1$, our solution simply becomes zero right away, if $h>1$, our solution will oscillate between positive and negative values. If $h>2$ our solution will grow without bound (in absolute value), while it oscillates between positive and negative values. All three of these cases are drastically different from the exact solution! In this notebook we will not go into further detail on instability, but we chose to include this example to demonstrate how numerical methods will fail under certain conditions.

## Conclusion
Euler's method is a simple numerical procedure for solving ordinary differential equations, and it's simplicity manifests itself both in the straightforward numerical implementation, but also in the power of the method to solve more complex problems, e.g., in high dimensions. For more sophisticated problems that are more prone to numerical errors, we will next study more powerful methods such as the widely-used __forth order Runge-Kutta algorithm__.
However, for the simple example here, it was sufficient!


# ニューラルネットワークの基礎

## 事前に読んでおく必要がある章

- 2 章 [Python 入門](https://tutorials.chainer.org/ja/02_Basics_of_Python.html)
- 4 章 [微分の基礎](https://tutorials.chainer.org/ja/04_Basics_of_Differential.html)
- 5 章 [線形代数の基礎](https://tutorials.chainer.org/ja/05_Basics_of_Linear_Algebra.html)
- 6 章 [確率・統計の基礎](https://tutorials.chainer.org/ja/06_Basics_of_Probability_Statistics.html)
- 7 章 [単回帰分析と重回帰分析](https://tutorials.chainer.org/ja/07_Regression_Analysis.html)
- 8 章 [NumPy 入門](https://tutorials.chainer.org/ja/08_Introduction_to_NumPy.html)

## ニューラルネットワークとは

ニューラルネットワークは、**微分可能な変換を繋げて作られた計算グラフ (computational graph)** です。
本章では、まずは下の図のような、円で表された**ノード (node)** に値が入っていて、ノードとノードが**エッジ (edge)** で繋がれているようなものを考えます。

この図でいうノードの縦方向の集まりのことを**層 (layer)** と呼びます。
そして**ディープラーニング (deep learning)** とは、層の数が非常に多いニューラルネットワークを用いた機械学習の手法や、その周辺の研究領域のことを指します。

### 層（layer）

上の図は、ニューラルネットワークを用いて、ワインに関するいくつかの情報から、そのワインが「白ワイン」なのか「赤ワイン」なのか、というカテゴリを予測する分類問題を解く例を表しています。

左側から、最初の層を**入力層 (input layer)**、最後の層を**出力層 (output layer)**といい、その間にある層は**中間層 （intermediate layer)** もしくは**隠れ層 (hidden layer)** といいます。
上のニューラルネットワークは、入力層、中間層、出力層がそれぞれ一つずつあるので、合計 **3 層**の**構造（アーキテクチャ; architecture）**となっています。複数の中間層を持たせれば、さらに多層のニューラルネットワークとすることができます。

ニューラルネットワークは、層から層へ、値を変換していきます。
そのため、ニューラルネットワークとはこの変換がいくつも連なってできる**一つの大きな関数**だと考えることができます。
従って、基本的には、入力を受け取って、何か出力を返すものです。
そして、どのようなデータを入力し、どのような出力を作りたいかによって、入力層と出力層のノード数が決定されます。

上の例では、入力変数 $x_1$ に年数、$x_2$ にアルコールの度数、$x_3$ に色合い、といったあるワインを表す定量的な情報が与えられています。
そのため、入力層のノードの数は入力変数の数 $M$ （上の図では、$M = 3$）によって決定されます。
ここで、層と層の間にあるノード間の結合は、一つ一つが重みを持っており、上のような**全結合型ニューラルネットワークの場合は、それらの重みをまとめて、一つの行列で表現します。**
この際、[単回帰分析と重回帰分析](https://tutorials.chainer.org/ja/07_Regression_Analysis.html) の章で解説した重回帰分析の例と同じように、バイアスをその重み行列に含めて扱うため、入力層の最後に常に $1$ を持つノードが追加されていることに注意してください。
このため、図では入力層のノード数が $M + 1 = 3 + 1 = 4$ 個となっています。

また、今回は**ワインの情報を入力して、それが「赤ワイン」か「白ワイン」かを予測する**という場合を表しているので、カテゴリ数が 2 の分類問題と言えます。
そのため、出力層のノード数は 2 となっています。
この出力層のノード数を変化させれば、3 つや 4 つのカテゴリ数の分類問題にも対応することができます。
また、離散的なカテゴリではなく、連続的な実数値を予測する回帰問題の場合は、**目標値の種類に合わせて出力層のノード数を決定**します。

### 構造（architecture）

下の図のように、層数が同じでも、中間層のノード数を 3 にするか、 5 にするか、などには任意性があります。
こういった値をどのように決めるかは、ニューラルネットワークを設計する人に委ねられています。
また、中間層自体をいくつ重ねるか、その個数も自分で決める必要があります。
こういった自分で決定するパラメータを、**ハイパーパラメータ**と呼びます。

中間層のノード数が 3 の場合

中間層のノード数が 5 の場合

### 色々な種類（type）

ニューラルネットワークには、上図のような層間のノードが互いに密に結合した**全結合型 (fully-connected)** のものだけでなく、画像処理などでよく用いられる**畳み込み型 (convolutional)** のもの、系列データの扱いによく用いられる**再帰型 (recurrent)** のものなど、いくつもの種類があります。
また、基本的にそれらの違いは層間の結合の仕方にあり、別々の層間には別々の種類の結合の仕方を用いることができます。
つまり、**1 つのニューラルネットワークの中に複数の種類の結合が混ざって現れることもあります。**
例えば、入力に近いところでは畳み込み型の結合を用い、出力に近いところでは全結合型の結合を用いる、といったことがよく行われています。

本章では、**まず最も基本となる全結合型のみに着目して、解説を行います。**
その他の種類については、後の章で解説を行います。

## ニューラルネットワークの計算

### 基本的な計算の流れ

それでは、「ワインの分類」を題材に、ニューラルネットワークにある入力が与えられたとき、どのように計算結果が得られ、その結果をどう使えば分類が行えるのかを、まずは数式を用いず、以下の図を見ながら理解しましょう。

例えば、**年数が 3 年、アルコール度数が 14 度、色合いが 0.2** で表されるワインがあるとします。

このワインの情報をニューラルネットワークの入力層に与えたところ、そこから中間層、そして出力層へと**順番に計算が進み**、最終的に $y_1 = 0.15, y_2= 0.85$ という 2 つの数値が得られたとします。

この **2 つの数値**は、入力されたワインが「赤ワイン」「白ワイン」という **2 つのカテゴリ**のそれぞれに属する確率を表しています。
分類問題では、**出力された数値のうちどのノードが最も大きな値を持っているか**によって、**ニューラルネットワークの予測**が決まります。
今回は、2 つの出力のうち $y_{2}=0.85$ が最も大きな値となっているため、$y_2$ に対応する「赤ワイン」が予測されたカテゴリとなります。

このように、入力が与えられたとき、ニューラルネットワークの各層を順番に計算していき、出力まで計算を行うことを、**順伝播 (forward propagation)** と言います。

それでは、ここからはこのような計算がニューラルネットワークの内部でどのように行われているのか、詳しく見ていきましょう。
ポイントは、ニューラルネットワークの各層では、**前の層の出力値に「線形変換」と「非線形変換」を順番に施している**、というところです。
それでは、**線形変換**とは何か、**非線形変換**とは何か、を順番に説明します。

### 線形変換

線形変換（[注釈1](#note1)）とは、入力ベクトルを ${\bf h}_0$ としたとき（[注釈2](#note2)）、重み行列 ${\bf W}_{10}$ と、バイアスベクトル ${\bf b}_1$ の 2 つを使って、次のような変換を行うことを言います。

$$
{\bf u}_1 = {\bf W}_{10}{\bf h}_0 + {\bf b}_1
$$

これを踏まえて、以下の 2 層の**全結合型ニューラルネットワーク**の図を見てみましょう。

このニューラルネットワークは、**上の式と同じ計算を行います。**
2 つの層の間にはノードとノードを接続する線（エッジ）が書かれています。
そして、エッジの上には、$w_{11}, w_{21}, \dots$ といった文字が書かれています。
これらは両端のノード間の**結合重み (connection weight)** を表しています。

入力層のノードが持つ値は、結合重みと掛け合わされ、出力層のノードに伝わります。出力層の 1 つのノードには、複数のノードから計算結果が伝わってくるので、これらを全部足し合わせます。

具体的には、以下のような計算をします。

$$
\begin{align}
u_{11} &= w_{11} h_{01} + w_{12} h_{02} + w_{13} h_{03} + b_{1} \\
u_{12} &= w_{21} h_{01} + w_{22} h_{02} + w_{23} h_{03} + b_{2}
\end{align}
$$

この式は、入力ベクトル ${\bf h}_0$、重み行列 ${\bf W}_{10}$、バイアスベクトル ${\bf b}_1$、および出力 ${\bf u}_1$ を

$$
\begin{align}
{\bf h}_{0} &=
\begin{bmatrix}
h_{01} \\
h_{02} \\
h_{03}
\end{bmatrix} \\
{\bf W}_{10} &=
\begin{bmatrix}
w_{11} & w_{12} & w_{13} \\
w_{21} & w_{22} & w_{23}
\end{bmatrix} \\
{\bf b}_1 &=
\begin{bmatrix}
b_{1} \\
b_{2}
\end{bmatrix} \\
{\bf u}_1 &=
\begin{bmatrix}
u_{11} \\
u_{12}
\end{bmatrix}
\end{align}
$$

と定義すれば、

$$
{\bf u}_1 = {\bf W}_{10} {\bf h}_0 + {\bf b}_1
$$

と書くことができ、本節の冒頭に示した式と一致していることが分かります。

以上から、全結合型ニューラルネットワークでは、層と層の間で線形変換が行われていることが分かりました。
ここで、出力層のノードを改めて見てみましょう。
ノードは真ん中に線が引かれて左右に分割されており、先程計算した出力値 $u_{11}, u_{12}$ はその左側の半円の中に書かれています。

ニューラルネットワークの隠れ層では、**一つ前の層に線形変換を適用した結果を受け取り、そこへさらに非線形変換を適用したものを出力します。**
その結果を、この右側の半円の中に書き入れます。

次に、その非線形変換とは何なのか、またなぜ必要なのか説明していきます。

### 非線形変換

前節で紹介した線形変換のみでは、下図左のような**入出力間が線形な関係性**はよく近似することができるとしても、下図右のような**入出力間が非線形な関係**になっている場合には、観測データをうまく近似することができません。

例えば、線形変換のみで下図右の白い丸で表される観測データから $x$ と $y$ の関係を近似した場合、点線のような直線が得られたとします。
これでは、一部のデータについてはあまりよく当てはまっていないのが分かります。
しかし、もし $xy$ 平面上で下図右の実線のような曲線を表現することができれば、より良く近似できるかもしれません。

このようなケースに対応できるよう、ニューラルネットワークでは各層において、線形変換に続いて非線形変換を施し、層を積み重ねて作られるニューラルネットワーク全体としても非線形性を持つことができるようにしています。

そして、この非線形変換を行う関数のことを、ニューラルネットワークの文脈では**活性化関数 (activation function)** と呼びます。

線形変換の節で用いた例に戻って、説明を進めます。
線形変換を行った結果 $u_{11}, u_{12}$ のそれぞれに活性化関数 $a$ を用いて非線形変換を行い、その結果を $h_{11}, h_{12}$ とおきます。つまり、

$$
\begin{align}
h_{11} &= a(u_{11}) \\
h_{12} &= a(u_{12})
\end{align}
$$

です。
これらは**活性値（activation）**と呼ばれ、もしもう一つ層が続いているならば、次の層に渡される入力の値となります。

#### ロジスティックシグモイド関数

活性化関数の具体例としては、下図に示す **ロジスティックシグモイド関数 (logistic sigmoid function)** が従来、よく用いられてきました。

以下、**シグモイド関数**と呼びます。

しかし近年、層の数が多いニューラルネットワークではシグモイド関数は活性化関数としてほとんど用いられません。
その理由の一つは、シグモイド関数を活性化関数に採用すると、**勾配消失 (vanishing gradient)** という現象によって学習が進行しなくなる問題が発生しやすくなるためです。
この問題の詳細は本章の最後で紹介します。

#### ReLU

この問題を回避するために、最近では**正規化線形関数 (ReLU: rectified linear unit)** がよく用いられています。
これは、以下のような関数です。

$\max(0, u)$ は、$0$ と $u$ を比べて**大きな方を返す**関数です。
すなわち、**ReLU は入力が負の値の場合には出力は 0 で一定**であり、**正の値の場合は入力をそのまま出力する**という関数です。
シグモイド関数では、入力が $0$ から離れた値をとると、どんどん曲線の傾き（勾配）が小さくなっていき、平らになっていくことが一つ前の図からも見て取れます。
それに対し、ReLU 関数は入力の値が正であれば、いくら大きくなっていっても、傾き（勾配）は一定です。
これが後ほど紹介する勾配消失という問題に有効に働きます。

### 数値を見ながら計算の流れを確認

それでは、上の図のような回帰問題を解く 3 層の全結合型ニューラルネットワークを考えて、**入力が与えられてから出力が得られるまでの一連の計算を、実際に数値を手計算しながら確認してみましょう。**

入力として、

$$
{\bf x} = \begin{bmatrix}
2 \\
3 \\
1
\end{bmatrix}
$$

が与えられたとします。
このとき、出力 $y$ はどのように計算されるのでしょうか。

まず、重み行列 ${\bf W}_{10}$ とバイアスベクトル ${\bf b}_1$ が、図より、以下のような値になっています。

$$
\begin{align}
{\bf W}_{10} &=
\begin{bmatrix}
3 & 1 & 2 \\
-2 & -3 & -1
\end{bmatrix} \\
{\bf b}_1 &=
\begin{bmatrix}
0 \\
0
\end{bmatrix}
\end{align}
$$

これを用いて、まずは入力に対し線形変換を行います。

$$
\begin{align}
{\bf u}_1 = {\bf W}_{10} {\bf x} + {\bf b}_1
&=
\begin{bmatrix}
3 & 1 & 2 \\
-2 & -3 & -1
\end{bmatrix}
\begin{bmatrix}
2 \\
3 \\
1
\end{bmatrix}
+
\begin{bmatrix}
0 \\
0
\end{bmatrix} \\
&=
\begin{bmatrix}
11 \\
-14
\end{bmatrix}
\end{align}
$$

次に、非線形変換を施します。
今回は、活性化関数として ReLU 関数を採用しましょう。
活性化関数は、ベクトルを受け取ったとしても基本的には要素ごとに適用されます。

$$
\begin{align}
{\bf h}_1 &= {\rm ReLU}({\bf u}_1) \\
&=
\begin{bmatrix}
\max(0, u_{11}) \\
\max(0, u_{12})
\end{bmatrix} \\
&=
\begin{bmatrix}
\max(0, 11) \\
\max(0, -14)
\end{bmatrix} \\
&=
\begin{bmatrix}
11 \\
0
\end{bmatrix}
\end{align}
$$

同様に、出力層の $y$ の値までを計算します。2 層目と 3 層目の間の重み行列 ${\bf W}_{21}$ は、出力側の次元が $1$ なので、

$$
{\bf W}_{21} = \begin{bmatrix} 3 & 2 \end{bmatrix}
$$

という $1 \times 2$ 行列です。バイアスベクトル ${\bf b}_2$ は、図から

$$
{\bf b}_2 = \begin{bmatrix} 0 \end{bmatrix}
$$

であることが分かります。
これらを用いて隠れ層の出力値 ${\bf h}_1$ に対して線形変換を行うと、

$$
y = {\bf W}_{21} {\bf h}_1 + {\bf b}_2 = 3 \times 11 + 2 \times 0 + 0 \times 1  = 33
$$

が求まりました。
ここでは、ニューラルネットワークを用いて回帰問題を解く場合について説明を行うため、最終出力層に活性化関数を用いていません（[注釈3](#note3)）。

ここまでで、ニューラルネットワークの順伝播計算の中身について詳しく見てきました。

このような計算過程は**推論 (inference)** とも呼ばれ、一般的には**訓練が完了したモデルを用いて新しいデータに対して予測を行う**ことを指します。
今回の例では、すでに ${\bf W}_{10}$ や ${\bf W}_{21}$ といったパラメータの値は与えられており、それを使って入力に対してニューラルネットワークがどのような値を出力するのかを計算しました。

次節では、この ${\bf W}_{10}$ や ${\bf W}_{21}$ といったパラメータを決定するニューラルネットワークの**訓練**について解説します。

## ニューラルネットワークの訓練

重回帰分析では、目的関数をモデルのパラメータで微分して出てきた式を $= 0$ とおいて、**実際の数値を使うことなく**変数のまま（${\bf x}$ や ${\bf t}$ と置いたまま）、解（最適なパラメータ）を求めることができました。
このように、変数のままで解を求めることを**解析的に解く**と言い、その答えのことを**解析解 (analytical solution)** と呼びます。

しかし、ニューラルネットワークで表現されるような複雑な関数の場合、最適解を解析的に解くことはほとんどの場合困難です。そのため、別の方法を考える必要があります。

解析的に解く方法に対し、計算機を使って繰り返し数値計算を行って解を求めることを**数値的に解く**といい、求まった解は**数値解 (numerical solution)** と呼ばれます。

**ニューラルネットワークでは、基本的に数値的な手法によって最適なパラメータを求めます。**

そして、これから紹介する代表的な数値的解法では、求めたいパラメータの**初期値 (initial value)** を事前に決めておく必要があります。
なぜなら、この初期値から少しずつ値を更新していき、望ましい解に近づけていくためです。

このような手法は、**最適化手法 (optimization method)** と呼ばれるものの一種です。

1. モデルを設計し
2. 目的関数を定め
3. 目的関数を最適化するパラメータを求める

という手順は、[単回帰分析と重回帰分析](https://tutorials.chainer.org/ja/07_Regression_Analysis.html)の章でも、繰り返し出てきました。
そして、**この手順は機械学習の他の多くの手法でもよく登場します。**
本章で扱うニューラルネットワークでも、この手順に従います。

今回は、モデルとしては前節で説明に用いた 3 層のニューラルネットワークを用います。
このため、ステップ 1 はすでに完了しています。
次節からは、ステップ 2 以降を説明していきます。

### 目的関数

ニューラルネットワークの訓練には、微分可能でさえあれば解きたいタスクに合わせて様々な目的関数を利用することができます。
ここでは、

- **回帰問題**でよく用いられる**平均二乗誤差 (mean squared error)**
- **分類問題**でよく用いられる**交差エントロピー (cross entropy)**

という代表的な 2 つの目的関数を紹介します。

#### 平均二乗誤差

**平均二乗誤差 (mean squared error)** は、**回帰問題を解きたい場合によく用いられる目的関数です。**
重回帰分析の解説中に紹介した二乗和誤差と似ていますが、各データ点における誤差の総和をとるだけでなく、それをデータ数で割って、誤差の平均値を計算している点が異なります。式で表すと、以下のようになります。

$$
L = \frac{1}{N} \sum_{n=1}^N (t_n - y_n)^2
$$

ここで、$N$ はサンプルサイズ、$y_n$ は $n$ 個目のデータに対するニューラルネットワークの出力値、$t_n$ は $n$ 個目のデータに対する望ましい正解の値です。

ここで、下の図のような例を考えてみましょう。

今、ある入力 ${\bf x}$ を与えたときのこのニューラルネットワークの出力 $y$ は、$33$ でした。ここで、もしこの入力に対応する目標値が $t = 20$ だとすると、今全体のサンプルサイズが 1 だとして、平均二乗誤差は

$$
L = \frac{1}{1} (20 - 33)^2 = 169
$$

と計算できます。

#### 交差エントロピー

**交差エントロピー (cross entropy)** は、**分類問題を解きたい際によく用いられる目的関数です。**
例として、$K$ クラスの分類問題を考えてみましょう。
ある入力 $x$ が与えられたとき、ニューラルネットワークの出力層に $K$ 個のノードがあり、それぞれがこの入力が $k$ 番目のクラスに属する確率

$$
y_k = p(y=k|x)
$$

を表しているとします。
これは、入力 $x$ が与えられたという**条件のもとで**、予測クラスを意味する $y$ が $k$ であるような確率、を表す**条件付き確率**です。

ここで、$x$ が所属するクラスの正解が、

$$
{\bf t} = \begin{bmatrix} t_1 & t_2 & \dots & t_K \end{bmatrix}^{\rm T}
$$

というベクトルで与えられているとします。
ただし、このベクトルは $t_k \ (k=1, 2, \dots, K)$ **のいずれか 1 つだけが 1 であり、それ以外は 0 であるようなベクトル**であるとします。
これを**ワンホットベクトル（1-hot vector）**と呼びます。
そして、この 1 つだけ値が 1 となっている要素は、その要素のインデックスに対応したクラスが正解であることを意味します。例えば、$t_3 = 1$ であれば 3 つ目のクラス（3 というインデックスに対応するクラス）が正解であるということになります。

以上を用いて、交差エントロピーは以下のように定義されます。

$$
- \sum_{k=1}^{K}t_{k}\log y_{k}
$$

これは、$t_k$ が $k = 1, \dots, K$ のうち正解クラスである一つの $k$ の値でだけ $1$ となるので、正解クラスであるような $k$ での $\log y_k$ を取り出して $-1$ を掛けているのと同じです。
また、$N$ 個すべてのサンプルを考慮すると、交差エントロピーは、

$$
L = - \sum_{n=1}^{N} \sum_{k=1}^{K}t_{n, k}\log y_{n, k}
$$

となります。

### 目的関数の最適化

目的関数の値を最小にするようなパラメータの値を求めることで、**ニューラルネットワークを訓練します。**
本節では、最適化アルゴリズムの一つである**勾配降下法 (gradient descent)** を紹介します。

#### 勾配降下法

図中の点線は、パラメータ $w$ を変化させた際の目的関数 $L$ の値を表しています。
この図では簡単のために二次関数で表します。実際のニューラルネットワークの目的関数は、多次元で、かつもっと複雑な形をしていることがほとんどです。
さて、この目的関数が最小値を与えるような $w$ は、どのようにして求められるのでしょうか。

前節で説明したように、ニューラルネットワークのパラメータの初期値を決める必要があります。一般的に**ニューラルネットワークのパラメータは乱数で初期化されます。**
ここでは、例として $w=4$ という初期化が行われたと考えてみましょう。

そうすると、$w=4$ における $L$ の接線の傾き $\frac{\partial L}{\partial w}$ が求まります。この**接線の傾きのことを勾配 (gradient)** と一般的に呼びます。

さて、ここでは仮に $w=4$ における $\frac{\partial L}{\partial w}$ が $5$ であったとしましょう。
このことを

$$
\frac{\partial L}{\partial w} \bigg\rvert_{w=4} = 5
$$

と書きます。
この $5$ という値は $w=4$ における $L(w)$ という関数の勾配を表しています。

**勾配とは、**$w$ **を増加させた際に** $L$ **が増加する方向**を意味しています。今は $L$ の値を小さくしたいわけですから、この**傾きの逆方向へ** $w$ **を変化させる**、すなわち $w$ から $\frac{\partial L}{\partial w}$ を**引けば良い** ことになります。

これが**ニューラルネットワークのパラメータを目的関数の勾配を用いて更新していく**際の基本的な考え方です。
このときの $w$ の一度の更新量の幅を調整するために、勾配に**学習率 (learning rate)** と呼ばれる値を乗じるのが一般的です。学習率を $\eta$ とおくと、 $w$ から $\eta \frac{\partial L}{\partial w}$ を引くことで、勾配そのものの値を $\eta$  にした量だけ $w$ を更新することになります。更新後の $w$ は、

$$
w \leftarrow w - \eta \dfrac{\partial L}{\partial w}
$$

となります。
ここで、 $\leftarrow$ は、右側の値で左側の値を置き換える、つまり**更新する**ことを意味しています。

例えば、学習率を $\eta = 0.5$ に設定して、更新後の $w$ を実際に求めてみましょう。
$w$ の更新量は**学習率** $\times$ **勾配**で決まるので、

$$
0.5 \times 5 = 2.5
$$

となります。
現在 $w=4$ なので、**この値を引いて**

$$
w \leftarrow 4 - 2.5
$$

と更新した後は、 $w=1.5$ です。

1 度目の更新を行って、$w$ が $w = 1.5$ の位置に移動しました。
そこで、再度この点においても勾配を求めてみます。
今度は勾配が $-2$ になっていたとしましょう。
すると**学習率** $\times$ **勾配**は

$$
0.5 \times (-2) = -1$$

となります。
これを再び用いて、

$$w \leftarrow 1.5 - (-1)$$

と 2 度目の更新を行うと、今度は $w = 2.5$ の位置にきます。
このようにして 2 回更新したあとは、以下の図の位置にパラメータが移動しています。

**徐々に** $L$ **が最小値をとるときの** $w$ **の値に近づいていることが見て取れます。**

こうして、**学習率** $\times$ **勾配**を更新量としてパラメータを変化させていくと、パラメータ $w$ を求めたい $L$ を最小にする $w$ に徐々に近づけていくことができます。

このような勾配を用いた目的関数の最適化手法を**勾配降下法 (gradient descent)** と呼びます。
ニューラルネットワークは、基本的に**微分可能な関数のみをつなげ合わせてできている**（[注釈4](#note4)）ため、ニューラルネットワーク全体が表す関数も微分可能であり、訓練データセットを用いて勾配降下法によって目的関数を小さくする（局所）最適なパラメータを求めることができます。

### ミニバッチ学習

通常ニューラルネットワークを勾配降下法で最適化する場合は、データを一つ一つ用いてパラメータを更新するのではなく、**いくつかのデータをまとめて入力し、それぞれの勾配を計算したあと、その勾配の平均値を用いてパラメータの更新を行う**方法がよく行われます。
これを**ミニバッチ学習**と呼びます。

ミニバッチ学習では、以下の手順で訓練を行います。

1. 訓練データセットから一様ランダムに $N_{b} \ (>0)$ 個のデータを抽出する
2. その $N_{b}$ 個のデータをまとめてニューラルネットワークに入力し、それぞれのデータに対する目的関数の値を計算する
3. $N_b$ 個の目的関数の値の平均をとる
4. この平均の値に対する各パラメータの勾配を求める
5. 求めた勾配を使ってパラメータを更新する

**そしてこれを、異なる** $N_{b}$ **個のデータの組み合わせに対して繰り返し行います。**

ここで、一度のパラメータ更新に用いられるサンプルの数 $N_{b}$ を**バッチサイズ (batch size)** と呼びます。
結果的にはデータセットに含まれる全てのデータを使用していきますが、1 度の更新に用いるデータは $N_{b}$ 個ずつであることに注意してください。

データセットから $N_b$ 個ずつデータを取ってきて使うというミニバッチ学習の様子を模式的に表した図を上に示します。この図では、データセットをそれぞれが $N_b$ 個ずつデータを含んでいる 10 個のグループに分割しています。そのあと、それぞれのグループごとにパラメータ更新を逐次的に行います。

**ミニバッチ学習**と異なり、データセット全体を一度に用いて一回の更新を行う、すなわちグループへ分割しない方法を**バッチ学習**と呼びます。

勾配降下法では、データを用いてパラメータを 1 度更新するまでのことを **1 イテレーション (iteration)** と呼び、バッチ学習では訓練データセットのデータ全体を 1 度の更新に一気に用いるため、**1 エポック = 1 イテレーション** となります。
一方、例えばデータセットを 10 個のグループに分割してグループごとに更新を行うミニバッチ学習の場合は、**1 エポック = 10 イテレーション** となります。

このようなミニバッチ学習を用いた勾配降下法は特に、**確率的勾配降下法（stocastic gradient descent; SGD）**と呼ばれます。
現在多くのニューラルネットワークの最適化手法はこの SGD をベースとした手法となっています。
SGD を用いると、全体の計算時間が劇的に少なくできるだけでなく、下図のように目的関数が複数の谷を持っていたとしても、適当な条件のもとで "ほとんど確実に" **局所最適解**（[注釈5](#note5)）に収束することが知られています。

### パラメータ更新量の算出

それでは今、下図のような3層の全結合型ニューラルネットワークを考え、3 次元の入力ベクトルから、1 次元の値を出力し、正解の値を予測する回帰問題を題材に、パラメータを更新する式の導入を行ってみましょう。

まず、1 層目と 2 層目の間の線形変換が ${\bf w}_1, {\bf b}_1$、2 層目と 3 層目の間の線形変換が ${\bf w}_2, {\bf b}_2$ というパラメータによって表されているとします。
また、これらをまとめて $\boldsymbol{\Theta}$ と表すことにします。つまり、$\boldsymbol{\Theta} = \{ {\bf w}_1, {\bf b}_1, {\bf w}_2, {\bf b}_2 \}$ です（[注釈6](#note6)）。

入力は ${\bf x} \in \mathbb{R}^{3}$、出力を $y$ とし、目標値を $t$ とおきます。目的関数には以下の平均二乗誤差関数を用いることにします。

$$
L = \dfrac{1}{N} \sum_{n=1}^{N} (t_{n} - y_{n})^2
$$

ただし、$N$ はデータ数です。
今回は $N=1$ なので、目的関数は簡単に

$$
L = (t - y)^2
$$

と書けます。

それでは、この目的関数を各パラメータで微分して、それぞれのパラメータについて更新量を算出してみましょう。

まず、順伝播を計算します。
ニューラルネットワーク全体を一つの関数として見て $f$ と書くことにすると、出力 $y$ は

$$
\begin{aligned}
y &= f({\bf x}; \boldsymbol{\Theta}) \\
&= {\bf w}_2 a_1({\bf w}_1 {\bf x} + {\bf b}_1) + {\bf b}_2
\end{aligned}
$$

と書くことができます。ここで、$a_1$ は 1 層目のノードで行われる非線形変換（に使用される活性化関数）を意味しています。今回は、**シグモイド関数**をこの活性化関数に用いることにします。

以下、簡単のために、入力に対して行われた線形変換の結果を ${\bf u}_1$ とし、中間層の値、すなわち ${\bf u}_1$ に活性化関数を適用した結果を ${\bf h}_1$ と書きます。
すると、これらの関係は以下のように整理することができます。

$$
\begin{aligned}
(入力の線形変換 )\hspace{1em} {\bf u}_1 &= {\bf w}_1 {\bf x} + {\bf b}_1 \\
(1層目のノードでの非線形変換) \hspace{1em} {\bf h}_1 &= a_1({\bf u}_1) \\
(1層目の出力の線形変換) \hspace{1em} y &= {\bf w}_2 {\bf h}_1 + {\bf b}_2
\end{aligned}
$$

#### パラメータ ${\bf w}_2$ の更新量

それではまず、出力層に近い方のパラメータ、${\bf w}_2$ についての $L$ の勾配を求めてみましょう。
これは、合成関数の偏微分なので、**連鎖律（chain rule）**を用いて以下のように展開できます。

$$
\begin{aligned}
\frac{\partial L}{\partial {\bf w}_2}
&= \frac{\partial L}{\partial y} \frac{\partial y}{\partial {\bf w}_2}
\end{aligned}
$$

2 つの偏微分の積が現れました。
これらはそれぞれ、

$$
\begin{aligned}
\frac{\partial L}{\partial y}
&= -2(t - y) \\
\frac{\partial y}{\partial {\bf w}_2}
&= {\bf h}_1
\end{aligned}
$$

と求まります。

それでは、実際に NumPy を使ってこの勾配の値を計算してみましょう。
ここでは簡単のために、バイアスベクトルはすべて 0 で初期化されているとします。

```python
import numpy as np

# 入力
x = np.array([2, 3, 1])

# 正解
t = np.array([20])
```

まず、NumPy モジュールを読み込んでから、入力の配列を定義します。
ここでは、上図と同じになるように `2, 3, 1` の3つの値を持つ3次元ベクトルを定義しています。
また、正解として図と同じように `20` を与えることにしました。
次に、パラメータを定義します。

```python
# 0-1層間のパラメータ
w1 = np.array([[3, 1, 2], [-2, -3, -1]])
b1 = np.array([0, 0])

# 2-3層間のパラメータ
w2 = np.array([[3, 2]])
b2 = np.array([0])
```

ここでは、以下の4つのパラメータを定義しました。

**1 層目と 2 層目の間の線形変換のパラメータ**

${\bf w}_1 \in \mathbb{R}^{2 \times 3}$ : 3 次元ベクトルを 2 次元ベクトルに変換する行列

${\bf b}_1 \in \mathbb{R}^2$ : 2 次元バイアスベクトル

**2 層目と 3 層目の間の線形変換のパラメータ**

${\bf w}_2 \in \mathbb{R}^{1 \times 2}$ : 2 次元ベクトルを 1 次元ベクトルに変換する行列

${\bf b}_2 \in \mathbb{R}^1$ : 1 次元バイアスベクトル

それでは、各層の計算を実際に実行してみましょう。

```python
# 中間層の計算
u1 = w1.dot(x) + b1
h1 = 1. / (1 + np.exp(-u1))

# 出力の計算
y = w2.dot(h1) + b2

print(y)
```

    [2.99995156]

出力は $2.99995156$ と求まりました。
つまり、$f([2, 3, 1]^T) = 2.99995156$ ということになります。
次に、上で導出した

$$
\frac{\partial L}{\partial {\bf w}_2}
= \frac{\partial L}{\partial y} \frac{\partial y}{\partial {\bf w}_2}
$$

の右辺の 2 つの偏微分をそれぞれ計算してみましょう。

```python
# dL / dy
dLdy = -2 * (t - y)

# dy / dw_2
dydw2 = h1
```

これらを掛け合わせれば、求めたかったパラメータ ${\bf w}_2$ についての勾配を得ることができます。

```python
# dL / dw_2: 求めたい勾配
dLdw2 = dLdy * dydw2

print(dLdw2)
```

    [-3.39995290e+01 -2.82720335e-05]

勾配が求まりました。
これが $\frac{\partial L}{\partial {\bf w}_2}$ の値です（[注釈7](#note7)）。
これを学習率でスケールさせたものを使えば、パラメータ ${\bf w}_2$ を更新することができます。
更新式は、具体的には以下のようになります。

$$
{\bf w}_2 \leftarrow {\bf w}_2 - \eta \frac{\partial L}{\partial {\bf w}_2}
$$

#### 学習率について

学習率が大きすぎると、繰り返しパラメータ更新を行っていく中で目的関数の値が振動したり、発散したりしてしまいます。
逆に小さすぎると、収束に時間がかかってしまいます。
そのため、この学習率を適切に決定することがニューラルネットワークの学習においては非常に重要となります。
多くの場合、学習がきちんと進むもっとも大きな値を経験的に探すということが行われます。
シンプルな画像認識のタスクなどでは大抵、$0.1$ から $0.01$ 程度の値が最初に試される場合が比較的多く見られます。

#### パラメータ ${\bf w}_1$ の更新量

次に、${\bf w}_1$ の更新量も求めてみましょう。
そのためには、${\bf w}_1$ で目的関数 $L$ を偏微分した値が必要です。
これは以下のように計算できます。

$$
\begin{aligned}
\frac{\partial L}{\partial {\bf w}_1}
&= \frac{\partial L}{\partial y} \frac{\partial y}{\partial {\bf w}_1} \\
&=
\frac{\partial L}{\partial y}
\frac{\partial y}{\partial {\bf h}_1}
\frac{\partial {\bf h}_1}{\partial {\bf w}_1} \\
&=
\frac{\partial L}{\partial y}
\frac{\partial y}{\partial {\bf h}_1}
\frac{\partial {\bf h}_1}{\partial {\bf u}_1}
\frac{\partial {\bf u}_1}{\partial {\bf w}_1}
\end{aligned}
$$

4 つの偏微分の積になりました。
ここで、活性化関数の勾配

$$
\frac{\partial {\bf h}_1}{\partial {\bf u}_1}
$$

が登場しています。
今回は活性化関数 $a_1$ にシグモイド関数を用いているので、

$$
{\bf h}_1 = a_1({\bf u}_1) = \frac{1}{1 + \exp(-{\bf u}_1)}
$$

を ${\bf u}_1$ で微分します。

$$
\begin{aligned}
\frac{\partial a_1({\bf u}_1)}{\partial {\bf u}_1}
&= -\frac{-(\exp(-{\bf u}_1))}{(1 + \exp(-{\bf u}_1))^2} \\
&= \frac{1}{1 + \exp(-{\bf u}_1)} \cdot \frac{\exp(-{\bf u}_1)}{1 + \exp(-{\bf u}_1)} \\
&= \frac{1}{1 + \exp(-{\bf u}_1)} \cdot \frac{1 + \exp(-{\bf u}_1) - 1}{1 + \exp(-{\bf u}_1)} \\
&= \frac{1}{1 + \exp(-{\bf u}_1)} \left(1 - \frac{1}{1 + \exp(-{\bf u}_1)}\right) \\
&= a_1({\bf u}_1)\left(1 - a_1({\bf u}_1)\right) \\
&= {\bf h}_1 ( 1 - {\bf h}_1 )
\end{aligned}
$$

このように、**シグモイド関数の勾配は、シグモイド関数の出力値を使って簡単に計算することができます。**

これで、上の 4 つの偏微分のうち、初めの 1 つと、3 つ目はすでに求めました。
残りの 2 つは、それぞれ、

$$
\begin{aligned}
\frac{\partial y}{\partial {\bf h}_1}
&= {\bf w}_2 \\
\frac{\partial {\bf u}_1}{\partial {\bf w}_1}
&= {\bf x}
\end{aligned}
$$

と計算できます。
では、さっそく実際に NumPy を用いて計算を実行してみましょう。

```python
# d y / d h1
dydh1 = w2

# d h1 / d u1
dh1du1 = h1 * (1 - h1)

# d u_1 / d w1
du1dw1 = x

# 上から du1 / dw1 の直前までを一旦計算
dLdu1 = dLdy * dydh1 * dh1du1

# du1dw1は (3,) というshapeなので、g_u1w1[None]として(1, 3)に変形
du1dw1 = du1dw1[None]

# dL / dw_1: 求めたい勾配
dLdw1 = dLdu1.T.dot(du1dw1)

print(dLdw1)
```

    [[-3.40704286e-03 -5.11056429e-03 -1.70352143e-03]
     [-1.13088040e-04 -1.69632060e-04 -5.65440200e-05]]

これが $\frac{\partial L}{\partial {\bf w}_1}$ の値です（[注釈7](#note7)）。
これを用いて、${\bf w}_2$ と同様に以下のような更新式でパラメータ ${\bf w}_1$ の更新をすることができます。

$$
{\bf w}_1 \leftarrow {\bf w}_1 - \eta \frac{\partial L}{\partial {\bf w}_1}
$$

これで ${\bf w}_1$ の更新をすることができました。 ${\bf b}_1$ 、${\bf b}_2$ についても同様の流れで更新することができます。

### 誤差逆伝播法（バックプロパゲーション）

ここまでで、各パラメータについての目的関数の導関数を手計算により導出して実際に勾配の数値計算を行うということを体験しました。
では、もっと層数の多いニューラルネットワークの場合は、どうなるでしょうか。

同様に手計算によって全てのパラメータの勾配を求めることも不可能ではないかもませんが、層数が多くなれば膨大な時間がかかるでしょう。しかし、**ニューラルネットワークの微分可能な関数を順に適用するという性質を用いる**と、コンピュータによって**自動的に勾配を与える関数を導き出す**ことが可能です。

まず、合成関数の偏微分は、**連鎖律によって複数の偏微分の積の形に変形できる**ことを思い出しましょう。

下図は、ここまでの説明で用いていた 3 層の全結合型ニューラルネットワークの出力を得るための計算（**順伝播**）と、その値を使って目的関数の値を計算する過程を青い矢印で、そして**前節で手計算によって行った各パラメータによる目的関数の偏微分を計算する過程を赤い矢印で**表現した動画となっています。

まず、目的関数の出力を $l = L(y, t)$ とします。
この図の丸いノードは変数を表し、四角いノードは関数を表しています。

今、一つの巨大な合成関数として見たニューラルネットワーク全体を $f$ と表し、その中で各層間の線形変換に用いられる関数を $f_1$, $f_2$、中間層での非線形変換を $a_1$ と表します。

今、上図の青い矢印で表されるように、新しい入力 ${\bf x}$ がニューラルネットワークに与えられ、それが順々に出力側に伝わっていき、最終的に目的関数の値 $l$ まで計算が終わったとします。

すると次は、目的関数の出力の値を小さくするような各パラメータの更新量を求めたいということになりますが、このために**必要な目的関数の勾配は、各パラメータの丸いノードより先の部分（出力側）にある関数の勾配だけで計算できる**ことが分かります。
具体的には、それらを全て掛け合わせたものになっています。

つまり、上図の赤い矢印で表されるように、**出力側から入力側に向かって**、**順伝播とは逆向きに**、各関数における入力についての勾配を求めて、掛け合わせていけば、パラメータについての目的関数の勾配が計算できるわけです。

このように、微分の連鎖律の仕組みを用いて、ニューラルネットワークを構成する関数が持つパラメータについての目的関数の勾配を、**順伝播で通った経路を逆向きにたどるようにして**途中の関数の勾配の掛け算によって求めるアルゴリズムを **誤差逆伝播法 (backpropagation)** と呼びます。

### 勾配消失

活性化関数について初めに触れた際、シグモイド関数には勾配消失という現象が起きやすくなるという問題があり、現在はあまり使われていないと説明をしました。
その理由についてもう少し詳しく見ていきましょう。

上で既に計算した、シグモイド関数の導関数を思い出してみます。

$$
\begin{aligned}
a\left( u\right) &=\dfrac {1}{1+\exp(-u)} \\
a'\left( u\right) &= a\left( u\right) \left( 1-a\left( u\right) \right)
\end{aligned}
$$

さて、この導関数の値を入力変数に関してプロットしてみると、下記のようになります。

この図の上 2 つは、導関数を構成する2つの部分、 $f(u)$ および $1 - f(u)$ の値を別々にプロットしたもので、中央下の図は全体の導関数の値となります。
中央下の導関数の形から明らかなように、入力が原点から遠くなるにつれ勾配の値がどんどん小さくなり、0に漸近していくことが分かります。

各パラメータの更新量を求めるには、前節で説明したように、**そのパラメータよりも先のすべての関数の勾配を掛け合わせる**必要がありました。
このとき、活性化関数にシグモイド関数を用いていると、勾配は必ず**最大でも 0.25** という値にしかなりません。
つまり、シグモイド関数がニューラルネットワーク中に現れるたびに、目的関数の勾配は、**多くとも** 0.25 倍されてしまいます。
これは、層数が増えていけばいくほど、この最大でも 0.25 にしかならないような値が、繰り返し掛け合わされることになってしまい、入力に近い層に流れていく勾配はどんどん 0 に近づいていってしまいます。

具体例を見てみましょう。
今回は 3 層のニューラルネットワークを用いて説明を行っていましたが、4層の場合を考えてみます。
すると、一番入力に近い線形変換のパラメータの勾配は、多くとも目的関数の勾配を $0.25 \times 0.25 = 0.0625$ 倍したものということになります。
層数が一つ増えるたびに、指数的に勾配が小さくなるということがよく分かります。

ディープラーニングでは、4 層よりもさらに多くの層を積み重ねたニューラルネットワークが用いられます。
そうすると、活性化関数としてシグモイド関数を使用した場合、**目的関数の勾配が入力に近い関数が持つパラメータへほぼ全く伝わらなくなってしまいます**。
あまりにも小さな勾配しか伝わってこなくなると、パラメータの更新量がほとんど 0 になるため、どんなに目的関数が大きな値になっていても、入力層に近い関数が持つパラメータは変化しなくなります。
つまり初期化時からほとんど値が変わらなくなるということになり、学習が行われていないという状態になるわけです。
これを**勾配消失**と呼び、長らく深い（十数層を超える）ニューラルネットワークの学習が困難であった一つの要因でした。

この解決策として、ReLU 関数が提案され、多層のニューラルネットワークに対する学習も勾配消失を回避しながら行うことができるようになっています。

**注釈 1**

通常、数学では線形変換とは ${\bf W}$ を掛ける操作のことを指し、${\bf b}$ を足す操作は含まれません。${\bf b}$ の加算を含む操作は厳密には「アファイン変換（もしくは アフィン変換）」と呼ばれるものです。しかし、ディープラーニングの文脈ではこの変換も線形変換と呼ばれることが多いです。

[▲上へ戻る](#ref_note1)

**注釈 2**

ここで、入力層のノードが $h_{01}, h_{02}, h_{03}$ という文字で表されていますが、この $h$ は**隠れ層**を意味する英語の **hidden layer** の頭文字である h から来ています。
入力層は隠れ層ではありませんが、同じ文字を使うことで表記を一般化し、簡便にしています。

[▲上へ戻る](#ref_note2)

**注釈 3**

また、分類問題を解きたい場合は、クラス数と同じだけのノードを出力層に用意しておき、各ノードがあるクラスに入力が属する確率を表すようにします。
このため、**全出力ノードの値の合計が 1 になるよう正規化**します。
これには、**要素ごと**に適用される活性化関数ではなく、**層ごと**に活性値を計算する別の関数を用いる必要があります。
    そのような目的に使用される代表的な関数には、ソフトマックス関数があります。

[▲上へ戻る](#ref_note3)

**注釈 4**

厳密には目的関数に微分不可能な点が存在する可能性はあります。例えば ReLU は $x = 0$ で微分不可能なため、ReLU を含んだニューラルネットワークには微分不可能な点が存在することになります。このような場合、**劣微分（subderivative, subdifferential）**という考え方を導入し、全ての点で勾配が決定できるようにすることなどが行われます。

[▲上へ戻る](#ref_note4)

**注釈 5**

この局所解が大域的最適解と一致する条件については、現在活発に研究が行われています。参考：["A Convergence Theory for Deep Learning via Over-Parameterization"](https://arxiv.org/abs/1811.03962)

[▲上へ戻る](#ref_note5)

**注釈 6**

$\{\}$ という記号は、集合を表し、中に要素を並べて書きます。

[▲上へ戻る](#ref_note6)

**注釈 7**

ここで、試しに Chainer を使って同じ勾配の計算を行い、NumPy を使ったコードと結果が一致するかどうかチェックしてみましょう。

[▲上へ戻る](#ref_note7)

```python
import chainer
import chainer.functions as F
import chainer.links as L

# 1-2層間のパラメータ
w1 = np.array([[3, 1, 2], [-2, -3, -1]], dtype=np.float32)
b1 = np.array([0, 0], dtype=np.float32)

l1 = L.Linear(2, initialW=w1, initial_bias=b1)

# 2-3層間のパラメータ
w2 = np.array([[3, 2]], dtype=np.float32)
b2 = np.array([0], dtype=np.float32)

l2 = L.Linear(1, initialW=w2, initial_bias=b2)

# 入力
x = np.array([[2, 3, 1]], dtype=np.float32)

# 出力
y = l2(F.sigmoid(l1(x)))
print(y)  # => should be [2.99995156]

# 正解
t = np.array([[20.]], dtype=np.float32)

# ロス
loss = F.mean_squared_error(y, t)

# dLdw_2
print(chainer.grad([loss], [l2.W]))  # => should be [-3.39995290e+01 -2.82720335e-05]

# dLdw_1
print(chainer.grad([loss], [l1.W]))  # => should be [[-3.40704286e-03 -5.11056429e-03 -1.70352143e-03]
                                                  #                      [-1.13088040e-04 -1.69632060e-04 -5.65440200e-05]]
```

    variable([[2.9999516]])
    [variable([[-3.3999527e+01, -2.8371891e-05]])]
    [variable([[-3.4045703e-03, -5.1068552e-03, -1.7022851e-03],
              [-1.1348747e-04, -1.7023120e-04, -5.6743735e-05]])]