PythonでSwift-likeなstructを使いたいので検討(してみたがダメだった)
Pythonはいろいろなことを簡単に書けるのでよく使っています. ところが『実践Swift』を読んでみたところ,その型のある世界に憧れるようになってしまいました… (大学で型についていろいろ勉強したのもある)
今回はSwift-like(と言い切ってしまっていいのか不安が残りますが)な構造体を使いたい場合にどんな実装をすれば良いのかについての検討です.
Swiftでは変数(や関数)をまとめた型としてenum
やstruct
,class
,protocol
などがあります.
『実践Swift』では,struct
は変数のまとまりを値型として扱いたい場合に用いると書かれています.
一番簡単な例が座標情報などですね. Pythonでクラスを用いて実装すると次のようになります.
class Point(): def __init__(self, x, y): self.x = x self.y = y p = Point(10, 20) print(p.x) # 10 print(p.y) # 20
一見これで良さそうに見えるのですが,インスタンス同士の代入を行なった場合クラスでは参照がコピーされます. すなわち片方の座標の値を変更するともう片方も変更されてしまいます. これは座標というただの値を表現する手法として適切ではありません.
以下がその例です.
# (上の続き) q = p q.x = 30 print(q.x) # 30 print(p.x) # 30
Swiftのstructを用いると,この座標を参照ではなく値そのものがコピーされるため,以上のような問題は発生しません.
(そもそもC言語でも同じです)
ちなみに上のコードでq = p
の部分をq = copy.deepcopy(p)
とするとうまくいきます(import copy
が必要)が,できればq = p
でコピーできてほしいものです.
すなわちPythonで値型で構造的に記述したいというのが今回の目的です.
ということをTwitterに呟いたら,enum.Enumを使うという啓示をいただきました.
早速調べると…おお…これは使えそうだ(早とちり)ということで書いてみました. 実装には次のサイトを参考にさせていただきました.
import enum class EnumTest(int, enum.Enum): # 継承元最初のintはvalueメソッドにおける加算にて型を決める必要があるため書く _x: int = 10 _y: int = 15 # getter を試した @property def x(self): return self._x # return self._x.value() # とすればうまくいくが全てのpropertyでこれをやるのは非効率 # いい方法がないものか…? @classmethod def value(self) -> int: return self._x + self._y # getterを利用(うまくいかない) print(EnumTest.x) # 要素を参照(名前が出てくる) print(EnumTest._x) # 要素の値を知る方法(ちょっとめんどい) print(EnumTest._x.value()) # メソッドを利用(これはうまくいく) print(EnumTest.value()) # 上から # <property object at 0x1050a6b38> # EnumTest._x # 25 # 25
やりたかったようにEnumTest.x
で簡単に値が取得できればよかったのですがちょっと追加の実装がいるのであまり綺麗じゃないですね.
上の実装では致命的な欠陥がいくつかあります.
- 値の変更ができない
- Constructorがない(座標データごとにclassを宣言する必要がある)
これではSwift-likeなstructとは離れ過ぎていますね. さらなる検討を重ねるか諦めるか…
ちなみにnamedtupleを使うとpoint.x
という形で値が取得できるようですが,こちらも値を変更できないという問題があるそうです.
うーん…そもそも値型の構造体を使うよりもっとPython-likeな手法があるのかもしれないなぁ…
というわけで検討してみたがダメだったという記録です.
(追記) namedtupleが値の変更を行えないと書きましたが関数型っぽく書くなら値の変更は必要ないですね… さらにnamedtupleにメソッドを追加した例もあるのでこちらを使った方が便利そうです. まあいずれにせよスッキリとは書けないみたいだ…
超基本的なコンパイラを作ってみる(1. 字句解析編)
コンパイラ
大学で言語処理系論というコンパイラを扱う講義を受講したので,その実践を兼ねて自分でコンパイラを作ろうと決意しました. いきなり"なんとか言語"のコンパイラを作るのは難しいので,本当に単純なものから始めたいと思います.
作る題材は何かと言うと,『よくある例 (The all-time favorite) 、卓上計算機です。』(引用元:http://ocaml.jp/archive/ocaml-manual-3.06-ja/manual026.html)
新規性のカケラもない企画ですが,自分で作るところに意味があると思っています. もちろんコピペではなく「参考にして」オリジナルの機能も導入しながら作っていきます.
コンパイラの構成
コンパイラを作るためにはまず構成を知らなくてはいけないので,調べました. 参考文献はこちらです. いわゆるタイガーブックというやつですね.
さて,コンパイラのフェーズには(上の本よりも)大雑把に分けて次のものがあります.
- 字句解析
- ソースファイルを個々のトークンへと分解する
- 構文解析
- プログラムの句構造を解析する
- 意味解析
- 変数や式の型を検査して各句の意味を決定する
- 中間コード生成
- 特定のプログラミング言語やマシンに依存しない中間コードを生成する
- 最適化
- 高速化を図る
- 機械語コード生成
- 最適化した中間コードを元に機械語コードを生成する
字句解析
まずは字句解析器から作ってみたいと思います. 字句解析の入出力は次のイメージです.
- 入力:
x = 3.1 + y * 20
- 出力:
ID("x") EQ Float(3.1) PLUS ID("y") TIMES INT(20)
入力された文字列の各単語について,対応するトークンを割り当てます.
字句解析器の仕組み
字句解析器をどのように作るかというと, “正規表現” を使います. 正規表現についての説明は割愛します.
正規表現のプログラムでの実装は,正規表現をオートマトンに変換することで行います.
しかしここでは簡単のため"字句解析器生成ツール"というものを使いたいと思います.
Lex
Lexは字句解析器生成ツールです. 正規表現やトークンの情報を書いたファイルを入力すると,字句解析器を生成してくれます.
今回はLexのOcaml版,Ocamllexを使います.(Ocamlをインストールすれば一緒についてきた気がします…)
Ocamllex
Ocamllexの使い方については,下のリンクに書いてあります(がかなり分かりづらかったです) Lexer and parser generators (ocamllex, ocamlyacc)
以下,上のサイトのまとめになっています.
Ocamllexの設定ファイル(拡張子は.mll)のフォーマットは次のようになっています.
%{ header:あらかじめ設定しておきたい事項や読み込んでおきたいモジュールを書いておく %} declarations:変数を宣言したりする.正規表現を変数にしておくときとか %% rules:正規表現を用いてトークンの定義を記述する %% trailer: 最後に実行したい処理や後に使いたいOCamlのコードを書いておける
以上を書いたファイルをlexer.mll
として保存します.
ここで,今回記述する文法の仕様を決めたいと思います.拡張する可能性もあるのでかなりざっくりですが…
- 四則演算はそのまま記述できるように
- 後に変数が使えるように,1文字目が英字,2文字目以降が英数字もしくはアンダースコアなものをIDとして認識
例えば今回は次のように記述しました. なおこのファイルは上のサイトだけでなく以下の講義サイトも参考にしています(講義資料は受講者しか見れませんが…)
http://www-kb.is.s.u-tokyo.ac.jp/~koba/class/compiler/
{ (* トークンの型を宣言 *) type token = PLUS | MINUS | TIMES | DIV | LPAREN | RPAREN | EOL | EOF | ID of string | INT of int } (* よく使う正規表現に名前をつける *) let space = [' ''\t''\r'] let digit = ['0'-'9'] let lower = ['a'-'z'] let upper = ['A'-'Z'] (* トークンのルール *) rule tokenize = parse | space+ { tokenize lexbuf } (* そのままバッファを返す *) | (lower|upper)(lower|digit|upper|"_")+ { ID(Lexing.lexeme lexbuf) } (* 上で述べたIDの定義に従って正規表現を書いた *) | digit+ { INT(int_of_string(Lexing.lexeme lexbuf)) } (* 数字 今はint型のみ *) | "+" { PLUS } | "-" { MINUS } | "*" { TIMES } | "/" { DIV } | "(" { LPAREN } | ")" { RPAREN } | "\n" { EOL } | eof { EOF } | _ { Format.eprintf "unknown token %s" (* エラー *) (Lexing.lexeme lexbuf); failwith "lex error" } { (* trailer *) (* 字句解析を実行するコード *) let rec readloop lexbuf = let t = tokenize lexbuf in if t=EOF then [] else t::(readloop lexbuf) (* main ファイル名 で実行 *) let main filename = let lexbuf = Lexing.from_channel(open_in filename) in readloop lexbuf }
以上を準備したら,次の手順で字句解析を実行できます.
- 入力ファイルを用意
- 例えば
x1 - (2 + 3) / 5 *6
と書いたファイルをin.ml
として用意
- 例えば
ocamllex lexer.mll
を実行する.エラーが出たら直すlexer.ml
が生成されているので,シェル上でocaml
と叩いてインタラクティブ環境を起動し,#use "lexer.ml"
と入力してエンター.ここでエラーになること重ある(特にtrailer内でのエラーが多い)- 同じインタラクティブ環境で,trailerに書いたコードを読み出す.この場合では
main "in.ml"
と入力する - エラーがなければ,次のように出力されるはず
token list = [ID "x1"; MINUS; LPAREN; INT 2; PLUS; INT 3; RPAREN; DIV; INT 5; TIMES; INT 6; EOL]
きちんとスペースなども考慮して字句解析が行われていることがわかります.
まとめ
Ocamllexを使うことで,オートマトンなどの記述の必要なく字句解析を実行することができました. 機会があればオートマトンからフルスクラッチで書いてみたいと思います.そこまで手間でないかもしれないので…
字句解析はただの文字列が解釈されるだけであまり面白くないので,次はこれを構文木の形に直す構文解析を行ってみたいと思います.
OpenAI Gymを動かしてみた(keras-rlを用いたDQN編)
前回OpenAI Gymのチュートリアルを使ってQ学習を試してみました. 今回はAlphaGoなどで話題のDQN(Deep Q Network)を試してみたいと思います.
1. Deep Q Network
Deep Q Networkとは,Q学習で用いられるQ関数(状態を入力にとり,各行動の価値を返す.行動価値関数)をDeep Neural Networkで表現したものです. Q学習においては,行動価値関数はテーブルを用いて表現されるのが一般的です. しかし,ゲームなどの多くの状態をもつ環境や,実世界などの連続値環境では,テーブルのサイズが大きくなりうまく学習ができません. このテーブルをニューラルネットワークで近似しようというのが基本的なアイデアです. (以上は私の定性的理解です)
DQNは学習が収束しにくいなどの欠点があり,それを回避するためにExperience Replayなどの様々な手法が取り入れられていますが,今回はkeras-rlという強化学習ライブラリを使って簡単に試してみたいと思います.
2. keras-rl
kerasは機械学習のライブラリの一つです.
TheanoやTensorflowのラッパーであり,これらに比べて簡単にニューラルネットワークを記述することができます.
keras-rlはkeras向けの強化学習ライブラリで,
など様々な強化学習手法を試すことができます.
ドキュメントはこちらです.
3. 試してみる
まずはkerasとkeras-rlのインストールを行います. なおこの部分は以下のリンクを参考にしました. qiita.com
- keras
pip install keras
- keras-rl
git clone https://github.com/matthiasplappert/keras-rl.git pip install ./keras-rl
keras-rlを展開したディレクトリに,example/dqn_cartpole.py
というファイルがあると思うので,それを実行します.
するとLayer情報が出力された後,Cartpoleの描画が始まります. しばらくすると学習され,持続時間が伸びていくことが確認できます.
私の環境では200エピソード程度でほぼ最後まで続くようになりました. (Q学習では数千エピソード程度の学習が必要だったので,それより優れているかもしれません.パラメータチューニングを行なっていないのでなんとも言えませんが…)
4. その他
自分でアルゴリズムを作りたい,などの場合は,CartPole-v0環境についてのドキュメントを確認すると良いと思います.
また,env.render()
で描画される画面は,ディスプレイに見えていない時(ウィンドウの後ろにあるなど)は描画がスキップされるらしく,必要な時だけ見えるようにしておくことで学習の高速化が図れます.
今回DQNの細かい理論,アルゴリズムには触れませんでしたが,下のリンクはPythonとKeras,Tensorflowを用いてDQNの実装例があるので,このような記事を参考にすると良いと思います.
OpenAI Gym を動かしてみた
OpenAI Gym
OpenAI Gymとは,公式サイトにも"A toolkit for developing and comparing reinforcement learning algorithms.“とあるように,主に強化学習を開発,比較検討するのに使える環境が集まったサイトです.
まずは,チュートリアルを進めてみます.以下のサイトが公式のドキュメントになっています.
上のサイトに従っていけばできますが,一応作業履歴ということでここに書いておきます. こちらのバージョンはPython3.5,anaconda3-4.0.0環境を使っています.
1. OpenGym AIのインストール
pip install gym
が一番簡単でした.
2. 環境を動かしてみる
import gym env = gym.make('CartPole-v0') for episode in range(20): observation = env.reset() for t in range(100): env.render() print(observation) action = env.action_space.sample() observation, reward, done, info = env.step(action) if done: print("finished after {} timestamps".format(t+1)) break
を実行すると,倒立振子が出てきて,20エピソード分実行され,描画されます.
3. Q学習で強化学習してみる
ここからは上のページにない内容ですが,せっかくなので強化学習を試してみます.
https://gym.openai.com/algorithms/alg_0eUHoAktRVWWM7ZoDBWQ9w
上に,どなたかが実装してくださったQ学習のコードが載っています. これをコピペして,Python3用に一部修正し(xrangeやprint)実行します. (私の環境ではenv.monitorがうまく動かなかったので(きちんと調査してはいませんが)env.monitor周りはコメントアウトして,その代わりに毎ループでenv.render()を実行しました.これにより倒立振子が学習されている様子を知ることができます)
うまく動くと,徐々にうまくなっていくことが確認できます.
4. 少し変更
先ほどコピペしたコードを,次を目標に一部変更します.
- 毎回の学習の様子をみる必要はないので,何回かに1回のみにする
- 学習の結果をみるテストメソッドを追加する(学習は行われず,greedyに行われる)
これを追加したコードが次になります.
まとめ
気づいた点をまとめておきます.
- CartPole-v0は200フレームで強制的にdoneになる(うまくいっても行かなくても)
- それ以上経ってからはactionの指示が効かない
- 連続空間でもQ学習は有効であることがわかった
- 連続空間では状態数が多くなりがちなので,離散化が粗すぎても細かすぎても適していないイメージだったが,すんなりと学習できた(学習タスクにもよると考えられる)
今回はせっかくQ学習までやったので,次回は流行りのDQNを試してみたいと思います(と書いていますが実はもう試してあるのでそれをまとめる時間があれば…)
それと
今後,自身がどんなことをやっているきたかの日記としての活用を考えていて,新規性のないやってみたレポートが増えそうな気がしています. 新規性がありそうなものはQiitaにもアップしたいと思っています(がそんなに新規性のある記事がポンポン浮かぶわけでもない)