আগের Vectors কনসেপ্টে আমরা উল্লেখ করেছিলাম যে, "অ্যারে ইচ্ছেমতো আকারের হতে পারে (এক্ষেত্রে আপনার হার্ডওয়্যারের মেমোরি সীমাবদ্ধতাই একমাত্র বাধা), আর এতে ইচ্ছেমতো মাত্রা থাকতে পারে।"
তারপর থেকে, বিষয়গুলো সহজ রাখার জন্যই আমরা একাধিক মাত্রার অ্যারে প্রায় সম্পূর্ণভাবে উপেক্ষা করে এসেছি। Julia-র রেফারেন্স ডকুমেন্টগুলো তাদের পুরো জটিলতাসহ পড়ার চেষ্টা করলে এই সিদ্ধান্তটি আরও যুক্তিসঙ্গত মনে হবে।
তবে, উচ্চমাত্রার অ্যারে বৈজ্ঞানিক কম্পিউটিংয়ে খুব, খুব গুরুত্বপূর্ণ, তাই সেগুলো আমাদের বুঝতেই হবে।
নামকরণ: গণিতের শতাব্দীপ্রাচীন রীতিকে অনুসরণ করে আমরা একমাত্রিক অ্যারেকে বলি Vectors আর দ্বিমাত্রিক অ্যারেকে বলি Matrices.
এই ডকুমেন্টের উদাহরণগুলো বেশিরভাগই ম্যাট্রিক্স হবে। তিন বা তার বেশি মাত্রা নিয়ে কাজ করা সিনট্যাক্সের দিক থেকে প্রায় হুবহু একই, কিন্তু (দ্বিমাত্রিক স্ক্রিনে) আউটপুট পড়া কঠিন ও বিভ্রান্তিকর।
কমা দিয়ে আলাদা করা একটি তালিকা তৃতীয় বন্ধনীর ভেতরে রেখে আমরা এরই মধ্যে অনেক ভেক্টর তৈরি করেছি। সেমিকোলনও সেপারেটর হিসেবে ব্যবহার করা যায়।
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 এটিকে একটি ১×৩ ম্যাট্রিক্স হিসেবে সংজ্ঞায়িত করে (অন্য প্রসঙ্গে আমরা এটিকে 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
উপরের উদাহরণটি ১ থেকে ৬ পর্যন্ত ইন্টিজার নিয়ে কলাম অনুসারে একটি ২×৩ ম্যাট্রিক্স ভরে দেয়।
সাধারণ ধরনের অ্যারে (ইউনিফর্ম বা র্যান্ডম) তৈরির জন্য নানা রকম ইউটিলিটি ফাংশন আছে।
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
দ্বিমাত্রিক অ্যারের জন্য আমরা সাধারণত [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 কলাম ১ বরাবর নিচে নামে, তারপর কলাম ২, যতক্ষণ না মেমোরিতে তৃতীয় এলিমেন্টটি খুঁজে পায়।
সাবধান থাকুন: জেনারেল-পারপাস লাইব্রেরি লেখার সময় এর কিছু ব্যবহার আছে, তবে বেশিরভাগ ক্ষেত্রেই এটি বিভ্রান্তিকর হয়ে দাঁড়ায়!
অ্যারের আকার জানতে চাইলে অনুরূপ সমস্যা দেখা দেয়।
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
আগে আমরা একমাত্রিক কালেকশনে প্রয়োগ করা sum() আর maximum()-এর মতো অ্যাগ্রিগেশন ফাংশন দেখেছি, যেখানে তারা সব এলিমেন্টের উপর কাজ করে একটি স্কেলার ফলাফল রিটার্ন করে।
এটি উচ্চতর মাত্রাতেও কাজ করে।
তবে হয়তো আমরা ফাংশনটি কেবল একটি মাত্রার উপর প্রয়োগ করতে চাইব, যেমন নিচের দিকে বা পাশের দিকে যোগ করে আকার ১-এর একটি singleton dimension বিশিষ্ট অ্যারে রিটার্ন করা।
এর জন্য একটি অপশনাল 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-কে একটি অ্যারে বা রেঞ্জ দেওয়া হলে উচ্চমাত্রার অ্যারে একাধিক মাত্রা কমাতে পারে (আবারও, আউটপুট বুঝতে একটু ভাবতে হতে পারে!)।
dims কিওয়ার্ড আর্গুমেন্ট sum()-এর মতো বিল্ট-ইন ফাংশনে কমন, কিন্তু নিজেদের কোডে এর সমতুল্য কিছু লিখব কীভাবে?
একটি ভালো উত্তর হলো 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-র অ্যারে সম্ভাব্য টেরাবাইট আকারের হতে পারে, তাই কপি করা মানে পারফরম্যান্সের সম্ভাব্য দুঃস্বপ্ন!
এমন একটি ভিউ লুপিং, ব্রডকাস্টিং, বা এখন পর্যন্ত সিলেবাসে আমরা যেসব অপারেশন দেখেছি, সেসব সবকিছুতেই কাজে লাগানো যায়।
এছাড়া, অ্যারে ইনপুট দিলে কম্প্রিহেনশন শক্তিশালী ও বহুমুখী হতে পারে।
সাধারণ কিছু ক্ষেত্রের কথা Loops কনসেপ্টে বলা হয়েছে, তবে পরের একটি কনসেপ্টে আরও বিস্তারিত আলোচনা থাকবে।
একমাত্রিক ক্ষেত্রটি নিয়ে আমরা Vector Operations কনসেপ্টে আলোচনা করেছি।
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]-এর সমতুল্য ধরে, তারপর এলিমেন্টভিত্তিক বিয়োগ করে।
এটাকে উচ্চতর মাত্রায় বাড়িয়ে নেওয়া আসলে একই জিনিসেরই আরেকটু বেশি।
উদাহরণস্বরূপ, একটি ২×১ সারি ভেক্টর [1.0 1.5 2.0]-কে ২×৩ ম্যাট্রিক্স [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-তে বেশ ভালোভাবেই কাজে লাগে।
একটি ডট ম্যাট্রিক্স হলো ডট ও হোয়াইটস্পেস দিয়ে গড়া একটি দ্বিমাত্রিক ছবি। হোয়াইটস্পেসের পটভূমিতে ডটগুলো কাঙ্ক্ষিত ছবিটি তৈরি করে।
একটি ডট ম্যাট্রিক্স ছবি মেমোরিতে একটি দ্বিমাত্রিক Matrix হিসেবে সংরক্ষণ ও পরিবর্তন করা যায়। নিচের অংশে ম্যাট্রিক্সগুলোর "হোয়াইটস্পেস"-কে 0 দিয়ে এবং "ডট"-গুলোকে শূন্য নয় এমন সংখ্যা দিয়ে বোঝানো হবে।
Matrixটি ডিফাইন করুনMatrixটি দেখতে নিচের মতো, যেখানে 0 হলো হোয়াইটস্পেস আর 1 হলো ডট:
[
0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0;
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
0 1 0 0 0 1 0 0 0 0 0 0 1 0 0 0 1 0;
0 1 0 0 1 0 1 0 0 0 0 1 0 1 0 0 1 0;
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1;
0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0;
0 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1 0;
0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0;
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0;
]
আপনি যদি এটিকে "রেন্ডার" করেন, তাহলে এটি দেখতে এমন হয়:
XX XX
X X
X X X X
X X X X X X
X X
X X
X X X X
X X X X
X XX X
X X
XX XX
frown!() ও frown() ফাংশন দুটি ডিফাইন করুন, যেগুলো ইনপুট হিসেবে Exercism লোগোর Matrix নেয়। হাসিমুখটি বদলে ভুরু কোঁচকানো মুখ করা একটি Matrix রিটার্ন করুন।
ফলাফল হিসেবে পাওয়া Matrixটি রেন্ডার হলে দেখতে এমন হবে:
XX XX
X X
X X X X
X X X X X X
X X
X X
X XX X
X X X X
X X X X
X X
XX XX
stickerwall() ফাংশনটি ডিফাইন করুন, যেটি ইনপুট হিসেবে Exercism ম্যাট্রিক্সটি নেয়। এমন একটি ডট ম্যাট্রিক্সের Matrix রিটার্ন করুন, যা রেন্ডার হলে দেখতে এমন হয়:
XX XX XX XX
X X X X
X X X X X X X X
X X X X X X X X X X X X
X X X X
X XX X
X X X X X XX X
X X X X X X X X
X XX X X X X X
X X X X
XX XX XX XX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XX XX XX XX
X X X X
X X X X X X X X
X X X X X X X X X X X X
X X X X
X XX X
X XX X X X X X
X X X X X X X X
X X X X X XX X
X X X X
XX XX XX XX
ডট হিসেবে শুধু 1 ব্যবহারেই আমাদের সীমাবদ্ধতা নেই, তাই চাইলে অন্য কাজের তথ্যও এতে এনকোড করা যায়। colpixelcount() ফাংশনটি ডিফাইন করুন, যেটি ইনপুট হিসেবে 1 ডটযুক্ত যেকোনো ডট ম্যাট্রিক্স নেয়। একই আকারের একটি ডট ম্যাট্রিক্স রিটার্ন করুন, যেখানে প্রতিটি কলামের ডট হবে সেই কলামে থাকা ডটের সংখ্যা।
ইনপুট হিসেবে Exercism লোগোর Matrix দিলে আউটপুট হিসেবে যে Matrix পাওয়া যায় তা নিচের মতো:
[
0 0 2 2 0 0 0 0 0 0 0 0 0 0 2 2 0 0;
0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
0 8 0 0 0 1 0 0 0 0 0 0 1 0 0 0 8 0;
0 8 0 0 1 0 2 0 0 0 0 2 0 1 0 0 8 0;
0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1;
0 8 0 0 0 0 2 0 0 0 0 2 0 0 0 0 8 0;
0 8 0 0 0 0 0 1 0 0 1 0 0 0 0 0 8 0;
0 8 0 0 0 0 0 0 1 1 0 0 0 0 0 0 8 0;
0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
0 0 2 2 0 0 0 0 0 0 0 0 0 0 2 2 0 0;
]
আমাদের সৃষ্টিগুলো রেন্ডার করা নিয়ে শুধু কথা বলেই থেমে না গিয়ে, সহজে দেখার সুবিধার জন্য সেটি সত্যিই করতে পারতে চাই। render() ফাংশনটি ডিফাইন করুন, যেটি ইনপুট হিসেবে একটি ডট ম্যাট্রিক্স নেয়। এমন একটি স্ট্রিং রিটার্ন করুন যেখানে ডটগুলো 'X' হিসেবে, 0গুলো ' ' হিসেবে রেন্ডার হবে এবং প্রতি সারিকে নিউলাইন দিয়ে জোড়া লাগানো হবে।
julia> render(E)
" XX XX \n X X \n X X X X \n X X X X X X \n X X \nX X\n X X X X \n X X X X \n X XX X \n X X \n XX XX "
প্রিন্ট করলে এটি প্রত্যাশামতো Exercism ম্যাট্রিক্সটি রেন্ডার করবে।
julia> print(render(E))
XX XX
X X
X X X X
X X X X X X
X X
X X
X X X X
X X X X
X XX X
X X
XX XX
এটি 1 ছাড়া অন্য "ডট"-এর সাথেও কাজ করা উচিত:
julia> print(render(colpixelcount(E)))
XX XX
X X
X X X X
X X X X X X
X X
X X
X X X X
X X X X
X XX X
X X
XX XX
Exercism-এ সাইন আপ করুন, Julia ট্র্যাকের 35টি কনসেপ্ট128টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।