でーたさいえんすって何それ食えるの?

JuliaとかRとかPythonとかと戯れていたい

R版metaflowの使い方

2019年12月にpython版が公開され、2020年8月にR版が公開されたので早速チュートリアルを参考にしながら使ってみてポイントをメモとして残しておく。

触ってみた所感は、DS人材のようにプロダクションReadyなソースをつくることに比重を置いていない人に対しても扱えそうで良さそう。ただし、このフレームワークだけでプロダクションReadyな状態に仕上げることは難しそう。なので、mlflowのような足りない部分を補うライブラリと併用する必要はありそう。とは言っても、metaflowで管理されているだけである程度安定して運用できそうではあるので、PoCでしばらく運用してみるという段階であればmetaflowだけの利用でも事足りるのではないかな、と感じた。

metaflowとは

NETFLIXが開発したMLワークフロー管理用フレームワーク
類似したものはAirflow、Luigi、kedro、RではrecipesやDrakeといったものがあるが、これらと比べて機能責務が狭く利用できるようになるまでの学習コストは低い印象。

metaflowを使うことで
- 処理の実行
- 実行開始と終了といった実行ステータスの管理
- 処理の並列化
- エラーハンドリング
といった機能を簡単に実装することができる。
更に、NETFLIXではawsでへデプロイしていたのかawsと連携したスケールアップ/アウトを簡単に行うための仕組みも含まれている。

もともとはPython用モジュールで、これをRから呼び出せるようにラップしたものがR版のmetaflowとなっている。
なので、R版metaflowの実行にはPythonの実行環境も必須となる。

基本的な使い方・考え方

関数で処理を定義し、第一引数で指定したオブジェクトを次の処理に引き渡して処理をつなぐイメージ。
この各処理のことをstepとよび、stepの開始から終了までをflowと呼んでいる。
チュートリアルに従うとselfとされていて、Pythonでクラスを定義してメソッドを利用しているような感覚。
この処理を連結させる部分はtidyverseの考え方と親和性が高く、パイプで処理ステップを連結させることが当たり前に想定されている。

例えば、チュートリアルのフローをそのまま掲載すると次のように書くことになる。
なので、Global envに処理がベタっと書かれたソースから必然的にモジュール化されたソースになるような仕組みとなっており、ML系のソースを運用のために整えるには都合が良さそう。

#  A flow where Metaflow prints 'Hi'.
#  Run this flow to validate that Metaflow is installed correctly.

library(metaflow)

# This is the 'start' step. All flows must have a step named 
# 'start' that is the first step in the flow.
start <- function(self){
    print("HelloFlow is starting.")
}

# A step for metaflow to introduce itself.
hello <- function(self){
    print("Metaflow says: Hi!") 
}

# This is the 'end' step. All flows must have an 'end' step, 
# which is the last step in the flow.
end <- function(self){
     print("HelloFlow is all done.")
}

metaflow("HelloFlow") %>%
    step(step = "start", r_function = start, next_step = "hello") %>%
    step(step = "hello", r_function = hello,  next_step = "end") %>%
    step(step = "end", r_function = end) %>% 
    run()

stepには空のstepを入れることも可能で、step(step = "end")のようにr_functionを指定しなくても良い。
処理完了や、ある程度の塊の処理が終わったところを通知させるためにログを吐かせるのに活用できそう。

最後のstepでselfに引き渡しておいたオブジェクトがすべて結果としてartifactとして保存される。 例えば最終stepで

self$result1 <- 'foo'
self$result2 <- c(1, 2, 3)

みたいにもたせておけば、このフローの実行結果としてresult1とresult2がartifactとして.metaflow内に格納される。
これは再利用が可能な上に、後で紹介するように別なフローから呼び出して再利用したりもできる。

各flowの実行結果はmetadataが保存される。 プロジェクトのルートディレクトリに.metaflowというディレクトリが作成されて、その下に実行したflowのデータが保持される(これをartifactと呼ぶ)。具体的にHelloFlowというフローを実行すると.metaflow/HelloFlow という感じで作られる。
このディレクトリの中にある各フローのartifactに対して実行毎にメタデータが保存され、その中間生成オブジェクトの情報も保存される。 これはmlflowのartifactをローカルファイルシステムとして保持させている状態と同じような状態で、メタデータjson形式で持っている。なので万が一スクリプト側でどう扱っていいか困ったときには.metaflowの中に実際に入ってデータを覗いてみることもできそう。

使い方のポイント

実行

フローを定義したソースをターミナルからRscriptで実行するかRStudioのjobsとして流すことで実行できる。
※RStudioでCtrl+Sで実行しても良いが、namespaceが切り替わってしまい実行後にGlobal Environmentにもどすのが面倒なのであまり積極的には使わない方が良さそう。

ターミナルからの実行時に与えるパラメータで実行方法が変えられる

  • run:通常の実行
    • run --with retry:stepの再実行を有効にしてフローを実行
  • resume:失敗したところから再実行
    • resume --origin-run-id 12345:特定の実行IDに対して再実行(この例だとIDが12345)
    • resume Step001:特定のステップから再実行(この例だとステップ名がStep001のところから再開)

例えばリトライを有効にして通常実行するならば、次のような感じで実行することになる。

Rscript flow01.R run --with retry

ジョブのパラメータ化

parameter()で実行パラメータを定義することができる。
さらに、ここでパラメータ化したものはソースに追記なしでターミナルからの実行パラメータオプションとしても利用できるようになる

metaflow("test_flow") %>%
  parameter("genre") %%
  step(step = "start", r_function = proc1, next_step = "end") %>% 
  step(step = "end") %>%
  run()

のように定義されているとすると、genreがパラメータとなっておりコンソールから実行するときは次のようにパラメータを与えることができるようになる。

Rscript flow02.R run -genre 'Sci-Fi'

stepの分岐

stepを定義するときにnext_stepを複数指定することでstepを分岐させることができ、 step(..., join = TRUE)としたstepで結果を纏めることができる。
joinするせずにstepを繋げれば、joinまで並列処理されることになる。各ステップが独立したRのプロセスとして処理されるので、Rでの並列処理のコードを書かずに簡単に並列化できるのが便利。
考え方としては、map/reduceを扱うようなイメージ。

stepの並列化

Rのforech、またはPythonのjoblibのような感じでグループごとに処理を並列処理がmetaflowの仕組みで実現が可能。
次のstepにわたすときにforechオプションを指定するだけで並列化ができる。
iterateする各要素はself$inputとして次のstepで実行する関数で取り出せる。
結果をまとめるための関数には、前の処理結果を受け取るために2つ目の引数を用意しておくのが実装としてやりやすいっぽい。 具体的には次のように関数を用意しておくのが良さそう。

f1 <- function(self, inputs){
    # some process 
}

joinのstepでは、2つ目の引数にiterateした各結果がリストとして入っているので関数内で取り出して統合する必要がある。
ここが、考え方が面倒だけど入れ子構造のリストになっているのでpurrrを活用して書くと良さそう。

例えば、おなじみirisデータについてSpecies毎にSepal.Lengthの平均を計算して、結果をまとめてから1つのvectorとしてprintする処理を考えると、次のように書くことになる。

library(metaflow)

s <- function(self){
  self$df <- iris
  self$i <- unique(iris$Species)
}

p1 <- function(self){
  library(magrittr)
  library(dplyr)
  tmp_df <- self$df %>%
    filter(Species == self$i)
  self$mean_SepalLength <- mean(tmp_df$Sepal.Length)
}

p2 <- function(self, inputs){
  library(magrittr)
  purrr::map(inputs, function(x)x$mean_SepalLength) %>% unlist %>% print
}

metaflow("test02") %>%
  step(step = "start", r_function = s, next_step = "proc1", foreach = "i") %>%
  step(step = "proc1", r_function = p1, next_step = "join") %>%
  step(step = "join", r_function = p2, next_step = "end", join = TRUE) %>%
  step(step = 'end') %>% 
  run()

フローの再実行

実行が失敗した場合に対して頑健になるようcatch/retryの仕組みが用意されている。
catch/retryを追加するにはstepにdecoratorとして渡せば良く、decoratorは複数渡すことが可能。

  • retry:decorator('retry') で再実行するようになる。再実行方法のオプションは次のものがある。
    • times:再実行回数
    • minutes_between_retries:再実行までの待機時間

エラー発生時の処理を定義するには、さらにdecorator('catch')を追加する。
catchのdecoratorにはvarが指定でき、varで指定した名前のスロットにエラーが入る。正常終了していれば、このスロットはNULLとなっている。
具体例として前にあった並列処理の例にdecoratorを追加して次のようにすると、joinするstepで呼ばれる関数p2のinputsにはvarで指定したcompute_failedの値がエラー発生時に入る。

library(metaflow)

s <- function(self){
  self$df <- iris
  self$i <- unique(iris$Species)
}

p1 <- function(self){
  library(magrittr)
  library(dplyr)
  tmp_df <- self$df %>%
    filter(Species == self$i)
  self$mean_SepalLength <- mean(tmp_df$Sepal.Length)
}

p2 <- function(self, inputs){
  library(magrittr)
  purrr::map(inputs, function(x)x$mean_SepalLength) %>% unlist %>% print
}

metaflow("test02") %>%
  step(step = "start", r_function = s, next_step = "proc1", foreach = "i") %>%
  step(step = "proc1", 
  decorator("catch", var="compute_failed"),
  r_function = p1, next_step = "join") %>%
  step(step = "join", r_function = p2, next_step = "end", join = TRUE) %>%
  step(step = 'end') %>% 
  run()

ここではエラーが発生しないコードだが仮にp1でエラーが発生していたとすると、p2の引数inputsにエラーオブジェクトが含まれていることになり、次のようにis.null()でエラーが起きていたかどうか確認ができる。

p2 <- function(self, inputs){
  for(input in inputs){
    if(!is.null(input$compute_failed)){
      print("Exception happended!!")
    }
  }
  library(magrittr)
  purrr::map(inputs, function(x)x$mean_SepalLength) %>% unlist %>% print
}

更にdecorator('timeout')を追加することで処理がハングアップしていると判定してプロセスを終了させるまでのタイムアウトまでの時間を指定できる。

フロー結果の再利用

既に実行済みのflowはあとで再利用が可能。また、別なflowで利用することも可能。 例えばtestflow01というflowで結果はresult01となっているflowを実行済みだとすると、次のようなコードで実行済みの結果を再利用することができる。

flow <- flow_client$new("testflow01")
run <- run_client$new(flow, flow$latest_successful_run)
run$artifact("result01")

使う上での注意点

  • 2020年8月時点ではWindowsネイティブサポートが出ていない
  • 他ライブラリとちがい実行stepのDAGを可視化するライブラリはまだ用意されていないらしい。
  • 各ステップ毎に環境が独立しているから、ライブラリや自作関数はそれぞれロードし直す必要がある

  • AICを使った変数選択をするstep関数とかぶってしまうので、名前空間を明示的に書くかconflictedを利用すると良さそう

  • step名にスペースは入れるとエラーになってしっまう
  • metaflow('class_name')でつくったclass_nameがRのenvironmentとしてオブジェクトが生まれてしまう
    例えば、helloという関数を定義した状態でmetaflow('hello')とflow名を設定すると、hello関数がenvironmentオブジェクトで上書きされて想定通り動かない。
  • metaflowで処理を実行するとnamespaceが自動的にuser:{username}になってしまう
    →REPLで実行するとenvironmentが意図せず切り替わるのでRStudioだとsuggest機能がうまく働かなくなってしまいちょっと面倒

mecab-pythonモジュールを使わずにpythonからmecabで処理する方法

mecab-pythonのインストールでエラー吐いて、インストール問題解決に時間がかかった。。。 簡単な関数をつくって回避したので備忘録がてら、残しておく

import os
import codecs
import pandas as pd
import subprocess

def extract_words(s, exclude_keys=['記号', '助詞', '助動詞', '数'], genkei=False):
  """
  @type s: str
  @param x: mecabに処理をかけたい文字列
  
  @param exclude_keys: 除外したい品詞を指定. 除外しない場合は空のリストを指定する
  
  @param genkei: 返す単語の活用を原形に戻す場合はTrueにする
  
  @return: MeCabで処理をかけた結果をDataFrameとして返却
  """
  # 一時的にファイルに書き出し
  dirname_tmp = './tmp_process'
  fn_tmp = os.path.join(dirname_tmp, 'mecab_tmp.txt')
  
  fp = codecs.open(fn_tmp, 'w', 'utf8')
  fp.write(s+"\n")
  fp.close()
  
  # 書き出したファイルをMeCabに食わせて結果を拾う
  cmd = 'mecab ' + fn_tmp + ' -b 10000000'
  mecab_result = subprocess.check_output(cmd, shell=True).decode()
   
  # MeCab処理結果をdataframeにするための前処理
  mecab_result = mecab_result.replace('EOS', '')
  mecab_result = mecab_result.split('\n')
  mecab_result = [s.replace('\r', '') for s in mecab_result]
  mecab_result = [s for s in mecab_result if s != '']
  mecab_result = [s.split('\t') for s in mecab_result ]

  # DataFrameに変換
  df_mecab_result = pd.DataFrame( [ [s[0]] + s[1].split(',') for s in mecab_result] )
  # Exclude_keysで指定した品詞を除外
  df_mecab_result = df_mecab_result.loc[~df_mecab_result[1].isin(exclude_keys)]
  df_mecab_result = df_mecab_result.reset_index(drop=True)
  
  # genkeiフラグがTrueのときは7列目にある原形を返すようにする
  if genkei:
    df_words = df_mecab_result[7]
  else:
    df_words = df_mecab_result[0]
  
  for i in range(df_words.shape[0]):
    if df_words[i] == "*":
      df_words[i] = df_mecab_result[0][i]
  
  return(df_words)  

例えば

extract_words("neologdでいろいろやってみたい", exclude_keys=[], genkei=True) 

とやると、品詞で除外はせず動詞の活用は原形にした単語ベクトルが得られて次のようになる。

0    NEologd
12       いろいろ
3         やる
45         みる
6         たい

R 3.4.0 のJITバイトコンパイラってどんくらい早くなるの?

R3.4.0がリリースされましたね。

で、大きな変更点のひとつにJITバイトコンパイラがデフォルトでONになってるとのことです。
これによって、forループやfunctionは特に何もせずともバイトコンパイルが行われて高速化が行われることになります。
※ただしbrowser()が入れられた関数はバイトコンパイル対象外になります。
コンパイルが不要なときは、compiler::enableJIT(0)または環境変数のR_ENABLE_JITを0にすればOKです

他にも色々アップデート情報の詳細はこちら
R: R News

で、RStudioもこのバイトコンパイル機能に対応したアップデートが行われたそうです。

cmpfun()使うとけっこう早くなるんだよなー。。。程度には覚えていたものの 具体的にどんだけ早いの?に対してはよく覚えてないので
このバイトコンパイラでどんだけ早くなったのか? を実際にやって記録を残しておきたいと思います。。

実行環境

MacbookAir macOS Sierra/2GHz Intel Core i7(2core 4thread)/8GB DDR3 Memory

チェック方法

forループを長めに回して、中で変数をインクリメントするだけの処理を走らせます。 この処理を100回走らせ平均を出してみました。 実際のスクリプトは次の通りです。

n_test <- 100
time_diff <- array(n_test)

loop_end <- 10^7
buf_in_loop <- 1

for(j in 1:n_test){
 time_start <- Sys.time()
 for(i in 1:loop_end){
  buf_in_loop <- buf_in_loop + 1
 }
 time_end <- Sys.time()
 time_diff[j] <- time_end - time_start
}

print(mean(time_diff))
hist(time_diff, breaks = seq(0,5,length.out = 30))

結果

R3.3.3 : 3.500181 sec on average
R3.4.0 : 0.366924 sec on average ※R3.4.0でcompiler::enableJIT(0)にして実行するとR3.3.3と同様でした

約10倍早い!

諸条件でここまでパフォーマンスが出ないケース(*1)もありそうですが
デフォでこんだけ高速化されるなら、Rでも躊躇わずループ書いても良さそうですね。

f:id:masato_613:20170425063949p:plain f:id:masato_613:20170425063943p:plain

ついでなのでfor()で1:loop_endとしている所で、 配列を生成してメモリ食っちゃうハズだけど、コンパイラーが有効だとどうなるか?もチェック
これは、profiler()でメモリの最も消費の多かった所を平均しました。

R3.3.3 : 34.27 MB on average
R3.4.0 : 0.058 MB on average
メモリ消費もだいぶ抑えられてて良い感じですね!!
一番メモリを消費してたのは、どちらもfor()の部分でした。

*1 例えば、forの中で走査してくベクトルを生成しないようにwhile()にするなんてハックだと 同じコードで平均 0.88秒くらいになり、倍以上の処理時間がかかってしまいます。 多分、値の代入がインクリメントする変数の分増えるので、そこが原因かな?と想像できます。

tf-seq2seq

what’s this?

google翻訳の中身とほぼ同じやつ

research.googleblog.com

Overview - seq2seq

GitHub - google/seq2seq: A general-purpose encoder-decoder framework for Tensorflow

KeyPoints

  • 翻訳で実際に使用しているのはseq2seq
  • tf-seq2seqはseq2seqのオープンソース
    • TensorFlowで実装
    • google翻訳のモノとまったく同じではない
  • いろいろパラメータを調整して実験できる
    • Encoder/Decoderの深さ
    • attention mechanism
    • RNN cell type
    • beam size
  • カリカリにチューニングしてないが、そこまで処理は重くない
  • 翻訳精度は他手法と同等かそれ以上(BLEUで評価 newstest2015では精度トップ)

memo

ある文章から何らかの文章を生成するタスクなら、翻訳以外にも使いみちがありそう

KeyPerson

  • Eugene Brevdo
  • Melody Guan
  • Lukasz Kaiser
  • Quoc V. Le
  • Thang Luong
  • Chris Olah

Related Papers

Sequence to Sequence Learning with Neural Networks

https://papers.nips.cc/paper/5346-sequence-to-sequence-learning-with-neural-networks.pdf

Author

  • Ilya Sutskever
  • Oriol Vinyals
  • Quoc V. Le

Massive Exploration of Neural Machine Translation Architectures

https://arxiv.org/pdf/1703.03906.pdf

Author

  • Denny Britz
  • Anna Goldie
  • Minh-Thang Luong
  • Quoc Le ※ all from google brain

Neural Machine Translation by Jointly Learning to Align and Translate

[1409.0473] Neural Machine Translation by Jointly Learning to Align and Translate

Author

  • Dzmitry Bahdanau
  • Kyunghyun Cho
  • Yoshua Bengio

Microsoft LightGBMをmacOSで触ってみた

モチベーション

普通なマシン(Macbook Air)で分析してるので、ライトウェイトかつ高速な勾配ブースティング環境って有難いわけですよね。
さらに、kaggleのBOSCHコンペで暫定3位の方のソリューションでもこのLightGBMが使われてたそうなので、
さらに興味が湧いて触ってみたという感じです。

www.kaggle.com

そもそもGBMって何?

GBMとは、Gradient Boosting Machineの略で、勾配ブースティング機のコトです。 機械学習コンペでは精度上げやすいのと、ディープラーニングほどリソースバカ食いしないためかよく使われているそうです。

詳しい事は、私が説明するよりここらへんのリンクを見ると良いです。

smrmkt.hatenablog.jp

要は損失関数の最小化って最適化をしていてて、損失関数を最小化するパラメータを見つける方法は勾配降下法(最急降下法ニュートン法e.t.c.)ってことです。

ここで取上げるLightGBMの他にはRのxgboostやscikit-learnに入ってるGradientBoostingClassifierでも使うことができます。 xgboostに関してはTJOさんのこの記事辺りを見ると良さげです。

tjo.hatenablog.com

で、LightGBMとは?

Microsoftが公開した勾配ブースティング用パッケージです。

githubで公開されており、そこでの説明だと次のような特徴があるそうです。

  • トレーニングが早くて効率的
  • メモリ消費が少ない
  • 精度が高い
  • 並列に学習させられる
  • 大規模なデータも対応可能

インストール

ここがちょっと詰まった…
ざっくり詰まりポイントをあげると2ポイントあって、
1. 私はmacport派なので、homebrewと同居させるのはコンフリクトの恐れもあるしで避けたかった
2. そもそもhomebrew + g++-6でうまくいかなかった
という残念な状況に。。。

で、結局はmacportのgcc6を使ってコンパイルして無事通りました。
gitとcmakeとgcc6のインストールが事前に必要なので、そこはよしなに入れておいてください。

LightGBM自体のインストール方法はgithubのインストール手順をすこーしだけ変えればOKで、
具体的には次のコマンドを打ち込めばOKです。

git clone --recursive https://github.com/Microsoft/LightGBM
cd LightGBM
mkdir build
cd build
cmake -DCMAKE_CXX_COMPILER=g++-mp-6 ..
make -j

5行目だけ、オリジナルのやり方と違います。
macportでgcc6をインストールしている環境だとg++-mp-6があるはずなので、
コンパイラの指定をそいつに変えてやれば無事コンパイルできます。 cmakeでwarningが出て来るかもしれませんが、実行には特に差し支えないので気にせずやっちゃってOKです。

うまくビルドできると、LightGBM直下にlightgbmという実行ファイルが生成されているハズです。

使い方

実行ファイルのlightgbmを実行したいディレクトリにコピーして、
データとトレーニング/スコアリングのやり方を定義したconfファイルをそれぞれ用意し、
lightgbmの実行引数に渡して実行してあげればOKです。

実際にLightGBMのレポジトリをクローンした時にいっしょにオチてくるexamplesでやってみましょう。
どの例も、macbook Airで秒単位で終わるので、サクっと気軽にお試しできます。

このexamplesの中には必要なデータとconfファイルが既に入ってるので、
README.md通りにコマンドを打ち込めばlightGMBを試すことができます。

例えばexamples/binary_classificationで試すとすると、binary_classificationの中にlightgbmをコピーして

./lightgbm config=train.conf

で予測モデルを作成して

./lightgbm config=predict.conf

で予測値を吐き出させる

これだけでOKです!

実際に自分のデータとモデルで実行する場合は、このexamplesにあるconfファイルをテンプレとして編集していけば良さそうです。 データはヘッダ無しのタブ区切りであれば良いみたいです。

詳しくはまた時間をとって探ってみたいなーと思います。

ENJOY!!

Rodeo --RStudioっぽいPython開発環境

motivation

データ分析をする時、Rを使う場合は大抵RStudioを使ってます。
RStudioがあれば、コーディング、コードのお試し実行、ヘルプ参照、プロットの閲覧etc. が1画面でサクサク作業出来て便利なわけです。[1]

私の場合、ある程度コードの塊を書いたら、その部分をコンソールに投げて実行してみて、想定通りっぽい動作をしてたらまた次のコードの塊を書いて… というやり方をしているので、RStudioの環境がとーっても快適なわけなんです。

ただ、全ての事をRだけで行うなんてのは当然無理でして、その時はお気楽に組める言語 pythonを使いたくなるわけです。
例えば、機械学習をして、その結果をウェブアプリとして使いたいなー ついでにShinyとかShinyDashboardで出来る事より凝った事したいなーって時、先ずはpythonを選んでみたりしてます。

pythonでRStudioライクなIDEがあったらいいなーと前から思いつつ、暇な時に調べてたらどうやらあるので、その紹介をしてみようかなと思います。

[1] ちなみに、今書いている時点で出ているRStudio previewにはソースのアウトライン機能や、add-on、roxygen skeltonの挿入なんて便利機能がいっぱいついてて更に手放せなくなってきてはいます。

Rodeo

RStudioみたいな便利なものがpythonにもあるかなーと思ってたら、ありました!

www.yhat.com

yhatが開発しているRodeoというIDEです。 数日使ってみた感覚として、RStudioとかなり近い使い勝手の環境だと感じました。 ただ、プロセスをターミネートしたり、ヘルプをどうのこうのという部分は、まだまだ発展途上な感じです。

他のpythonIDEとして、JupyterやSpyderもありますが、Rodeoの方がpythonをデータ分析的な使い方をする時には使い勝手が良いと感じますね。

因みにインストール方法は、サイトにアクセスしてインストーラを使ってインストールするか

pip install rodeo

で完了です。

実行時は

rodeo .

で起動させればOK 必ずドットを忘れない様に。