かざんぶろぐ

だらだらと備忘録とってます

強化学習,DQNについて自分なりにまとめてみた

強化学習に関連するキーワードや手法についてのメモ。
最終的にはDeep Q-Learning、いわゆるDQNにも触れていこうと思う。

参考にさせていただいた記事、書籍

DQNの生い立ち + Deep Q-NetworkをChainerで書いた - Qiita
ゼロからDeepまで学ぶ強化学習 - Qiita
これからの強化学習 | 森北出版株式会社
正直参考記事、書籍を読めばこの記事はいらないくらいめちゃくちゃ詳しく書いてあるので、ぜひ参考にしてほしい。

強化学習

強化学習とは何かを簡潔にまとめると、「環境に置かれたエージェントが、環境との相互作用を通して、環境に応じた最適な方策を学習する手法」である。

エージェントが環境に行う作用を行動(action)といい、エージェントの行動によって変化する環境の要素を状態(state)という。

エージェントは環境から状態を受け取り、行動を決定し、環境がその行動に応じて変化。
さらに新しい状態をエージェントが受け取る。
このような環境とエージェントの相互作用を繰り返していく。

f:id:okuya-KAZAN:20171019134826p:plain


強化学習は他の機械学習と異なり、教師による答えがない。
その代わり、行動に対する「報酬」というものが存在する。
例えば、ある状態Sで行動A1をとったら報酬+1pt、行動A2をとったら報酬-1ptといった具合だ。

強化学習のキーワード

強化学習を勉強する上で欠かせないキーワードをまとめておく。

エージェント

行動する主体。環境から得られた観測結果をもとに行動を決定する。

環境

エージェントが決定した行動を受けて、次なる状態と報酬を決定し、エージェントに引き渡す。
環境の挙動は設計者の設計対象外。
ただし、報酬に関しては問題設定の一部として設計者が与える必要がある。

時間ステップとエピソード

時間ステップ

エージェントと環境の相互作用における基本的時間単位。1時間ステップの間に、エージェントは環境から状態や報酬を受け取り、それをもとに行動を決定して環境に引き渡す。

エピソード

タスクの開始から終了までの時間。
囲碁であれば碁盤に石がない状態から、試合が終了するまでがエピソードである。

マルコフ性

t+1ステップ目における状態は、tステップ目での状態と行動を使って、
P\bigl(S_{t+1}\mid S_t,A_t\bigr)で定められる。この時S_{t+1}は、S_{t-1}A_{t-1}などには依存せず、S_{t}A_{t}のみに依存して定まる。このように直前の状態のみで遷移確率が決まる性質をマルコフ性と呼ぶ。

方策

状態を入力として、行動を出力する行動決定のための関数。
方策は\piで表し、方策\piのもとである状態sにおけるある行動aが選択される確率を\pi\bigl(a\mid s\bigr)と表す。

最終目標は、エージェントが置かれた状態において、できるだけ多くの報酬得られるよう方策を設計していくこと。
もっと詳しく言うと、環境から得られる「報酬」「状態」から方策を改善していくこと(DQNでは方策の改善 -> NNのパラメータのチューニング)。

報酬、収益

ここでいう報酬というのは、ある行動を取った直後に得られる報酬(即時報酬)のことであるが、即時報酬の大きさだけを行動の指標にしてしまうと局所解に落ち込んでしまう危険性がある。

例えば、無人島に漂着した場合、周囲を探索するより、じっと座り込んでいる方が、体力の消耗が少ないので即時報酬は高い。
しかし、体力を使うという負の即時報酬を得ながらも探索を続けることで、食料を見つけ、結果的にじっと座ってるよりも大きな報酬を得られる可能性がある。

つまり、即時的には小さな報酬しか得られない行動でも、後にとても大きな報酬を得ることに繋がれば、その行動は全体としてみれば良い行動とみなせる。

よって、即時報酬だけでなく、その後に得られる全ての報酬の累積である収益を考える必要があり、強化学習においてエージェントは、「行動の選択を通して得られる収益の総和の最大化」を目的として学習していく。

収益にはそのまま報酬を足し合わせて定義されるものも存在するが、主には、未来にいくに従って割引されて定義されていく割引報酬和で収益が多く採用されているらしい。
割引報酬和とは、報酬を足し合わせる際に、未来の報酬ほど減衰させることで未来の不確実性を表現したものであり、時間ステップtでの収益G_tを、

G_t = \sum_{\tau=0}^{\infty} \gamma^{\tau}R_{t+1+\tau}=R_{t+1}+\gamma R_{t+2}+\gamma^{2}R_{t+3}+\cdots
のように定義する。
ここで\gamma\left(0\le\gamma\leq1\right)は割引率と呼ばれ、どれだけ未来を割引くかを表す定数である。

価値,行動価値関数

エージェントと環境との相互作用の中身は確率的に決定されてしまうため、収益も確率的に変動してしまう値となってしまう。
そのため収益は、方策を設計する際の行動の評価指標としては扱いづらい。

そこで、ある状態sから方策に従って行動を決定していった時に得られる収益の期待値を求め、それを価値と呼び、良い方策を設計する際の指標とする。

期待値をとることで、確率的に変動する報酬の平均的な値を算出することができる。
価値の中でも、ある状態sで行動aを選択した場合の収益の期待値を状態sにおける、行動aの行動価値と呼び、さらに行動価値を求める関数を行動価値関数をとし、

Q^\pi \left(s,a\right)
のように定義する。

行動価値を求める手法

ベルマン方程式

実際に行動価値関数を推定していくためのアルゴリズムの1つである、ベルマン最適方程式について述べていく。

この方程式では、未来の収益の期待値である行動価値関数を算出するために、異なる時刻における行動価値関数の間に成り立つ再帰的な関係を利用している。

ある方策\piの元での行動価値関数Q^\pi \left(s,a\right)は、ある状態sで行動aを選択した場合の収益の期待値であるので、

Q^\pi \left(s,a\right)=\mathbb{E}^\pi\left[G_t|S_t=s,A_t=a \right]
と表せる。
ここで収益は、

G_t = \sum_{\tau=0}^{\infty} \gamma^{\tau}R_{t+1+\tau}=R_{t+1}+\gamma R_{t+2}+\gamma^{2}R_{t+3}+\cdots
と表せるので、

\begin{eqnarray}
Q^\pi \left(s,a\right)&=&\mathbb{E}^\pi\left[\sum_{\tau=0}^{\infty}\gamma^{\tau}R_{t+1+\tau}\mid S_t=s,A_t=a \right] \\
&=&\mathbb{E}^\pi\left[R_{t+1}+\gamma R_{t+2}+\gamma^{2}R_{t+3}+\cdots\mid S_t=s,A_t=a \right]
\end{eqnarray}
となる。

\mathbb{E}^\pi\left[\cdot\right]は線形な演算なので、

Q^\pi \left(s,a\right)=\mathbb{E}^\pi\left[R_{t+1}\mid S_t=s,A_t=a \right]+\gamma\mathbb{E}^\pi\left[R_{t+2}+\gamma R_{t+3}+\cdots\mid S_t=s,A_t=a \right]
とすることができる。

右辺の第一項については、

\mathbb{E}^\pi\left[R_{t+1}\mid S_t=s,A_t=a \right]=\sum_{s'}P\left(s'\mid s,a\right)r\left(s,a,s'\right)
となる。
P\left(s'\mid s,a\right)は状態sで行動aを選択した際に状態s’に遷移する確率を表す状態遷移確率であり、r\left(s,a,s'\right)はその時に得られる報酬である。

続いて第二項は、

\begin{eqnarray}
&\gamma&\mathbb{E}^\pi\left[R_{t+2}+\gamma R_{t+3}+\cdots\mid S_t=s,A_t=a \right]\\
=&\gamma&\sum_{s'}P\left(s'\mid s,a\right)\sum_{a'}\pi\left(a'\mid s'\right)\mathbb{E}^\pi\left[R_{t+2}+\gamma R_{t+3}+\cdots\mid S_{t+1}=s',A_{t+1}=a'\right]
\end{eqnarray}
となる。
このうち\mathbb{E}^\pi\left[R_{t+2}+\gamma R_{t+3}+\cdots\mid S_{t+1}=s',A_{t+1}=a'\right]の部分はQ^\pi\left(s',a'\right)とみることができるので、
第二項については、

\gamma\sum_{s'}P\left(s'\mid s,a\right)\sum_{a'}\pi\left(a'\mid s'\right)Q^\pi\left(s',a'\right)
となる。

これらをまとめると、

Q^\pi\left(s,a\right)=\sum_{s'}P\left(s'\mid s,a\right)\Bigl(r\bigl(s,a,s'\bigr)+\gamma\sum_{a'}\pi\bigl(a'\mid s'\bigr)Q^\pi\bigl(s',a'\bigr)\Bigr)
を得る。この方程式をベルマン方程式という。

ここで\sum_{a'}\pi\bigl(a'\mid s'\bigr)Q^\pi\bigl(s',a'\bigr)を見ると、状態s’にて、選択できる全ての行動a'から期待値を求めていることがわかる。

ベルマン最適方程式

状態s’での行動を方策で選択する代わりに、価値関数の値が最大となる行動a’を選択した場合の期待値を求めると、ベルマン方程式は、

Q\left(s,a\right)=\sum_{s'}P\left(s'\mid s,a\right)\Bigl(r\bigl(s,a,s'\bigr)+\gamma\max_{a'}Q^\pi\bigl(s',a'\bigr)\Bigr)
となる。この方程式をベルマン最適方程式という。

この方程式にはベルマン方程式とは違い、計算に直接的に行動選択確率が含まれていない。
実際にこのような問題を解く際には、全ての状態に対する行動価値関数の表を用意し、表の全ての要素をベルマン最適方程式で計算していく。

Q-Learning

ベルマン最適方程式を使って最適行動価値関数を求める方法は、状態遷移確率が既知であれば最も効率良く最適価値関数を計算することができる。
しかし、強化学習において、エージェントは環境に関して事前の知識を持っていない。
よって、不完全な知識の上で、知識を収集しながら最適な行動を学習していかなくてはならない。
Q-Learningとは、このような不完全な知識の中での探索結果から行動価値関数を近似し、最適方策を学習していく学習手法である。

例えば状態sで行動aを選択して状態s'に遷移した場合、
遷移時に得た即時報酬と、状態s'からずっと最適な行動を選び続けた時に得ることのできる収益の期待値の和を教師(target)として、Q\bigl(s,a\bigl)とtargetの誤差から新たにQ\bigl(s,a\bigl)を更新していく。
ちなみにこのQ\bigl(s,a\bigl)とtargetの誤差をTD誤差という。



\begin{align*}
&target=r\bigl(s,a,s'\bigr)+\gamma\max_{a'}Q^\pi\bigl(s',a'\bigr)\\
&loss=target-Q\bigl(s,a\bigr)\\
&Q\bigl(s,a\bigr)\leftarrow Q\bigl(s,a\bigr)+\alpha\times loss
\end{align*}


行動aによってエピソードが終了した場合、
targetはただ単にr\bigl(s,a,s'\bigr)となる。

エージェントが行動を選択する方法(方策)

先ほどのQ-Learningの説明で、「状態sで行動aを選択」と書いたが、
実際にどのように行動を選択していくか(方策)について述べる。

greedy

まず、これまでの相互作用の結果から、最も価値の高い行動を選択していくという方策がある。これをgreedy方策という。

ε-greedy

greedy方策は際的行動価値観数が既知であれば最適な方策となる。しかし、現実には最適行動価値観数は始めからわかっておらず、最適な行動価値を探索して推定していく必要がある。
その際に、これまでで得られた最も価値の高い行動を選択するだけでは、他の選択肢がどのくらい良い結果をもたらすか知ることができない。

そこで、今までの学習結果を利用するだけでなく、たまにランダムに探索する必要がある。
このように利用と探索を織り交ぜていく手法としてε-greedy方策が存在する。
確率εでランダムに行動し、確率1-εでgreedy方策をとる。

行動価値関数の近似

状態数が少なければ、テーブル関数Q\bigl(s,a\bigr)を作ることができるが、
例えばカメラの画像などを状態として扱うと、状態数が莫大になり、テーブル関数では表現できなくなってしまう。
そこでQ関数をパラメータ\thetaで近似する。

DQN(Deep Q-Network)

以上の手法にDeep Learningの技術を適用したもの。
関数近似ニューラルネットワークを用いて、Q関数を学習させていく。
ひと昔前までの関数近似は人手で構築された特徴を用いるて性能を向上させてきたが、Deep Q-Network(以下DQN)は入力として画面のデータなどの状態のみを用いて、同じ学習アルゴリズムを適用するだけで、囲碁やチェス、昔の家庭用ゲームなど様々なゲームにおいて人間以上の強さを発揮できるようになったことで注目を集めた。

DQNのネットワーク構造

DQNは行動価値関数を推定するための多層ニューラルネットワークである。
エージェントの視界や画面データなど画素情報を入力とするため、ただの多層ニューラルネットワークでなく畳み込みニューラルネットワークが利用されることが多い。
畳み込みニューラルネットワークの詳しい説明は今回省略する。

DQNの出力は行動価値関数Q\bigl(s,a\bigl)である。
つまり行動aの取りうる種類がN通りの場合、DQNはN通りの出力をもつニューラルネットワークとなる。

DQNの学習におけるテクニック

DQNディープラーニングを用いてネットワークのパラメータを更新していくのだが、上手く学習されるよう下記の工夫がなされている。

  • ターゲットの固定化(neural fitted Q)
  • 学習に用いるサンプルの偏りの抑制(Experience Replay)
neural fitted Q

先ほどQ-Learningの説明のところでも述べたが、
DQNのパラメータを更新してくには、TD誤差r_{t+1}+\gamma\max_{a_{t+1}}Q\bigl(s_{t+1},a_{t+1}:\theta_t\bigr)-Q\bigl(s_t,a_t:\theta_t\bigr)を求める必要がある。

第一項のr_{t+1}+\gamma\max_{a_{t+1}}Q\bigl(s_{t+1},a_{t+1}:\theta_t\bigr)ディープラーニングでいうtargetにあたるのだが、
このtargetはパラメータ\thetaに依存するため収束が安定しないとのこと。

そこでtargetにおけるパラメータ\thetaをある時点でのパラメータ\theta^-で固定し、
r_{t+1}+\gamma\max_{a_{t+1}}Q\bigl(s_{t+1},a_{t+1}:\theta^-\bigr)とする。
そしてある程度学習を繰り返した後に、targetとなるQ関数のパラメータ\theta^-を更新する。

Experience Replay

関数近似に多層ニューラルネットワークなどを用いた場合、
「サンプル取得 -> 学習 -> サンプル取得 -> 学習」という学習法だと、どうしても学習が遅くなってしまう。
そこで、サンプル\bigl(s,a,r,s'\bigr)が無限回得られるなら、それらをどのような順番で学習しても最適な価値関数が得られるという特徴を生かして、経験したサンプルを全てメモリーとして記録しておき、学習を行う際はそこからランダムサンプリングして利用するExperience Replayという手法を使う。
この手法は、時系列的に連続であるサンプル間の相関をバラすというメリットもある。

もっと噛み砕いて書くと、
「ある状態でどう行動をとったら、どんな結果を得て、次にどんな状態になった」という経験をエージェントは蓄積していく。
そしてたまった経験から「あの時、右に曲がればぶつからずに済んだんだな」といった風に、ディープラーニングで最適な行動を学習し、ニューラルネットワークをチューニングしていく。

f:id:okuya-KAZAN:20171019135101p:plain


まとめ

強化学習のキーワードをまとめ、最適な行動価値関数を求める手法についてまとめた。
実際にPythonのChainerでDQNの実装もしたので今度改めてブログにまとめようと思う。

Pythonの標準モジュール「argparse」の使い方についてのメモ

コマンドラインからPythonプログラムに引数を渡す際に便利な、標準モジュール「argparse」に関する備忘録。

環境

Mac Book Air : OS X EI Caption(version 10.11.6)
Python 2.7.12 |Anaconda custom (x86_64)|

Pythonプログラムの中身

コマンドラインから引数を受け取るPythonプログラムを以下のような流れで書いていく。

  • argparseモジュールをimport
  • argparseモジュールのArgumentParserクラスを呼び出しパーサーを生成
  • ArgumentParserクラスのadd_argumentメソッドで引数を追加
  • ArgumentParserクラスのparse_argsメソッドで引数の解析

※プログラムは「Parser_practice.py」という名前のファイルで実行していく。

argparseモジュールをimport

これをしないとはじまらないので早速import

import argparse

ArgumentParserクラスを呼び出しパーサーを生成

argparseモジュールのArgumentParserクラスを呼び出す。
このArgumentParserクラスのコンストラクタを使用することで、構文解析の処理を行うパーサーが生成される。

import argparse
parser = argparse.ArgumentParser()

ここから、add_argumentメソッドで引数を追加していくのだが、
実はコンストラクタが呼び出された時点で[-h/--help]という引数がデフォルトで追加されている。
先ほど記述した順番とは異なるが、parse_argsメソッドで引数の解析を行い、実際に[-h]引数を指定してコマンドラインにヘルプを表示させてみる。

import argparse
parser = argparse.ArgumentParser()
args = parser.parse_args()



実行結果
f:id:okuya-KAZAN:20170927155339p:plain

「usage」はプログラムの利用方法であり(後述)、
「optional arguments:」以下には追加されているオプションが表示されるのだが、今表示されているのは、コンストラクタによって追加された[-h],[--help]引数のみである。
さらに、コンストラクタに引数を指定することによって、ヘルプの表示内容を設定することができる。
設定できる引数は以下の通り

  • prog : プログラム名
  • usage : プログラムの利用方法
  • description :「optional arguments」の前に表示される説明文
  • epilog :「optional arguments」後に表示される文字列
  • add_help : [-h],[--help]オプションをデフォルトで追加するか

実際にこれらを指定してみる。

import argparse

parser = argparse.ArgumentParser(
            prog="aaaaaaaaaa", #プログラム名
            usage="bbbbbbbbbb", #プログラムの利用方法
            description="cccccccccc", #「optional arguments」の前に表示される説明文
            epilog = "dddddddddd", #「optional arguments」後に表示される文字列
            add_help = True #[-h],[--help]オプションをデフォルトで追加するか
            )



実行結果
f:id:okuya-KAZAN:20170927155353p:plain

ちなみに、引数のadd_helpをFalseにして実行すると、


f:id:okuya-KAZAN:20170927155409p:plain


[-h]なんて引数はないと怒られエラーとなる。

add_argumentメソッドで引数を追加

続いてArgumentParserクラスのadd_argumentメソッドで引数を追加していく。
こちらのメソッドは、parse_argsメソッドで引数の解析を行う前に使用する。
add_argumentメソッドの引数は以下の通り

  • name or flags : 位置引数もしくはオプション引数の指定
  • help : 引数の説明
  • required : 引数の省略を不可にするか
  • type : 引数が変換される型
  • choises : 引数の選択肢を指定
  • nargs : 引数の数の指定
  • default : 引数がなかった場合に生成される値
  • metavar : メッセージで表示される引数の名前
  • action : 引数の取り扱いの指定

何がなんだかわからなくなりそうなので1個ずつ見ていく。

name or flags

引数が"位置引数"か"オプション引数"かを指定する。

  • 位置引数 : 関数に対して必須となる引数
  • オプション引数 : 与えても与えなくても良く、接頭辞に「-」または「--」を指定する必要がある
位置引数の例

位置引数を受け取るプログラムを以下のように記述する。

import argparse

parser = argparse.ArgumentParser(
            prog="aaaaaaaaaa", #プログラム名
            usage="bbbbbbbbbb", #プログラムの利用方法
            description="cccccccccc", #「optional arguments」の前に表示される説明文
            epilog = "dddddddddd", #「optional arguments」後に表示される文字列
            add_help = True #[-h],[--help]オプションをデフォルトで追加するか
            )
# 引数の追加
parser.add_argument("test", #位置引数
                    help="eeeeeeeeee" #引数の説明)
args = parser.parse_args() # 引数の解析
print args.test

そして、位置引数「test」に値を入れて出力させてみる。


実行結果
f:id:okuya-KAZAN:20170927155420p:plain


[-h]引数でヘルプを表示すると[positional arguments:]以下に位置引数「test」追加されていることがわかる。さらにhelpで指定した内容も表示されている。
最後の実行結果は位置引数を指定しなかったためにエラーとなる。

オプションの例

オプション引数を受け取るプログラムを以下のように記述する。

import argparse

parser = argparse.ArgumentParser(
            prog="aaaaaaaaaa", #プログラム名
            usage="bbbbbbbbbb", #プログラムの利用方法
            description="cccccccccc", #「optional arguments」の前に表示される説明文
            epilog = "dddddddddd", #「optional arguments」後に表示される文字列
            add_help = True #[-h],[--help]オプションをデフォルトで追加するか
            )
# 引数の追加
parser.add_argument("-t","--test", #オプション引数
                    help="eeeeeeeeee" #引数の説明
                    )
args = parser.parse_args() # 引数の解析
print args.test



実行結果
f:id:okuya-KAZAN:20170927155428p:plain


[-h]引数でヘルプを表示すると[optional arguments:]以下に位置引数「test」追加されていることがわかる。
最後の実行結果はオプションtestには値が入っていないため「None」となっている。

required

オプション引数も位置引数のように、省略を不可にすることができる。
それにはadd_argumentメソッドの引数であるrequiredをTrueにする必要がある。

import argparse

parser = argparse.ArgumentParser(
            prog="aaaaaaaaaa", #プログラム名
            usage="bbbbbbbbbb", #プログラムの利用方法
            description="cccccccccc", #「optional arguments」の前に表示される説明文
            epilog = "dddddddddd", #「optional arguments」後に表示される文字列
            add_help = True #[-h],[--help]オプションをデフォルトで追加するか
            )
# 引数の追加
parser.add_argument("-t","--test", #オプション引数
                    help="eeeeeeeeee", #引数の説明
                    required = True #引数の省略を不可にするか
                    )
args = parser.parse_args() # 引数の解析
print args.test



実行結果
f:id:okuya-KAZAN:20170927155440p:plain


オプション引数「-t」を指定しないと「error: argument -t/--test is required」というエラーが出る。

type

引数は何も指定しないとstr型として扱われるが、add_argumentメソッドの引数であるtypeで引数の型を指定できる。
異なる型の引数を指定するとエラーが出る。

import argparse

parser = argparse.ArgumentParser(
            prog="aaaaaaaaaa", #プログラム名
            usage="bbbbbbbbbb", #プログラムの利用方法
            description="cccccccccc", #「optional arguments」の前に表示される説明文
            epilog = "dddddddddd", #「optional arguments」後に表示される文字列
            add_help = True #[-h],[--help]オプションをデフォルトで追加するか
            )
# 引数の追加
parser.add_argument("-t","--test", #オプション引数
                    help = "eeeeeeeeee",#引数の説明
                    required = True ,#引数の省略を不可にするか
                    type = int
                    )
args = parser.parse_args() # 引数の解析
print args.test



実行結果
f:id:okuya-KAZAN:20170927161718p:plain


choices

引数に指定する値を限定したい場合に使う、こちらの引数で指定した値以外を指定するとエラーとなる。

import argparse

parser = argparse.ArgumentParser(
            prog="aaaaaaaaaa", #プログラム名
            usage="bbbbbbbbbb", #プログラムの利用方法
            description="cccccccccc", #「optional arguments」の前に表示される説明文
            epilog = "dddddddddd", #「optional arguments」後に表示される文字列
            add_help = True #[-h],[--help]オプションをデフォルトで追加するか
            )
# 引数の追加
parser.add_argument("-t","--test", #オプション引数
                    help = "eeeeeeeeee",#引数の説明
                    required = True ,#引数の省略を不可にするか
                    type = int,
                    choices = [0,1] #このオプション引数の値は0か1にしか指定できない
                    )
args = parser.parse_args() # 引数の解析
print args.test



実行結果
f:id:okuya-KAZAN:20170927155505p:plain

nargs

引数に配列を指定したい場合に用いる。
nargsで指定する値は、配列の大きさ。
コマンドラインで指定した引数の数と、nargsの値が異なるとエラーが出る。

import argparse

parser = argparse.ArgumentParser(
            prog="aaaaaaaaaa", #プログラム名
            usage="bbbbbbbbbb", #プログラムの利用方法
            description="cccccccccc", #「optional arguments」の前に表示される説明文
            epilog = "dddddddddd", #「optional arguments」後に表示される文字列
            add_help = True #[-h],[--help]オプションをデフォルトで追加するか
            )
# 引数の追加
parser.add_argument("-t","--test", #オプション引数
                    help = "eeeeeeeeee",#引数の説明
                    required = True ,#引数の省略を不可にするか
                    type = int,
                    nargs = 4
                    )
args = parser.parse_args() # 引数の解析
print args.test



実行結果
f:id:okuya-KAZAN:20170927155514p:plain

default

コマンドラインで引数を指定しなかった場合に、オプション引数に格納される値を設定できる。

import argparse

parser = argparse.ArgumentParser(
            prog="aaaaaaaaaa", #プログラム名
            usage="bbbbbbbbbb", #プログラムの利用方法
            description="cccccccccc", #「optional arguments」の前に表示される説明文
            epilog = "dddddddddd", #「optional arguments」後に表示される文字列
            add_help = True #[-h],[--help]オプションをデフォルトで追加するか
            )
# 引数の追加
parser.add_argument("-t","--test", #オプション引数
                    help = "eeeeeeeeee",#引数の説明
                    default = 5
                    )
args = parser.parse_args() # 引数の解析
print args.test



実行結果
f:id:okuya-KAZAN:20170927155522p:plain

metavar

ヘルプやエラーの出力時の引数名はmetavarという項目で変更することができる。

import argparse

parser = argparse.ArgumentParser(
            prog="aaaaaaaaaa", #プログラム名
            usage="bbbbbbbbbb", #プログラムの利用方法
            description="cccccccccc", #「optional arguments」の前に表示される説明文
            epilog = "dddddddddd", #「optional arguments」後に表示される文字列
            add_help = True #[-h],[--help]オプションをデフォルトで追加するか
            )
# 引数の追加
parser.add_argument("-t","--test", #オプション引数
                    )
parser.add_argument("-o","--origin" ,#オプション引数
                    metavar = "My Optional argument"
                    )
args = parser.parse_args() # 引数の解析
print args.test



実行結果
f:id:okuya-KAZAN:20170927155530p:plain

action

今までは引数の後に値を指定して引数に格納していたが、
actionという項目を使うことで、引数の後に値を入力せずとも引数の指定のみで値を格納することができる。

まず、actionで指定できるものの一部を見てみる。

  • store : 今まで通り引数の後に指定された値を格納する。
  • store_const : constキーワード引数で指定された値を格納
  • store_true : 引数の指定があればtrueを格納、なければfalseを格納
  • store_false : 引数の指定があればfalseを格納、なければtrueを格納
import argparse

parser = argparse.ArgumentParser(
            prog="aaaaaaaaaa", #プログラム名
            usage="bbbbbbbbbb", #プログラムの利用方法
            description="cccccccccc", #「optional arguments」の前に表示される説明文
            epilog = "dddddddddd", #「optional arguments」後に表示される文字列
            add_help = True, #[-h],[--help]オプションをデフォルトで追加するか
            )



# 引数の追加
parser.add_argument("-a","--aaa", #オプション引数
                    action = "store"
                    )
parser.add_argument("-b","--bbb", #オプション引数
                    action = "store_const",const=42
                    )
parser.add_argument("-c","--ccc", #オプション引数
                    action = "store_true"
                    )
parser.add_argument("-d","--ddd", #オプション引数
                    action = "store_false"
                    )

args = parser.parse_args() # 引数の解析

print args.aaa
print args.bbb
print args.ccc
print args.ddd



実行結果
f:id:okuya-KAZAN:20170927155540p:plain

さらに以下の設定をすればactionで引数の取り扱いの指定もできる。

  • append : オプション引数にリストを格納し、そのリストに引数の値を格納する。
  • count : キーワード引数の数を格納
import argparse

parser = argparse.ArgumentParser(
            prog="aaaaaaaaaa", #プログラム名
            usage="bbbbbbbbbb", #プログラムの利用方法
            description="cccccccccc", #「optional arguments」の前に表示される説明文
            epilog = "dddddddddd", #「optional arguments」後に表示される文字列
            add_help = True, #[-h],[--help]オプションをデフォルトで追加するか
            )



# 引数の追加
parser.add_argument("-a","--aaa", #オプション引数
                    action = "append"
                    )
parser.add_argument("-b","--bbb", #オプション引数
                    action = "count"
                    )

args = parser.parse_args() # 引数の解析

print args.aaa
print args.bbb
print args.ccc
print args.ddd



実行結果
f:id:okuya-KAZAN:20170927155550p:plain

まとめ

Pythonの標準モジュールのargparseについてほんの一部だけまとめた。
さらにリファレンスを読めば深い知識が得られそうだが、今回は疲れてしまったのでここまで...

Unityでオブジェクトをスクリプトで動かし視界に映る画像をファイル保存

Unity上でエージェントをキー入力で動かし、エージェントの視界に映る映像をPNGファイルとして保存できるようにした。

環境

Mac Book Air : OS X EI Caption(version 10.11.6)
Unity : 2017.1.0f3

流れ

  • エージェントと、エージェントを動かす環境を作成
  • エージェントの視界に映った画像をPNGファイルとして保存するスクリプトの作成

シーンの環境構築

早速Unityを起動し、新しくプロジェクトを作成し、シーン上に環境を作っていく。
まずHierarchy上にて[Create Empty]で空オブジェクトを作成し、名前を「Agent」とする。
f:id:okuya-KAZAN:20170830174158p:plain
そして、Hierarchy上に最初からある「Main Camera」を「Agent」の子オブジェクトにし、名前を「Eye Camera」に変更。
f:id:okuya-KAZAN:20170830174214p:plain
続いて、エージェントが移動する地面を構築する。
シーン上にTerrainを配置し、名前を「Ground」に変更し、適当に隆起させたりテクスチャをつけた。
f:id:okuya-KAZAN:20170830174335p:plain
※地面のテクスチャはProjectビュー上のAssetsフォルダ内で右クリック->[Import Package]->[Environment]と進みインポートした。
f:id:okuya-KAZAN:20170830174355p:plain


Agentを動かす

キー入力を受けつけ、上向き矢印キーでエージェントが前に進み、左右の矢印キーで方向転換、スペースキーでエージェントがジャンプするようなスクリプトを作成する。
スクリプトを作成する前にまず、
「Character Controllerコンポーネント」をAgentオブジェクトに追加しておく。
f:id:okuya-KAZAN:20170830174454p:plain


Projectビュー上にて[Create]->[C# Script]で新規スクリプトを作成、名前をPlayerController.csとし、中身は以下のようにした。


PlayerController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour {
  CharacterController controller;
  Vector3 moveDirection;

  public float gravity;
  public float speed;
  public float amountRotate;
  public float speedJump;

  void Start () {
    controller = GetComponent<CharacterController>();
    moveDirection = Vector3.zero;
  }

  void Update () {
    //地上にいる場合のみ操作を行う
    if(controller.isGrounded){
      //Inputの検知->0未満となってバックすることを避ける
      if(Input.GetAxis("Vertical") > 0.0f){
        moveDirection.z = Input.GetAxis("Vertical")*speed;
      }
      else{
        moveDirection.z = 0;
      }

      //方向転換
      transform.Rotate(0,Input.GetAxis("Horizontal")*amountRotate ,0);

      //ジャンプ
      if(Input.GetButton("Jump")){
        moveDirection.y = speedJump;
      }
    }
    //毎フレーム重力を加算
    moveDirection.y -= gravity * Time.deltaTime;

    //移動の実行
    Vector3 globalDirection = transform.TransformDirection(moveDirection);
    controller.Move(globalDirection * Time.deltaTime);

    //移動後地面についてるなら重力をなくす
    if(controller.isGrounded){
      moveDirection.y = 0;
    }
  }
}



スクリプトの詳しい解説は割愛する(疲れてしまったので...)
作成したPlayerController.csをAgentオブジェクトに追加し、Inspectorでpublic変数の値を設定。
f:id:okuya-KAZAN:20170830174552p:plain


プレビューを開始するとAgentを動かすことができた。
f:id:okuya-KAZAN:20170830174611p:plain


エージェントの視界に映った画像をPNGファイルとして保存

というわけでこの記事のメインに入っていく。
流れとしてはこんな感じ。
1.ProjectViewでRenderTextureを作成
2.EyeCameraにRenderTextureを設定
3.RenderTextureをTexture2D変換し、さらにPNG画像として保存するスクリプトを作成

RenderTextureを作成し、TargetTextureに設定

RenderTextureとは、カメラで撮影した映像をTextureとして使える機能である。
まず、Projectビュー上にて、[Create]->[Render Texture]でRenderTextureを作成、名前をMyRenderTextureに変更。
続いて、EyeCameraオブジェクトのInspector上にある[Target Texture]に作成したMyRenderTextureを設定。
これでEyeCameraで描画したものはMyRenderTextureに描画されるようになる。
f:id:okuya-KAZAN:20170830174750p:plain


f:id:okuya-KAZAN:20170830174859p:plain


視界を保存するためのスクリプトの作成

Projectビュー上にて[Create]->[C# Script]で新規スクリプトを作成し、名前をSaveImage.csとした。
実際のスクリプトは以下。


SaveImage.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//File.~を使うため
using System.IO;

public class SaveImage : MonoBehaviour {
  public Camera eyeCamera;
  private Texture2D texture;

  private int photoNumber = 1;


// Use this for initialization
void Start () {
    texture = new Texture2D (eyeCamera.targetTexture.width, eyeCamera.targetTexture.height,
                            TextureFormat.RGB24, false);
                          }
// Update is called once per frame
void Update () {
    //キーボードの「s」を押したら画像を保存
    if(Input.GetKeyDown("s")){
      SaveCameraImage();
    }
  }

  public void SaveCameraImage() {
    // Remember currently active render textureture
    RenderTexture currentRT = RenderTexture.active;
    // Set the supplied RenderTexture as the active one
    RenderTexture.active = eyeCamera.targetTexture;
    eyeCamera.Render();
    // Create a new Texture2D and read the RenderTexture texture into it
    texture.ReadPixels(new Rect(0, 0, eyeCamera.targetTexture.width, eyeCamera.targetTexture.height), 0, 0);
    //転送処理の適用
    texture.Apply();
    // Restorie previously active render texture to avoid errors
    RenderTexture.active = currentRT;
    //PNGに変換
    byte[] bytes =  texture.EncodeToPNG ();
    //保存
    File.WriteAllBytes("/Users/okuyamatakashi/Desktop/Unity/photo/Hoge" + photoNumber + ".png", bytes);
    photoNumber++;
  }
}



まず、視界が描画されたRenderTextureの転送先であるTexture2Dを作成している。

texture = new Texture2D (eyeCamera.targetTexture.width, eyeCamera.targetTexture.height,
                        TextureFormat.RGB24, false);


キーボードの「S」キーを押したら、画像が保存されるようにした。

void Update () {
  //キーボードの「s」を押したら画像を保存
  if(Input.GetKeyDown("s")){
    SaveCameraImage();
  }
}



RenderTextureからピクセル値を取得するには、RenderTexture.activeに取得元のRenderTextureを指定する必要があるらしい。
そして、RenderTexture.activeに指定された映像をキャプチャするReadPixels()で、ピクセルデータを転送。
しかし、このままではピクセル値をスクリプトから参照できないので、targetTexture.Apply()で転送処理を適用している。

// Set the supplied RenderTexture as the active one
RenderTexture.active = eyeCamera.targetTexture;
eyeCamera.Render();
// Create a new Texture2D and read the RenderTexture texture into it
texture.ReadPixels(new Rect(0, 0, eyeCamera.targetTexture.width, eyeCamera.targetTexture.height), 0, 0);
//転送処理の適用
texture.Apply();



Texture2DをPNGに変換し。
フォルダを指定し保存。

//PNGに変換
byte[] bytes =  texture.EncodeToPNG ();
//保存
File.WriteAllBytes("/Users/okuyamatakashi/Desktop/Unity/photo/Hoge" + photoNumber + ".png", bytes);
photoNumber++;

※File.○○を使用するにはスクリプト冒頭に

using System.IO;

と記述する必要がある。


作成したPlayerController.csをAgentオブジェクトに追加し、eyeCamera変数にEyeCameraオブジェクトを設定。
プレビューを開始し、「S」キーを押すと指定したフォルダにエージェントの視界に映る画像が保存されていることが確認できる。
f:id:okuya-KAZAN:20170830175303p:plain


f:id:okuya-KAZAN:20170830175310p:plain

Life in Silicoのサンプルを動かした

最近Unityをハマりだしているのだが、Unity上で作成した環境内でAIを学習させる方法はないかと思い検索してみると、
ドワンゴさんの人工知能研究所が開発した超人工生命Life in Sillico(以下LIS)という強化学習を体験できるアプリケーションを見つけた。
今回はそのLISをインストールし、サンプルを動かすまでしたので、その備忘録。

環境

OS : Windows10 Enterprise
Python : Python 2.7.12 |Anaconda custom (x86_64)|
Unity : 2017.1.0f3

LISについて

LISの原理を簡単にメモしておく。
まずUnity内に作成した環境内に存在するエージェントが見た視界をPythonプログラムに送信。
Python側では、
•視界をCNN(Convolutional Neural Network)を通して抽象化したもの
•障害物などの深度情報
この2つの情報を特徴量として、Q-Learningのネットワークに入力。
出力として行動を取得しUnity側に返信する。
そしてUnity側はPython側から受け取った行動を実際に行う。


f:id:okuya-KAZAN:20170816182311p:plain
図:LISの構造(サイト[1]より)

導入

実際にLISのサンプルを動かすまでをやってみる。

Unityのインストール

Unityのホームページから最新版をインストールしておく。

pipを使えるようにする

1番手っ取り早いのはAnacondaをインストールすることなんだろうけど、今回は詳しい説明は省略。

Gitをインストール

こちらのページからインストール

LISのダウンロード

コマンドプロンプトを開いて、
以下のコマンドを実行。

$ git clone https://github.com/wbap/lis.git

必要なPythonパッケージをインストール

lis/python-agent/requirements.txtを開くと、
LISを動かすのに必要なPythonのパッケージが書いてある。


f:id:okuya-KAZAN:20170816182345p:plain


これを全てpipでインストールしておく。
一気にインストールしたい場合は、

$ pip install -r python-agent/requirements.txt

このコマンドでOK。

必要なデータをダウンロード

コマンドプロンプトにて、

$ cd lis

でダウンロードしたlisディレクトリに移動し、

$ fetch.sh

でデータをダウンロード(結構時間かかる...)

Python側のサーバーを起動

$ cd python-agent

ディレクトリを移動し、

$ python server.py

でサーバーを起動。

Unity側の準備

Unityを起動し、「Open」を選び、lisディレクトリ内にある「unity-sample-environment」を開く。
この時、"元のunity-sample-environmentを作成時とUnityのバージョンが違うぞ"的なアラートが出てくるが、無視してContinue。
ProjectビューのScenesの中のsampleシーンを選択し、


f:id:okuya-KAZAN:20170816182405p:plain


プレビューをスタート。
数分待つと、サンプルが動き出した。

Pythonで作成した自作モジュールを様々な階層からimport

作成したPythonファイルをモジュールとして、他のPythonファイルから呼び出し(import)、1つのファイルを複数のファイルから再利用することができる。
今回はそのモジュールのimportに関する備忘録。

パッケージとモジュールと__init__.py

一応メモしておくと、モジュールが.pyファイルなのに対し、パッケージは複数のモジュールがまとまったディレクトリのことを指すらしい。
ここで注意したいのは、パッケージとなるディレクトリには__init__.pyというファイルを置かなくてはならないこと。
__init__.pyとは、モジュールをimportする時の初期化を行ってくれるファイルであり、このファイルが置いていないパッケージのモジュールをimportしようとしても、import errorとなってしまう。
あくまで、「このファイルにはモジュールが存在する」ということを表すだけなので、今回はこのファイルの中身は空である。

環境

Mac Book Air : OS X EI Caption(version 10.11.6)
Python 2.7.12 |Anaconda custom (x86_64)|

前準備

Parentsディレクトリ内に、
__init__.py , main.py , parent_main.py , parent_module.pyの4つのファイルと、
Childrenディレクトリを作成し、
Childrenディレクトリ内に、
__init__.py , child_main.py , child_module.pyの3つのファイルを作成した。

Parentsディレクトリ
      |
      ---__init__.py
      |
      ---main.py
      |
      ---parent_main.py
      |
      ---parent_module.py
      |
      ---Childrenディレクトリ
                  |
                  ---__init__.py
                  |
                  ---child_main.py
                  |
                  ---child_module.py



parent_module.py

def goal():
    print "I am parent."



child_module.py

def goal():
    print "I am child."

やったこと

  1. main.pyからparant_moduleをimport(同じ階層からの呼び出し)
  2. parent_main.pyからchild_moduleをimport(上の階層からの呼び出し)
  3. child_main.pyからparent_moduleをimport(下の階層からの呼び出し)

1.同じ階層からの呼び出し

Parentsディレクトリ
      |
      ---main.py
      |
      ---parent_module.py

同じディレクトリに存在するモジュールをimportする


main.py

import parent_module
parent_module.goal()



結果

$ python -B main.py
I am parent

※オプションで-Bをつけることで、モジュールの.pycファイルができなくなる。


同じ階層の場合は上記の通り、importしたいファイルのファイル名から.pyを抜いたものを記入すればOK。

2.上の階層からの呼び出し

Parentsディレクトリ
      |
      ---parent_main.py
      |
      ---Childrenディレクトリ
                  |
                  ---child_module.py

1つ下のディレクトリに存在するモジュールをimportする


parent_main.py

import Children.child_module
Children.child_module.goal()

または

from Children.child_module import goal
goal()



結果

$ python -B parent_main.py
I am child



fromを使う場合と使わない場合の違いについては、この記事では省略する。
注意すべき点としては、fromを使った場合は、importの後にaaa.bbbのように「.」を使うことはできない。
そして以下のようなコードはエラーが出る

import Children.child_module.goal
Children.child_module.goal()

3.下の階層からの呼び出し

Parentsディレクトリ
      |
      ---parent_module.py
      |
      ---Childrenディレクトリ
                  |
                  ---child_main.py

1つ上のディレクトリに存在するモジュールをimportする。
まずはエラーが出る例について示す。


child_main.py

from .. import parent_module
parent_module.goal()

このようにコードを書くと、以下のようなエラーメッセージが出ていてしまう。

ValueError: Attempted relative import in non-package



ここで、child_main.pyに以下のようなコードを書いて実行してみる。

import sys
print sys.path



結果

$ python Children/child_main.py
['/Users/okuyamatakashi/Parents/Children', 環境変数で設定しているpath達]

sys.pathは絶対パスの文字列リストであり、リスト内には、実行スクリプトが存在するディレクトリの、絶対パスが加わっている。
ポイントは、「sys.pathより上はパッケージの検索対象ではない」という点だ。
なので解決方法としては、
「sys.pathに上の階層にあるパッケージ(Parents)の絶対パスを追加して検索対象にする」といった感じ。

import sys
import os

print "os.getcwd() -> ",os.getcwd()

sys.path.append(os.getcwd())

import parent_module
parent_module.goal()



結果

$ python Children/child_main.py
os.getcwd() ->  /Users/okuyamatakashi/Parents
I am parent!!



os.getcwd()で実行パス(Parentsディレクトリ)を取得し、それをsys.pathに追加することでParentsディレクトリを検索対象にしている。
注意すべき点としては、今回の実行パスがParentsディレクトリであるということ。
これがもしChildrenディレクトリならまた違った方法でParentsディレクトリのパスをsys.pathに加える必要がある。


ここまで書いておいてあれなんだが、sys.pathにパスを追加して無理矢理別階層のモジュールをインポートしたりすることは、インポートされる側が把握しきれなくなったりのであまり良くない。
sys.pathは全てのモジュールが参照するため、他のモジュールに影響を及ぼす可能性があるらしい。
(例えば、あるモジュールを外すと今まで動いていたモジュールが、途端にimport Errorが発生する、挙動が変わってしまうなどの不具合)
解決法としては、
環境変数PYTHONPATHを設定する方法がある。
以下のように環境変数を設定しすると、sys.pathには常にPatentsへのパスが入る。

f:id:okuya-KAZAN:20170624013450p:plain

よってchild_mainの中身は、

import parent_module
parent_module.goal()

これだけで良い。

まとめ

色々なところで使いたいモジュールを置いているパッケージが固定で、そのモジュールを様々なところからimportしたいのであれば、PYTHONPATHを設定すると楽かも。

pipのコマンドのメモ

Pythonのパッケージ管理の際に使いまくるpipだが、コマンドをすぐ忘れてしまうのでここにメモしておく。

pipのバージョンを確認

pip -V



パッケージの検索

pip search <パッケージ名>



インストー

pip install <パッケージ名>


バージョンを指定してインストー

pip install <パッケージ名> == 1.2.3.
pip install <パッケージ名> >= 1.2.3
pip install <パッケージ名> < 1.2.3
||< 
<br>

アンインストール
>||
pip uninstall <パッケージ名>



再インストー

まずはパッケージをアンインストール。その後再インストールということをしたい場合、
前回インストール(ビルド)した時のキャッシュが使われてしまうため、それを無効にするため、--no-cache-dirオプションをつける

pip --no-cache-dir install -I pillow <パッケージ名>


パッケージのアップデート

pip install -U <パッケージ名>


インストールされているパーケージを表示(-oまたは--outdatedオプションをつけるとアップデート可能なパッケージを表示してくれる)

pip list



インストールされているパッケージの情報を表示

pip show <パッケージ名>


Pythonでフレームワークを使わずにニューラルネットワークを実装

Chainerや、TensorFlowといった、深層学習のフレームワークを使用せず、NumpyとMatplotlibのみでニューラルネットワークを実装した。

全てのコードなどが公開されたリポジトリはこちら

やったこと

3クラス分類を行うニューラルネットワークPythonで実装した。
(ついでに2クラス分類を行ったり、XORを解いたりしたのでリポジトリに公開しておく)

データの準備

先ほどドヤ顔で深層学習のフレームワークを使用しないと宣言した手前情けないのだが、データの作成に関しては、自分で1から作成せず、scikit-learnmake_classificationという機能を使って作成した。

import sklearn.datasets
x , label = sklearn.datasets.make_classification(
n_features=2, n_samples=300, n_redundant=0, 
n_informative=2,n_clusters_per_class=1, n_classes=3)

これでxに入力データが、labelに教師データが格納される。
make_classification関数の引数は(3)の方の記事に詳しく書いてある。

import matplotlib.pyplot as plt
plt.scatter(x[:, 0], x[:, 1],c=label,cmap=plt.cm.jet)
plt.show()

f:id:okuya-KAZAN:20170523164550p:plain
このような感じでデータが生成された。
xは2つの入力値(座標)を持つサンプルを300個含んだ配列であり、
labelは各サンプルのクラス(0,1,2)を含んだ配列である。

print x[:10]
array([[ 0.52358787, -1.15423424],
       [-0.52633513, -1.76794057],
       [ 0.55418671, -0.46817551],
       [-1.17471257, -0.44251872],
       [-1.34866206, -0.54916791],
       [ 0.67537883, -0.53149583],
       [ 1.61055345,  0.0481599 ],
       [-0.09616488, -1.51676462],
       [ 0.22409607,  1.40511859],
       [ 2.24986887,  1.18862095]])
print label[:10]
array([2, 0, 0, 0, 0, 2, 1, 0, 1, 1])

ニューラルネットワークの構造

以下のようにニューラルネットワークを設計した。


f:id:okuya-KAZAN:20170523173356p:plain


層は入力層、隠れ層、出力層の3つで、各層のユニット数は(2,3,3)とし、
隠れ層の活性化関数にはTanh関数、
出力層の活性化関数にはSoftmax関数を使用した。

class Function(object):
    def tanh(self, x):
        return np.tanh(x)

    def dtanh(self, x):
        return 1. - x * x

    def softmax(self, x):
        e = np.exp(x - np.max(x))  # オーバーフローを防ぐ
        if e.ndim == 1:
            return e / np.sum(e, axis=0)
        else:
            return e / np.array([np.sum(e, axis=1)]).T # サンプル数が1の時

実装

※これ以降定義する関数は全てNewral_Networkクラスの中のメソッドである。

コンストラクタ

コンストラクタとは、新たにオブジェクトが生成される際に呼び出されて内容の初期化などを行なうメソッドのことであり、メッソド名は__init__とする。
ここでニューラルネットワークの構築を行う。

class Newral_Network(object):
    def __init__(self, unit):
        print "Number of layer = ",len(unit)
        print unit
        print "-------------------------"
        self.F = Function()
        self.unit = unit
        self.W = []
        self.B = []
        self.dW = []
        for i in range(len(self.unit) - 1):
            w = np.random.rand(self.unit[i], self.unit[i + 1])
            self.W.append(w)
            dw = np.random.rand(self.unit[i], self.unit[i + 1])
            self.dW.append(dw)
            b = np.random.rand(self.unit[i + 1])
            self.B.append(b)

unitは各層のユニット数が格納された配列である(今回の場合、unit=[2,3,3])。
unitの情報をもとに、学習するパラメータ(W, B)をランダムに初期化している。
dwは重みの修正量を保持する配列であり、モメンタムでの計算に使う。

順伝搬

構築したネットワークに入力値を与え、出力を得るforwardメソッドを定義する。
配列Zを用意し、そこに各層の出力値を加えていく。

# 順伝搬
def forward(self, _inputs):
    self.Z = []
    self.Z.append(_inputs)
    for i in range(len(self.unit) - 1):
        u = self.U(self.Z[i], self.W[i], self.B[i])
        if(i != len(self.unit) - 2):
            z = np.tanh(u)
        else:
            z = self.F.softmax(u)
        self.Z.append(z)
    return np.argmax(z, axis=1)

# ユニットへの総入力を返す関数
def U(self, x, w, b):
        return np.dot(x, w) + b

forwardメソッドの返り値だが、
出力層の各ユニットの出力値の中で最も高い値を出力したユニットの番号、つまり入力値の属するクラスの予測値である。
f:id:okuya-KAZAN:20170523184335p:plain

誤差の算出

ニューラルネットワークの出力値と目標出力値(label)から誤差を算出する。
誤差関数には交差エントロピーを用いている。
※出力値はforwardメソッド内で算出された配列Zの中の1番末尾にある配列であり、forwardメソッドの返り値ではない。

# 誤差の計算
def calc_loss(self, label):
    error = np.sum(label * np.log(self.Z[-1]), axis=1)
    return -np.mean(error)

誤差逆伝搬

今回のプログラムの肝となる誤差逆伝搬法によるパラメータの学習を行う部分。

# 誤差逆伝搬
def backPropagate(self, _label, eta, M):
    # calculate output_delta and error terms
    W_grad = []
    B_grad = []
    for i in range(len(self.W)):
        w_grad = np.zeros_like(self.W[i])
        W_grad.append(w_grad)
        b_grad = np.zeros_like(self.W[i])
        B_grad.append(b_grad)

    output = True

    delta = np.zeros_like(self.Z[-1])
    for i in range(len(self.W)):
        delta = self.calc_delta(
            delta, self.W[-(i)], self.Z[-(i + 1)], _label, output)
        W_grad[-(i + 1)], B_grad[-(i + 1)] = self.calc_grad(self.W[-(i + 1)], self.B[-(i + 1)], self.Z[-(i + 2)], delta)

        output = False

    # パラメータのチューニング
    for i in range(len(self.W)):
        self.W[i] = self.W[i] - eta * W_grad[i] + M * self.dW[i]
        self.B[i] = self.B[i] - eta * B_grad[i]
        # モメンタムの計算
        self.dW[i] = -eta * W_grad[i] + M * self.dW[i]

# デルタの計算
def calc_delta(self, delta_dash, w, z, label, output):
    # delta_dash : 1つ先の層のデルタ
    # w : pre_deltaとdeltaを繋ぐネットの重み
    # z : wへ向かう出力
    if(output):
        delta = z - label
    else:
        delta = np.dot(delta_dash, w.T) * self.F.dtanh(z)
    return delta

デルタの計算方法は出力層とそれ以外の層では異なるため、bool変数であるoutputを定義し、その値によってデルタの算出方法を変えている。
変数Mはモメンタムでの加算の割合を制御するハイパーパラメータ。

学習

これまでに定義したメソッドを駆使し、ニューラルネットワークの出力が目標出力(label)に近づいていくようパラメータをチューニングしていく。

def train(self, dataset, N, iterations=1000, minibatch=4, eta=0.5, M=0.1):
    print "-----Train-----"
    # 入力データ
    inputs = dataset[:, :self.unit[0]]
    # 訓練データ
    label = dataset[:, self.unit[0]:]

    errors = []

    for val in range(iterations):
        minibatch_errors = []
        for index in range(0, N, minibatch):
            _inputs = inputs[index: index + minibatch]
            _label = label[index: index + minibatch]
            self.forward(_inputs)
            self.backPropagate(_label, eta, M)

            loss = self.calc_loss(_label)
            minibatch_errors.append(loss)
        En = sum(minibatch_errors) / len(minibatch_errors)
        print "epoch", val + 1, " : Loss = ", En
        errors.append(En)
    print "\n"
    errors = np.asarray(errors)
    plt.plot(errors)

うーん...なんかコードが汚い...


実際に3000回学習させた時の誤差の変動を示したグラフがこち
f:id:okuya-KAZAN:20170523190031p:plain

結果

学習させたパラメータを引数に取るdraw_testメソッドを定義

def draw_test(self, x, label, W, B):
    self.W = W
    self.B = B
    x1_max = max(x[:, 0]) + 0.5
    x2_max = max(x[:, 1]) + 0.5
    x1_min = min(x[:, 0]) - 0.5
    x2_min = min(x[:, 1]) - 0.5
    xx, yy = np.meshgrid(np.arange(x1_min, x1_max, 0.01),
                         np.arange(x2_min, x2_max, 0.01))
    Z = self.forward(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)

    plt.contourf(xx, yy, Z, cmap=plt.cm.jet)
    plt.scatter(x[:, 0], x[:, 1], c=label, cmap=plt.cm.jet)
    plt.show()

f:id:okuya-KAZAN:20170523164550p:plain
f:id:okuya-KAZAN:20170523190637p:plain
認識できてる...!!

2クラス分類

scikit-learnmake_moonsという機能を使って2クラスのデータも作成し、
クラスの分類を行うニューラルネットワークを構築し学習させた。
層は入力層、隠れ層、出力層の3つで、各層のユニット数は(2,3,2)とした。
f:id:okuya-KAZAN:20170523191231p:plain
f:id:okuya-KAZAN:20170523191243p:plain


ちなみに、隠れ層のユニット数を20にした場合
f:id:okuya-KAZAN:20170523191511p:plain
多少オーバーフィットを起こしている恐れがある。