- लेखक NumPy के प्रति अपनी असंतुष्टि पर कई उदाहरणों के साथ समस्याएँ समझाते हैं
- साधारण array operations NumPy में आसान हैं, लेकिन dimensions बढ़ते ही जटिलता और भ्रम तेज़ी से बढ़ जाते हैं
- Broadcasting और advanced indexing जैसी NumPy की डिज़ाइन में स्पष्टता और abstraction की कमी है
- explicit रूप से axis निर्दिष्ट करने के बजाय, अनुमान और trial-and-error पर निर्भर कोड लिखना लगभग अनिवार्य हो जाता है
- बेहतर array language के लिए कुछ विचार प्रस्तुत किए गए हैं, और ठोस विकल्प अगली पोस्ट में पेश किए जाएंगे
परिचय: NumPy के प्रति प्रेम और असंतोष
- लेखक बताते हैं कि उन्होंने लंबे समय तक NumPy का उपयोग किया है, लेकिन उसकी सीमाओं से काफ़ी निराश भी हुए हैं
- NumPy Python में array computation के लिए एक अनिवार्य और प्रभावशाली library है
- PyTorch जैसी आधुनिक machine learning libraries में भी NumPy जैसी समस्याएँ मौजूद हैं
NumPy की आसान और कठिन बातें
- बुनियादी operations, जैसे सरल linear equations हल करना, स्पष्ट और सुरुचिपूर्ण syntax के साथ किए जा सकते हैं
- लेकिन जैसे ही array dimensions बढ़ते हैं या operations जटिल होते हैं, for loops के बिना batch processing की ज़रूरत पड़ती है
- ऐसे environment में जहाँ loops का उपयोग नहीं किया जा सकता (जैसे GPU computation), अजीब vectorized syntax या विशेष function call patterns की आवश्यकता होती है
- लेकिन इन functions का सटीक उपयोग अस्पष्ट होता है, और केवल documentation से भी उसे साफ़-साफ़ समझना कठिन है
- वास्तव में, numpy का
linalg.solve function high-dimensional arrays के मामले में सही तरह से कैसे इस्तेमाल किया जाए, इस पर किसी को भी पूरी तरह भरोसा होना मुश्किल है
NumPy की समस्याएँ
- NumPy में multi-dimensional arrays के किसी हिस्से या किसी खास axis पर operations लागू करने के लिए एक सुसंगत सिद्धांत की कमी है
- जब array dimensions 2 या उससे कम हों तो बात स्पष्ट रहती है, लेकिन 3 या उससे अधिक dimensions पर हर array में operation किन axes पर होना चाहिए, यह अस्पष्ट हो जाता है
- dimensions को explicit रूप से मिलाने के लिए None का उपयोग, broadcasting,
np.tensordot जैसी जटिल विधियाँ मजबूरन अपनानी पड़ती हैं
- ये तरीके गलतियों की संभावना बढ़ाते हैं, code readability घटाते हैं, और bugs की आशंका बढ़ा देते हैं
लूप और स्पष्टता
- वास्तव में यदि loops की अनुमति हो, तो और भी संक्षिप्त और स्पष्ट code लिखा जा सकता है
- loop-based code कम परिष्कृत दिख सकता है, लेकिन स्पष्टता के लिहाज़ से इसका बड़ा लाभ है
- इसके विपरीत, array dimensions बदलते ही transpose या axis order के बारे में बार-बार सोचना पड़ता है, जिससे जटिलता बढ़ती है
np.einsum: एक अपवादस्वरूप अच्छा function
- np.einsum शक्तिशाली है क्योंकि यह axes के नाम निर्दिष्ट करने वाली एक लचीली domain-specific language देता है
- einsum में operation का इरादा स्पष्ट रहता है और उसका generalization भी शानदार है, इसलिए जटिल axis operations को explicit रूप से लागू किया जा सकता है
- लेकिन einsum जैसी शैली का support कुछ ही operations तक सीमित है; उदाहरण के लिए, इसे
linalg.solve पर लागू नहीं किया जा सकता
Broadcasting की समस्याएँ
- NumPy की मुख्य तरकीब broadcasting है, जो dimensions न मिलने पर उन्हें अपने-आप मिलाने की सुविधा देती है
- सरल मामलों में यह सुविधाजनक है, लेकिन व्यवहार में यह dimensions को स्पष्ट रूप से समझना कठिन बना देती है और गलती के कई मामले पैदा करती है
- broadcasting implicit होने के कारण code पढ़ते समय हर बार यह जाँचना पड़ता है कि operation वास्तव में कैसे काम करेगा
Indexing की अस्पष्टता
- NumPy की advanced indexing में array shape का अनुमान लगाना बेहद कठिन और अस्पष्ट है
- indexing के अलग-अलग संयोजनों के अनुसार result array का shape बदल जाता है, इसलिए वास्तविक अनुभव के बिना इसका अनुमान लगाना मुश्किल है
- indexing rules समझाने वाला documentation भी लंबा और जटिल है, इसलिए इसे सीखने में काफ़ी समय लगता है
- भले ही केवल simple indexing ही इस्तेमाल करनी हो, कुछ operations में advanced indexing से बचना संभव नहीं होता
NumPy function design की सीमाएँ
- कई NumPy functions केवल कुछ खास array shapes के लिए optimized हैं
- high-dimensional arrays के लिए अतिरिक्त axes arguments, अलग function names, या conventions का उपयोग करना पड़ता है, और यह function दर function सुसंगत नहीं है
- यह abstraction और reuse जैसे मूल programming principles के विरुद्ध जाने वाली संरचना है
- किसी विशेष समस्या को हल करने वाला function इस्तेमाल करने के बाद भी, अलग-अलग arrays और axes पर उसे दोबारा लागू करने के लिए अक्सर code को लगभग नए सिरे से लिखना पड़ता है
वास्तविक उदाहरण: self-attention implementation
- NumPy में self-attention implementation लिखते समय, loops का उपयोग करने पर code स्पष्ट रहता है, लेकिन vectorization को मजबूरी बनाने पर code जटिल हो जाता है
- multi-head attention जैसे high-dimensional operations की ज़रूरत होने पर einsum और axis transformation को मिलाकर उपयोग करना पड़ता है, जिससे code कठिन हो जाता है
निष्कर्ष और विकल्प
- लेखक कहते हैं कि NumPy "दूसरी array languages की तुलना में कई मायनों में खराब है, लेकिन फिर भी बाज़ार में महत्वपूर्ण बन चुकी लगभग एकमात्र पसंद" है
- NumPy की कई समस्याओं—जैसे broadcasting, indexing की अस्पष्टता, और functions की असंगति—को दूर करने के लिए उन्होंने एक बेहतर array language का prototype बनाया है
- ठोस सुधार प्रस्ताव (नई array language API) को वे आगे एक अलग लेख में प्रस्तुत करने की योजना रखते हैं
4 टिप्पणियां
यह सुनने में Julia के पैदा होने की वजह वाली कहानी जैसा लगता है। लाइब्रेरीज़ को सीखना तो पड़ता है, लेकिन NumPy की कई समस्याओं को हल कर देने के मामले में यह सचमुच एक बहुत आकर्षक विकल्प लगता है।
अगर
vectorizationका सही इस्तेमाल न कर पाएं तो NumPy की performance बुरी तरह गिर जाती है। उन बातों को ध्यान में रखकर लिखना तनावपूर्ण भी है और मुश्किल भी।लगता है कि थोड़ी पुरानी Python libraries में लगभग सबमें एक जैसी समस्या होती है।
Hacker News राय
bका type देखकर docs पढ़ना कठिन है, लेकिन returned shape का विवरण दिया गया है, इसलिए यह जाँचना ज़रूरी है किbvector वास्तव में matrix रूप में है या नहीं, ख़ासकरK=1के मामले मेंda.sel(x=some_x).isel(t=-1).mean(["y", "z"])जैसे indexing आसान हैं, और dimension names का सम्मान होने से broadcasting भी स्पष्ट रहता है. कई CRS वाले geospatial data को संभालने में इसकी ताकत है. Arviz के साथ भी इसका उपयोग शानदार है, इसलिए Bayesian analysis में अतिरिक्त dimensions संभालना आसान हो जाता है. कई arrays को एक dataset में बाँधकर common coordinates साझा किए जा सकते हैं, इसलिएds.isel(t=-1)जैसा ऑपरेशन time axis वाले सभी arrays पर आसानी से चल जाता हैarray[:, :, None]जैसी syntax असुविधाजनक लगती थी; लगा था शायद सिर्फ़ मुझे ऐसा लगता है, इसलिए समान राय देखकर अच्छा लगाnp.linalg.solveजैसी चीज़ को सबसे तेज़ मानकर हर हाल में उसी के अनुरूप लिखने वाली ‘black box’ approach सही नहीं लगती; कई कारणों से problem-specific kernels ख़ुद लिखना बेहतर हो सकता है'\'operator जैसा पूरी तरह optimized black box ही हैvmapआज़माना उपयोगी हो सकता हैsqueezeआदि से किया जा सकता है; यह समस्या इतनी अस्पष्ट है कि समझना ही कठिन लगता हैreshapeसे भी इसे संभाला जा सकता हैpoly1dobjectPको दाईं तरफ़ सेz0के साथ गुणा करें तोpoly1dमिलता है, लेकिन बाईं तरफ़ सेz0*Pलिखें तो सिर्फ़ array लौटता है और type conversion चुपचाप हो जाता है. quadratic का leading coefficient भीP.coef[0]औरP[2]दोनों तरीकों से लिया जा सकता है, जो भ्रम पैदा करता है. आधिकारिक रूप सेpoly1dएक ‘पुराना’ API है और नए code के लिएPolynomialclass की सिफारिश की जाती है, लेकिन व्यवहार में deprecated warning भी नहीं मिलती. इस तरह के type conversions और datatype inconsistencies पूरी library में बिखरे हुए landmines जैसे हैं, जो debugging को दुःस्वप्न बना देते हैंto_numpy()जैसी कुछ चीज़ें ही थोड़ी एकरूप हैं. नतीजा यह कि समस्या हल करने से ज़्यादा समय data format conversion में चला जाता है. Julia भी सिर्फ़ फ़ायदों से भरी नहीं है, लेकिन units और uncertainty जैसी कई libraries के बीच उसका integration अच्छा है, जबकि Python में हमेशा बहुत boilerplate code लिखना पड़ता हैarray-apiproject पूरे Python ecosystem में array manipulation API को standardize करने की कोशिश कर रहा हैeinsumमें डालकरoptimize="optimal"के साथ matrix chain multiplication algorithm इस्तेमाल कर performance बढ़ाने की कोशिश की. सचमुच सामान्य vectorized implementation की तुलना में लगभग 2x तेज़ी मिली, लेकिन हैरानी की बात यह रही कि loop-आधारित सीधी-सादी implementation उससे भी तेज़ निकली. वजह जानने वालों के लिए code देखना उपयोगी होगा. अनुमान है किeinsumके अंदर cache coherency में अभी और सुधार की गुंजाइश है