Javaでの行列乗算

1. 概要

このチュートリアルでは、Javaで2つの行列を乗算する方法を見ていきます。
マトリックスの概念はネイティブに言語に存在しないため、私たちはそれを自分で実装します。また、いくつかのライブラリーを使用して、マトリックスの乗算を処理する方法を確認します。
最後に、最速のソリューションを決定するために、検討したさまざまなソリューションのベンチマークを少し行います。

2. 例

このチュートリアル全体を通して参照できる例を設定することから始めましょう。
最初に、3×2マトリックスを想像します。
link:/uploads/firstMatrix.png []
今度は、2行4列の2番目のマトリックスを想像してみましょう。
link:/uploads/secondMatrux.png []
次に、最初のマトリックスに2番目のマトリックスを乗算すると、3×4マトリックスになります。
link:/uploads/multiplicatedMatrix.png []
念のため、*この結果は、結果の行列の各セルを次の式で計算することで得られます*。
link:/uploads/multiplicationAlgorithm.png []
ここで、_r_はマトリックス_A_の行数、__ c __はマトリックス_B_の列数、_n_はマトリックス_B_の行数と一致する必要があるマトリックス_A_の列数です。

3. 行列乗算

3.1. 独自の実装

マトリックスの独自の実装から始めましょう。
シンプルに保ち、* 2次元の_double_配列を使用します*:
double[][] firstMatrix = {
  new double[]{1d, 5d},
  new double[]{2d, 3d},
  new double[]{1d, 7d}
};

double[][] secondMatrix = {
  new double[]{1d, 2d, 3d, 7d},
  new double[]{5d, 2d, 8d, 1d}
};
これらは、この例の2つのマトリックスです。 乗算の結果として期待されるものを作成しましょう:
double[][] expected = {
  new double[]{26d, 12d, 43d, 12d},
  new double[]{17d, 10d, 30d, 17d},
  new double[]{36d, 16d, 59d, 14d}
};
すべてが設定されたので、乗算アルゴリズムを実装しましょう。 *最初に空の結果配列を作成し、セルを反復処理して、各セルに期待される値を保存します。*
double[][] multiplyMatrices(double[][] firstMatrix, double[][] secondMatrix) {
    double[][] result = new double[firstMatrix.length][secondMatrix[0].length];

    for (int row = 0; row < result.length; row++) {
        for (int col = 0; col < result[row].length; col++) {
            result[row][col] = multiplyMatricesCell(firstMatrix, secondMatrix, row, col);
        }
    }

    return result;
}
最後に、単一のセルの計算を実装しましょう。 それを実現するために、*例のプレゼンテーションで前述した式を使用します*:
double multiplyMatricesCell(double[][] firstMatrix, double[][] secondMatrix, int row, int col) {
    double cell = 0;
    for (int i = 0; i < secondMatrix.length; i++) {
        cell += firstMatrix[row][i] * secondMatrix[i][col];
    }
    return cell;
}
最後に、アルゴリズムの結果が期待される結果と一致することを確認しましょう。
double[][] actual = multiplyMatrices(firstMatrix, secondMatrix);
assertThat(actual).isEqualTo(expected);

3.2. EJML

最初に調べるライブラリはEJMLで、これはhttp://ejml.org/wiki/index.php?title=Main_Page[Efficient Java Matrix Library]の略です。 このチュートリアルを書いている時点では、*最新のJavaマトリックスライブラリ*の1つです。 その目的は、計算とメモリ使用に関して可能な限り効率的であることです。
_pom.xml_にhttps://search.maven.org/search?q=g:org.ejml%20AND%20a:ejml-all [ライブラリへの依存関係]を追加する必要があります。
<dependency>
    <groupId>org.ejml</groupId>
    <artifactId>ejml-all</artifactId>
    <version>0.38</version>
</dependency>
以前とほとんど同じパターンを使用します。この例に従って2つの行列を作成し、乗算の結果が以前に計算したものであることを確認します。
それでは、EJMLを使用してマトリックスを作成しましょう。 これを達成するために、*ライブラリが提供する_SimpleMatrix_クラスを使用します*。
2次元の_double_配列をコンストラクターの入力として使用できます。
SimpleMatrix firstMatrix = new SimpleMatrix(
  new double[][] {
    new double[] {1d, 5d},
    new double[] {2d, 3d},
    new double[] {1d ,7d}
  }
);

SimpleMatrix secondMatrix = new SimpleMatrix(
  new double[][] {
    new double[] {1d, 2d, 3d, 7d},
    new double[] {5d, 2d, 8d, 1d}
  }
);
次に、予想される乗算の行列を定義しましょう。
SimpleMatrix expected = new SimpleMatrix(
  new double[][] {
    new double[] {26d, 12d, 43d, 12d},
    new double[] {17d, 10d, 30d, 17d},
    new double[] {36d, 16d, 59d, 14d}
  }
);
すべてのセットアップが完了したので、2つのマトリックスを乗算する方法を見てみましょう。 * _SimpleMatrix_クラスは、別の_SimpleMatrix_をパラメーターとして取り、2つの行列の乗算を返すa__mult()_メソッドを提供します。
SimpleMatrix actual = firstMatrix.mult(secondMatrix);
取得した結果が期待される結果と一致するかどうかを確認しましょう。
_SimpleMatrix_は_equals()_メソッドをオーバーライドしないため、検証を行うためにこれに頼ることはできません。 しかし、* itは代替手段を提供します:_isIdentical()_ method *は、別のマトリックスパラメーターだけでなく、倍精度による小さな違いを無視する_double_フォールトトレランスパラメーターも使用します。
assertThat(actual).matches(m -> m.isIdentical(expected, 0d));
これで、EJMLライブラリを使用した行列乗算が終了します。 他のものが提供しているものを見てみましょう。

3.3. ND4J

では、https://deeplearning4j.org/docs/latest/nd4j-overview [ND4J Library]を試してみましょう。 ND4Jは計算ライブラリであり、https://deeplearning4j.org/ [deeplearning4j]プロジェクトの一部です。 とりわけ、ND4Jはマトリックス計算機能を提供します。
まず、https://search.maven.org/search?q = g:org.nd4j%20AND%20a:nd4j-native [ライブラリの依存関係]を取得する必要があります。
<dependency>
    <groupId>org.nd4j</groupId>
    <artifactId>nd4j-native</artifactId>
    <version>1.0.0-beta4</version>
</dependency>
GAリリースにはいくつかのバグがあるようであるため、ここではベータ版を使用していることに注意してください。
簡潔にするために、2次元の_double_配列を書き直さず、各ライブラリでの使用方法に焦点を合わせます。 したがって、ND4Jでは、_INDArray_を作成する必要があります。 そのためには、* * _Nd4j.create()_ファクトリーメソッドを呼び出し、マトリックスを表す_double_配列を渡します*:
INDArray matrix = Nd4j.create(/* a two dimensions double array */);
前のセクションと同様に、3つのマトリックスを作成します。2つを乗算し、1つを期待される結果にします。
その後、実際に_INDArray.mmul()_メソッドを使用して最初の2つのマトリックス間の乗算を行います。
INDArray actual = firstMatrix.mmul(secondMatrix);
次に、実際の結果が期待される結果と一致することを再度確認します。 今回は、等価性チェックに依存できます。
assertThat(actual).isEqualTo(expected);
これは、ND4Jライブラリーを使用してマトリックス計算を行う方法を示しています。

3.4. Apache Commons

では、https://commons.apache.org/proper/commons-math/ [Apache Commons Math3 module]について話しましょう。これは、行列操作を含む数学計算を提供します。
繰り返しますが、_pom.xml_でhttps://search.maven.org/search?q=g:org.apache.commons%20AND%20a:commons-math3 [依存関係]を指定する必要があります。
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-math3</artifactId>
    <version>3.6.1</version>
</dependency>
設定が完了したら、* _ RealMatrix_インターフェイスとその__Array2DRowRealMatrix_実装*を使用して、通常のマトリックスを作成できます。 実装クラスのコンストラクターは、パラメーターとして2次元の_double_配列を受け取ります。
RealMatrix matrix = new Array2DRowRealMatrix(/* a two dimensions double array */);
行列の乗算に関しては、* _ RealMatrix_インターフェイスは、_multiply()_メソッド*を提供し、別の_RealMatrix_パラメーターを取得します。
RealMatrix actual = firstMatrix.multiply(secondMatrix);
最終的に、結果が予想と等しいことを確認できます。
assertThat(actual).isEqualTo(expected);
次のライブラリを見てみましょう!

3.5. LA4J

これはLA4Jという名前で、http://la4j.org/ [Java用の線形代数]を表しています。
これにもhttps://search.maven.org/search?q=g:org.la4j%20AND%20a:la4j [依存関係]を追加しましょう。
<dependency>
    <groupId>org.la4j</groupId>
    <artifactId>la4j</artifactId>
    <version>0.6.0</version>
</dependency>
現在、LA4Jは他のライブラリとほとんど同じように機能します。 * 2次元_double_配列を入力としてとるa__Basic2DMatrix_実装*を備えたa__Matrix_インターフェイスを提供します。
Matrix matrix = new Basic2DMatrix(/* a two dimensions double array */);
Apache Commons Math3モジュールと同様に、*乗算方法は_multiply()_ *であり、パラメーターとして別の_Matrix_を取ります。
Matrix actual = firstMatrix.multiply(secondMatrix);
もう一度、結果が予想と一致することを確認できます。
assertThat(actual).isEqualTo(expected);
最後のライブラリであるColtを見てみましょう。

3.6. Colt

https://dst.lbl.gov/ACSSoftware/colt/[Colt]はCERNによって開発されたライブラリです。 高性能の科学技術計算を可能にする機能を提供します。
以前のライブラリと同様に、https://search.maven.org/search?q = g:colt%20AND%20a:colt [正しい依存関係]を取得する必要があります。
<dependency>
    <groupId>colt</groupId>
    <artifactId>colt</artifactId>
    <version>1.2.0</version>
</dependency>
Coltでマトリックスを作成するには、* DoubleFactory2Dクラスを使用する必要があります*。 _dense、sparse_、_rowCompressed_の3つのファクトリインスタンスが付属しています。 それぞれが一致する種類のマトリックスを作成するために最適化されます。
この目的のために、_dense_インスタンスを使用します。 今回は、呼び出すメソッドは_make()_ *であり、再び2次元の_double配列_を取り、_DoubleMatrix2D_オブジェクトを生成します。
DoubleMatrix2D matrix = doubleFactory2D.make(/* a two dimensions double array */);
マトリックスがインスタンス化されたら、それらを乗算します。 今回は、それを行うための行列オブジェクトにはメソッドがありません。 パラメーターに2つの行列をとるa__mult()_メソッド*を持つ* _Algebra_クラスのインスタンスを作成する必要があります。
Algebra algebra = new Algebra();
DoubleMatrix2D actual = algebra.mult(firstMatrix, secondMatrix);
次に、実際の結果と期待される結果を比較できます。
assertThat(actual).isEqualTo(expected);

4. ベンチマーク

これで、行列乗算のさまざまな可能性の探索が完了したので、どれが最もパフォーマンスが高いかを確認しましょう。
*パフォーマンステストを実装するには、https://www.baeldung.com/java-microbenchmark-harness [* JMHベンチマークライブラリ*]を使用します。 次のオプションを使用してベンチマーククラスを構成しましょう。
public static void main(String[] args) throws Exception {
    Options opt = new OptionsBuilder()
      .include(MatrixMultiplicationBenchmarking.class.getSimpleName())
      .mode(Mode.AverageTime)
      .forks(2)
      .warmupIterations(5)
      .measurementIterations(10)
      .timeUnit(TimeUnit.MICROSECONDS)
      .build();

    new Runner(opt).run();
}
このようにして、JMHは_ @ Benchmark_アノテーションが付けられた各メソッドに対して2回の完全な実行を行い、それぞれ5回のウォームアップ反復(平均計算には含まれません)と10回の測定反復を行います。 測定に関しては、さまざまなライブラリの平均実行時間をマイクロ秒単位で収集します。
次に、配列を含む状態オブジェクトを作成する必要があります。
@State(Scope.Benchmark)
public class MatrixProvider {
    private double[][] firstMatrix;
    private double[][] secondMatrix;

    public MatrixProvider() {
        firstMatrix =
          new double[][] {
            new double[] {1d, 5d},
            new double[] {2d, 3d},
            new double[] {1d ,7d}
          };

        secondMatrix =
          new double[][] {
            new double[] {1d, 2d, 3d, 7d},
            new double[] {5d, 2d, 8d, 1d}
          };
    }
}
そのようにして、配列の初期化がベンチマークの一部ではないことを確認します。 その後、_MatrixProvider_オブジェクトをデータソースとして使用して、行列の乗算を行うメソッドを作成する必要があります。 以前に各ライブラリを見たので、ここではコードを繰り返しません。
最後に、_main_メソッドを使用してベンチマークプロセスを実行します。 これにより、次の結果が得られます。
Benchmark                                                           Mode  Cnt   Score   Error  Units
MatrixMultiplicationBenchmarking.apacheCommonsMatrixMultiplication  avgt   20   1,008 ± 0,032  us/op
MatrixMultiplicationBenchmarking.coltMatrixMultiplication           avgt   20   0,219 ± 0,014  us/op
MatrixMultiplicationBenchmarking.ejmlMatrixMultiplication           avgt   20   0,226 ± 0,013  us/op
MatrixMultiplicationBenchmarking.homemadeMatrixMultiplication       avgt   20   0,389 ± 0,045  us/op
MatrixMultiplicationBenchmarking.la4jMatrixMultiplication           avgt   20   0,427 ± 0,016  us/op
MatrixMultiplicationBenchmarking.nd4jMatrixMultiplication           avgt   20  12,670 ± 2,582  us/op
ご覧のとおり、* _EJML_と_Colt_は、1オペレーションあたり約5分の1マイクロ秒で非常に良好なパフォーマンスを示しています。 他のライブラリのパフォーマンスは中間に位置しています。

5. 結論

この記事では、Javaでマトリックスを自分自身で、または外部ライブラリーで乗算する方法を学びました。 すべてのソリューションを調査した後、それらすべてのベンチマークを行い、ND4Jを除き、すべてが非常に良好に機能することを確認しました。
いつものように、この記事の完全なコードはhttps://github.com/eugenp/tutorials/tree/master/java-math[GitHubで]にあります。