temple

機械学習と周辺知識を整理する

Pandas でテーブルデータを読み書きする際のフォーマットごとの性能比較

動機

pandas を使うようになってからというもの、デファクトスタンダードである(と思っていた)csv形式でテーブルデータの読み書きをしていた。 最近は数GB程度のテーブルデータを扱う機会が増えてきており、データの読み書きをするだけでも相当な時間を要していることに気づいた。 「そもそもそんな規模感のデータを素の pandas で扱うな、daskを使え」などという批判も真摯に受け流しつつ、今一度 csv の代替となるフォーマットを探すべく我々はアマゾンの奥地へ向かった。

まずは結論

こまけえこたあいいんだよ

という人向けに。 結論、読み書き速度とデータサイズの観点だけで言えば csv からの乗り換え先は parquet 形式ではないでしょうか。 csv 形式と比較した結果は以下。

フォーマット データサイズ read 時間 write 時間
csv 3.5 GB 1 min 145 sec
parquet 1.1 GB 1.08 sec 19.3 sec

当然データを扱う状況によって最適なフォーマットは変わり得るので盲信はよくないが、「あんまりめんどくさいことはせずにとりあえず csv から乗り換えたい!」というユースケースには適合すると思う。

比較対象フォーマット

今回 csv 形式との比較対象としたものを以下表にまとめる。

形式 選定理由
jsonl 特に深い理由はない。強いて言うなら BigQuery のデータをエクスポートする際の選択肢として存在しているから…
parquet カラムナフォーマットの代表格。GCP の各種プロダクトでサポートしていることが多い。
pickle ML モデルを保存する際のデファクトスタンダード
joblib かつて個人的に pickle からの乗り換え先として愛用していたので。もはや pandas 関係ない。

上記のフォーマット以外にも様々な保存形式は存在するが、「極力 csv 形式からの引越しコストを抑えたい」「あまり馴染みのないフォーマットを扱いたくない」という個人的な怠慢から他のフォーマットは検討していない。 例えば TFRecord などは依存関係として tensorflow を持たせなきゃいけなかったり pandas とは異なるインターフェースでデータを扱う必要があり、データフォーマットを変えるだけでその副作用が出過ぎてしまうので候補に入れていない。

www.tensorflow.org

ちなみにフォーマットという観点以外にも cudf や dask を使うという選択肢もあるが、 cudf はまず GPU を用意するところから始まるので今までなんとな〜く csv を使っていた人がとりあえず使ってみるツールにしてはちょっとハードルが高い気がする。dask は pandas ライクに扱えるがインターフェースが違ったりハマりどころが色々あるので、しっかり腰を据えて高速化を検討するときの選択肢という印象…。

github.com

docs.dask.org

使用するデータ

以下のようにダミーデータを生成した。初めは調子に乗って 5,000 万行を float で作ろうとして普通に OOM で逝ったので控えめの integer 500万行にした。

import pandas as pd
import numpy as np

n_rows = 5_000_000
n_cols = 256
rng = np.random.default_rng()
table_df = pd.DataFrame(rng.integers(low=0, high=100, size=(n_rows, n_cols)))

path = "./table.{}"

write/read に使用するコード

保存フォーマットごとに使用したコードを記載する。

csv

# write
table_df.to_csv(path.format("csv"))
# read
table_df = pd.read_csv(path.format("csv"))

jsonl

# write
table_df.to_json(path.format("jsonl"), orient='records', lines=True)
# read
table_df = pd.read_json(path.format("jsonl"), orient='records', lines=True)

parquet

# write
table_df.to_parquet(path.format("parquet"))
# read
table_df = pd.read_parquet(path.format("parquet"))

pickle

# write
table_df.to_pickle(path.format("pkl"))
# read
table_df = pd.read_pickle(path.format("pkl"))

joblib

import joblib

# write
with open(path.format("joblib"), "wb") as f:
    joblib.dump(table_df, f, compress=3)
# read
with open(path.format("joblib"), "rb") as f:
    table_df = joblib.load(f)

(joblib だけ圧縮形式にしててズルいけど気にしない)

実行結果

jupyter notebook で%%timeマジックコマンドを使って実行した結果を表にまとめた。

フォーマット データサイズ read 時間 write 時間
csv 3.5 GB 1 min 145 sec
jsonl 11 GB N/A 177 sec
parquet 1.1 GB 1.08 sec 19.3 sec
pickle 9.6 GB 3.34 sec 42.3 sec
joblib 1.8 GB 25 sec 163 sec

jsonl は read しようとすると OOM で逝ってしまったので N/A とした。 表の通り、データサイズ・read 時間・write 時間の全項目で parquet 形式が圧勝だった。csv と比較して read は55倍、 write は7.5倍程度早くなっている。 ちなみに読み書きの速度に関しては pickle もいい線をいっているがデータサイズが csv の3倍弱になっているので、高速化の代償としては大きい気がする…。

結論

csv からなんか別なフォーマットにしたいな〜という人には parquet 形式をオススメします。 なお parquet はバイナリフォーマットなので、 csv のように vim とか head でファイルの中身をチラ見できないのがデメリットでしょうか。

Feature Storeを概観する

はじめに

Feature Storeについて調べた際に日本語で資料がまとまったものがなかったのでまとめる。とはいえ、この記事もまとまってはいないかもしれない。

Feature Storeって何

端的に言うと「機械学習モデルで扱う特徴量をひとまとめにして管理し、かつ特徴量の提供も行う基盤」。これだけ聞くと特徴量専用のデータストレージを作ればいいじゃんと思うかもしれないので、Feature Storeを使うモチベーションを述べる。

より詳しく知りたい方はUberのMichelangeloというシステムの紹介記事を参照されたし。

eng.uber.com

Feature Storeを利用するモチベーション

機械学習モデルを構築し本番環境にリリース・運用しているチームでは以下のような状況になることがある。

  • 特徴量のソースがデータレイクやストリーミングデータなど様々
  • 他のプロジェクトやチームで作成した特徴量を使い回していない(使い回せない)
  • 学習時とモデルサーブ時でモデルに入力する特徴量の参照先が異なる
  • 上記に伴い、学習時とモデルサーブ時でデータを提供する仕組みが異なる

一つ二つのモデルを作って終わりならまだしもモデルを日々改善・監視しつつ更に新たなモデルを増やしたりする場合に上記の状況は深刻な問題となるので、この辺りを解決していこうというのがFeature Storeの目指すところ。

先の「特徴量専用のデータストレージを作ればいいじゃん」という意見に対しては一つのデータソースにまとめることができるならそれでいいが、本番環境に載せるモデルは往々にして色んなところからデータを取りたくなるもの。特に学習時はRDBから、サーブ時はKey-Value Storeから、といった具合に分かれる上に一度作ったシステムを使い回すのは中々に大変なので、そこにFeature Storeの大きな存在意義があると思っている。

ちなみに個人的な話で言うと、モデルを作るときに「こういう特徴量って絶対他のチームで作ったことあるよな」と思いつつモヤモヤしながら車輪の再発明をしたことがあるので、Feature Storeはモデルを作る人の作業に対する納得感を担保する一つの要素と考えている。

Feature Storeのコンポーネント

Feature Storeのマネージドサービスを提供しているTectonの以下のテックブログでは、Feature Storeを構成する5つの要素について紹介されている。

www.tecton.ai

1. Serving

f:id:f6wbl6:20210106150002p:plain

学習時と推論時で一貫したデータソースを参照して提供する。注目すべき点は学習時と推論時でほとんど同じインターフェースを利用できることで、同じようなインターフェースながらも推論時には低遅延でレスポンスが返ってくることが求められている。

この図には明記されていないが、学習時に渡しているエンティティ(entities_df)にはuser_idとデータを取得する期間が入っていて、推論時はuser_iditem_idだけを指定してデータストレージにある最新の特徴量を暗黙的に返却するものと思われる。

インターフェースが統一できるのは割とバカにできないもので、作るモデルやプロジェクトによってデータの取得方法を思い出したりする手間が省けるから便利。データの取得先がBigQueryだったりGCSだったりBigtableだったりした時に、その度にデータの取り出し方を調べてる気がするので…。

あとは特徴量サーブで重要な概念として「タイムトラベル」があり、ある特定時点での特徴量のビューを提供する。例えば2020/12/24 21:00時点での特徴量のビューをタイムトラベルにより提供することができるため、模擬的に特徴量の状態を再現することができる。

2. Storage

f:id:f6wbl6:20210106160456p:plain

定義された特徴量を永続化する。オフラインストレージとオンラインストレージの二つから構成される。

オフラインストレージ オンラインストレージ
用途 学習/推論 推論
DBタイプ DWH, データレイク等 KVS
DB例 BigQuery, GCS Bigtable, Redis

「大きいデータはDWHやデータレイクに保存して応答性が求められるものはKVS」という至極普通の構成。Feature Storeというもので中身を隠蔽しているだけなんだけど、自前でFeature Storeと同じ仕組みを構築するとなると二つのストレージ間で同期を取ったりするのが大変そうではある。

3. Transform

f:id:f6wbl6:20210106163407p:plain

Feature Storeに入力される特徴量に対して前処理を加える。ログとして収集された生データをそのまま特徴量として使えないものに何らかの前処理を加えてから保存しようということ。

Transformの種類として以下の3パターンがある。

種類 概要 入力 特徴量の例
バッチ変換 オフラインストレージに対する処理 DWH, データレイク等 都道府県, 商品カテゴリ
ストリーミング変換 ストリーミングデータソースに対する処理 Pub/Sub, Kafka等 過去30分間でのカテゴリごとのクリック数
オンデマンド変換 リアルタイムデータソースに対する処理 APIへのリクエストパラメータ等 ユーザーの現在地

こうした処理はSparkやHadoopで実行されてオフラインストレージないしオンラインストレージに処理結果を保存してモデルに特徴量を提供するため、これらの処理を行うためのインフラなどをその都度用意する必要がなくなるのがメリットか。

確かに単純なOne-hot EncodingとかならFeature Store側でその処理を受け持って欲しいと思うが、例えばあるモデルのembedding layerといった分散表現を特徴量として使うような場合にはむしろ管理が複雑になる気もする。

4. Monitoring

Feature Storeで利用する特徴量の品質を管理する。管理観点を大まかに分類すると以下のようになる。

監視対象 項目
モデルサーブ時に提供する特徴量 Training - Serving Skew
Feature Store自体 特徴量使用率、各ストレージの容量、スループット、エラー率
外部システム Transformジョブの状態、スループット、エラー率

一つ目のTraining - Serving Skewは学習時とモデルサーブ時に提供するデータの偏りのことで、初歩的なものだと学習時とは異なるOne-hot Encodingの結果が得られた時とか。Training - Serving SkewについてはGoogleの"Rules of Machine Learning: Best Practices for ML Engineering"でも取り上げられているので気になる方は以下を参照。

developers.google.com

5. Registry

これまでに述べてきた各コンポーネントを統合してチーム間でFeature Storeを扱えるようにするための中央インターフェースのこと。レジストリを介して上に述べた各処理を実行する、というだけ。

どんなFeature Storeがあるか

Feature Storeは企業が独自に構築していたりマネージドサービスで提供していたりOSSとして存在していたりする。よく目にするFeature Storeについて分類した表が以下。

アーキテクチャ 種別 概要
Michelangelo 内製 Uberで運用されている、Feature Storeに限らないML基盤
Zipline 内製 Airbnbで運用されているFeature Store
Tecton マネージド 大規模なMLシステム向けの商用Feature Store
Amazon SageMaker Feature Store マネージド re:Invent 2020で発表されたAWS謹製のFeature Store
Hopsworks OSS KubeflowのようなML基盤で、その中の機能の一つとしてFeature Storeがある
Feast OSS GO-JEKとGoogleCloudが共同で開発を進めているFeature Store

Feature Storeの概念自体はUberの例が初出という記事をどこかで見たが、ただ初出というだけで実際には各企業が内々で似たようなものを作っていたんだな…と思った。AWSがリリースしたものは自社で運用していたものの一部なのだろうか。GoogleCloudはFeastの開発を進めているのでGCPで利用できるマネージドFeature StoreはFeastベースになるのだろうが、どこまで使えるものになるか個人的に期待している。

ちなみにTectonは昨年11月にFeastの開発を支援する旨の声明を出していて、Feastでカバーしきれない部分をTectonでカバーし集客に繋げたい模様。

www.tecton.ai

その他のFeature Storeについては以下を参照されたし。

www.featurestore.org

まとめ

  • Feature StoreはMLモデルで扱う特徴量を管理する基盤で、学習もサーブも共通のIFで特徴量を提供する
  • Feature StoreはServing, Storage, Transform, Monitoring, Registryの5つの要素で考えることができる
  • マネージドサービスからOSSまで様々なFeature Storeがある

MLモデルを運用したことのある人なら「せやな」で終わる話ばかりだったかもしれない。

今度はOSSとして開発が進められているFeastについてまとめてみる。