LoginSignup
13

More than 5 years have passed since last update.

強化学習入門#2 PyTorchでのネットワークの作り方と、TensorBoardX、CartPole攻略

Posted at

引き続きPyTorch・強化学習の入門と復習用の記事です。過去の記事はこちら。#1
環境は引き続きKaggle Kernelで進めていきます(バージョンなどの詳細は#1の記事をご確認ください)。

この記事で触れること

  • PyTorchでのネットワークの作り方の基礎の復習
  • 学習過程のビジュアライズのための、クラウドカーネル上でのTensorBoardXの利用
  • Cross Entropyを使った、CartPoleの強化学習による攻略

各コードに関しては、Kaggle Kernelで公開しておいたので、動かしたいときなどはそのままforkしたり、Colaboratoryなどでご利用ください。
Kaggle Kernel : https://www.kaggle.com/simonritchie/2-pytorch-tensorboardx-cartpole

PyTorchでのネットワークの作り方

ネットワークを組むには、nnモジュールを使っていきます。
試しに、(2,)のサイズのベクトルを入力値として受け取り、(5,)のサイズのベクトル値を出力する、シンプルな線形のネットワークを考えてみます。

from torch import nn
import torch

in_featuresとout_featuresに、それぞれ入力値と出力値の型を指定します。今回は入力値2、出力値5としました。

linear_net = nn.Linear(in_features=2, out_features=5)

サイズが(2,)のベクトルを用意して、ネットワークに入れてみます。

vector = torch.FloatTensor([1, 2])
linear_net(input=vector)

そうすると、(5,)のサイズで指定した形でベクトルが出力されます。

tensor([-0.7198, -0.1944, -1.6179, -0.2621,  1.2208], grad_fn=<ThAddBackward>)

また、もちろん他のディープラーニングライブラリで用意されている、ReLUやDropout、Softmaxといったものは最初から色々用意されています。
KerasのようにSequentialクラスを使っていくことで、それらをシンプルに繋げてネットワークを作ることができます。(それぞれ、inとoutの値を揃えていきます)

sequential = nn.Sequential(
    nn.Linear(in_features=2, out_features=5),
    nn.ReLU(),
    nn.Linear(in_features=5, out_features=20),
    nn.ReLU(),
    nn.Linear(in_features=20, out_features=10),
    nn.Dropout(p=0.5),
    nn.Softmax(dim=0),
)

生成したSequentialのオブジェクトを出力すると、各層の情報が出力されます。

sequential
Sequential(
  (0): Linear(in_features=2, out_features=5, bias=True)
  (1): ReLU()
  (2): Linear(in_features=5, out_features=20, bias=True)
  (3): ReLU()
  (4): Linear(in_features=20, out_features=10, bias=True)
  (5): Dropout(p=0.5)
  (6): Softmax()
)

先ほどと同様に、入力値を入れると出力値として指定した形でのベクトルが返ってきます。

output_vector = sequential(input=vector)
output_vector
tensor([0.0804, 0.0277, 0.0804, 0.1484, 0.0804, 0.1735, 0.1258, 0.0941, 0.0758,
        0.1134], grad_fn=<SoftmaxBackward>)

独自の層を作りたい場合

Moduleクラスを継承してクラスを作成し、forward関数を上書きしてそこにやりたい計算を記載します。

class YourLayer(nn.Module):

    def __init__(self):
        super(YourLayer, self).__init__()

    def forward(self, x):
        return x * 100


sequential = nn.Sequential(
    nn.Linear(in_features=2, out_features=5),
    nn.ReLU(),
    YourLayer(),
)
sequential(input=vector)
tensor([17.8000,  0.0000, 34.3154,  0.0000, 73.7268], grad_fn=<MulBackward>)

損失関数やオプティマイザ

こちらも同様に、MSELossやSGD、Adamなどの有名どころはnnパッケージや、torch.optimパッケージ内に色々入っているのでそちらを利用していきます。

mse = nn.MSELoss()
output_tensor = torch.FloatTensor([1, 2, 3])
y_tensor = torch.FloatTensor([2, 2, 3])
mse(output_tensor, y_tensor)
tensor(0.3333)
from torch.optim import Adam

学習過程をビジュアライズする : TensorBoardX

TensorFlowやKerasなどで学習を進める場合、TensorBoardで学習過程のビジュアライズなどを行うことが多いと思います。
PyTorchでは、TensorBoardの亜種のTensorBoardXを使うと便利なそうです。(PyTorch、Chainer、NumPyなどのためのTensorBoard)

ただし、自身のローカルだったり、もしくはJupyterの起動から自分で色々調整できる環境ではなく、Kaggle Kernel(もしくはColaboratoryなど)ではTensorBoard起動後のアクセスをどうすべきなのか、という問題があります。

軽く調べた感じ、他人のKaggle KernelのTensorboard on Kaggle - Very Concise - Part-1をforkして動かしてみたところ、普通のTensorBoardが使えることを確認できたので、こちらでTensorBoardXを試してみます。
(他にも、Jupyter上でTensorBoardを表示できるようにする、といったライブラリなど世の中に色々ある印象ではあります)

どうやら、ngrokというサービスを使っているようです。

ngrokとは
簡単にいうと、ローカルPC上で稼働しているネットワーク(TCP)サービスを外部公開できるサービスです。例えば、ローカルPCのWebサーバを外部公開することができます。
ngrokが便利すぎる

仕事で使うのは公開面的に問題になりそうですが、仕事ではUbuntuのラップトップなりを使えばいいのでこのまま進めます。(補足 無料のアカウント登録をすると、サブドメインやパスワードの保護ができるようです。登録無しの場合有効期限が8時間のようですが、ColaboratoryやKaggle Kernelで扱う分には問題ない時間と思われます。参考 : ローカルサイトを外部に公開するためのツール「ngrok」が便利いつの間にかアカウント登録無しでのngrok利用にセッション時間制限が設定されてた。

まずはTensorboardXのインストール。Colaboratoryの場合、なにも考えずにpipコマンドが使えますが、Kaggle Kernelではインターネットの接続を有効にしないといけないので注意してください。カーネルの右のメニューのSeetingsのInternetの部分で、設定を切り替えられます。

!pip install tensorboardX
...
Successfully installed tensorboardX-1.4

シンプルに、整数の乱数を書きこんでいってみましょう。

import math

import numpy as np
from tensorboardX import SummaryWriter

TENSOR_BOARD_LOG_DIR = './tensorboard_log'
writer = SummaryWriter(log_dir=TENSOR_BOARD_LOG_DIR)

for i in np.arange(0, 100):
    scalar_value = np.random.randint(0, 100)
    writer.add_scalar(
        tag='sample_data', scalar_value=scalar_value, 
        global_step=i)
writer.close()

SummaryWriterクラスをインスタンス化し、add_scalarメソッドで時系列の値をTensorBoardXに追加することができます。データの保存先のディレクトリは任意の文字列に調整してください。
引数のtagにはなんのデータなのかが区別が付くように任意の文字列、scalar_valueに追加したい数値、global_stepには時系列のプロットの横軸の値(インデックスや学習のイテレーション数など)を指定します。

!wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip
!unzip ngrok-stable-linux-amd64.zip

続いてngrokのダウンロードと解凍関係。これは特に考えることはなく実行するだけです。

tensorboard_command = 'tensorboard --logdir %s --host localhost --port 6006 &' \
    % TENSOR_BOARD_LOG_DIR
ngrok_command = './ngrok http 6006 &'

TensorBoard起動用のコマンドと、ngrok用のコマンドを定義しました。
TensorBoardのコマンドのパスは、SummaryWriterで指定した時のディレクトリと同じパスを指定してください。
また、TensorBoardとngrokのポート番号も一致させてください。
コマンド最後の&の記号は、コマンドを非同期で行う、といった記述です。これをしないと、ノート上でコマンドを実行する都合、TensorBoardなどを停止させるまで、他のセルの実行ができなくなってしまいます。

TensorBoardなどのコマンドの実行は、ipythonのインスタンス経由で行います。!の記号を付与した状態でのコマンドではうまくいきませんでした。

ipython_instance = get_ipython()
ipython_instance.system_raw(cmd=tensorboard_command)
ipython_instance.system_raw(cmd=ngrok_command)

続いてngrokのAPIを実行して、TensorBoardへのアクセス用のURLを作成します。(お手軽・・)

output_list = !curl -s http://localhost:4040/api/tunnels
output_list = list(output_list)

なお、!の記号での結果の出力を、普通にイコールの記述だけでJupyterで変数に格納できるようです(知らなかった)。IPython関係のリストのサブクラスのような型で格納されるようで、一旦普通のlistにキャストしておきます。

import json
ngrok_result_dict = json.loads(output_list[0])
print(ngrok_result_dict['tunnels'][0]['public_url'])

出力の先頭のインデックスに、ngrokのAPI結果のJSON形式の文字列が格納されるので、jsonモジュールでリストに変換し、結果のpublic_urlのキーに対象のTensorBoardアクセス用のURLが格納されているので表示します。

http://9453ef56.ngrok.io

上記のようなURLが表示されると思います。(実行の度に変わります)
アクセスすると、Kaggle Kernel上の値のTensorBoardが確認できます。

20181208_1.png

TensorFlowやKerasでTensorBoardをよく使っている方はご存知だと思いますが、TensorBoardXでは以下の特徴があります。

  • インタラクティブなプロット
  • GUIですぐに移動平均や表示・非表示などの設定が変更できる
  • 値がリアルタイム(少しラグがあるので、急ぎで確認したい場合は右上の更新アイコンクリック)に更新されていく(長期間の学習時など、終わる前から過程を観測し続けられる)
  • 過去のデータとも比較がしやすい(実験・検証時などに便利)
  • 画像だったり色々なデータを放り込める(この点は使わないので、記事では取り扱いません)

簡単に、いくつか基本的なところを説明しておきます。

移動平均の調整

20181209_1.png

上記の赤枠部分のSmoothingの部分を調整すると、移動平均的なところが調整できます。
結構上下に値がぶれて、傾向が見えづらい場合に高い値を設定すると、値の傾向(上がっているのか下がっているのか)が見えやすくなります。
薄い色で表示されているのが、実際の値、濃い色が平均の値となります。

複数のプロット(別のタグ名)

add_scalar関数実行時に、tagの引数を指定しますが、これを別の値にすることで、別のプロットを表示することができます。

writer = SummaryWriter(log_dir=TENSOR_BOARD_LOG_DIR)
for i in np.arange(0, 100):
    scalar_value = np.random.randint(0, 100)
    writer.add_scalar(
        tag='sample_data_2', scalar_value=scalar_value, 
        global_step=i)
writer.close()

20181209_2.png

CartPoleをCrossEntropyメソッドで解く

ここまでで、簡単なPyTorchやGym、TensorBoardなどの使い方の復習が終わったので、実際にCartPoleのゲームを解いていきます。
DQN方面の手法の方が強力だったりはしますが、結構複雑になってくるので、まずは入門としてCrossEntropyメソッドで解いていきます。複雑な問題は他の手法が必要になってきますが、今回のCartPoleであればCrossEntropyでも十分に解けます。
単語自体はディープラーニングの誤差関数でよく出てきますが、今回使うCrossEntropyメソッドは、「良くないEpisodeのものを切り捨てて、良いものだけ残して学習を進めていく」といった、遺伝的な感じの手法になります。

The method approximates the optimal importance sampling estimator by repeating two phases[1]:
1. Draw a sample from a probability distribution.
2. Minimize the cross-entropy between this distribution and a target distribution to produce a better sample in the next iteration.
WikiPediaより。

流れとしては以下のようなものになります。

  1. 指定の回数、Episodeを実行する
  2. 各Episodeでの合計のRewardを計算し、指定する一定のしきい値未満になるかどうかで、そのEpisodeを切り捨てるか残すのかを判定する
  3. 残す判定になった、優れたEpisode群を使って学習を続ける
  4. 以降、目標としていた値を達成するまでこの計算を繰り返す

早速Pythonで書いていきます。

import gym
import numpy as np
from tensorboardX import SummaryWriter
import torch
from torch import nn
from torch.optim import Adam

まずは必要なモジュールのインポート。

HIDDEN_SIZE = 128
BATCH_SIZE = 16
PERCENTILE = 70

続いて、3つの定数を定義しました。
今回、隠れ層は1つだけのシンプルなネットワークで学習を進めます。HIDDEN_SIZEはその隠れ層のUnitの数として定義しています。

BATCH_SIZEは、普通のディープラーニングでは一度に同時に計算する数として、パフォーマンス・GPUメモリを加味して設定されますが、今回の強化学習では意味合いか若干異なり、「一度に試すEpisodeの数」として使います。つまり、16個のEpisodeを試し、その中から優れたEpisodeのみ学習のために残す、といった具合になります。

PERCENTILEはEpisodeの残す / 捨てるの判断のためのしきい値として参照します。今回は70を指定しているので、上位30%だけを残す、といった具合です。

env = gym.make('CartPole-v0')
env.observation_space.shape
(4,)
OBSERVATION_SIZE = env.observation_space.shape[0]
NUM_ACTIONS = env.action_space.n
NUM_ACTIONS
2

CartPoleのEnvironmentを用意し、観測値(Observation)の値の数と取れるActionの数を定数に入れます。#1の時に調べた通り、CartPoleではObservationは棒の傾きなどの4つの値と、取れるActionは土台を左に動かすか右に動かすかの2のみです。

network = nn.Sequential(
    nn.Linear(in_features=OBSERVATION_SIZE, out_features=HIDDEN_SIZE),
    nn.ReLU(),
    nn.Linear(in_features=HIDDEN_SIZE, out_features=NUM_ACTIONS),
)

ネットワークはまだとてもシンプルです。入力層、ReLUの活性化関数、出力層のみです。

class Episode():
    """
    Episodeの情報を保持するためのクラス。

    Attributes
    ----------
    reward : int
        獲得した報酬の値。
    episode_step_list : list of EpisodeStep
        Episode内の各アクション単位のオブジェクトを格納したリスト。
    """

    def __init__(self, reward, episode_step_list):
        """
        Parameteres
        -----------
        reward : int
            獲得した報酬の値。
        episode_step_list : list of EpisodeStep
            Episode内の各アクション単位のオブジェクトを格納したリスト。
        """
        self.reward = reward
        self.episode_step_list = episode_step_list        


class EpisodeStep():
    """
    Episode中のAction単体分の情報の保持するためのクラス。

    Attributes
    ----------
    observation : ndarray
        (4,)のサイズの、観測値の配列。
    action : int
        選択されたActionの番号。
    """

    def __init__(self, observation, action):
        """
        Parameters
        ----------
        observation : ndarray
            (4,)のサイズの、観測値の配列。
        action : int
            選択されたActionの番号。
        """
        self.observation = observation
        self.action = action

また、学習の過程のEpisode関係の情報を保持するためのクラスを設けました。
Action単体の情報をEpisodeStepクラス、Episodeの始まりから終わりまでの情報を扱うためのクラスとしてEpisodeクラスを用意しました。一つ一つのActionのリストを保持する形でEpisodeクラスを使う、といった具合です。

続いて、1つのバッチ分の記述を書いていきます。
前述の通り、バッチのサイズが「実行するEpisodeの数」となります。
後でループで回す形になりますが、最初は分かりやすく、且つ確認を取りつつ進めるため、バッチ単体でのコードを書いていきましょう。

# 一度のバッチでの各Episodeの情報を格納するリスト。
episode_list = []

episode_reward = 0.0
episode_step_list = []

obs = env.reset()
sm = nn.Softmax(dim=1)

while True:
    obs_v = torch.FloatTensor([obs])
    act_probabilities_v = sm(network(input=obs_v))
    act_probabilities = act_probabilities_v.data.numpy()[0]
    action = np.random.choice(a=len(act_probabilities), p=act_probabilities)

    next_obs, reward, is_done, _ = env.step(action=action)
    episode_reward += reward

    # 新しいObservationではなく、今回のRewardを獲得した時点のObservation
    # をリストに追加します。
    episode_step = EpisodeStep(observation=obs, action=action)
    episode_step_list.append(episode_step)

    # is_doneがTrueになった、ということはEpisode単体の終了を意味します。
    if is_done:
        episode = Episode(
            reward=episode_reward, episode_step_list=episode_step_list)
        episode_list.append(episode)

        # 次のEpisodeのために、各値をリセットします。
        episode_reward = 0.0
        episode_step_list = []
        next_obs = env.reset()

        if len(episode_list) == BATCH_SIZE:
            break

    obs = next_obs

ループでバッチサイズで指定した分、Episodeが完了したらbreakさせています。
episode_rewardがEpisode単体のRewardで、Actionごとの値が積み上げられていきます。
各変数などを、少し詳細まで見てみましょう。

obs_v = torch.FloatTensor([obs])
obs_v.shape
torch.Size([1, 4])

PyTorchで扱う都合、(4,)のサイズから、(1, 4)のサイズになるように次元の調整しています。

act_probabilities_v = sm(network(input=obs_v))
act_probabilities_v
tensor([[0.5039, 0.4961]], grad_fn=<SoftmaxBackward>)

Actionの選択用に、Softmaxを通して確率を出しています。
初期値はほぼ50%ずつです。

act_probabilities_v.data
tensor([[0.5039, 0.4961]])

FloatTensorクラスのオブジェクトの、data属性には、勾配関係とは無関係の単純なテンソルのデータが格納されています。

act_probabilities_v.data.numpy()
array([[0.5038988 , 0.49610123]], dtype=float32)

そちらのテンソルで、numpy関数を通すとNumPy配列が取得できます。
Actionの選択にNumPyを使うので、一度変換をしています。

バッチ1回分の処理を流してみて、内容をさらに確認してみます。

episode_list
[<__main__.Episode at 0x7f4101c15080>,
 <__main__.Episode at 0x7f4101c15128>,
 <__main__.Episode at 0x7f4101c156d8>,
 <__main__.Episode at 0x7f4101ba1080>,
 <__main__.Episode at 0x7f4101ba1160>,
 <__main__.Episode at 0x7f4101ba16a0>,
 <__main__.Episode at 0x7f4101ba1a20>,
 <__main__.Episode at 0x7f4101ba1cf8>,
 <__main__.Episode at 0x7f4101bbd080>,
 <__main__.Episode at 0x7f4101bc4080>,
 <__main__.Episode at 0x7f4101bc8080>,
 <__main__.Episode at 0x7f4101bc8240>,
 <__main__.Episode at 0x7f4101bc8550>,
 <__main__.Episode at 0x7f4101bc89b0>,
 <__main__.Episode at 0x7f4101bc8c88>,
 <__main__.Episode at 0x7f4101bcd080>]

バッチサイズを16としてあるので、今回は16件分のEpisodeがリストに追加されます。

for episode in episode_list:
    print('Episode reward :', episode.reward)
Episode reward : 45.0
Episode reward : 25.0
Episode reward : 28.0
Episode reward : 17.0
Episode reward : 23.0
Episode reward : 15.0
Episode reward : 12.0
Episode reward : 13.0
Episode reward : 19.0
Episode reward : 111.0
Episode reward : 20.0
Episode reward : 13.0
Episode reward : 19.0
Episode reward : 12.0
Episode reward : 12.0
Episode reward : 37.0

まだ学習をさせておらず、ランダムな感じなので、獲得てきるRewardは安定せずばらけています。

バッチ単体でのコードが大雑把に確認が取れたので、イテレーションでバッチ単位で何度も計算する都合、関数化しておきます。
内容はほぼ変わりません。breakでループを止めていた個所をreturnに変えて、Episodeのリストを返却するように調整しただけです。

def iter_batch():
    """
    バッチ1回分の処理を行う。

    Returns
    -------
    episode_list : list of Episode
        1回のバッチで実行されたEpisodeを格納したリスト。
        バッチサイズの件数分、Episodeが追加される。
    """
    # 一度のバッチでの各Episodeの情報を格納するリスト。
    episode_list = []

    episode_reward = 0.0
    episode_step_list = []

    obs = env.reset()
    sm = nn.Softmax(dim=1)

    while True:
        obs_v = torch.FloatTensor([obs])
        act_probabilities_v = sm(network(input=obs_v))
        act_probabilities = act_probabilities_v.data.numpy()[0]
        action = np.random.choice(a=len(act_probabilities), p=act_probabilities)

        next_obs, reward, is_done, _ = env.step(action=action)
        episode_reward += reward

        # 新しいObservationではなく、今回のRewardを獲得した時点のObservation
        # をリストに追加します。
        episode_step = EpisodeStep(observation=obs, action=action)
        episode_step_list.append(episode_step)

        # is_doneがTrueになった、ということはEpisode単体の終了を意味します。
        if is_done:
            episode = Episode(
                reward=episode_reward, episode_step_list=episode_step_list)
            episode_list.append(episode)

            # 次のEpisodeのために、各値をリセットします。
            episode_reward = 0.0
            episode_step_list = []
            next_obs = env.reset()

            if len(episode_list) == BATCH_SIZE:
                return episode_list

        obs = next_obs

後で使うので、誤差関数やAdamのオプティマイザを用意しておきます。
Adamのparams引数で、対象となるパラメーターを指定する形となりますが、基本的にはネットワークのparameter()関数で返却される値を指定すれば動きます。

loss_func = nn.CrossEntropyLoss()
optimizer = Adam(params=network.parameters(), lr=0.01)
writer = SummaryWriter(log_dir=TENSOR_BOARD_LOG_DIR)

cross-entropyの実装を進めます。
指定の割合によるしきい値で、優れたRewardのもののみを残していくといった対応になります。
最終的にはこちらもループで回していくことになるので関数化しますが、まずは分かりやすく確認しやすいように、バッチ単体を想定したコードで進めます。

reward_list = []
for episode in episode_list:
    reward_list.append(episode.reward)
reward_bound = np.percentile(a=reward_list, q=PERCENTILE)
reward_mean = float(np.mean(reward_list))

train_obs_list = []
train_act_list = []
for episode in episode_list:
    # 各Episodeに対して、パーセンタイルで算出したしきい値未満のものを
    # 対象外とする。
    if episode.reward < reward_bound:
        continue

    for episode_step in episode.episode_step_list:
        train_obs_list.append(episode_step.observation)
        train_act_list.append(episode_step.action)

train_obs_v = torch.FloatTensor(train_obs_list)
train_act_v = torch.LongTensor(train_act_list)

reward_boundが、バッチ単位でのEpisodeのリストからパーセンタイルで算出した、しきい値の値となります。

reward_bound
24.0

お試しの1つのバッチでは、しきい値が24でした。

reward_mean
26.3125

reward_meanという変数に、対象のバッチにおけるRewardの平均値を格納しています。
この値は、学習が終わったかどうか(十分に棒が倒れないレベルまで学習できたか)の判定に使用します。平均を使っているのは、単体だとランダム要素が強いため、偶然高い数字が出て学習終了という判定にならないようにするためです。

train_obs_v
tensor([[ 2.5499e-02, -1.8400e-03, -4.0561e-02, -1.0612e-02],
        [ 2.5462e-02, -1.9636e-01, -4.0773e-02,  2.6900e-01],
        [ 2.1535e-02, -6.7806e-04, -3.5393e-02, -3.6256e-02],
        [ 2.1522e-02,  1.9493e-01, -3.6118e-02, -3.3989e-01],
        [ 2.5420e-02,  3.4317e-04, -4.2916e-02, -5.8815e-02],
...
        [-1.3484e-01,  4.0126e-01, -2.0343e-01, -1.6489e+00]])
train_obs_v.shape
torch.Size([246, 4])
train_act_v
tensor([0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0,
        0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1,
        0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1,
        0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0,
        1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1,
        1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1,
        1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1,
        1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1,
        1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0,
        0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0,
        1, 1, 1, 1, 1, 1])
train_act_v.shape
torch.Size([246])

train_obs_vとtrain_act_vは、それぞれ学習のためのObservationとActionを格納したテンソルです。しきい値以上の(優秀な)Episodeの値のみを格納します。
train_act_vの方でLongTensorを使っていますが、なんでだろう、と調べたり他で動かしてみこところ、あとで使う誤差関数のところで指定する際に、弾かれました。どうやら整数の値としてはLongTensorを使うのが一般的なようで。

LongTensor
整数を扱う場合はlong型を使います(int型も別途ありますが、ニューラルネットのラベルとして受け付けてくれませんので、よほど使う機会はないです)。
https://www.hellocybernetics.tech/entry/2017/10/19/070522

値の内容の確認ができたので、ループで回すことを想定して、こちらも関数化しておきます。

def get_episode_filtered_results(episode_list):
    """
    バッチ単位の処理で生成されたEpisodeのリスト内容を、指定されている
    パーセンタイルのしきい値を参照してフィルタリングし、結果の(優秀な)
    エピソードのObservationやActionのテンソル、Rewardの平均値などを
    取得する。

    Parameters
    ----------
    episode_list : list of Episode
        対象のバッチ単位でのEpisodeを格納したリスト。

    Returns
    -------
    train_obs_v : FloatTensor
        しきい値によるフィルタリング後の残ったEpisodeの、Observationの
        (4,)のサイズのデータを各エピソードのAction数分だけ格納した
        テンソル(M, 4)のサイズで設定される。(Mは残ったEpisodeの
        Action数に依存する)。学習用に参照される。
    train_act_v : LongTensor
        しきい値によるフィルタリング後の残ったEpisodeの、各Actionの
        値を格納したテンソル(M,)のサイズで設定される。(Mは残った
        EpisodeのAction数に依存し、train_obs_vのサイズと一致した
        値が設定される)。学習用に参照される。
    reward_bound : int
        フィルタリング処理で参照された、報酬のしきい値の値。
    reward_mean : float
        指定されたバッチでのEpisode全体の、Rewardの平均値。
    """
    reward_list = []
    for episode in episode_list:
        reward_list.append(episode.reward)
    reward_bound = np.percentile(a=reward_list, q=PERCENTILE)
    reward_mean = float(np.mean(reward_list))

    train_obs_list = []
    train_act_list = []
    for episode in episode_list:
        # 各Episodeに対して、パーセンタイルで算出したしきい値未満のものを
        # 対象外とする。
        if episode.reward < reward_bound:
            continue

        for episode_step in episode.episode_step_list:
            train_obs_list.append(episode_step.observation)
            train_act_list.append(episode_step.action)

    train_obs_v = torch.FloatTensor(train_obs_list)
    train_act_v = torch.LongTensor(train_act_list)

    return train_obs_v, train_act_v, reward_bound, reward_mean

バッチ単位のEpisodeのリストを引数に取るようにしたのと、返却値周りを設定した程度で、ほぼ同じ内容です。

最後に、今まで用意したものを使って、バッチ単位でループを回し、以下の対応を行います。

  • 誤差の計算や勾配を動かして学習をさせる。
  • バッチ単位での過程をprintで出力する。
  • printと同じ内容を、TensorBoardに出力する。
iter_no = 0
while True:

    episode_list = iter_batch()
    train_obs_v, train_act_v, reward_bound, reward_mean = \
        get_episode_filtered_results(episode_list=episode_list)
    optimizer.zero_grad()
    network_output_tensor = network(train_obs_v)
    loss_v = loss_func(network_output_tensor, train_act_v)
    loss_v.backward()
    optimizer.step()

    loss = loss_v.item()
    log_str = 'iter_no : %d' % iter_no
    log_str += ', loss : %.3f' % loss
    log_str += ', reward_bound : %.1f' % reward_bound
    log_str += ', reward_mean : %.1f' % reward_mean
    print(log_str)

    writer.add_scalar(
        tag='loss', scalar_value=loss, global_step=iter_no)
    writer.add_scalar(
        tag='reward_bound', scalar_value=reward_bound,
        global_step=iter_no)
    writer.add_scalar(
        tag='reward_mean', scalar_value=reward_mean,
        global_step=iter_no)

    if reward_mean > 199:
        print('Rewardの平均値が目標値を超えたため、学習を停止します。')
        break

    iter_no += 1

writer.close()

個別にコードを見ていきます。

episode_list = iter_batch()

この部分は、用意したiter_batch関数を使って、1バッチ分の処理を行い、生成されたEpisodeのリストを取得しています。

train_obs_v, train_act_v, reward_bound, reward_mean = \
    get_episode_filtered_results(episode_list=episode_list)

この部分は、生成されたEpisodeのリストを参照して、パーセンタイルによるフィルタリングをし、残ったEpisodeによる学習用の値や、全体の報酬の平均値などを取得しています。しきい値(reward_bound)も参考として出力するため取得しています。

    network_output_tensor = network(train_obs_v)
    loss_v = loss_func(network_output_tensor, train_act_v)
    loss_v.backward()
    optimizer.step()

フィルタリング後のObservationの値をネットワークに入れて、誤差と勾配を動かし、学習を進めます。

    loss = loss_v.item()
    log_str = 'iter_no : %d' % iter_no
    log_str += ', loss : %.3f' % loss
    log_str += ', reward_bound : %.1f' % reward_bound
    log_str += ', reward_mean : %.1f' % reward_mean
    print(log_str)

ここは、各情報をprintで出力しているだけです。テンソルになっているところは、item関数などを使ってPythonのfloatのスカラー値を取得したりしています。

    writer.add_scalar(
        tag='loss', scalar_value=loss, global_step=iter_no)
    writer.add_scalar(
        tag='reward_bound', scalar_value=reward_bound,
        global_step=iter_no)
    writer.add_scalar(
        tag='reward_mean', scalar_value=reward_mean,
        global_step=iter_no)

printしたのと同様の内容をTensorBoardに書き込んでいるだけです。

    if reward_mean > 199:
        print('Rewardの平均値が目標値を超えたため、学習を停止します。')
        break

学習を進めて、Rewardの平均値が目標とする一定値を超えた時点で学習を止めています。
たしか、CartPoleが一定以上はプレイできずに終了となってしまうので、無尽蔵に目標値を高くとはできなかったと記憶しています。

学習結果 :

iter_no : 0, loss : 0.696, reward_bound : 17.0, reward_mean : 15.7
iter_no : 1, loss : 0.698, reward_bound : 24.0, reward_mean : 20.1
iter_no : 2, loss : 0.669, reward_bound : 25.0, reward_mean : 21.8
iter_no : 3, loss : 0.668, reward_bound : 38.5, reward_mean : 34.1
iter_no : 4, loss : 0.652, reward_bound : 34.5, reward_mean : 29.5
iter_no : 5, loss : 0.656, reward_bound : 37.0, reward_mean : 36.6
iter_no : 6, loss : 0.633, reward_bound : 41.5, reward_mean : 37.6
iter_no : 7, loss : 0.628, reward_bound : 51.5, reward_mean : 39.5
iter_no : 8, loss : 0.639, reward_bound : 45.5, reward_mean : 42.1
iter_no : 9, loss : 0.630, reward_bound : 76.5, reward_mean : 59.5
iter_no : 10, loss : 0.626, reward_bound : 55.0, reward_mean : 57.5
iter_no : 11, loss : 0.612, reward_bound : 97.5, reward_mean : 77.7
iter_no : 12, loss : 0.600, reward_bound : 64.0, reward_mean : 58.8
iter_no : 13, loss : 0.597, reward_bound : 82.0, reward_mean : 73.1
iter_no : 14, loss : 0.591, reward_bound : 106.0, reward_mean : 83.0
iter_no : 15, loss : 0.590, reward_bound : 91.0, reward_mean : 82.4
iter_no : 16, loss : 0.583, reward_bound : 105.5, reward_mean : 95.7
iter_no : 17, loss : 0.591, reward_bound : 101.0, reward_mean : 91.0
iter_no : 18, loss : 0.569, reward_bound : 141.0, reward_mean : 129.6
iter_no : 19, loss : 0.576, reward_bound : 138.5, reward_mean : 124.6
iter_no : 20, loss : 0.585, reward_bound : 136.5, reward_mean : 120.6
iter_no : 21, loss : 0.557, reward_bound : 149.5, reward_mean : 134.6
iter_no : 22, loss : 0.568, reward_bound : 166.0, reward_mean : 137.3
iter_no : 23, loss : 0.569, reward_bound : 142.5, reward_mean : 130.9
iter_no : 24, loss : 0.567, reward_bound : 187.0, reward_mean : 145.8
iter_no : 25, loss : 0.539, reward_bound : 178.0, reward_mean : 160.3
iter_no : 26, loss : 0.557, reward_bound : 183.0, reward_mean : 156.7
iter_no : 27, loss : 0.556, reward_bound : 199.0, reward_mean : 167.8
iter_no : 28, loss : 0.558, reward_bound : 200.0, reward_mean : 173.9
iter_no : 29, loss : 0.538, reward_bound : 200.0, reward_mean : 157.8
iter_no : 30, loss : 0.531, reward_bound : 200.0, reward_mean : 185.0
iter_no : 31, loss : 0.539, reward_bound : 200.0, reward_mean : 183.2
iter_no : 32, loss : 0.536, reward_bound : 200.0, reward_mean : 174.8
iter_no : 33, loss : 0.527, reward_bound : 200.0, reward_mean : 188.7
iter_no : 34, loss : 0.522, reward_bound : 200.0, reward_mean : 191.4
iter_no : 35, loss : 0.532, reward_bound : 200.0, reward_mean : 182.2
iter_no : 36, loss : 0.517, reward_bound : 200.0, reward_mean : 187.6
iter_no : 37, loss : 0.513, reward_bound : 200.0, reward_mean : 190.6
iter_no : 38, loss : 0.507, reward_bound : 200.0, reward_mean : 193.1
iter_no : 39, loss : 0.522, reward_bound : 200.0, reward_mean : 199.7
Rewardの平均値が目標値を超えたため、学習を停止します。

順調に、各イテレーションごとに学習していっているのが分かります。

TensorBoardの方も見てみましょう。

20181216_1.png

20181216_2.png

20181216_3.png

問題無くTensorBoard側にも反映されているようです。

参考書籍

Deep Reinforcement Learning Hands-On

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13