# 解析強化學習
###### tags: `學習筆記`
## 前言
本文為個人閱讀 R.S Sutton - Reinforcement Learning: An Introduction 的筆記。如果覺得這個筆記幫助你理解強化學習,那是書寫得好,請買書(?)支持大神,讓大神繼續寫書。
## 適合閱讀之對象
* 不了解強化學習,但具機器學習相關背景
* 想透過 **數學** 與 **程式碼** 了解強化學習演算法
---
## 馬可夫鏈 (Markov chains)
在談強化學習之前,我們先討論讓強化學習可以收斂的關鍵 ─ 馬可夫矩陣。[Andrey Markow](https://en.wikipedia.org/wiki/Andrey_Markov) 是俄羅斯的一名數學家,主要研究興趣是[隨機程序](https://en.wikipedia.org/wiki/Stochastic_process)。特別由一連串相關的事件所組成的系統,會怎麼隨著時間變化的問題。
> Stochastic Process/隨機程序/隨機過程/隨機處理:討論[隨機變數](https://en.wikipedia.org/wiki/Random_variable)隨時間如何變化。
> [name=TCW][color=#00B2FF]
在 1906 年時,馬可夫定義一種稱為 **鏈 (chain)** 的觀念,指的是離散狀態集的(變化)過程,後世稱之為「馬可夫鏈」。以下我們分項說明馬可夫鏈中的一些專有名詞,往後出現粗體字,指的就是這些專有名詞。若非粗體字,則只是一般性的描述。
1. **state (狀態)**:一種攏統的稱呼,可以指很多東西。
ex: 人的動作 (站著、坐著)、電腦的狀態 (開機、關機)
2. **states set (狀態集)**:以集合表示所有狀態,記作 $S = \{s_0, s_1, s_2, ..., s_m\}$,初始狀態為 $s_0$。
3. **transition model (轉換模型)**:狀態之間有可能改變,改變規則依據狀態模型訂定,以機率表示。
4. **Markov property (馬可夫性)**:狀態的轉變,只與現在有關,與過去無關。
5. **steps (階段)**:狀態穩定(未改變)時,即為一個階段,狀態轉換發生於兩階段之間。
6. **process (過程)**:描述「狀態連續改變」一事。
> [Comment]
> 1:數學家用簡單符號,表達複雜概念的一個例子...
> 4:有關無關,指的是機率上的獨立 (independent)
> 5:成立於時間離散型,這個定義在時間連續型馬可夫鏈不成立。
> [color=#00B2FF]
上述的專有名詞可能過於複雜,下列舉個例子說明:
* 假設小明是一個很懶惰的人,平時只會有兩個**狀態**,躺著 ($s_0$) 跟坐著 ($s_1$)。觀察其**過程**,躺著的時候,有 $90\%$ 的機率繼續躺著,有 $10\%$ 會變成坐著;坐著的時候,有 $50\%$ 會變成躺著,有 $50\%$ 繼續坐著。
根據前面**過程**的描述,我們用矩陣表達小明的**轉換模型**,稱為 **transition matrix (轉換矩陣)**。
$$
T=
\begin{bmatrix}
0.90 & 0.10 \\
0.50 & 0.50
\end{bmatrix}
$$
**轉換矩陣**一定是方陣 (square matrix),每個橫列表示某一原始**狀態**,轉換到其他**狀態**的機率,總和必定為 $1$。可以用條件機率表示成:
$$
T=
\begin{bmatrix}
P(s_0|s_0) & P(s_1|s_0) \\
P(s_0|s_1) & P(s_1|s_1)
\end{bmatrix}
$$
如果不喜歡數學,覺得「列式不能算懂……列式!……數學家的事,能算懂麼?」。那可以改用有向圖,表達小明的**過程**。用點表示狀態,用邊表示轉換機率,邊的粗細表示對應機率大小。

接著我們討論時間的部分,前面提到的**階段**,即是馬可夫鏈計算時間的方式。隨著不斷前進到下個**階段** ,**狀態**也不斷地一直改變。在計算上,我們可以計算**轉換矩陣**的次方,表示現在是第幾**階段**,以下用程式碼舉例:
```python
import numpy as np
Matrix_trans = np.array([[0.9,0.1],[0.5,0.5]])
# after k steps
Matrix_trans_step3 = np.linalg.matrix_power(Matrix_trans, 3)
Matrix_trans_step5 = np.linalg.matrix_power(Matrix_trans, 5)
Matrix_trans_step10 = np.linalg.matrix_power(Matrix_trans, 10)
Matrix_trans_step50 = np.linalg.matrix_power(Matrix_trans, 50)
Matrix_trans_step100 = np.linalg.matrix_power(Matrix_trans, 100)
# print the result
print('After 3 steps: \n', Matrix_trans_step3, '\n')
print('After 5 steps: \n', Matrix_trans_step5, '\n')
print('After 10 steps: \n', Matrix_trans_step10, '\n')
print('After 50 steps: \n', Matrix_trans_step50, '\n')
print('After 100 steps: \n', Matrix_trans_step100, '\n')
```
```
After 3 steps:
[[0.844 0.156]
[0.78 0.22 ]]
After 5 steps:
[[0.83504 0.16496]
[0.8248 0.1752 ]]
After 10 steps:
[[0.83335081 0.16664919]
[0.83324595 0.16675405]]
After 50 steps:
[[0.83333333 0.16666667]
[0.83333333 0.16666667]]
After 100 steps:
[[0.83333333 0.16666667]
[0.83333333 0.16666667]]
```
從運算的結果,我們發現隨著時間改變,轉移矩陣最終呈現一個定值。那麼如果我們現在給定起始**狀態** $s_{0} = [1, 0]$ 或是使用 numpy 給定任意**狀態**,計算結果會如何?
```python
# succeeding the above scripts
s0 = np.array([1.0, 0.0])
# print the result
print('Initial state: \n', s0, '\n')
print('After 3 steps: \n', np.dot(s0, Matrix_trans_step3), '\n')
print('After 5 steps: \n', np.dot(s0, Matrix_trans_step5), '\n')
print('After 10 steps: \n', np.dot(s0, Matrix_trans_step10), '\n')
print('After 50 steps: \n', np.dot(s0, Matrix_trans_step50), '\n')
print('After 100 steps: \n', np.dot(s0, Matrix_trans_step100), '\n')
```
```
Initial state:
[1. 0.]
After 3 steps:
[0.844 0.156]
After 5 steps:
[0.83504 0.16496]
After 10 steps:
[0.83335081 0.16664919]
After 50 steps:
[0.83333333 0.16666667]
After 100 steps:
[0.83333333 0.16666667]
```
```python
# succeeding the above scripts
# random initial state
rand_num = np.random.rand(1)
random_s0 = np.array([rand_num, 1-rand_num]).reshape(2)
# print the result
print('Initial state: \n', random_s0, '\n')
print('After 3 steps: \n', np.dot(random_s0, Matrix_trans_step3), '\n')
print('After 5 steps: \n', np.dot(random_s0, Matrix_trans_step5), '\n')
print('After 10 steps: \n', np.dot(random_s0, Matrix_trans_step10), '\n')
print('After 50 steps: \n', np.dot(random_s0, Matrix_trans_step50), '\n')
print('After 100 steps: \n', np.dot(random_s0, Matrix_trans_step100), '\n')
```
```
Initial state:
[0.6913491 0.3086509]
After 3 steps:
[0.82424634 0.17575366]
After 5 steps:
[0.83187941 0.16812059]
After 10 steps:
[0.83331845 0.16668155]
After 50 steps:
[0.83333333 0.16666667]
After 100 steps:
[0.83333333 0.16666667]
```
由上述的計算,我們可以發現在符合某些情況 $^{[1]}$ 下,隨著時間的改變,**狀態**最終會收斂於定值。然而,強化學習是一個能夠與環境互動的方法,並不是給定一個**轉換矩陣**後,讓結果自動收斂。因此接著介紹由馬可夫鏈延伸出的概念 - 馬可夫決策過程 (Markov Decision Process)
> $^{[1]}$以有向圖來說,必須滿足
> * 任一點可以抵達其他所有點 [[example]()]
> * 路徑是非週期性 (不可以在固定的路徑上移動) [[example](https://hackmd.io/s/ry0EssCw7)]
>
> 違反範例:[[違反1:不完全收斂](https://hackmd.io/s/BJmEx20v7)], [[違反2:震盪](https://hackmd.io/s/ry0EssCw7)], [[違反1+2](https://hackmd.io/s/r1Kh6jRDm)]
> [[reference](https://www.quora.com/How-do-I-tell-whether-a-Markov-chain-can-converge-to-equilibrium)]
> [[math proof](http://www.tcs.hut.fi/Studies/T-79.250/tekstit/lecnotes_02.pdf)]
> [name=TCW][color=#00B2FF]
## 馬可夫決策過程 (Markov Decision Process)
在強化學習中,我們考慮各種 **狀態** 後,再決定採用的動作,這個過程與上述的 Markov Chain 並不完全相同。不一樣的地方在於, Markov Chain 是依據 **轉換矩陣** 改變狀態,而在 Markov Decision Process 中,**轉移矩陣** 並不是固定的,受到 **狀態** 與 **動作** 的影響。組成 MDP 的部件如下:
1. **狀態集** $S = \{s_{0}, s_{1}, s_{2}, ..., s_{m}\}$
2. **初始狀態** $s_0$
3. **動作集** $A = \{a_{0}, a_{1}, a_{2}, ..., a_{n}\}$
4. **轉移矩陣** $T(s,a,s')$
5. **獎勵函數** $r(s,a,s')$
> 雖然這邊這樣定義獎勵函數,但它可能並不只與狀態和動作有關
> [name=TCW][color=#00B2FF]
跟上述的馬可夫鏈比起來,主要差異在於第 3,4,5 項。首先我們解釋 3 與 4,這邊導入了原本沒有的 **動作** ,而這個 **動作** 同時也作為 **轉移矩陣** 的輸入內容,並由計算產生移動到下一個狀態的機率。也就是說,「處於的狀態、採取的動作,都會影響我們移動到某個狀態的機率」。
* 回想小明的例子,當小明在躺著 ($s_{0}$) 與坐著 ($s_{1}$) 的同時,他會採取兩種動作,看漫畫 ($a_{0}$) 跟讀書 ($a_{1}$)。我們可以把上述的行為,表示成條件機率的形式。例如
* $P(a_{0}|s_{0})$ 表示小明躺著時,看漫畫的機率。
* $P(s_{0}|s_{0},a_{0})$ 表示原本小明躺著看漫畫,下一時刻還是躺著的機率。
* 關於前面提的兩個例子,我們需要額外補充一些說明
* 動作是由狀態決定的,這個決定是由 **agent**(無合適的翻譯,暫譯**代理人**) 決策。
* 條件機率只與現在的狀態、動作有關,與過去無關,仍然保持馬可夫性質。
這邊所提到的 **代理人**,是指在馬可夫決策過程中,我們不可能一直跟環境互動,所以需要 **代理人** 來代替我們跟環境互動。但是 **代理人** 不知道我們的偏好,所以每次行動後都需要由給予回饋 ( r ),告訴 **代理人** 這個動作是好的,要多多出現;或是這個動作是壞的,要減少出現。
而這個回饋,就由 **獎勵函數** 代勞了。透過 **獎勵函數** 在每次 **動作** 後給予正回饋或負回饋,進而讓 **代理人** 學習每個 **狀態** 的最佳 **動作**,產生最佳的決策 **策略**。用數學式來說,可以這個表達獎勵函數:
* $R \propto r(s,a,s')$
表示回饋與「現在狀態 $(s)$、動作 $(a)$、動作後狀態 $(s')$」三者是有關連的。
### 整體來說
* 任務:我們要讓 **代理人** 在任何 **狀態**,進行最佳的 **動作**,獲得最高的正回饋。
* 解法:評估可以得到的正回饋,最高者即為最佳策略。
因此在做決定之前,我們需要先評估這個狀態,要採用哪個動作,可以得到的最好的回饋。我們稱這個評估得到回饋的函數為「**價值函數(Value funciton)**」
## 價值函數
在講解價值函數之前,我們需要先定義幾項東西。
* 策略 $(\pi)$:將狀態與動作對應到機率的函數,表示某狀態下,採取某動作的機率,記做 $\pi(a|s)$
* 整體回饋 $(G_{t})$:我們將所有可以得到的回饋累加起來,以數學式表達:
$$
G_{t} = R_{t+1} + R_{t+2} + R_{t+3} +...+ R_{T},其中\ T\ 表示最終動作。
$$
然而,這樣定義遇到 $T \rightarrow \infty$ 的情況時,需稍微調整上式:
$$
G_{t} = R_{t+1} + \gamma R_{t+2} + \gamma^{2} R_{t+3} +... = \sum^{T-t-1}_{k=0} \gamma^{k} R_{t+k+1}
$$
其中 $0 \leq \gamma \leq 1^{[1]}$ ,在 $T \rightarrow \infty ^{[1]}$ 的情況下, $\gamma^{k} \rightarrow 0$,可以理解 $\gamma$ 為對未來獎懲的重視程度。
* 當 $\gamma \rightarrow 0$,$G_{t} \rightarrow R_{t+1}$,只在乎現在的獎懲。
* 當 $\gamma \rightarrow 1$,$G_{t} \rightarrow R_{t+1} + R_{t+2} + R_{t+3} +...+ R_{T}$,在乎每一次的表現。
> [1] $\gamma=1$ 與 $T \rightarrow \infty$ 只會擇一成立。
> [name=TCW][color=#00B2FF]
### 價值函數定義
分成兩種
* $v_{\pi}(s)$:狀態價值函數 (state-value function)。用來估計在某個狀態下,可以得到多少回饋的函數。定義如下:
$$
v_{\pi}(s) = \mathbb{E}_{\pi}[G_{t}|S_{t} = s]
$$
將 $G_{t} = \sum^{\infty}_{k=0} \gamma^{k} R_{t+k+1}$ 代入
$$
v_{\pi}(s) = \mathbb{E}_{\pi} [\sum^{\infty}_{k=0} \gamma^{k} R_{t+k+1} | S_{t} = s]
$$
* $q_{\pi}(s,a)$:動作價值函數 (action-value function)。類似 $v_{\pi}(s)$,用於評估某狀態下,某動作可以得到多少回饋的函數。
$$
q(s, a) = \mathbb{E}_{\pi}[G_{t}|S_{t} = s, A_{t} = a]
$$
一樣將 $G_{t}$ 代入
$$
q(s, a) = \mathbb{E}_{\pi}[\sum^{\infty}_{k=0} \gamma^{k} R_{t+k+1} | S_{t} = s, A_{t} = a]
$$
### 計算價值函數
#### 動態規劃 - 貝爾曼方程
如此一來,我們有了評估狀態、動作的函式,此外使用上述的定義,我們可以將價值函數寫成遞迴的形式。例如:
$$
v_{\pi}(s) = \mathbb{E}_{\pi}[G_{t} | S_{t} = s]
$$
$$
= \mathbb{E}_{\pi}[\sum^{\infty}_{k=0} \gamma^{k} R_{t+k+1} | S_{t} = s]
$$
$$
= \mathbb{E}_{\pi}[R_{t+1} + \gamma \cdot \sum^{\infty}_{k=0} \gamma^{k} R_{t+k+2} | S_{t} = s]
$$
將期望值展開 ($\mathbb{E}[X] = \sum p_{i} \cdot x_{i}$)、並代入上述數值
$$
= \sum_{a} \pi(a|s) \sum_{s'}p(s'|s,a)[r(s,a,s')+\gamma \cdot \mathbb{E}_{\pi} [\sum^{\infty}_{k=0} \gamma^{k} R_{t+k+2} | S_{t+1} = s']]
$$
> 處理完 $S_{t}$ 後,下一個狀態的期望值,要考慮 $S_{t+1}$ ,所以要加上這個條件。
> [name=TCW][color=#00B2FF]
$$
= \sum_{a} \pi(a|s) \sum_{s'}p(s'|s,a)[r(s,a,s')+\gamma \cdot v_{\pi}(s')]
$$
這條式子可以遞迴運算,又稱為 $v_{\pi}$ 的 Bellman equation (貝爾曼方程)
同理,我們也可以推論 $q(s,a)$ 的貝爾曼方程。
$$
q(s,a) = \mathbb{E}[R_{t+1}+ \gamma \cdot v(S_{t+1})|S_{t}=s,A_{t}=a]\\
= \sum_{s'}p(s'|s,a)[r(s,a,s')+ \gamma \cdot v(s')]
$$
---
好的,現在我們手上有動態規劃這個方法了,接著要開始真的評狀態價值了。這邊有兩種方法,一個是策略迭代,另一個是價值迭代,我們先介紹策略迭代。
#### 動態規劃 - 策略評估 (Policy Evaluation)
想像我們要從隨機的策略下,決定狀態的價值,這個過程稱作 **策略評估 (Policy Evaluation)** 。實際上,我們可以透過迭代,一步一步的更新狀態價值。
根據狀態價值函數定義,結合迭代,可以得到
$$
v_{k+1}= \sum_{a} \pi(a|s) \sum_{s'}p(s'|s,a)[r(s,a,s')+\gamma \cdot v_{k}(s')]
$$
可以透過這條式子逼近狀態價值。
演算法:

> 1. 以策略為單位進行更新,也就是說我們經過一次迭代,所有的狀態價值都會更新,因此稱為策略迭代。
> 2. 所有新的價值函數,都是由舊的價值函數計算的,沒有先計算先更新的問題。(ex: 現在有狀態 A, B, C。更新 A 的價值後,若計算 B 時需要 A 的價值,仍使用舊的價值進行計算。)
> [name=TCW][color=#00B2FF]
* 範例 - Example 4.1

* 目標:評估各位置的狀態價值
* 狀態:1-14 為非終點、左上與右下為終點
* 動作:上、左、下、右 (假設移動各方向機率相同)
* 獎勵函數:非終點 $= -1$
* 其他規定:
* 一次走一格
* 抵達終點後,不會離開終點
* 撞牆回原位 (ex: 在位置 1 時,往上還是移動到位置 1)
* 轉移矩陣:[[Link](https://github.com/ChampDBG/LearnRL/blob/master/gridworld/T.npy)]
* 實作:[[Link](https://github.com/ChampDBG/LearnRL/blob/master/Iterative_Policy_Evaluation.py)]
* 結果
```
============================================================
[Parameters]
Gamma = 0.99
Thershold = 0.05
[Variables]
No.50 iteration
Delta = 0.04759090371885932
[State-Value]
[[ 0. -10.62647251 -15.48618269 -17.07138964]
[-10.62647251 -13.90097573 -15.50704743 -15.48618269]
[-15.48618269 -15.50704743 -13.90097573 -10.62647251]
[-17.07138964 -15.48618269 -10.62647251 0. ]]
============================================================
```
#### 動態規劃 - 策略增進 (Policy Improvement)
不過我們剛剛的結果,是隨機亂走得情況,假如我們不要隨便亂走呢?假如我們根據 **動作價值** 選擇我們的動作,結果會不會不一樣?
這邊我們回頭看動作價值函數:
$$
q_{\pi}(s,a) = \mathbb{E}[R_{t+1} + \gamma \cdot v_{\pi}(S_{t+1}) | S_{t} = s, A_{t} = a] \\
=\sum_{s'}p(s'|s,a)[r(s,a,s')+ \gamma \cdot v_{\pi}(s')]
$$
透過動作價值函數,我們可以使用狀態價值,評估出每個狀態下,最適合的動作。為了簡單表示,我們把在狀態 $s$ 下,最適合的動作記為 $\pi '(s)$。這種,每次都選擇最好的動作的策略,我們稱為 greedy policy,數學定義如下:
$$
\pi '(s) = \arg \max_{a}q_{\pi}(s,a) \\
= \arg \max_{a} \mathbb{E}[R_{t+1} + \gamma \cdot v_{\pi}(S_{t+1}) | S_{t} = s, A_{t} = a] \\
= \arg \max_{a} \sum_{s'}p(s'|s,a)[r(s,a,s') + \gamma \cdot v_{\pi}(s')]
$$
用這個動作策略,我們可以再回去更新我們的價值函數:
$$
v_{\pi '} = \max_{a} \mathbb{E}[R_{t+1} + \gamma \cdot v_{\pi '} (S_{t+1}) | S_{t} = s, A_{t} = a] \\
=\max_{a} \sum_{s'} p(s'|s,a)[r(s,a,s')+ \gamma \cdot v_{\pi '}(s')]
$$
#### 動態規劃 - 策略迭代 (Policy Iteration)
透過重複前兩個過程 - 「策略評估」與「策略增進」,我們可以不斷逼近真實的狀態價值,以及得到最好的狀態動作。演算法如下:

* 實作 - Example 4.1 - [GitHub](https://github.com/ChampDBG/LearnRL/blob/master/Policy_Iteration.py)
* 結果:
```
============================================================
Final Result
============================================================
[State-value]
[[ 0. 0. -1. -1.99]
[ 0. -1. -1.99 -1. ]
[-1. -1.99 -1. 0. ]
[-1.99 -1. 0. 0. ]]
============================================================
[Policy]
[['*' '<' '<' '<']
['^' '^' '^' 'v']
['^' '^' 'v' 'v']
['^' '>' '>' '*']]
============================================================
```
#### 動態規劃 - 價值迭代 (Value Iteration)
上述的策略迭代,有一個很明顯的缺點 - 我們需要花兩個步驟,且不斷來回計算,才能夠逼近狀態價值以及得到最佳動作。那麼,我們有沒有辦法,把這兩個步驟再更簡化一些呢?
答案是可以的,我們能將「策略增進」容納到「策略評估」的過程中,如下式:
$$
v_{k+1}(s) = \max_{a} \mathbb{E}[R_{t+1} + \gamma \cdot v_{k}(S_{t+1}) | S_{t}=s, A_{t}=a] \\
= \max_{a} \sum_{s'} p(s'|s,a) [r(s,a,s') + \gamma \cdot v_{k}(s')]
$$
其中我們這邊的計算,並非從 **策略增進** 得到最適動作,而是直接計算各結果產生的價值,並以最高價值更新現有價值。

* 實作內容:[GitHub](https://github.com/ChampDBG/LearnRL/blob/master/Value_Iteration.py)
* 實作結果:
```
============================================================
Final Result
============================================================
[State-value]
[[ 0. 0. -1. -1.99]
[ 0. -1. -1.99 -1. ]
[-1. -1.99 -1. 0. ]
[-1.99 -1. 0. 0. ]]
============================================================
[Policy]
[['*' '<' '<' '<']
['^' '^' '^' 'v']
['^' '^' 'v' 'v']
['^' '>' '>' '*']]
============================================================
```
#### 動態規劃 - 特色討論
* 更新所有狀態價值
* 優點:更新所有狀態價值,是由價值函數的定義,衍伸出來的解決方案。因此滿足這個條件時,我們可以確保得到的狀態估計值,可以代表狀態價值,賦予這個數值意義。
* 缺點:更新所有狀態價值,計算量會隨著狀態數量而改變。舉個例子來說,我們前面提到的 GridWorld 是一個 4x4 的世界,但如果我今天的世界大一點,是個 40x40 的世界。光是要更新狀態價值,我們就給花上 100 倍的時間,更何況我們不知需要更新狀態價值一次。另外,我們在處理的問題,也未必只有小維度 (ex: 二維、三維)
* 小結:更新所有狀態價值,確保我們得到數值的意義,但也限制我們問題的大小。過於複雜的問題,可能不適合使用這個方法。事實上,這個限制來自於動態規劃本身,有興趣請見 [Curse of dimensionality](https://en.wikipedia.org/wiki/Curse_of_dimensionality)。
* 其他討論:那如果我們不更新狀態價值,會發生什麼事?
從數學上來說,不更新所有狀態價值時,將不滿足價值函數定義。所以計算出來的出來的數值不一定代表真實的狀態價值,造成動作決策錯誤。
* 迭代輪流更新狀態價值與策略
* 優點:我們可以確保評估價值與策略,是往同一個方向、有相同目標。如下圖所示:

* 缺點:透過迭代更新,代表我們必須不斷與環境互動,以更新價值狀態與策略。想像以下情境:
> 針對同一個問題,現在有三個人同時處理,我們可以得到三人的狀態價值與策略。
雖然我們獲得其他三人的資訊,但我們卻沒法統整這些資訊 (ex: 評估誰的資訊比較好),產生一個更準確的狀態價值或策略。因為你不知道那個人與環境互動到什麼程度了。說簡單一點,這個方法沒辦法分工。
* 小結:迭代輪流更新狀態價值與策略,確保我們評估狀態價值和策略有相同的目標,這個過程讓我們限制沒辦法分工處理問題。
#### 動態規劃 - 小結
以上,說明一些這個方法的特色。整體看來,動態規劃方法比較適合少量狀態的問題。除了動態規劃會遇到的 Curse of Dimensionality 外,沒辦法分工在處理複雜問題時會很麻煩。
[[Next Page](https://hackmd.io/s/HJ4N-p9o7)]