かざん技術ブログ

Python, Unity, Deep Q Network, 強化学習

Unity, Python, Deep Q Network Learningを使って自動運転

前回の更新からずいぶんと経ってしまった...
ドワンゴさんの人工知能研究所が開発した超人工生命Life in Sillico(以下LIS)という、
AIエージェントを強化学習で鍛えられるモジュールを使用して、自動運転のエージェントを作成したので、それについてまとめていこうと思う。
このLISというのが本当に素晴らしいモジュールで、Unityの環境上で作成したエージェントを、AIの学習によく使われるPythonを使って鍛え上げることができる。


LISの導入はこちらの記事で取り上げている。

参考にさせていただいたサイト、論文、記事

  1. Life in Silico : http://github.com/wbap/lis
  2. Lillicrap, et al. Continuous control with Deep Reinforcement Learning
  3. https://yanpanlau.github.io/2016/10/11/Torcs-Keras.html

手法

LISのドキュメントを直接見てくれた方が早い気もするが、AIエージェントがどう最適な行動を学んでいくのか(LISのシステム)について記述していく。

距離センサーは入力情報に使っていない

まず、今回作成したエージェントの入力情報だが、距離センサーに頼らず、車載カメラに映る画像のみを入力情報としている。
エージェントは車載カメラに映る画像からコースアウトしたり、障害物と衝突しない行動を選択していく。

Deep Q Network

エージェントを制御するアルゴリズムとして、LISではDeep Q Network(以下DQN)を用いている。
DQNは、環境から与えられた状態を入力として受け取り、車が選択できる各行動の価値(Q値)を出力するニューラルネットワークのこと。
ここでは、車載カメラの映像が状態となる。

強化学習

車の自律走行の学習方法として、強化学習を用いている。
強化学習とは、エージェントが環境との相互作用を通して、得られる報酬の総和を最大にする行動を学習していく学習法のこと。
ここでいう相互作用というのは、

  • エージェントが今どんな状態なのかを環境から受け取る
  • エージェントは状態を基に行動を選択して環境に返す
  • 環境はエージェントから受け取った行動を基に状態を変化させる
  • 環境は新しい状態と行動に対する報酬をエージェントに返す

といったサイクルのことである。 f:id:okuya-KAZAN:20180524144052p:plain
この学習方法で、車はコースアウトしたり、障害物とぶつからない最適な運転技術を学んでいく。
強化学習の詳細については、こちらの記事でまとめている。

学習の流れ

強化学習を用いて、DQNを学習させていく流れは以下のようになる。

  1. 車載カメラの映像をDQNに入力
  2. 行動の選択
    → ε-greedy法と呼ばれる手法で行動を選択する。
    確率εでランダムな行動を選択し、確率1-εで、DQNから出力(各行動のQ値)が最大となる行動を選択する。
    ε-greedy法を採用することで、他に価値の高い行動がないかを探索でき、様々な行動に対する適切なQ値の学習が可能となる。
  3. 選択された行動の評価
    → 2.で選択した行動に対して報酬を与える。
    例えば、コースに沿って走れていたならプラスの報酬、コースアウトしたり障害物と衝突してしまったらマイナスの報酬といった感じ。
  4. 経験の蓄積
    → 「ある状態である行動をとったらどれだけの報酬を得て、環境がどんな状態に変化した」といった経験を、メモリーと呼ばれる場所にストック。
  5. DQNの学習
    → メモリーに記録されている情報からランダムに経験を選び出し、ディープラーニングによってDQNをチューニングしていく。 f:id:okuya-KAZAN:20180524144123p:plain

実験

ここまで理論的なことを述べてきたが、ここからは実際にLISを使ってどんな実験をしたかを書いていく。

実験の目的

実験の最終的な目的としては、実際の車の1/10スケールの「RoboCar1/10(以下RoboCar)」が、研究室内に作成したコースを自律走行できるよう鍛え上げること。

実験の流れ

シミュレーション環境上でDQNモデルを訓練し、訓練されたモデルを実世界に適用させていく。 行った主な実験は以下の3つ。 1. Virtual Simulation →直線しかないシンプルな道路で自律走行できるよう学習。 2. Laboratry Simulation →シミュレーション環境上に、研究室とRoboCarとドライビングコースを作成し、RoboCarが自律走行できるよう学習。 3. RoboCar verification →Laboratry Simulationでの学習モデルを現実世界に適用。

※ 1,2がシミュレーション環境、3が現実世界での学習となる。

Virtual Simulation

環境

一般道路とほぼ同じ道幅の直線道路を用意して、30mごとに障害物を発生させる。
どちらの車線に障害物が発生するかはランダムに決定。

エージェント

一般的な軽自動車をイメージして作成し、DQNの出力は、右に10度ハンドル切る場合のQ値、左に10度ハンドル切る場合のQ値、直進する場合のQ値の3種類とした。

報酬設計

障害物と衝突するかコースアウトした場合、報酬として-1を与え、障害物を避けるごと(30m進むごと)に+1の報酬を与える。

結果

学習過程、学習の結果を動画にまとめて、YouTubeにアップしているので、ぜひご覧ください。

www.youtube.com

Laboratry Simulation

環境

以下の画像のようなコースを作成し、1エピソードにつき1つ、コース上のストレートの部分のランダムな位置に障害物を発生させる。

エージェント

実際のRoboCarと同じエージェントをシミュレーション環境上作成した。
DQNの出力は、右に25度ハンドル切る場合のQ値、左に25度ハンドル切る場合のQ値、直進する場合のQ値の3種類とした。

報酬設計


よりコースの真ん中を、よりコースの軸に沿って進むと多くの報酬がもらえるよう報酬設計した。 コースの軸に対する車体の傾きによる報酬から、|trackPos|による減点をし、最後に車のスピードと比例定数をかけたものを報酬とする。 |trackPos|はコースの真ん中を最小値0とし、真ん中からズレるだけ値が上昇して、コースの端が最大値の1となるよう正規化。

結果

こちらも学習過程、学習の結果を動画にまとめて、YouTubeにアップしているのでぜひご覧ください。

www.youtube.com

RoboCar verification

環境

Laboratory Simulationでの環境と同じになるようドライビングコースを設計した。

エージェント

実際の車の1/10スケールの「RoboCar1/10」をエージェントとした。
こちらもLaboratory Simulation上のAgentと同じプロパティ。

System Architecture

どんなかんじでRoboCarが動くのかを図にまとめる。

ZeroMQというのはネットワークを介して他言語、他OS間でもデータのやりとりができるインターフェース。 Distributed Messaging - zeromq

+αの実験

+αの実験として、信号認識モジュールとの連携を行った。
信号認識モジュールとは同じ研究室の同僚が作成したニューラルネットワークのモジュールで、車載カメラの映像から信号認識を行い、認識結果(Stop, Go)を出力するようチューニングされている。

結果

しつこいようだが、こちらも学習過程、学習の結果を動画にまとめて、YouTubeにアップしているのでぜひご覧ください。

www.youtube.com

まとめ

今回3つの実験を大雑把に書いてしまったが、今後はこれらの実験のより詳細を書き留められたらなと思う次第です。

強化学習,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をインストールすることなんだろうけど、今回は詳しい説明は省略。

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 <パッケージ名>