Régen, a Vektorok fogalomban megjegyeztük, hogy „a tömbök tetszőleges méretűek lehetnek (csak a hardvered memóriakorlátai szabnak határt nekik), és tetszőleges számú dimenziójuk lehet.”
Azóta nagyrészt figyelmen kívül hagytuk az egynél több dimenziós tömböket, hogy egyszerű maradjon a dolog. Ez a döntés sokkal érthetőbb lesz, ha megpróbálod elolvasni a Julia referenciadokumentumait, teljes bonyolultságukban.
A magasabb dimenziós tömbök azonban nagyon-nagyon fontosak a tudományos számításokban, ezért meg kell értenünk őket.
Nevezéktan: A matematika évszázados hagyományát követve az egydimenziós tömböket Vectors, a kétdimenziósakat Matrices néven emlegetjük.
A dokumentumban szereplő példák többnyire mátrixok lesznek. A 3 vagy több dimenzióval való munka szintaktikailag szinte ugyanaz, de a kimenetet nehéz és zavaró olvasni (egy kétdimenziós képernyőn).
Egy N-dimenziós, T eltype-ű tömb type-ja Array{T, N}.
A kényelem és a matematikai nevezéktannal való összhang kedvéért a Julia definiál néhány típusáltnevet: Vector{T} a Array{T, 1} helyett és Matrix{T} a Array{T, 2} helyett.
# 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
Sok vektort hoztunk már létre úgy, hogy egy vesszővel elválasztott listát tettünk szögletes zárójelbe. Elválasztóként pontosvessző is használható.
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
Ha szóközt (vagy tabulátort) használunk elválasztóként, az eredmény más lesz.
julia> u = [1 2 3]
1×3 Matrix{Int64}:
1 2 3
A Julia ezt most egy 1×3-as mátrixként definiálja (más összefüggésben row vector-nak neveznénk).
Általában a szóközök vízszintesen kapcsolnak össze dolgokat, a pontosvesszők (vagy a sortörések) pedig függőlegesen.
A »dolgok« említése szándékosan homályos, mert a Julia megpróbál dolgozni azzal, amit adsz neki.
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
Léteznek hcat() és vcat() függvények, amelyek ugyanezt teszik, és egyértelműbbé teszik, hogy ezek vízszintes és függőleges összefűzések.
A magasabb dimenziós általánosítás a cat() függvény.
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
A kifejezett mátrixokat kényelmesen sorfolytonos sorrendben írjuk le, mert ez illeszkedik az emberi intuícióhoz (könnyebb ránézni, a vízszintes szöveget használó kultúrákban):
julia> m = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
1 2 3
4 5 6
Jegyezd meg azonban, hogy a Julia (a Fortranhoz, R-hez és Matlabhoz hasonlóan, de a C/C++-tól vagy a NumPy-tól eltérően) oszlopfolytonos sorrendben tárolja az N-dimenziós tömböket, és ez óriási teljesítménykülönbséget jelenthet, ha végigiterálsz az elemeken. Segíts a CPU-gyorsítótáradnak, hogy segíthessen neked!
# 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
A fenti példa az 1-től 6-ig terjedő egészeket veszi, és oszloponként tölti fel velük a 2×3-as mátrixot.
Több segédfüggvény is létezik a gyakori tömbtípusok létrehozására (egyenletes vagy véletlen).
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
A NumPy rajongói tudják, hogy a np.linspace széles körben használt függvény meghatározott hosszúságú, meghatározott végpontokkal és egyenletesen elosztott értékekkel rendelkező tömb létrehozására.
Ezt jellemzően egy grafikon x-tengelyeként vagy lineáris modellek független változójaként használják.
A Juliának nincs pontos megfelelője, de a nagyon rugalmas range() függvény utánozhatja, ha megadjuk az alsó és felső korlátot, valamint a length kulcsszóargumentumot.
Aki helyben dolgozik, és hozzáfér a Plots csomaghoz, lefuttathatja ezt a kódot:
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])
A StepRangeLen úgy használható, mint egy vektor, beleértve a mátrixoszloppá alakítást is.
Lásd még a megfelelő logrange() függvényt, amellyel egy logaritmikus tengelyen egyenletesen elosztott értékeket generálhatsz.
Egy kétdimenziós tömbnél általában két indexet használunk, [row, col] sorrendben.
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
Az utolsó példa talán meglepő: egy egyszerű index nem hiba, és egyetlen elemet ad vissza.
A magyarázat visszavezet az oszlopfolytonos sorrendről szóló megjegyzéshez: a Julia végigmegy az 1. oszlopon, majd a 2. oszlopon, amíg meg nem találja a 3. elemet a memóriában.
Vigyázz: ennek van néhány haszna általános célú könyvtárak írásakor, de valószínűbb, hogy összezavar!
Hasonló probléma merül fel, amikor egy tömb méretét kérdezzük le.
A length() az elemek teljes számát adja, a size() pedig egy ndims() elemű tuple-t ad, amely az egyes dimenziók hosszát tartalmazza.
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
Könnyedén másolhatunk egy részmátrixot.
Az alábbi példában a reshape() függvény a megadott dimenziókra kényszeríti a tömböt úgy, hogy oszloponként tölti fel, majd felszeleteljük.
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
Egy önmagában álló : azt jelenti, hogy »másolj le mindent ebben a dimenzióban«.
Nem egymást követő sorokhoz/oszlopokhoz vektorokat használj:
julia> m12[[1, 3], :] # rows 1 and 3
2×3 Matrix{Int64}:
1 5 9
3 7 11
Egy önmagában álló : bármely tömböt egy vektorrá lapít, oszloponként.
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> m[:]
6-element Vector{Int64}:
1
4
2
5
3
6
Korábban már láttunk olyan aggregációs függvényeket, mint a sum() és a maximum(), egydimenziós gyűjteményekre alkalmazva, ahol az összes elemen működnek, és skaláris eredményt adnak vissza.
Ez magasabb dimenziókban is működik.
Előfordulhat azonban, hogy a függvényt csak egy dimenzióra szeretnénk alkalmazni, például lefelé vagy keresztben összegezve, hogy egy 1 méretű singleton dimension-nel rendelkező tömböt kapjunk.
Erre szolgál az opcionális dims kulcsszóargumentum.
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
A dims értéke az a dimenzió, amely singleton lesz, tehát a fenti példákban dims=1 => 1×3 és dims=2 => 2×1.
Általánosítva, a magasabb dimenziós tömbök több dimenziót is redukálhatnak, ha a dims értéke egy tömb vagy tartomány (a kimenet megértése ismét némi gondolkodást igényelhet!).
A dims kulcsszóargumentum gyakori a beépített függvényekben, mint a sum(), de hogyan írjunk valami egyenértékűt a saját kódunkban?
Az egyik jó válasz a magasabb rendű függvények használata, mint a reduce(), amit egy későbbi fogalomban részletesebben is tárgyalunk.
Alternatívaként a Julia több olyan függvényt kínál, amelyek segítségével az N-dimenziós tömböket úgy kezelhetjük, mintha vektorok egymásba ágyazott vektorai lennének.
Mátrixokhoz a eachrow() és a eachcol() kényelmes, de az általánosabb függvény az eachslice(), amely tetszőleges dimenziókat kezel.
# 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]
A típus kissé ijesztő, de csak azért, mert ez egy nézet az eredeti tömbre, ami lehetővé teszi, hogy másolás nélkül dolgozzunk rajta. A Julia tömbök akár terabájt méretűek is lehetnek, ezért a másolás potenciális teljesítményrémálom lehet!
Egy ilyen nézet használható ciklusokhoz, broadcastinghoz vagy bármely más művelethez, amit eddig a tantervben láttunk.
Emellett a comprehensionök hatékonyak és sokoldalúak lehetnek tömbbemenettel. Az egyszerű eseteket a Ciklusok fogalomban említettük, de egy későbbi fogalomban bővebben is kitérünk rájuk.
Az egydimenziós esetet a Vektorműveletek fogalomban tárgyaltuk.
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
A pontozott .- operátor az egyetlen 0.5-öt egyenértékűnek tekinti a [0.5, 0.5, 0.5]-tel, mivel broadcasting útján a dimenziókhoz igazítja, majd elemenként elvégzi a kivonást.
Ennek kiterjesztése magasabb dimenziókra valójában csak ugyanaz még egyszer.
Például amikor egy 2x1-es sorvektor [1.0 1.5 2.0] és a 2x3-as mátrix [1 2 3; 4 5 6] szorzását broadcasteljük, az egyenértékű azzal, mintha [1.0 1.5 2.0]-t [1.0 1.5 2.0; 1.0 1.5 2.0]-ra broadcastelnénk, majd a szorzást elemenként végeznénk el.
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
Megjegyzés: A broadcastinghez a nem singleton dimenzióknak kell azonos méretűnek lenniük (pl. 2x3 Matrix .* 2x1 Matrix).
Továbbá egy függvény pontosan ugyanúgy broadcastelhető egy Matrix minden elemére, ahogy egy Vector esetében is történik.
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
Eleinte úgy érezheted, hogy a programozói összezavarodás lehetősége exponenciálisan nő a dimenziók számával, de a gyakorlás sokat segít. Ráadásul a NumPy tömbökkel való jártasság meglehetősen jól átvihető a Juliára, az eltérő szintaxis ellenére is.
A vektorokon és mátrixokon végzett sok gyakori művelet a lineáris algebra általános körébe tartozik.
(Jelenleg) két további fogalom található az Exercism weboldalán, amelyek csak dokumentum formájában léteznek, amíg nem tudunk hozzájuk feladatokat tervezni.
A jövőben lehet egy harmadik fogalom is a mátrixfaktorizációkról.
Ne feledd azonban, hogy a Lineáris algebra alapjai meglehetősen matematikai jellegű, és nem biztos, hogy minden tanulónak megfelel.
A későbbi fogalmak (egyenletmegoldás és faktorizációk) fejlettebbek, és valójában csak azoknak szólnak, akik otthonosan mozognak az egyetemi szintű matematikában.
Elnézést, de ilyen a téma természete, és ez a Julia egyik fontos felhasználási esete.