در مفهوم 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).
بهعلاوه، یک تابع را میتوان دقیقاً همانطور که با یک Vector انجام میشود، روی هر عنصر یک Matrix پخش کرد.
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 لوگوی Exercism را تعریف کنید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() را تعریف کنید که Matrix لوگوی Exercism را به عنوان ورودی میگیرند. یک 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 را به عنوان ورودی میگیرد.
یک ماتریس نقطهای با همان اندازه برگردانید که در آن نقطههای هر ستون برابر با تعداد نقطههای همان ستون است.
با Matrix لوگوی Exercism بهعنوان ورودی، خروجی یک 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