jupyterの.ipynbファイルをスクリプトに変換する処理を実装してみた
概要
タイトル通りですが、jupyterの.ipynbファイルをスクリプト(.py)に変換する処理を書いてみました.
色々試行錯誤しながら処理を書いているときには.ipynbは非常に使いやすいのですが、コマンドラインから直で実行できた方が良さそうだなと思って書いてみました.
ですが、たった今こういう情報を見つけてしまったので、なんというか最初に想定していた意義がなくなってしまいました...
とはいえ、.pyファイルの方がgit管理しやすいと思うので(白目)一応残しておこうと思います. argparseを少し触ってみたかったってのも結構ある.
中身
中身についてですが、 .ipynb は中身はただのJSONになっているので、プログラムの処理に該当する部分の情報を引っ張ってきてそのまま出力してあげれば良いです.
入力用の .ipynb ファイルと出力用のファイルのパスをコマンドライン引数から受け取るようにしました. 入力ファイルの拡張子に対する制約をちゃんとコードにしてないので色々とマズそうですが、萎えてしまったのでそのままにしてやろうと思います
argparse について
pythonでコマンドラインから何かしらの引数を受け取る時って多くの場合はsys.argv[1]とかやると思うんですが、これをもっとキレイな形で書くためのライブラリが実はpythonには組み込まれています.
それがargparseと呼ばれるライブラリで、実に色々な設定を行うことができるようになっています.
導入
まずはサイトにある例を眺めてみます。
import argparse parser = argparse.ArgumentParser(description='Process some integers.') parser.add_argument('integers', metavar='N', type=int, nargs='+', help='an integer for the accumulator') parser.add_argument('--sum', dest='accumulate', action='store_const', const=sum, default=max, help='sum the integers (default: find the max)') args = parser.parse_args() print(args.accumulate(args.integers))
これをprog.py
として保存し、 -h とつけて呼び出してみます.
$ python prog.py -h usage: prog.py [-h] [--sum] N [N ...] Process some integers. positional arguments: N an integer for the accumulator optional arguments: -h, --help show this help message and exit --sum sum the integers (default: find the max)
かなり情報量が多く、しっかりしているように見えますね!
argparse.ArgumentParser(description='Process some integers.')
の部分で引数のパーサを定義し、さらに -h をつけて呼び出したときの説明文の情報を与えられます。
parser.add_argument()
とすることで具体的に引数を定義できます。引数を指定するときには様々な情報を加えて引数を指定できます。
例えば、一つ目の
parser.add_argument('integers', metavar='N', type=int, nargs='+', help='an integer for the accumulator')
では,
- 最初の 'integers' で実際に格納される変数名を指定
- metavar='N' で -h としたとき表示される文字を指定
- type=int で引数として渡された文字列を変換したい型に変換する関数を指定
- nargs='+' で引数の個数を指定
- help=''sum the integers (default: find the max)'で-hとしたときのメッセージを指定
としています.
そして、parser.parse_args()
で受け取った引数をパースします.
ドキュメントを見ると、おびただしい数のオプションがあります...いくつかを紹介します
epilog
エピローグみたいな感じだと思いますが、 -h としたとき最後に表示する文字列を指定できます.
>>> parser = argparse.ArgumentParser( ... description='A foo that bars', ... epilog="And that's how you'd foo a bar") >>> parser.print_help() usage: argparse.py [-h] A foo that bars optional arguments: -h, --help show this help message and exit And that's how you'd foo a bar
add_argument() メソッド
nargs
const
default
type
dest
などなどがかなり使えそうな感じです。(気が向いたら更新します)
ディープラーニングで映画レビュー分類
導入
買ったまましばらくさわれていなかった、PythonとKerasによるディープラーニング
をやっていこうと思っています。
今の所そこそこわかりやすい感じがします。元が英語なのでそういう意味でのわかりづらさは多少ありますし、内容としても0から勉強するには少し向かないような気もしますが(特にそこそこ数学的な表現がたまに出てくる)、雰囲気をつかむのにはいいのかもしれないです。
今回の記事では3.4についての内容をまとめます。
タスクについて
今回考えるタスクは、映画レビューをポジティブなものとネガティブなものに分類するというタスクです。IMDbというデータセットが用意されていて、それについて教師あり学習を行って判定ができるようにします。
コード
データのロード
まず、データをロードします。
from keras.datasets import imdb (train_data, train_labels), (test_data, test_labels) = \ imdb.load_data(num_words=10000)
num_words = ... で出現頻度が高い順から10000番目までの語のみを残して、残りを捨ててしまいます。どれくらい出現するかによるとは思うのですが、ある程度出現頻度が低い単語は捨ててしまうというのはよく使えそうな手法ではあります。
このtrain_data
中の一つ一つの要素(レビューの一つ一つ)は、例えば [1, 14, 22, ... 16, ..] という感じで、単語を表すインデックスからなるリストになっていて、imdbに用意されている単語とインデックスの対応関係を保持しているディクショナリを使うことでそれぞれのレビューを数字での表現から文字列での表現に戻すことができます。
このインデックスの値を用いて(整数だと思って)学習を行っていきます。直感的には、これはかなり素朴な方法であると言えると思います。なぜなら、このインデックス同士の関係には言葉同士の関係性はほとんど考慮されていないと考えられるためです。例えば単語の埋め込み表現なんかを使うと、単語同士の関係性も考慮したような計算が行えるのではないかと思っています(やったことはないけど)
データを整形
整数のリストはそのままニューラルネットには入力できないので、テンソルに変換する必要があります。変換する方針として2通りの方針があるようです。 * リストをパディングして全て同じ長さに揃え、(samples, word_indices) という形状のテンソルに変換する. * one-hot エンコーディングを用いて変換する. 今回はone-hot エンコーディングを用いて変換します。 例えば、[1, 3] という表現に対しては、[0, 1, 0, 1, 0, ..., 0]というベクトルが対応します。
ネットワークを定義
ネットワークを構築します。今回得たい出力はネガティブorポジティブの2値分類なので、[0,1]の範囲になるように出力層でシグモイド関数により変換を行います。入力は、語数が10000のone-hot エンコーディングされたベクトルなので、(10000,) とします。オプティマイザとしてrmspropを用いることとします。損失関数として、出力層でsoftmaxを使うときによく用いられる交差エントロピー
を用います。
from keras import models from keras import layers model = models.Sequential() model.add(layers.Dense(16, activation="relu", input_shape=(10000, ))) model.add(layers.Dense(16, activation="relu")) model.add(layers.Dense(1, activation="sigmoid")) model.compile(optimizer="rmsprop", loss="binary_crossentropy", metrics=["accuracy"])
あとは、学習データと確認用データに分けたりして学習を行うと良いです。
これだけでも88%くらいの精度が出ます。かなり良い精度が出ていると言えると思います。
感想
バイトでもDNNを使うのですが(主に物体検出のネットワーク)正直かなりコードを書くのが怪しいので、かなりゴリゴリ書いていかないといけないなと思った次第。。。
関数の部分適用 : functools.partial
functools
functools は pythonの組み込みモジュールで、主に高階関数を扱うモジュールです。
functools モジュールは高階関数、つまり関数に影響を及ぼしたり他の関数を返したりする関数のためのものです。一般に、どんな呼び出し可能オブジェクトでもこのモジュールの目的には関数として扱えます。
前にブログで書いたlru_cache もこのfunctoolsに含まれる関数のうちの一つです。今回はモジュールに含まれる partial という関数について。
partial
partial は関数とその引数の一部を受け取り、一部の引数のみを適用させた状態のオブジェクトを返します。 こういうのを関数型言語なんかでは部分適用と呼ぶことが多いと思います。カリー化・アンカリー化なんかの流れで出てくる印象があります。 一部の引数のみを適用させた〜というのは、例えば次のようにかけるということです。
>>> from functools import partial >>> basetwo = partial(int, base=2) >>> basetwo('10010') 18
partial(int, base=2)の部分で、int
関数にbase=2
というキーワード引数を渡して、それを固定したオブジェクトを計算しています。 そして、そのオブジェクトを関数のように扱い、basetwo('10010')
の値を計算します。
basetwo('10010')
は int('10010', base=2)
と等価です。 当然ですが、キーワード引数以外でも可能です。例えば、
>>> from operator import add >>> from functools import partial >>> add_10 = partial(add, 10) >>> add_10(2) 12
addは二つの変数を受け取り、その和を返す関数です。partial(add, 10)
により、そのうちの一つを10で固定した関数を計算しています。 10を足すという処理がたくさん出てくる時にはadd_10 とだけ書けばいいということですね!
ちょっとうまい例が思いつかないですが、一般的な形で書いておいた関数を、よりシンプルにかけるようになる、便利な関数だと思います。
条件付き確率場
あらまし
卒論でソフトウェアマイニングみたいなことをやるという方針がとりあえず定まって、色々論文読んでみな〜と言われて教えてもらった論文の一つで、
Predicting Program Properties from “Big Code”
こういうのがありました。
Javascriptでコードのサイズを小さくするために色々な処理が行われるのだが、その一環で変数名をtとかmとか短いものに変換することでサイズを小さくするという処理が行われるそうで、その変換後の変数名から元の変数名を推測する、という研究です。
その中で条件付き確率場(Conditional Random Field) という考えが出てきていて、なんじゃそりゃという感じだったので一度勉強しておこうと思います。
自分でさっとメモ書き程度にまとめてみようかと思います。
ちなみに条件付き確率場を前もって知らなくても一応読めるには読める。が明らかに知っていて読んだ方がわかりやすかっただろうなと思いました。
https://www.ism.ac.jp/editsec/toukei/pdf/64-2-179.pdf この資料で勉強しました。
確率付き条件場
概要
入力系列 に対して、出力系列 を推定することを考えます。
多クラスのロジスティック回帰と同じような定式化をすると動的計画法なんかを使うとうまく計算ができるよーということが述べられています。
実際に書いてみると確かに式の表現としては多クラスのロジスティック回帰と同じよう形で定式化できています。
ここで問題になるのが、愚直に計算してしまうと候補となるラベルの数、分配関数の値、各ラベルに対するスコアの値が現実的に計算できないくらいの計算量になってしまうという点。
一般の素性関数についてはおそらく計算することは事実上難しいのではないかと思いますが、ラベル列に対して一次マルコフ性
を仮定して、素性関数の形を制限することによってうまく計算することができるようになります。
一次マルコフ性とは、各時点において、一つ前の状態が決まれば今の状態に対する制約が定まるという性質です。
例えば前置詞の後は名詞がきやすい、文の初めは(文の始まりをあるトークンだと思えばよくて)名詞がきやすい、などなど一つ前の状況によって今の状況に関する制約が得られることはままあり、それに対するモデル化であるそうです。
このマルコフ性のおかげでかなりスコアの計算が楽になり、動的計画法により現実的な計算時間でスコアを計算することができるようになります。
ラベル列の推定
途中までのラベル列 に対して、あるを固定したとき、
については、 のスコアが最大になるようにを計算しておきます。
そうすれば、先ほど述べたマルコフ性から、 を考えるときにスコアの変化量に関係があるのはのみなので、 を変更する必要がなく、 の選び方のみを考えれば最適なを計算することができます。
これを、 から順番に計算していくことで、求めるべき を計算することができます!
感想
なんかマルコフ過程みたいな感じだなーと思いました。ビタビ・アルゴリズム(Viterbi algorithm)というらしく、まさしく前に聞いたことのあるアルゴリズムでした(笑) しかし、どこでやったんだろうなー。。。明らかにどこかで見たことのあるアルゴリズムであることは間違い無いんですけどね。 そこそこ昔からあるアルゴリズムのようですが、単純に用いるだけでもそれなりに精度が出そうなアルゴリズムだなーと思いますた。
最近はニューラルネットが流行りがちですが、こういう昔のアルゴリズムを学んでモデル化とかのアイデアを知るのも大切なのかなと思ったり思わなかってりしています。
関数をキャッシュする : lru_cache
Decoratorについて
Decoratorは、大雑把に言ってしまうと高階関数をシンプルに書くための記法です。Pythonでは関数もまた第1級関数であるため、関数を引数にとったり、関数を返値として返すことができます。関数を引数にとるような関数のことを高階関数と呼びます。有名なところでは、map
やfilter
が高階関数の例として挙げられます。
list(map(fact, range(6))) # => [1, 1, 2, 6, 24, 120] list(filter(lambda x: x < 0, range(-5, 5))) #=> [-5, -4, -3, -2, -1]
pythonではmapやfilterの返値はイテレータなので、例えばlistなどで受けてあげることになります。ちなみに、通常このような処理は通常内包表記で書けるので、できればmapやfilterで書くべきではないですし、内包表記で書くように設計されています。 さて、自分で高階関数を書くことを考えます。例えば、再帰的に関数を呼び出すような関数に対して処理を見やすくするための関数を考えてみましょう。関数を実行するときに、関数の名前や引数を表示してくれるような次のような関数を書くことを考えます。
def track(func): def tracked(*args, **kwargs): result = func(*args, **kwargs) name = func.__name__ arg_str = ",".join(repr(arg) for arg in args) print("%s(%s) -> %r" % (name, arg_str, result)) return result return tracked
この関数はある関数を受け取って、その関数を引数に適用させ、関数の名前、引数、返値を表示したあとでその適用結果を返すような関数になっています。例えば、フィボナッチ数列に適用させるとき、次のようにかけます。
@track def fibonacci_naive(n): if n< 2: return n return fibonacci_naive(n-2) + fibonacci_naive(n-1)
結果は次のように出力されます。
fibonacci_naive(0) -> 0 fibonacci_naive(1) -> 1 fibonacci_naive(2) -> 1 fibonacci_naive(1) -> 1 fibonacci_naive(0) -> 0 fibonacci_naive(1) -> 1 fibonacci_naive(2) -> 1 fibonacci_naive(3) -> 2 fibonacci_naive(4) -> 3 fibonacci_naive(1) -> 1 fibonacci_naive(0) -> 0 fibonacci_naive(1) -> 1 fibonacci_naive(2) -> 1 fibonacci_naive(3) -> 2 fibonacci_naive(0) -> 0 fibonacci_naive(1) -> 1 fibonacci_naive(2) -> 1 fibonacci_naive(1) -> 1 fibonacci_naive(0) -> 0 fibonacci_naive(1) -> 1 fibonacci_naive(2) -> 1 fibonacci_naive(3) -> 2 fibonacci_naive(4) -> 3 fibonacci_naive(5) -> 5 fibonacci_naive(6) -> 8
うまく表示されましたね。ここの@track
がデコレータと呼ばれる処理になります。真面目にこの処理を書こうと思ったら、
def fibonacci_naive(n): if n < 2: return n return fibonacci_naive(n-2) + fibonacci_naive(n-1) fibonacci_naive = track(fibonacci_naive)
こーんな書き方をしないといけません。とってもダサいですね。と、いうわけで、まず単純に関数を引数とするような処理が書きやすいというのがあります。もう一つ良い理由として、二つ以上の関数を関数に適用させるようなときにもスマートにかけます。これについてはもうすぐ出てきます。
このような書き方を導入しているのだから当然ライブラリとしていくつかの便利なデコレータが提供されていそうだなーと思っていると便利なものがありました。
functools.lru_cache()
この関数は、大雑把に言ってしまうとメモ化をしてくれるようなデコレータになります。公式ドキュメントの説明では、
Decorator to wrap a function with a memoizing callable that saves up to the maxsize most recent calls. It can save time when an expensive or I/O bound function is periodically called with the same arguments.
という風になっています。最近呼ばれた関数の値をメモ化し、キャッシュしておくデコレータになります。例えば次のように使います.
import functools @functools.lru_cache() @track def fibonacci_memonized(n): if n < 2: return n return fibonacci_memonized(n-2) + fibonacci_memonized(n-1)
デコレータがあるおかげでスッキリかけています!本来なら二回関数に包むように書かないといけないですからね、だいぶスマートにかけているのではないでしょうか。比較のためにlru_cacheを使わないようなフィボナッチ関数と比較してみます。次のようなコードを実行してみましょう。
import functools def track(func): def tracked(*args, **kwargs): result = func(*args, **kwargs) name = func.__name__ arg_str = ",".join(repr(arg) for arg in args) print("%s(%s) -> %r" % (name, arg_str, result)) return result return tracked @functools.lru_cache() @track def fibonacci_memonized(n): if n < 2: return n return fibonacci_memonized(n-2) + fibonacci_memonized(n-1) @track def fibonacci_naive(n): if n < 2: return n return fibonacci_naive(n-2) + fibonacci_naive(n-1) print("-"*10, "naive-fibonacci", "-"*10, "\n") fibonacci_naive(6) print("\n") print("-"*10, "memonized-fibonacci", "-"*10, "\n") fibonacci_memonized(6)
これらを実行すると、次のように表示されます。
---------- naive-fibonacci ---------- fibonacci_naive(0) -> 0 fibonacci_naive(1) -> 1 fibonacci_naive(2) -> 1 fibonacci_naive(1) -> 1 fibonacci_naive(0) -> 0 fibonacci_naive(1) -> 1 fibonacci_naive(2) -> 1 fibonacci_naive(3) -> 2 fibonacci_naive(4) -> 3 fibonacci_naive(1) -> 1 fibonacci_naive(0) -> 0 fibonacci_naive(1) -> 1 fibonacci_naive(2) -> 1 fibonacci_naive(3) -> 2 fibonacci_naive(0) -> 0 fibonacci_naive(1) -> 1 fibonacci_naive(2) -> 1 fibonacci_naive(1) -> 1 fibonacci_naive(0) -> 0 fibonacci_naive(1) -> 1 fibonacci_naive(2) -> 1 fibonacci_naive(3) -> 2 fibonacci_naive(4) -> 3 fibonacci_naive(5) -> 5 fibonacci_naive(6) -> 8 ---------- memonized-fibonacci ---------- fibonacci_memonized(0) -> 0 fibonacci_memonized(1) -> 1 fibonacci_memonized(2) -> 1 fibonacci_memonized(3) -> 2 fibonacci_memonized(4) -> 3 fibonacci_memonized(5) -> 5 fibonacci_memonized(6) -> 8
キャッシュがいい感じにされていて呼び出し回数が最小限になっていますね。ちなみにデフォルトだと128個まで保持するようです。引数を指定すれば無制限にキャッシュを保持するようにもできますが、下手な処理でそんなことをするとデータが溢れかえりそうなので気をつけないといけないですね。
おわり
こういうメモ化は結構使う処理なのでこれだけシンプルにかけるととても便利ですね。functoolsには他にも関数を部分適用する関数とかもあって面白そうなので1度目を通してみたいですね。なお、内容としてはfluent pythonを読んでいてこのあたりの話を読んでまとめておこうと思いまとめたものになります。
備考
この記事は(https://qiita.com/Akutagawa/items/dbb035f117c764409281)をそのままコピーして少しいじったものになっていますが、本人です。