ずっと前のベクトルのコンセプトで、_「配列は任意のサイズにでき(ハードウェアのメモリーの制約を受けるだけです)、いくつでも次元を持てます」_と述べました。
それ以来、話を簡単に保つために、2次元以上の配列はほとんど無視してきました。 この判断は、Juliaのリファレンスをそのままの複雑さで読んでみると、より納得できるでしょう。
しかし、高次元の配列は科学技術計算では_とてもとても_重要なので、理解しておく必要があります。
用語について: 何世紀にもわたる数学の慣例に従い、1次元配列をVectors、2次元配列をMatricesと呼びます。
このドキュメントの例は、ほとんどが行列です。 3次元以上を扱う構文はほぼ同じですが、(2次元の画面では)出力が読みにくく、混乱しやすいものです。
eltypeがTであるN次元配列のtypeは、Array{T, N}です。
利便性と数学の用語法との一貫性のために、Juliaはいくつかの型エイリアスを定義しています。Array{T, 1}に対するVector{T}、Array{T, 2}に対するMatrix{T}です。
# 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
これまで、角括弧の中にカンマ区切りのリストを書くことで、たくさんのベクトルを作ってきました。 区切り文字にはセミコロンも使えます。
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のMatrixとして定義します(別の文脈では、これを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のMatrixを列方向に埋めています。
よくある種類の配列(一様なもの、ランダムなもの)を作るためのユーティリティ関数がいろいろ用意されています。
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
NumPyに親しんでいる方なら、np.linspaceが広く使われている関数だとご存じでしょう。指定した長さで、両端を指定し、等間隔の値を持つ配列を生成します。
ふつうはグラフのx軸や、線形モデルの独立変数として使われます。
Juliaにまったく同じものはありませんが、とても柔軟なrange()関数が、下限と上限、そしてlengthキーワード引数を指定することで同じ役割を果たせます。
ローカル環境でPlotsパッケージを使える方は、このコードを実行できます。
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])
StepRangeLenはベクトルのように使えます。行列の列に変換することもできます。
対数軸上で等間隔になる値を生成する、同等のlogrange()関数も見てみましょう。
2次元配列では、ふつう[row, col]の順で2つのインデックスを使います。
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
最後の例は少し意外かもしれません。_1つだけ_のインデックスはエラーにならず、1つの要素を返します。
その理由は、列優先の順序についての説明に戻ります。Juliaは、_メモリー上で_3番目の要素を見つけるまで、列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
:だけを書くと、どんな配列も列方向に平坦化してVectorにします。
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> m[:]
6-element Vector{Int64}:
1
4
2
5
3
6
これまでに、sum()やmaximum()のような集約関数を1次元のコレクションに適用する例を見てきました。これらはすべての要素に対して働き、スカラーの結果を返します。
これは高次元でも同じように機能します。
ただし、関数を1つの次元にだけ適用したいこともあります。たとえば、縦方向または横方向に合計して、サイズ1のsingleton dimensionを持つ配列を返す場合です。
そのために、省略可能な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に指定する値は、_サイズ1になる次元_です。上の例ではdims=1 => 1×3、dims=2 => 2×1になります。
さらに、dimsに配列や範囲を指定すれば、高次元配列の複数の次元をまとめて縮約できます_(繰り返しますが、出力を理解するには少し考える必要があるかもしれません!)_。
dimsキーワード引数はsum()のような組み込み関数ではよく見かけますが、同じものを自分のコードで書くにはどうすればよいでしょうか?
1つの良い答えは、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の配列はテラバイト級になることもあるので、コピーは性能面での悪夢になり得ます!
このようなビューは、ループ、ブロードキャスト、そしてこれまでのシラバスで見てきたあらゆる操作に使えます。
また、内包表記は配列を入力にすると強力で柔軟です。 簡単な例はループのコンセプトで触れましたが、後のコンセプトでより詳しく説明します。
1次元の場合については、ベクトル演算のコンセプトで説明しました。
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をbroadcastingして次元を合わせることで[0.5, 0.5, 0.5]と等価なものとして扱い、要素ごとに引き算を行います。
これを高次元に拡張しても、やることは基本的に同じです。
たとえば、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)。
さらに、関数はVectorのときとまったく同じように、Matrixの各要素にブロードキャストできます。
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にもよく当てはまります。
ベクトルと行列に対するよくある操作の多くは、線形代数という大きな分野に含まれます。
(現在)Exercismのサイトにはさらに2つのコンセプトがあり、ペアになる演習を設計できるまでは、ドキュメントだけが公開されています。
将来、行列の分解についての3つ目のコンセプトができるかもしれません。
ただし、線形代数の基礎はかなり数学的で、すべての学習者に合うとは限らないことにご注意ください。
後のコンセプト(方程式の解法と行列の分解)はより高度で、大学レベルの数学に抵抗がない人向けです。
申し訳ありませんが、これがこの分野の性質であり、Juliaの重要な用途でもあります。