الوحدة 10 — خطوط معالجة قابلة للتكرار مع scikit-learn
عشر وحدات من التحويلات. وهي إن طُبّقت يدويًّا في دفتر شكّلت قنبلة موقوتة: ترتيب تنفيذ تنساه، وخليّة تُشغّل مرّتَين، وتعويض مضبوط على البيانات كاملةً. وتحوّل هذه الوحدة كلّ ما سبق إلى كائن واحد قابل للتكرار، حصينٍ بنيويًّا على التسرّب.
مشكلة المعالجة المسبقة اليدوية
ثلاثة عيوب، تظهر كلّها في أسوأ لحظة.
أوّلًا التسرّب: فاستخدام fit_transform على البيانات كاملةً قبل التقسيم هو الخطأ المُشار إليه في كلّ وحدة من هذه الدورة. وثانيًا الفارق بين ا لتدريب والإنتاج: فالتحويلات نفسها، بالترتيب نفسه، بالوسائط المتعلّمة نفسها، يجب أن تُعاد على كلّ بيانة جديدة — وإعادة ذلك يدويًّا في برنامج آخر تضمن الاختلاف. وثالثًا تعذّر التكرار: فدفترٌ نُفّذت خلاياه بترتيب غير خطّيّ لم يعد قابلًا للإعادة، ولا من مؤلّفه.
Pipeline: شيء واحد تضبطه وشيء واحد تطبّقه
يسلسل Pipeline التحويلات والنموذج النهائي في كائن يعرض الواجهة نفسها التي يعرضها نموذج بسيط.
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
pipe = Pipeline([
("التعويض", SimpleImputer(strategy="median")),
("المقياس", StandardScaler()),
("النموذج", LogisticRegression()),
])
pipe.fit(X_train, y_train) # يضبط كل خطوة على التدريب وحده
pipe.predict(X_test) # يعيد التحويلات نفسها بالضبط
والآليّة هي ما يجعل التسرّب مستحيلًا. فـ pipe.fit ينادي fit_transform على كلّ خطوة ببيانات التدريب وحدها، ثمّ ينادي pipe.predict الدالّة transform — لا fit أبدًا — على البيانات الجديدة. فوسيط التعويض ومتوسّط التقييس يأتيان من التدريب بالضرورة.
ويصير العائد حاسمًا في التحقّق المتقاطع: إذ تُعاد ملاءمة المعالجة المسبقة داخل كلّ ثنية، على بيانات تدريب هذه الثنية وحدها. وهذا هو العلاج البنيويّ المُعلَن في الدورة 04 والمُذكَّر به في كلّ وحدة من هذه الدورة.
from sklearn.model_selection import cross_val_score, GridSearchCV
cross_val_score(pipe, X_train, y_train, cv=5) # لا تسرّب ممكنًا
GridSearchCV(pipe, {
"التعويض__strategy": ["median", "mean"],
"النموذج__C": [0.1, 1, 10],
}, cv=5) # المعالجة المسبقة نفسها تصير وسيطًا فائقًا
وتستحقّ هذه الإمكانية الأخيرة الملاحظة: فاستراتيجية التعويض أو نوع تغيير المقياس يُضبَط كأيّ وسيط فائق، بالصيغة اسم_الخطوة__الوسيط. فتكفّ اختيارات الوحدتَين 2 و3 عن كونها حدوسًا وتصير قرارات مقيسة.
ColumnTransformer: معالجات مختلفة حسب نوع العمود
مجموعة البيانات الواقعية غير متجانسة: فالأعمدة العددية تقتضي تعويضًا وتغييرًا للمقياس، والفئوية ترميزًا، والنصّ تمثيلًا متجهيًّا. ويطبّق ColumnTransformer معالجةً مختلفة على كلّ مجموعة ثمّ يلصق النتيجة.
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder
عددية = ["العمر", "الدخل", "القدم"]
فئوية = ["المدينة", "نوع_العقد"]
pre = ColumnTransformer([
("عدد", Pipeline([
("التعويض", SimpleImputer(strategy="median")),
("المقياس", StandardScaler()),
]), عددية),
("فئة", Pipeline([
("التعويض", SimpleImputer(strategy="constant", fill_value="مجهول")),
("الترميز", OneHotEncoder(handle_unknown="ignore")),
]), فئوية),
], remainder="drop")
pipe = Pipeline([("pre", pre), ("النموذج", LogisticRegression())])
ونقطتان للحيطة. فـ remainder="drop" يُقصي صراحةً الأعمدة غير المدرجة؛ وهو السلوك الحصيف الذي يمنع تسلّل معرّف إلى النموذج. ويبقى handle_unknown="ignore" لا غنى عنه، فظهور فئة مجهولة في الإنتاج هو القاعدة لا الاستثناء.
تصنيع تحويل مخصّص
المتغيّرات المبنيّة في الوحدتَين 5 و7 — الدورات والفوارق عن العادة — لا وجود لها في scikit-learn. ولإدخالها في الخطّ لا بدّ من تغليفها.
وأبسط الحالات، حين لا يتعلّم التحويل شيئًا من البيانات:
from sklearn.preprocessing import FunctionTransformer
import numpy as np
def ترميز_دوري(X):
return np.column_stack([np.sin(2 * np.pi * X / 12), np.cos(2 * np.pi * X / 12)])
FunctionTransformer(ترميز_دوري)
أمّا إذا كان على التحويل أن يتعلّم شيئًا من التدريب (متوسّطات لكلّ عميل، أو مفردات)، فلا بدّ من كتابة صنف بدالّتَي fit وtransform بالوراثة من BaseEstimator وTransformerMixin. وهذه هي الوسيلة الوحيدة لضمان أنّ الإحصاءات المتعلّمة تأتي من التدريب وتُعاد بحرفيّتها في الإنتاج.
الحفظ والنشر
يُسلسَل الخطّ كاملًا في ملفّ واحد، بمعالجته المسبقة ونموذجه معًا:
import joblib
joblib.dump(pipe, "modele.joblib")
pipe = joblib.load("modele.joblib")
pipe.predict(البيانات_الجديدة) # خامّة غير معالَجة
وهذه غاية الدورة: فخدمة التنبّؤ تتلقّى بيانات خامّة وتنادي predict. فلا تحويل يُعاد، فلا اختلاف ممكنًا بين التدريب والإنتاج.
يعالج Pipeline الصفوف معالجةً مستقلّة. فمتغيّرات الوحدة 7 التي تقتضي تاريخًا أو فرزًا زمنيًّا — groupby وshift وrolling — لا تندمج فيه اندماجًا طبيعيًّا: بل تُحسَب سابقًا، في خطوة تحضير بيانات مُصدَّرة النسخ ومُختبَرة، أو في مخزن متغيّرات (وهو موضوع الدورة 33). ثمّ إنّ إصدارات المكتبات تهمّ: فكائنٌ سُلسِل بإصدار من scikit-learn لا يُحمَّل بالضرورة بإصدار آخر. فلا بدّ من تثبيت الإصدارات وحفظ ملفّ تَبعيّات مع النموذج.
الخلاصة
- المعالجة المسبقة اليدوية تُعرّضك لـالتسرّب والفارق بين التدريب والإنتاج وتعذّر التكرار.
- يضبط
Pipelineببيانات التدريب وحدها ويعيد التحويلات بحرفيّتها؛ وفي التحقّق المتقاطع تُعاد ملاءمة المعالجة داخل كلّ ثنية. - يطبّق
ColumnTransformerمعالجات مختلفة حسب نوع العمود؛ وremainder="drop"وhandle_unknown="ignore"يحميان الإنتاج. - تندمج التحويلات المخصّصة بـ
FunctionTransformerأو بصنفfit/transform؛ ويُسلسَل الخطّ كاملًا، ويجب تثبيت إصدارات المكتبات.
الخطوة الأخيرة: المراجعة واختبار الأربعين سؤالًا الذي يُصادق على الدورة كلّها.