Linear Algebra

Smile Shell provides an MATLAB like environment. In the simplest case, you can use it as a calculator.


    smile> "Hello, World"
    res0: String = Hello, World

    smile> 2
    res1: Int = 2

    smile> 2+3
    res2: Int = 5
    

    smile> "Hello, World"
    $9 ==> "Hello, World"

    smile> 2
    $10 ==> 2

    smile> 2+3
    $11 ==> 5
          

Math Functions

Besides java.lang.Math functions, smile.math.MathEx provides many other important mathematical functions such as factorial, choose, etc.


    smile> choose(10, 3)
    res8: Double = 120.0
    

    smile> import static smile.math.MathEx.*

    smile> choose(10, 3)
    $14 ==> 120.0
          

Special Functions

Special mathematical functions include beta, erf, gamma and their related functions. Special functions are particular mathematical functions which have more or less established names and notations due to their importance in mathematical analysis, functional analysis, physics, or other applications. Many special functions appear as solutions of differential equations or integrals of elementary functions. For example, the error function erf (also called the Gauss error function) is a special function of sigmoid shape which occurs in probability, statistics, materials science, and partial differential equations. The complementary error function, denoted erfc, is defined as erfc(x) = 1 - erf(x). The error function and complementary error function are special cases of the incomplete gamma function.


    smile> erf(1.0)
    res0: Double = 0.8427007929497149

    smile> digamma(1.0)
    res11: Double = -0.5772156649015328
    

    smile> import smile.math.special.*

    smile> Erf.erf(1.0)
    $16 ==> 0.8427007929497149

    smile> Gamma.digamma(1.0)
    $17 ==> -0.5772156649015328
          

Vector Operations

Common arithmetic operations on vectors and scalars are similar as in R and Matlab.


    smile> val x = c(1.0, 2.0, 3.0, 4.0)
    smile> val y = c(4.0, 3.0, 2.0, 1.0)

    smile> x + y
    res22: smile.math.VectorAddVector = Array(5.0, 5.0, 5.0, 5.0)

    smile> 1.5 * x - 3.0 * y
    res24: smile.math.VectorSubVector = Array(-10.5, -6.0, -1.5, 3.0)
    

    smile> double[] x = {1.0, 2.0, 3.0, 4.0}
    x ==> double[4] { 1.0, 2.0, 3.0, 4.0 }

    smile> double[] y = {4.0, 3.0, 2.0, 1.0}
    y ==> double[4] { 4.0, 3.0, 2.0, 1.0 }

    // vector expression is not supported in Java
          

Note that these operations are lazy. The computation is only performed when the results are needed, e.g. when the expression is used where a vector is expected. In the Shell, the expression is immediately performed because the Shell always prints out the results.

For a vector, there are multiple functions to calculate its norm such as norm (L2 norm), norm1 (L1 norm), norm2 (L2 norm), normInf (infinity norm), normFro (Frobenius norm). We can also standardize a vector to mean 0 and variance 1, unitize it so that L2 norm be 1, or unitize1 it so that L1 norm be 1.


    smile> norm(x)
    res13: Double = 5.477225575051661

    smile> unitize(y)
    smile> y
    res14: Array[Double] = Array(0.7302967433402214, 0.5477225575051661, 0.3651483716701107, 0.18257418583505536)
    

    smile> norm(x)
    $20 ==> 5.477225575051661

    smile> unitize(y)

    smile> y
    y ==> double[4] { 0.7302967433402214, 0.5477225575051661, 0.3651483716701107, 0.18257418583505536 }
          

For a pair of vectors, we can calculate the dot product, distance, divergence, covariance, and correlations with dot, distance, kld (Kullback-Leibler Divergence), jsd (Jensen-Shannon Divergence), cov, cor (Pearson Correlation), spearman (Spearman Rank Correlation Coefficient), kendall (Kendall Tau Rank Correlation Coefficient).


    smile> dot(x, y)
    res16: Double = 3.651483716701107

    smile> cov(x, y)
    res17: Double = -0.30429030972509225
    

    smile> dot(x, y)
    res5: Double = 3.651483716701107

    smile> cov(x, y)
    res6: Double = -0.30429030972509225
          

Matrix Operations

Like Matlab, we can use eye, zeros and ones to create identity, zero, or all-ones matrix, respectively. To create a matrix from 2-dimensional array, we can use the constructor matrix or the ~ operator. The ~ operator can be applied to 1-dimensional array too, which creates a single column matrix.


    val a = matrix(
        c(0.7220180, 0.07121225, 0.6881997),
        c(-0.2648886, -0.89044952, 0.3700456),
        c(-0.6391588, 0.44947578, 0.6240573)
    )
    val b = matrix(
        c(0.6881997, -0.07121225, 0.7220180),
        c(0.3700456, 0.89044952, -0.2648886),
        c(0.6240573, -0.44947578, -0.6391588)
    )
    val C = Array(
        Array(0.9527204, -0.2973347, 0.06257778),
        Array(-0.2808735, -0.9403636, -0.19190231),
        Array(0.1159052, 0.1652528, -0.97941688)
    )

    val c = ~C // or val c = matrix(C)
    

    import smile.math.matrix.*;

    double[][] A = {
        {0.7220180, 0.07121225, 0.6881997},
        {-0.2648886, -0.89044952, 0.3700456},
        {-0.6391588, 0.44947578, 0.6240573}
    };
    double[][] B = {
        {0.6881997, -0.07121225, 0.7220180},
        {0.3700456, 0.89044952, -0.2648886},
        {0.6240573, -0.44947578, -0.6391588}
    };
    double[][] C = {
        {0.9527204, -0.2973347, 0.06257778},
        {-0.2808735, -0.9403636, -0.19190231},
        {0.1159052, 0.1652528, -0.97941688}
    };
    var a = Matrix.of(A);
    var b = Matrix.of(B);
    var c = Matrix.of(C);
          

In Scala, matrix-vector operations are just like in math formula.


    smile> val x = c(1.0, 2.0, 3.0)
    x: Array[Double] = Array(1.0, 2.0, 3.0)

    smile> val y = c(3.0, 2.0, 1.0)
    y: Array[Double] = Array(3.0, 2.0, 1.0)

    smile> val res: Array[Double] = a * x + 1.5 * y
    res: Array[Double] = Array(7.4290416, 2.06434916, 3.63196466)
    

    smile> double[] x = {1.0, 2.0, 3.0}
    x ==> double[3] { 1.0, 2.0, 3.0 }

    smile> double[] y = {3.0, 2.0, 1.0}
    y ==> double[3] { 3.0, 2.0, 1.0 }

    smile> a.mv(1.0, x, 1.5, y)
    $48 ==> double[3] { 7.4290416, 2.06434916, 3.63196466 }
          

Similarly, for matrix-matrix operations:


    smile> a + b
    res27: smile.math.MatrixAddMatrix =
        1.4102    0.0000    1.4102
        0.1052    0.0000    0.1052
       -0.0151    0.0000   -0.0151
    

    smile> var d = Matrix.zeros(3, 3)
    d ==> 3 x 3
      0.0000    0.0000    0.0000
      0.0000     ... 000    0.0000    0.0000

    smile> a.add(b, d) // result saved in d, a.add(b) update a directly
    $44 ==> 3 x 3
      1.4102    0.0000    1.4102
      0.1052    0.0000    0.1052
     -0.0151    0.0000   -0.0151
          

Note that a * b are element-wise:


    smile> a * b
    res28: smile.math.MatrixMulMatrix =
        0.4969   -0.0051    0.4969
       -0.0980   -0.7929   -0.0980
       -0.3989   -0.2020   -0.3989
    

    smile> a.mul(b, d)
    $45 ==> 3 x 3
      0.4969   -0.0051    0.4969
     -0.0980   -0.7929   -0.0980
     -0.3989   -0.2020   -0.3989
          

For matrix multiplication, the operator is %*%, same as in R


    smile> a %*% b %*% c
    [main] INFO smile.math.MatrixOrderOptimization - The minimum cost of matrix multiplication chain: 54
    res29: smile.math.MatrixExpression =
        0.9984    0.0067    0.0554
       -0.0257    0.9361    0.3508
       -0.0495   -0.3517    0.9348
    

    smile> a.mm(b).mm(c)
    $49 ==> 3 x 3
      0.9984    0.0067    0.0554
     -0.0257    0.9361    0.3508
     -0.0495   -0.3517    0.9348
          

The method DenseMatrix.transpose returns the transpose of matrix, which executes immediately. However, the method t is preferred on MatrixExpression as it is lazy.


    smile> a %*% b.t %*% c
    [main] INFO smile.math.MatrixOrderOptimization - The minimum cost of matrix multiplication chain: 54
    res30: smile.math.MatrixExpression =
        0.8978   -0.4369    0.0543
        0.4189    0.8856    0.2006
       -0.1357   -0.1574    0.9782
    

    smile> a.mt(b).mm(c)
    $50 ==> 3 x 3
      0.8978   -0.4369    0.0543
      0.4189    0.8856    0.2006
     -0.1357   -0.1574    0.9782
          

Smile has runtime optimization for matrix multiplication chain, which can greatly improve the performance. Note that this optimization is only available in Scala API. In the below we generate several random matrices and multiply them together.


    val a = randn( 300,  900)
    val b = randn( 900,  150)
    val c = randn( 150, 1800)
    val d = randn(1800,   30)

    time("matrix multiplication") {(a %*% b %*% c %*% d).toMatrix}
    

    smile> var a = Matrix.randn( 300,  900)
    [main] INFO smile.math.MathEx - Set RNG seed 19650218 for thread main
    a ==> 300 x 900
     -0.9299   -0.4984    1.3793    1.8589  ... 4842   -0.5907  ...
      ...

    smile> var b = Matrix.randn( 900,  150)
    b ==> 900 x 150
      0.9851    0.9842    0.7543   -0.6598  ... 9706    0.9420  ...
      ...

    smile> var c = Matrix.randn( 150, 1800)
    c ==> 150 x 1800
      0.8682   -1.9094   -0.2466    0.1238 ... 2070   -1.1657  ...
      ...

    smile> var d = Matrix.randn(1800,   30)
    d ==> 1800 x 30
     -0.1421   -0.4016   -1.7960    0.2153  ... 6566   -1.0292  ...
      ...

    smile> a.mm(b).mm(c).mm(d)
    $55 ==> 300 x 30
    1027.7940  -7083.7899  20850.3728  14316.0928  3122.5039  6656.6392  -14332.0066  ...
    -15355.3544  18424.0367  3362.8806  1969.2299  -23705.3085  -8948.9324  7468.9138  ...
    -442.4282  7575.2694  -8070.4564  15107.1986  10726.3271  -170.4820  -19199.5856  ...
    4155.9123  -11273.9462  4326.8992  -276.7401  22746.9657  23260.6079  -1052.8137  ...
    27450.9909  -353.9005  26619.2334  -2807.0904  -18675.1774  -7891.4804  9164.3414  ...
    11257.9267  -12587.2370  -15836.0616  -8085.9522  -1277.4189  -11561.2331  -8508.3348  ...
    -7136.4159  3785.3912  -15033.8276  9799.7746  -16499.4337  16218.9645  13444.4842  ...
      ...
          

where randn() creates a matrix of normally distributed random numbers. The shell will try to load machine optimized BLAS/LAPACK native libraries for most matrix computation. If BLAS/LAPACK is not available, smile will fall back to pure Java implementation.

Matrix Decomposition

In linear algebra, a matrix decomposition or matrix factorization is a factorization of a matrix into a product of matrices. There are various matrix decompositions. In Smile, we provide LU, QR, Cholesky, eigen, and SVD decomposition by functions lu, qr, cholesky, eigen, and svd, respectively.

With these decompositions, many important linear algebra operations can be performed such as calculating matrix rank, determinant, solving linear systems, computing inverse matrix, etc. In fact, Smile has functions det, rank, inv and operator \ for these common computation.


    smile> val x = Array(1.0, 2.0, 3.0)
    x: Array[Double] = Array(1.0, 2.0, 3.0)

    smile> a \ x
    res14: Array[Double] = Array(2.9290414582113184, -0.9356509345036078, 2.131964578605774)

    smile> inv(a)
    res19: smile.math.matrix.DenseMatrix =
      0.7220   -0.2649   -0.6392
      0.0712   -0.8904    0.4495
      0.6882    0.3700    0.6241

    smile> inv(a) %*% a
    res21: smile.math.MatrixExpression =
      1.0000    0.0000    0.0000
     -0.0000    1.0000    0.0000
     -0.0000    0.0000    1.0000
    

    smile> double[] x = {1.0, 2.0, 3.0}
    x ==> double[3] { 1.0, 2.0, 3.0 }

    smile> var inv = a.inverse()
    inv ==> 3 x 3
      0.7220   -0.2649   -0.6392
      0.0712   -0.8904    0.4495
      0.6882    0.3700    0.6241

    smile> inv.mm(a)
    $67 ==> 3 x 3
      1.0000   -0.0000    0.0000
     -0.0000    1.0000    0.0000
      0.0000    0.0000    1.0000

    smile> var lu = a.lu()
    lu ==> smile.math.matrix.Matrix$LU@5f375618

    smile> lu.solve(x)
    $69 ==> double[3] { -1.7252356779053744, -0.3612592362819077, 3.3004624918302046 }
          
Fork me on GitHub