«جبر خطی» چیست؟
تعاریف فنی زیادی وجود دارد، در ویکیپدیا، در کتابهای درسی و در وبسایتهای عالیای مثل 3Blue1Brown، اما برای هدف ما میتوانیم غیررسمیتر باشیم.
جبر خطی دربارهی کارهای جالب (و بسیار کاربردی) زیادی است که میتوانید با بردارها و ماتریسها انجام دهید.
نگهدارندگان Julia (و Python) عاشق این جور کارها هستند. مدیریت Exercism اما، نه چندان.
این مفهوم محدود به جنبههای سادهتر یک موضوع غولآسا خواهد بود، اما هشدار: این ناگزیر مفهومی کاملاً ریاضی است.
بستگی دارد از چه کسی بپرسید، و بخواهید یک مفهوم نسبتاً انتزاعی را چطور تجسم کنید.
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 آن را به فضای نام میآورد، اما این را در مثالها تکرار نمیکنیم (شلوغی بصری زیادی دارد).
این موضوع در مفهوم عملیات برداری بحث شد.
عملگر .* است که بهصورت جفتی روی بردارهای ورودی عمل میکند و خروجیای با همان اندازه و نوع ورودیها میدهد.
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 تلاشی برای بیان این است، با کاهش بردار به یک اسکالر مناسب.
خانوادهی کاملی از «نُرم»ها تعریف شدهاند، اما تا حد زیادی رایجترین آنها نُرم ۲ است که برابر √(v ⋅ v) است.
این عملیات ریشهی میانگین مربعات، فاصلهی فیثاغورسی از مبدأ است (در فضای N-بعدی). اگر بردار را بهصورت پیکانی تصور کنیم که دنبالهاش در مبدأ است، نُرم ۲ همان طول پیکان است.
هر نُرم p را میتوان با دادن p بهعنوان آرگومان دوم محاسبه کرد.
نُرم ۱ گاهی مفید است: همان مجموع قدر مطلق درایههاست (پس محاسبهاش بسیار سریع و آسان است).
# 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 وجود ندارد.
احتمالاً برای تازهواردها، تجسم ضرب ماتریسی فقط با خواندن متن سخت است. یوتیوب ویدیوهای زیادی دارد که آن را بهصورت گرافیکی نشان میدهند، پس «matrix multiplication» را جستوجو کنید و یکی را با سبک، سطح جزئیات و زبان دلخواهتان انتخاب کنید.
ضرب نقطهای دو بردار به برابر بودن طولشان وابسته است.
به همین ترتیب، در ضرب ماتریسی تعداد ستونهای ماتریس چپ باید با تعداد سطرهای ماتریس راست برابر باشد.
اگر اندازهها را بهصورت تاپلهای (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
شما برای یک استارتاپ رباتیک کار میکنید که در حال توسعهی یک ربات ساده بهعنوان اثبات مفهوم است. وظیفهی شما این است که قابلیتهایی برای کنترل حرکت ربات فراهم کنید.
ربات برای اینکه جهت و امتداد خود را دنبال کند، سه نشانگر دارد که هر کدام ۱ واحد از مرکز آن فاصله دارند. برای مقداردهی اولیه به موقعیت آن، باید سه بردار جهت را برداریم، آنها را نرمال کنیم و در یک ماتریس قرار دهیم.
تابع 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
در مرحلهی بعد، به قابلیتی نیاز داریم که نحوهی تغییر جهت حرکت را مشخص کند. برای این کار باید ربات را بچرخانیم تا رو به جایی که میخواهد برود قرار بگیرد.
تابع 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
برای جابهجا کردن ربات از یک نقطه به نقطهی دیگر، اول باید بررسی کنیم که پیش از حرکت، جهت درستی دارد. ستون دوم ماتریس جهت، جهت رو به جلو را نشان میدهد.
تابع 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
از آنجا که ماتریس جهت، شکل ربات را هم دنبال میکند، باید بدانیم این نقاط پس از حرکت ربات نسبت به مبدأ کجا قرار دارند. این کار به ربات کمک میکند هنگام حرکت، از برخورد با اشیای دیگر جلوگیری کند.
تابع 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