बहुत पहले, वेक्टर कॉन्सेप्ट में हमने ध्यान दिया था कि "ऐरे किसी भी आकार के हो सकते हैं (सीमा सिर्फ आपके हार्डवेयर की मेमोरी है), और उनमें कितने भी आयाम हो सकते हैं।"
तब से हमने चीज़ों को सरल रखने के लिए एक से ज़्यादा आयाम वाले ऐरे पर ज़्यादातर ध्यान नहीं दिया है। अगर आप Julia के संदर्भ दस्तावेज़ों को उनकी पूरी जटिलता में पढ़ने की कोशिश करें, तो यह फैसला आपको ज़्यादा समझ आएगा।
लेकिन वैज्ञानिक कंप्यूटिंग में ज़्यादा आयाम वाले ऐरे बहुत, बहुत ज़रूरी हैं, इसलिए हमें उन्हें समझना होगा।
नामकरण: गणित की सदियों पुरानी परंपरा का पालन करते हुए, हम 1-D ऐरे को Vectors और 2-D ऐरे को Matrices कहते हैं।
इस दस्तावेज़ में ज़्यादातर उदाहरण मैट्रिक्स के होंगे। 3 या उससे ज़्यादा आयामों के साथ काम करना सिंटैक्स की दृष्टि से लगभग एक जैसा ही है, लेकिन (2-D स्क्रीन पर) उसका आउटपुट पढ़ना मुश्किल और उलझाने वाला होता है।
eltype T वाले N-आयामी ऐरे का type Array{T, N} होता है।
सुविधा के लिए और गणितीय नामकरण के साथ एकरूपता बनाए रखने के लिए, Julia कुछ टाइप एलियस परिभाषित करता है: Array{T, 1} के लिए Vector{T} और Array{T, 2} के लिए Matrix{T}.
# 3-D array
julia> m3 = ones(2, 3, 4);
julia> typeof(m3)
Array{Float64, 3}
# Vector
julia> v = [1, 2]
2-element Vector{Int64}:
1
2
julia> typeof(v) == Array{Int64, 1}
true
# Matrix
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> typeof(m)
Matrix{Int64} (alias for Array{Int64, 2})
julia> typeof(m) == Array{Int64, 2}
true
हमने चौकोर ब्रैकेट के अंदर कॉमा से अलग किए गए मानों की सूची डालकर बहुत से वेक्टर बनाए हैं। विभाजक के तौर पर अर्धविराम भी इस्तेमाल किया जा सकता है।
julia> v = [1, 2, 3]
3-element Vector{Int64}:
1
2
3
julia> w = [1; 2; 3]
3-element Vector{Int64}:
1
2
3
julia> v == w
true
अगर विभाजक के तौर पर स्पेस (या टैब) इस्तेमाल करें, तो नतीजा अलग आता है।
julia> u = [1 2 3]
1×3 Matrix{Int64}:
1 2 3
Julia अब इसे 1×3 मैट्रिक्स मानता है (दूसरे संदर्भों में हम इसे row vector कहते हैं)।
सामान्य तौर पर स्पेस चीज़ों को आड़ी दिशा में जोड़ता है, और अर्धविराम (या नई लाइनें) उन्हें खड़ी दिशा में जोड़ते हैं।
"चीज़ों" का ज़िक्र जानबूझकर ढीला रखा गया है, क्योंकि Julia आप जो भी देते हैं, उसी के साथ काम करने की कोशिश करता है।
julia> [v 2v]
3×2 Matrix{Int64}:
1 2
2 4
3 6
julia> [v; 2v]
6-element Vector{Int64}:
1
2
3
2
4
6
यही काम करने वाले दो फंक्शन hcat() और vcat() भी हैं, जो यह और साफ़ कर देते हैं कि ये आड़ी और खड़ी दिशा में जोड़ने वाले काम हैं।
इसका ज़्यादा आयामों वाला रूप cat() फंक्शन है।
jjulia> hcat(v, 2v)
3×2 Matrix{Int64}:
1 2
2 4
3 6
julia> vcat(v, 2v)
6-element Vector{Int64}:
1
2
3
2
4
6
मैट्रिक्स को सीधे लिखने के लिए पंक्ति-प्रधान क्रम सुविधाजनक रहता है, क्योंकि यह मनुष्य की सहज समझ से मेल खाता है (आड़ा पाठ लिखने वाली संस्कृतियों के लिए इसे देखना आसान होता है):
julia> m = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
1 2 3
4 5 6
लेकिन ध्यान रखिए कि Julia (Fortran, R और Matlab की तरह, पर C/C++ या NumPy के विपरीत) N-आयामी ऐरे को स्तंभ-प्रधान क्रम में संग्रहीत करता है, और अगर आप एलिमेंटों पर लूप चलाते हैं तो यह परफॉर्मेंस में बहुत बड़ा फर्क डाल सकता है। अपने CPU कैश की मदद कीजिए, ताकि वह आपकी मदद कर सके!
# put these integers in 2 rows and 3 columns
julia> reshape(collect(1:6), 2, 3)
2×3 Matrix{Int64}:
1 3 5
2 4 6
ऊपर का उदाहरण 1 से 6 तक के पूर्णांक लेता है और उन्हें स्तंभ के क्रम में भरकर एक 2×3 मैट्रिक्स बनाता है।
आम तरह के ऐरे (सब मान एक जैसे हों या रैंडम हों) बनाने के लिए कई उपयोगी फंक्शन हैं।
julia> zeros(2, 3) # see also ones()
2×3 Matrix{Float64}:
0.0 0.0 0.0
0.0 0.0 0.0
julia> falses(2, 2) # booleans, see also trues()
2×2 BitMatrix:
0 0
0 0
julia> rand(Float32, 2, 3) # random numbers in the interval [0, 1)
2×3 Matrix{Float32}:
0.768823 0.169633 0.632565
0.388451 0.109176 0.850381
NumPy से परिचित लोग जानते होंगे कि np.linspace एक बहुत इस्तेमाल किया जाने वाला फंक्शन है, जो तय लंबाई, तय सिरों और समान अंतराल वाली वैल्यू वाला ऐरे बनाता है।
इसे आम तौर पर किसी ग्राफ़ के x-अक्ष के रूप में, या रैखिक मॉडलों में स्वतंत्र वेरिएबल के रूप में इस्तेमाल किया जाता है।
Julia में ठीक इस जैसा कोई फंक्शन नहीं है, लेकिन बहुत लचीला range() फंक्शन निचली और ऊपरी सीमा, और साथ में length कीवर्ड आर्गुमेंट देकर यही काम कर सकता है।
जो लोग अपने कंप्यूटर पर काम करते हैं और जिनके पास Plots पैकेज है, वे यह कोड चला सकते हैं:
julia> using Plots
julia> x = range(0, 2π; length=100)
0.0:0.06346651825433926:6.283185307179586
julia> typeof(x)
StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
julia> vals = [x sin.(x) cos.(x)]
100×3 Matrix{Float64}:
0.0 0.0 1.0
0.0634665 0.0634239 0.997987
0.126933 0.126592 0.991955
0.1904 0.189251 0.981929
(...truncated)
julia> plot(vals[:, 1], vals[:, 2:3])
StepRangeLen को वेक्टर की तरह इस्तेमाल किया जा सकता है, और इसे मैट्रिक्स के किसी स्तंभ में भी बदला जा सकता है।
लॉग अक्ष पर समान अंतराल वाली वैल्यू बनाने के लिए इसी तरह का logrange() फंक्शन भी देखिए।
2-D ऐरे के लिए हम आम तौर पर [row, col] क्रम में दो इंडेक्स इस्तेमाल करते हैं।
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> m[1, 2] # row 1, col 2
2
# Stay within bounds! There is no row 3.
julia> m[3, 1]
ERROR: BoundsError: attempt to access 2×3 Matrix{Int64} at index [3, 1]
julia> m[3]
2
अंतिम उदाहरण शायद चौंकाने वाला है: एक ही इंडेक्स एरर नहीं है, बल्कि एक अकेला एलिमेंट लौटाता है।
इसकी वजह वही स्तंभ-प्रधान क्रम वाली बात है: Julia पहले स्तंभ 1 में नीचे जाता है, फिर स्तंभ 2 में, और इस तरह मेमोरी में तीसरा एलिमेंट ढूँढ लेता है।
सावधान रहिए: सामान्य काम के लिए बनने वाली लाइब्रेरी लिखते समय यह कभी-कभी काम आता है, लेकिन इससे उलझन होने की संभावना ज़्यादा है!
ऐरे का आकार पूछने पर भी कुछ ऐसी ही दिक्कत आती है।
length() कुल एलिमेंटों की संख्या बताता है, और size() ndims() एलिमेंटों वाला ट्यूपल देता है, जिसमें हर आयाम की लंबाई होती है।
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> length(m)
6
julia> size(m) # 2 rows, 3 cols
(2, 3)
julia> ndims(m) # how many dimensions? Like `m |> size |> length`
2
हम मैट्रिक्स के किसी हिस्से की आसानी से नकल बना सकते हैं।
नीचे के उदाहरण में reshape() फंक्शन ऐरे को स्तंभ के क्रम में भरकर बताए गए आयामों में ढाल देता है, और उसके बाद हम उसमें से एक हिस्सा निकालते हैं।
julia> m12 = reshape(collect(1:12), 4, 3)
4×3 Matrix{Int64}:
1 5 9
2 6 10
3 7 11
4 8 12
julia> m12[2:4, 1:2]
3×2 Matrix{Int64}:
2 6
3 7
4 8
# some rows, all columns
julia> m12[2:4, :]
3×3 Matrix{Int64}:
2 6 10
3 7 11
4 8 12
अकेले : का मतलब है "इस आयाम की हर चीज़ की नकल करो"।
लगातार न आने वाली पंक्तियों/स्तंभों के लिए वेक्टर इस्तेमाल कीजिए:
julia> m12[[1, 3], :] # rows 1 and 3
2×3 Matrix{Int64}:
1 5 9
3 7 11
अकेला : किसी भी ऐरे को स्तंभ के क्रम में समतल करके एक वेक्टर बना देता है।
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> m[:]
6-element Vector{Int64}:
1
4
2
5
3
6
हम पहले 1-D संग्रहों पर sum() और maximum() जैसे एग्रीगेशन फंक्शन लगते देख चुके हैं, जहाँ वे सभी एलिमेंटों पर काम करके एक स्केलर नतीजा लौटाते हैं।
यह ज़्यादा आयामों में भी काम करता है।
लेकिन कभी-कभी हमें फंक्शन सिर्फ एक आयाम पर लगाना होता है, जैसे नीचे की ओर या आड़ी दिशा में जोड़कर ऐसा ऐरे लौटाना जिसका singleton dimension आकार 1 का हो।
इसके लिए एक वैकल्पिक dims कीवर्ड आर्गुमेंट है।
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> sum(m) # sum everything
21
julia> sum(m; dims=1) # sum down
1×3 Matrix{Int64}:
5 7 9
julia> sum(m; dims=2) # sum across
2×1 Matrix{Int64}:
6
15
# other aggregation functions are similar
julia> maximum(m; dims=2)
2×1 Matrix{Int64}:
3
6
dims की वैल्यू वह आयाम होती है जो सिंगलटन बन जाता है, इसलिए ऊपर के उदाहरणों में dims=1 => 1×3 और dims=2 => 2×1 है।
इसी तरह, अगर dims को किसी ऐरे या रेंज पर सेट किया जाए, तो ज़्यादा आयाम वाले ऐरे एक साथ कई आयाम घटा सकते हैं (फिर से, आउटपुट समझने के लिए थोड़ा सोचना पड़ सकता है!)।
sum() जैसे बिल्ट-इन फंक्शनों में dims कीवर्ड आर्गुमेंट आम है, लेकिन अपने कोड में इस जैसा कुछ लिखें तो कैसे?
इसका एक अच्छा जवाब है reduce() जैसे हायर-ऑर्डर फंक्शन इस्तेमाल करना, और इस पर आगे किसी कॉन्सेप्ट में थोड़ा विस्तार से बात होगी।
दूसरा तरीका यह है कि Julia में कई ऐसे फंक्शन हैं जिनसे हम N-आयामी ऐरे के साथ ऐसे काम कर सकते हैं जैसे वे वेक्टरों के अंदर रखे वेक्टर हों।
मैट्रिक्स के लिए eachrow() और eachcol() सुविधाजनक हैं, लेकिन इससे ज़्यादा सामान्य फंक्शन eachslice() है, जो किसी भी आयाम के साथ काम कर सकता है।
# m is as in the previous examples
julia> eachrow(m)
2-element RowSlices{Matrix{Int64}, Tuple{Base.OneTo{Int64}}, SubArray{Int64, 1, Matrix{Int64}, Tuple{Int64, Base.Slice{Base.OneTo{Int64}}}, true}}:
[1, 2, 3]
[4, 5, 6]
julia> eachcol(m)
3-element ColumnSlices{Matrix{Int64}, Tuple{Base.OneTo{Int64}}, SubArray{Int64, 1, Matrix{Int64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, true}}:
[1, 4]
[2, 5]
[3, 6]
यह टाइप थोड़ा डरावना लगता है, लेकिन सिर्फ इसलिए कि यह मूल ऐरे का एक व्यू है, जिससे हम बिना नकल बनाए उस पर काम कर सकते हैं। Julia के ऐरे टेराबाइट तक के हो सकते हैं, इसलिए नकल बनाना परफॉर्मेंस के लिए बड़ा सिरदर्द साबित हो सकता है!
ऐसे व्यू का इस्तेमाल लूप चलाने, ब्रॉडकास्टिंग, या अब तक सिलेबस में देखे गए बाकी सारे ऑपरेशनों के लिए किया जा सकता है।
साथ ही, ऐरे इनपुट के साथ कॉम्प्रिहेंशन बहुत शक्तिशाली हो सकते हैं और कई तरह से काम आ सकते हैं। सरल मामलों का ज़िक्र लूप कॉन्सेप्ट में हुआ था, लेकिन आगे किसी कॉन्सेप्ट में इस पर विस्तार से चर्चा होगी।
1-D वाले मामले पर हमने वेक्टर ऑपरेशन्स कॉन्सेप्ट में बात की थी।
julia> v = [1.2, 1.5, 1.7]
3-element Vector{Float64}:
1.2
1.5
1.7
julia> v .- 0.5
3-element Vector{Float64}:
0.7
1.0
1.2
डॉट वाला ऑपरेटर .- अकेली वैल्यू 0.5 को आयामों से मेल खाने तक broadcasting करके [0.5, 0.5, 0.5] के बराबर मान लेता है, और फिर हर एलिमेंट पर घटाव करता है।
इसे ज़्यादा आयामों तक बढ़ाना वास्तव में बस इसी का और रूप है।
उदाहरण के लिए, किसी 2x1 पंक्ति वेक्टर [1.0 1.5 2.0] को 2x3 मैट्रिक्स [1 2 3; 4 5 6] से गुणा करके ब्रॉडकास्ट करना, [1.0 1.5 2.0] को [1.0 1.5 2.0; 1.0 1.5 2.0] तक ब्रॉडकास्ट करके फिर हर एलिमेंट पर गुणा करने जैसा है।
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> m .* [1.0 1.5 2.0] # broadcast a 1x3 row vector down
2×3 Matrix{Float64}:
1.0 3.0 6.0
4.0 7.5 12.0
julia> m .* [1.0 1.5 2.0; 1.0 1.5 2.0] # 2x3 elementwise multiplication
2×3 Matrix{Float64}:
1.0 3.0 6.0
4.0 7.5 12.0
julia> m ./ [1, 2] # broadcast a 2x1 column vector across
2×3 Matrix{Float64}:
1.0 2.0 3.0
2.0 2.5 3.0
julia> m ./ [1 1 1; 2 2 2] # 2x3 elementwise division
2×3 Matrix{Float64}:
1.0 2.0 3.0
2.0 2.5 3.0
ध्यान दें: ब्रॉडकास्टिंग के लिए सिंगलटन न होने वाले आयामों का आकार मेल खाना चाहिए (जैसे 2x3 Matrix .* 2x1 Matrix)।
इसके अलावा, किसी फंक्शन को Matrix के हर एलिमेंट पर ठीक उसी तरह ब्रॉडकास्ट किया जा सकता है जैसे Vector के साथ किया जाता है।
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> (x -> x^2).(m) # broadcast a function to all elements
2×3 Matrix{Int64}:
1 4 9
16 25 36
शुरू में लग सकता है कि जैसे-जैसे आयामों की संख्या बढ़ती है, प्रोग्रामर के उलझने की गुंजाइश भी तेज़ी से बढ़ती जाती है, लेकिन अभ्यास से बहुत मदद मिलती है। साथ ही, सिंटैक्स अलग होने के बावजूद, NumPy ऐरे की जानकारी Julia पर काफी अच्छी तरह लागू होती है।
वेक्टर और मैट्रिक्स पर होने वाले बहुत से आम ऑपरेशन रैखिक बीजगणित के दायरे में आते हैं।
Exercism की वेबसाइट पर (फिलहाल) दो और कॉन्सेप्ट हैं, जो तब तक सिर्फ दस्तावेज़ हैं जब तक हम उनके साथ जोड़ने के लिए अभ्यास न बना लें।
भविष्य में मैट्रिक्स फैक्टराइज़ेशन पर तीसरा कॉन्सेप्ट भी हो सकता है।
लेकिन ध्यान रखिए कि रैखिक बीजगणित की मूल बातें काफी गणितीय है, और यह हर किसी के लिए उपयुक्त नहीं हो सकती।
बाद के कॉन्सेप्ट (समीकरण हल करना और फैक्टराइज़ेशन) ज़्यादा उन्नत हैं, और वास्तव में उन्हीं लोगों के लिए हैं जिन्हें कॉलेज स्तर का गणित सहज लगता है।
क्षमा कीजिए, लेकिन विषय की प्रकृति ही ऐसी है, और Julia के लिए यह एक ज़रूरी उपयोग है।