diff --git a/.github/runner.d b/.github/runner.d index d36c942..e4ecfc1 100644 --- a/.github/runner.d +++ b/.github/runner.d @@ -23,6 +23,9 @@ static: PackageInfo("libdparse_usage"), // workaround https://github.com/dlang-jp/Cookbook/issues/198 PackageInfo("vibe-d_usage", ["windows-x86_omf-", "linux-x86-", "osx-x86-", "osx-x86_64-"]), + // ldc2は以下Issueが原因でx86では動作しないため除外 + // https://github.com/libmir/mir-algorithm/issues/461 + PackageInfo("mir_usage", ["linux-x86-ldc"]), PackageInfo("windows"), ]; } diff --git a/dub.sdl b/dub.sdl index e372e5b..522e21c 100644 --- a/dub.sdl +++ b/dub.sdl @@ -10,7 +10,8 @@ subPackage "linux" subPackage "thirdparty/libdparse" subPackage "thirdparty/json" subPackage "thirdparty/vibe-d" +subPackage "thirdparty/mir" dependency "dateparser" version="~>3.0.4" -buildOptions "coverage" \ No newline at end of file +buildOptions "coverage" diff --git a/thirdparty/mir/dub.sdl b/thirdparty/mir/dub.sdl new file mode 100644 index 0000000..12eb800 --- /dev/null +++ b/thirdparty/mir/dub.sdl @@ -0,0 +1,16 @@ +name "mir_usage" +description "A minimal D application." +authors "lempiji" +copyright "Copyright © 2023, lempiji" +license "public domain" +dependency "mir-algorithm" version="~>3.19.1" + +configuration "library" { + targetType "library" +} + +configuration "include-blas" { + targetType "library" + dependency "mir-blas" version="~>1.1.14" + versions "MIRUSAGE_INCLUDE_BLAS" +} diff --git a/thirdparty/mir/source/mir_usage/slice_blas.d b/thirdparty/mir/source/mir_usage/slice_blas.d new file mode 100644 index 0000000..618f88d --- /dev/null +++ b/thirdparty/mir/source/mir_usage/slice_blas.d @@ -0,0 +1,45 @@ +/** +BLAS利用 + +mirのSliceとBLASを合わせて使う方法を整理します。 +BLASを利用するには、環境に合わせて適切なライブラリを用意する必要があります。 + +Windowsでは既定で Intel MKL が使われます。 +Posixでは既定で Open BLAS が使われます。 + +Source: $(LINK_TO_SRC thirdparty/mir/source/mir_usage/slice_blas.d) +Macros: + TITLE=mirのSliceとBLASを組み合わせて使う例 +*/ +module mir_usage.slice_blas; + +version (MIRUSAGE_INCLUDE_BLAS): + +/** +行列積を計算する方法 + +mir-blasの gemm 関数を利用します +*/ +unittest +{ + import mir.ndslice : slice, sliced, transposed; + import mir.blas : gemm; + + // B = A.x を計算します + auto A = [ + 1f, 2, 3, + 4, 5, 6 + ].sliced(2, 3); + + auto x = [ + 1f, 2, 3 + ].sliced(1, 3); + + auto B = slice!float(A.shape[0], x.shape[0]); + + // 通常のgemmには転置フラグがありますが、transposedの有無を見て適切に処理されます + gemm(1, A, x.transposed, 0, B); + + assert(B[0, 0] == 14); + assert(B[1, 0] == 32); +} diff --git a/thirdparty/mir/source/mir_usage/slice_common.d b/thirdparty/mir/source/mir_usage/slice_common.d new file mode 100644 index 0000000..f552c73 --- /dev/null +++ b/thirdparty/mir/source/mir_usage/slice_common.d @@ -0,0 +1,337 @@ +/++ +Slice基本操作 + +mirでは多次元配列(いわゆるテンソル)を扱うための型として `Slice` 型を提供します。 +ここでは `Slice` の基本操作について、数値計算で扱う内容を主に整理します。 + +- Slice型の宣言方法 +- Sliceの構築 +- Sliceの変更 +- Sliceの変形 +- Slice同士の基本的な演算 + +Source: $(LINK_TO_SRC thirdparty/mir/source/mir_usage/slice_common.d) +Macros: + TITLE=mirのSlice基本操作 ++/ +module mir_usage.slice_common; + +/** +Slice型のインポート、型宣言の利用方法 +*/ +unittest +{ + // 多次元配列を扱うには、`mir.ndslice` から `Slice` という定義をインポートします。 + // なお今後扱う操作は mirパッケージのモジュール構成上 `mir.ndslice.*` として多数出てきますが、それらは `mir.ndslice` からもインポート可能です。 + // `Slice`を扱うだけであれば、import は基本的に `mir.ndslice` だけ覚えれば十分、ということです。 + import mir.ndslice : Slice; + + // `Slice` 型の変数宣言を行います。 + // 簡単に扱う範囲では、テンプレートの第1引数に要素型のポインタ、第2引数にテンソルの階数、を指定すればOKです。 + // 厳密にはメモリ上の要素レイアウトによって第3引数を指定できますが、ここでは省略します。 + // 省略した場合は `contiguous` というレイアウトになり、全要素が連続なメモリ上に1列に整列された高速な計算に向くレイアウトとなります。 + + // たとえば以下の変数は 1階のテンソル であり、つまり要素が1次元に並んだベクトルとなります。 + // なお、型宣言の時点ではベクトルの長さ、つまり「形状(shape)」の情報を持っていません。 + // テンソルの形状は、実際にメモリを確保して初期化する時に決定します。 + Slice!(float*, 1) vector; + + // 第3引数は、省略しているだけで同じ意味です + import mir.ndslice : SliceKind; + + static assert(is(Slice!(float*, 1) == Slice!(float*, 1, SliceKind.contiguous))); + + // 階数に2を指定した場合、縦横に要素を持った行列になります。 + Slice!(float*, 2) matrix; + + // 3以上を指定して任意のテンソル型を表現することもできます。 + Slice!(float*, 3) tensor; +} + +/** +Slice型に新たなメモリを割り当てて初期化する方法 +*/ +unittest +{ + // slice型の初期化には、主に `slice` 関数を使います。 + import mir.ndslice : Slice, slice; + + // `slice` 関数はテンプレート引数として要素型を受け取り、 + // 関数としての引数で次元ごとの大きさを指定します。 + Slice!(float*, 1) vec3f = slice!float(3); + Slice!(int*, 1) vec4i = slice!int(4); + + // 引数の数が階数に対応付けられます + Slice!(int*, 2) mat22i = slice!int(2, 2); + + // 16x16の8bit1チャンネルなアイコン画像を保持するのであれば以下のように初期化します + Slice!(ubyte*, 2) icon = slice!ubyte(16, 16); + + // アイコン画像を4枚束ねて保持するのであれば次のようになります + Slice!(ubyte*, 3) icons = slice!ubyte(4, 16, 16); +} + +/** +既存の配列データからSliceを構築する方法 + +sliced 関数を利用します。 +*/ +unittest +{ + // 既存の配列から Slice 型を構築するには、 `sliced` という関数を使います。 + import mir.ndslice : Slice, sliced; + + // 6要素の配列を2x3行列だと思ってSlice型を構築します。 + // この操作はメモリ確保を行わず、単にビューとして働きます。 + int[] arr = [1, 2, 3, 4, 5, 6]; + + // ビューの大きさに指定する部分は、すべて掛け合わせた値が配列の長さと一致する必要があります + Slice!(int*, 2) view = sliced(arr, 2, 3); +} + +/** +既存の多次元配列データから高階のSliceを構築する方法 + +fuse 関数を利用します。 +*/ +unittest +{ + // 多次元配列をSliceに変換する場合は fuse 関数を使用します。 + // 元のメモリレイアウトが不明のため、この関数は新しくメモリを確保して要素をコピーします + import mir.ndslice : Slice, fuse; + + // 多次元データを準備します + auto src23 = [[1, 2], [3, 4], [5, 6]]; + + // fuse を引数なしで呼び出します + Slice!(int*, 2) mat23 = src23.fuse(); + assert(mat23[0, 0] == 1); + + // これを sliced でやろうとすると以下の解釈になり失敗します。 + import mir.ndslice : sliced; + + version (none) + { + // 一見上手くいきそうに期待しますが、要素が配列となる1階のテンソルとなるので型が不一致です + Slice!(int*, 2) miss = src23.sliced(2, 3); + } + // このように解釈されます + Slice!(int[]*, 1) success = src23.sliced(3); +} + +/** +Sliceを多次元配列に変換する方法 +*/ +unittest +{ + // Slice型をD言語における多次元配列へ変換するには、`ndarray` 関数を利用します + import mir.ndslice : Slice, slice, ndarray; + + // 階数が1であれば1次元配列が得られます + Slice!(float*, 1) vec = slice!float(3); + float[] vec_arr = vec.ndarray(); + assert(vec_arr.length == 3); + + // 行列であれば、すべての配列が同じ長さである整った多次元配列が得られます + Slice!(float*, 2) mat = slice!float(2, 2); + float[][] mat_arr = mat.ndarray(); + assert(mat_arr.length == 2); + assert(mat_arr[0].length == 2); + assert(mat_arr[1].length == 2); +} + +/** +Sliceのデータを読み取ったり書き換えたりする方法 +*/ +unittest +{ + // Slice型は通常のD言語における標準的な配列とほぼ同じ操作が可能です。 + import mir.ndslice : Slice, slice; + + Slice!(int*, 1) vec3i = slice!int(3); + + // インデックスアクセスによるread/write、配列と同様にスライス操作も可能です。 + assert(vec3i[0] == 0); + assert(vec3i[1] == 0); + assert(vec3i[2] == 0); + + vec3i[0 .. 2] = 100; + vec3i[2] = 1; + assert(vec3i[0] == 100); + assert(vec3i[1] == 100); + assert(vec3i[2] == 1); + + // 2階以上の場合も同様ですが、1つの [] 内にインデックスを並べるようになります + Slice!(int*, 2) mat33 = slice!int(3, 3); + // 3x3の行列に対する左上要素の書き換え + mat33[0, 0] = 10; + + // 複数の次元で同時にスライス操作が可能です。 + // 3x3の行列に対する右下2x2部分の書き換え + mat33[1 .. 3, 1 .. 3] = 100; + + assert(mat33[0, 0] == 10); + assert(mat33[1, 1] == 100); + assert(mat33[2, 2] == 100); + + // 配列と同じで 0 .. $ といった$指定が可能です。 + // 0 .. $ は最初から最後までとなるため、これを使うと行列から列ベクトルを取り出すこともできます。 + auto col1 = mat33[0 .. $, 1]; + assert(col1[0] == 0); + assert(col1[1] == 100); + assert(col1[2] == 100); +} + +/** +Sliceの値を複数まとめて確認・比較する方法 + +Sliceの演算は複雑になりやすいため、単体テストで様々な確認がしたくなります。 +ここではいくつかの簡便な確認方法を整理します。 +*/ +unittest +{ + import mir.ndslice : Slice, sliced; + + // 元となるデータを行列らしく用意します + // dfmt off + Slice!(int*, 2) mat22 = [ + 1, 2, + 3, 4, + ].sliced(2, 2); + // dfmt on + + // 2x2のデータは次のように直接多次元配列と比較することができます。 + assert(mat22 == [[1, 2], [3, 4]]); + + // 0行目を取り出す、という指定で2x2の上段が得られます。 + // 加えてレンジに対するopEqualsが提供されるため、配列とそのまま比較できます。 + assert(mat22[0] == [1, 2]); + + // この時は、Sliceの一部を取り出したことで階数が1つ下がったSliceが得られます + static assert(is(typeof(mat22[0]) == Slice!(int*, 1))); + + // 列ベクトルも階数が落ちるため、配列と同じように比較できます。 + auto col1 = mat22[0 .. $, 1]; + assert(col1 == [2, 4]); +} + +/** +Sliceの形状(shape)を確認する方法 + +shape プロパティで完全な形状情報を得る方法と length を使って特定次元のみ確認する方法があります。 +*/ +unittest +{ + import mir.ndslice : sliced; + + int[] arr = [1, 2, 3, 4, 5, 6]; + + // 配列からビューとしていくつか異なる形状のSliceを作成します。 + auto vec6i = arr.sliced(6); + auto mat23i = arr.sliced(2, 3); + + // 1階のテンソルは長さ1の配列で形状が得られます。 + size_t[1] shape1 = vec6i.shape; + assert(shape1 == [6]); + + // 2階のテンソルは長さ2の配列で形状が得られます。以下同様です。 + size_t[2] shape2 = mat23i.shape; + assert(shape2 == [2, 3]); + + + // length プロパティを使うと特定の次元のみ確認することができます。 + // 1階の場合は単に length でアクセスします + assert(vec6i.length == 6); + + // 2階以上の場合はテンプレート引数で階数を指定します + assert(mat23i.length!0 == 2); + assert(mat23i.length!1 == 3); +} + +/** +Sliceの全要素数がいくつか確認する方法 + +shapeをすべて掛け合わせる代わりに elementCount が利用できます。 +*/ +unittest +{ + import mir.ndslice : Slice, slice; + + Slice!(int*, 3) mat = slice!int(2, 2, 3); + assert(mat.elementCount == 12); +} + +/** +要素が初期化されていないSliceの構築方法 + +自分が望む行列などを構築するにあたり、何も値が初期化されていないSliceを用意したくなることがあります。 +そういった場合は、 `uninitSlice` を使うことで用意できます。 +*/ +unittest +{ + import mir.ndslice : Slice, uninitSlice; + + // slice関数と利用方法は同じですが、確保されたメモリの値が初期化されないため初期化前の読み取りには注意してください + Slice!(int*, 2) mat23 = uninitSlice!int(2, 3); + + // 0初期化ではなく1で埋めるような場合に利用できます。 + mat23[] = 1; +} + +/** +2階以上のSliceを平坦なSliceおよび配列にする方法 +*/ +unittest +{ + import mir.ndslice : Slice, sliced; + + Slice!(int*, 2) mat23 = [1, 2, 3, 4, 5, 6].sliced(2, 3); + + // flattened を使うことで要素を1列に並べ直した1階のテンソルに変形できます + import mir.ndslice : flattened; + + auto vec6 = mat23.flattened(); + + // このあと ndarray を呼び出すと1次元配列になります + import mir.ndslice : ndarray; + + int[] arr = vec6.ndarray(); + assert(arr == [1, 2, 3, 4, 5, 6]); +} + +/** +Sliceのデータをそのままに形状だけ変更する方法 + +reshape 関数を利用すると形状を変更できます。 +*/ +unittest +{ + import mir.ndslice : Slice, sliced, reshape; + + Slice!(int*, 1) x = [1, 2, 3, 4, 5, 6].sliced(6); + + // 要素数が合わないと実行時エラーになるため、エラーフラグを準備します + int reshapeErr; + + // 形状とエラーフラグを渡して形状を変更します + Slice!(int*, 2) y = x.reshape([2, 3], reshapeErr); + assert(y.shape == [2, 3]); +} + +/** +新しいメモリを割り当ててSliceをコピーする方法 +*/ +unittest +{ + import mir.ndslice : slice; + + auto x = slice!int(2, 2); + x[0, 0] = 10; + + // Slice型をもう一度slice関数に通すとメモリが割り当てられコピーされます。 + auto y = x.slice(); + assert(y[0, 0] == 10); // 元の値が引き継がれています + + y[1, 1] = -1; + assert(x[1, 1] != -1); // 元の値は書き換わりません +} \ No newline at end of file diff --git a/thirdparty/mir/source/mir_usage/slice_matrix.d b/thirdparty/mir/source/mir_usage/slice_matrix.d new file mode 100644 index 0000000..f24bc1f --- /dev/null +++ b/thirdparty/mir/source/mir_usage/slice_matrix.d @@ -0,0 +1,102 @@ +/++ +行列関連操作 + +mirパッケージのSlice操作のうち、2次元の行列に関する頻出の操作を整理します。 + +Source: $(LINK_TO_SRC thirdparty/mir/source/mir_usage/slice_matrix.d) +Macros: + TITLE=mirのSliceにおける行列関連操作 ++/ +module mir_usage.slice_matrix; + +/** +行列の転置操作を行う方法 + +`transposed` 関数を使います +*/ +unittest +{ + import mir.ndslice : Slice, sliced, transposed; + + // dfmt off + Slice!(float*, 2) mat23f = [ + 1.0f, 2.0f, 3.0f, + 4.0f, 5.0f, 6.0f + ].sliced(2, 3); + // dfmt on + assert(mat23f.shape == [2, 3]); + + // 2x3 の行列を転置すると 3x2 になります + auto mat32f = mat23f.transposed(); + assert(mat32f.shape == [3, 2]); + + assert(mat32f == [ + [1.0f, 4.0f], + [2.0f, 5.0f], + [3.0f, 6.0f] + ]); +} + +/** +行列を1行ずつインデックスを見ながら処理する方法 +*/ +unittest +{ + import mir.ndslice : sliced; + + auto mat23 = [ + 1, 2, 3, + 4, 5, 6 + ].sliced(2, 3); + + // 行情報だけ回すだけならforeachも可 + foreach (row; mat23) + { + assert(row.shape == [3]); + } + + // インデックス付きで回すなら shape から取ると高階の場合でも使えて汎用性は高い + foreach (i; 0 .. mat23.shape[0]) + { + assert(mat23[i].shape == [3]); + assert(mat23[i] == [1 + 3 * i, 2 + 3 * i, 3 + 3 * i]); + } + + // コードを短くしたり細かい列挙コストを気にする場合は each も利用できる + import std.algorithm : each; + + mat23.each!((i, row) { + // i は foreach と同様に 0, 1, 2, ...として行の数だけ渡される + // row が束縛された状態で評価されるので事故が少ない + assert(row.shape == [3]); + assert(row == [1 + 3 * i, 2 + 3 * i, 3 + 3 * i]); + }); +} + +/** +行列の一部に別の行列の値をコピーする +*/ +unittest +{ + import mir.ndslice : Slice, slice, sliced; + + Slice!(int*, 2) target = slice!int(4, 4); + Slice!(int*, 2) part = [1, 2, 3, 4].sliced(2, 2); + + // 貼り付けたい位置をスライス構文で指定して代入します + target[0 .. 2, 0 .. 2] = part; + assert(target[0 .. 2, 0 .. 2] == [[1, 2], [3, 4]]); + + // offsetが動的に決まるような場合は、lengthと組み合わせます。 + const offset_row = 1; + const offset_col = 0; + + target[ + offset_row .. offset_row + part.length!0, + offset_col .. offset_col + part.length!1 + ] = part; + assert(target[ + offset_row .. offset_row + part.length!0, + offset_col .. offset_col + part.length!1 + ] == [[1, 2], [3, 4]]); +}