方策ベースアルゴリズムの基礎（方策勾配法/REINFORCE）


この記事は自作している強化学習フレームワーク [SimpleDistributedRL](https://qiita.com/pocokhc/items/a2f1ba993c79fdbd4b4d) の解説記事です。
また、強化学習の基礎については[こちら](https://qiita.com/pocokhc/items/953585fa60fa71651969)を参考にしてください。

追記：22/7/2 全体的に見直して修正しました。また REINFORCE を追加しました。
追記：25/2/2 今更ですがREINFORCEを修正しました。即時報酬のみは誤りでモンテカルロ法のエピソード報酬が正しかったです。

# 方策勾配法の基礎

ベルマン方程式は以下でした。

$$
\begin{align}
V_{\pi}(s) &= \sum_{a} \pi(a|s) Q_{\pi}(s, a)
\end{align}
$$

マルコフ決定過程ではアクションが決まると次の状態の確率が決まりました。
という事はこの式において方策 $\pi$ が決まると全ての不確定要素が決まるので、期待値（価値）を求めることができます。
これを利用し、価値が最大となる方策を学習しようという考えが方策ベースのアルゴリズムとなります。

ではどうするかというと、方策を確率モデルとし、確率を決めるパラメータ $\theta$ を用いてこの価値が最大となるパラメータを探すという問題に置き換えます。

$$
J(\theta) \propto V_{\pi_{\theta}}(s) = \sum_{a} \pi_{\theta}(a|s) Q_{\pi_{\theta}}(s, a)
$$

$J(\theta)$ はエピソード長に比例するので比例関係を表す $\propto$ (proportional to) を使っています。


## 勾配法（最急降下法）

勾配法は最適化問題を解くアルゴリズムの１つで、ある関数を最小にするパラメータを求めるアルゴリズムです。
以下手順で最適解を探します。

1. 初期地点 $x$ を決める
2. $x$ での傾き(微分値)を求める
3. 傾きと学習率をもとに次の探索地点 $x'$ を求める
4. 2~3の更新を傾きが0となる $x$ が見つかるまで繰り返す

更新式は以下です。($\eta$ は学習率です)

$$x \leftarrow x - \eta f'(x)$$

具体例で見てみます。
$f(x)=x^2+1$ の関数に対して勾配法で最小値を求めてみます。
微分した関数は $f'(x)=2x$、学習率は0.25とします。
最適解は $f'(x)=2x=0$ の時なので $x=0$ です。

$x=2$ から見てみます。

``` math
\begin{align}
x &\leftarrow 2 - 0.25 \times 4 = 1 \\
x &\leftarrow 1 - 0.25 \times 2 = 0.5 \\
x &\leftarrow 0.5 - 0.25 \times 1 = 0.25 \\
x &\leftarrow 0.25 - 0.25 \times 0.5 = 0.125 \\
...
\end{align}
```

これを繰り返すと $x=0$ になりそうです。
こうやって最小値を求める手法が勾配法です。


参考：[勾配法の仕組みを具体例でわかりやすく解説](https://ai-trend.jp/basic-study/neural-network/optimizer/)


# 方策勾配法の更新

方策を、勾配法を用いて目的関数 $J(\theta)$ を最大化するパラメータ $\theta$ を求める問題として解きます。

$$\theta \leftarrow \theta + \alpha \nabla J(\theta)$$

$\alpha$ が学習率で、$\nabla J(\theta)$ が $J(\theta)$ の微分です。
符号が $+$ になっているのは最大値を求めるためです。

次に $\nabla J(\theta)$ ですが、これは方策勾配定理を用いると以下になります。

$$
\nabla J(\theta) \propto E_{\pi_{\theta}} \Bigl[ \nabla \log \pi_{\theta}(a|s) Q_{\pi_{\theta}}(s,a) \Bigr]
$$


この定理に関しては導出が複雑らしく詳細は以下を参考にしてください。

+ [Reinforcement Learning: An Introduction(269ページ)](http://incompleteideas.net/book/bookdraft2017nov5.pdf)
+ [方策勾配定理のすっきりした証明](https://qiita.com/itok_msi/items/3c278e06cf2b0bb32f4e)
+ [【強化学習入門】方策勾配定理の証明メモ　【Policy Gradient Theorem】](https://www.tcom242242.net/entry/ai-2/%e5%bc%b7%e5%8c%96%e5%ad%a6%e7%bf%92/%e6%96%b9%e7%ad%96%e5%8b%be%e9%85%8d%e6%b3%95/policy-gradient-theorem-proof/)

これを計算すればいいわけですが解析的には解けないので、実際にはサンプリングして近似解を得ることで更新します。（モンテカルロ法）

``` math
\begin{align}
\nabla J(\theta) &\propto E_{\pi_{\theta}} \Bigl[ \nabla \log \pi_{\theta}(a|s) Q_{\pi_{\theta}}(s,a) \Bigr] \\
& \approx \frac{1}{M} \sum_{m=0}^{M-1} \frac{1}{T} \sum_{t=0}^{T-1} \nabla \log \pi_{\theta}(a_{t,m}|s_{t,m}) Q_{\pi_{\theta}}(s_{t,m},a_{t,m})
\end{align}
```

$M$ がエピソード回数、$T$ 1エピソードの総ステップ数です。


# ベースライン

更新パラメータの $\nabla J(\theta)$ ですが、分散が大きいと更新後の方策が大きく変わるので、分散が小さいほうが学習が安定します。
新たにベースライン $b(s)$ というものを用いて分散を小さくするテクニックを紹介します。
以下のように行動価値関数にベースラインを引くというものです。

``` math
\begin{align}
E_{\pi_{\theta}} \Big[ \nabla \log \pi_{\theta}(a|s) & Q_{\pi_{\theta}}(s,a) \Big] \\
↓ \\
E_{\pi_{\theta}} \Big[ \nabla \log \pi_{\theta}(a|s) & \Big( Q_{\pi_{\theta}} (s,a) - b(s) \Big) \Big]
\end{align}
```

ベースラインを引いても期待値に変化はありません。（分散のみ変わります）

変化がない事を、以下の式で右辺が0である事で確認してみました。
（数式に強くないので間違っていたらすいません）

``` math
\begin{align}
&E_{\pi_{\theta}} \Big[ \nabla \log \pi_{\theta}(a|s) \Big( Q_{\pi_{\theta}} (s,a) - b(s) \Big) \Big] \\
= &E_{\pi_{\theta}} \Big[ \nabla \log \pi_{\theta}(a|s) Q_{\pi_{\theta}}(s,a) \Big] - 
E_{\pi_{\theta}} \Big[  \nabla \log \pi_{\theta}(a|s) b(s) \Big]
\end{align}
```

以下0の計算。

``` math
\begin{align}

E_{\pi_{\theta}} \Big[ \nabla \log \pi_{\theta}(a|s) b(s) \Big]

&= E_{s_{0:t},a_{0:t-1}} \Big[ E_{s_{t+1:T},a_{t:T-1}} \Big[ \nabla \log \pi_{\theta}(a_t|s_t) b(s_t) \Big] \Big] \\

&= E_{s_{0:t},a_{0:t-1}} \Big[ b(s_t) E_{s_{t+1:T},a_{t:T-1}} \Big[ \nabla \log \pi_{\theta}(a_t|s_t) \Big] \Big] \\

&= E_{s_{0:t},a_{0:t-1}} \Big[ b(s_t) E_{,a_t} \Big[ \nabla \log \pi_{\theta}(a_t|s_t) \Big] \Big] \\

&= E_{s_{0:t},a_{0:t-1}} \Big[ b(s_t) \times 0 \Big] \\

&= 0 \\
\end{align}
```

$b(s)$ が外にでるのは $a$ に依存しないからです。
また、$E_{a_t}$ は以下です。

``` math
\begin{align}

E_{a_t} \Big[ \nabla \log \pi_{\theta}(a_t|s_t) \Big]

&= \int \frac{\nabla_\theta \pi_\theta(a_t|s_t)}{\pi_\theta(a_t|s_t)} \pi_\theta(a_t|s_t) da_t \\

&= \nabla_\theta \int \pi_\theta(a_t|s_t) da_t \\

&= \nabla_\theta 1 \\

&= 0 \\
\end{align}
```

ベースラインは状態 $s$ で決まる値ならどんな値でも問題ない事が分かります。


参考
・[強化学習理論の基礎3](https://qiita.com/sconeman/items/2432e8a1d1fb11c1111b)
・[Policy Gradient Algorithms](https://lilianweng.github.io/posts/2018-04-08-policy-gradient/)
・[Going Deeper Into Reinforcement Learning: Fundamentals of Policy Gradients](https://danieltakeshi.github.io/2017/03/28/going-deeper-into-reinforcement-learning-fundamentals-of-policy-gradients/)
・https://yagami12.hatenablog.com/entry/2019/02/22/210608





# REINFORCE

更新に必要な行動価値をモンテカルロ法で予測し、ベースラインをその平均値で引いたものがREINFORCEになります。
（もしかしたらベースラインはREINFORCEに含まれていないかも…）


``` math
\begin{align}
Q'_{\pi_{\theta}}(s_t,a_t) &= r_{t+1} + \gamma Q'_{\pi_{\theta}}(s_{t+1},a_{t+1}) \\
&=r_{t+1} + \gamma r_{t+2} + \gamma^2 r_{t+3} + ... + \gamma^{T-t} r_{T}\\
\end{align}
```

$Q'$ は実際にサンプリングして得た行動価値になります。
あるステップ$t$での行動価値は、そこからエピソード終了までに得た割引報酬の合計としてサンプリングします。

またベースラインは実際にサンプリングされた行動価値の平均値になります。


## Advantage

ベースラインに状態価値を用いた価値をAdvantageといいます。

``` math
B(s) = V(s) \\
A(s,a) = Q(s,a) - V(s)
```

``` math
E_{\pi_{\theta}} \Big[ \nabla \log \pi_{\theta}(a|s) \Big( Q_{\pi_{\theta}} (s,a) - B(s) \Big) \Big] \\
= E_{\pi_{\theta}} \Big[ \nabla \log \pi_{\theta}(a|s) A(s,a) \Big] \\
```

方策は行動の価値が知りたいだけで、現在の状態の価値は必要ないので、状態価値を引いたアクションのみの価値であるAdvantage関数を使用するという考えは理にかなっている気がします。

別の視点ですが、考え方はRainbowの[Dueling networks](https://qiita.com/pocokhc/items/9972a528901c15e42519#3-dueling-networks)と同じですね。（Advantageの意味についてはリンク先の記事を参照）



# 方策モデル

最後に実装に向けて $\nabla \log \pi_{\theta}(a|s)$ に関する具体的なモデルを見てみます。


## Softmax(離散値)

Softmax関数は以下で、パラメータは各アクション毎に用意されます。

$$
\pi_{\theta}(a_i|s) = f(\theta_i) = \frac{ e^{\theta_i}}{\sum_{k=1}^n e^{\theta_k}}  \quad(i=1,2,...,n)
$$

方策勾配定理で必要になる対数微分を計算します。


<details><summary>計算過程</summary><div>

``` math
\begin{align}
\nabla \log( f(x_i)) &= \nabla \log(\frac{e^{x_i}}{\sum_{k=1}^n e^{x_k}} ) \\
&= \nabla ( \log(e^{x_i}) - \log(\sum_{k=1}^n e^{x_k}))
\\
&= \nabla x_i - \nabla \log(\sum_{k=1}^n e^{x_k})
\\
&= 1 - \frac{\nabla \sum_{k=1}^n e^{x_k}}{\sum_{k=1}^n e^{x_k}}
\\
&= 1 - \frac{e^{x_i}}{\sum_{k=1}^n e^{x_k}}
\\
&= 1 - f(x_i)
\\
\end{align}
```

3から4行目の変換は微分の連鎖率を利用しています。

$$
\nabla \log f(x) = \frac{\nabla f(x)}{f(x)}
$$

参考：https://tadaoyamaoka.hatenablog.com/entry/2019/08/13/000546


</div></details>

$$
\nabla \log( \pi_{\theta}(a_i|s) ) = 1 - \pi_{\theta}(a_i|s)  \quad(i=1,2,...,n)
$$




## ガウス分布(連続値)

ガウス分布は以下で、パラメータは平均 $\mu$ と分散 $\sigma^2$ の２つとなります。

$$
\begin{align}
\pi_{\theta}(a|s) = f(a)  &= \frac{1}{\sqrt{2 \pi \sigma^2 } } 
\exp(- \frac{(a - \mu)^2}{ 2 \sigma^2} )
\end{align}
$$

対数微分ですが、パラメータ毎の偏微分になるので平均と分散を別々で計算します。

* 平均

<details><summary>計算過程</summary><div>

``` math
\begin{align}
\log(f(x)) &= \log (\frac{1}{\sqrt{2 \pi \sigma^2 } }
\exp(- \frac{(x - \mu)^2}{ 2 \sigma^2} ) ) \\
&= \log (\frac{1}{\sqrt{2 \pi \sigma^2 } }) + \log(\exp(- \frac{(x - \mu)^2}{ 2 \sigma^2} ) )
\\
&= log (1) - log(\sqrt{2 \pi \sigma^2 }) - \frac{(x - \mu)^2}{ 2 \sigma^2}
\\
&= 0 - log((2 \pi \sigma^2)^{ \frac{1}{2}}) - \frac{(x - \mu)^2}{ 2 \sigma^2}
\\
&= -\frac{1}{2} log(2 \pi \sigma^2) - \frac{(x - \mu)^2}{ 2 \sigma^2}
\\
\frac{\partial}{\partial \mu} \log(f(x)) &= \frac{\partial}{\partial \mu}(-\frac{1}{2} log(2 \pi \sigma^2)) - \frac{\partial}{\partial \mu}(\frac{(x - \mu)^2}{ 2 \sigma^2}))
\\
&= 0 - \frac{\partial}{\partial \mu}\frac{(x - \mu)^2}{ 2 \sigma^2} \\
&= \frac{2(x - \mu)}{ 2 \sigma^2} \\
&= \frac{x - \mu}{\sigma^2} \\
\end{align}
```

計算結果確認サイト：https://ja.wolframalpha.com/input?i2d=true&i=Partial%5BLog%5BDivide%5B1%2CSqrt%5B2%CF%80Power%5B%CF%83%2C2%5D%5D%5DExp%5B-Divide%5BPower%5B%5C%2840%29x-%CE%BC%5C%2841%29%2C2%5D%2C2Power%5B%CF%83%2C2%5D%5D%5D%5D%2C%CE%BC%5D

</div></details>

$$
\frac{\partial}{\partial \mu} \log( \pi_{\theta}(a|s) ) = \frac{a - \mu}{\sigma^2}
$$


* 分散

<details><summary>計算過程</summary><div>

```math
\begin{align}
\frac{\partial}{\partial \sigma} \log(f(x)) &= \frac{\partial}{\partial \sigma}  (-\frac{1}{2} log(2 \pi \sigma^2) - \frac{(x - \mu)^2}{ 2 \sigma^2})
\\
&= \frac{\partial}{\partial \sigma}(-\frac{1}{2} ( log(2 \pi) + \log(\sigma^2))) - \frac{\partial}{\partial \sigma}\frac{(x - \mu)^2}{ 2 \sigma^2}
\\
&= \frac{\partial}{\partial \sigma}(-\frac{1}{2} log(2 \pi)) - \frac{\partial}{\partial \sigma}(\frac{1}{2} \log(\sigma^2)) - \frac{\partial}{\partial \sigma}\frac{(x - \mu)^2}{ 2 \sigma^2}
\\
&= 0 - \frac{\partial}{\partial \sigma}(\log(\sigma)) - \frac{\partial}{\partial \sigma}\frac{(x - \mu)^2}{ 2 \sigma^2}
\\
&= -\frac{1}{\sigma} - \frac{-2(x - \mu)^2}{ 2 \sigma^3}
\\
&= -\frac{1}{\sigma} + \frac{(x - \mu)^2}{ \sigma^3}
\end{align}
```

計算結果確認サイト：https://ja.wolframalpha.com/input?i2d=true&i=Partial%5BLog%5BDivide%5B1%2CSqrt%5B2%CF%80Power%5B%CF%83%2C2%5D%5D%5DExp%5B-Divide%5BPower%5B%5C%2840%29x-%CE%BC%5C%2841%29%2C2%5D%2C2Power%5B%CF%83%2C2%5D%5D%5D%5D%2C%CF%83%5D

</div></details>

$$
\frac{\partial}{\partial \sigma} \log( \pi_{\theta}(a|s) ) = \frac{(a - \mu)^2 - \sigma^2}{ \sigma^3}
$$

実装は予測値をモンテカルロ法(エピソード最後まで展開)で求めます。

$$
Q_{\pi_{\theta}}(s_t, a_t) = \sum(r_{t+1} + \gamma r_{t+2} + \gamma^2 r_{t+3} +...)
$$

ベースラインは引いていません。
またサンプリングの回数は1回毎としています。
（バニラな方策勾配法です）



# 実装(Softmax)

関係ある箇所を抜粋して書いています。
フレームワーク上の実装は[github](https://github.com/pocokhc/simple_distributed_rl/blob/main/srl/algorithms/vanilla_policy.py)を見てください。


## Config(ハイパーパラメータ)

ハイパーパラメータは以下です。

``` python
@dataclass
class Config(TableConfig):
    gamma: float = 0.9  # 割引率
    lr: float = 0.1     # 学習率
```

## Parameter

ポリシーパラメータは状態毎・アクション毎に必要です。
状態は文字列にして柔軟に格納できるようにしています。


``` python
class Parameter(RLParameter):
    def __init__(self, *args):

        # ポリシーパラメータ
        self.policy = {}

    # 行動の各確率を返す
    def get_probs(self, state: str):
        # 状態がない場合、パラメータを初期化
        if state not in self.policy:
            self.policy[state] = [0.0 for _ in range(self.config.nb_actions)]

        probs = np.array(self.policy[state])

        # softmax
        probs = np.exp(probs)
        probs /= np.sum(probs)

        return probs
```



## Trainer

学習部分です。


``` python
class Trainer(RLTrainer):
    def train(self):
        batchs = self.remote_memory.sample()
        for batch in batchs:
            # 1stepで得た経験
            state = batch["state"]
            action = batch["action"]
            reward = batch["reward"]
            prob = self.parameter.get_probs(state)[action]

            # ∇logπ = 1 - prob
            diff_logpi = 1 - prob

            # ∇J = ∇logπ Q
            diff_j = diff_logpi * reward
            
            # θ ← θ + α∇J
            self.parameter.policy[state][action] += self.config.lr * diff_j
```

## Worker

確率を元にアクションを決めます。
また、モンテカルロ法により価値を求めます。


``` python
class Worker(TableWorker):
    def call_on_reset(self, state: np.ndarray, invalid_actions: List[int]) -> None:
        self.history = []  # 1エピソード分の経験を保存

    def call_policy(self, state: np.ndarray, invalid_actions: List[int]) -> int:
        # 状態を文字列にする
        self.state = str(state.tolist())

        # 各アクションの確率
        probs = self.parameter.get_probs(self.state)

        # 確率を元にアクションを決定
        self.action = np.random.choice([a for a in range(self.config.nb_actions)], p=probs)
        return self.action

    def call_on_step(
        self,
        next_state: np.ndarray,
        reward: float,
        done: bool,
        next_invalid_actions: List[int],
    ) -> Dict[str, Union[float, int]]:
        if not self.training:
            return {}
        # 各stepの経験を保存
        self.history.append([self.state, self.action, reward])
        
        # エピソード終了時に割引報酬を計算し、メモリに送る
        if done:
            reward = 0
            for h in reversed(self.history):
                reward = h[2] + self.config.gamma * reward
                batch = {
                    "state": h[0],
                    "action": h[1],
                    "reward": reward,
                }
                self.remote_memory.add(batch)

        return {}
```




# 実装(ガウス分布)

関係ある箇所を抜粋して書いています。
フレームワーク上の実装は[github](https://github.com/pocokhc/simple_distributed_rl/blob/main/srl/algorithms/vanilla_policy.py)を見てください。


## Config(ハイパーパラメータ)

ハイパーパラメータは以下です。

``` python
@dataclass
class Config(TableConfig):
    gamma: float = 0.9  # 割引率
    lr: float = 0.1     # 学習率
```

## Parameter

パラメータは各状態に対して平均と標準偏差の線形値となります。
標準偏差は正 $\sigma > 0$ の制約を持つので、指数関数を通しています。

``` python
class Parameter(RLParameter):
    def __init__(self, *args):

        # ポリシーパラメータ
        self.policy = {}

    # パラメータを返す
    def get_param(self, state: str):
        # 状態がない場合、パラメータを初期化
        if state not in self.policy:
            self.policy[state] = {
                "mean": 0.0,
                "stddev_logits": 0.5,
            }
        
        mean = self.policy[state]["mean"]
        stddev = np.exp(self.policy[state]["stddev_logits"])

        return mean, stddev
```


## Trainer

学習部分です。
logpiの計算部分を見ればわかりますが、値によっては更新幅がかなり大きな値になります。
（平均でσ^2、分散でσ^3の計算をしているので、例えばσが0.01の場合、分散はn/(0.01^3)の形になり、n=1の場合で1000000の値を取ります。）

勾配法の特性上、更新幅が大きすぎると学習が安定しません。
ですので、更新幅に制限を設けています。（値はヒューリスティックな値です）


``` python
class Trainer(RLTrainer):
    def train(self):
        batchs = self.remote_memory.sample()
        for batch in batchs:
            # 1stepで得た経験
            state = batch["state"]
            action = batch["action"]
            reward = batch["reward"]
            mean, stddev = self.parameter.get_param(state)

            # 平均
            mean_diff_logpi = (action - mean) / (stddev**2)
            mean_diff_j = mean_diff_logpi * reward
            new_mean = self.parameter.policy[state]["mean"] + self.config.lr * mean_diff_j

            # 分散
            stddev_diff_logpi = (((action - mean) ** 2) - (stddev**2)) / (stddev**3)
            stddev_diff_j = stddev_diff_logpi * reward
            new_stddev = self.parameter.policy[state]["stddev_logits"] + self.config.lr * stddev_diff_j

            # 更新幅が大きすぎる場合は更新しない
            if abs(mean_diff_j) < 1 and abs(stddev_diff_j) < 5:
                self.parameter.policy[state]["mean"] = new_mean
                self.parameter.policy[state]["stddev_logits"] = new_stddev

```



## Worker

平均・分散を元にアクションを決めます。
また、予測値がないのでモンテカルロ法により価値を求めます。


``` python
class Worker(ActionContinuousWorker):
    def call_on_reset(self, state: np.ndarray) -> None:
        self.history = []  # 1エピソード分の経験を保存

    def call_policy(self, state: np.ndarray) -> List[float]:
        # 状態を文字列にする
        self.state = str(state.tolist())

        # パラメータ
        mean, stddev = self.parameter.get_param(self.state)

        # ガウス分布に従った乱数を出す
        self.action = env_action = mean + np.random.normal() * stddev

        # ガウス分布は-inf～infの範囲を取るので、
        # 実際に環境に渡すアクションは、最小と最高で切り取る
        # 本当はポリシーが変化しちゃうのでよくない（暫定対処）
        env_action = np.clip(env_action, self.config.action_low[0], self.config.action_high[0])

        return env_action

    def call_on_step(
        self,
        next_state: np.ndarray,
        reward: float,
        done: bool,
        next_invalid_actions: List[int],
    ) -> Dict:
        if not self.training:
            return {}
        # 各stepの経験を保存
        self.history.append([self.state, self.action, reward])
        
        # エピソード終了時に割引報酬を計算し、メモリに送る
        if done:
            reward = 0
            for h in reversed(self.history):
                reward = h[2] + self.config.gamma * reward
                batch = {
                    "state": h[0],
                    "action": h[1],
                    "reward": reward,
                }
                self.remote_memory.add(batch)

        return {}
```


# 実行結果(softmax)

本フレームワーク内にある Grid という環境を学習した結果は以下です。

※srl v0.18.0 でのコード例です。（バージョンが変わると動かない可能性があります）

``` python
import numpy as np

import srl
from srl.algorithms import vanilla_policy


def main():
    rl_config = vanilla_policy.Config()
    runner = srl.Runner("Grid", rl_config)

    # --- train
    runner.train(timeout=10)

    # --- evaluate
    rewards = runner.evaluate(max_episodes=100)
    print(f"Average reward for 100 episodes: {np.mean(rewards)}")

    # --- render
    runner.render_terminal()


if __name__ == "__main__":
    main()

```

* 学習結果（一部）

``` python
### 最初の地点
......
.   G.
. . X.
.P   .
......
 ←  :   0.1% (-2.72310)
 ↓  :   1.5% (0.31042)
 →  :   0.0% (-4.81747)
*↑  :  98.4% (4.46286)

### ゴール手前
......
.  PG.
. . X.
.    .
......
 ←  :   0.0% (-0.12817)
 ↓  :   0.0% (-0.31378)
*→  :  98.6% (24.20097)
 ↑  :   1.4% (19.93994)
 ```



<details><summary>学習結果(全体)</summary><div>


```
Average reward for 100 episodes: 0.7299999999999999
### 0, action 3, rewards [0.], next 0
env   None
work0 None
......
.   G.
. . X.
.P   .
......


 ←  :   0.1% (-2.72310)
 ↓  :   1.5% (0.31042)
 →  :   0.0% (-4.81747)
*↑  :  98.4% (4.46286)

### 1, action 3, rewards [-0.04], next 0
env   {}
work0 {}
......
.   G.
.P. X.
.    .
......


 ←  :   0.0% (-0.91846)
 ↓  :   0.0% (-2.24249)
 →  :   1.3% (4.22557)
*↑  :  98.7% (8.58121)

### 2, action 2, rewards [-0.04], next 0
env   {}
work0 {}
......
.P  G.
. . X.
.    .
......


 ←  :   0.0% (-0.70717)
 ↓  :   0.0% (-0.18141)
*→  :  98.7% (13.16927)
 ↑  :   1.3% (8.80523)

### 3, action 3, rewards [-0.04], next 0
env   {}
work0 {}
......
.   G.
.P. X.
.    .
......


 ←  :   0.0% (-0.91846)
 ↓  :   0.0% (-2.24249)
 →  :   1.3% (4.22557)
*↑  :  98.7% (8.58121)

### 4, action 2, rewards [-0.04], next 0
env   {}
work0 {}
......
.P  G.
. . X.
.    .
......


 ←  :   0.0% (-0.70717)
 ↓  :   0.0% (-0.18141)
*→  :  98.7% (13.16927)
 ↑  :   1.3% (8.80523)

### 5, action 3, rewards [-0.04], next 0
env   {}
work0 {}
......
.   G.
.P. X.
.    .
......


 ←  :   0.0% (-0.91846)
 ↓  :   0.0% (-2.24249)
 →  :   1.3% (4.22557)
*↑  :  98.7% (8.58121)

### 6, action 2, rewards [-0.04], next 0
env   {}
work0 {}
......
.P  G.
. . X.
.    .
......


 ←  :   0.0% (-0.70717)
 ↓  :   0.0% (-0.18141)
*→  :  98.7% (13.16927)
 ↑  :   1.3% (8.80523)

### 7, action 2, rewards [-0.04], next 0
env   {}
work0 {}
......
. P G.
. . X.
.    .
......


 ←  :   0.0% (-0.62838)
 ↓  :   0.5% (4.92942)
*→  :  99.5% (10.28625)
 ↑  :   0.1% (2.98535)

### 8, action 2, rewards [-0.04], next 0
env   {}
work0 {}
......
.  PG.
. . X.
.    .
......


 ←  :   0.0% (-0.12817)
 ↓  :   0.0% (-0.31378)
*→  :  98.6% (24.20097)
 ↑  :   1.4% (19.93994)

### 9, action 2, rewards [1.], done(env), next 0
env   {}
work0 {}
......
.   P.
. . X.
.    .
......

```

</div></details>



# 実行結果(ガウス分布)

本フレームワーク内にある Grid という環境を学習した結果は以下です。

Gridの入力は離散値なのですが、強制的に連続値に変換しています。
なので、

  0～0.5 : 0 (LEFT)
0.5～1.5 : 1 (DOWN)
1.5～2.5 : 2 (RIGHT)
2.5～3.0 : 3 (UP)

となります。

※srl v0.18.0 でのコード例です。（バージョンが変わると動かない可能性があります）

``` python
import numpy as np

import srl
from srl.algorithms import vanilla_policy


def main():
    rl_config = vanilla_policy.Config(override_action_type="CONTINUOUS")
    runner = srl.Runner("Grid", rl_config)

    # --- train
    runner.train(timeout=10)

    # --- evaluate
    rewards = runner.evaluate(max_episodes=100)
    print(f"Average reward for 100 episodes: {np.mean(rewards)}")

    # --- render
    runner.render_terminal()


if __name__ == "__main__":
    main()

```

* 学習結果（一部）

``` python
### 最初
......
.   G.
. . X.
.P   .
......
mean 7.02388, stddev 0.00477

### ゴール手前
......
.  PG.
. . X.
.    .
......
mean 2.01022, stddev 0.02746
```


<details><summary>学習結果（全体）</summary><div>


```
Average reward for 100 episodes: 0.6944000054895878
### 0, action 3, rewards [0.], next 0
env   None
work0 None
......
.   G.
. . X.
.P   .
......


mean 7.02388, stddev 0.00477

### 1, action 0, rewards [-0.04], next 0
env   {}
work0 {}
......
.   G.
. . X.
. P  .
......


mean -1.02970, stddev 0.01026

### 2, action 3, rewards [-0.04], next 0
env   {}
work0 {}
......
.   G.
. . X.
.P   .
......


mean 7.02388, stddev 0.00477

### 3, action 3, rewards [-0.04], next 0
env   {}
work0 {}
......
.   G.
.P. X.
.    .
......


mean 9.90385, stddev 0.01101

### 4, action 2, rewards [-0.04], next 0
env   {}
work0 {}
......
.P  G.
. . X.
.    .
......


mean 1.63379, stddev 0.00830

### 5, action 3, rewards [-0.04], next 0
env   {}
work0 {}
......
.   G.
.P. X.
.    .
......


mean 9.90385, stddev 0.01101

### 6, action 3, rewards [-0.04], next 0
env   {}
work0 {}
......
.   G.
.P. X.
.    .
......


mean 9.90385, stddev 0.01101

### 7, action 2, rewards [-0.04], next 0
env   {}
work0 {}
......
.P  G.
. . X.
.    .
......


mean 1.63379, stddev 0.00830

### 8, action 2, rewards [-0.04], next 0
env   {}
work0 {}
......
. P G.
. . X.
.    .
......


mean 1.99982, stddev 0.00659

### 9, action 2, rewards [-0.04], next 0
env   {}
work0 {}
......
. P G.
. . X.
.    .
......


mean 1.99982, stddev 0.00659

### 10, action 2, rewards [-0.04], next 0
env   {}
work0 {}
......
. P G.
. . X.
.    .
......


mean 1.99982, stddev 0.00659

### 11, action 2, rewards [-0.04], next 0
env   {}
work0 {}
......
. P G.
. . X.
.    .
......


mean 1.99982, stddev 0.00659

### 12, action 2, rewards [-0.04], next 0
env   {}
work0 {}
......
.  PG.
. . X.
.    .
......


mean 2.01022, stddev 0.02746

### 13, action 2, rewards [-0.04], next 0
env   {}
work0 {}
......
.  PG.
. . X.
.    .
......


mean 2.01022, stddev 0.02746

### 14, action 2, rewards [1.], done(env), next 0
env   {}
work0 {}
......
.   P.
. . X.
.    .
......

```

</div></details>