Metaflow概要

業務で機械学習のETL処理を行う必要があったので，2019年12月に発表されたNetflix内製ツールのMetaflowを試しに使ってみました．色々と知見が得られたので，詳細な概要記事を書こうと思います．

# はじめに
MetaflowはNetflixがデータサイエンティストの生産性を高めるために作成した内製のワークフローツールです．
バージョン管理やデータウェアハウスなどのインフラについてはあまり考えずにモデルやビジネスロジックの構築に集中できるようにすることを目的に作られたそうです．検証段階からMetaflowで開発することで商用への移行が楽になります．
これまでNetflixでは，約2年前からこの内製ツールを使用してきたらしいのですが，2019年12月にOSS化されました．米国一の高所得エンジニア達が生産性を高めるために使っていたツールなので気になりますよね．
本記事では，[公式ドキュメント](ref=https://docs.metaflow.org/)に記載されている内容をかいつまんで詳細に解説しました．
本記事を読むだけでざっくりと使えるようになることが目標です．

# インストール
```
pip install metaflow
```
Metaflowのライブラリ管理機能はcondaが必要なので，conda環境推奨です．

# 基本的な概念（Flow, Run, Step）
## Flow
定義された一連のワークフローをFlowと呼びます．（そのままですね．）

## Run
Flowの各実行のことをRunと呼びます．各Runはバージョン管理されており，各Runにおけるメタデータは自動的に保存されます．デバッグや実行結果の確認を行う際は対象のRunを指定してそのメタデータにアクセスします．

## Step
Flowを定義する処理のブロックをStepと呼びます．処理全体を意味のあるブロックに分割し，Stepとして記述しましょう．Step内のインスタンス変数はメタデータと共に保存されます．通常，デバッグの際などに確認するのはStep内のデータとなります．

# サンプルコードによる解説

例えば，下記のような機械学習パイプラインを作成するとします．
<img width="1029" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228793/0d639ef8-21e4-ea72-b6f4-1b1d231a88a0.png">

これを実現するコードの大枠は下記のようになります．

```sample_flow.py

from metaflow import FlowSpec, step

class  SampleFlow(FlowSpec):

    @step
    def start(self):
        self.next(self.preprocessing)

    @step
    def preprocessing(self):
        # 処理内容を記述
        self.next(self.model_training)

    @step
    def model_training(self):
        # 処理内容を記述
        self.next(self.evaluation)

    @step
    def evaluation(self):
        # 処理内容を記述
        self.next(self.end)

    @step
    def end(self):
        pass

if __name__ == "__main__":
    SampleFlow()

```

- 記述したいワークフローをFlowSpecを継承したクラス内に記述していきます．
- それぞれの処理は``@step``以下に記述します．
- FlowにはstartとendのStepが必要です．
- 各Stepの終わりにself.next()で次の処理を指定します．  

簡単ですね．それでは，実際に処理内容を記述したものを以下に記します．
有名なTitanicデータを用いてモデルを作成し，TestデータのAccuracyとモデルのパラメータを表示します．


```titanic_classification.py
from metaflow import FlowSpec, step
import numpy as np
import pandas as pd
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score


class TitanicClassification(FlowSpec):

    @step
    def start(self):
        self.next(self.preprocessing)

    @step
    def preprocessing(self):
        # Titanicデータの読み込み
        data = pd.read_csv('data/train.csv')
        # LabelEncodingや欠損値の補完などの前処理
        data['Sex'].replace(['male','female'], [0, 1], inplace=True)
        data['Embarked'].fillna(('S'), inplace=True)
        data['Embarked'] = data['Embarked'].map( {'S': 0, 'C': 1, 'Q': 2} ).astype(int)
        data['Fare'].fillna(np.mean(data['Fare']), inplace=True)
        data['Age'].fillna(np.mean(data['Age']), inplace=True)
        # 必要なさそうなカラムの削除
        delete_columns = ['Name', 'PassengerId', 'SibSp', 'Parch', 'Ticket', 'Cabin']
        data.drop(delete_columns, axis=1, inplace=True)
        # train,test分割
        self.X_train, self.X_validation, self.y_train, self.y_validation = \
        train_test_split(data.drop('Survived', axis = 1), data['Survived'], train_size=0.8, random_state=0)
        self.next(self.model_training)

    @step
    def model_training(self):
        # max_depth, n_estimators, subsampleについてはパラメータで指定した値を設定
        self.params = {"objective":"binary:logistic",'learning_rate': 0.1, \
            'max_depth': 6, 'n_estimators': 100, "subsample": 0.8}
        self.model = xgb.XGBClassifier(**self.params)
        self.model.fit(self.X_train, self.y_train)        
        self.next(self.evaluation)

    @step
    def evaluation(self):
        self.y_pred = self.model.predict(self.X_validation)
        self.accuracy = accuracy_score(self.y_validation, self.y_pred)
        print('Accuracy : %f' % self.accuracy)
        print('Parameter : %s' % self.params)
        self.next(self.end)

    @step
    def end(self):
        pass

if __name__ == "__main__":
    TitanicClassification()
```

## 実行コマンド (Show, Run, Resume)
次に作成したFlowを実行するために実行コマンドについて説明します．
Flowが記述されたファイルを実行する際，ファイル名の後にshow, run, resumeのいずれかを指定して実行します．
``python ファイル名 [show|run|resume]``
### Show
DAG (Stepの推移)を表示します．結構，見づらいですね... DAGはGUIで見たいところですが現在その機能はありません．

```TREMINAL
$ python titanic_classification.py show
Metaflow 2.0.3 executing TitanicClassification for user:kenji_yabuki



Step start
    ?
    => preprocessing

Step preprocessing
    ?
    => model_training

Step model_training
    ?
    => evaluation

Step evaluation
    ?
    => end

Step end
    ?
```

### Run
Flowを最初から順に実行します．

```
$ python titanic_classification.py run
Metaflow 2.0.3 executing TitanicClassification for user:kenji_yabuki
Validating your flow...
    The graph looks good!
Running pylint...
    Pylint is happy!
2020-03-12 08:41:10.142 Workflow starting (run-id 1583970070135480):
2020-03-12 08:41:10.151 [1583970070135480/start/1 (pid 55394)] Task is starting.
2020-03-12 08:41:11.821 [1583970070135480/start/1 (pid 55394)] Task finished successfully.
2020-03-12 08:41:11.829 [1583970070135480/preprocessing/2 (pid 55399)] Task is starting.
2020-03-12 08:41:13.704 [1583970070135480/preprocessing/2 (pid 55399)] Task finished successfully.
2020-03-12 08:41:13.710 [1583970070135480/model_training/3 (pid 55404)] Task is starting.
2020-03-12 08:41:15.411 [1583970070135480/model_training/3 (pid 55404)] Task finished successfully.
2020-03-12 08:41:15.422 [1583970070135480/evaluation/4 (pid 55409)] Task is starting.
2020-03-12 08:41:17.473 [1583970070135480/evaluation/4 (pid 55409)] Accuracy : 0.837989
2020-03-12 08:41:17.723 [1583970070135480/evaluation/4 (pid 55409)] Parameter : {'objective': 'binary:logistic', 'learning_rate': 0.1, 'max_depth': 6, 'n_estimators': 100, 'subsample': 0.8}
2020-03-12 08:41:17.726 [1583970070135480/evaluation/4 (pid 55409)] Task finished successfully.
2020-03-12 08:41:17.736 [1583970070135480/end/5 (pid 55414)] Task is starting.
2020-03-12 08:41:19.783 [1583970070135480/end/5 (pid 55414)] Task finished successfully.
2020-03-12 08:41:19.784 Done!
```
### Resume
過去のRunの任意のStepから処理を再開します．このとき，再開するStep以前までの実行結果は過去の実行結果を再利用するため，素早く処理を再開できます．(=再開するStepまでの処理はきちんと実行されていなければなりません)
RunやStepを指定しなければ，直近のRunの処理が失敗したところから再開されます．処理が途中でこけたときのデバッグや途中のStepでコードやパラメータを修正した際などに使用することが多いです．詳細な使い方は[こちら](https://docs.metaflow.org/metaflow/debugging)をご確認ください．
先ほどの例でn_estimateorsを100 -> 50 に修正し，Model Trainingから再開した結果がこちらです．

```TERMINAL
$ python titanic_classification.py resume model_training
Metaflow 2.0.3 executing TitanicClassification for user:kenji_yabuki
Validating your flow...
    The graph looks good!
Running pylint...
    Pylint is happy!
2020-03-12 08:50:08.663 Gathering required information to resume run (this may take a bit of time)...
2020-03-12 08:50:08.672 Workflow starting (run-id 1583970608661883):
2020-03-12 08:50:08.675 [1583970608661883/start/1] Cloning results of a previously run task 1583970070135480/start/1
2020-03-12 08:50:10.587 [1583970608661883/preprocessing/2] Cloning results of a previously run task 1583970070135480/preprocessing/2
2020-03-12 08:50:12.258 [1583970608661883/model_training/3 (pid 55569)] Task is starting.
2020-03-12 08:50:14.027 [1583970608661883/model_training/3 (pid 55569)] Task finished successfully.
2020-03-12 08:50:14.033 [1583970608661883/evaluation/4 (pid 55574)] Task is starting.
2020-03-12 08:50:15.622 [1583970608661883/evaluation/4 (pid 55574)] Accuracy : 0.865922
2020-03-12 08:50:15.842 [1583970608661883/evaluation/4 (pid 55574)] Parameter : {'objective': 'binary:logistic', 'learning_rate': 0.1, 'max_depth': 6, 'n_estimators': 50, 'subsample': 0.8}
2020-03-12 08:50:15.845 [1583970608661883/evaluation/4 (pid 55574)] Task finished successfully.
2020-03-12 08:50:15.855 [1583970608661883/end/5 (pid 55579)] Task is starting.
2020-03-12 08:50:17.582 [1583970608661883/end/5 (pid 55579)] Task finished successfully.
2020-03-12 08:50:17.583 Done!
```
preprocessingまでは結果がcloneされ，Model Trainingから処理が再開し，Accuracy, Parameterが書き換わっていることが分かります．

## 3種類の構文 (Linear, Branch, Foreach)
Metaflowの処理では，①Linear ②Branch ③Foreach の３種類の構文が使えます．

### ①Linear
順番にStepをつなげていくだけの処理をLinearと呼びます．先ほどの例はLinearですね．

### ②Branch
次のStepを指定する際に，self.next()で複数のStepを指定すると処理が分岐し，並列処理が行われます．分岐を行った際は必ずJoin Stepが必要です．Join Stepでは，inputs引数を与えることで，分岐した各Stepの変数を受け取ります．（また，inputs引数の有無でJoin Stepかどうか判定されます）
下図は先ほどの例でモデルを学習する際に，XGBoostとRandomForestをそれぞれ並列でモデルを作成し，一番精度の良いモデルの結果を表示する例です．
<img width="797" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228793/6bd4b815-31e8-37cf-5dc4-88332c3443cd.png">

```model_selection.py
from metaflow import FlowSpec, step
import os
import numpy as np
import pandas as pd
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.ensemble import RandomForestClassifier

class ModelSelection(FlowSpec):

    @step
    def start(self):
        self.next(self.feature_engineering)
    
    @step
    def feature_engineering(self):
        # 省略
        self.next(self.xgb_fit, self.rf_fit)

    @step
    def xgb_fit(self):
        self.model = xgb.XGBClassifier(random_state=0)
        self.model.fit(self.X_train, self.y_train)
        self.y_pred = self.model.predict(self.X_validation)
        self.score = accuracy_score(self.y_validation, self.y_pred)
        self.next(self.evaluate_model)
    
    @step
    def rf_fit(self):
        self.model = RandomForestClassifier(random_state=0)
        self.model.fit(self.X_train, self.y_train)
        self.y_pred = self.model.predict(self.X_validation)
        self.score = accuracy_score(self.y_validation, self.y_pred)
        self.next(self.evaluation)

    @step
    def evaluation(self, inputs):
        self.best = max((i.score, i.model) for i in inputs)
        print('Accuracy : %f' % self.best[0])
        print('Best Model : %s' % self.best[1])
        self.next(self.end)

    @step
    def end(self):
        pass

if __name__ == "__main__":
    ModelSelection()
```

```TERMINAL
$ python model_selection.py run
Metaflow 2.0.3 executing ModelSelection for user:kenji_yabuki
Validating your flow...
    The graph looks good!
Running pylint...
    Pylint is happy!
2020-03-12 09:44:29.393 Workflow starting (run-id 1583973869387632):
~~省略~~
2020-03-12 09:44:35.999 [1583973869387632/evaluation/5 (pid 55734)] Task is starting.
2020-03-12 09:44:37.865 [1583973869387632/evaluation/5 (pid 55734)] Accuracy : 0.854749
2020-03-12 09:44:38.020 [1583973869387632/evaluation/5 (pid 55734)] Best Model : RandomForestClassifier(bootstrap=True, ccp_alpha=0.0, class_weight=None,
2020-03-12 09:44:38.020 [1583973869387632/evaluation/5 (pid 55734)] criterion='gini', max_depth=None, max_features='auto',
2020-03-12 09:44:38.020 [1583973869387632/evaluation/5 (pid 55734)] max_leaf_nodes=None, max_samples=None,
2020-03-12 09:44:38.020 [1583973869387632/evaluation/5 (pid 55734)] min_impurity_decrease=0.0, min_impurity_split=None,
2020-03-12 09:44:38.020 [1583973869387632/evaluation/5 (pid 55734)] min_samples_leaf=1, min_samples_split=2,
2020-03-12 09:44:38.020 [1583973869387632/evaluation/5 (pid 55734)] min_weight_fraction_leaf=0.0, n_estimators=100,
2020-03-12 09:44:38.020 [1583973869387632/evaluation/5 (pid 55734)] n_jobs=None, oob_score=False, random_state=0, verbose=0,
2020-03-12 09:44:38.020 [1583973869387632/evaluation/5 (pid 55734)] warm_start=False)
2020-03-12 09:44:38.023 [1583973869387632/evaluation/5 (pid 55734)] Task finished successfully.
2020-03-12 09:44:38.034 [1583973869387632/end/6 (pid 55739)] Task is starting.
2020-03-12 09:44:40.339 [1583973869387632/end/6 (pid 55739)] Task finished successfully.
2020-03-12 09:44:40.339 Done!
```


### ③Foreach

Branchでは，各分岐を手動で定義しましたが，分岐を入力によって動的に変えたい場合はForeachを使います．
使い方は，分岐先のStepを指定する際に``foreach=分岐対象のパラメータのList (or Tupple)``を与えるだけです．Join StepについてはBranchと同様です．
先ほどのtitanic_classification.py でmax_depth, n_estimators, subsampleのパラメータでGrid Searchを行い，一番精度の高いパラメータの結果を表示する例を示します．
<img width="774" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228793/854f59c1-b708-7512-cf38-2f4cd296014d.png">

```grid_search_xgb.py
from metaflow import FlowSpec, step
import os
import itertools
import numpy as np
import pandas as pd
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score


class GridSearchXGB(FlowSpec):

    @step
    def start(self):
        self.next(self.feature_engineering)

    @step
    def feature_engineering(self):
        # 省略
        self.next(self.param_setting)

    @step
    def param_setting(self):
        max_depth_params = [2,4,6]
        n_est_params = [50,100,150]
        subsample_params = [0.4,0.6,0.8]
        self.target_params = list(itertools.product(max_depth_params, n_est_params, subsample_params))
        self.next(self.train_model, foreach='target_params')

    @step
    def train_model(self):
        self.params = {"objective":"binary:logistic",'learning_rate': 0.1, \
            'max_depth': self.input[0], 'n_estimators': self.input[1], "subsample": self.input[2]}
        self.model = xgb.XGBClassifier(**self.params)
        self.model.fit(self.X_train, self.y_train)
        self.y_pred = self.model.predict(self.X_validation)
        self.score = accuracy_score(self.y_validation, self.y_pred)
        self.next(self.evaluation)

    @step
    def evaluation(self, inputs):
        best_index = np.argmax([i.score for i in inputs])
        self.best_accuracy = inputs[best_index].score
        self.best_params = inputs[best_index].params
        print('Accuracy : %f' % self.best_accuracy)
        print('max_depth : %s' % self.best_params['max_depth'])
        print('n_estimators : %s' % self.best_params['n_estimators'])        
        print('subsample : %s' % self.best_params['subsample'])        
        self.next(self.end)

    @step
    def end(self):
        pass

if __name__ == "__main__":
    GridSearchXGB()
```

```
$ python grid_search_xgb.py run
Metaflow 2.0.3 executing GridSearchXGB for user:kenji_yabuki
Validating your flow...
    The graph looks good!
Running pylint...
    Pylint is happy!
2020-03-12 09:46:19.875 Workflow starting (run-id 1583973979869090):
2020-03-12 09:46:19.880 [1583973979869090/start/1 (pid 55789)] Task is starting.
2020-03-12 09:46:21.776 [1583973979869090/start/1 (pid 55789)] Task finished successfully.
2020-03-12 09:46:21.785 [1583973979869090/feature_engineering/2 (pid 55794)] Task is starting.
2020-03-12 09:46:23.972 [1583973979869090/feature_engineering/2 (pid 55794)] Task finished successfully.
2020-03-12 09:46:23.987 [1583973979869090/param_setting/3 (pid 55799)] Task is starting.
2020-03-12 09:46:25.942 [1583973979869090/param_setting/3 (pid 55799)] Foreach yields 27 child steps.
2020-03-12 09:46:25.942 [1583973979869090/param_setting/3 (pid 55799)] Task finished successfully.
2020-03-12 09:46:25.954 [1583973979869090/train_model/4 (pid 55804)] Task is starting.
2020-03-12 09:46:25.970 [1583973979869090/train_model/5 (pid 55805)] Task is starting.
~~省略~~
2020-03-12 09:46:48.751 [1583973979869090/evaluation/31 (pid 55939)] Task is starting.
2020-03-12 09:46:50.788 [1583973979869090/evaluation/31 (pid 55939)] Accuracy : 0.871508
2020-03-12 09:46:50.932 [1583973979869090/evaluation/31 (pid 55939)] max_depth : 4
2020-03-12 09:46:50.932 [1583973979869090/evaluation/31 (pid 55939)] n_estimators : 100
2020-03-12 09:46:50.932 [1583973979869090/evaluation/31 (pid 55939)] subsample : 0.6
2020-03-12 09:46:50.935 [1583973979869090/evaluation/31 (pid 55939)] Task finished successfully.
2020-03-12 09:46:50.943 [1583973979869090/end/32 (pid 55944)] Task is starting.
2020-03-12 09:46:52.685 [1583973979869090/end/32 (pid 55944)] Task finished successfully.
2020-03-12 09:46:52.686 Done!
```

3種類のパラメータで3つずつの値を試しているので，27個の分岐が発生していることが分かります．


# Metaflowの特徴的な機能

Metaflowの特徴的な機能をいくつかPick upして説明します．

## バージョン管理とメタデータの自動保存
Flowが実行される度に各Runはバージョン管理されます．保存されるデータはRunそのものについてのメタデータ(実行時刻や実行環境のバージョンなど)や各Stepのメタデータ, インスタンス変数などがあります．
 ※ インスタンス変数 (e.g. self.x)は保存されますが，通常の変数(e.g. x)は保存されません．

あるRunのデータを確認したい場合はIPythonなどを利用して簡単に確認できます．
例えば，先ほどのBranchの例で2つのモデルの精度を比較しRandom Forestの方が良い結果となりましたが，確認のためXGBoostのAccuracyを以下のようにして調べます．

```TERMINAL
$ ipython
Python 3.6.9 |Anaconda, Inc.| (default, Jul 30 2019, 13:42:17) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.10.2 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from metaflow import Flow, Run, Step                                                                                                

In [2]: run = Flow('ModelSelection').latest_run # ModelSelection Flowの直近のRunを取得                                                      

In [3]: list(run) # RunのStep一覧を確認                                                                                                     
Out[3]: 
[Step('ModelSelection/1583973869387632/end'),
 Step('ModelSelection/1583973869387632/evaluation'),
 Step('ModelSelection/1583973869387632/xgb_fit'),
 Step('ModelSelection/1583973869387632/rf_fit'),
 Step('ModelSelection/1583973869387632/feature_engineering'),
 Step('ModelSelection/1583973869387632/start')]

In [4]: Step('ModelSelection/1583973869387632/xgb_fit').task.data # xgb_fitで参照できるデータ一覧                                           
Out[4]: <MetaflowData: model, name, y_train, score, y_validation, X_train, y_pred, X_validation>

In [5]: Step('ModelSelection/1583973869387632/xgb_fit').task.data.score # XGBoostモデルのAccuracy                                           
Out[5]: 0.8435754189944135
```

## 耐障害性のための機能
ワークフローツールとして，安定して処理を継続するために，インフラの障害や予期せぬデータが来た際の対策などがあると便利です．Metaflowでは，一過性の障害をRetryで対処したり，予期せぬ処理のループを継続させないためにTimeoutを設ける，などをデコレータを用いて簡単に実現できます．これらのデコレータの使い方は後に紹介します．
詳細については[こちら](https://docs.metaflow.org/metaflow/failures)をご参照ください．

## Cloud(AWS)との連携
MetaflowはAWSと密に連携して開発されました．現在はAWSとの連携機能として，S3へのデータアクセス（メタデータの保存など）やAWS Batchを使用した処理があります．大規模な入力データを保存するとなるとS3との連携は嬉しいですね．また，AWS Batchを使用して，重たい処理だけAWS Batchで処理し，その他の処理はローカルで行うといったことも可能です．各Stepでリソースの制御も簡単にできるのでCloudとの連携がしやすいです．
また，[Github](https://github.com/Netflix/metaflow)のIssuesやPull requestsを見てると，GCPとの連携機能の開発も進められていそうです．

## 各種デコレータ
Metaflowには，ワークフローに使える便利なデコレータがいくつか用意されています．
使用する場合は各　各Stepの``@Step``の前にこれから説明するデコレータを付け足してください．

### ``@resources``
resourcesデコレータは各StepでのメモリやCPU, GPUなどのリソースの割り当てを行うことができます．
例えば，memory:6GB, CPU:1 を割り当てる場合，下記のようになります．


```
@resources(memory=60000, cpu=1)
```

### ``@batch``
AWS Batchで処理したいStepにはbatchデコレータを使います．

```
@batch(memory=60000, cpu=1)
```
### ``@retry``
一時的な障害などで処理が中断したときに，中断したStepの最初から再開するようにする場合はretryデコレータを用います．
デフォルトでは，2分間隔で3回までretryを行います．
retry回数と間隔を変更したい場合は下記のように引数を渡します．(retry回数と間隔の最大値はそれぞれ4回と20分です)

```
@retry(times=4, minutes_between_retries=20)
```

基本的にクラウド上でMetaflowを使う場合は全てのStepでリトライを使用することが望ましいですが，全てのStepに``@retry``を追加するのは手間です．実行時に``--with retry``オプションを与えることで全てのStepにリトライ機能が追加されます．

### ``@catch``
デフォルトでは，MetaflowはどこかのStepでエラーが起こると全体のフローが中止されます．しかし，catchデコレータを使うことで，そのStepで処理が失敗したとしても，後続のStepへと処理が続きます．
もちろん，エラーが起きたまま処理が進むので，後続のStepで何らかのエラーハンドリングを記述する必要があります．
exceptionをmetadataとして保存するには，下記のようにvar引数を与える必要があります．

```
@catch(var=hoge)
```

また，retryと併用することで，上限までリトライしても成功しなかったときにcatchを行うことできます．

### ``@timeout``
予期せぬループにはまったり，処理に時間がかかり過ぎる際に処理を中断するにはtimeoutを用います．
下記は5分30秒経過したらタイムアウトを行う例です．引数には，seconds, minutes, hoursを指定できます．

```
@timeout(minutes=5, seconds=30)
```
また，catchと併用することで，timeout後の処理を継続させることができます．

## ``@conda``
異なる環境での実行や再現性を担保する上で，ライブラリ管理は重要です．Metaflowでは，``@conda``デコレータを使うことで，各Step毎に使用するライブラリとバージョンを指定できます．なので，Step毎に同じライブラリでも違うバージョンを指定することが可能です．
例

```
@conda(libraries={'numpy':'1.16.3'})
@step
def foo(self):
   ...

@conda(libraries={'numpy':'1.15.4'})
@step
def bar(self):
    ...

```
``@conda``デコレータを使用する場合は，実行時に``--environment=conda``オプションが必要です．
また，``@conda_base``デコレータを使用することで，Flow全体で使用するベースのライブラリとPythonを指定できます．

```
@conda_base(libraries={'numpy':'1.15.4'}, python='3.6.5')
class Hoge(FlowSpec):
   ... 
```

# 所感
使ってみての感想ですが，とにかく手軽に処理を記述でき，様々なデコレータを駆使して簡単にインフラ周りの課題に対処できるのが良いと感じました．GUIでの操作ができない，DAG schedulerの機能がないなど他のリッチなワークフローツールに比べると機能が少ない点もありますが，シンプルで使いやすいので個人的には良いんじゃないかと思います．また，AWSとの連携が強力なので，AWS環境で運用している会社であれば開発段階からMetaflowを用いて進めることで本番移行が容易にできそうです．
逆に，それ以外の環境であったりInternet Reachability がない場合は，スケーリングやcondaでのライブラリ管理の恩恵を受けづらい（あるいは，受けれない）のであまりメリットがないかもしれません．
また，条件分岐付きのDAGを定義できるとさらに良いなと思いました．例えば，ある条件のときに後方のStepまでSkipするということが今のところできません．（他のツールは出来るんですかね？）ここが改善されるとさらに使い勝手が良くなると感じました．


# 終わりに
Metaflowについて詳細な概要を書きました．
OSS版のMetaflowでは，R言語のサポートや大規模データのインメモリ処理など，公開されていない機能もあるそうですが，要望が多ければこれらの機能も公開するみたいです．皆さんも使ってみてどんどん要望を上げていきましょう！