"লিনিয়ার অ্যালজেবরা" কী?
এর টেকনিক্যাল সংজ্ঞা প্রচুর আছে: [Wikipedia][linalg-wiki]-তে, পাঠ্যবইয়ে, আর [3Blue1Brown][3blue1brown]-এর মতো দারুণ সব ওয়েবসাইটে। তবে আমাদের এখানে দরকার অনুযায়ী আমরা একটু সহজ-সরল ভাষাতেই বলতে পারি।
লিনিয়ার অ্যালজেবরা হলো ভেক্টর আর ম্যাট্রিক্স দিয়ে করা নানা মজার (আর খুবই কাজের) কাজ।
Julia (আর Python)-এর মেইনটেইনাররা এসব কাজ খুব পছন্দ করেন। Exercism-এর ম্যানেজমেন্ট অবশ্য তেমন নয়।
এই কনসেপ্টটি এত বড় একটা বিষয়ের কেবল সহজ দিকগুলোতেই সীমাবদ্ধ থাকবে, তবে আগেই সাবধান করে দিই: এটা অনিবার্যভাবেই বেশ গাণিতিক একটা কনসেপ্ট।
এটা নির্ভর করে আপনি কাকে জিজ্ঞেস করছেন, আর বেশ বিমূর্ত একটা জিনিসকে আপনি কীভাবে চোখে দেখতে চান তার উপর।
Vector{T} আসলে Array{T, 1}-এর একটি অ্যালিয়াস, আর eltype T যেকোনো কিছু হতে পারে।length আর direction-সহ একটি তীর, যার কোনো নির্দিষ্ট অবস্থান নেই।point হিসেবে দেখানো যায়, যার 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' সিনট্যাক্স করে এটা স্পষ্টভাবে, কিন্তু u আর v কমপ্লেক্স হলে sum(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-এর মধ্যে কোনো সরল সম্পর্ক নেই।
যে কারও কাছে বিষয়টা নতুন, তার পক্ষে শুধু লেখা পড়ে ম্যাট্রিক্স গুণ চোখে ভাবা সম্ভবত কঠিন। 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
আপনি একটি রোবোটিক্স স্টার্টআপের হয়ে কাজ করছেন, যারা প্রমাণ-ধারণা (proof-of-concept) হিসেবে একটি সহজ রোবট তৈরি করছে। রোবটটির গতি নিয়ন্ত্রণ করার জন্য কিছু ফাংশনালিটি তৈরি করার দায়িত্ব আপনাকে দেওয়া হয়েছে।
রোবটের অভিমুখ ও বিস্তার ট্র্যাক রাখতে এর তিনটি মার্কার আছে, যেগুলো এর কেন্দ্র থেকে ১ একক দূরত্বে অবস্থিত। এর অবস্থান ইনিশিয়ালাইজ করতে, আমাদের তিনটি দিকনির্দেশক ভেক্টর নিতে হবে, সেগুলো নরমালাইজ করতে হবে, এবং একটি ম্যাট্রিক্সে বসাতে হবে।
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
Exercism-এ সাইন আপ করুন, Julia ট্র্যাকের 35টি কনসেপ্ট128টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।