قراية
وضع الحكايات

الـ SVM في التطبيق

~22 دقيقة قراية الدرس 3 من 4 في الوحدة 4

من النظرية للـ code

الدرسين اللي فاتوا بنوا النظرية الكاملة للـ support vector machines (آلات المتجهات الداعمة): الـ maximum margin classifier، والـ slack variables (متغيرات الاسترخاء)، والـ kernel trick، والصياغة الـ dual. ودلوقتي جه وقت نستخدم الـ SVM على بيانات حقيقية. في الدرس ده هنمشي في خطوات الشغل العملي كاملة: الـ preprocessing، والتدريب، وظبط الـ hyperparameters بالـ grid search، والـ cross-validation، والتقييم.

هنستخدم scikit-learn، الـ library المعتادة للـ Machine Learning في Python، اللي بتنفّذ الـ SVM عن طريق الـ classes SVC (SVM بـ kernel) وLinearSVC (SVM خطي متحسّن للبيانات الكبيرة).

ليه الـ preprocessing مهم في الـ SVM

الـ SVMs مش محايدة ناحية المقياس. حسابات الـ margin والـ kernel بتعتمد على المسافات الإقليدية. feature بالكيلومتر هتسيطر على feature بالمتر، وده بيميّل الـ decision boundaries. اعمل standardization للـ features بتاعتك دايمًا قبل ما تدرّب SVM.

الوصفة المعتادة هي الـ standardization لمتوسط صفر وvariance واحد باستخدام StandardScaler:

from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.svm import SVC

# اعمل الـ scaling دايمًا جوه Pipeline عشان تتجنّب الـ data leakage
pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('svm', SVC(kernel='rbf', C=1.0, gamma='scale'))
])

استخدام Pipeline مهم جدًا: بيضمن إن الـ scaler بيتدرّب على بيانات التدريب بس ويتطبّق بنفس الشكل على folds الـ validation والـ test. وإنك تدرّب الـ scaler على الـ dataset كلها قبل التقسيم — غلطة مشهورة — بيسرّب إحصائيات الـ test set للتدريب، فبيطلّع تقديرات accuracy متفائلة زيادة عن اللزوم.

الـ data leakage

الـ data leakage (تسرب البيانات) من أشهر مصادر النتايج المتفائلة كدب في الـ Machine Learning. لما تعمل scaling للبيانات باستخدام الـ dataset كلها قبل التقسيم، إحصائيات الـ test set بتأثّر على الـ scaler، اللي بدوره بيأثّر على التدريب. والـ Pipeline بيتجنّب ده بإنه يضمن إن الـ preprocessing بيتدرّب من جديد جوه كل fold في الـ cross-validation.

الـ API بتاع SVC

أهم الـ parameters بتاعة SVC:

الـ parameter الافتراضي المعنى
kernel 'rbf' نوع الـ kernel: 'linear'، 'poly'، 'rbf'، 'sigmoid'، أو function خاصة
C 1.0 قوة الـ regularization. C أصغر → margin أوسع، وأخطاء مسموحة أكتر
gamma 'scale' الـ bandwidth بتاع RBF/poly/sigmoid. 'scale' = 1/(n_features · X.var())
degree 3 درجة الـ polynomial kernel بس
probability False يشغّل تقديرات الاحتمال عن طريق Platt scaling (أبطأ)
class_weight None خليه 'balanced' للفئات اللي مش متوازنة

الـ multi-class classification

الـ SVM في الأساس classifier ثنائي — بيلاقي hyperplane واحد بيفصل بين فئتين. وعشان يتعامل مع عدد فئات K > 2، المسألة بتتفكّ لكذا مسألة ثنائية صغيرة بطريقة من طريقتين.

الـ one-vs-one (واحد-ضد-واحد) (one-vs-one, OvO) بيدرّب classifier واحد لكل جوز فئات — بعدد K(K−1)/2 منهم. ووقت التنبؤ كل classifier بيدّي صوته لواحدة من فئتيه، والفئة اللي ليها أصوات أكتر بتكسب. وده اللي SVC بيعمله من جوه: الـ solver بتاع LIBSVM دايمًا one-vs-one، والـ parameter decision_function_shape بيعيد تشكيل درجات الـ output بس — ومش بيغيّر طريقة التدريب. كل مسألة صغيرة بتشوف الفئتين بتوعها بس، فالـ fits الفردية رخيصة، بس عدد الـ classifiers بيكبر تربيعيًا مع K.

الـ one-vs-rest (واحد-ضد-البقية) (one-vs-rest, OvR) — واسمه كمان one-vs-all — بيدرّب classifier واحد لكل فئة، الإجمالي K، كل واحد بيفصل فئته عن كل الفئات التانية مع بعض. والفئة اللي الـ decision function بتاعتها أعلى بتكسب. وLinearSVC بيستخدم OvR افتراضيًا. محتاج K classifiers بس (خطي في عدد الفئات)، بس كل واحد بيتدرّب على الـ dataset كلها.

OvO قصاد OvR: الـ trade-off

الـ one-vs-one بيبني عدد classifiers من رتبة O(K²)، كل واحد على subset صغيرة من فئتين؛ والـ one-vs-rest بيبني عدد classifiers من رتبة O(K)، كل واحد على البيانات كلها. ومع عدد فئات قليل OvO هو المشهور (وهو الافتراضي لـ SVC بـ kernel)؛ ولما K تبقى كبيرة، OvR بيخلّي عدد الـ models معقول. وفي الحالتين scikit-learn بتعمل التفكيك بدالك — فإنت لسه بتنادي fit وpredict مرة واحدة.

استراتيجية الـ cross-validation

تقسيمة واحدة للبيانات بتدّي تقدير فيه noise لأداء الـ generalization. الـ k-fold cross-validation (التحقق المتقاطع) (عادةً k = 5 أو 10) بيقسم البيانات لـ k fold، يدرّب على k-1 fold، ويقيّم على الـ fold اللي على جنب، ويلف على كل الـ k احتمالات. والدرجة اللي بتتبلّغ هي المتوسط (ومعاه الـ standard deviation لو حبيت) على الـ folds.

درجة الـ CV
\hat{S}_{\text{CV}} = \frac{1}{k}\sum_{i=1}^{k} S_i
تقدير الـ cross-validation بياخد متوسط الدرجة على k folds اتشالوا على جنب. كل fold بيستخدم model متدرّب على الـ k-1 fold الباقيين، والـ preprocessing كله بيتعاد تطبيقه جوه كل تقسيمة.
from sklearn.model_selection import cross_val_score, StratifiedKFold

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(pipe, X_train, y_train, cv=cv, scoring='accuracy')
print(f"CV accuracy: {scores.mean():.3f} ± {scores.std():.3f}")

الـ StratifiedKFold بيحافظ على نسب الفئات في كل fold، وده مهم للبيانات اللي مش متوازنة. والـ shuffle قبل التقسيم بيمنع توزيع folds يعتمد على ترتيب البيانات.

البحث عن الـ hyperparameters: C وγ

أهم اتنين hyperparameters للـ SVM بـ RBF kernel هما C وγ. وهما بيتفاعلوا: الـ C الأمثل بيعتمد على γ والعكس. ومعنى ده إنك لازم تدوّر على grid من أزواج (C، γ)، مش تظبط كل واحد لوحده.

دوّر على مقياس لوغاريتمي: استجابة الـ SVM لـ C وγ تقريبًا خطية لوغاريتميًا، فاختبار قيم زي {0.01، 0.1، 1، 10، 100} بيفيد أكتر من grid خطي.

from sklearn.model_selection import GridSearchCV
import numpy as np

param_grid = {
    'svm__C':     np.logspace(-2, 3, 6),
    'svm__gamma': np.logspace(-4, 1, 6),
}

search = GridSearchCV(
    pipe, param_grid, cv=5, scoring='f1_weighted',
    n_jobs=-1, verbose=1
)
search.fit(X_train, y_train)
print(search.best_params_)

خد بالك من الـ syntax بتاع الـ underscore المزدوجة 'svm__C': كده GridSearchCV بيعدّي الـ parameters لخطوة ليها اسم جوه الـ Pipeline. اسم الخطوة هو المفتاح اللي اديته في الـ constructor بتاع الـ Pipeline (هنا 'svm')، واسم الـ parameter بييجي بعد الـ underscoreين.

الـ random search للـ grids الأوسع

لو الـ grid بتاعك كبير (parameters كتير، مدى واسع)، RandomizedSearchCV أكفأ من الـ grid search الشامل. بياخد عدد ثابت من تركيبات الـ parameters من التوزيعات اللي حدّدتها، وغالبًا بيلاقي حل قريب من الأمثل في جزء من الوقت. استخدمه لما الـ grid search ياخد ساعات.

تقييم أداء الـ SVM

الـ accuracy (الدقة) metric مضلّل لما الفئات تبقى مش متوازنة. classifier بيتنبأ دايمًا بالفئة الأكتر ممكن يجيب accuracy 90% على تقسيمة 90/10 وهو مالوش أي لازمة للفئة الأقل. استخدم الـ precision، والـ recall، والـ F1، والـ confusion matrix (مصفوفة الالتباس) بدلها.

from sklearn.metrics import classification_report, confusion_matrix

best_model = search.best_estimator_
y_pred = best_model.predict(X_test)

print(classification_report(y_test, y_pred))
print(confusion_matrix(y_test, y_pred))

الـ classification_report بيوري الـ precision والـ recall والـ F1 لكل فئة مع المتوسطات الـ macro والـ weighted. وفي المسائل الثنائية، اختار scoring metric ماشي مع هدف النشر: الـ precision لو الـ false positives مكلّفة، والـ recall لو الـ false negatives مكلّفة، والـ F1 لو محتاج توازن.

درجات القرار والاحتمالات المتعايرة

افتراضيًا، SVC.predict() بيرجّع class label قاطع. وتقدر تاخد القيمة الخام للـ decision function (المسافة بالإشارة للـ margin) عن طريق decision_function(). ولتقديرات الاحتمال، خلّي probability=True في الـ constructor: scikit-learn بتستخدم الـ Platt scaling (تحجيم Platt) — بتدرّب logistic regression فوق outputs الـ SVM باستخدام cross-validation داخلي — عشان تحوّل درجات القرار لاحتمالات.

الـ Platt scaling
P(y=1 \mid f) = \sigma(Af + B) = \frac{1}{1+e^{-(Af+B)}}
الـ Platt scaling بيدرّب logistic sigmoid P(y=1|f) = σ(Af + B) وf هي قيمة الـ decision function بتاعة الـ SVM. وA وB بيتعمل لهم fit بالـ maximum likelihood على بيانات على جنب. وده بيزوّد خطوة تدريب تانية ويطوّل وقت التدريب.

LinearSVC للبيانات الكبيرة

للبيانات الكبيرة (>100,000 مثال) بـ kernel خطي، LinearSVC أسرع بكتير من SVC(kernel='linear'). بيستخدم الـ solver بتاع LIBLINEAR (primal coordinate descent) بدل LIBSVM (SMO على الـ dual)، اللي بيكبر زي O(n) في أمثلة التدريب بدل O(n²) لـ O(n³).

from sklearn.svm import LinearSVC

# LinearSVC: SVM خطي سريع، من غير kernel trick، ومن غير predict_proba
pipe_linear = Pipeline([
    ('scaler', StandardScaler()),
    ('svm', LinearSVC(C=1.0, max_iter=2000))
])

الـ support vector regression (SVR)

نفس آلية الـ maximum margin بتحل مسألة الـ regression (الانحدار)، مش الـ classification بس. الـ support vector regression (انحدار المتجهات الداعمة) بيناسب function بتفضل مسطّحة قد ما يمكن وفي نفس الوقت بيخلّي أغلب نقط التدريب جوه هامش سماح حواليها.

الفكرة الأساسية هي الأنبوبة الـ ε-insensitive (غير الحسّاسة): الأخطاء الأصغر من ε بتتجاهل خالص — أي prediction جوه ±ε من القيمة الحقيقية الـ loss بتاعه صفر. والنقط اللي واقعة على الأنبوبة أو بره بس هي اللي بتبقى support vectors وبتشكّل الـ fit. وده عكس الـ classification، اللي فيه الـ support vectors هي النقط اللي على الـ margin أو جواه.

الـ loss الـ ε-insensitive
L_\varepsilon\big(y, f(x)\big) = \max\big(0,\; |y - f(x)| - \varepsilon\big)
الأخطاء جوه ±ε من الـ target مبتكلّفش حاجة؛ وبره الأنبوبة الـ loss بتكبر خطيًا مع الـ residual. وε أكبر معناها أنبوبة أوسع، وsupport vectors أقل، وfit أكتر استواءً.

ولأن أغلب النقط عادةً بتقع جوه الأنبوبة، الحل بيبقى sparse (متفرّق) — متحدّد بعدد قليل نسبيًا من الـ support vectors، بالظبط زي الـ classification. والـ SVR بيعيد استخدام نفس مجموعة الـ kernels (linear، polynomial، RBF)، فالـ SVR بـ kernel بيمسك العلاقات اللي مش خطية بنفس الطريقة اللي SVC بيمسك بيها الحدود اللي مش خطية. وscikit-learn بتوفّره عن طريق SVR وNuSVR وLinearSVR السريع الخطي بس، زي classes الـ classifiers بالظبط. وبيتحكم فيه زرارين: ε بيظبط عرض الأنبوبة، وC بيوازن بين كسر الأنبوبة والاستواء — نفس الدور اللي C بيعمله في SVC.

نصايح عملية وغلطات مشهورة

ابدأ بـ SVM خطي. سريع وسهل تفسّره وغالبًا بينافس على البيانات اللي أبعادها عالية. وروح للـ RBF بس لو الـ linear kernel عنده underfitting.

اعمل scaling دايمًا. إنك تنسى StandardScaler هي أشهر غلطة في الـ SVM.

الـ grid search على مقياس لوغاريتمي. استخدم np.logspace().

خد بالك من تحذيرات التقارب. لو LinearSVC أو SVC طبع تحذير تقارب، زوّد max_iter. ومتتجاهلش التحذيرات — ممكن الـ model ميكونش اتقارب لحل صالح.

الفئات اللي مش متوازنة. خلّي class_weight='balanced' لما فئة تبقى أقل بكتير من التانية.

عدد الـ support vectors كاختبار منطق. بعد التدريب، بص على model.n_support_. لو تقريبًا كل نقطة تدريب support vector، يبقى الـ model عنده underfitting. ولو عددهم قليل أوي، ممكن يكون عنده overfitting.

إمتى تختار SVM بدل الـ classifiers التانية

الـ SVMs بتلمع على الـ datasets الصغيرة لحد المتوسطة واللي أبعادها عالية، اللي عدد الـ features فيها قريب من عدد الأمثلة أو أكتر — text classification، والبيولوجيا الجزيئية، وfeature vectors الصور. وبتشتغل كويس كمان لما الفئات تبقى بتتفصل تقريبًا بخط (linear SVM) أو لما تحتاج decision boundary غير parametric (SVM بـ RBF). وللبيانات الكبيرة جدًا، الـ tree ensembles أو الـ neural networks عادةً بتغلب الـ SVMs.

الوحدة 4 بتخلص هنا. والوحدات الجاية بتغطّي الـ unsupervised learning وتقييم الـ models على نطاق واسع وأساسيات الـ deep learning — مبنية على الأساس القوي للـ supervised learning اللي اتبنى في الوحدات 1–4.

أهم النقط
  • اعمل scaling للـ features دايمًا قبل تدريب الـ SVM — استخدم StandardScaler جوه Pipeline عشان تمنع الـ data leakage.
  • أهم parameters في SVC: kernel (RBF افتراضيًا)، وC (الـ regularization)، وgamma (الـ bandwidth بتاع RBF).
  • استخدم الـ cross-validation بـ StratifiedKFold عشان تاخد تقدير موثوق ومش منحاز لأداء الـ generalization.
  • ظبط C وγ مع بعض بالـ grid search على مقياس لوغاريتمي — هما بيتفاعلوا ومينفعش يتظبطوا كل واحد لوحده.
  • للبيانات اللي مش متوازنة، استخدم class_weight='balanced' وقيّم بالـ F1 أو الـ AUC بدل الـ accuracy.
  • للبيانات الكبيرة بـ kernel خطي، فضّل LinearSVC — بيكبر زي O(n) بدل O(n²–n³).
  • الـ SVM ثنائي في الأساس: الـ multi-class classification بيستخدم one-vs-one (SVC، عدد classifiers O(K²)) أو one-vs-rest (LinearSVC، O(K))؛ والـ SVR بيوسّع نفس فكرة الـ margin للـ regression عن طريق أنبوبة ε-insensitive.
  • الـ SVMs بتلمع على الـ datasets الصغيرة لحد المتوسطة، اللي أبعادها عالية، واللي اتعملها processing كويس.
اللي فات الـ kernel trick (خدعة النواة) نظرة عامة الوحدة التالية الـ clustering (التجميع) — K-Means