المسارات
/
Julia
Julia
/
التمارين
/
ثورة الروبوتات
ثورة الروبوتات

ثورة الروبوتات

تمرين تعلّمي

مقدمة

ما هو "الجبر الخطي"؟

هناك تعريفات تقنية كثيرة، على [ويكيبيديا][linalg-wiki]، وفي الكتب المدرسية، وفي مواقع ممتازة مثل [3Blue1Brown][3blue1brown]، لكن لأغراضنا يمكننا أن نكون أقل رسمية.

Note

الجبر الخطي يتناول الكثير من الأمور المثيرة للاهتمام (والمفيدة جدًا) التي يمكنك القيام بها باستخدام المتجهات والمصفوفات.

مشرفو Julia (وPython) يحبون هذا النوع من الأشياء. أما إدارة Exercism، فحماسها أقل بكثير.

سيقتصر هذا المفهوم على أبسط جوانب موضوع ضخم، لكن خذ حذرك: فهو بلا شك مفهوم رياضي إلى حد كبير.

ما هو المتجه؟

يعتمد الأمر على من تسأل، وعلى كيفية تخيلك لشيء مجرد إلى حد ما.

  • في Julia، Vector{T} مجرد اسم بديل لـ Array{T, 1}، ويمكن أن يكون eltype T أي شيء.
  • في (الكثير من) الفيزياء، المتجه سهم له length وdirection في فضاء ذي N أبعاد، لكن دون موضع ثابت.
  • في الجبر الخطي، يُثبَّت سهم الفيزيائيين في الفضاء، وتكون بدايته عند نقطة الأصل. هذا يجعل جسم السهم زائدًا عن الحاجة، لذا يمكننا تمثيل المتجه على أنه point في فضاء ذي N أبعاد، حيث تمثل العناصر N المسافة من نقطة الأصل على طول كل من المحاور N.

في سياقنا الحالي، تجاهل المتجهات المكوّنة من سلاسل نصية أو محارف. سنعمل في هذا المفهوم مع أنواع عددية: Int أو Float أو Complex.

ما هي المصفوفة؟

مرة أخرى، ستجد مجموعة متنوعة من الإجابات.

  • مصفوفة مستطيلة ثنائية الأبعاد من الأعداد (بدون حواف غير منتظمة). والمصفوفة المربعة حالة خاصة شائعة.
  • linear combination من متجهات عمودية، مرصوصة جنبًا إلى جنب.
  • transformation يمكن تطبيقها على متجه، تمامًا كما يُطبَّق function على أنواع أخرى من المدخلات.

بعض أنواع المصفوفات المربعة شائعة بما يكفي ليكون لها أسماء خاصة.

مصفوفة قطرية: جميع العناصر غير الصفرية تقع على main diagonal (من أعلى اليسار إلى أسفل اليمين).

julia> [1 0 0; 0 2 0; 0 0 3]
3×3 Matrix{Int64}:
 1  0  0
 0  2  0
 0  0  3

# a convenient shortcut
julia> diagm(1:3)
3×3 Matrix{Int64}:
 1  0  0
 0  2  0
 0  0  3

مصفوفة الوحدة: مصفوفة قطرية تحتوي على الآحاد فقط على القطر (لأسباب ستتضح أكثر لاحقًا). وغالبًا ما يُختصر اسمها إلى I.

julia> Matrix{Float64}(I, 3, 3)
3×3 Matrix{Float64}:
 1.0  0.0  0.0
 0.0  1.0  0.0
 0.0  0.0  1.0

مثلثية عليا: قيم غير صفرية على القطر وفوقه، وأصفار تحته. أما مثلثية سفلى فيمكنك تخمينها.

نقل المصفوفة

إذا أردت فعلًا تبديل الصفوف بالأعمدة، فإن الدالة permutedims() تفعل ذلك وتعيد لك مصفوفة جديدة.

يتضمن هذا نسخًا، وهو أمر بطيء ويستهلك ذاكرة كبيرة عند التعامل مع مصفوفات ضخمة.

لأغراض الجبر الخطي، تكون الدالة transpose() أكثر فائدة، إذ تنشئ بسرعة غلافًا مؤجلًا حول المصفوفة الأصلية.

والدالة adjoint() أكثر فائدة، فهي تقلب أيضًا إشارة الجزء التخيلي في أي أعداد مركبة. هذه العملية شائعة بما يكفي حتى يمكننا ببساطة إضافة فاصلة عليا ' إلى اسم المتغير لإنشاء المرافق.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

# new, full copy
julia> permutedims(m)
3×2 Matrix{Int64}:
 1  4
 2  5
 3  6

# lazy version
julia> transpose(m)
3×2 transpose(::Matrix{Int64}) with eltype Int64:
 1  4
 2  5
 3  6

julia> mc = [1+2im 2+3im; 3+2im 1+2im]
2×2 Matrix{Complex{Int64}}:
 1+2im  2+3im
 3+2im  1+2im

# lazy conjugate transpose
julia> adjoint(mc)
2×2 adjoint(::Matrix{Complex{Int64}}) with eltype Complex{Int64}:
 1-2im  3-2im
 2-3im  1-2im

# syntactic sugar for adjoint
julia> mc'
2×2 adjoint(::Matrix{Complex{Int64}}) with eltype Complex{Int64}:
 1-2im  3-2im
 2-3im  1-2im

الضرب

تتضمن بقية هذه الوثيقة وظائف من الوحدة LinearAlgebra. سيجلب سطر using LinearAlgebra هذه الوظائف إلى نطاق الأسماء، لكننا لن نكرر ذلك في الأمثلة (ففي ذلك فوضى بصرية أكثر من اللازم).

المتجهات، الضرب العنصري

ناقشنا هذا في مفهوم [عمليات المتجهات][vector-ops]. العامل هو .*، ويعمل على متجهي الإدخال زوجًا زوجًا، ويعطي ناتجًا بحجم المدخلات ونوعها.

julia> [1, 2] .* [3, 4]
2-element Vector{Int64}:
 3
 8

المتجهات، الضرب النقطي

تُكتب هذه العملية الشائعة جدًا في الكتب المدرسية على هيئة u ⋅ v، وتُسمى الضرب "النقطي".

الضرب النقطي يعادل مجموع الضرب العنصري.

يمكن ضرب متجهين بالعامل المعتاد *، لكن فقط إذا كان المتجه الأيسر adjoint: ويُكتب هذا بشكل مريح u' * v. سيصبح هذا أوضح في القسم اللاحق عن ضرب المصفوفات.

النقطة المرتفعة (الوسطى) متاحة في Julia (تُدخل بكتابة \cdot ثم Tab) كاختصار نحوي للدالة dot(). ولا حاجة لتحديد المرافق، لأن هذه التفصيلة تُعالَج تلقائيًا.

julia> using LinearAlgebra

# with element-wise syntax
julia> sum([1, 2] .* [3, 4])
11

# with u' * v syntax
julia> [1, 2]' * [3, 4]
11

# with dot()
julia> dot([1, 2], [3, 4])
11

# with \cdot syntax
julia> [1, 2] ⋅ [3, 4]
11

بالنسبة للمتجهات ذات القيم المركبة، يجب أن يكون المتجه الأيسر هو المرافق (مع قلب إشارة الجزء التخيلي). تفعل الدالة dot هذا تلقائيًا، وتفعله الصيغة u' صراحةً، لكن sum(u .* v) سيفشل إذا كان u وv مركبين.

المعايير

ما مدى "كبر" المتجه؟

إن norm محاولة لالتقاط هذا، باختزال المتجه إلى قيمة قياسية مناسبة.

هناك عائلة كاملة من المعايير معرَّفة، لكن أكثرها شيوعًا بفارق كبير هو معيار 2، وهو √(v ⋅ v).

هذه العملية، الجذر التربيعي لمتوسط المربعات، هي المسافة الفيثاغورية من نقطة الأصل (في فضاء ذي N أبعاد). وإذا تخيلنا المتجه سهمًا بدايته عند نقطة الأصل، فإن معيار 2 هو طول السهم.

يمكن حساب أي معيار p بتقديم p كوسيط ثانٍ. ويكون معيار 1 مفيدًا أحيانًا: فهو ببساطة مجموع القيم المطلقة للعناصر (لذا حسابه سريع وسهل جدًا).

# defaults to the 2-norm
julia> norm([1, 2, 3])
3.7416573867739413
# the 1-norm
julia> norm([1, -2, 3], 1)
6.0

ضرب المصفوفات

يبدو أحيانًا أن كل عالم رياضيات تطبيقي في العالم قضى جزءًا كبيرًا من الثمانين عامًا الماضية في تحويل كل حساب إلى متتالية من عمليات ضرب المصفوفات.

وهي عملية تتفوق فيها الحواسيب بشكل كبير:

  • فهي شديدة التكرار.
  • ويمكن تنفيذها بالتوازي بكفاءة.
  • وقد طُوّرت الكثير من العتاد المتخصص لجعلها أسرع، بما في ذلك وحدة معالجة الرسوميات المدمجة على الأرجح في حاسوبك المحمول.

التفاصيل بسيطة إلى حد ما، وإن كانت غير بديهية كثيرًا من النظرة الأولى.

خذ مصفوفة A تضرب متجهًا v لتعطي ناتجًا w (بحسب عُرف الجبر الخطي، سنستخدم حروفًا كبيرة للمصفوفات وحروفًا صغيرة للمتجهات).

يُضرب الصف الأول من A ضربًا نقطيًا في v للحصول على العنصر الأول من w، ويعطي الصف الثاني العنصر الثاني، وهكذا نزولًا.

julia> A = [1 2; 3 4]
2×2 Matrix{Int64}:
 1  2
 3  4
julia> v = [5, 6]
2-element Vector{Int64}:
 5
 6
julia> A * v
2-element Vector{Int64}:
 17  # equals [1, 2] ⋅ [5, 6]
 39  # equals [3, 4] ⋅ [5, 6]

يوسّع ضرب مصفوفة في مصفوفة هذا الأمر ليشمل أعمدة المصفوفة الواقعة على اليمين.

بالنسبة إلى C = A * B، يمكننا أن نعتبر أن A تضرب كل عمود من B لتعطي العمود المقابل من C: متتالية من عمليات ضرب مصفوفة في متجه.

وبالمثل، يمكننا القول إن الصف الأول من A يُضرب ضربًا نقطيًا في كل عمود من B ليعطي الصف الأعلى من C، ويعطي الصف الثاني الصف الثاني، وهكذا نزولًا.

julia> A
2×2 Matrix{Int64}:
 1  2
 3  4

julia> B = [5 7; 6 8]
2×2 Matrix{Int64}:
 5  7
 6  8

# left column is the same as A*v previously
julia> A * B
2×2 Matrix{Int64}:
 17  23
 39  53
 
julia> B * A
2×2 Matrix{Int64}:
 26  38
 30  44

كما يظهر في المثال أعلاه، فإن ضرب المصفوفات لا يتبادل: فلا توجد علاقة بسيطة بين A*B وB*A.

من المرجح أن يكون تصوّر ضرب المصفوفات صعبًا على كل من هو جديد عليه، بمجرد قراءة الكلمات. يحتوي YouTube على الكثير من الفيديوهات التي توضحه رسوميًا، لذا ابحث عن "ضرب المصفوفات" واختر واحدًا يناسب أسلوبك ومستوى التفصيل الذي تريده ولغتك.

الأبعاد

يعتمد الضرب النقطي لمتجهين على أن يكون لهما الطول نفسه.

وبالقياس على ذلك، في ضرب المصفوفات يجب أن يتطابق عدد الأعمدة في المصفوفة اليسرى مع عدد الصفوف في المصفوفة اليمنى.

إذا عبّرنا عن الأحجام على هيئة أزواج مرتبة (nrows, ncols)، كما يُخرجها size(A)، فلدينا (a, b) * (b, c) -> (a, c). الأبعاد "الداخلية"، وهما هنا b وb، متوافقة مع إجراء الضرب النقطي. أما الأبعاد "الخارجية"، وهما هنا a وc، فتحددان أبعاد الناتج.

مثال بمصفوفات مستطيلة:

julia> D = reshape(1:6, 2, 3)
2×3 reshape(::UnitRange{Int64}, 2, 3) with eltype Int64:
 1  3  5
 2  4  6

julia> E = reshape(1:12, 3, 4)
3×4 reshape(::UnitRange{Int64}, 3, 4) with eltype Int64:
 1  4  7  10
 2  5  8  11
 3  6  9  12

julia> D * E
2×4 Matrix{Int64}:
 22  49   76  103
 28  64  100  136

# (3, 4) * (2, 3) not possible
julia> E * D
ERROR: DimensionMismatch: matrix A has axes (Base.OneTo(3),Base.OneTo(4)), matrix B has axes (Base.OneTo(2),Base.OneTo(3))

ضرب أزواج المتجهات بأسلوب المصفوفات له احتمالان.

بحسب العُرف، سنستخدم u' * v كمعادل للضرب النقطي. الأبعاد هنا (1, 3) * (3, 1)، وتُبسّط Julia الناتج (1, 1) إلى قيمة قياسية (على عكس، مثلًا، لغة R).

وبدلًا من ذلك، يمكننا استخدام u * v'، بأبعاد (3, 1) * (1, 3) -> (3, 3)، لضرب كل الأزواج الممكنة من العناصر وتوسيع المتجهين إلى مصفوفة.

julia> u = [1, 2]
2-element Vector{Int64}:
 1
 2
julia> v = [3, 4]
2-element Vector{Int64}:
 3
 4
julia> u' * v
11
julia> u * v'
2×2 Matrix{Int64}:
 3  4
 6  8

يُسمى u' * v أحيانًا الضرب الداخلي.

الدورانات

هناك نوع شائع بشكل خاص من ضرب المصفوفات يتضمن مصفوفات الدوران.

في الفضاء ثنائي الأبعاد، توجد مصفوفة بسيطة نسبيًا لتدوير متجه عكس اتجاه عقارب الساعة بمقدار θ راديان.

julia> rot2d(θ, vec) = [cos(θ) -sin(θ); sin(θ) cos(θ)] * vec
rot2d (generic function with 1 method)

# unit vector in the x direction
julia> i_hat = [1, 0]
2-element Vector{Int64}:
 1
 0

# rotate 45 degrees
julia> rot2d(π/4, i_hat)
2-element Vector{Float64}:
 0.7071067811865476
 0.7071067811865475

# rotate 90 degrees -> unit vector in the y direction, j_hat
julia> rot2d(π/2, i_hat)
2-element Vector{Float64}:
 6.123233995736766e-17  # zero, within numerical error
 1.0

التعليمات

أنت تعمل في شركة ناشئة للروبوتات تطوّر روبوتًا بسيطًا كنموذج أولي لإثبات الفكرة. وقد أُوكلت إليك مهمة توفير بعض الوظائف للتحكّم في حركة الروبوت.

1. وجّه الروبوت

لتتبّع اتجاه الروبوت وامتداده، لديه ثلاث علامات تقع كل منها على مسافة وحدة واحدة من مركزه. ولتهيئة موضعه، علينا أن نأخذ متجهات الاتجاه الثلاثة، ونطبّعها، ثم نضعها في مصفوفة.

نفّذ الدالة orientrobot(vectors) التي تأخذ متجهًا مكوّنًا من ثلاثة متجهات. أرجع مصفوفة 2x3 تكون فيها المتجهات المطبَّعة أعمدةً.

julia> orientrobot([[-1,1],[1,0],[-1,-1]])
2×3 Matrix{Float64}:
 -0.707107  1.0  -0.707107
  0.707107  0.0  -0.707107

2. دوّر الروبوت

بعد ذلك، نحتاج إلى إمكانية تغيير اتجاه الحركة. وللقيام بذلك، علينا تدوير الروبوت ليواجه الوجهة التي يريد الذهاب إليها.

نفّذ الدالة rotaterobot(orientation, θ) التي تأخذ مصفوفة توجيه الروبوت وزاوية θ للدوران عكس اتجاه عقارب الساعة حولها. أرجع مصفوفة التوجيه الجديدة.

julia> orientmatrix = initialize([[-1,1],[1,0],[-1,-1]]);

julia> rotaterobot(orientmatrix, π/2)
2×3 Matrix{Float64}:
 -0.707107  6.12323e-17   0.707107
 -0.707107  1.0          -0.707107

3. تحقّق من صحة التوجيه

لنقل الروبوت من موضع إلى آخر، علينا أولًا التحقق من أنه يتخذ التوجيه الصحيح قبل تحريكه. يمثّل العمود الثاني من مصفوفة التوجيه اتجاه المواجهة الأمامية.

نفّذ الدالة robotoriented(orientation, direction) التي تأخذ مصفوفة توجيه ومتجه موضع نسبي. أرجع true إذا كان الروبوت موجّهًا في الاتجاه نفسه الذي يشير إليه متجه الموضع النسبي (ضمن خطأ التقريب).

julia> orientmatrix = initialize([[-1,1],[1,0],[-1,-1]]);

julia> robotoriented(orientmatrix, [5, 0])
true

julia> robotoriented(orientmatrix, [0, 5])
false

julia> robotoriented(orientmatrix, [-5, 0])
false

4. إحداثيات جسم الروبوت

بما أن مصفوفة التوجيه تتبّع أيضًا شكل الروبوت، نحتاج إلى معرفة أين تقع هذه النقاط بالنسبة إلى نقطة الأصل بعد تحريك الروبوت. سيساعد ذلك الروبوت على تجنّب التصادم مع الأجسام الأخرى أثناء تحركه.

نفّذ الدالة bodylocation(orientation, position) التي تأخذ مصفوفة توجيه والموضع الحالي لمركز الروبوت. أرجع مصفوفة التوجيه المُزاحة.

julia> orientmatrix = initialize([[-1,1],[1,0],[-1,-1]]);

julia> bodylocation(orientmatrix, [5, 3])
2×3 Matrix{Float64}:
 4.29289  6.0  4.29289
 3.70711  3.0  2.29289
تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة
Julia Exercism

مستعد لبدء ثورة الروبوتات؟

سجّل في Exercism لتتعلّم وتتقن Julia عبر 35 مفهومًا128 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.