ट्रैक
/
Julia
Julia
/
सिलेबस
/
वेक्टर ऑपरेशन
वे

वेक्टर ऑपरेशन में Julia

2 अभ्यास

वेक्टर ऑपरेशन के बारे में

Vectors कॉन्सेप्ट में हमने कहा था कि "ऐरे Julia भाषा के केंद्र में हैं" और वेक्टर एक 1-आयामी ऐरे होता है।

इसे देखते हुए हम यह उम्मीद कर सकते हैं कि भाषा वेक्टर के साथ कुछ करने के लिए ढेरों बहुमुखी और शक्तिशाली तरीके देती होगी, चाहे उसका मतलब कुछ भी हो।

शब्दावली पर एक बात: इस दस्तावेज़ में भले ही बहुत कुछ "वेक्टर" के बारे में हो, इसका एक बड़ा हिस्सा किसी भी इटरेबल टाइप पर भी लागू होता है: रेंज, ट्यूपल, सेट और कई अन्य।

वेक्टर इनपुट लेने वाले फंक्शन

कुछ बहुत आसान फंक्शन वेक्टर इनपुट लेते हैं और (1-आयामी इनपुट के लिए) एक स्केलर आउटपुट लौटाते हैं।

v = [2, 3, 4]
length(v)  # => 3
sum(v)  # => 9

जब हम बहुआयामी ऐरे वाले कॉन्सेप्ट तक पहुँचेंगे, तब यह साफ हो जाएगा कि यहाँ एक स्केलर लौटाना ज़रूरी नहीं, बल्कि यह डाइमेंशन में कमी है। अगर यह बात आपको समझ नहीं आ रही, तो अभी इसकी चिंता छोड़ दीजिए।

इस तरह के और भी बहुत से फंक्शन हैं। कुछ उदाहरणों के लिए Statistics कॉन्सेप्ट देखिए।

कुछ फंक्शन एक साथ कई वेक्टर पर भी काम करते हैं, जैसे (बहुत काम का) zip.

julia> z = zip( 1:3, ['a', 'b', 'c'], ["I", "make", "tuples"] )
zip([1, 2, 3], ['a', 'b', 'c'], ["I", "make", "tuples"])

# convert iterator to vector
julia> collect(z)
3-element Vector{Tuple{Int64, Char, String}}:
 (1, 'a', "I")
 (2, 'b', "make")
 (3, 'c', "tuples")

zip() कितने भी वेक्टर जैसे इनपुट ले सकता है और ट्यूपल का एक इटरेटर लौटाता है।

आम तौर पर सभी इनपुट की लंबाई एक जैसी होती है। अगर कोई एक छोटा है, तो बाकी सबको काटकर सबसे छोटी लंबाई तक सीमित कर दिया जाता है: शायद यही आपका इरादा रहा हो, पर आम तौर पर यह आपके कोड में एक बग होता है।

अंकगणित

मान लीजिए आपके पास एक संख्यात्मक वेक्टर है और आप हर वैल्यू में से 0.5 घटाना चाहते हैं।

julia> v = [1.2, 1.5, 1.7]
3-element Vector{Float64}:
 1.2
 1.5
 1.7

julia> v - 0.5
ERROR: MethodError: no method matching -(::Vector{Float64}, ::Float64)

यह काम नहीं करता। तो फिर किसी दूसरे वेक्टर को घटाने पर क्या होगा?

julia> v - [0.5, 0.5, 0.5]
3-element Vector{Float64}:
 0.7
 1.0
 1.2

यह काम तो कर गया, पर जैसे-जैसे वेक्टर लंबे होते जाते हैं, यह काफी झंझट भरा हो जाता है और बहुत सारी मेमोरी भी खाता है।

आप सिलेबस में जितना आगे बढ़ चुके हैं, उसके हिसाब से आप शायद और तरीके सोच सकते हैं:

  • एक लूप लिखना, हालाँकि यह लंबा और भद्दा होगा।
  • कॉम्प्रिहेंशन इस्तेमाल करना: [x - 0.5 for x in v] से मनचाहा नतीजा मिल जाता है (Python की तरह)।
  • हायर-ऑर्डर फंक्शन इस्तेमाल करना: map(x -> x - 0.5, v) भी काम करता है (Haskell की तरह, हालाँकि यह कई भाषाओं में आम है)।

अच्छी बात यह है कि Julia में इस समस्या को बहुत आसानी से हल करने के लिए एक "जादुई" डॉट है: आपको बस v .- 0.5 लिखना है।

अगला भाग बताता है कि ऐसा क्यों होता है।

ब्रॉडकास्टिंग

तो v - 0.5 फेल होता है, पर v .- 0.5 काम कर जाता है। अब हमें समझना है कि यह डॉट कर क्या रहा है।

यह दो चीज़ें करता है, और दोनों मिलकर मनचाहा नतीजा देती हैं।

1) हर एलिमेंट पर अलग-अलग लागू करना

पहली बात, किसी इन्फिक्स ऑपरेटर के पहले डॉट लगाने का मतलब है "इस ऑपरेशन को हर एलिमेंट पर अलग-अलग लागू कीजिए"।

इसी तरह, फंक्शन के नाम के बाद डॉट लगाने से वह "वेक्टराइज़" हो जाता है, भले ही वह फंक्शन स्केलर इनपुट के लिए लिखा गया हो।

julia> sqrt.([1, 4, 9])
3-element Vector{Float64}:
 1.0
 2.0
 3.0

एक और बात, इन्फिक्स ऑपरेटर वास्तव में उसके पीछे वाले फंक्शन के लिए सिर्फ सिंटैक्टिक शुगर होते हैं।

यानी, उदाहरण के लिए, इंटरप्रेटर [1, 5, 10] .% 3 को mod.([1, 5, 10], 3) में बदल देता है, और फिर mod. सिंटैक्स चलता है (दोनों तरह से [1, 2, 1] ही लौटता है)।

2) सिंगलटन का विस्तार

पिछले उदाहरण में हमने देखा कि बराबर लंबाई के वेक्टर घटाए जा सकते हैं। पर यह समझ लीजिए कि .- ऑपरेटर - से सुरक्षित इसलिए है क्योंकि यह साफ जताता है कि गणना हर एलिमेंट पर अलग-अलग होनी है।

julia> v .- [0.5, 0.5, 0.5]
3-element Vector{Float64}:
 0.7
 1.0
 1.2

और अगर वेक्टर की लंबाई बराबर न हो तो?

julia> v .- [0.5, 0.5]
ERROR: DimensionMismatch: arrays could not be broadcast to a common size

julia> v .- [0.5,]
3-element Vector{Float64}:
 0.7
 1.0
 1.2

आम तौर पर अलग-अलग लंबाई होना एक एरर है, सिवाय उस स्थिति के जब किसी एक की लंबाई 1 हो (तकनीकी रूप से इसे "सिंगलटन" डाइमेंशन कहते हैं)।

[0.5,] जैसे सिंगलटन, या सिर्फ 0.5, दोहराव के ज़रिए अपने आप ज़रूरी लंबाई तक बढ़ा दिए जाते हैं। यही broadcasting की जान है।

इस "दोहराव" से मेमोरी की चिंता करने वाले लोग निश्चिंत रहें: इसे बहुत कारगर तरीके से लागू किया गया है, जिसमें वैल्यू मेमोरी में वास्तव में कॉपी नहीं होतीं।

दूसरी भाषाओं की ब्रॉडकास्टिंग से परिचित प्रोग्रामर ध्यान दें कि Julia का तरीका (मोटे तौर पर) NumPy जैसा है, पर साइज़ के मेलमिलाप में यह R से बहुत कम ढील देता है।

जगह पर ही ब्रॉडकास्टिंग

अगर मेमोरी की चिंता हो, तो जो ऑपरेशन उसी जगह पर काम करते हैं, वे आवंटन कम करने का आम तरीका होते हैं। पर ऊपर के उदाहरणों में ब्रॉडकास्टिंग वाला ऑपरेशन मूल वेक्टर को बदलने के बजाय एक नया Vector बना देता है।

julia> v = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> v .+ 1
3-element Vector{Int64}:
 2
 3
 4

julia> v
3-element Vector{Int64}:
 1
 2
 3

v को उसी जगह पर बदलने के लिए बदलाव को ब्रॉडकास्ट करना पड़ता है।

julia> v .= v .+ 1
3-element Vector{Int64}:
 2
 3
 4

julia> v .+= 1  # equivalent operation to above
3-element Vector{Int64}:
 3
 4
 5

julia> v
3-element Vector{Int64}:
 3
 4
 5

पर सावधान रहिए! असाइनमेंट ऑपरेटर .= के पहले लगा डॉट बहुत ज़रूरी है।

julia> v = [1, 2, 3];

julia> v = v .+ 1
3-element Vector{Int64}:
 2
 3
 4

julia> v
3-element Vector{Int64}:
 2
 3
 4

लगता है कि यह भी उसी तरह काम कर गया, पर यहाँ v .+ 1 ने एक नया वेक्टर [2, 3, 4] बनाया और फिर उसे वेरिएबल v में असाइन कर दिया, जिससे शुरू वाला वेक्टर [1, 2, 3] मेमोरी में पड़ा रह गया और गार्बेज कलेक्ट होने का इंतज़ार करने लगा। इससे पिछले उदाहरण के मुकाबले दोगुनी मेमोरी लगती है, जहाँ शुरू वाले वेक्टर के लिए आवंटित मेमोरी दोबारा इस्तेमाल हो जाती है।

इसी तरह, बराबर साइज़ के वेक्टर पर ब्रॉडकास्ट करते समय भी ऐसी ही बारीकियाँ आती हैं। उदाहरण के लिए, अगर v और w दो वेक्टर बराबर साइज़ के हों:

  • v .= w v की मेमोरी जगह पर w की एक copy बना देता है। इसके बाद v में किए गए बदलाव w पर असर नहीं डालते, और न ही उलटा।
  • v = w w की मेमोरी जगह की ओर एक और पॉइंटर बना देता है, जिसका नाम v है। इसके बाद v में किए गए बदलाव w में भी दिखते हैं, और उलटा भी।

बिना डॉट वाले ऑपरेटर: एक चेतावनी भरी कहानी

यह उपभाग काफी गणित-भारी है, इसलिए अधिकांश विद्यार्थियों से यह उम्मीद नहीं की जाती कि वे इसे सच में समझें। फिर भी यह एक काम की चेतावनी है, जो तब डिबगिंग में मदद कर सकती है जब आपको अनपेक्षित एरर संदेश दिखें।

julia> v = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> v * v
ERROR: MethodError: no method matching *(::Vector{Int64}, ::Vector{Int64})

# look, no commas
julia> u = [1 2 3]
1×3 Matrix{Int64}:
 1  2  3

julia> u * v
1-element Vector{Int64}:
 14

julia> v * u
3×3 Matrix{Int64}:
 1  2  3
 2  4  6
 3  6  9

अगर संयोग से आपकी पृष्ठभूमि रैखिक बीजगणित में रही है, तो (1) आप आम Exercism उपयोगकर्ता नहीं हैं (पर यहाँ आपका बहुत स्वागत है!) और (2) आप पहचान सकते हैं कि v एक कॉलम वेक्टर है, u एक रो वेक्टर, u * v इनर प्रोडक्ट है और v * u आउटर प्रोडक्ट। Julia गणित के नियमों का पालन करता है, इस बात में भी और हर बात में।

बाकी सब के लिए: कृपया इतना समझ लीजिए कि हम हर एलिमेंट पर गणना के लिए हमेशा डॉट वाले ऑपरेटर इस्तेमाल करने की सलाह क्यों देते हैं: v .* v बिल्कुल वैसा ही काम करता है जैसा आप उम्मीद करते हैं, और [1, 4, 9] देता है।

इंडेक्सिंग

इंडेक्स नंबर से वेक्टर के एलिमेंट चुनने की बात पिछले कॉन्सेप्ट में हो चुकी है।

a = collect('A':'Z')  # => 26-element Vector{Char}

# index with an integer
a[2]  # => 'B'

# index with a range
 a[12:2:18]  # => ['L', 'N', 'P, 'R']
 
 # index with another vector
 a[ [1, 3, 5] ]  # => ['A', 'C', 'E']

लॉजिकल इंडेक्सिंग

ऐसे एलिमेंट भी चुने जा सकते हैं जो किसी लॉजिकल एक्सप्रेशन को पूरा करते हों (तकनीकी रूप से इसे "प्रेडिकेट" कहते हैं)। इसके लिए आम तौर पर ब्रॉडकास्टिंग करनी पड़ती है।

julia> a[a .< 'D']
3-element Vector{Char}:
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
 'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)

और जटिल एक्सप्रेशन में डॉट बढ़ते चले जाते हैं (पर वे छोटे होते हैं और टाइप करने में आसान)।

julia> a[a .< 'D' .|| a .> 'W']
6-element Vector{Char}:
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
 'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
 'X': ASCII/Unicode U+0058 (category Lu: Letter, uppercase)
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'Z': ASCII/Unicode U+005A (category Lu: Letter, uppercase)

याद दिला दें कि "वेक्टर" की जगह आप वास्तव में कोई भी उपयुक्त क्रमित इटरेबल ले सकते हैं, जैसे कोई रेंज:

julia> n = 3:10
3:10

julia> n[isodd.(n)]
4-element Vector{Int64}:
 3
 5
 7
 9

अंदर से प्रेडिकेट को एक BitVector में बदल दिया जाता है, और फिर उसी को इंडेक्स की तरह इस्तेमाल किया जाता है।

julia> condition = a .< 'D'
26-element BitVector:
 1
 1
 1
 0
 # display truncated

julia> a[condition]
3-element Vector{Char}:
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
 'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
GitHub के ज़रिए संपादित करें यह लिंक नई विंडो या टैब में खुलता है।

वेक्टर ऑपरेशन सीखिए