ভেক্টর কনসেপ্টে অনেক আগেই আমরা উল্লেখ করেছিলাম যে "অ্যারে ইচ্ছেমতো আকারের হতে পারে (আপনার হার্ডওয়্যারের মেমোরি সীমাবদ্ধতা ছাড়া আর কোনো বাধা নেই), এবং এতে ইচ্ছেমতো অনেক মাত্রা থাকতে পারে।"
এরপর থেকে আমরা সাধারণত একের বেশি মাত্রার অ্যারেগুলো এড়িয়ে গিয়েছি, শুধু বিষয়টা সহজ রাখতে। Julia-র রেফারেন্স ডকুমেন্টগুলো পুরো জটিলতা নিয়ে পড়তে গেলে এই সিদ্ধান্তটি আরও যুক্তিসঙ্গত মনে হবে।
তবে উচ্চমাত্রার অ্যারেগুলো বৈজ্ঞানিক কম্পিউটিংয়ে খুবই, খুবই গুরুত্বপূর্ণ, তাই এগুলো আমাদের বুঝতে হবে।
নামকরণ: গণিতের বহু শতাব্দীর রীতি অনুসরণ করে আমরা এক-মাত্রিক অ্যারেকে Vectors আর দুই-মাত্রিক অ্যারেকে Matrices বলি।
এই ডকুমেন্টের উদাহরণগুলো বেশিরভাগই ম্যাট্রিক্স হবে। তিন বা তার বেশি মাত্রা নিয়ে কাজ করা সিনট্যাক্সের দিক থেকে প্রায় একই রকম, কিন্তু (দুই-মাত্রিক পর্দায়) আউটপুট পড়া কঠিন ও বিভ্রান্তিকর।
যে অ্যারেয়ের 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
উপরের উদাহরণটি ১ থেকে ৬ পর্যন্ত ইন্টিজার নিয়ে সেগুলো দিয়ে কলাম অনুসারে একটি 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() ফাংশনটিও দেখুন।
দুই-মাত্রিক অ্যারেতে আমরা সাধারণত [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
আগে আমরা এক-মাত্রিক কালেকশনের উপর sum() আর maximum()-এর মতো অ্যাগ্রিগেশন ফাংশন প্রয়োগ করতে দেখেছি, যেখানে তারা সব এলিমেন্টের উপর কাজ করে একটি স্কেলার ফলাফল রিটার্ন করে।
এটি উচ্চতর মাত্রাতেও কাজ করে। তবে আমরা হয়তো ফাংশনটি কেবল একটি মাত্রার উপর প্রয়োগ করতে চাই, যেমন নিচে বা আড়াআড়ি যোগ করে আকার 1-এর একটি 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-র অ্যারে সম্ভাব্য টেরাবাইট আকারের হতে পারে, তাই কপি করা পারফরম্যান্সের জন্য এক বিরাট দুঃস্বপ্ন!
এমন একটি ভিউ লুপিং, ব্রডকাস্টিং, বা এখন পর্যন্ত সিলেবাসে দেখা অন্য সব অপারেশনে ব্যবহার করা যায়।
এছাড়া, অ্যারে ইনপুট নিয়ে কম্প্রিহেনশন খুবই শক্তিশালী ও বহুমুখী হতে পারে। সাধারণ ক্ষেত্রগুলোর কথা লুপ কনসেপ্টে বলা হয়েছে, তবে পরের একটি কনসেপ্টে আরও বিস্তারিত আলোচনা থাকবে।
এক-মাত্রিক ক্ষেত্রটি নিয়ে আমরা ভেক্টর অপারেশন কনসেপ্টে আলোচনা করেছি।
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-র জন্য এটি একটি গুরুত্বপূর্ণ ব্যবহারক্ষেত্র।