اختبر كما كنت ستتداول
التقسيم العشوائي إلى صفوف تدريب واختبار كما في التعلم الآلي العادي يفشل مع السلاسل الزمنية: فمجموعة التدريب تحوي أيامًا بعد أيام الاختبار، والأيام المتجاورة مترابطة، فيكون الاختبار نسخة جزئية من التدريب. والتحقق المتدرج (walk-forward) يحترم الزمن: تدريب على كتلة من الماضي واختبار على الكتلة التالية لها مباشرة ثم إزاحة النافذة والتكرار. وكل تنبؤ يصدر عن نموذج لم ير المستقبل قط، وهذا هو التداول الحي بالضبط.
import numpy as np
n, train_size, test_size = 10, 4, 2
start = 0
while start + train_size + test_size <= n:
train = np.arange(start, start + train_size)
test = np.arange(start + train_size, start + train_size + test_size)
print("train", train.tolist(), "test", test.tolist())
start += test_size # slide forward by one test blockمتحرك أو متوسع، والحظر
هناك نكهتان. النافذة المتحركة تثبّت طول التدريب بإسقاط أقدم الصفوف: تتكيف مع تغير الأنظمة بكلفة ثابتة. أما النافذة المتوسعة فتبدأ دائمًا من الصف 0 فيكبر التدريب: تستخدم كل المشاهدات لكن البيانات القديمة قد تكون بالية. والعنصر الثالث هو الفجوة (أو الحظر): فإذا نظرت سمة أو الهدف h يومًا إلى الخلف أو الأمام فإن الصفوف السابقة مباشرة لفترة الاختبار تشاركها المعلومات، فيُتخطى بعض الصفوف بين التدريب والاختبار.
def walk_forward_splits(n, train_size, test_size, gap=0):
splits, start = [], 0
while start + train_size + gap + test_size <= n:
train = list(range(start, start + train_size))
test = list(range(start + train_size + gap, start + train_size + gap + test_size))
splits.append((train, test))
start += test_size
return splits
for train, test in walk_forward_splits(12, 5, 2, gap=1):
print(train, "->", test) # the row between them is skippedتقييم كل طيّة
لكل طيّة نضبط على صفوف التدريب ونقيّم على صفوف الاختبار، فنحصل على خطأ لكل طيّة بدل رقم واحد: ويدل تشتتها على ثبات النموذج عبر الزمن. وخط الأساس المفيد هو التنبؤ البسيط، وهنا متوسط التدريب لكل يوم اختبار. اعرض الأخطاء طيّة بطيّة: فنموذج جيد في المتوسط لكنه سيئ جدًا في نظام واحد ليس كنموذج مقبول باستمرار.
import numpy as np
def walk_forward_splits(n, train_size, test_size):
out, start = [], 0
while start + train_size + test_size <= n:
out.append((np.arange(start, start + train_size), np.arange(start + train_size, start + train_size + test_size)))
start += test_size
return out
rng = np.random.default_rng(2)
returns = rng.normal(0.0004, 0.01, size=300)
errors = []
for train, test in walk_forward_splits(len(returns), 100, 50):
errors.append(np.mean((returns[test] - returns[train].mean()) ** 2))
print(np.round(np.array(errors) * 1e4, 3)) # squared error in basis points squared, fold by foldاكتشاف تسرب
التقسيم نظيف فقط إذا سبق كل صف تدريب بصرامة كل صف اختبار. وفحص train.max() < test.min() سطر واحد ويصطاد أشيع الأخطاء: فهرس غير مرتب أو مخلوط، أو شريحة مزاحة بواحد، أو تقسيم من دالة مكتبة تخلط افتراضيًا. ضع هذا الفحص داخل الشيفرة التي تبني الطيّات ليوقف التسرب التنفيذ بدل أن ينتج نتيجة مجاملة.
import numpy as np
def leaks_future(train_idx, test_idx):
return bool(np.max(train_idx) >= np.min(test_idx))
train = np.arange(0, 100)
print(leaks_future(train, np.arange(100, 120))) # False: clean
shuffled = np.random.default_rng(1).permutation(120) # what a shuffling splitter does
print(leaks_future(shuffled[:100], shuffled[100:])) # True: training rows after test rows- Walk-forward: تدريب على كتلة من الماضي واختبار على التالية ثم إزاحة؛ كل صف اختبار يأتي بعد كل صفوف تدريبه.
- النوافذ المتحركة تثبّت حجم التدريب؛ والمتوسعة تبدأ من الصف 0؛ وفجوة (حظر) تفصل التدريب عن الاختبار حين تمتد السمات على عدة أيام.
- قيّم كل طيّة على حدة وانظر إلى التشتت؛ وافحص
train.max() < test.min()؛ ولا تضبط أبدًا على الطيّات التي تبلغ عنها.
التالي: دفتر الأوامر المحددة، بنية البيانات التي تتكون فيها الأسعار فعلًا.
المختبر: ما الذي يُقيَّم
- حرّك نافذة عبر الزمن باستخدام
walk_forward_splits25 نقطة - احتفظ بكل التاريخ باستخدام
expanding_splits25 نقطة - اختبر تنبؤًا طيّة بطيّة باستخدام
forecast_mse25 نقطة - اكتشف التسرب باستخدام
leaks_future25 نقطة