PyTorchと強化学習の勉強を始めたので、復習がてら色々進めます。
以下のトピックに対して進めます。
- 強化学習関係の用語の復習。
- OpenAIのGymライブラリ(今回はCartPoleという、レトロゲーム)の復習。
- PyTorchの基本の復習。
クラウドカーネル上での環境の準備
Kaggle Kernelで進めていきます。Kaggle同様に、GoogleのサービスであるColaboratoryでも似たような形で進められると思います。
GymとPyTorchは最初からインストールされているため、インストールされている以下のバージョンをそのまま利用します。
import gym
gym.__version__
'0.10.8'
import torch
torch.__version__
'0.4.1.post2'
※PyTorchの0.3.x系だと、結構違うようなので注意してください。
もし新たにインストールが必要な環境の場合は、各ライブラリのリポジトリのpipなどの記述に合わせてご対応ください。
Gymって何?CartPoleって何?
Githubには、
OpenAI Gym is a toolkit for developing and comparing reinforcement learning algorithms.
と書かれています。
ディープラーニングでは、MNISTやらCIFAR10やら、入門時にさくっと使えるシンプルなデータセットが色々ありますが、強化学習でもまずはシンプル目なゲームを色々扱えると、準備やらで捗ります。
そういったレトロゲームなどを、Python上で簡単に扱えて、学習用に使用できるようにしたのがGymライブラリです。PyTorchなどのPython環境と共に色々使えます。
CartPoleは、Gymの中でも特にシンプルなレトロゲームで、ゲームの強化学習の方面での、MNISTのような存在と言えます。(そういった分野でのHelloWorld的なものになります)
Githubに、ゲームの概要が書かれています : CartPole v0
棒(Pole)が一本あり、その下に土台(Cart)があります。
土台を左右に動かしたり、動かすスピードを調整して、棒が倒れないようにするという、至極シンプルなゲームです。
動画で見るとイメージが付きやすいと思われます。https://www.youtube.com/watch?v=5SEEwqRH8_c
人間であれば、物理などの感覚が身についているため、棒が左に傾いていれば左に土台を動かすべき、またその逆もしかり、と自然に分かりますが、AIは最初は分かりません。
これを学習させるのがゴールです。
Environment、Observation、Agent、Actions、Reward、Episode
CartPoleの資料に、Environment、Observation、Actions、Rewardという単語がでてきます。また、それらに加えてAgentという単語も出てきます。
英単語そのままですが、強化学習方面の単語で、以下のように使われます。(GymのAPIなどもこれらに準じたラベルになっています。)
Environment
学習環境です。今回はCartPoleとなります。どんな要素が観測できるのか、どんな行動の選択肢があるのか、どうすると高い成果となるのかの各要素を含みます。
Gymでは、make関数を使うことで、各ゲームのEnvironmentを作ることができます。
import gym
env = gym.make('CartPole-v0')
第一引数に、対象のゲームのラベルを指定します。v0という表記は、ゲームのバージョンで、ものによってはこれがv1などの他の値が必要になるケースがあります。
Observation
Environment上での観測できる内容です。
CartPoleではとてもシンプルで、結果に影響する、観測される値は土台の位置、土台の速度、棒の角度、棒の頂点の速度の4つだけです。
Gymでは以下のように、観測値の件数などの情報を得ることができます。
env.observation_space
Box(4,)
Boxというクラスは一旦置いておいて、4つの値が観測できる、ということが分かります。
また、観測値の最小値と最大値は以下のように確認することができます。
env.observation_space.low
array([-4.8000002e+00, -3.4028235e+38, -4.1887903e-01, -3.4028235e+38],
dtype=float32)
env.observation_space.high
array([4.8000002e+00, 3.4028235e+38, 4.1887903e-01, 3.4028235e+38],
dtype=float32)
若干、土台の最小値と最大値の値が、ドキュメントの2倍の値になっていますが、恐らくライブラリの値の方が正しいのでしょう・・。
Agent
環境に対して行動する要素です。ゲームのキャラクターとか、将棋を指す人、といったように考えるといいかもしれません。
今回のCartPoleでは、棒の下の土台部分が該当します。
Actions
Agentが取れる行動の選択肢です。
ゲームによっては現在の状態によって取れるActionsが変動するものが多いと思いますが、CartPoleは変わらずに土台を左に動かすか右に動かすかの2つだけです。
Gymでは、以下のようにいくつのActionsがあるのかを確認できます。
env.action_space
Discrete(2)
不連続(Discrete)の2つの選択肢がある、ということが分かります。
Discrete以外にも、連続した選択肢を取るケースがあり、そちらは今回は触れません。(たとえば、ハンドルを右に回して移動するような、複雑なアクションが該当し、逆に単純に回れ右する、といったシンプルなアクションが不連続なものに該当します。)
Reward
Agentが取ったActionに応じて得られる報酬です。
ゲームによってはある程度のアクションの長さを加味することがRewardの獲得までに必要だったりしますが、CartPoleでは毎回のアクションごとにRewardが獲得できます。
取ったアクションで棒が倒れなければRewardが+1されます。
この値の合計をなるべく高くする(=なるべく長い時間棒を倒れないままにする)ことが目標です。
Episode
Actionの流れの固まりがEpisodeと呼ばれます。
チェスなどであれば、ゲームが終わるまでの流れと考えると分かりやすいかもしれません。(もしくは、何らか報酬を得られるまでの範囲での固まり)
CartPoleでは、スタートしてから棒が倒れるか、クリアするまでが1Episodeとなります。
終了になる条件
いくつかCartPoleに、終了(その時点でのRewardの合計が結果となる)条件が設けてあります。
- 棒の角度がプラマイ12度以上傾いた場合。
- 土台の位置が画面の端を超えて移動してまった場合。
- Episodeの件数が200を超えた場合。
ゲームクリア条件
100回の連続したテストで、Rewardの合計の平均が195を超えたらクリアだそうです。
Actionを取る
把握しやすいように、まずは手動でActionを取ってみましょう。
まずはEnvironmentに対してreset関数を実行します。
これをやると、Environmentの状態がリセットされています。
CartPoleであれば、スタート時の棒があまり傾いていない状態になります。(わずかには傾いている)
返却値にObservationの配列が返されます。
env.reset()
array([-0.01689238, 0.02276794, 0.01806427, -0.04499923])
2のインデックスの0.01806427という値が棒の傾き(角度)です。
わずかに右に傾いているようです。
Actionの選択にはstep関数を使います。
obs, reward, done, _ = env.step(action=1)
Actionの0が左に、1が右に土台を移動させます。
1つ目の返却値にAction後のObservation、2つ目に獲得したReward、3っつ目にEpisodeが終わったかどうか(今回は棒が一定以上倒れたりしたかどうか)、4つ目に追加情報が返却されます。4つ目は今回は利用しません。
obs
array([-0.01643702, 0.21762626, 0.01716429, -0.33192842])
右に傾いている状態で土台を右に移動させるActionを取ったため、インデックス1の棒の速度が0.02276794から0.21762626に減少、インデックス2の棒の角度が0.01806427から0.01716429と0に近づいています(棒が垂直に近くなった)。
reward
1.0
Rewardは、棒が倒れていないので1を獲得できました。
done
False
Episodeが終わったかどうかの値も、まだ棒が倒れたりしていないのでFalseが返ります。
一度、ずっと右のActionを取り続けて、棒が倒れるまでループしてみましょう。
env.reset()
total_reward = 0
step_num = 1
while True:
obs, reward, done, _ = env.step(action=1)
total_reward += reward
print('step', step_num, obs)
step_num += 1
if done:
break
print('total_reward', total_reward)
step 1 [ 0.01929589 0.16704773 -0.03490598 -0.31881345]
step 2 [ 0.02263685 0.36264899 -0.04128225 -0.62229688]
step 3 [ 0.02988983 0.55832234 -0.05372818 -0.92769036]
step 4 [ 0.04105628 0.75412694 -0.07228199 -1.23676193]
step 5 [ 0.05613881 0.95009935 -0.09701723 -1.55118537]
step 6 [ 0.0751408 1.14624191 -0.12804094 -1.87249401]
step 7 [ 0.09806564 1.3425092 -0.16549082 -2.20202622]
step 8 [ 0.12491582 1.53879236 -0.20953134 -2.54086094]
total_reward 8.0
8回Actionを取った時点での棒の傾きで、ゲームが終了となったようです。
大まかなAction周りの操作はこんな感じです。
初期化 → Action → Action → ... → Action → Episode終了、以降初期化して次の試行、といった具合てす。
このイテレーションで、学習を行ったりしてRewardの値が最大になるようにします。
ディスプレイの設定は?
ゲーム画面の動画のキャプチャなど、デスクトップのUbuntuマシンで以前試したときはスムーズにいったのですが、今回はクラウドのカーネルであり、且つノートの起動時のコマンド制御などもできず、なんとかならないか色々検証していたもののいまいちうまくいかないので一旦スキップします・・
(Colaboratoryで「こうすると動くよ」的な記事も、古くなっているようで動かず・・)
まあ、今のところはシンプルなゲームなのでディスプレイ設定がなくても何とかなる・・と判断します。
PyTorchの基本
※複雑なコードは今回は扱いませんが、一応Kaggle Kernelをpublic設定にしておきました。必要に応じてforkしたりしてご利用ください。
Kaggle Kernl : 強化学習入門#1 基本的な用語とGym、PyTorch入門
Gymの操作がある程度分かりましたので、PyTorch側の基本に移ります。
GymでのActionやEpisodeのイテレーション中にPyTorchでの学習を挟んで、次のActionやEpisodeに繋げていくためです。
テンソル操作の基本
基本的なAPIは、NumPyやTensorFlowなどに結構似ている感じです。
例えば、テンソルを作るようなケース。
import torch
tensor = torch.FloatTensor([[1, 2, 3], [4, 5, 6]])
tensor
tensor([[1., 2., 3.],
[4., 5., 6.]])
NumPyでnp.array()的にテンソルを作るときの感覚です。
また、NumPy配列を放り込むこともできます。
import numpy as np
arr = np.array([[1, 2, 3], [4, 5, 6]])
tensor = torch.tensor(arr)
tensor
tensor([[1, 2, 3],
[4, 5, 6]])
データの型もNumPyなどと同じような感覚です。ただし、torchパッケージのものを指定する必要があります。np.float32的な指定をしないように注意します。
tensor = torch.tensor(arr, dtype=torch.float32)
tensor
tensor([[1., 2., 3.],
[4., 5., 6.]])
npのものを指定すると怒られます。
tensor = torch.tensor(arr, dtype=np.float32)
TypeError: tensor(): argument 'dtype' must be torch.dtype, not type
要約統計量などを出して、テンソルからPythonの値に変換したいときには、item()関数を使うようです。sum()関数などを実行した後の時点の変数では、まだテンソルのままになっています。
summed_tensor = tensor.sum()
summed_tensor
tensor(21.)
summed_tensor.item()
21.0
ちなみに、単一の値になっているテンソル以外でitem関数を実行しても、リストなどになったりはせず、弾かれるようです。
tensor.item()
ValueError: only one element tensors can be converted to Python scalars
テンソルを通常のCPUメモリに置くか、GPUメモリに置くかの指定は色々方法があるようですが、to関数などがシンプルな印象です。
'cpu'と指定するとCPUに、'cuda'といった指定をするとGPUメモリに渡されます。
今どのメモリ領域に渡されているかは、テンソルオブジェクトのdeviceプロパティや出力した際などに確認することができます。
tensor.device
device(type='cpu')
tensor_gpu = tensor.to('cuda')
tensor_gpu
tensor([[1., 2., 3.],
[4., 5., 6.]], device='cuda:0')
今のところ、会社のマシンもプライベートで触るColaboratoryやKaggle KernelなどもGPU一枚なので、cudaという指定で対応できますが、Titan複数枚差しみたいな環境な場合には、cuda:0 みたいな、コロンと番号(0~)で指定すると、何枚目のGPUを対象とするのかといったことを指定することができます。(いつか、4枚差しとかを仕事で使ってみたい・・)
tensor_gpu = tensor.to('cuda:0')
上記のように、to関数などで変更できるため、TensorFlow的にライブラリがtensorflowとtensorflow-gpuといった具合に分かれておらず、PyTorch単体のライブラリで制御されます。
環境によって、ライブラリが分かれたりせず、ノート内の定数やコマンドラインの引数でプログラム内で分岐できるので、これはこれでシンプルでいいね・・という印象です。
テンソルの関数は、アンダースコアの有無でinplaceかどうかの設定になる
NumPyやPandasではinplaceやcopyといった引数の真偽値で、inplace(コピーを作らず、直接値を編集するかどうか)の設定を扱っていましたが、PyTorchでは関数でアンダースコア付きかどうかで、inplaceで計算を行うかどうかが判定されます。exp関数とexp_関数といった具合に、それぞれ用意されています。
tensor.exp()
tensor([[ 2.7183, 7.3891, 20.0855],
[ 54.5981, 148.4132, 403.4288]])
tensor
tensor([[1., 2., 3.],
[4., 5., 6.]])
上記のように、アンダースコア無しの関数では元のテンソルに対して更新はされません。コピーが返されます。
tensor.exp_()
tensor
tensor([[ 2.7183, 7.3891, 20.0855],
[ 54.5981, 148.4132, 403.4288]])
上記のように、アンダースコア付きの関数を使用すると、元のテンソル自体に更新がされます。
勾配の計算は?
基本的には、気にしなくてもPyTorch側でよしなに対応してくれるようです。
ただし、手動で調整したい場合には色々できるようになっています。
サンプルとして、2つのベクトルを用意して、要素ごとの加算をして、そのあとさらに合計値の単一のスカラ値を求める計算グラフを考えてみます。
\begin{array}{ll}
\textrm{Vector 1 →} &\\
& \textrm{Added vector → Summed scalar}\\
\textrm{Vector 2 →} &
\end{array}
Vector 1だけ勾配を求める形に設定したとします。その設定は、requires_gradプロパティにTrueを指定することでできます。
vector_1 = torch.FloatTensor([1, 2])
vector_1.requires_grad = True
vector_2 = torch.FloatTensor([3, 4])
added_vector = vector_1 + vector_2
summed_scalar = added_vector.sum()
vector_1に対して、勾配を求めるように指定したため、それに紐づくadded_vectorやsummed_vectorなどのテンソルの変数も、自動的にrequires_gradがTrueになります。
print('vector_1.requires_grad', vector_1.requires_grad)
print('vector_2.requires_grad', vector_2.requires_grad)
print('added_vector.requires_grad', added_vector.requires_grad)
print('summed_scalar.requires_grad', summed_scalar.requires_grad)
vector_1.requires_grad True
vector_2.requires_grad False
added_vector.requires_grad True
summed_scalar.requires_grad True
逆方向にvector_1の個所にまで戻っていく際に、vector_2は経由しないのでFalseのままです。
backward関数で勾配を計算してくれます。
また、各テンソルで、gradプロパティにアクセスすると値に参照することができます。
summed_scalar.backward()
vector_1.grad
tensor([1., 1.])
vector_2側では勾配を求めない設定なので、以下のようにアクセスしてもなにも値が返ってきません。
vector_2.grad
なお、余談ですが、勾配を求める設定にした上で、過程のテンソルにアクセスすると、勾配を求めるための関数(grad_fn)が自動で設定されていることが分かります。
added_vector
tensor([4., 6.], grad_fn=<ThAddBackward>)
summed_scalar
tensor(10., grad_fn=<SumBackward0>)
Summary
一つ目の記事はまだ本格的な強化学習、というモードではありませんが、どんな用語があるのかであったり、GymやPyTorchなどの基本を復習しました。
次回の記事では恐らく、PyTorchでネットワークを組んでみたり、TensorBoardXなどのツールにも触れられたら・・とか考えています。