フィーチャーストアアーキテクチャとその構築方法

(Feachā sutoa ākitekucha to sono kōchiku hōhō ni tsuite no shōsai gaido)

機械学習がビジネスの運営にますます不可欠になるにつれて、MLプラットフォームチームの役割が注目されています。これらのチームは、機械学習を実験から現実世界のアプリケーションへと進めるために必要なツールを開発または選択する役割を担っています。そのような不可欠なツールの一つがフィーチャーストアです。もしもMLモデルのデータパイプラインの複雑さに苦しんでいるのであれば、フィーチャーストアはあなたが求めている解決策かもしれません。この記事では、フィーチャーストアの理解と実装について、階層ごとに包括的なガイドを提供することを目指しています。私たちの目標は、フィーチャーストアがあなたのニーズに合致するかどうかについて、情報を提供してお手伝いすることです。

フィーチャーストアのアーキテクチャ?なぜ先に考えるのか

ちょっとリアルな話をしましょう。フィーチャーストアを建てるのは楽しみのためではありません。リアルな問題を抱えていて、それに対する実際の解決策が必要だからです。では、最初にフィーチャーストアを検討する理由は何でしょうか。以下は私たちが聞いた中で最も説得力のある理由の一部です。

リアルタイムのフィーチャーサービス – 機械学習モデルは低レイテンシとスケーラビリティを持つフィーチャーが必要です。これは必要不可欠なことです。

標準化 – フィーチャーパイプラインにおけるワイルドウエスト的なアプローチに疲れました。すべてのMLプロジェクトのフィーチャーを構築、保存、管理する標準化された方法を望んでいます。

統合されたデータパイプライン – トレーニングとサービングのために別々のパイプラインを維持する時代は終わりました。トレーニング/サービングの違いを減らし、生活を簡単にする統合アプローチを探しています。

フィーチャーの再利用と効率性 – 一元的なフィーチャーストアは、プロジェクト間でのフィーチャーの共有を容易にするだけでなく、発見性、精度、コスト効率性を向上させます。フィーチャーの唯一の真実の情報源を持つことで、冗長な計算やモデルでの不一致した使用を避けることができます。

もしこれらのいずれかがあなたに共鳴するのであれば、正しい場所にいます。フィーチャーストアはこれらの課題に立ち向かい、データの摂取からサービスまで、フィーチャーを管理するための構造化された拡張可能な方法を提供します。そして、一番のポイントは何でしょうか?それは一つのサイズが全てに適合する解決策ではなく、あなたの特定のニーズと制約に合わせて調整できるフレームワークです。

では、最初にフィーチャーストアを構築することを考えさせるものは何でしょうか?

フィーチャーストア vs. データストア vs. ETLパイプライン:ニュアンスを理解する

機械学習のデータ管理の領域を探っていくと、それぞれ独自の機能と制約を持ついくつかの主要な要素に出会います。フィーチャーストアがこのガイドの主役である一方で、伝統的なデータストアやETL(抽出、変換、読み込み)パイプラインとは異なる点を理解することが重要です。これにより、情報を元に意思決定を行うだけでなく、これらのコンポーネントをシームレスに統合することも可能になります。

フィーチャーストアの役割

フィーチャーストアは、機械学習フィーチャーの専門的なリポジトリ以上のものです。それはフィーチャーエンジニアリングのライフサイクル全体を管理するMLエコシステムの重要な一部です。次のセクションでそのアーキテクチャについて詳しく説明しますが、フィーチャーストアは単なるデータストレージソリューションではありません。リアルタイムとバッチモードの両方でのフィーチャーの作成、バージョニング、サービス提供の包括的なフレームワークを提供します。機械学習におけるフィーチャーとは何か、なぜ重要なのかについてより深く理解するために、私たちの記事「車のためのアプリストアは近いうちに実現するかもしれません」を読むことができます。

伝統的なデータストア

対照的に、データベースやデータレイクのような伝統的なデータストアは汎用的なものです。生データや処理済みデータの保存には優れていますが、フィーチャーエンジニアリングとサービス提供における専門的な機能を持たないため、フィーチャーストアが提供するものとは異なります。たとえば、フィーチャーのバージョニングや低レイテンシでのリアルタイムサービスを本質的にサポートしていません。伝統的なデータストアの上にこれらの機能を構築することは可能ですが、それには大規模なエンジニアリング作業が必要です。しかし、フィーチャーストアではそれらのことはもう手間要らずです。

ETLパイプライン

一方、ETLパイプラインはデータ変換の作業馬です。さまざまなソースからデータを抽出し、利用可能な形式に変換し、データストアにロードする責任を持っています。データの準備にはETLパイプラインが不可欠ですが、機械学習のためのフィーチャーエンジニアリングの複雑さを管理するためには設計されていません。ETLパイプラインは一方通行の道路のようなものであり、フィーチャーストアが提供するようなニュアンスのある管理とサービス能力はありません。

インタープレイ

区別を理解することは、他のものを選ぶことを意味しません。それはそれぞれの長所を最大限に活用することです。生データを準備し、初期保存のために従来のデータストアにロードするためにETLパイプラインを使用することができます。そこから、フィーチャーストアは、このデータを取り込み、価値あるフィーチャーに変換し、機械学習モデルに提供することができます。このようにして、ETLパイプライン、従来のデータストア、およびフィーチャーストアの各コンポーネントは、データエコシステムで調和した役割を果たすことができます。

次のセクションでは、フィーチャーストアがデータリポジトリ以上の役割を果たすようにするアーキテクチャコンポーネントについて包括的に説明します。フィーチャーエンジニアリング、管理、およびリアルタイムサービスのための堅牢なフレームワークとして機能する方法について探求します。同時に、拡張性と信頼性も確保します。

フィーチャーストアアーキテクチャ:独自の構築のための実践的なガイド

手を汚してフィーチャーストアの詳細な仕組みに取り組む前に、一歩引いて考えてみましょう。青写真を見ているときは、各部屋がどこにあるかを知っていると家を建てやすいですよね?ここでも同じロジックが適用されます。フィーチャーストアの設計は、基本的に3つのコアレイヤーに分かれており、それぞれにコンポーネントと責任があります。

データインフラストラクチャレイヤー

これは基礎です。生データが取り込まれ、処理され、保存される場所です。このレイヤーは他の全てが築かれる土台です。主要なコンポーネントには、バッチ処理エンジン、ストリーム処理エンジン、オフラインストア、オンラインストアなどがあります。

サービングレイヤー

ここが正面玄関で、処理されたフィーチャーがアプリケーションやサービスにアクセス可能になるゲートウェイです。スピードに最適化され、スケーラブルに設計されています。ここでは、フィーチャーを提供するRESTful APIやgRPCサービスを見つけることができます。

アプリケーションレイヤー

最後に、これを制御室と考えてください。他のレイヤーやコンポーネントが円滑に動作するように調整する役割を果たします。ジョブオーケストレーションからフィーチャートラッキング、システムの健全性モニタリングまで、このレイヤーは船をスムーズに航行させます。

このフィーチャーストアアーキテクチャを理解することは非常に重要です。それはツールの選択からワークフローの確立まで、あなたが行うすべての決定に影響を与えます。だから、各レイヤーとそのコンポーネントをより詳しく掘り下げる際には、この青写真を念頭に置いてください。きっと、前方に進む旅がはるかに困難ではなくなるでしょう。

データインフラストラクチャレイヤー:全てが始まる場所

データインフラストラクチャレイヤーはフィーチャーストアのバックボーンです。データ取り込み、処理、保存の初期段階に責任を持ちます。このレイヤーは後続のより専門的な操作のための舞台を設定し、システム全体の拡張性と信頼性にとって重要です。

バッチ処理エンジン

バッチ処理エンジンは、生データを特徴量に変換する計算ハブとして機能します。リアルタイム処理が必要ない大規模なデータセットを処理し、オフラインの特徴量ストアに格納する準備をします。

考慮事項

  • データの一貫性 – 機械学習モデルの整合性を維持するために一貫性は重要です。異なる実行間で生成される特徴量が一貫していることを確認してください。
  • バージョニング – 特徴量の異なるバージョンを追跡してください。特徴量が更新されたり非推奨になったりした場合は、これを記録する必要があります。
  • 同時性 – バッチジョブが同時に実行されることを考慮し、それらが特徴量ストアで競合しないように計画してください。

SDKコンセプトへの関連性

  • データソース – エンジンは、SQLデータベース、フラットファイル、外部APIなどのSDKで定義されたさまざまなソースからの生データを取り込む場所です。SDKを設計する際には、これらのデータソースの遅延とスループット要件を考慮してください。
  • 特徴量変換 – エンジンはSDKで定義された変換を実行します。機械学習モデルの種類に応じて、異なる変換方法が適している場合があります。たとえば、分類モデルの場合はラベルエンコーディングを考慮することができますが、回帰モデルの場合は多項式特徴量が有用かもしれません。

ベストプラクティス

  • バッチサイズ – 計算速度とシステム負荷の両方を最適化するために、バッチサイズを選択してください。時系列データの場合、日次または時間ごとのバッチを選択することもあります。
  • 特徴量の検証 – 計算された特徴量が品質と一貫性の基準を満たしていることを保証するためのチェックを実装してください。まるで料理がキッチンを出る前の品質チェックのようなものです。
  • 依存関係の管理 – 特徴量の計算順序を管理してください。特に1つの特徴量が他の特徴量に依存する場合は、レシピの手順のように管理します。

注目されたオプション

  • Apache Spark – Sparkは、スケーラビリティと耐障害性に優れた分散コンピューティング環境を提供しています。さまざまなデータソースとフォーマットに対応しており、様々な特徴量エンジニアリングタスクに対して汎用性があります。機械学習ライブラリへのネイティブサポートもあり、特徴量の計算と特徴量ストアへの格納において堅牢な選択肢となります。

ストリーム処理エンジン

ストリーム処理エンジンは、リアルタイムのデータ処理ニーズに対応するために設計されたデータインフラストラクチャの「ファーストフードカウンター」のような存在です。データが到着するとすぐに処理を行うため、リアルタイムの分析やモニタリングが必要なアプリケーションに最適です。

考慮事項

  • 遅延 – バッチ処理とは異なり、遅延が重要な要素です。システムは最小の遅延でデータを処理できる必要があります。
  • スケーラビリティ – データストリームは非常に可変的な場合があるため、システムは迅速にスケールアップまたはスケールダウンできるようにする必要があります。
  • データの整合性と修正 – 間違いは起こりますし、間違ったデータがストリームされることがあります。エンジンは正しい順序で届かなかったり遅れて到着したりしたデータを処理できるだけでなく、これらのエラーをリアルタイムまたは後続のバッチ再計算を通じて修正できる必要があります。

SDKコンセプトへの関連性

  • データソース – このエンジンは、Kafkaストリーム、IoTセンサー、リアルタイムAPIなど、SDKで定義されたリアルタイムデータソースに対応します。
  • 特徴量変換 – ウィンドウ集計やリアルタイムの異常検知など、ストリーム固有の変換をここで実行できます。たとえば、不正検知システムを作業している場合、リアルタイムの変換によって異常な取引パターンをフラッグすることができます。

ベストプラクティス

  • 状態管理 – 特徴量が複数のストリームからのデータを必要としたり、時間的な依存関係を持つ場合には、データストリームの状態を管理してください。
  • 障害耐性 – システムの障害からの回復を実装し、データが失われず処理がスムーズに再開できるようにしてください。
  • 適応的なスケーリング – レート制限を課すよりも、受信データストリームの要求に応じてスケーリングするシステムを構築することに重点を置いてください。

注目されたオプション

  • Apache Spark Structured Streaming – Apache Spark Structured Streamingを推奨します。非常に耐障害性があり、使いやすく、Spark SQLエンジンとのネイティブな統合をサポートしています。複雑なイベント時間ベースのウィンドウ操作が可能であり、さまざまなソースとシンクをサポートし、リアルタイムの分析や特徴量計算に非常に適しています。広範なSparkエコシステムとDataFrame APIとの互換性により、バッチ処理とリアルタイムデータ処理の両方に堅牢な選択肢となります。充実したエコシステム、広範なコミュニティ、商用サポートも、構造化ストリーミングタスクのための優れた選択肢として確固たる存在感を示しています。

オフラインストア

オフラインストアは、バッチやストリームエンジンによって処理されたフィーチャーデータが格納される「データウェアハウス」となります。大量のデータを扱う設計で、バッチ解析に最適化されています。

検討事項

  • データ保持期間 – ストレージコストとデータの有用性を考慮して、データを保持する期間を決定します。
  • アクセシビリティ – データに簡単にアクセスできるようにし、同時にセキュリティも確保します。
  • データスキーマ – データが簡単に解釈可能で使いやすいよう、一貫性のあるスキーマを維持します。

SDKコンセプトへの関連性

  • フィーチャーセット – フィーチャーのグループで、共通の概念や関係を持ちます。SDKで定義され、ここに格納されます。これには簡単な数値フィーチャーから、前処理済みのテキストや画像などのより複雑なタイプまで様々なものが含まれます。例えば、レコメンデーションエンジンを構築する場合、ユーザーの行動メトリクス(クリック率、ページ滞在時間、購買履歴など)がフィーチャーセットに含まれるかもしれません。これらのフィーチャーは、ユーザーの嗜好を理解するために貢献します。
  • フィーチャー取得 – これはフィーチャーのバッチ取得に使用される主要な手段であり、機械学習モデルのトレーニングに頻繁に使用されます。タイムトラベルサポートも含まれ、特定の時点でのフィーチャーを問い合わせることができます。これはデバッグや監査に役立ちます。

ベストプラクティス

  • ACIDトランザクション – データの整合性を確保するためにACIDトランザクションを実装します。
  • インデックス作成 – 特に大規模なデータセットに対してデータの検索を高速化するためにインデックスを使用します。
  • データの検証 – 格納前にデータを検証し、品質と一貫性の要件を満たしていることを確認します。

ハイライトされたオプション

  • DeltaまたはIcebergファイルを使用したS3 – これは高セキュリティで気候制御された倉庫と考えてください。これらのファイル形式はACIDトランザクション、スケーラブルなメタデータ処理、ストリームとバッチデータ処理の統一を提供し、オフラインストアとしての堅牢な選択肢となります。

オンラインストア

オンラインストアは、フィーチャーデータへの低レイテンシなアクセスを目的とした「小売店」のようなものです。クイックリードに最適化されており、リアルタイムアプリケーションのための行き先となります。

検討事項

  • レイテンシ – ここでは低レイテンシが重要であり、データはミリ秒単位で取得可能であるべきです。
  • 高可用性 – オンラインストアはリアルタイムアプリケーションの要求に応えるために高い可用性を持つべきです。
  • スケーラビリティ – フィーチャーの数やリクエストレートが増えるにつれて、システムはシームレスにスケールする機能を備えているべきです。

SDKコンセプトへの関連性

  • フィーチャー取得 – これはリアルタイムフィーチャー取得の主要なソースであり、本番での機械学習モデルの提供に頻繁に使用されます。
  • オンデマンドフィーチャーの計算 – SDKがサポートしている場合、一部の軽量なフィーチャー計算もリアルタイムでここで行うことができます。

ベストプラクティス

  • データのパーティショニング – パフォーマンス向上のためにデータを複数のサーバーに分散させるためのパーティショニング戦略を使用します。
  • キャッシング – 頻繁なデータの取得を高速化するためにキャッシングメカニズムを実装します。
  • 一貫性 – オンラインストアとオフラインストアの間でデータの一貫性を維持することは重要です。特に両方のストアが同時に更新される場合は、トランザクションの整合性と復元性が重要です。例えば、データがオフラインストアに正常に書き込まれたがオンラインストアへの書き込みに失敗した場合、そのような不整合を処理し、優雅に回復するための堅牢なメカニズムが必要です。

ハイライトされたオプション

  • Redisキャッシング – Redisはオープンソースのインメモリデータストアであり、超高速な読み書き操作を提供します。低レイテンシと高スループットの機能により、リアルタイム機械学習アプリケーションでのフィーチャー提供に優れた選択肢です。様々なデータ構造とアトミック操作により、特定のニーズに合わせて効率的なフィーチャーストアを設計する柔軟性を提供します。

サービングレイヤー:API駆動のフィーチャーアクセス

サービングレイヤーは「カスタマーサービスデスク」のような存在であり、外部のアプリケーションやサービスがフィーチャーデータを要求して受け取るインターフェースです。高可用性と低レイテンシに最適化されており、フィーチャーを迅速かつ信頼性の高い方法で提供できるようにしています。

考慮事項

  • APIデザイン – APIは使いやすさを重視し、明確なドキュメントとバージョニングを備えて設計するべきです。
  • 負荷分散 – 入力されるリクエストを複数のサーバに分散させることで、高い可用性と低いレイテンシを実現します。
  • セキュリティ – 認証および認可メカニズムを実装して、機能データへのアクセスを制御します。

SDKコンセプトへの関連性

  • 特徴の取得 – このレイヤーは、通常はSDKで定義されたRESTfulなAPIまたはgRPCサービスを介して外部アプリケーションに特徴を提供する責任があります。
  • リアルタイム計算 – 事前計算された特徴だけでなく、このレイヤーはSDKの機能に応じてリアルタイムで軽量な計算も実行できます。たとえば、推薦エンジンの特徴を提供する場合、リアルタイムのユーザの相互作用に基づいてアイテムの「人気スコア」を計算する必要があります。このスコアはServing Layerでリアルタイムに計算され、アプリケーションに送信される前に処理されます。

ベストプラクティス

  • レート制限 – 悪用を防止し公正な利用を保証するために、レート制限を実装します。
  • モニタリング – APIの使用状況、エラー、レイテンシを追跡して持続的な最適化を行います。
  • キャッシング – 頻繁なデータの取得を高速化するためにキャッシュメカニズムを使用します。これは、よく整理されたカスタマーサービスデスクのように、顧客のために一般的な書類や情報を迅速に取得する役割を果たします。

ハイライトされたオプション

  • Kubernetes – 頑強なServing Layerには、選択したマネージドサービスプロバイダとのKubernetesクラスタをお勧めします。Prometheusを使用してシステムのメトリクスをリアルタイムで監視し、Kafkaを使用して効果的なレート制限を行います。高い入力リクエストがある場合、これらのサービスはそれらをキューに入れ、制御された速度でServing Layerに供給します。これにより、システムが過負荷になることを防ぎ、リソースを最適に使用できます。リソースの悪用を防止し、公正な利用を保証するために、レート制限は特に重要です。

アプリケーションレイヤ:コントロールタワー

アプリケーションレイヤは、特徴ストアのオーケストレータとして機能します。データパイプラインを管理し、特徴とそのメタデータを追跡し、システムの健全性を監視します。このレイヤは、特徴ストアのすべてのコンポーネントが調和して動作することを保証するため、システム全体のパフォーマンスと信頼性に重要です。

ジョブオーケストレータ

ジョブオーケストレータは「オーケストラの指揮者」として、様々なコンポーネントを調和させます。タスクが正しいシーケンスで実行され、それらの間の依存関係が管理されるよう、データパイプラインを調整します。

考慮事項

  • ワークフローデザイン – タスクのシーケンスと依存関係を明確に定義した有向非循環グラフ(DAG)またはワークフローを設計します。

SDKコンセプトへの関連性

  • フィーチャーセット – オーケストレーターはSDKで定義されたフィーチャーセットの計算とストレージをトリガーします。

ベストプラクティス

  • イデンポテンシー – タスクをイデンポテントに設計し、副作用なく安全に再試行できるようにします。これは、楽曲を再開することなく混乱を引き起こさずに指揮者が曲を再開できることに似ています。
  • 統合モニタリングとログ – モニタリングダッシュボードとジョブログをフィーチャーストアのUIに組み込むことで、アクセスを損なわずに迅速なデバッグが可能です。これにより、ジョブのパフォーマンスと問題の一元的な表示が可能になり、より迅速な解決が容易になります。モニタリングにはデータの「新鮮さ」、遅延、エラーレートの追跡が含まれることがあります。たとえば、リアルタイムの株価データを取り込んでいる場合、直近の5分間でデータが更新されていない場合にアラートを設定することができます。
  • データのバリデーションとアラート – 計算されたデータの絶対的な正確性を保証することは難しいですが、データのバリデーションチェックとアラート機構を実装することで助けになる場合があります。たとえば、ETLジョブが売上データを集計することが期待されている場合、売上の急激な50%の減少が手動のレビューのためのアラートをトリガーするかもしれません。

特集オプション

  • Airflow – Airflowはプログラムによってワークフローを作成し、スケジュールし、監視するために設計されたオープンソースのプラットフォームです。豊富な機能セットと拡張性により、MLOpsを含む複雑なデータパイプラインのオーケストレーションに堅牢な選択肢となります。タスクの依存関係とスケジューリングの定義のネイティブサポートを備えたAirflowは、ワークフロー管理の包括的なソリューションを提供します。また、PrometheusやGrafanaなどのツールやPagerDutyなどのアラートサービスを介した監視とアラートの統合ポイントも提供しています。

フィーチャーレジストリ(メタデータストア)

フィーチャーレジストリはフィーチャーストアの「図書館カタログ」として機能し、各フィーチャーのメタデータ、ラインナップ情報、その他の属性についてのメタデータを管理します。これはCRUD操作をサポートし、フィーチャーの起源とフィーチャーのラインナップの追跡を提供するバックボーンです。

考慮事項

  • メタデータスキーマ – メタデータには、フィーチャーの名前、タイプ、ラインナップ情報などの明確なスキーマを定義します。
  • 検索可能性 – メタデータに基づいてフィーチャーを簡単に検索して取得できるようにします。
  • バージョニング – フィーチャーのバージョニングを実装して、時間の経過に伴う変更を追跡します。

SDKコンセプトへの関連性

  • フィーチャーセット – SDKで定義されたフィーチャーセットに関するメタデータがここに格納されます。フィーチャータイプ、デフォルト値、データソースなどの詳細が含まれます。

ベストプラクティス

  • データラインナップ – 各フィーチャーのラインナップの記録を維持し、そのソースから提供までの経緯を示します。これは、図書館のカタログのように、本を一覧表示するだけでなく、その起源と図書館への到着方法も表示するものです。
  • アクセス制御 – フィーチャーメタデータを表示または変更できるユーザーを制限する細かいアクセス制御を実装します。
  • 監査トレイル – フィーチャーへのアクセスや変更のログを保持し、図書館の貸出履歴のように明確な変更履歴を提供します。

特集オプション

  • Feastを使用したPostgreSQL – この関係データベースは、メタデータの格納に優れた機能を提供します。Feastと組み合わせて使用することで、フィーチャーストアフレームワークとの簡単な統合、フィーチャーラインナップの追跡などの追加の利点が得られます。

コントロールプレーン

コントロールプレーンはフィーチャーストアの「航空交通管制塔」として、すべての操作を監視し、スムーズに実行する役割を果たします。データのドリフト監視、アクセス制御、その他の管理機能のためのUIとして機能します。

ベストプラクティス

  • データのドリフトとスケウ監視 – 機械学習モデルの整合性を維持するために、データのドリフトとスケウを検出するアルゴリズムを実装します。
  • アラート – Slack Webhooks、Opsgenieなどの統合を使用して、重要なイベントや異常の検出に対するアラート機構を設定します。
  • 監査ログ – すべての操作のログを保持し、変更とアクセスの明確な履歴を提供します。これは航空交通管制ログのようなものです。

特集オプション

  • Serving LayerのKubernetes – Serving LayerにはKubernetesを推奨しているため、コントロールプレーンにも同じクラスタを使用することは合理的です。これにより、統一されたスケーラブルかつコスト効果の高い管理体験が提供され、必要なサービスの数を減らすことでアーキテクチャを簡素化することができます。

まとめ

フィーチャーストアの構築は簡単なものではありません。データと使えるツールの両方の深い理解が必要な複雑な取り組みです。しかし、良いニュースは、一人で取り組む必要はないということです。マネージドサービスからオープンソースプロジェクトまで、フィーチャーストアの構築方法をサポートするリソースがあります。ポイントは、しっかりとした基盤から始めることであり、それはフィーチャーストアのアーキテクチャを理解することから始まります。私たちは、データインフラストラクチャ、サービング、アプリケーションレイヤーを紹介し、それらを構成する要素を解説しました。フィーチャーストアは単なる部品の合計以上のものであり、生態系です。そして、生態系ではバランスが重要です。

この旅に乗り出す際には、地平線に目を向けつつも、しっかりと足元を固くしてください。なぜなら、機械学習の未来は単なるアルゴリズムだけではなく、フィーチャーに関しても重要な要素です。フィーチャーの保管、管理、提供方法が重要です。それこそが、価値のある未来を構築するためのものなのです。

この記事は、Qwakさんによって元々こちらに投稿されました。許可を得て再掲載しています。

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more