2025, Oct 19 12:31
राउंडिंग वाले उद्देश्यों पर L-BFGS-B क्यों अटकता: बेहतर रणनीतियाँ और सेटिंग्स
राउंडिंग/क्वांटाइज़्ड उद्देश्यों पर L-BFGS-B क्यों ठहर जाता: उपाय—eps बढ़ाना, derivative‑free COBYQA/Nelder‑Mead, और abs जैसी फ़ंक्शन का वर्ग न्यूनतम करना।
उद्देशीय फ़ंक्शन के भीतर राउंडिंग करना अक्सर ग्रेडिएंट-आधारित ऑप्टिमाइज़रों को उलझा देता है। इससे बनने वाले असतत प्लेटो और तीखे मोड़ संख्यात्मक अवकलन को अविश्वसनीय बना देते हैं, और L-BFGS-B जैसी विधियाँ बेहतर न्यूनतम मौजूद होने पर भी शुरुआती बिंदु पर अभिसरण की रिपोर्ट कर सकती हैं।
समस्या सेटअप
मान लें हमारे पास ऐसा उद्देशीय है जो मानों को छह दशमलव तक राउंड करता है। यह उन वास्तविक पाइपलाइनों जैसा है जहाँ ज्यामितीय गणना लगभग 1e-6 सटीकता पर होती है।
import numpy as np
def metric_snap6(u):
    return np.round(abs(-0.3757609503198057 * (u - 0.2) + 0.03785161636761336), 6)
इसे सीमाओं के साथ SciPy की डिफ़ॉल्ट विधि (सीमाएँ होने पर L-BFGS-B) से ऑप्टिमाइज़ करने पर प्रक्रिया तुरंत रुक सकती है:
from scipy.optimize import minimize
res = minimize(metric_snap6, 1, bounds=((0, np.inf),))
CONVERGENCE: NORM OF PROJECTED GRADIENT <= PGTOL
शून्य इटरेशन और शुरुआत में शून्य जैकोबियन इस बात का संकेत है कि finite-difference ग्रेडिएंट का आकलन एक सपाट, राउंड किए गए प्लेटो पर हुआ।
वास्तव में कहाँ दिक्कत आती है
छह दशमलव तक राउंड करने से बड़े क्षेत्र बनते हैं जहाँ इनपुट में छोटे बदलाव आउटपुट को बिल्कुल नहीं बदलते। L-BFGS-B एक छोटे स्टेप (उसका eps) से संख्यात्मक रूप से ग्रेडिएंट का अनुमान लगाता है, और अगर दोनों सैंपल एक ही राउंड किए गए मान पर आ जाएँ, तो अंतर शून्य जैसा दिखता है। तब विधि मान लेती है कि उतरने की कोई दिशा नहीं है और रुक जाती है। यदि आपका उद्देशीय abs() जैसा है, तो मोड़ पर व्युत्पन्न अचानक बदलता है; यह आकृति L-BFGS-B जैसी विधियों की लाइन सर्च में प्रयुक्त Wolfe शर्तों का उल्लंघन कर सकती है, और द्विघात मॉडल फिट करने वाले एल्गोरिद्म के लिए भी चुनौती बनती है।
व्यावहारिक उपाय
ऐसी कम-सटीकता, असमतल उद्देश्यों के साथ प्रगति करने के तीन सीधे तरीके हैं।
पहला, L-BFGS-B में उपयोग किए जाने वाले finite-difference स्टेप को बढ़ाएँ। बड़ा eps इस संभावना को बढ़ाता है कि संख्यात्मक ग्रेडिएंट के सैंपल अलग-अलग राउंड किए गए मानों पर आएँ।
from scipy.optimize import minimize
import numpy as np
def metric_snap6(u):
    return np.round(abs(-0.3757609503198057 * (u - 0.2) + 0.03785161636761336), 6)
print("L-BFGS-B with larger eps")
print(minimize(
    metric_snap6,
    1,
    bounds=((0, np.inf),),
    method="L-BFGS-B",
    options=dict(eps=1e-5)  # default is 1e-8
))
दूसरा, ऐसी विधियों पर जाएँ जिन्हें ग्रेडिएंट की जरूरत नहीं होती—यानी derivative-free तरीके। इस परिदृश्य में COBYQA एक मजबूत सामान्य-उद्देश्य विकल्प है, और Nelder-Mead भी काम आता है। COBYQA में आप initial_tr_radius से आरंभिक स्टेप तय कर सकते हैं। Nelder-Mead शून्य-नहीं पैरामीटरों के लिए 5% और शून्य पैरामीटरों के लिए 0.00025 के आरंभिक स्टेप से शुरू करता है; initial_simplex देकर आप इसे बदल सकते हैं।
from scipy.optimize import minimize
import numpy as np
def metric_snap6(u):
    return np.round(abs(-0.3757609503198057 * (u - 0.2) + 0.03785161636761336), 6)
print("COBYQA (derivative-free)")
print(minimize(
    metric_snap6,
    1,
    bounds=((0, np.inf),),
    method="COBYQA",
    options=dict(initial_tr_radius=1.0)
))
print("Nelder-Mead (derivative-free)")
print(minimize(
    metric_snap6,
    1,
    bounds=((0, np.inf),),
    method="Nelder-Mead"
))
तीसरा, यदि आपका उद्देशीय abs() जैसा है—टुकड़ों में रैखिक और तीखे न्यूनतम वाला—तो इसके वर्ग को ऑप्टिमाइज़ करें। वर्ग करने से वक्रता इतनी सम हो जाती है कि लाइन-सर्च-आधारित विधियों को Wolfe शर्तें पूरी करने में मदद मिलती है, और COBYQA जैसे द्विघात मॉडल बनाने वाले एल्गोरिद्म को भी लाभ होता है। यही काम कम मूल्यांकन के साथ हो सकता है।
from scipy.optimize import minimize
import numpy as np
def metric_snap6(u):
    return np.round(abs(-0.3757609503198057 * (u - 0.2) + 0.03785161636761336), 6)
def metric_snap6_sq(u):
    return metric_snap6(u) ** 2
print("L-BFGS-B on squared objective")
print(minimize(
    metric_snap6_sq,
    1,
    bounds=((0, np.inf),),
    method="L-BFGS-B",
    options=dict(eps=1e-5)
))
print("COBYQA on squared objective")
print(minimize(
    metric_snap6_sq,
    1,
    bounds=((0, np.inf),),
    method="COBYQA",
    options=dict(initial_tr_radius=1.0)
))
print("Nelder-Mead on squared objective")
print(minimize(
    metric_snap6_sq,
    1,
    bounds=((0, np.inf),),
    method="Nelder-Mead"
))
इस उदाहरण में, L-BFGS-B 144 की जगह 6 मूल्यांकन में अभिसरित हो जाता है, और COBYQA 29 की बजाय 16 में। Nelder-Mead में कोई बदलाव नहीं आता।
यह क्यों मायने रखता है
कम-सटीकता, क्वांटाइज़्ड या टुकड़ों में रैखिक उद्देश्यों का सामना वास्तविक प्रणालियों में अक्सर होता है, खासकर जहाँ गणना तय राउंडिंग या डिस्क्रिटाइज़ेशन से बंधी हो। यह समझना कि सपाट प्लेटो पर ग्रेडिएंट अनुमान कैसे बर्ताव करते हैं, abs() जैसी किन्क्स लाइन सर्च के लिए क्यों परेशानी बनती हैं, और ट्रस्ट-रीजन या सिंप्लेक्स विधियाँ कैसे प्रतिक्रिया देती हैं—आपको ऐसी तकनीक चुनने में मदद करता है जो सचमुच आगे बढ़े। इससे आप उन झूठे “अभिसरणों” से भी बचते हैं जो राउंडिंग के आर्टिफैक्ट हैं, वास्तविक सर्वोत्तम नहीं।
मुख्य बातें
यदि कोई ऑप्टिमाइज़र शुरू में ही projected gradient शून्य बताकर रुक जाए, तो राउंडिंग या असमतलता पर संदेह करें। L-BFGS-B के लिए eps बढ़ाएँ ताकि finite-difference ग्रेडिएंट उपयोगी मिले। जब ग्रेडिएंट भरोसेमंद न हों, तो COBYQA या Nelder-Mead जैसी derivative-free रणनीतियाँ आज़माएँ, और आवश्यकता अनुसार initial_tr_radius या initial simplex को ट्यून करें। और अगर उद्देशीय abs() जैसा है, तो उसके वर्ग को न्यूनतम करें ताकि लाइन सर्च और द्विघात मॉडलों के लिए वक्रता बेहतर हो। अक्सर ये छोटे बदलाव ठहरे हुए रन को वास्तविक न्यूनतम की विश्वसनीय खोज में बदल देते हैं।
यह लेख StackOverflow पर एक प्रश्न (लेखक: Logan Pageler) और Nick ODell के उत्तर पर आधारित है।