في وقت سابق من مفهوم المتجهات، أشرنا إلى أن "المصفوفات يمكن أن تكون بأي حجم (رهنًا بقيود الذاكرة في عتادك فقط)، ويمكن أن تمتلك أي عدد من الأبعاد."
ومنذ ذلك الحين، تجاهلنا إلى حد كبير المصفوفات ذات أكثر من بعد واحد، فقط لإبقاء الأمور بسيطة. سيبدو هذا القرار أكثر منطقية إذا جرّبت قراءة وثائق Julia المرجعية بكامل تعقيدها.
ومع ذلك، فإن المصفوفات متعددة الأبعاد مهمة جدًا جدًا في الحوسبة العلمية، لذا نحتاج إلى فهمها.
التسمية: اقتداءً بقرون من السوابق الرياضية، نسمي المصفوفات أحادية البعد Vectors والمصفوفات ثنائية البعد Matrices.
ستكون الأمثلة في هذا المستند في معظمها مصفوفات. العمل بثلاثة أبعاد أو أكثر متطابق تقريبًا من ناحية الصياغة، لكن المخرجات صعبة ومربكة للقراءة (على شاشة ثنائية البعد).
نوع (type) مصفوفة ذات N من الأبعاد ونوع عناصرها eltype T هو 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 الآن هذا كمصفوفة بأبعاد 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 من الأبعاد بترتيب العمود أولًا، وقد يُحدث هذا فرقًا هائلًا في الأداء إذا مررت على العناصر بحلقة. ساعد الذاكرة المؤقتة لوحدة المعالجة المركزية على مساعدتك!
# 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 دالة مستخدمة على نطاق واسع لتوليد مصفوفة بطول محدد، وبنقطتي بداية ونهاية محددتين وقيم متباعدة بالتساوي.
ويُستخدم هذا عادةً كمحور سيني في رسم بياني، أو كمتغير مستقل في النماذج الخطية.
لا تملك 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() مطبقة على مجموعات أحادية البعد، حيث تعمل على كل العناصر وتُرجع قيمة عددية واحدة.
يعمل هذا أيضًا في الأبعاد الأعلى.
لكن قد نرغب في تطبيق الدالة على بعد واحد فقط، مثلًا بالجمع نزولًا أو عرضًا كي تُرجع مصفوفة ذات 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 على مصفوفة أو نطاق (ومرة أخرى، قد يتطلب فهم المخرجات بعض التفكير!).
وسيط الكلمة المفتاحية 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 كما لو كانت [0.5, 0.5, 0.5] عبر broadcasting به لتطابق الأبعاد، ثم ينفذ الطرح عنصرًا عنصرًا.
وتوسيع هذا إلى أبعاد أعلى ليس في الحقيقة إلا المزيد من الشيء نفسه.
مثلًا، عند بث ضرب متجه صفّي بأبعاد 2x1 هو [1.0 1.5 2.0] في المصفوفة [1 2 3; 4 5 6] ذات الأبعاد 2x3، فهذا يكافئ بث [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.