یکی از رایجترین و کلافهکنندهترین چالشهای مهندسی یادگیری ماشین، دیدن مدلی است که در مرحلهی ارزیابی آفلاین دقتی رویایی دارد اما به محض رفتن به محیط پروداکشن، عملکردش افت شدیدی پیدا میکند. این تناقض اغلب به دلیل پدیدهای به نام انحراف آموزش و سرویسدهی (Training-Serving Skew) و نشت اطلاعات آینده به دادههای آموزش رخ میدهد؛ جایی که منطق تولید ویژگیها در زمان آموزش با کد اجرایی در زمان سرویسدهی تفاوت دارد.
حل این مسئله نیازمند تغییری بنیادین در معماری دادههای مدل است: انتقال از کدهای پراکنده به یک ذخیرهساز ویژگی (Feature Store). ذخیرهساز ویژگی با ایجاد یک نقطهی اتکای واحد، تاریخچهی دادهها را برای آموزش و تازهترین مقادیر را برای سرویسدهی بلادرنگ مهیا میکند و جلوی دوگانگی محاسبات را میگیرد.
در این درس یاد میگیرید که چگونه با تکنیکهای کلیدی مانند پیوند مبتنی بر نقطه در زمان (Point-in-time Join)، مادیتبخشی (Materialization) و مدیریت TTL، معماری خطوط لولهی داده را ایمن کنید و مفاهیم یک ذخیرهساز ویژگی متنباز مانند Feast را از صفر پیادهسازی و تست نمایید.
هر ویژگی یک تعریف دارد و دو خواننده: آموزش، تاریخچهی آن را تا زمان هر برچسب میخواند و سرویسدهی، تازهترین مقدارش را. هرگاه این دو خواننده با هم ناسازگار شوند، مدل ورودیهایی را میبیند که هرگز روی آنها آموزش ندیده است.
اهداف یادگیری
- ذخیرهساز آفلاین، ذخیرهساز آنلاین، موجودیتها، برچسبهای زمانی رخداد، و TTL را به همراه وظیفهی هرکدام شرح دهید.
- یک پیوند مبتنی بر نقطه در زمان (point-in-time correct join) پیادهسازی کنید و نشان دهید پیوند سادهلوحانه چطور مقادیر آینده را وارد دادههای آموزش میکند.
- چهار عامل انحراف آموزش و سرویسدهی (training-serving skew) را نام ببرید: دو مسیر کدنویسی، ویژگیهای کهنه، مقادیر پیشفرض متفاوت، و سفر در زمان.
- یک رجیستری ویژگی (feature registry) بسازید که در آن یک تعریف واحد، دادههای هر دو مسیر آموزش و سرویسدهی را تأمین کند.
- یک ویژگی جدید را روی تاریخهای گذشته بازپر (backfill) کنید، بیآنکه دادههای پس از هر تاریخ خوانده شوند.
- با مقایسهی لاگ ویژگیهای سرویسدهی با مسیر آموزش برای همان موجودیت و زمان، انحراف را شناسایی کنید.
صورت مسئله
تیمی در حال ساخت یک مدل ریزش (churn) است. هر برچسب مشخص میکند که آیا کاربر در بازهی 30 روزهی پس از یک روزِ برچسب، خریدی انجام داده است یا خیر. تیم، جدول برچسبها را بر اساس user_id به جدول ویژگیهای کاربر پیوند میزند و قاعدهای روی days_since_last_purchase آموزش میدهد. دقت آفلاین مدل به 0.948 میرسد.
اما روی ترافیک زنده، عملکرد مدل از یک نسخهی قدیمیتر و سادهتر هم بدتر میشود. ماجرا این بود که جدول ویژگیها، تازهترین مقدار هر کاربر را نگه میداشت؛ یعنی مقادیری از هفتهها پس از روزهای برچسب. کاربری که آخرین خریدش در روز 115 بود، در اسنپشات روز 175، عدد 60 روز بدون فعالیت را نشان میداد؛ بنابراین ویژگی عملاً پاسخ مسئله را در خود داشت. همزمان، تیمی دیگر ویژگیهای سرویسدهی را در سرویسی مجزا از نو نوشته بود؛ پنجرهی 30 روزهی آنها یک روز اضافه داشت و مقدار پیشفرض کاربر جدید را به جای 365، عدد -1 گذاشته بودند.
ریشهی هر دو اشکال یکی است: آموزش و سرویسدهی «یک ویژگی یکسان» را به دو روش متفاوت محاسبه میکردند. در این درس، اجزای یک ذخیرهساز ویژگی (feature store) را از صفر پیادهسازی میکنید: یک رجیستری، یک بازپر، یک پیوند مبتنی بر نقطه در زمان، یک ذخیرهساز آنلاین مجهز به TTL، و یک ارزیابی انحراف. دموی این درس هر دو اشکال را با ارقام واقعی بازسازی میکند.
مفاهیم پایه
دو خواننده برای یک ویژگی
آموزش و سرویسدهی، ویژگیها را در قالبهای متفاوتی میخواهند:
| ویژگی | آموزش (آفلاین) | سرویسدهی (آنلاین) |
|---|---|---|
| پرسش | «مقدار ویژگی برای هر یک از 10 میلیون جفت (موجودیت، زمان) چقدر بوده است؟» | «مقدار ویژگی برای این یک موجودیت، همین حالا چقدر است؟» |
| زمان | نقطههای متعددی در گذشته | زمان حال |
| حجم داده | میلیونها ردیف در هر جاب | یک ردیف در هر درخواست |
| تأخیر مجاز | چند دقیقه مشکلی ندارد | چند میلیثانیه |
| ذخیرهسازی | ذخیرهساز آفلاین: تاریخچهی کامل با برچسبهای زمانی، در فایلهای ستونی یا انبار داده | ذخیرهساز آنلاین: تازهترین مقدار برای هر موجودیت، در پایگاه دادهی کلید-مقداری |
یک ذخیرهساز ویژگی، هر دو مخزن داده را بر اساس مجموعهای واحد از تعاریف ویژگی پر نگه میدارد. به جابی که تازهترین مقادیر را از ذخیرهساز آفلاین در ذخیرهساز آنلاین کپی میکند، مادیتبخشی (materialization) میگویند.
موجودیتها، برچسبهای زمانی و TTL
چهار مفهوم، هر ردیف از دادههای ویژگی را توصیف میکنند:
- موجودیت (Entity): کلیدی که ویژگی آن را توصیف میکند، مانند
user_idیاcard_id. - برچسب زمانی رخداد (Event timestamp): زمانی که مقدار در دنیای واقعی محقق شد. تعداد خریدهای یک کاربر «تا روز 14»، برچسب زمانی رخداد روز 14 را دارد.
- برچسب زمانی ایجاد (Created timestamp): زمانی که سیستم، ردیف را ثبت کرد. فرایند بازپری که امروز اجرا میشود، ردیفهایی با برچسبهای زمانی رخداد گذشته و برچسب زمانی ایجاد جدید مینویسد.
- طول عمر (TTL یا time to live): بیشترین عمر مجاز یک مقدار. مقداری که عمرش از TTL بیشتر باشد مفقود شمرده میشود و خواننده مقدار پیشفرض را به کار میبرد.
به گروهی از ویژگیها با موجودیت، منبع و TTL یکسان، معمولاً نمای ویژگی (feature view) میگویند.
پیوندهای مبتنی بر نقطه در زمان
هر ردیف آموزش، یک برچسب را با مقادیر ویژگیهایی جفت میکند که در زمان آن برچسب وجود داشتهاند. پیوند مبتنی بر نقطه در زمان این کار را برای هر برچسب انجام میدهد:
- ردیفهای ویژگی مربوط به همان موجودیت را پیدا کن.
- ردیفهایی را نگه دار که برچسب زمانی رخداد آنها در زمان برچسب یا پیش از آن است.
- تازهترین ردیف را از میان آنها انتخاب کن.
- اگر اختلاف زمان برچسب با برچسب زمانی رخداد آن بیش از TTL باشد، آن را کنار بگذار.
جدول زیر یک کاربر، یک برچسب در روز 16، و ردیفهای هفتگی ویژگی را نشان میدهد:
| روز ردیف ویژگی | purchases_30d |
پیوند مبتنی بر نقطه در زمان | پیوند سادهلوحانه روی user_id |
|---|---|---|---|
| 7 | 4 | ||
| 14 | 3 | انتخاب شد | |
| 21 | 1 | آینده | |
| 28 | 0 | آینده | انتخاب شد |
پرسش برچسب این است: آیا کاربر در روزهای 17 تا 46 خریدی انجام میدهد؟ ردیف روز 28 از قبل روزهای بیفعالیتِ پس از روز برچسب را شمرده است؛ این ردیف عدد 0 را نشان میدهد چون کاربر دیگر خریدی نکرده؛ یعنی دقیقاً همان رخدادی که برچسب اندازه میگیرد. پیوند سادهلوحانه عملاً پاسخ را داخل ورودی کپی میکند.
ما در درس سریهای زمانی دادهها را به همین دلیل بر اساس زمان جدا کردیم. پیوند مبتنی بر نقطه در زمان، این قاعده را به جای یک نقطهی برش، روی تکتک ردیفها پیاده میکند.
دلایل انحراف آموزش و سرویسدهی
راهنمای Rules of Machine Learning گوگل، انحراف آموزش و سرویسدهی را تفاوت میان عملکرد در زمان آموزش و عملکرد در زمان سرویسدهی تعریف میکند. این راهنما سه علت را برمیشمارد: پردازش متفاوت دادهها در دو خط لوله، تغییر در دادهها میان آموزش و سرویسدهی، و حلقهی بازخورد. در خطوط لولهی ویژگی، چهار علت مشخص بارها و بارها رخ میدهند:
| علت | چه رخ میدهد | نمونه در دمو |
|---|---|---|
| دو مسیر کدنویسی | ویژگی در آموزش و سرویسدهی با دو کد متفاوت محاسبه میشود | پنجرهی سرویسدهی روز now - 30 را هم در نظر میگیرد، اما پنجرهی آموزش نه |
| ویژگیهای کهنه | سن مقداری که سرویسدهی میخواند با سن مقداری که آموزش استفاده کرده متفاوت است | آموزش از اسنپشاتهای هفتگی میخواند و سرویسی جدید مقادیر تازه را محاسبه میکند |
| پیشفرضهای متفاوت | دو مسیر، مقدار گمشده را با مقادیر متفاوتی پر میکنند | آموزش برای «هنوز خریدی نداشته» عدد 365 را میگذارد و سرویسدهی عدد -1 |
| سفر در زمان | آموزش، مقداری از زمانهای بعد از برچسب را میخواند | پیوند سادهلوحانه تازهترین اسنپشات را برای همهی برچسبها برمیدارد |
قاعدهی شمارهی 32 همان راهنما تأکید میکند تا حد امکان کدها را میان خط لولهی آموزش و خط لولهی سرویسدهی بازاستفاده کنید. یک ذخیرهساز ویژگی این قاعده را با داشتن تعریفی یکتا پیاده میسازد که هر دو مسیر آن را صدا میزنند.
رجیستری ویژگی
رجیستری، کاتالوگ تعاریف ویژگیهاست. هر ورودی شامل نام، موجودیت، منبع، تبدیل، پنجره، مقدار پیشفرض، TTL، مالک، نسخه، و توضیحات است.
دو قاعده سلامت آن را حفظ میکنند:
- هر دو مسیر، تعریف را از رجیستری میخوانند. هیچ سرویسی کپی اختصاصی از تبدیل را پیش خود نگه نمیدارد.
- هر تغییری در تعریف، نسخهای تازه میسازد. نسخهی قبلی دستنخورده باقی میماند تا مدلهای قدیمی همان ویژگیهایی را داشته باشند که رویشان آموزش دیدهاند.
بازپرها
یک ویژگی تازه هیچ تاریخچهای ندارد، اما آموزش به مقدار آن در تکتک زمانهای برچسبهای قدیمی نیاز دارد. یک بازپر تعریف ویژگی را روی تکتک تاریخهای گذشته اجرا میکند و برای هر موجودیت و تاریخ، یک ردیف مینویسد. هر بار اجرا تنها رخدادهایی را میخواند که برچسبهای زمانی آنها در همان تاریخ یا پیش از آن باشد.
هزینهی پردازشی بازپر با حاصلضرب تعداد موجودیتها در تعداد تاریخها رشد میکند. این فرایند نشتی ظریفی هم به همراه دارد: بازپری که امروز اجرا میشود ردیفهایی با برچسبهای زمانی رخداد گذشته مینویسد، اما میتواند دادههایی را بخواند که با تأخیر رسیدهاند یا بعداً اصلاح شدهاند. یک پیوند سختگیرانهی مبتنی بر نقطه در زمان حتی بررسی میکند که برچسب زمانی ایجاد نیز در زمان برچسب یا پیش از آن باشد.
شناسایی انحراف
قاعدهی شمارهی 29 همان راهنما پیشنهاد میکند ویژگیها را در زمان سرویسدهی لاگ کنید و از آنها در آموزش بهره ببرید. حتی نمونهی کوچکی از لاگها امکان بررسی سازگاری را فراهم میکند. هر مقدار لاگشده را با مقداری مقایسه کنید که مسیر آموزش برای همان موجودیت و زمان تولید میکند. برای هر ویژگی، نسبت ناهمخوانیها و میانگین قدر مطلق اختلاف را گزارش دهید.
برای ریشهیابی علت، این سه جفت را مقایسه کنید:
- خواندن از ذخیرهساز آنلاین در برابر پیوند زمان آموزش. این کار کل سیستم را میآزماید و باید انحراف صفر نشان دهد.
- محاسبهی بلادرنگ در برابر پیوند زمان آموزش. این مقایسه اثر کهنگی را جدا میکند.
- مسیر دوم کدنویسی در برابر محاسبهی بلادرنگ. این مقایسه تفاوتهای کد، مانند پنجرهها و مقادیر پیشفرض را جدا میکند.
پیادهسازی
کد موجود در code/main.py تنها از کتابخانهی استاندارد استفاده میکند. زمان به صورت عددی صحیح از روزهاست. یک جاب دستهای (batch job) هفتگی، اسنپشاتهای ویژگیها را در روزهای 7، 14 و به همین ترتیب تا روز 175 ثبت میکند.
گام 1: تعاریف ویژگیها و رجیستری
کلاس FeatureDefinition تبدیل را در قالب داده نگه میدارد: یک تجمیع، یک پنجره، و یک مقدار پیشفرض. متد compute آن هرگز رخدادهای پس از as_of را نمیخواند.
@dataclass(frozen=True)
class FeatureDefinition:
name: str
entity: str
aggregate: str
window_days: int | None
default: float
version: int = 1
owner: str = "growth-team"
description: str = ""
def compute(self, events: list[Event], as_of: int) -> float:
visible = [e for e in events if e.day <= as_of]
if self.window_days is not None:
visible = [e for e in visible if e.day > as_of - self.window_days]
if self.aggregate == "count":
return float(len(visible))
if self.aggregate == "sum":
return round(sum(e.amount for e in visible), 2)
if self.aggregate == "days_since_last":
return float(as_of - max(e.day for e in visible)) if visible else self.default
raise ValueError(f"unknown aggregate {self.aggregate!r}")
متد FeatureRegistry.register تعریف یکسان را دو بار میپذیرد، اما اگر تعریفی متفاوت با نام و نسخهی یکسان بیاید، آن را رد میکند. تابع fill_defaults به هر دو مسیر مقدار پیشفرض یکسانی برای دادههای مفقود میدهد.
یک مدل با ارجاعی مانند spend_30d@1 تعریف ویژگی را سنجاق (pin) میکند. متدهای compute و fill_defaults این ارجاع را حلوفصل میکنند، و نام بدون علامت @ تازهترین نسخه را میخواند. ارجاعی که @ دارد اما شماره نسخه ندارد خطای ValueError میدهد تا یک اشتباه تایپی نتواند بیصدا تازهترین نسخه را بخواند. این ارجاع کلیدِ هر ردیف ذخیرهشده نیز هست؛ بنابراین بازپر، ذخیرهساز آنلاین و خواندن در زمان سرویسدهی همگی از تعریف سنجاقشده استفاده میکنند.
گام 2: بازپر ذخیرهساز آفلاین
def backfill(registry: FeatureRegistry, names: list[str], events: list[Event], days: list[int]) -> list[FeatureRow]:
rows = []
for entity, entity_events in sorted(group_events(events).items()):
for day in days:
rows.append(FeatureRow(entity, day, registry.compute(names, entity_events, day)))
return rows
دمو سه ویژگی را برای 299 کاربر در 25 روز اسنپشات بازپر میکند که حاصل آن 7475 ردیف است.
گام 3: پیوند مبتنی بر نقطه در زمان
ردیفهای هر موجودیت را یک بار بر اساس روز مرتب کنید. برای هر برچسب، bisect_right آخرین ردیف در روز برچسب یا پیش از آن را پیدا میکند. بررسی TTL ردیفهای بیش از حد کهنه را کنار میگذارد.
def point_in_time_join(labels: list[LabelRow], rows: list[FeatureRow], ttl_days: int | None = None) -> list[JoinedRow]:
by_entity: dict[str, list[FeatureRow]] = defaultdict(list)
for row in rows:
by_entity[row.entity].append(row)
days_index = {}
for entity, entity_rows in by_entity.items():
entity_rows.sort(key=lambda r: r.day)
days_index[entity] = [r.day for r in entity_rows]
joined = []
for label in labels:
days = days_index.get(label.entity, [])
pos = bisect.bisect_right(days, label.day) - 1
match = by_entity[label.entity][pos] if pos >= 0 else None
if match is not None and ttl_days is not None and label.day - match.day > ttl_days:
match = None
joined.append(JoinedRow(
label.entity, label.day, label.label,
match.day if match else None, match.values if match else None,
))
return joined
گام 4: سنجش نشتی پیوند سادهلوحانه
تابع naive_latest_join پیوند را تنها بر اساس موجودیت برقرار میکند؛ بنابراین برای هر برچسب، تازهترین ردیف را برمیگرداند. دمو برچسبهایی را در روزهای 100، 120 و 140 میسازد و روی هر مجموعهی دادهی آموزش، قاعدهای با یک ویژگی برازش میدهد. سپس هر دو قاعده را روی برچسبهای روز 145، با ویژگیهای مبتنی بر نقطه در زمان میآزماید:
naive join used a snapshot from after the label day for 100% of rows
offline accuracy, naive join: 0.948 (rule: idle > 53 days)
offline accuracy, PIT join: 0.893 (rule: idle > 19 days)
accuracy on day 145, trained with naive join: 0.763
accuracy on day 145, trained with PIT join: 0.876
پیوند سادهلوحانه در حالت آفلاین بهتر به نظر میرسد: 0.948 در برابر 0.893. اما روی دادههای جدید ضعیفتر است: 0.763 در برابر 0.876. ویژگی نشتکرده آستانهی برازششده را از 19 روز به 53 روز بیفعالیت جابهجا کرده است.
گام 5: ذخیرهساز آنلاین و مادیتبخشی
ذخیرهساز آنلاین تنها یک ردیف به ازای هر موجودیت نگه میدارد و ردیفهای قدیمیتر از TTL را رد میکند. متد materialize تنها ردیفهایی را مینویسد که روز آنها در زمان اجرای جاب یا پیش از آن باشد.
class OnlineStore:
def __init__(self, ttl_days: int):
self.ttl_days = ttl_days
self._latest: dict[str, FeatureRow] = {}
def write(self, row: FeatureRow) -> None:
current = self._latest.get(row.entity)
if current is None or row.day >= current.day:
self._latest[row.entity] = row
def read(self, entity: str, now: int) -> dict[str, float] | None:
row = self._latest.get(entity)
if row is None or row.day > now or now - row.day > self.ttl_days:
return None
return dict(row.values)
با در نظر گرفتن TTL معادل 10 روز، خواندن داده در روز 145 ردیف روز 140 را برمیگرداند. خواندن در روز 160، بدون مادیتبخشی جدید، مقادیر پیشفرض را برمیگرداند، چون ردیف 20 روزه شده است.
گام 6: سنجش انحراف
تابع detect_skew دو نما را که کلیدشان (entity, day) است مقایسه میکند. این تابع نرخ ناهمخوانی و میانگین قدر مطلق اختلاف را برای هر ویژگی گزارش میدهد و اگر بیش از 1 درصد مقادیر تفاوت داشته باشند، ویژگی را علامت میزند. اگر این دو نما کلیدهای متفاوتی داشته باشند یا هیچ کلیدی نداشته باشند، خطای ValueError میدهد؛ چرا که غیبت یک درخواست میتواند انحراف را پنهان کند و بررسی بدون درخواست هم چیزی نشان نمیدهد.
def detect_skew(
training: dict[tuple[str, int], dict[str, float]],
serving: dict[tuple[str, int], dict[str, float]],
features: list[str],
tolerance: float = 1e-6,
max_mismatch_rate: float = 0.01,
) -> list[SkewReport]:
if set(training) != set(serving):
raise ValueError(f"{len(set(training) ^ set(serving))} keys are in only one view: log and compare the same requests")
if not training:
raise ValueError("no requests to compare")
keys = sorted(training)
reports = []
for feature in features:
diffs = [abs(training[k][feature] - serving[k][feature]) for k in keys]
mismatches = sum(1 for d in diffs if d > tolerance)
rate = mismatches / len(keys)
mean_diff = sum(diffs) / len(keys)
reports.append(SkewReport(feature, len(keys), rate, mean_diff, rate > max_mismatch_rate))
return reports
دمو درخواستهای روزهای 141 تا 150 را لاگ میکند و سه مقایسهی شرح داده شده در بخش مفاهیم را اجرا میکند. استخر درخواستها شامل 20 کاربر جدید بدون تاریخچه است.
python3 code/main.py
=== Skew checks on 383 logged requests, days 141 to 150 ===
online store read vs training join (one definition, same snapshots)
ok days_since_last_purchase mismatch 0.0% mean |diff| 0.00
ok purchases_30d mismatch 0.0% mean |diff| 0.00
ok spend_30d mismatch 0.0% mean |diff| 0.00
fresh compute vs training join (staleness only)
SKEW days_since_last_purchase mismatch 83.0% mean |diff| 2.77
SKEW purchases_30d mismatch 30.5% mean |diff| 0.38
SKEW spend_30d mismatch 37.1% mean |diff| 16.77
second code path vs fresh compute (code differences only)
SKEW days_since_last_purchase mismatch 5.5% mean |diff| 20.07
SKEW purchases_30d mismatch 12.5% mean |diff| 0.13
SKEW spend_30d mismatch 12.5% mean |diff| 5.09
این سه بلوک را به ترتیب بخوانید:
- یک تعریف، اسنپشاتهای یکسان: ذخیرهساز آنلاین و پیوند زمان آموزش، ردیفهای مادیتیافتهی یکسانی را میخوانند؛ در نتیجه انحراف صفر است.
- کهنگی: مقدار بلادرنگ در بیشتر درخواستها با اسنپشات هفتگی فرق دارد. در اینجا دادهی تازهتر لزوماً بهتر نیست، چون مدل بر اساس مقادیری آموزش دیده که تا شش روز کهنگی داشتهاند.
- تفاوتهای کد: مسیر دوم کدنویسی در 12.5 درصد از شمارشهای پنجره تفاوت دارد؛ چرا که پنجرهی آن یک روز اضافه دارد. این مسیر در
days_since_last_purchaseفقط برای کاربران جدید تفاوت دارد، اما با اختلافی بزرگ: -1 در برابر 365.
کاربرد در عمل
Feast یک ذخیرهساز ویژگی متنباز است. هر قطعه از کدی که از صفر نوشتیم، معادلی در Feast دارد:
| پیادهسازی از صفر | Feast |
|---|---|
FeatureDefinition، FeatureRegistry |
Entity، FeatureView و Field در یک مخزن ویژگی، ثبتشده با feast apply |
تاریخچهی FeatureRow |
یک ذخیرهساز آفلاین؛ مثلاً FileSource روی فایلهای Parquet یا جدولی در انبار داده |
point_in_time_join با TTL |
store.get_historical_features(entity_df=..., features=[...]) |
materialize |
feast materialize-incremental <end time> |
OnlineStore.read |
store.get_online_features(features=[...], entity_rows=[...]) |
| بررسی برچسب زمانی ایجاد | created_timestamp_column در منبع و filter_by_created_timestamp=True |
تعاریف به این شکل نوشته میشوند. این کد طبق راهنمای شروع سریع Feast نوشته شده و برای اجرای این درس الزامی نیست:
from datetime import timedelta
from feast import Entity, FeatureView, Field, FileSource
from feast.types import Float32, Int64
user = Entity(name="user", join_keys=["user_id"])
user_activity_source = FileSource(
name="user_activity_source",
path="data/user_activity.parquet",
timestamp_field="event_timestamp",
created_timestamp_column="created",
)
user_activity = FeatureView(
name="user_activity",
entities=[user],
ttl=timedelta(days=10),
schema=[
Field(name="purchases_30d", dtype=Int64),
Field(name="spend_30d", dtype=Float32),
Field(name="days_since_last_purchase", dtype=Int64),
],
source=user_activity_source,
)
سپس آموزش و سرویسدهی هر دو از یک نمای ویژگی میخوانند:
from feast import FeatureStore
store = FeatureStore(repo_path=".")
training_df = store.get_historical_features(
entity_df=labels_df,
features=["user_activity:purchases_30d", "user_activity:days_since_last_purchase"],
filter_by_created_timestamp=True,
).to_df()
online = store.get_online_features(
features=["user_activity:purchases_30d", "user_activity:days_since_last_purchase"],
entity_rows=[{"user_id": 42}],
).to_dict()
دیتافریم labels_df به ازای هر برچسب شامل یک ردیف با ستونهای user_id، event_timestamp و خود برچسب است. مستندات Feast بیان میکند که TTL نسبت به هر برچسب زمانی در دیتافریم موجودیت سنجیده میشود، نه نسبت به زمان کوئری. این دقیقاً مطابق با بررسی TTL در متد point_in_time_join ما است. همان صفحه filter_by_created_timestamp=True را توصیف میکند که مانع نشت مقادیر بازپرشده به دادههای آموزش میشود. همهی ذخیرهسازهای آفلاین از این گزینه پشتیبانی نمیکنند؛ ذخیرهسازی که فاقد پشتیبانی باشد به جای نادیده گرفتن گزینه، خطا میدهد.
ابزارهای Tecton، Hopsworks و ذخیرهسازهای ویژگی در پلتفرمهای ابری بزرگ یادگیری ماشین از همین تفکیک ذخیرهساز آفلاین و آنلاین استفاده میکنند. برای بررسی انحراف، TensorFlow Data Validation میتواند دادههای آموزش را با دادههای سرویسدهی مقایسه کند تا انحراف شناسایی شود. برک و همکاران سیستم اعتبارسنجی داده در TFX (پلتفرم سرتاسری یادگیری ماشین گوگل) را توصیف کردهاند که انحراف آموزش و سرویسدهی را نیز شامل میشود.
خروجی کاربردی
حاصل این درس فایل outputs/skill-feature-skew-audit.md است. این مهارت، یک خط لولهی ویژگی را از نظر سفر در زمان و انحراف آموزش و سرویسدهی بازرسی میکند. این راهنما پیوند، تعاریف، مقادیر پیشفرض، تازگی هر مسیر و لاگگیری مورد نیاز برای بررسی انحراف را گامبهگام مرور میکند.
تمرینها
- بازهی زمانی اسنپشاتها را از 7 روز به 1 روز تغییر دهید. مقایسهی کهنگی را دوباره اجرا کنید و نتیجه را توضیح دهید.
- مقدار TTL را روی 3 روز بگذارید. تعداد ردیفهای آموزشی را که ویژگیهای خود را از دست میدهند و مقدار پیشفرض میگیرند بشمارید.
- روز ایجاد (created day) را به
FeatureRowاضافه کنید. کاری کنید پیوند دادهها ردیفهای ثبتشده پس از روز برچسب را نادیده بگیرد. - ویژگی
spend_7dرا به عنوان نسخهی 2 ویژگیspend_30dثبت کنید. آن را بازپر کنید و هر دو نسخه را از رجیستری چاپ کنید. - تابع
legacy_serving_featuresرا تا جایی اصلاح کنید که سومین بررسی انحراف با موفقیت پاس شود. هر تغییر را فهرست کنید.
اصطلاحات کلیدی
| اصطلاح | اصطلاح رایج | معنای دقیق |
|---|---|---|
| ذخیرهساز ویژگی (Feature store) | «پایگاه دادهی ویژگیها» | سیستمی که ویژگیها را از روی مجموعهای واحد از تعاریف محاسبه میکند و به آموزش و سرویسدهی تحویل میدهد |
| ذخیرهساز آفلاین (Offline store) | «دادههای آموزش» | تاریخچهی کامل ویژگیها همراه با برچسبهای زمانی رخداد، که جابهای آموزش و بازپر آن را میخوانند |
| ذخیرهساز آنلاین (Online store) | «کش ویژگیها» | تازهترین مقدار برای هر موجودیت، که مسیر سرویسدهی ظرف چند میلیثانیه آن را میخواند |
| موجودیت (Entity) | «شناسه» | کلیدی که یک ویژگی توصیف میکند؛ مانند کاربر یا کارت |
| برچسب زمانی رخداد (Event timestamp) | «زمان وقوع» | زمانی که مقدار یک ویژگی محقق شد |
| پیوند مبتنی بر نقطه در زمان (Point-in-time join) | «پیوند as-of» | پیوندی که هر برچسب را با تازهترین ردیف ویژگی در زمان برچسب یا پیش از آن جفت میکند |
| طول عمر (TTL) | «انقضا» | حداکثر عمر یک مقدار پیش از آنکه خواننده آن را مفقود به شمار آورد |
| مادیتبخشی (Materialization) | «همگامسازی با آنلاین» | جابی که تازهترین مقادیر ویژگی را از ذخیرهساز آفلاین در ذخیرهساز آنلاین کپی میکند |
| بازپر (Backfill) | «محاسبهی تاریخچه» | اجرای تعریف یک ویژگی در تاریخهای گذشته برای ساخت تاریخچهی آن |
| انحراف آموزش و سرویسدهی (Training-serving skew) | «در آفلاین که کار میکرد» | تفاوت میان ویژگیها یا رفتار در آموزش و سرویسدهی |
برای مطالعهی بیشتر
- Feast، پیوندهای مبتنی بر نقطه در زمان: مفاهیم TTL و فیلتر برچسب زمانی ایجاد
- مستندات Feast: نماهای ویژگی، ذخیرهسازهای آفلاین، ذخیرهسازهای آنلاین و مادیتبخشی
- گوگل، راهنمای Rules of Machine Learning: قواعد مربوط به انحراف آموزش و سرویسدهی، از قاعدهی شمارهی 29 تا 37
- برک و همکاران، Data Validation for Machine Learning (SysML 2019): اعتبارسنجی دادهها در گوگل، از جمله انحراف آموزش و سرویسدهی
- راهنمای TensorFlow Data Validation: بررسی شِما، شناسایی انحراف و شناسایی رانش
- اسکالی و همکاران، Hidden Technical Debt in Machine Learning Systems (NeurIPS 2015): وابستگیهای دادهای و بدهی خط لوله در اطراف یک مدل
