في وقت سابق، في مفهوم 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 هذا الآن على أنه مصفوفة 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
يأخذ المثال أعلاه الأعداد الصحيحة من 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
بالنسبة لمصفوفة ثنائية البعد، نستخدم عمومًا فهرسين بترتيب [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
رأينا سابقًا دوال التجميع مثل 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 تيرابايتات، لذا يمكن أن تكون النسخ كابوسًا للأداء!
يمكن استخدام عرض كهذا للتكرار، أو البث، أو كل العمليات الأخرى التي رأيناها في المنهج حتى الآن.
أيضًا، يمكن أن تكون صيغ الاستيعاب قوية ومتعددة الاستخدامات مع مدخلات المصفوفات.
ذُكرت حالات بسيطة في مفهوم 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 كما لو كانت مكافئة لـ [0.5, 0.5, 0.5] عبر broadcasting لتطابق الأبعاد، ثم ينفّذ الطرح عنصرًا عنصرًا.
توسيع هذا إلى أبعاد أعلى هو في الحقيقة مجرد المزيد من الشيء نفسه.
مثلًا، عند بث ضرب متجه صفّي 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، رغم اختلاف الصياغة.
مصفوفة النقاط صورة ثنائية الأبعاد تتكوّن من نقاط وفراغات. تشكّل النقاط الصورة المقصودة على خلفية الفراغات.
يمكن تخزين صورة مصفوفة النقاط ومعالجتها في الذاكرة على هيئة 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 كمدخل.
تُرجع مصفوفة نقاط بالحجم نفسه، حيث تمثّل النقاط في كل عمود عدد النقاط في ذلك العمود.
عند إدخال 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
سجّل في Exercism لتتعلّم وتتقن Julia عبر 35 مفهومًا128 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.