مدتها پیش، در مفهوم بردارها اشاره کردیم که «آرایهها میتوانند اندازهی دلخواه داشته باشند (فقط محدود به محدودیتهای حافظه در سختافزار شما)، و میتوانند هر تعداد بُعد داشته باشند.»
از آن زمان، تا حد زیادی آرایههای بیش از یک بُعد را نادیده گرفتهایم، فقط برای اینکه کار را ساده نگه داریم. این تصمیم منطقیتر به نظر میرسد اگر سعی کنید مستندات مرجع Julia را با تمام پیچیدگیهایشان بخوانید.
با این حال، آرایههای با ابعاد بالاتر در محاسبات علمی بسیار بسیار مهماند، پس باید آنها را بفهمیم.
نامگذاری: به پیروی از قرنها رویهی ریاضی، ما آرایههای تکبعدی را Vectors و آرایههای دوبعدی را Matrices مینامیم.
مثالهای این سند بیشتر ماتریس خواهند بود. کار کردن با ۳ بُعد یا بیشتر از نظر نحوهی نگارش تقریباً یکسان است، اما خواندن خروجی دشوار و گیجکننده است (روی صفحهای دوبعدی).
نوع یک آرایهی N-بعدی که eltype آن T است، type آن Array{T, N} است.
برای راحتی، و هماهنگی با نامگذاری ریاضی، Julia چند نام مستعار نوع تعریف میکند: Vector{T} برای Array{T, 1} و Matrix{T} برای Array{T, 2}.
# 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 این را یک ماتریس ۱×۳ تعریف میکند (در زمینههای دیگر، آن را 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
علاقهمندان به 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 از ستون ۱ پایین میرود، بعد ستون ۲، تا اینکه سومین عنصر در حافظه را پیدا کند.
مراقب باشید: این کار در نوشتن کتابخانههای همهکاره کاربردهایی دارد، اما به احتمال بیشتر گیجکننده است!
موضوع مشابهی هنگام گرفتن اندازهی یک آرایه پیش میآید.
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
یک : بهتنهایی هر آرایهای را بهصورت ستونی به یک Vector تخت میکند.
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() را دیدیم که روی مجموعههای تکبعدی اعمال میشوند، جایی که روی همهی عناصر عمل میکنند و یک نتیجهی اسکالر برمیگردانند.
این در ابعاد بالاتر هم کار میکند.
با این حال، ممکن است بخواهیم تابع را فقط روی یک بُعد اعمال کنیم، مثلاً با جمع کردن در جهت پایین یا عرض تا آرایهای با یک 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 همان بُعدی است که تکنمونه (singleton) میشود، بنابراین در مثالهای بالا 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 ممکن است به اندازهی ترابایت باشند، بنابراین کپی کردن بهطور بالقوه یک کابوس کارایی است!
نمایی مثل این میتواند برای حلقه زدن، broadcasting، یا همهی عملیات دیگری که تا الان در سرفصلها دیدهایم استفاده شود.
همچنین، درکهای فهرستی با ورودی آرایه میتوانند قدرتمند و همهکاره باشند. موارد ساده در مفهوم حلقهها ذکر شدهاند، اما بحث گستردهتری در یک مفهوم بعدی خواهد بود.
ما حالت تکبعدی را در مفهوم عملیات برداری بحث کردیم.
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
عملگر نقطهدار .- با broadcasting کردن 0.5 تکنمونه به ابعاد منطبق، آن را معادل [0.5, 0.5, 0.5] در نظر میگیرد، سپس تفریق را عنصربهعنصر انجام میدهد.
تعمیم این موضوع به ابعاد بالاتر واقعاً فقط ادامهی همین کار است.
برای مثال، هنگام broadcasting ضرب یک بردار سطری ۲×۱ مثل [1.0 1.5 2.0] در ماتریس ۲×۳ مثل [1 2 3; 4 5 6]، این معادل broadcasting کردن [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
توجه: برای broadcasting، ابعاد غیر تکنمونه باید هماندازه باشند (مثلاً 2x3 Matrix .* 2x1 Matrix).
علاوه بر این، یک تابع میتواند دقیقاً مثل کاری که با یک Vector انجام میشود، روی هر عنصر یک Matrix broadcasting شود.
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 به شمار میرود.