هندسة الميزات والخطوط
“قمامة داخل، قمامة خارج — فن تحويل البيانات الخام إلى إشارات جاهزة للنموذج”
خط أنابيب المعالجة المسبقة الكامل: الإحلال والترميز الفئوي والتحجيم وإنشاء الميزات وخطوط أنابيب sklearn لتقييم خالٍ من تسرب البيانات.
المتطلبات الأساسية
المفاهيم المغطاة
∑الصيغ الرئيسية
StandardScaler
متوسط صفري، تباين وحدوي — حساس للشواذ
MinMaxScaler
القياس إلى [0,1] — يحافظ على التفرق، حساس للشواذ
RobustScaler
القياس باستخدام الوسيط وIQR — متين للشواذ
التحويل اللوغاريتمي
يضغط التوزيعات المائلة — مفيد للدخول وأعداد السكان
▶محاكاة تفاعلية
لماذا هندسة الميزات تفوز بالمسابقات
قال Andrew Ng: 'إيجاد الميزات صعب ومستهلك للوقت ويتطلب معرفة خبراء. التعلم الآلي التطبيقي هو في الأساس هندسة ميزات.' في مسابقات Kaggle، الحلول الأعلى ترتيباً تمتلك باستمرار هندسة ميزات أفضل من هندسة نماذج أفضل. نموذج خطي بميزات رائعة يتفوق على شبكة عميقة بميزات خام في معظم مسائل البيانات الجدولية. الميزات تُرمِّز المعرفة البشرية بالمجال — هي الجسر بين القياس الخام والبنية الرياضية التي يمكن للنموذج استغلالها.
في Netflix Prize (مليون دولار)، تضمنت ميزات الفريق الفائز أنماطاً زمنية معقدة وإشارات ملاحظات ضمنية للمستخدمين وتفاعلات بيانات الأفلام الوصفية — ليس تعقيد النموذج.
عقلية خط الأنابيب
فكّر في هندسة الميزات كسلسلة تحويلات: بيانات خام ← إحلال (ملء القيم الناقصة) ← ترميز (تحويل الفئات لأعداد) ← توسيع (وضع الميزات على مقاييس مقارنة) ← اختيار (حذف الميزات الصاخبة/المتكررة). يجب ضبط كل خطوة على بيانات التدريب فحسب وتطبيقها بشكل متسق على بيانات الاختبار — استخدم Pipelines من scikit-learn لضمان ذلك. الـPipeline قابل للتسلسل أيضاً، لذا معالجتك المسبقة مجمَّعة دائماً مع نموذجك للنشر.
تسريب البيانات هو أخطر ثغرة في التعلم الآلي: إذا أثّرت بيانات الاختبار في أي خطوة معالجة مسبقة، فتقييمك محسوب بشكل متفائل خاطئ. تمنع Pipelines ذلك بالتصميم.
خط الأنابيب من 5 مراحل
الإحلال: SimpleImputer (متوسط/وسيط/منوال/ثابت) أو IterativeImputer (MICE متعدد المتغيرات)
الترميز: OrdinalEncoder للفئات المرتبة، OneHotEncoder للاسمية (drop='first' لتجنب فخ المتغيرات الوهمية)
التوسيع: StandardScaler للبيانات شبه الغاوسية، RobustScaler عند وجود شواذ، MinMaxScaler للمدخلات المحدودة
إنشاء الميزات: PolynomialFeatures (x²، تفاعلات x·y)، تحليل التواريخ (يوم/شهر/يوم الأسبوع)، تحويلات المجال (log، sqrt)
الاختيار: VarianceThreshold، SelectKBest (معلومات مشتركة / chi²)، SelectFromModel (أهميات الأشجار)، RFECV
استراتيجيات ترميز البيانات الفئوية
يُنشئ ترميز One-Hot عموداً ثنائياً لكل فئة — مثالي للفئات غير المرتبة ذات القيم القليلة. مع الفئويات عالية الكثافة (المدن، الرموز البريدية، معرفات المنتجات)، يُضخِّم OHE الأبعاد. استخدم Target Encoding بدلاً: استبدل كل فئة بمتوسط قيمة الهدف لتلك الفئة. لكن Target encoding يُسرِّب إذا لم يُنجَز بأطواق تحقق متقاطع. للميزات الترتيبية (منخفض/متوسط/مرتفع)، دائماً استخدم OrdinalEncoder مع ترتيب فئات صريح.
كثافة عالية + OHE = كارثة. 10,000 رمز بريدي → 10,000 عمود، معظمها شبه فارغة. استخدم Target encoding أو طبقات التضمين أو Feature hashing بدلاً.
خيارات التوسيع وتأثيراتها
StandardScaler: يفترض التوزيع الغاوسي، يجعل mean=0 وstd=1. مطلوب لـSVMs والنماذج الخطية المنظَّمة (Lasso/Ridge) وACP وKNN والشبكات العصبية. غير مطلوب للنماذج القائمة على الأشجار (Random Forest، XGBoost — تستخدم فقط ترتيب الميزات لا حجمها). MinMaxScaler: مطلوب عندما يحتاج الخوارزم مدخلات محدودة. RobustScaler: استخدمه عند وجود شواذ — يُعيّر بالوسيط وIQR مما يجعله متيناً للقيم المتطرفة.
خط أنابيب sklearn — مثال كامل
from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.preprocessing import (StandardScaler, OneHotEncoder, RobustScaler, PolynomialFeatures) from sklearn.impute import SimpleImputer from sklearn.feature_selection import SelectFromModel from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier from sklearn.model_selection import cross_val_score, train_test_split import pandas as pd import numpy as np class="tok-comment"># ── Sample DataFrame ─────────────────────────────────────────────────── np.random.seed(class="tok-num">42) n = class="tok-num">300 df = pd.DataFrame({ class="tok-str">'age': np.random.randint(class="tok-num">18, class="tok-num">70, n).astype(float), class="tok-str">'income': np.random.exponential(class="tok-num">40000, n), class="tok-str">'score': np.random.uniform(class="tok-num">300, class="tok-num">850, n), class="tok-str">'city': np.random.choice([class="tok-str">'Paris', class="tok-str">'Lyon', class="tok-str">'Toulouse'], n), class="tok-str">'occupation': np.random.choice([class="tok-str">'engineer', class="tok-str">'teacher', class="tok-str">'doctor'], n), class="tok-str">'target': np.random.randint(class="tok-num">0, class="tok-num">2, n), }) class="tok-comment"># Add some missing values df.loc[np.random.choice(n, class="tok-num">20, replace=False), class="tok-str">'age'] = np.nan df.loc[np.random.choice(n, class="tok-num">15, replace=False), class="tok-str">'city'] = np.nan X_train = df.drop(class="tok-str">'target', axis=class="tok-num">1) y_train = df[class="tok-str">'target'] class="tok-comment"># ── Define column groups ─────────────────────────────────────────── num_features = [class="tok-str">'age', class="tok-str">'income', class="tok-str">'score'] cat_features = [class="tok-str">'city', class="tok-str">'occupation'] class="tok-comment"># ── Preprocessing for numeric columns ───────────────────────────── numeric_transformer = Pipeline([ (class="tok-str">'imputer', SimpleImputer(strategy=class="tok-str">'median')), (class="tok-str">'scaler', RobustScaler()), ]) class="tok-comment"># ── Preprocessing for categorical columns ───────────────────────── categorical_transformer = Pipeline([ (class="tok-str">'imputer', SimpleImputer(strategy=class="tok-str">'most_frequent')), (class="tok-str">'encoder', OneHotEncoder(handle_unknown=class="tok-str">'ignore', drop=class="tok-str">'first')), ]) class="tok-comment"># ── Combine with ColumnTransformer ───────────────────────────────── preprocessor = ColumnTransformer([ (class="tok-str">'num', numeric_transformer, num_features), (class="tok-str">'cat', categorical_transformer, cat_features), ]) class="tok-comment"># ── Full pipeline: preprocess → feature select → model ──────────── pipe = Pipeline([ (class="tok-str">'prep', preprocessor), (class="tok-str">'poly', PolynomialFeatures(degree=class="tok-num">2, interaction_only=True, include_bias=False)), (class="tok-str">'select', SelectFromModel(RandomForestClassifier(n_estimators=class="tok-num">50), threshold=class="tok-str">'median')), (class="tok-str">'clf', GradientBoostingClassifier(n_estimators=class="tok-num">200, learning_rate=class="tok-num">0.05)), ]) class="tok-comment"># Train / evaluate — preprocessing is always fitted on train only pipe.fit(X_train, y_train) scores = cross_val_score(pipe, X_train, y_train, cv=class="tok-num">5, scoring=class="tok-str">'roc_auc') print(fclass="tok-str">"CV AUC: {scores.mean():.3f} ± {scores.std():.3f}")
مزالق المعالجة المسبقة
ضبط المُعيِّرات على مجموعة البيانات الكاملة (قبل التقسيم) تسريب بيانات — إحصائيات الاختبار تلوث التدريب. دائماً اضبط داخل Pipeline أو على X_train فحسب. الثاني: قد يرى OneHotEncoder فئات غير مرئية في بيانات الاختبار ← استخدم handle_unknown='ignore'. الثالث: الإحلال بالمتوسط قبل التقسيم يُسرِّب متوسط الاختبار للتدريب. الرابع: الميزات متعددة الحدود تُفجِّر الذاكرة — 100 ميزة × degree=2 ← 5,050 عموداً. استخدم interaction_only=True مع اختيار الميزات لاحقاً. الخامس: Target encoding بلا تحقق متقاطع يُسرِّب معلومات الهدف.
كائن Pipeline في scikit-learn ليس مجرد وسيلة راحة — هو مطلوب للتحقق المتقاطع الصحيح. أي معالجة مسبقة 'تتعلم' من البيانات (مُعيِّرات، مشفِّرات، مُحِلّات) يجب أن تكون داخل الـpipeline.
?اختبار المعرفة
يتم حفظ التقدم في متصفحك — لا حاجة لحساب.