تطوير الويب

Feast: مَركَزَة features ML مع feature store مفتوح المصدر

8 دقائق للقراءة

📌 نظرة عامّة: MLOps حديث: من النموذج إلى الإنتاج

المشكلة: training/serving skew

تخيّل فريق data يعمل على نموذج كشف احتيال. data scientists يكتبون notebook يُجَمِّع، لكلّ معاملة، عدد المدفوعات الصادرة من نفس المستخدم في آخر 7 أيّام. يستعملون GROUP BY على جدول BigQuery بنافذة منزلقة. النموذج يحصل على AUC 0.93.

عند النشر، الفريق backend يستلم التعليمة: « احسب نفس feature لحظيًّا لكلّ معاملة واردة ». المهندس backend، بـ Node.js، يكتب SQL خاصّ به بفارق نطاق زمني، ينسى استثناء المعاملات المُلغاة، ويُقَرِّب النافذة إلى 7 أيّام تقويمية بدل 7×24 ساعة منزلقة. النتيجة: feature المُقَدَّمة للنموذج في الإنتاج لم تعد بنفس التوزيع كما عند التدريب. النموذج يبدأ التصنيف الخاطئ، دون أيّ تنبيه.

هذا اللاتطابق بين features التدريب وfeatures الإنتاج له اسم: training/serving skew. أحد أكثر الأسباب ذكرًا لإخفاق النماذج الصامت في الإنتاج. الـ feature store يحلّه بكشف تعريف وحيد للـ features، قابل للاستهلاك من العالمين.

معمارية Feast

Feast هو feature store مفتوح المصدر، مكتوب في Python وGo، ترعاه LF AI & Data. خمس كائنات:

  • Entity — مفتاح العمل الذي تصفه. مستخدم، بطاقة بنكية، متجر، منتج. لكلّ entity اسم (driver) ونوع (Int64، String).
  • Data source — من أين تأتي القيم التاريخية. ملفّ Parquet، جدول BigQuery، Snowflake، Redshift، أو موضوع Kafka.
  • Feature view — تجميع features تتقاسم نفس entity ونفس source. الوحدة التي يُسَجِّلها Feast ويُجَسِّدها.
  • Offline store — محرّك يحلّ الاستعلامات التاريخية point-in-time correct للتدريب. File (Parquet)، BigQuery، Snowflake، Redshift، Spark، DuckDB.
  • Online store — قاعدة مفتاح/قيمة بـ زمن استجابة منخفض للاستنتاج. SQLite (dev)، Redis، DynamoDB، Bigtable، Postgres.

الـ registry ملفّ بيانات وصفية (افتراضيًّا registry.db SQLite، أو Postgres أو S3) يحفظ كلّ تعريفاتك. feast apply يقرأ كود Python، يُسَلسِل التعريفات ويدفعها للـ registry.

المتطلّبات

Python ≥ 3.10 (Feast 0.63 يدعم 3.10 إلى 3.13) وpip. لا قاعدة بيانات خارجية مطلوبة: نبدأ بـ Parquet للـ offline وSQLite للـ online. لـ Redis، يلزم Redis محلّي (أو Docker). للنموذج، scikit-learn ≥ 1.4.

الخطوة 1 — تثبيت Feast وتهيئة المشروع

python -m venv .venv
source .venv/bin/activate

pip install --upgrade pip
pip install "feast[redis]" pandas scikit-learn
feast version

يُرجع Feast SDK Version: "feast 0.63.x".

feast init driver_demo

اقبل الافتراضي local. مجلّد driver_demo/ يحوي الآن مشروعًا كاملًا.

الخطوة 2 — استكشاف البنية المُوَلَّدة

cd driver_demo/feature_repo
ls -la

ثلاثة ملفّات مفتاحية. الأوّل feature_store.yaml:

project: driver_demo
provider: local
registry: data/registry.db
online_store:
  type: sqlite
  path: data/online_store.db
offline_store:
  type: file
entity_key_serialization_version: 3

المشروع يُسَمَّى driver_demo، registry SQLite، online store SQLite، offline store ملفّ (Parquet). provider: local يُشير إلى أنّ كلّ شيء يدور على حاسوبك.

الثاني example_repo.py حيث تُعلَن entities وfeature views. الثالث مجلّد data/ يحوي driver_stats.parquet بآلاف الأسطر من إحصائيات سائقين اصطناعية.

import pandas as pd
df = pd.read_parquet("data/driver_stats.parquet")
print(df.head())
print(df.dtypes)

خمسة أعمدة: event_timestamp، driver_id، conv_rate، acc_rate، avg_daily_trips. timestamp أساسي: Feast يستعمله لحلّ الاستعلامات point-in-time correct.

الخطوة 3 — تعريف Entity وFeatureView بـ Python

from datetime import timedelta
from feast import Entity, FeatureView, Field, FileSource
from feast.types import Float32, Int64

# 1. Entity العمل: سائق، مُعَرَّف بـ Int64
driver = Entity(
    name="driver",
    join_keys=["driver_id"],
    description="Identifiant unique d'un chauffeur",
)

# 2. مصدر البيانات التاريخية (offline)
driver_stats_source = FileSource(
    name="driver_stats_source",
    path="data/driver_stats.parquet",
    timestamp_field="event_timestamp",
    created_timestamp_column="created",
)

# 3. feature view: 3 features مُلحَقة بسائق
driver_stats_fv = FeatureView(
    name="driver_hourly_stats",
    entities=[driver],
    ttl=timedelta(days=1),
    schema=[
        Field(name="conv_rate",        dtype=Float32),
        Field(name="acc_rate",         dtype=Float32),
        Field(name="avg_daily_trips",  dtype=Int64),
    ],
    online=True,
    source=driver_stats_source,
    tags={"team": "data_science"},
)

الـ Entity لها اسم منطقي driver وjoin key فيزيائي driver_id. الـ FileSource يُشير إلى Parquet ويُعَيِّن event_timestamp كعمود مرجع للوقت. الـ FeatureView يجمع 3 features ويُلحق ttl بيوم — حماية ضدّ استعمال بيانات قديمة. online=True يسمح بـ matérialisation نحو الـ online store.

الخطوة 4 — تشغيل feast apply لتسجيل

feast apply

Feast يعرض الكائنات المُنشَأة: Created entity driver، Created feature view driver_hourly_stats. الـ registry يعيش الآن في data/registry.db.

feast feature-views list
feast entities list

كلّ أمر يُرجع جدولًا ASCII بالكائنات المعروفة من registry.

الخطوة 5 — خدمة الـ offline store: get_historical_features

السيناريو: لديك جدول أحداث، كلّ سطر يحمل driver_id وevent_timestamp. تريد، لكلّ سطر، features هذا السائق كما كانت في تلك اللحظة. لا أبكر، لا أبعد. هذا ما يمنع data leakage في التدريب.

from datetime import datetime
import pandas as pd
from feast import FeatureStore

store = FeatureStore(repo_path=".")

entity_df = pd.DataFrame.from_dict({
    "driver_id": [1001, 1002, 1003],
    "event_timestamp": [
        datetime(2021, 4, 12, 10, 59, 42),
        datetime(2021, 4, 12,  8, 12, 10),
        datetime(2021, 4, 12, 16, 40, 26),
    ],
})

training_df = store.get_historical_features(
    entity_df=entity_df,
    features=[
        "driver_hourly_stats:conv_rate",
        "driver_hourly_stats:acc_rate",
        "driver_hourly_stats:avg_daily_trips",
    ],
).to_df()

print(training_df.head())
python fetch_historical.py

DataFrame Pandas بـ 5 أعمدة (الاثنان للدخل + الـ 3 features). تحت الغطاء، Feast حمّل Parquet، عمل as-of join بـ driver_id وevent_timestamp، باحترام ttl.

الخطوة 6 — التجسيد إلى الـ online store

للاستنتاج في الإنتاج، لا نريد as-of join على Parquet لكلّ طلب: بطيء جدًّا. نريد قاعدة مفتاح/قيمة تُرجع في بضعة ميلي ثوانٍ آخر قيمة معروفة.

CURRENT_TIME=$(date -u +"%Y-%m-%dT%H:%M:%S")
feast materialize-incremental $CURRENT_TIME

Feast يعرض اسم feature view، الفترة المُعالَجة، وعدد الأسطر المكتوبة في الـ online store. أوّل تشغيل ينطلق من أقدم تاريخ في Parquet؛ التاليات من آخر cursor مُسَجَّل في registry.

الخطوة 7 — خدمة الـ online store: get_online_features

from feast import FeatureStore

store = FeatureStore(repo_path=".")

features = store.get_online_features(
    features=[
        "driver_hourly_stats:conv_rate",
        "driver_hourly_stats:acc_rate",
        "driver_hourly_stats:avg_daily_trips",
    ],
    entity_rows=[
        {"driver_id": 1001},
        {"driver_id": 1002},
    ],
).to_dict()

print(features)

زمن استجابة نمطي على SQLite محلّي: مللي ثانية. على Redis أو DynamoDB، أقلّ من مللي ثانية في الخادم. get_online_features لا يأخذ timestamp: يُرجع آخر قيمة مُجَسَّدة. إن تجاوزت آخر matérialisation الـ ttl، Feast يُرجع None.

الخطوة 8 — تهيئة online store Redis للإنتاج

docker run -d --name feast-redis -p 6379:6379 redis:7-alpine
project: driver_demo
provider: local
registry: data/registry.db
online_store:
  type: redis
  connection_string: "localhost:6379"
offline_store:
  type: file
entity_key_serialization_version: 3
feast apply
feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S")

أعد تشغيل script الخطوة 7. الخرج مطابق لكنّ القراءات تمرّ الآن عبر Redis. تحقّق بـ redis-cli MONITOR: ترى أوامر HMGET.

الخطوة 9 — تشغيل خادم features REST/gRPC

feast serve --host 0.0.0.0 --port 6566
curl -X POST http://localhost:6566/get-online-features \
  -H "Content-Type: application/json" \
  -d '{
    "features": [
      "driver_hourly_stats:conv_rate",
      "driver_hourly_stats:avg_daily_trips"
    ],
    "entities": {
      "driver_id": [1001, 1002]
    }
  }'

JSON مع القيم لكلّ entity. للأداء، انتقل إلى gRPC بـ --type grpc. في الإنتاج، الخادم يدور في pod Kubernetes مخصَّص خلف Service مع autoscaling أفقي. يقرأ registry وonline store لكن لا يكتب أبدًا.

الخطوة 10 — ربط نموذج scikit-learn

from datetime import datetime
import pandas as pd
from sklearn.linear_model import LinearRegression
from feast import FeatureStore

store = FeatureStore(repo_path=".")

# 1. مرحلة التدريب: نسترد التاريخ
entity_df = pd.DataFrame({
    "driver_id":       [1001, 1002, 1003, 1004, 1005] * 100,
    "event_timestamp": [datetime(2021, 4, 12, 10, 0, 0)] * 500,
    "trip_completed":  [1, 0, 1, 0, 1] * 100,
})

training_df = store.get_historical_features(
    entity_df=entity_df,
    features=[
        "driver_hourly_stats:conv_rate",
        "driver_hourly_stats:acc_rate",
        "driver_hourly_stats:avg_daily_trips",
    ],
).to_df().dropna()

X = training_df[["conv_rate", "acc_rate", "avg_daily_trips"]]
y = training_df["trip_completed"]
model = LinearRegression().fit(X, y)

# 2. مرحلة الاستنتاج: نسترد features online
online = store.get_online_features(
    features=[
        "driver_hourly_stats:conv_rate",
        "driver_hourly_stats:acc_rate",
        "driver_hourly_stats:avg_daily_trips",
    ],
    entity_rows=[{"driver_id": 1001}],
).to_dict()

features_for_pred = pd.DataFrame({
    "conv_rate":       online["conv_rate"],
    "acc_rate":        online["acc_rate"],
    "avg_daily_trips": online["avg_daily_trips"],
})

prediction = model.predict(features_for_pred)
print(f"Prédiction pour driver 1001 : {prediction[0]:.3f}")

نفس تعريف features في التدريب والاستنتاج. منطق حساب features يعيش مرّة واحدة، في feature views.

أخطاء شائعة

العَرَض السبب التصحيح
get_online_features يُرجع None في كلّ مكان لا matérialisation، أو TTL متجاوز أعد feast materialize-incremental بـ timestamp محدَّث
RegistryNotBuiltException عند الإقلاع feast apply لم يُشَغَّل في هذا المجلّد كن في المجلّد الذي يحوي feature_store.yaml
خلايا NaN في DataFrame التدريب TTL أقصر من الفارق بين event_timestamp وآخر feature متاحة زد ttl للـ FeatureView
ConnectionError على Redis container Redis غير مُشَغَّل docker ps، docker start feast-redis
FeatureViewNotFoundException في الخادم الخادم أُطلق قبل feast apply جديد أعد تشغيل feast serve
خلاف بين online وoffline على نفس المفتاح matérialisation جزئية أعد feast materialize كاملة

مصادر رسمية

  • توثيق Feast: https://docs.feast.dev
  • Quickstart الرسمي
  • مرجع Feature View
  • مستودع GitHub: https://github.com/feast-dev/feast

الأدلّة المرتبطة

FAQ

هل يلزم feature store عند بدء مشروع ML؟ لا. لنموذج batch منعزل في notebook، إفراط في الهندسة. يصير ضروريًّا فور (أ) نموذج يُقَدَّم لحظيًّا بـ features مشتقّة، (ب) عدّة نماذج تتشارك features، أو (ج) عدّة فِرَق على نفس الإشارات.

الفرق مع Redis مخصَّص؟ Redis مخصَّص يُقَدِّم features online، لكن لا يحلّ المشكلة الرئيسية: تعريف features في مكان آخر. Feast يُوَحِّد التعريف، التاريخ point-in-time correct، matérialisation، والخدمة خلف API وحيد.

هل Feast يحلّ محلّ data warehouse؟ لا. Feast يستند إلى warehouse (BigQuery، Snowflake، Redshift) كـ offline store. لا يُخَزِّن شيئًا جديدًا.

كيف نُؤَتمت matérialisation؟ بجدولتها كـ job batch: CronJob Kubernetes، DAG Airflow، أو Dataflow scheduler GCP يُشَغِّل feast materialize-incremental دوريًّا (5 دقائق إلى ساعة).

كيف نرفع الانتعاش تحت الدقيقة؟ بـ push sources: تكتب بنفسك في online store عبر store.push("source_name", df) فور وصول حدث. مثالي لـ features محسوبة في streaming.

وإصدارات features؟ registry Feast قابل للتسلسل؛ تضعه في Git بتوجيه registry: إلى ملفّ مُؤَرَّخ، أو تستعمل registry SQL (Postgres) مع snapshots.

مقالات ذات صلة

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité