2025, Sep 29 23:31
imblearn के साथ क्रॉस‑वैलिडेशन में अंडरसैंपलिंग की पुनरावृत्ति कैसे रोकें
असंतुलित डेटा के लिए imblearn RandomUnderSampler संग क्रॉस‑वैलिडेशन तेज करें: HalvingRandomSearchCV में पाइपलाइन से रिसैंपलिंग हटाएँ, पूर्व‑गणना अंडरसैंपल्ड फोल्ड्स को cv में दें।
असंतुलित डेटा के साथ क्रॉस‑वैलिडेशन अक्सर आपको imblearn के RandomUnderSampler जैसी रिसैंपलिंग रणनीति अपनाने की ओर धकेलता है। पेच यह है कि जब आप री‑सैंपलर को पाइपलाइन के अंदर रखते हैं और फिर हाइपरपैरामीटर सर्च शुरू करते हैं, तो हर स्प्लिट और हर कैंडिडेट के लिए रिसैंपलिंग बार‑बार होती रहती है। किसी दिए गए फोल्ड के लिए अगर अंडरसैंपलिंग निर्धारक है, तो यह दोहराया काम सिर्फ़ व्यर्थ है।
Problem setup
ऐसा तरीका लें जिसमें imblearn‑संगत Pipeline के भीतर स्केलिंग, अंडरसैंपलिंग और एक ग्रेडिएंट बूस्टिंग मॉडल हों। हाइपरपैरामीटर HalvingRandomSearchCV से ट्यून किए जाते हैं। सेटअप सुथरा दिखता है, पर सर्च के दौरान हर fit पर अंडरसैंपलिंग फिर‑फिर चलती रहती है।
def fit_with_resampling(sampler_obj, estimator_obj, do_scale, search_space, X_tr, y_tr):
    # टेस्ट फोल्ड को अलग रखने के लिए रिसैंपलिंग पाइपलाइन का हिस्सा है
    if do_scale is True:
        pipe = Pipeline([
            ("scale", MinMaxScaler()),
            ("balance", sampler_obj),
            ("algo", estimator_obj),
        ])
    else:
        pipe = Pipeline([
            ("balance", sampler_obj),
            ("algo", estimator_obj),
        ])
    searcher = HalvingRandomSearchCV(
        estimator=pipe,
        param_distributions=search_space,
        n_candidates="exhaust",
        factor=3,
        resource="algo__n_estimators",
        max_resources=500,
        min_resources=10,
        scoring="roc_auc",
        cv=3,
        random_state=10,
        refit=True,
        n_jobs=-1,
    )
    searcher.fit(X_tr, y_tr)
    return searcherयह सेटअप सर्च में हर कैंडिडेट की CV इटरेशन पर दोबारा अंडरसैंपल करेगा। अगर किसी फोल्ड के लिए अंडरसैंपलिंग रन के बीच नहीं बदलती, तो यह पुनरावृत्ति अनावश्यक है।
What actually causes the inefficiency
पाइपलाइन के भीतर की रिसैंपलिंग सर्च द्वारा किए गए हर fit पर फिर से चलती है। HalvingRandomSearchCV कई कैंडिडेट्स को आज़माता है और अपने successive halving शेड्यूल के तहत प्रति स्प्लिट कई fits करता है। चूंकि पाइपलाइन में सैंपलर शामिल है, इसलिए हर fit उसी फोल्ड के लिए वही अंडरसैंपल्ड सबसेट दोबारा निकालता है, भले ही सैंपलर और डेटा न बदले हों।
Solution: precompute undersampled folds and pass them to the search
व्यावहारिक रास्ता यह है कि फोल्ड्स एक बार बनाएं, प्रत्येक ट्रेनिंग फोल्ड को ठीक एक बार अंडरसैंपल करें, सिर्फ़ इंडेक्स सुरक्षित रखें, और इन्हीं पूर्व‑गणना किए गए स्प्लिट्स को HalvingRandomSearchCV को उसके cv आर्गुमेंट के जरिए दें। इससे इवैल्यूएशन प्रोटोकॉल जस‑का‑तस रहता है, अलग छोड़ा गया टेस्ट फोल्ड साफ़ बना रहता है, और एक ही रिसैंपलिंग को दोहराना बंद हो जाता है।
def build_sampled_folds(X_arr, y_arr, splitter, sampler):
    cached = []
    for tr_idx, te_idx in splitter.split(X_arr, y_arr):
        sampler.fit_resample(X_arr[tr_idx], y_arr[tr_idx])
        tr_idx_sampled = tr_idx[sampler.sample_indices_]
        cached.append((tr_idx_sampled, te_idx))
    return cached
fold_splits = build_sampled_folds(
    X_arr=X_train, y_arr=y_train,
    splitter=KFold(3),
    sampler=RandomUnderSampler()
)इन फोल्ड्स के साथ, इन्हें सर्च में प्लग करें और पाइपलाइन को न्यूनतम रखें। पाइपलाइन में मॉडल स्टेप का नाम उन पैरामीटर नामों से मेल खाना चाहिए जिन्हें आप सर्च में पास करते हैं।
learners = [
    (GradientBoostingClassifier(), {"algo__max_depth": [1, 3]}),
    (RandomForestClassifier(), {"algo__max_depth": [1, 3]})
]
for clf, grid in learners:
    print(clf)
    pipe = Pipeline([
        ("algo", clf)
    ])
    tuner = HalvingRandomSearchCV(
        estimator=pipe,
        param_distributions=grid,
        n_candidates="exhaust",
        factor=3,
        resource="algo__n_estimators",
        max_resources=500,
        min_resources=10,
        scoring="roc_auc",
        cv=fold_splits,  # पूर्व‑गणना किए गए अंडरसैंपल्ड फोल्ड्स
        random_state=10,
        refit=True,
        n_jobs=-1,
        verbose=True
    )
    tuner.fit(X_train, y_train)2 कैंडिडेट्स के लिए 3 फोल्ड्स फिट हो रहे हैं, कुल 6 फिट्स
Details that matter when refitting
जब refit=True होता है, HalvingRandomSearchCV पूरे डेटासेट पर सबसे अच्छा एस्टीमेटर फिर से फिट करता है। अगर उद्देश्य ट्रेनिंग सेट के अंडरसैंपल्ड संस्करण पर ही ट्रेनिंग जारी रखना है, तो refit=False सेट करें और फिर चुनी गई सर्वोत्तम कॉन्फिगरेशन को अलग से उसी अंडरसैंपल्ड ट्रेनिंग डेटा पर फिट करें जिसे आप इस्तेमाल करना चाहते हैं। इससे सर्च के बाद पूरे इनपुट पर दोबारा ट्रेनिंग करने से बचा जा सकता है।
Why this approach is worth it
पूर्व‑गणना किए गए अंडरसैंपल्ड फोल्ड्स हाइपरपैरामीटर ऑप्टिमाइज़ेशन के दौरान फालतू रिसैंपलिंग को हटा देते हैं, बिना इवैल्यूएशन प्रोटोकॉल बदले। आप यही ट्रेन/टेस्ट स्प्लिट लॉजिक XGBoost, CatBoost, GradientBoostingClassifier या RandomForestClassifier जैसे अलग‑अलग मॉडलों में दोबारा उपयोग कर सकते हैं, जबकि प्रत्येक फोल्ड पर अंडरसैंपलिंग की लागत एक ही पास तक सीमित रहती है। यह उन नाज़ुक वर्कअराउंड्स से भी बचाता है जिनमें मूल और अंडरसैंपल्ड डेटा को जोड़कर यह समन्वय बैठाया जाता है कि किस हिस्से का कौन‑सा फोल्ड है — यह रास्ता गलती‑प्रवण है और लीकेज का जोखिम बढ़ाता है।
Takeaways
जब रिसैंपलिंग निर्धारक और फोल्ड‑विशिष्ट हो, तो उसे सर्च में इस्तेमाल होने वाली पाइपलाइन से बाहर रखें। फोल्ड्स एक बार बनाएं, सैंपलर की sample_indices_ से अंडरसैंपल्ड ट्रेनिंग इंडेक्स निकालें, और इन्हें सीधे HalvingRandomSearchCV को cv के रूप में पास करें। यदि अंतिम मॉडल को अंडरसैंपल्ड सेट पर ट्रेन करना ज़रूरी है, तो ऑटो‑रीफिट बंद करें और वही डेटा लेकर अलग से अंतिम फिट करें जिस पर आप मॉडल को सिखाना चाहते हैं। इस तरह आपकी क्रॉस‑वैलिडेशन साफ़, कुशल और जिन‑जिन मॉडलों को आप ट्यून कर रहे हैं, उन सबमें सुसंगत रहती है।
यह लेख StackOverflow पर एक प्रश्न (लेखक: Sole Galli) और SLebedev777 के उत्तर पर आधारित है।