temple

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

LightGCN の課題と発展系モデルの改善アプローチ

1. はじめに

Graph Neural Network(GNN)はユーザーとアイテムの相互作用をグラフ構造として捉え、高次の協調フィルタリング信号を効果的に学習できることから、最近の推薦システムでは主流なモデルの一つになっています。

中でも LightGCN(He et al., SIGIR 2020)は GCN の本質的な要素である近傍集約のみを残し、埋め込みの変換や非線形活性化といった処理を削除することで、シンプルかつ高性能なモデルを実現しています。

近年の推薦モデルの研究では LightGCN をベースラインとして比較されることも多く、PyG (PyTorch Geometric) を使えば簡単に検証もでき、実験のためのエコシステムも充実しています。

本記事ではそんな LightGCN の特徴と課題を整理した上で、それらを解決するために提案された以下の発展系モデルを改善の方向性ごとに紹介します。

気力が残っていれば、後日に簡易的な実装例も追加するかもしれません、、、

2. LightGCN の特徴と課題

2.1 NGCFとの比較

LightGCN を理解するには、その前身である NGCF(Neural Graph Collaborative Filtering, Wang et al., SIGIR 2019)と比較するとその違いがよく分かります。

モデル propagation rule
NGCF \displaystyle{\mathbf{e} _ u^ {(k+1)} = \mathbf{ReLU} \left( \mathbf{W} _ 1 \mathbf{e} _ u^ {(k)} + \sum _ {i \in \mathbf{N} _ u} \frac{1}{\sqrt{|\mathbf{N} _ u||\mathbf{N} _ i|}} \left( \mathbf{W} _ 1 \mathbf{e} _ i^ {(k)} + \mathbf{W} _ 2 (\mathbf{e} _ u^ {(k)} \odot \mathbf{e} _ i^ {(k)}) \right) \right)}
LightGCN \displaystyle{\mathbf{e} _ u^ {(k+1)} = \sum _ {i \in \mathbf{N} _ u} \frac{1}{\sqrt{|\mathbf{N} _ u||\mathbf{N} _ i|}} \mathbf{e} _ i^ {(k)}}

NGCF の propagation rule は GCN の基本的な構造を踏襲しており、非常に複雑そうな計算であることが分かるかと思います。一方、LightGCN は NGCF が持つ以下の要素を排除し、シンプルな近傍集約を行う propagation rule でノードの埋め込みを計算しています。

  • 特徴変換行列: \displaystyle{\mathbf{W} _ 1, \mathbf{W} _ 2}
  • 非線形活性化: \displaystyle{\mathbf{ReLU}(x)}
  • 自己接続: \displaystyle{\mathbf{W} _ 1 \mathbf{e} _ u^ {(k)}}

その結果、LightGCN での最終的な埋め込みは各層の出力の重み付き和として計算されます。

\displaystyle{\mathbf{e} _ u = \sum _ {k=0}^ {K} \alpha _ k \mathbf{e} _ u^ {(k)}}

なぜシンプルな近傍集約のみで良いのか?

ユーザー・アイテムの ID のみを使った推薦モデルの場合、メタデータのような意味のある初期特徴量を持たないのが一般的です。

LightGCN の論文中では、このような設定で推薦を行う場合に複雑な構造を持つ NGCF で以下の問題が生じると指摘しています。

  1. 入力が one-hot ID 埋め込みのみであるため、非線形変換による表現力向上の恩恵が薄い
  2. 過剰なパラメータによって学習が困難になる

NGCF は GCN を推薦タスクに応用した事例ですが、論文の著者らはそもそも GCN はノードの特徴量が豊富に存在するグラフに対して有効であり、ID 埋め込みのみを扱う推薦タスクではその利点が活かされにくいと指摘しており、LightGCN ではこうした推薦タスク特有の性質を鑑みて、IDベースの表現学習において実質的に機能していない不要な処理を排除したことが良い性能に繋がったと主張しています。

2.2 LightGCN の課題

とはいえ、LightGCN にも課題がないわけではありません。

LightGCN はシンプルな propagation rule であるが故、層を深くしても性能が向上しにくい、むしろ層を深くすると性能が低下してしまうという問題を抱えています。

LightGCN は隣接ノードの埋め込みの加重平均で propagation していくため、この操作を繰り返す(=層を深くする)と、以下のようにしてモデルの性能悪化に繋がります。

  1. ノードの情報が広範囲に伝播し、情報が均一化する
  2. ノードに伝播する情報が増えることで埋め込みが均一的になり、ノードごとの埋め込みが似通う
  3. 全てのノードで似たような埋め込みとなるため、嗜好を捉えた推論ができずモデル性能が悪化する

この現象を Over-smoothing と呼んでおり、グラフ系のモデルでは一般的なトピックです。LightGCN では浅い層数(\displaystyle{L=3} 程度)で十分、あるいは最高の性能に達するとされていますが、これは裏を返せば Over-smoothing を避ける意図でもあるでしょう。

LightGCN は線形変換のみでメッセージパッシングをして埋め込みの学習を行っている都合上、Over-smoothing が生じやすくなっていると考えられます。そのため、改善手法の多くはこの問題を回避して高い性能を目指すことを念頭に置いています。

ここで、私の主観で LightGCN の課題を以下の3つに分類し、以降のセクションではこれらの課題に対応する発展系モデルだったり改善手法を紹介していきます。*1

No. 課題 概要
1 多層化の限界 (Over-smoothing) 多層 propagation の計算コストが大きい・そもそも多層化が推奨されない
2 人気バイアスと Collapsing への対応 次数の高いノード(=人気のアイテム・アクティブユーザー)の影響が強くなりすぎる・層を通すごとに情報が希釈されてしまう
3 データの sparseness とノイズへの対処 観測データが少ない場合に十分な教師信号を得られず、グラフに含まれる誤クリック等のノイズの影響も受けやすい

3. 課題ごとの改善アプローチ

3.1 多層化の限界 (Over-smoothing)

LightGCN はモデル構造を簡素化したものの、学習時に近傍ノードの情報を集約してそれを次の層に伝播させるというメッセージパッシングは必要です。大規模なグラフ構造においては、これが非常に計算コストのかかる処理であることに変わりはありません。

このセクションでは「Over-smoothing を回避しながら多層化を目指す」という方向性ではなく、逆転の発想で「多層化せずに多層化と同等の性能を目指す」というアプローチを紹介します。

UltraGCN(CIKM 2021)

Mao et al., UltraGCN: Ultra Simplification of Graph Convolutional Networks for Recommendation, Figure 1 より引用

論文: Mao et al., "UltraGCN: Ultra Simplification of Graph Convolutional Networks for Recommendation" (CIKM 2021)

イデア

UltraGCN は LightGCN のメッセージパッシングを完全に省略し、層を無限に深くした場合の収束状態(極限)を直接近似するというアプローチを採っています。

具体的には GCN ベースのメッセージパッシングを無限に繰り返した際の到達点を数学的に導出し、その関係性を満たすよう制約する損失関数(Constraint Loss)を定義しました。さらにこの損失関数に負例サンプリングを組み込むことで、多層化による Over-smoothing を防ぎつつ、明示的なメッセージパッシングなしでの高速かつ高精度な学習を実現しています。

手法

UltraGCN は以下の損失関数を用いて学習します。

\displaystyle{ \mathbf{L} = \mathbf{L} _ {O} + \lambda \mathbf{L} _ {C} + \gamma \mathbf{L} _ {I} }
  • \displaystyle{ \mathbf{L} _ {O} } : User-Item 間の予測損失(Binary Cross Entropy)
  • \displaystyle{ \mathbf{L} _ {C} } : サンプルごとにノードの次数を考慮した重み付きの予測損失
  • \displaystyle{ \mathbf{L} _ {I} } : Item-Item グラフにおける類似アイテムの埋め込みを近づけるための損失

解釈を加えるとすれば、\displaystyle{ \mathbf{L} _ {O} } は通常の損失、\displaystyle{ \mathbf{L} _ {C} } はグラフの構造を考慮した損失、\displaystyle{ \mathbf{L} _ {I} } は類似アイテムのスコアも上げる損失、といった具合でしょう。メッセージパッシングによる行列計算の計算コストを除外する代わりに、その複雑な機能を損失関数で表現していることがわかります。

LightGCN と UltraGCN の違いはコードで見るとよく分かります。LightGCN は self.convs によってメッセージパッシングを繰り返し行っていますが、UltraGCN では単純にユーザーとアイテムの埋め込みを内積してスコアを計算しています。

# PyG の LightGCN 実装の embedding 計算部分を一部抜粋
# https://github.com/pyg-team/pytorch_geometric/blob/44dee49ad88cf5253e8092cfce3e453c00bc8c3e/torch_geometric/nn/models/lightgcn.py#L98C1-L111C19
    def get_embedding(
        self,
        edge_index: Adj,
        edge_weight: OptTensor = None,
    ) -> Tensor:
        r"""Returns the embedding of nodes in the graph."""
        x = self.embedding.weight
        out = x * self.alpha[0]

        for i in range(self.num_layers):
            x = self.convs[i](x, edge_index, edge_weight)
            out = out + x * self.alpha[i + 1]

        return out

# UltraGCN の著者が公開している実装の embedding 計算部分を一部抜粋
# https://github.com/reczoo/RecZoo/blob/88fae23ff0b8321c60655d9c21e0a1bb1173f6af/matching/gnn/UltraGCN/main.py#L367C1-L372C47
    def test_foward(self, users):
        items = torch.arange(self.item_num).to(users.device)
        user_embeds = self.user_embeds(users)
        item_embeds = self.item_embeds(items)
         
        return user_embeds.mm(item_embeds.t())

前述した損失関数を使って学習することで無限層の GCN を近似できるため、隣接ノードの集約から次層へのメッセージパッシングといった処理は一切不要である、というのが UltraGCN の主張です。

メッセージパッシングを完全に省略しているため、実質的には重み付きの Matrix Factorization と言える形態になっています。

効果
  • LightGCN 比で +10〜76% の性能向上
  • 学習速度は 10x 以上の高速化
  • メモリ効率の大幅な改善

似たようなアプローチ: GF-CF

論文: Shen et al., "How Powerful is Graph Convolution for Recommendation?" (SIGIR 2021)

GF-CF はさらに極端なアプローチで、ニューラルネットワークによる反復的な学習自体を不要としています。

まず GCN が行っている近傍集約とは高周波(ノイズ)をカットし、低周波(ユーザーの嗜好)だけを残すローパスフィルタの一種である、と解釈できます。

そこで著者らは、LightGCN などの GCN ベースの手法が成功している本質的な理由はその構造にあるのではなく、「埋め込みの分布を適切に平滑化するローパスフィルタの機能」にあることを突き止め、この適切な平滑化をするためのローパスフィルタを数学的に導き出しました。

つまり GF-CF ではモデルの学習(バックプロパゲーション)という概念は存在せず、SVD(特異値分解)を含む事前に定義された行列計算を一度実行するだけでスコアが算出できます。

精度面では、特に大規模でスパースなデータセットAmazon-Booksなど)においては、LightGCN と比較して 70% 近く性能を改善したことが報告されています。

3.2 人気バイアスと Collapsing への対応

多層構造を持つ GCN モデルは層が深くなるほど高次の関係性を捉えますが、一方で次数が高いノード(=人気のアイテム・アクティブなユーザー)の影響が強くなりすぎてしまう人気バイアスの問題に対処する必要があります。

高次数のノードは多くのノードと繋がっているため、メッセージパッシングの過程でその影響が他のノードに広がりやすくなっています。

また情報の重要度という観点では、ユーザーやコンテキストごとにどの層の情報が重要かは異なると考えられます。LightGCN の論文では層ごとに様々なバリエーションで重み付けをする実験も行われましたが、結局「全層の重みを一律にする」(=全層の平均をとる)というシンプルな形が最も良い結果を出しました。

単純に考えれば層ごとの重みを学習可能にすれば更に良くなりそうに思えますが、これには Solution Collapsing と呼ばれる、特定の層に重みが偏ってしまうという問題が発生します。

本セクションでは、情報の集約方法を工夫することで、この Collapsing を防ぎつつ人気バイアスにも対応した手法を紹介します。

LayerGCN(ICDE 2023)

Zhou et al., Layer-refined Graph Convolutional Networks for Recommendation, Fig. 2 より引用

論文: Zhou et al., "Layer-refined Graph Convolutional Networks for Recommendation" (ICDE 2023)

イデア

LayerGCN は、LightGCN が抱えていた「層を重ねるほどノイズが混じり、特定の層への依存度が高まってしまう」という課題に対し、伝播する情報の質をその都度磨き上げるというアプローチを採っています。

この論文では、単純に層ごとの重みを学習可能にした場合、学習が進むにつれて初期層の重みが極端に大きくなってしまい、深い層の重みはほぼゼロになることを示しています。これは Solution Collapsing と呼ばれており、「深い層の情報は平滑化されているので予測の役に立たず、結局は自分自身の初期情報が最も有用である」として近傍ノードの情報を無視してしまう現象です。

そこで LayerGCN では各層で伝播する情報に自分自身の埋め込みとの差分を強調するフィルタを導入することで、動的に層ごとの重み付けを行うアプローチを提案しています。

手法

LayerGCN の新規性は以下の 2 つの仕組みに集約されます。

手法 概要
Layer refinement 各層の出力に対して、初期層(ego layer)との類似度に基づいた動的な重み付けを行う
Degree-sensitive edge pruning 次数(人気度)が高いノードほど高い確率でエッジを削除し、情報の過剰流入を抑える

Layer refinement は、各層の埋め込み\displaystyle{\mathbf{e}^{(k)}} をそのまま集約せず、初期層 \displaystyle{\mathbf{e}^{(0)}} とのコサイン類似度を用いて「その層がどれだけ信頼できるか」を動的に判定します。

\displaystyle{ \mathbf{e} _ {out} = \sum _ {k=0}^{L} \text{cos}(\mathbf{e}^{(k)}, \mathbf{e}^{(0)}) \mathbf{e}^{(k)} }

発想としては ResNet のスキップコネクションに近いですが、LayerGCN は「初期層の埋め込みから離れすぎた(類似度が低い)情報はノイズとみなして重みを下げる」というゲート機構のような役割を動的に果たしている点が特徴です。これにより深い層での情報の希釈化を防ぎ、Solution Collapsing を回避しています。

Degree-sensitive edge pruning は、人気バイアスを構造的に解消する仕組みです。高次数のノードほどメッセージパッシング時に周囲に強い影響を与えてしまうため、あえて次数に比例した確率でエッジを間引きます。一律のランダム削除よりも推薦におけるノイズを効果的に除去できるため、学習の収束が早くなることが報告されています。

効果
  • Over-smoothing, Solution Collapsing, 人気バイアスを同時に緩和
  • 学習の収束が非常に高速
  • 複数の公開データセットにおいて、当時の SOTA の性能を達成(特に dense なデータセットで精度改善が顕著)

LightGCN++(RecSys 2024)

論文: Lee et al., "Revisiting LightGCN: Unexpected Inflexibility, Inconsistency, and A Remedy Towards Improved Recommendation" (RecSys 2024)

イデア

LightGCN では人気バイアスと Collapsing への対処として、あらかじめ以下の「固定ルール」が組み込まれています。

  1. 対称正規化:ノードの次数に応じて埋め込みの大きさを一律に調整し、人気ノードの影響力を抑制する
  2. 一様な層の統合:全層を平等に平均化することで、特定の層(ego layer)への過度な依存を防止する

1 つ目の対称正規化は、ノードの次数に応じて重みを割り引くことで人気バイアスを抑える仕組みです。また 2 つ目は、先述した Solution Collapsing を回避するための制約として機能します。

しかし LightGCN++ の著者らは、「これらの固定的なルールがデータセットごとの個性を無視した設計になっている」と指摘しています。

すべてのノードや層の影響を一律に縛るのではなく、データセットの特性に応じて柔軟に調整(スケーリング)できるようにすべきである、というのが LightGCN++ の主張です。*2

手法

LightGCN++ ではノードや層の影響度合いを「学習によって調整可能」にすることで、LightGCN 本来のポテンシャルを解放することを目指しています。

学習可能なパラメータの追加による弊害として最も懸念されるのは計算コストの増加ですが、計算コストをほぼ増やさずに以下の 2 つの仕組みを導入しました。

1. Adaptive Norm Scaling (適応的ノルムスケーリング)

メッセージパッシングの際、次数に基づく正規化項をハイパーパラメータ \displaystyle{\alpha, \beta} によって制御します。

\displaystyle{ \mathbf{e} _ i^{(k+1)} = \frac{1}{|\mathbf{N} _ i|^\alpha} \sum _ {u \in N _ i} \frac{1}{|\mathbf{N} _ u|^\beta} \mathbf{e} _ u^{(k)} }

これにより、これまでの対称正規化(\displaystyle{\alpha, \beta=0.5} 固定)によって強制的に固定されていた人気度と埋め込みベクトルの大きさの関係を、データセットに最適なバランスへ調整できるようになります。

2. Adaptive Layer Pooling (適応的レイヤープーリング)

初期層(ego layer)とそれ以降の集約層の平均を、ハイパーパラメータ \displaystyle{\gamma} を用いて統合しています。

\displaystyle{ \mathbf{e} _ i = \gamma \mathbf{e} _ i^{(0)} + (1 - \gamma) \frac{1}{K} \sum _ {k=1}^{K} \mathbf{e} _ i^{(k)} }

一律固定だった層の重みを、初期層の寄与度 \displaystyle{\gamma} によって調整可能になっています。単に重みを増やすだけでは Solution Collapsing が再発してしまいますが、前述の正規化項の調整と組み合わせることで、安定性を保ちながら柔軟な集約を実現しています。

効果
  • 行列計算のルールを微調整するだけで最大 +17.81%(NDCG@20)の性能向上
  • 追加の計算コストはほぼゼロでありながら実務上の精度改善に有効
  • Bundle 推薦や知識グラフ推薦など LightGCN をベースにした他の発展モデルにもそのまま適用可能な汎用性

3.3 データの sparseness とノイズへの対処

一般的な GNN ベースのモデルはユーザーのフィードバックを元にグラフ構造を構築し、そのグラフ構造に基づいた学習を行います。

しかし現実の推薦データは極端にスパースであり、全てのユーザー・アイテムのインタラクションが発生するわけではありません。さらに、ユーザーの誤クリックや一時的な興味など、ノイズとなる情報も多く含まれています。

この課題に対し、これまではエッジを削るなどグラフの加工(データ拡張)をして情報を水増しする手法が主流でしたが、本セクションでは対照学習を用いてノイズ耐性と表現力を高めるアプローチを紹介します。

SimGCL(SIGIR 2022)

論文: Yu et al., "Are Graph Augmentations Necessary? Simple Graph Contrastive Learning for Recommendation" (SIGIR 2022)

イデア

データのスパース性やノイズを克服する手段として、近年注目を集めているのが対照学習(Contrastive Learning)です。

GNN × 対照学習の先駆けとなった SGLWu et al., "Self-supervised Graph Learning for Recommendation" SIGIR 2021)では、エッジやノードをランダムに削ることで、元々のグラフとは少し異なる「兄弟グラフ」を複数作り出します。

それらを互いに比較させ、同じユーザーやアイテムであれば「グラフの構造が多少変わっても埋め込みベクトルは一貫しているべきである」としてモデルを学習し、堅牢な表現を獲得しました。

しかし、SimGCL ではこのような複雑かつ計算コストの高いグラフの加工は不要であると主張しています。

実験の結果、SGL がグラフを加工して得ていた恩恵の本質は複雑な構造の変化そのものではなく、最終的な埋め込みベクトルに適度なノイズが乗ることによる正規化効果にあることが分かりました。これを受けて SimGCL ではグラフ構造には一切手を加えず、埋め込みベクトルに直接一様なノイズを加えるだけで SGL を超える精度と効率が得られることを証明しています。

手法

SimGCL は、計算負荷の高いグラフの Dropout 処理を使用せず、埋め込みベクトルに一様なノイズを注入することで、対照学習のための「微妙に異なる 2 つのサンプル(論文中では view と表現)」を生成します。

\displaystyle{ \mathbf{e}' _ u = \mathbf{e} _ u + \Delta _ u, \quad \Delta _ u \sim \text{Uniform}(-\epsilon, \epsilon) }

このようにして生成された「微妙に異なる 2 つのサンプル」を使い、対照学習損失(InfoNCE loss)を用いて「元は同じノードであること」を学習させます。

\displaystyle{ \mathbf{L} _ {CL} = -\log \frac{\exp(\text{sim}(\mathbf{e} _ u, \mathbf{e}' _ u) / \tau)}{\sum _ {v \in \mathbf{U}} \exp(\text{sim}(\mathbf{e} _ u, \mathbf{e}' _ v) / \tau)} }

最終的な損失関数は、通常の推薦損失(BPR loss)と対照学習損失を組み合わせたものになります。

\displaystyle{ \mathbf{L} = \mathbf{L} _ {BPR} + \lambda \mathbf{L} _ {CL} }

この損失関数の組み合わせにより、モデルは \displaystyle{ \mathbf{L} _ {BPR} } を通じて「観測された履歴」を再現するだけでなく、\displaystyle{ \mathbf{L} _ {CL} } によって埋め込み空間全体の分布を最適化することを目指しています。

一様なノイズを加えても同一ノードとして識別できるように制約をかけることで、特定のグラフ構造(エッジの有無)に過剰適合するのを防ぎ、データが少ない状況であってもユーザーやアイテムの本質的な特徴を捉えた汎化性能の高い埋め込みを獲得しています。

効果
  • 複数の公開データセットにおいて、LightGCN 比で最大 +30% の精度改善
  • 兄弟グラフの再構築やサンプリングが不要なため SGL と比較してエポックあたりの学習時間を短縮(ただし LightGCN よりは低速)
  • グラフ拡張が不要であることを実証

対照学習ベースの他のアプローチ

SimGCL 以外の GNN + 対照学習ベースの手法をいくつか紹介します。

論文 概要
SGL(Wu et al., SIGIR 2021) Edge/Node Dropout によるグラフ augmentation の先駆けで、3 種類の augmentation(Edge Dropout, Node Dropout, Random Walk)を提案
XSimGCL(Yu et al., TKDE 2023) 対照学習用にノイズを含んだメッセージパッシングを繰り返し行っていたが、メッセージパッシングはせず最終層の埋め込みにノイズを加える手法に簡略化し計算コストを削減
LightGCL(Cai et al., ICLR 2023) グラフ全体を SVD で低ランクに近似し、グローバルな構造を反映した埋め込み & 通常の LightGCN の埋め込みを活用して対照学習を行う

4. おわりに

本記事では、LightGCN の基本的な仕組みとその課題、そして各課題に対応するための代表的な改善アプローチを紹介しました。

実務においては素の LightGCN をそのまま使うことはそう多くなく、何らかの改善手法が組み込まれていることが一般的かと思います。

特に大規模データを使った推薦システムでは計算コストと精度のバランスを取ることが重要になるため、今回紹介したような論文のアイデアが実務に応用されるケースが多いのではないでしょうか。

次はここで紹介した手法を実際にオープンデータセットで試してみたいです。気力が持てば…

参考文献

*1:「LightGCN の課題」と表現しているものの、LightGCN 特有の課題だけでなく GCN ベースの推薦モデル全般に共通する課題だったり推薦モデルの一般的な課題も含まれています

*2:LightGCNはシンプルさを追求したモデルですが、そのシンプルさが逆に柔軟性を欠く原因になっている、という皮肉な指摘でもあります…

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についてまとめてみる。