Upgrade to Pro — share decks privately, control downloads, hide ads and more …

テーブルデータの推論結果をS3に出力するpipelineをgokartを使って書く / Wri...

テーブルデータの推論結果をS3に出力するpipelineをgokartを使って書く / Write a pipeline using gokart to output inference results of table data to S3

■イベント :【Sansan×エムスリー】gokartで爆速開発!MLOps勉強会
https://sansan.connpass.com/event/288525/

■登壇概要
タイトル:テーブルデータの推論結果をS3に出力するpipelineをgokartを使って書く
発表者: 技術本部 研究開発部 研究員 齋藤 慎一朗

◉ 研究開発職 採用情報
募集中のポジションや関連記事など
https://media.sansan-engineering.com/

◉ Sansan Tech Blog
Sansanのものづくりを支えるメンバーのテックブログ(R&Dメンバーの連載も多数)

https://buildersbox.corp-sansan.com/

Sansan R&D

July 12, 2023
Tweet

More Decks by Sansan R&D

Other Decks in Technology

Transcript

  1. - 機械学習ワークフローライブラリ - Luigi(Spotifyが開発しているPythonによるパイプラインパッケージ)のラッパー - https://github.com/spotify/luigi - 最小単位は下記 gokartとは class

    ExampleTaskA(gokart.TaskOnKart): param = luigi.Parameter() int_param = luigi.IntParameter(default=2) def run(self): self.dump(f'DONE {self.param}_{self.int_param}') https://github.com/m3dev/gokart/blob/master/examples/gokart_notebook_example.ipynb より引用
  2. - Taskごとの依存関係をrequiresで表現し、再現性を担保 gokartとは class ExampleTaskC(gokart.TaskOnKart): def run(self): self.dump('TASKC') class ExampleTaskD(gokart.TaskOnKart):

    def run(self): self.dump('TASKD') class ExampleTaskB(gokart.TaskOnKart): param = luigi.Parameter() def requires(self): return dict(task_c=ExampleTaskC(), task_d=ExampleTaskD()) def run(self): task_c = self.load('task_c') task_d = self.load('task_d') self.dump(f'DONE {self.param}_{task_c}_{task_d}') https://github.com/m3dev/gokart/blob/master/examples/gokart_notebook_example.ipynb より引用 ExampleTaskC ExampleTaskD ExampleTaskB
  3. - gokart.TaskInstanceParameterを使うとtaskを変数として受け取れる gokartとは class ExampleTaskC(gokart.TaskOnKart): def run(self): self.dump('TASKC') class ExampleTaskD(gokart.TaskOnKart):

    def run(self): self.dump('TASKD') class ExampleTaskB(gokart.TaskOnKart): param = luigi.Parameter() task_1 = gokart.TaskInstanceParameter() task_2 = gokart.TaskInstanceParameter() def requires(self): return dict(task_1=self.task_1, task_2=self.task_2) # required tasks are decided from the task parameters `task_1` and `task_2` def run(self): task_1 = self.load('task_1') task_2 = self.load('task_2') self.dump(f'DONE {self.param}_{task_1}_{task_2}') task_b = ExampleTaskB(param='example', task_1=ExampleTaskC(), task_2=ExampleTaskD()) # Dependent tasks are defined here output = gokart.build(task=task_b) print(output) https://github.com/m3dev/gokart/blob/master/examples/gokart_notebook_example.ipynb より引用
  4. アジェンダ - pipelineの全体像 - requiresにpipelineを書く - 精度の確認用のtaskを挟む - staticmethodでテストをする -

    panderaによるデータのvalidation - 推論ファイル配置用のS3にputをする - pipelineが巨大になった際に、requiresを更にnestする
  5. ディレクトリ構成 ❯ tree -L 3 . ├── README.md ├── conf

    │ ├── logging.ini │ └── param.ini ├── gokart_pipeline_sample │ ├── __init__.py │ ├── pipeline │ │ ├── __init__.py │ │ ├── __pycache__ │ │ ├── check.py │ │ ├── evaluate.py │ │ ├── gokart_util.py │ │ ├── load_data.py │ │ ├── make_output.py │ │ ├── pred.py │ │ ├── preprocess.py │ │ ├── run.py │ │ ├── run_middle.py │ │ ├── schemas │ │ ├── split_data.py │ │ ├── train.py │ │ └── upload.py │ └── utils │ └── template.py ├── main.py ├── poetry.lock ├── pyproject.toml └── test ├── __init__.py └── unit_test ├── __pycache__ └── test_preprocess.py
  6. requiresにpipelineを書く - 処理の全体像がすぐにわかる class RunTask(GokartTask): require_acc = luigi.FloatParameter(default=0.9) def requires(self)

    -> gokart.TaskInstanceParameter: data_task = LoadDataTask() data_task = PreprocessTask(data_task=data_task) data_task = SplitDataTask(data_task=data_task) train_data_task = GetDataTask(data_task=data_task, name="train") valid_data_task = GetDataTask(data_task=data_task, name="valid") test_data_task = GetDataTask(data_task=data_task, name="test") model_task = TrainTask(train_task=train_data_task, valid_task=valid_data_task) pred_task = PredTask(model_task=model_task, test_task=test_data_task) output_task = MakeOutputTask(pred_task=pred_task, test_task=test_data_task) checked_output_task = CheckAccuracyTask( pred_task=output_task, test_task=test_data_task, require_acc=self.require_acc ) s3_upload_task = UploadS3Task(output_task=checked_output_task) return s3_upload_task def run(self): self.dump(self.__class__.__name__)
  7. - 精度の確認用のtaskを挟むことで、ドメインシフトなどによる精度低下を自動で 検知 精度の確認⽤のtaskを挟む class CheckAccuracyTask(GokartTask): pred_task = gokart.TaskInstanceParameter() test_task

    = gokart.TaskInstanceParameter() require_acc = luigi.FloatParameter() def requires(self) -> dict[str, gokart.TaskInstanceParameter]: return {"pred": self.pred_task, "test": self.test_task} def run(self) -> None: pred = self.load("pred") test = self.load("test") y_test = test["y_test"] acc = accuracy_score(y_test, pred["pred"]) if acc <= self.require_acc: raise Exception(f"必要な精度に達していません 必要精度:{self.require_acc} 実際の精度:{acc}") self.dump(pred)
  8. - テストが必要な関数はstaticmethodやclassmethodに切り出す class PreprocessTask(GokartTask): data_task = gokart.TaskInstanceParameter() def requires(self) ->

    gokart.TaskInstanceParameter: return self.data_task @staticmethod def make_area(length: pd.Series, width: pd.Series) -> pd.Series: return length * width @pa.check_types def add_area_feature(self, data: DataFrame[IrisFeatureSchema]) -> DataFrame[PreprocessedSchema]: # 花びらの面積を新たな特徴量として追加 data["petal_area"] = self.make_area(data["petal_length"], data["petal_width"]) return data def run(self) -> None: data = self.load_data_frame() df = self.add_area_feature(data=data) self.dump(df) staticmethodでテストをする class TestMakeAreaTask: @pytest.mark.parametrize( ("length", "width", "expect"), [ ( pd.Series([2.0, 4.0, 6.0]), pd.Series([1.0, 2.0, 3.0]), pd.Series([2.0, 8.0, 18.0]) ) ], ) def test_make_area( self, length: pd.Series, width: pd.Series, expect: pd.Series ) -> None: actual = PreprocessTask.make_area(length, width) assert actual.equals(expect)
  9. panderaによるデータのvalidation - inputとoutputのpandas.DataFrameのvalidationを行うために、panderaを利用 - add_area_featureメソッドを作成し、inputとoutputの型ヒントを付与 class PreprocessTask(GokartTask): data_task = gokart.TaskInstanceParameter()

    def requires(self) -> gokart.TaskInstanceParameter: return self.data_task @staticmethod def make_area(length: pd.Series, width: pd.Series) -> pd.Series: return length * width @pa.check_types def add_area_feature(self, data: DataFrame[IrisFeatureSchema]) -> DataFrame[PreprocessedSchema]: # 花びらの面積を新たな特徴量として追加 data["petal_area"] = self.make_area(data["petal_length"], data["petal_width"]) return data def run(self) -> None: data = self.load_data_frame() df = self.add_area_feature(data=data) self.dump(df)
  10. - 指定したデータ以外を検知した場合に、Errorを出す panderaによるデータのvalidation class PreprocessedSchema(BaseSchema): """ 前処理済データのスキーマ """ id: Series[int]

    = pa.Field(nullable=False, description="id") sepal_length: Series[float] = pa.Field(gt=0.0, nullable=False, description="がく片の長さ(cm)") sepal_width: Series[float] = pa.Field(gt=0.0, nullable=False, description="がく片の幅(cm)") petal_length: Series[float] = pa.Field(gt=0.0, nullable=False, description="花びらの長さ(cm)") petal_width: Series[float] = pa.Field(gt=0.0, nullable=False, description="花びらの幅(cm)") petal_area: Series[float] = pa.Field(gt=0.0, nullable=False, description="花びらの面積(cm^2)") target: Series[int] = pa.Field(isin=(0, 1, 2), nullable=False, description="あやめの種類")
  11. 推論ファイル配置⽤のS3にputをする - Circuit基盤上では意図的にファイル出力を制限をしているため、dataframeをスト リームで直接putする class UploadS3Task(GokartTask): output_task = gokart.TaskInstanceParameter() def

    requires(self) -> gokart.TaskInstanceParameter: return self.output_task def put_file(self, output_data: pd.DataFrame) -> None: s3 = boto3.resource("s3") bucket_name = os.environ["BUCKET_NAME"] with StringIO() as csv_buffer: output_data.to_csv(csv_buffer, index=False) s3.Object(bucket_name, "output.csv").put(Body=csv_buffer.getvalue()) def run(self) -> None: output = self.load_data_frame() self.put_file(output) self.dump(self.__class__.__name__)
  12. pipelineが巨⼤になった際に、requiresを更にnestする - requiresの中身が巨大になった際には、requiresのtaskを更にnestするようなtask を作成 - この程度だとあまり差はないが、巨大になった場合は見通しが改善する class RunTask(GokartTask): def requires(self)

    -> gokart.TaskInstanceParameter: data_task = LoadDataTask() data_task = PreprocessTask(data_task=data_task) data_task = SplitDataTask(data_task=data_task) –中略-- return s3_upload_task def run(self) -> None: self.dump(self.__class__.__name__) class RunNestTask(GokartTask): def requires(self) -> gokart.TaskInstanceParameter: data_task = PrepareDataTask() –中略-- return s3_upload_task def run(self) -> None: self.dump(self.__class__.__name__) class PrepareDataTask(GokartTask): def requires(self) -> gokart.TaskInstanceParameter: data_task = LoadDataTask() data_task = PreprocessTask(data_task=data_task) data_task = SplitDataTask(data_task=data_task) return data_task def run(self) -> None: data = self.load() self.dump(data)