2025, Sep 29 15:32

functools.partial के साथ curve_fit में पैरामीटर फ्रीज़ करने का भरोसेमंद तरीका

scipy.optimize.curve_fit में functools.partial से पैरामीटर फ्रीज़ क्यों टूटता है, और inspect.signature रैपर से भरोसेमंद समाधान कैसे पाएं. स्पष्ट उदाहरण और कोड।

मॉडल फिट करते समय एक या अधिक पैरामीटर को फ्रीज़ करने की जरूरत अक्सर पड़ती है। scipy.optimize.curve_fit के साथ बहुत-से डेवलपर नाम से कुछ आर्ग्युमेंट तय करने के लिए functools.partial का सहारा लेते हैं। यह कुछ परिस्थितियों में ठीक काम करता है, लेकिन कुछ में पहली नज़र में हैरान करने वाले तरीके से टूट जाता है। नीचे बताया गया है कि ऐसा क्यों होता है और मॉडल की लॉजिक बदले बिना इसे भरोसेमंद रूप से कैसे ठीक किया जा सकता है।

समस्या दिखाने वाला न्यूनतम उदाहरण

पहले, जब किसी पैरामीटर को पोज़िशनल तरीके से फिक्स किया जाता है, तो प्रवाह अपेक्षित रूप से काम करता है।

from scipy.optimize import curve_fit
from functools import partial
x_vals = [0] * 5
y_vals = x_vals
def model_fn(x, a, b):
    return x + a + b
fixed_pos = partial(model_fn, 2)
opt_pos, _ = curve_fit(fixed_pos, x_vals, y_vals)
print(opt_pos)  # [-2.]

कीवर्ड द्वारा फिक्स करना कभी चलता है, कभी नहीं। b को नाम से फ्रीज़ करना ठीक रहता है:

from scipy.optimize import curve_fit
from functools import partial
x_vals = [0] * 5
y_vals = x_vals
def model_fn(x, a, b):
    return x + a + b
fixed_b = partial(model_fn, b=2)
opt_b, _ = curve_fit(fixed_b, x_vals, y_vals)
print(opt_b)  # [-2.]

लेकिन a को नाम से फ्रीज़ करने पर त्रुटि आती है:

from scipy.optimize import curve_fit
from functools import partial
x_vals = [0] * 5
y_vals = x_vals
def model_fn(x, a, b):
    return x + a + b
fixed_a = partial(model_fn, a=2)
opt_a, _ = curve_fit(fixed_a, x_vals, y_vals)  # ValueError उठाता है: Unable to determine number of fit parameters.

असल में हो क्या रहा है

curve_fit दिए गए फ़ंक्शन की कॉल सिग्नेचर को देखकर तय करता है कि किन आर्ग्युमेंट को डेटा मिलेगा और कौन-से फ्री पैरामीटर ऑप्टिमाइज़ किए जाएंगे। खास तौर पर, यह inspect.signature का उपयोग करता है और कम से कम दो पोज़िशनल पैरामीटर की उम्मीद करता है: एक x डेटा के लिए, और एक या अधिक जिन्हें ऑप्टिमाइज़ करना है।

partial का उपयोग सिग्नेचर को प्रभावित करता है। पोज़िशनल बाइंडिंग शेष पैरामीटर को पोज़िशनल ही बनाए रखती है; जबकि कीवर्ड बाइंडिंग शेष पैरामीटर को KEYWORD_ONLY में बदल सकती है। जब ऑप्टिमाइज़र के लिए कोई पोज़िशनल पैरामीटर नहीं बचता, तो curve_fit “Unable to determine number of fit parameters” त्रुटि फेंकता है।

from functools import partial
from inspect import signature
def model_fn(x, a, b):
    pass
q1 = partial(model_fn, 1)       # x को पोज़िशनल रूप से बाँधें
q2 = partial(model_fn, a=1)     # a को कीवर्ड से बाँधें
q3 = partial(model_fn, b=1)     # b को कीवर्ड से बाँधें
print(*[f"{p.name}: {p.kind.name}" for p in signature(q1).parameters.values()], sep=", ")
print(*[f"{p.name}: {p.kind.name}" for p in signature(q2).parameters.values()], sep=", ")
print(*[f"{p.name}: {p.kind.name}" for p in signature(q3).parameters.values()], sep=", ")

इन सिग्नेचर से पैटर्न साफ दिखता है: जब a को कीवर्ड से फिक्स किया जाता है, तो x पोज़िशनल रहता है लेकिन बाकी पैरामीटर KEYWORD_ONLY हो जाते हैं, जिससे ऑप्टिमाइज़ करने के लिए कोई पोज़िशनल पैरामीटर नहीं बचता; जबकि b को कीवर्ड से फिक्स करने पर x और a पोज़िशनल बने रहते हैं, इसलिए curve_fit x को डेटा दे सकता है और a को फिट कर सकता है।

कीवर्ड-स्टाइल फ्रीज़िंग को बनाए रखने वाला व्यावहारिक उपाय

अगर आप पैरामीटर को नाम से फ्रीज़ करना चाहते हैं और साथ ही curve_fit को साफ पोज़िशनल इंटरफ़ेस देना चाहते हैं, तो मॉडल को ऐसे रैप करें कि फिक्स किए गए पैरामीटर सिग्नेचर से हट जाएँ और शेष पैरामीटर पोज़िशनल बने रहें। नीचे दिया हेल्पर मॉडल की लॉजिक जस की तस रखता है और सिर्फ ऑप्टिमाइज़र के लिए कॉल करने योग्य की सिग्नेचर समायोजित करता है।

import inspect
def lock_params(target, **fixed):
    sig0 = inspect.signature(target)
    pos_kinds = (
        inspect.Parameter.POSITIONAL_OR_KEYWORD,
        inspect.Parameter.POSITIONAL_ONLY,
    )
    posnames = [p.name for p in sig0.parameters.values() if p.kind in pos_kinds]
    remain_params = [
        inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD)
        for name in posnames
        if name not in fixed
    ]
    sig_new = inspect.Signature(remain_params)
    def bridge(*args, **kwargs):
        bound = sig_new.bind(*args, **kwargs)
        bound.apply_defaults()
        return target(**bound.arguments, **fixed)
    bridge.__signature__ = sig_new
    bridge.__name__ = target.__name__
    return bridge

इसे ऐसे उपयोग करें:

from scipy.optimize import curve_fit
x_vals = [0] * 5
y_vals = [0] * 5
def model_fn(x, a, b):
    return x + a + b
lock_x = lock_params(model_fn, x=1)
lock_a = lock_params(model_fn, a=1)
lock_b = lock_params(model_fn, b=1)
print(curve_fit(lock_x, x_vals, y_vals)[0])  # [-1.]
print(curve_fit(lock_a, x_vals, y_vals)[0])  # [-1.]
print(curve_fit(lock_b, x_vals, y_vals)[0])  # [-1.]

इससे गणना-सम्बंधी लॉजिक वही रहता है, और curve_fit को कम-से-कम एक पोज़िशनल पैरामीटर फिट करने के लिए सुनिश्चित किया जाता है।

यह बारीकी क्यों मायने रखती है

मॉडल-फिटिंग पाइपलाइन अक्सर सरल शुरू होती है और बाधाएँ तथा प्रायर जुड़ने के साथ जटिल होती जाती है। अगर फ़ंक्शन की सिग्नेचर चुपचाप पैरामीटर को KEYWORD_ONLY में धकेल दे, तो ऐसा ऑप्टिमाइज़र जो पोज़िशनल पैरामीटर पर निर्भर है, गैर-स्पष्ट तरीकों से विफल हो सकता है। यह समझना कि curve_fit कॉल करने योग्य की सिग्नेचर को पढ़ता है, नाज़ुक रैपर से बचने और “no fit parameter” जैसी त्रुटियों को डिबग करने में समय बचाता है।

निष्कर्ष

यदि आप functools.partial से पैरामीटर फ्रीज़ करते हैं और “fit parameters” तय न हो पाने वाली ValueError मिलती है, तो संभव है कि आप ऐसा callable दे रहे हों जिसमें ऑप्टिमाइज़ करने के लिए कोई पोज़िशनल आर्ग्युमेंट नहीं बचा। पोज़िशनल बाइंडिंग अपनाना या ऐसा रैपर उपयोग करना जो पोज़िशनल आर्ग्युमेंट्स को बनाए रखे, समस्या को साफ़-सुथरे ढंग से सुलझा देता है। संदेह हो तो अपने callable पर inspect.signature चलाएँ—यही curve_fit देखेगा—और यह पक्का करने का सबसे तेज़ तरीका है कि ऑप्टिमाइज़र के पास फिट करने के लिए कम-से-कम एक पोज़िशनल पैरामीटर है।

यह लेख StackOverflow के प्रश्न (लेखक — euronion) और James के उत्तर पर आधारित है।