فاز 2 · درس 12

تنظیم ابرپارامترها: از جستجوی تصادفی تا بهینه‌سازی بیزی و Optuna

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
صفحه‌ی کنترل فلزی و آنالوگ با دسته‌ها و ولوم‌های چرخان مختلف که سوزن‌های درجه‌بندی دقیق را تنظیم می‌کنند.
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. تفاوت پارامتر و ابرپارامتر
  5. جستجوی شبکه‌ای
  6. جستجوی تصادفی
  7. بهینه‌سازی بیزی
  8. توقف زودهنگام (Early Stopping)
  9. برنامه‌های زمان‌بندی نرخ یادگیری (Learning Rate Schedulers)
  10. میزان اهمیت ابرپارامترها
  11. استراتژی عملی
  12. ادغام با اعتبارسنجی متقاطع
  13. نکات کاربردی
  14. پیاده‌سازی از صفر
  15. گام 1: جستجوی شبکه‌ای از صفر
  16. گام 2: جستجوی تصادفی از صفر
  17. گام 3: بهینه‌سازی بیزی (نسخه‌ی ساده‌شده)
  18. گام 4: مقایسه‌ی تمام روش‌ها
  19. استفاده از ابزارهای واقعی
  20. کار عملی با Optuna
  21. Optuna همراه با هرس کردن شاخه‌ها
  22. جستجوگرهای درون‌ساخت scikit-learn
  23. اشتباهات متداول در تنظیم ابرپارامترها
  24. تمرین‌ها
  25. اصطلاحات کلیدی
  26. برای مطالعه‌ی بیشتر

رسیدن به بالاترین دقت در مدل‌های یادگیری ماشین صرفاً به انتخاب الگوریتم خلاصه نمی‌شود؛ تنظیم دقیق پیچ‌های کنترلی یا همان ابرپارامترها تفاوت اصلی یک خروجی معمولی با یک مدل آماده‌ی محیط عملیاتی است. با افزایش تعداد پارامترها، جستجوی کورکورانه روی تک‌تک حالت‌ها (Grid Search) ساعت‌ها یا روزها منابع پردازشی گران‌قیمت را هدر می‌دهد بدون اینکه لزوماً بهترین نتیجه را کشف کند.

در شرایطی که دسترسی به پردازنده‌های گرافیکی پرقدرت و سرورهای ابری خارجی به دلیل محدودیت‌های پرداخت ارزی پرهزینه و دشوار است، مدیریت هوشمندانه‌ی زمان آموزش و منابع اهمیت دوچندان پیدا می‌کند. استفاده از روش‌های نمونه‌وری مانند جستجوی تصادفی و به‌ویژه بهینه‌سازی بیزی (Bayesian Optimization) کمک می‌کند تا با صرف کمترین بودجه‌ی پردازشی، نقاط بهینه را بیابید.

در این درس یاد می‌گیرید چطور با کنار گذاشتن گرید سرچ‌های طولانی، از استراتژی‌های بهینه‌سازی مدرن، مکانیزم‌های توقف زودهنگام (Early Stopping) و ابزارهای استانداردی مثل Optuna برای رسیدن سریع به بهترین مدل بهره ببرید.

ابرپارامترها همان پیچ‌های تنظیمی هستند که پیش از شروع آموزش می‌چرخانید. درست چرخاندن آن‌ها، مرز میان یک مدل معمولی و یک مدل فوق‌العاده است.

اهداف یادگیری

  • پیاده‌سازی جستجوی شبکه‌ای (grid search)، جستجوی تصادفی (random search) و بهینه‌سازی (optimization) بیزی (Bayesian optimization) از صفر و مقایسه‌ی کارایی آن‌ها در مصرف نمونه‌ها
  • درک این‌که چرا وقتی بعد مؤثر بیشترِ ابرپارامترها پایین است، جستجوی تصادفی از جستجوی شبکه‌ای بهتر عمل می‌کند
  • ساخت حلقه‌ی بهینه‌سازی بیزی با استفاده از یک مدل جانشین (surrogate model) و تابع فراگیری (acquisition function) برای هدایت مسیر جستجو
  • طراحی استراتژی تنظیم ابرپارامترها با اعتبارسنجی متقاطع صحیح، به‌گونه‌ای که مدل روی مجموعه‌ی اعتبارسنجی بیش‌برازش نشود

صورت مسئله

مدل گرادیان بوستینگ (gradient boosting) شما شامل این موارد است: نرخ یادگیری (learning rate)، تعداد درخت‌ها، حداکثر عمق، حداقل نمونه در هر برگ، نسبت زیرنمونه‌گیری و نسبت نمونه‌برداری از ستون‌ها. این یعنی شش ابرپارامتر. اگر برای هر کدام فقط 5 مقدار منطقی در نظر بگیرید، شبکه‌ی جستجو شامل 56=15,6255^6 = 15{,}625 ترکیب مختلف خواهد بود. اگر آموزش هر کدام 10 ثانیه طول بکشد، امتحان کردن همه‌ی آن‌ها به 43 ساعت توان پردازشی نیاز دارد.

جستجوی شبکه‌ای دم‌دستی‌ترین راهکار است، ولی در مقیاس‌های بزرگ، بدترین گزینه به شمار می‌رود. جستجوی تصادفی با مصرف منابع پردازشی کمتر، نتیجه‌ی بهتری می‌دهد. بهینه‌سازی بیزی از این هم فراتر می‌رود و از ارزیابی‌های قبلی درس می‌گیرد. اگر بدانید کدام استراتژی را انتخاب کنید و کدام ابرپارامترها واقعاً تأثیرگذارند، جلوی هدررفت روزها زمان باارزش GPU را خواهید گرفت.

مفاهیم پایه

تفاوت پارامتر و ابرپارامتر

پارامترها در طول آموزش یاد گرفته می‌شوند (وزن‌ها، بایاس‌ها و آستانه‌های تفکیک). اما ابرپارامترها پیش از شروع آموزش تنظیم می‌شوند و چگونگی روند یادگیری را کنترل می‌کنند.

ابرپارامتر چه چیزی را کنترل می‌کند بازه‌ی معمول
نرخ یادگیری اندازه‌ی گام در هر به‌روزرسانی 0.001 تا 1.0
تعداد درخت‌ها/اپوک‌ها مدت زمان آموزش 10 تا 10,000
حداکثر عمق پیچیدگی مدل 1 تا 30
منظم‌سازی (λ\lambda) جلوگیری از بیش‌برازش 0.0001 تا 100
اندازه‌ی دسته (batch size) نوفه‌ی تخمین گرادیان (gradient) 16 تا 512
نرخ دراپ‌اوت (dropout) کسر نورون‌های حذف‌شده 0.0 تا 0.5

جستجوی شبکه‌ای تک‌تک ترکیب‌های مقادیر تعیین‌شده را ارزیابی می‌کند. این روش جامع و کاملاً فهم‌پذیر است، اما هزینه‌ی آن با افزایش تعداد ابرپارامترها به‌صورت نمایی بالا می‌رود.

Grid for 2 hyperparameters:

  learning_rate: [0.01, 0.1, 1.0]
  max_depth:     [3, 5, 7]

  Evaluations: 3 x 3 = 9 combinations

  (0.01, 3)  (0.01, 5)  (0.01, 7)
  (0.1,  3)  (0.1,  5)  (0.1,  7)
  (1.0,  3)  (1.0,  5)  (1.0,  7)

جستجوی شبکه‌ای یک ایراد بنیادی دارد: اگر یکی از ابرپارامترها مهم باشد و دیگری هیچ اهمیتی نداشته باشد، بیشتر ارزیابی‌ها هدر می‌روند. در واقع شما از دل 9 بار ارزیابی، فقط 3 مقدار یکتا از پارامتر مهم را بررسی کرده‌اید.

جستجوی تصادفی به‌جای حرکت روی شبکه، ابرپارامترها را از توزیع‌های آماری نمونه‌برداری می‌کند. با همان بودجه‌ی 9 ارزیابی، حالا برای هر ابرپارامتر 9 مقدار یکتا به دست می‌آورید.

جستجوی تصادفیجستجوی شبکه‌ای3 نرخ یادگیری یکتا3 بیشینه عمق یکتا9 ارزیابی در مجموع9 نرخ یادگیری یکتا9 بیشینه عمق یکتا9 ارزیابی در مجموع

چرا جستجوی تصادفی بهتر از جستجوی شبکه‌ای عمل می‌کند؟ (برگسترا و بنجیو، 2012):

  • بیشتر ابرپارامترها بعد مؤثر پایینی دارند. در هر مسئله‌ی خاص، معمولاً فقط 1 تا 2 تا از 6 ابرپارامتر واقعاً سرنوشت‌ساز هستند.
  • جستجوی شبکه‌ای ارزیابی‌ها را روی ابعاد بی‌اهمیت تلف می‌کند.
  • جستجوی تصادفی با همان میزان بودجه، ابعاد مهم را متراکم‌تر پوشش می‌دهد.
  • با 60 بار تلاش تصادفی، 95% شانس دارید نقطه‌ای را پیدا کنید که در فاصله‌ی 5 درصدی از نقطه‌ی بهینه قرار دارد (به شرطی که چنین نقطه‌ای در فضای جستجو وجود داشته باشد).

بهینه‌سازی بیزی

جستجوی تصادفی توجهی به نتایج گذشته ندارد؛ یاد نمی‌گیرد که نرخ یادگیری بالا باعث واگرایی می‌شود یا عمق 3 همیشه بهتر از عمق 10 جواب می‌دهد. در مقابل، بهینه‌سازی بیزی بر پایه‌ی ارزیابی‌های قبلی تصمیم می‌گیرد که نقطه‌ی بعدی برای جستجو کجا باشد.

NoYesDefine search spaceEvaluate initial random pointsFit surrogate model to resultsUse acquisition function to pick nextpointEvaluate the model at that pointBudget exhausted?Return best hyperparameters found

دو مؤلفه‌ی کلیدی:

مدل جانشین: مدلی ارزان از نظر محاسباتی (معمولاً فرایند گاوسی یا Gaussian process) که تابع هدف گران‌قیمت را تقریب می‌زند. این مدل در هر نقطه‌ای از فضای جستجو، هم یک پیش‌بینی و هم تخمینی از عدم قطعیت ارائه می‌دهد.

تابع فراگیری: تصمیم می‌گیرد ارزیابی بعدی در کدام نقطه انجام شود؛ این کار را با برقراری تعادل میان بهره‌برداری (جستجو در نزدیکی نقاط خوبِ شناخته‌شده) و اکتشاف (جستجو در مناطقی با عدم قطعیت بالا) انجام می‌دهد. گزینه‌های رایج:

  • بهبود مورد انتظار (Expected Improvement یا EI): در این نقطه چقدر انتظار بهبود نسبت به بهترین مقدار فعلی داریم؟
  • کران بالای اطمینان (Upper Confidence Bound یا UCB): حاصل‌جمع مقدار پیش‌بینی با مضربی از عدم قطعیت. UCB بالاتر یعنی آن نقطه یا امیدبخش است یا هنوز کاوش نشده.
  • احتمال بهبود (Probability of Improvement یا PI): احتمال این‌که این نقطه از بهترین مقدار فعلی بهتر عمل کند چقدر است؟

بهینه‌سازی بیزی معمولاً با 2 تا 5 برابر ارزیابیِ کمتر، ابرپارامترهای بهتری نسبت به جستجوی تصادفی پیدا می‌کند. سربار محاسباتی برازش مدل جانشین، در مقایسه با آموزش مدل اصلی، کاملاً ناچیز است.

توقف زودهنگام (Early Stopping)

لازم نیست هر اجرای آموزشی تا انتها پیش برود. اگر مشخص است که یک پیکربندی بعد از 10 اپوک نتیجه‌ی ضعیفی دارد، متوقفش کنید و سراغ بعدی بروید. مفهوم توقف زودهنگام در بستر جستجوی ابرپارامترها دقیقاً همین است.

استراتژی‌ها:

  • بر پایه‌ی صبر (Patience-based): اگر تابع زیان (loss) اعتبارسنجی برای N اپوک پیاپی بهبود نیافت، آموزش را متوقف کنید.
  • هرس بر مبنای میانه (Median pruning): اگر نتیجه‌ی میانی یک آزمایش در یک گام مشخص، ضعیف‌تر از میانه‌ی آزمایش‌های تمام‌شده در همان گام بود، متوقف شود.
  • هایپربند (Hyperband): ابتدا بودجه‌های اندکی به پیکربندی‌های متعددی اختصاص دهید، سپس به‌مرور بودجه را برای بهترین گزینه‌ها افزایش دهید.

Hyperband فوق‌العاده مؤثر است. کار را با 81 پیکربندی که هر کدام فقط 1 اپوک اجرا می‌شوند شروع می‌کند، یک‌سوم برتر را نگه می‌دارد، به آن‌ها 3 اپوک فرصت می‌دهد، باز یک‌سوم برتر را حفظ می‌کند و همین روند را ادامه می‌دهد. این روش پیکربندی‌های مناسب را 10 تا 50 برابر سریع‌تر از ارزیابی کامل تمام حالات پیدا می‌کند.

برنامه‌های زمان‌بندی نرخ یادگیری (Learning Rate Schedulers)

نرخ یادگیری تقریباً همیشه مهم‌ترین ابرپارامتر است. برنامه‌ی زمان‌بندی نرخ یادگیری (learning rate schedule) به‌جای ثابت نگه‌داشتن آن، مقدارش را در حین آموزش تنظیم می‌کند.

برنامه‌ریز فرمول زمان استفاده
کاهش پله‌ای (Step decay) ضرب در 0.1 بعد از هر N اپوک آموزش کلاسیک CNN
انحلال کسینوسی (Cosine annealing) lr⋅0.5⋅(1+cos⁡(πt/T))\text{lr} \cdot 0.5 \cdot (1 + \cos(\pi t / T)) گزینه‌ی پیش‌فرض امروزی
گرم‌کردن + افت (Warmup + decay) افزایش خطی و سپس افت کسینوسی ترنسفورمرها (Transformers)
تک‌چرخه‌ای (One-cycle) افزایش و سپس کاهش در طول یک چرخه همگرایی سریع
کاهش در فلات (Reduce on plateau) کاهش با ضریبی مشخص هنگام متوقف شدن بهبود معیار گزینه‌ی پیش‌فرض و مطمئن

میزان اهمیت ابرپارامترها

همه‌ی ابرپارامترها اهمیت برابری ندارند. پژوهش‌های انجام‌شده روی جنگل‌های تصادفی (Random Forests) (پروبست و همکاران، 2019) و گرادیان بوستینگ الگوهای یکسانی را نشان می‌دهند:

اهمیت بالا:

  • نرخ یادگیری (همیشه اول از همه تنظیم کنید)
  • تعداد برآوردگرها / اپوک‌ها (به‌جای تنظیم کردن، از توقف زودهنگام استفاده کنید)
  • شدت منظم‌سازی

اهمیت متوسط:

  • حداکثر عمق / تعداد لایه‌ها
  • حداقل نمونه در هر برگ / کاهش وزن (weight decay)
  • نسبت زیرنمونه‌گیری

اهمیت پایین:

  • حداکثر ویژگی‌ها (برای جنگل‌های تصادفی)
  • انتخاب دقیق تابع فعال‌ساز
  • اندازه‌ی دسته (در بازه‌ای معقول)

همیشه ابرپارامترهای مهم را اول تنظیم کنید و بقیه را روی مقادیر پیش‌فرض بگذارید.

استراتژی عملی

Start with defaultsCoarse random search: 20-50 trialsIdentify important hyperparametersFine random or Bayesian search: 50-100trials in narrowed spaceFinal model with best hyperparametersRetrain on full training data

گردش کار دقیق و عملی:

  1. با مقادیر پیش‌فرض کتابخانه‌ها شروع کنید. این مقادیر را متخصصان باتجربه انتخاب کرده‌اند و اغلب 80% مسیر کیفیت نهایی را طی می‌کنند.
  2. جستجوی تصادفی اولیه. بازه‌های وسیع، 20 تا 50 آزمایش. از توقف زودهنگام برای متوقف کردن سریع اجراهای ضعیف استفاده کنید.
  3. تحلیل نتایج. کدام ابرپارامترها با عملکرد مدل همبستگی دارند؟ فضای جستجو را محدود کنید.
  4. جستجوی دقیق. بهینه‌سازی بیزی یا جستجوی تصادفیِ متمرکز در همان فضای محدودشده؛ 50 تا 100 آزمایش.
  5. آموزش مجدد روی تمام داده‌های آموزش با بهترین ترکیب ابرپارامترهای کشف‌شده.

ادغام با اعتبارسنجی متقاطع

تنظیم ابرپارامترها روی یک برشِ اعتبارسنجی منفرد ریسک بالایی دارد؛ ممکن است بهترین ابرپارامترها روی همان فولد اعتبارسنجی خاص بیش‌برازش شوند. اعتبارسنجی متقاطع تودرتو (Nested cross-validation) با دو حلقه‌ی مجزا این چالش را حل می‌کند:

  • حلقه‌ی بیرونی (ارزیابی): داده‌ها را به «آموزش+اعتبارسنجی» و «آزمون» تقسیم می‌کند و عملکردی بدون سوگیری را گزارش می‌دهد.
  • حلقه‌ی درونی (تنظیم): داده‌های «آموزش+اعتبارسنجی» را به آموزش و اعتبارسنجی می‌شکند و بهترین ابرپارامترها را می‌یابد.
مجموعه داده کاملفولد بیرونی 1: تستفولد بیرونی 2: تستفولد بیرونی 3: تستفولد بیرونی 4: تستفولد بیرونی 5: تستCV درونی 5-fold روی داده‌های باقی‌ماندهبهترین ابرپارامترها برای فولد 1ارزیابی روی فولد تست بیرونی 1CV درونی 5-fold روی داده‌های باقی‌ماندهبهترین ابرپارامترها برای فولد 2ارزیابی روی فولد تست بیرونی 2

هر فولد بیرونی به‌طور کاملاً مستقل بهترین ابرپارامترهای خودش را پیدا می‌کند. در نتیجه، امتیازهای بیرونی برآوردی بدون سوگیری از قابلیت تعمیم‌پذیری مدل خواهند بود.

پیاده‌سازی با sklearn:

from sklearn.model_selection import cross_val_score, GridSearchCV
from sklearn.ensemble import GradientBoostingRegressor

inner_cv = GridSearchCV(
    GradientBoostingRegressor(),
    param_grid={
        "learning_rate": [0.01, 0.05, 0.1],
        "max_depth": [2, 3, 5],
        "n_estimators": [50, 100, 200],
    },
    cv=5,
    scoring="neg_mean_squared_error",
)

outer_scores = cross_val_score(
    inner_cv, X, y, cv=5, scoring="neg_mean_squared_error"
)

print(f"Nested CV MSE: {-outer_scores.mean():.4f} +/- {outer_scores.std():.4f}")

این فرایند پرهزینه است (5 فولد بیرونی ضرب در 5 فولد درونی ضرب در 27 نقطه‌ی شبکه = 675 بار برازش مدل)، اما برآوردی کاملاً قابل‌اعتماد از عملکرد به شما می‌دهد. زمانی از این رویکرد استفاده کنید که در حال ارائه‌ی نتایج نهایی در مقاله‌اید یا پیامد تصمیم پیش رو بسیار سنگین است.

نکات کاربردی

همیشه با نرخ یادگیری شروع کنید. برای روش‌های مبتنی بر گرادیان، نرخ یادگیری همواره مهم‌ترین ابرپارامتر است. اگر نرخ یادگیری بد باشد، هیچ چیز دیگری اهمیتی نخواهد داشت. سایر ابرپارامترها را روی پیش‌فرض بگذارید و اول نرخ یادگیری را پویش کنید.

برای نرخ یادگیری و منظم‌سازی از توزیع یکنواخت لگاریتمی (log-uniform) استفاده کنید. تفاوت بین 0.001 و 0.01 به اندازه‌ی تفاوت 0.1 و 1.0 اهمیت دارد. جستجوی خطی بودجه‌ی شما را در سمت مقادیر بزرگ هدر می‌دهد.

به‌جای تنظیم n_estimators، از توقف زودهنگام استفاده کنید. در روش‌های بوستینگ و شبکه‌های عصبی (neural network)، تعداد n_estimators یا اپوک‌ها را بالا بگذارید و بگذارید توقف زودهنگام زمان توقف را تعیین کند. با این کار یک ابرپارامتر از فضای جستجو حذف می‌شود.

تخصیص بودجه. 60% از کل بودجه‌ی بهینه‌سازی را روی 2 ابرپارامتر بسیار مهم خرج کنید و 40% باقی‌مانده را به بقیه‌ی موارد اختصاص دهید؛ چرا که بخش اعظم تفاوت عملکرد ناشی از همان 2 مورد اول است.

مقیاس متغیرها مهم است. اندازه‌ی دسته را هرگز در مقیاس لگاریتمی جستجو نکنید (مقادیر 16، 32 و 64 کاملاً مناسب‌اند)، اما نرخ یادگیری را همیشه در مقیاس لگاریتمی بگردید. توزیع جستجو را با نحوه‌ی اثرگذاری ابرپارامتر بر مدل تطبیق دهید.

نوع مدل ابرپارامترهای برتر روش جستجوی پیشنهادی بودجه
جنگل تصادفی n_estimators، max_depth، min_samples_leaf جستجوی تصادفی، 50 آزمایش کم (آموزش سریع)
گرادیان بوستینگ learning_rate، n_estimators، max_depth بیزی، 100 آزمایش + توقف زودهنگام متوسط
شبکه عصبی learning_rate، weight_decay، batch_size بیزی یا تصادفی، بیش از 100 آزمایش بالا (آموزش کند)
ماشین بردار پشتیبان (SVM) C، gamma (کرنل RBF) شبکه‌ای در مقیاس لگاریتمی، 25 تا 50 آزمایش کم (2 پارامتر)
لاسو / ریج (Lasso/Ridge) alpha جستجوی یک‌بعدی در مقیاس لگاریتمی، 20 آزمایش بسیار کم
XGBoost learning_rate، max_depth، subsample، colsample بیزی، 100 تا 200 آزمایش + توقف زودهنگام متوسط

اگر شک دارید: جستجوی تصادفی را با تعداد تکراری معادل 2 برابر تعداد ابرپارامترها اجرا کنید (مثلاً برای 6 ابرپارامتر، حداقل 12 آزمایش). شگفت‌زده خواهید شد وقتی ببینید جستجوی تصادفی با 50 آزمایش تا چه حد در عمل از یک جستجوی شبکه‌ایِ با دقت طراحی‌شده جلو می‌زند.

اعتبارسنجی متقاطع K-Foldk را بکشید
K-FOLD CROSS-VALIDATIONEach row is one round: the gold fold is held out for validation, the blue folds are used for training. Rotating the held-out fold means every example is scored exactly once, and averaging the k scores gives a more stable estimate than a single split.
هر سطر نشان‌دهنده یک دور است: بخش طلایی برای اعتبارسنجی کنار گذاشته می‌شود و بخش‌های آبی برای آموزش استفاده می‌شوند. با چرخش بخش کنارگذاشته‌شده، به هر نمونه دقیقاً یک‌بار امتیاز داده می‌شود و میانگین‌گیری از این k امتیاز، برآوردی پایدارتر نسبت به یک‌بار تقسیم داده‌ها ارائه می‌دهد.

پیاده‌سازی از صفر

گام 1: جستجوی شبکه‌ای از صفر

کد موجود در فایل code/tuning.py جستجوی شبکه‌ای، جستجوی تصادفی و یک بهینه‌ساز بیزی ساده را از صفر پیاده‌سازی می‌کند.

def grid_search(model_fn, param_grid, X_train, y_train, X_val, y_val):
    keys = list(param_grid.keys())
    values = list(param_grid.values())
    best_score = -float("inf")
    best_params = None
    n_evals = 0

    for combo in itertools.product(*values):
        params = dict(zip(keys, combo))
        model = model_fn(**params)
        model.fit(X_train, y_train)
        score = evaluate(model, X_val, y_val)
        n_evals += 1

        if score > best_score:
            best_score = score
            best_params = params

    return best_params, best_score, n_evals

گام 2: جستجوی تصادفی از صفر

def random_search(model_fn, param_distributions, X_train, y_train,
                  X_val, y_val, n_iter=50, seed=42):
    rng = np.random.RandomState(seed)
    best_score = -float("inf")
    best_params = None

    for _ in range(n_iter):
        params = {k: sample(v, rng) for k, v in param_distributions.items()}
        model = model_fn(**params)
        model.fit(X_train, y_train)
        score = evaluate(model, X_val, y_val)

        if score > best_score:
            best_score = score
            best_params = params

    return best_params, best_score, n_iter

گام 3: بهینه‌سازی بیزی (نسخه‌ی ساده‌شده)

ایده‌ی اصلی: یک فرایند گاوسی را روی جفت‌های مشاهده‌شده‌ی (ابرپارامتر، امتیاز) برازش کنید و سپس با استفاده از تابع فراگیری تصمیم بگیرید گام بعدی به کجا تعلق دارد.

class SimpleBayesianOptimizer:
    def __init__(self, search_space, n_initial=5):
        self.search_space = search_space
        self.n_initial = n_initial
        self.X_observed = []
        self.y_observed = []

    def _kernel(self, x1, x2, length_scale=1.0):
        dists = np.sum((x1[:, None, :] - x2[None, :, :]) ** 2, axis=2)
        return np.exp(-0.5 * dists / length_scale ** 2)

    def _fit_gp(self, X_new):
        X_obs = np.array(self.X_observed)
        y_obs = np.array(self.y_observed)
        y_mean = y_obs.mean()
        y_centered = y_obs - y_mean

        K = self._kernel(X_obs, X_obs) + 1e-4 * np.eye(len(X_obs))
        K_star = self._kernel(X_new, X_obs)

        L = np.linalg.cholesky(K)
        alpha = np.linalg.solve(L.T, np.linalg.solve(L, y_centered))
        mu = K_star @ alpha + y_mean

        v = np.linalg.solve(L, K_star.T)
        var = 1.0 - np.sum(v ** 2, axis=0)
        var = np.maximum(var, 1e-6)

        return mu, var

    def _expected_improvement(self, mu, var, best_y):
        sigma = np.sqrt(var)
        z = (mu - best_y) / (sigma + 1e-10)
        ei = sigma * (z * norm_cdf(z) + norm_pdf(z))
        return ei

    def suggest(self):
        if len(self.X_observed) < self.n_initial:
            return sample_random(self.search_space)

        candidates = [sample_random(self.search_space) for _ in range(500)]
        X_cand = np.array([to_vector(c) for c in candidates])
        mu, var = self._fit_gp(X_cand)
        ei = self._expected_improvement(mu, var, max(self.y_observed))
        return candidates[np.argmax(ei)]

    def observe(self, params, score):
        self.X_observed.append(to_vector(params))
        self.y_observed.append(score)

مدل جانشین فرایند گاوسی در هر نقطه‌ی کاندید دو خروجی می‌دهد: یک امتیاز پیش‌بینی‌شده (mu) و یک عدم قطعیت (var). «بهبود مورد انتظار» میان این دو توازن برقرار می‌کند: اولویت را به نقاطی می‌دهد که یا امتیاز پیش‌بینی‌شده در آن‌ها بالاست یا عدم قطعیت زیادی دارند. در اوایل کار، عدم قطعیتِ بیشترِ نقاط بالاست و در نتیجه بهینه‌ساز دست به اکتشاف می‌زند؛ کمی بعد، تمرکزش را روی امیدوارکننده‌ترین ناحیه می‌گذارد.

گام 4: مقایسه‌ی تمام روش‌ها

هر سه روش را روی یک تابع هدف مصنوعی اجرا و مقایسه کنید. این مقایسه از یک پوسته‌ی ساده‌شده استفاده می‌کند که هر بهینه‌ساز را مستقیماً با تابع هدف فراخوانی می‌کند (بدون آموزش مدل)؛ بنابراین رابط آن با پیاده‌سازی‌های مبتنی بر مدل در بخش‌های بالا کمی تفاوت دارد:

def synthetic_objective(params):
    lr = params["learning_rate"]
    depth = params["max_depth"]
    return -(np.log10(lr) + 2) ** 2 - (depth - 4) ** 2 + 10

param_grid = {
    "learning_rate": [0.001, 0.01, 0.1, 1.0],
    "max_depth": [2, 3, 4, 5, 6, 7, 8],
}

grid_best = None
grid_score = -float("inf")
grid_history = []
for combo in itertools.product(*param_grid.values()):
    params = dict(zip(param_grid.keys(), combo))
    score = synthetic_objective(params)
    grid_history.append((params, score))
    if score > grid_score:
        grid_score = score
        grid_best = params

param_dist = {
    "learning_rate": ("log_float", 0.001, 1.0),
    "max_depth": ("int", 2, 8),
}

rand_best = None
rand_score = -float("inf")
rand_history = []
rng = np.random.RandomState(42)
for _ in range(28):
    params = {k: sample(v, rng) for k, v in param_dist.items()}
    score = synthetic_objective(params)
    rand_history.append((params, score))
    if score > rand_score:
        rand_score = score
        rand_best = params

optimizer = SimpleBayesianOptimizer(param_dist, n_initial=5)
bayes_history = []
for _ in range(28):
    params = optimizer.suggest()
    score = synthetic_objective(params)
    optimizer.observe(params, score)
    bayes_history.append((params, score))
bayes_score = max(s for _, s in bayes_history)

print(f"{'Method':<20} {'Best Score':>12} {'Evaluations':>12}")
print("-" * 50)
print(f"{'Grid Search':<20} {grid_score:>12.4f} {len(grid_history):>12}")
print(f"{'Random Search':<20} {rand_score:>12.4f} {len(rand_history):>12}")
print(f"{'Bayesian Opt':<20} {bayes_score:>12.4f} {len(bayes_history):>12}")

با بودجه‌ی یکسان، بهینه‌سازی بیزی معمولاً در سریع‌ترین زمان ممکن بهترین امتیاز را پیدا می‌کند، چون ارزیابی‌ها را در نواحی مسلماً نامناسب هدر نمی‌دهد. جستجوی تصادفی گستره‌ی وسیع‌تری نسبت به جستجوی شبکه‌ای را پوشش می‌دهد. جستجوی شبکه‌ای فقط زمانی برنده می‌شود که ابرپارامترهای بسیار کمی داشته باشید و بتوانید تمام حالات را بدون نگرانی ارزیابی کنید.

استفاده از ابزارهای واقعی

کار عملی با Optuna

کتابخانه‌ی Optuna گزینه‌ی پیشنهادی برای تنظیم حرفه‌ای ابرپارامترهاست. این ابزار از هرس کردن شاخه‌ها (pruning)، جستجوی توزیع‌شده و بصری‌سازی به‌طور پیش‌فرض پشتیبانی می‌کند.

import optuna

def objective(trial):
    lr = trial.suggest_float("learning_rate", 1e-4, 1e-1, log=True)
    n_est = trial.suggest_int("n_estimators", 50, 500)
    max_depth = trial.suggest_int("max_depth", 2, 10)

    model = GradientBoostingRegressor(
        learning_rate=lr,
        n_estimators=n_est,
        max_depth=max_depth,
    )
    model.fit(X_train, y_train)
    return mean_squared_error(y_val, model.predict(X_val))

study = optuna.create_study(direction="minimize")
study.optimize(objective, n_trials=100)

print(f"Best params: {study.best_params}")
print(f"Best MSE: {study.best_value:.4f}")

قابلیت‌های کلیدی Optuna:

  • تابع suggest_float(..., log=True) برای پارامترهایی که بهتر است در مقیاس لگاریتمی جستجو شوند (مثل نرخ یادگیری و منظم‌سازی)
  • تابع suggest_int برای پارامترهای صحیح
  • تابع suggest_categorical برای گزینه‌های گسسته
  • کلاس درون‌ساخت MedianPruner برای توقف زودهنگام آزمایش‌های ناموفق
  • متد study.trials_dataframe() برای تحلیل نتایج

Optuna همراه با هرس کردن شاخه‌ها

هرس کردن (pruning) آزمایش‌های ناامیدکننده را زودتر متوقف می‌کند و بخش عظیمی از توان پردازشی را نجات می‌دهد. این هم الگوی کار:

import optuna
from sklearn.model_selection import cross_val_score

def objective(trial):
    params = {
        "learning_rate": trial.suggest_float("lr", 1e-4, 0.5, log=True),
        "max_depth": trial.suggest_int("max_depth", 2, 10),
        "n_estimators": trial.suggest_int("n_estimators", 50, 500),
        "subsample": trial.suggest_float("subsample", 0.5, 1.0),
    }

    model = GradientBoostingRegressor(**params)
    scores = cross_val_score(model, X_train, y_train, cv=3,
                             scoring="neg_mean_squared_error")
    mean_score = -scores.mean()

    trial.report(mean_score, step=0)
    if trial.should_prune():
        raise optuna.TrialPruned()

    return mean_score

pruner = optuna.pruners.MedianPruner(n_startup_trials=10, n_warmup_steps=5)
study = optuna.create_study(direction="minimize", pruner=pruner)
study.optimize(objective, n_trials=200)

کلاس MedianPruner اگر متوجه شود نتیجه‌ی میانی یک آزمایش از میانه‌ی تمام آزمایش‌های پایان‌یافته در همان گام بدتر است، ادامه‌ی آن را قطع می‌کند. هرس کردن مستلزم فراخوانی trial.report() برای گزارش معیارهای میانی و trial.should_prune() برای بررسی لزوم توقف آزمایش است. آرگومان n_startup_trials=10 تضمین می‌کند که قبل از شروع هرس، حداقل 10 آزمایش به‌طور کامل اجرا شوند. این قابلیت معمولاً 40 تا 60 درصد از کل هزینه‌ی پردازشی می‌کاهد.

جستجوگرهای درون‌ساخت scikit-learn

کتابخانه‌ی sklearn برای آزمایش‌های سریع کلاس‌های GridSearchCV، RandomizedSearchCV و HalvingRandomSearchCV را ارائه می‌دهد:

from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import loguniform, randint

param_dist = {
    "learning_rate": loguniform(1e-4, 0.5),
    "max_depth": randint(2, 10),
    "n_estimators": randint(50, 500),
}

search = RandomizedSearchCV(
    GradientBoostingRegressor(),
    param_dist,
    n_iter=100,
    cv=5,
    scoring="neg_mean_squared_error",
    random_state=42,
    n_jobs=-1,
)
search.fit(X_train, y_train)
print(f"Best params: {search.best_params_}")
print(f"Best CV MSE: {-search.best_score_:.4f}")

از تابع loguniform در scipy برای نرخ یادگیری و منظم‌سازی، و از randint برای ابرپارامترهای عدد صحیح استفاده کنید. فلگ n_jobs=-1 عملیات را روی تمام هسته‌های CPU موازی می‌کند.

اشتباهات متداول در تنظیم ابرپارامترها

نشت داده (Data Leakage) از طریق پیش‌پردازش. اگر مقیاس‌بند (scaler) را قبل از اعتبارسنجی متقاطع روی کل مجموعه‌داده برازش کنید، اطلاعات مربوط به فولد اعتبارسنجی به آموزش نشت می‌کند. همیشه مراحل پیش‌پردازش را درون یک Pipeline قرار دهید تا تبدیل‌ها فقط روی فولد آموزش برازش شوند.

بیش‌برازش روی مجموعه‌ی اعتبارسنجی. اجرای هزاران آزمایش عملاً به معنای آموزش مدل روی مجموعه‌ی اعتبارسنجی است. برای برآورد نهایی عملکرد از اعتبارسنجی متقاطع تودرتو استفاده کنید یا یک مجموعه‌ی آزمون جداگانه کنار بگذارید که در طول فرایند تنظیم اصلاً به آن دست نزنید.

جستجو در بازه‌ای بسیار باریک. اگر بهترین مقدار به‌دست‌آمده روی مرز فضای جستجوی شما باشد، یعنی فضای خود را به اندازه‌ی کافی گسترده نگرفته‌اید. مقدار بهینه ممکن است بیرون از بازه‌ی شما باشد. همیشه چک کنید که آیا بهترین پارامترها روی لبه‌ها قرار گرفته‌اند یا خیر.

نادیده‌گرفتن اثرات برهم‌کنشی. نرخ یادگیری و تعداد برآوردگرها در الگوریتم‌های بوستینگ اثر متقابل شدیدی بر هم دارند. نرخ یادگیری پایین به تعداد درخت بیشتری نیاز دارد. تنظیم جداگانه‌ی آن‌ها نتایج ضعیف‌تری نسبت به تنظیم هم‌زمان آن‌ها به همراه خواهد داشت.

استفاده نکردن از توقف زودهنگام در مدل‌های تکرارشونده. در گرادیان بوستینگ و شبکه‌های عصبی، n_estimators یا تعداد اپوک‌ها را بالا بگذارید و از توقف زودهنگام استفاده کنید. این کار بی‌برو‌برگرد بهتر از آن است که تعداد تکرارها را مثل یک ابرپارامتر عادی تنظیم کنید.

تمرین‌ها

  1. جستجوی شبکه‌ای و جستجوی تصادفی را با بودجه‌ی یکسان (مثلاً 50 ارزیابی) اجرا کنید. بهترین امتیازهای حاصل را با هم مقایسه کنید. آزمایش را 10 بار با سیدهای (seeds) مختلف تکرار کنید. جستجوی تصادفی چند بار برنده می‌شود؟

  2. الگوریتم Hyperband را از صفر پیاده‌سازی کنید. کار را با 81 پیکربندی شروع کنید که هر کدام فقط 1 اپوک آموزش می‌بینند. در هر دور، یک‌سوم برتر را نگه دارید و بودجه‌ی آن‌ها را 3 برابر کنید. کل توان پردازشی صرف‌شده (مجموع تمام اپوک‌ها در همه‌ی پیکربندی‌ها) را با اجرای کامل تمام 81 پیکربندی با بودجه‌ی نهایی مقایسه کنید.

  3. یک برنامه‌ی زمان‌بندی نرخ یادگیری (انحلال کسینوسی) را به پیاده‌سازی گرادیان بوستینگ از درس 11 اضافه کنید. آیا در مقایسه با نرخ یادگیری ثابت کمکی می‌کند؟

  4. با استفاده از Optuna، مدل RandomForestClassifier را روی یک مجموعه‌داده‌ی واقعی (مثلاً مجموعه‌داده‌ی breast cancer در sklearn) تنظیم کنید. با استفاده از optuna.visualization.plot_param_importances(study) بررسی کنید کدام ابرپارامترها بیشترین اهمیت را دارند. آیا نتایج با رتبه‌بندی اهمیت در این درس همخوانی دارد؟

  5. یک تابع فراگیری ساده (بهبود مورد انتظار) را پیاده‌سازی کرده و مفهوم اکتشاف در برابر بهره‌برداری را با آن نمایش دهید. میانگین و عدم قطعیتِ مدل جانشین را رسم کنید و نشان دهید تابع EI کدام نقطه را برای ارزیابی بعدی انتخاب می‌کند.

اصطلاحات کلیدی

اصطلاح در گفتگوهای عامیانه معنای دقیق فنی
ابرپارامتر «تنظیماتی که انتخاب می‌کنی» مقداری که پیش از شروع آموزش تعیین می‌شود و فرایند یادگیری را کنترل می‌کند، نه مقداری که از داده‌ها آموخته شود
جستجوی شبکه‌ای «امتحان کردن همه‌ی حالات» جستجوی فراگیر روی یک شبکه‌ی مشخص از پارامترها. هزینه‌ی نمایی دارد.
جستجوی تصادفی «فقط نمونه‌برداری تصادفی» نمونه‌برداری ابرپارامترها از توزیع‌های آماری. ابعاد مهم را بهتر از جستجوی شبکه‌ای پوشش می‌دهد.
بهینه‌سازی بیزی «جستجوی هوشمند» استفاده از یک مدل جانشین برای تخمین تابع هدف جهت تصمیم‌گیری درباره‌ی محل ارزیابی بعدی، با برقراری تعادل میان اکتشاف و بهره‌برداری
مدل جانشین «یک تقریب ارزان» مدلی سبک (معمولاً فرایند گاوسی) که تابع هدف پرهزینه را بر اساس ارزیابی‌های مشاهده‌شده تقریب می‌زند
تابع فراگیری «کجا را در مرحله‌ی بعد بررسی کنیم» امتیازدهی به نقاط کاندید با متوازن‌سازیِ بهبود مورد انتظار و عدم قطعیت. EI و UCB از انتخاب‌های رایج هستند.
توقف زودهنگام «جلوگیری از هدررفت زمان» متوقف کردن زودهنگام آموزش به محض این‌که روند بهبود عملکرد روی اعتبارسنجی متوقف شد
هایپربند «جدول حذفی برای پیکربندی‌ها» تخصیص تطبیقی منابع: شروع با پیکربندی‌های زیاد و بودجه‌های کم، نگه‌داشتن بهترین‌ها و افزایش بودجه‌ی آن‌ها
برنامه‌ی زمان‌بندی نرخ یادگیری «تغییر lr در حین آموزش» تابعی که نرخ یادگیری را در طول دوره‌ی آموزش برای دستیابی به همگرایی بهتر تنظیم می‌کند

برای مطالعه‌ی بیشتر

سؤالات متداول

چرا جستجوی تصادفی (Random Search) از جستجوی شبکه‌ای (Grid Search) بهتر عمل می‌کند؟

در مسائل واقعی معمولاً فقط 1 یا 2 ابرپارامتر اثر تعیین‌کننده دارند (بعد مؤثر پایین). گرید سرچ ارزیابی‌های خود را روی ابعاد بی‌اهمیت تلف می‌کند، در حالی که جستجوی تصادفی با همان بودجه، مقادیر یکتا و متنوع‌تری از پارامترهای حیاتی را پوشش می‌دهد.

بهینه‌سازی بیزی در مقایسه با جستجوی تصادفی چه مزیتی دارد؟

جستجوی تصادفی از تجربیات آزمایش‌های قبلی درس نمی‌گیرد، اما بهینه‌سازی بیزی با استفاده از یک مدل جانشین (معمولاً فرایند گاوسی) و تابع فراگیری، میان اکتشاف نواحی ناشناخته و بهره‌برداری از نقاط امیدبخش تعادل برقرار کرده و با 2 تا 5 برابر ارزیابی کمتر به نتیجه می‌رسد.

قابلیت هرس کردن (Pruning) در فریم‌ورک Optuna چگونه هزینه‌ها را کاهش می‌دهد؟

با بهره‌گیری از هرس‌کننده‌هایی مثل MedianPruner، آزمایش‌هایی که در مراحل اولیه نتایج ضعیف‌تری نسبت به میانه‌ی آزمایش‌های گذشته دارند متوقف می‌شوند که این کار معمولاً 40 تا 60 درصد از کل زمان پردازش را صرفه‌جویی می‌کند.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.