ずっと前にVectorsの概念で、_配列は任意のサイズ(ハードウェアのメモリ制約だけに縛られます)を持て、任意の数の次元も持てます_と述べました。
それ以降は、話を簡単に保つために、2次元以上の配列はほとんど無視してきました。 この判断の意味は、Juliaのリファレンスをそのままの複雑さで読んでみると、よくわかるでしょう。
しかし、多次元配列は科学技術計算では_とてもとても_重要なので、理解しておく必要があります。
用語について: 何世紀にもわたる数学の慣例に従い、1次元配列をVectors、2次元配列をMatricesと呼びます。
このドキュメントの例は、ほとんどが行列です。 3次元以上を扱う構文はほぼ同じですが、出力は(2次元の画面では)読みにくく、混乱しやすいものです。
これまでは、角括弧([])の中にカンマ区切りのリストを書いて、たくさんのベクトルを作ってきました。
区切り文字としてセミコロンも使えます。
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
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は1列目を下へ、次に2列目を下へと進み、_メモリ上で_3番目の要素を見つけるのです。
注意してください。汎用ライブラリを書くときには役立つこともありますが、むしろ混乱を招くことのほうが多いです!
配列のサイズを調べるときにも、似た問題が起こります。
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
これまでに、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の配列はテラバイト級になることもあるので、コピーは性能上の悪夢になりかねません!
このようなビューは、ループやブロードキャスト、これまでシラバスで見てきたそのほかの操作に使えます。
また、内包表記は配列を入力にすると強力で、応用範囲も広いです。
簡単な例はLoopsの概念で触れましたが、後の概念でより詳しく説明します。
1次元の場合についてはVector Operationsの概念で説明しました。
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
注意: ブロードキャストでは、サイズ1でない次元の大きさが一致している必要があります(例: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にもかなり活かせます。
ドットマトリクスは、ドットと空白でできた2次元の画像です。 ドットが、空白を背景として意図した画像を形作ります。
ドットマトリクス画像は、2次元のMatrixとしてメモリに保存し、操作できます。
以下では、行列内の「空白」を0で、「ドット」を非ゼロで表します。
Matrixを定義するMatrixは次のようになります。ここでは0を空白、1をドットとします。
[
0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0;
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
0 1 0 0 0 1 0 0 0 0 0 0 1 0 0 0 1 0;
0 1 0 0 1 0 1 0 0 0 0 1 0 1 0 0 1 0;
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1;
0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0;
0 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1 0;
0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0;
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0;
]
これを「レンダリング」すると、次のようになります。
XX XX
X X
X X X X
X X X X X X
X X
X X
X X X X
X X X X
X XX X
X X
XX XX
frown!()とfrown()という関数を定義します。これらの関数はExercismロゴのMatrixを入力として受け取り、笑っている口をしかめ面の口に変えたMatrixを返します。
結果として得られるMatrixをレンダリングすると、次のようになります。
XX XX
X X
X X X X
X X X X X X
X X
X X
X XX X
X X X X
X X X X
X X
XX XX
関数stickerwall()を定義します。これはExercismの行列を入力として受け取り、次のようにレンダリングされるドットマトリクスのMatrixを返します。
XX XX XX XX
X X X X
X X X X X X X X
X X X X X X X X X X X X
X X X X
X XX X
X X X X X XX X
X X X X X X X X
X XX X X X X X
X X X X
XX XX XX XX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XX XX XX XX
X X X X
X X X X X X X X
X X X X X X X X X X X X
X X X X
X XX X
X XX X X X X X
X X X X X X X X
X X X X X XX X
X X X X
XX XX XX XX
ドットとして1だけを使う必要はありません。必要なら、他の有用な情報をエンコードすることもできます。
関数colpixelcount()を定義します。これは1をドットとする任意のドットマトリクスを入力として受け取ります。
同じサイズのドットマトリクスを返します。各列のドットは、その列にあるドットの数になります。
ExercismロゴのMatrixを入力とすると、出力は次のようなMatrixになります。
[
0 0 2 2 0 0 0 0 0 0 0 0 0 0 2 2 0 0;
0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
0 8 0 0 0 1 0 0 0 0 0 0 1 0 0 0 8 0;
0 8 0 0 1 0 2 0 0 0 0 2 0 1 0 0 8 0;
0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1;
0 8 0 0 0 0 2 0 0 0 0 2 0 0 0 0 8 0;
0 8 0 0 0 0 0 1 0 0 1 0 0 0 0 0 8 0;
0 8 0 0 0 0 0 0 1 1 0 0 0 0 0 0 8 0;
0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
0 0 2 2 0 0 0 0 0 0 0 0 0 0 2 2 0 0;
]
自分たちの作品をレンダリングする話をするだけでなく、簡単に確認できるように実際にレンダリングできるようにしましょう。
関数render()を定義します。これはドットマトリクスを入力として受け取ります。
ドットを'X'、0を' 'としてレンダリングし、各行を改行でつないだ文字列を返します。
julia> render(E)
" XX XX \n X X \n X X X X \n X X X X X X \n X X \nX X\n X X X X \n X X X X \n X XX X \n X X \n XX XX "
出力すると、ExercismのMatrixが期待どおりにレンダリングされるはずです。
julia> print(render(E))
XX XX
X X
X X X X
X X X X X X
X X
X X
X X X X
X X X X
X XX X
X X
XX XX
これは、1以外の「ドット」でも動作するはずです。
julia> print(render(colpixelcount(E)))
XX XX
X X
X X X X
X X X X X X
X X
X X
X X X X
X X X X
X XX X
X X
XX XX