Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Add lapack support * Wrapper done. TODO tests * Least square working for 1d, not for 2d * Fix residual, the first value wasn't squared * Least squares solver fully implemented 🎆
- Loading branch information
Showing
13 changed files
with
317 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
# Copyright (c) 2018 the Arraymancer contributors | ||
# Distributed under the Apache v2 License (license terms are at http://www.apache.org/licenses/LICENSE-2.0). | ||
# This file may not be copied, modified, or distributed except according to those terms. | ||
|
||
import ../tensor/tensor, ../tensor/backend/metadataArray, | ||
../tensor/private/p_init_cpu, | ||
nimlapack, fenv, math | ||
|
||
|
||
proc gelsd(m: ptr cint; n: ptr cint; nrhs: ptr cint; a: ptr cfloat; lda: ptr cint; | ||
b: ptr cfloat; ldb: ptr cint; s: ptr cfloat; rcond: ptr cfloat; rank: ptr cint; | ||
work: ptr cfloat; lwork: ptr cint; iwork: ptr cint; info: ptr cint) {.inline.}= | ||
sgelsd( | ||
m, n, nrhs, | ||
a, m, | ||
b, ldb, | ||
s, rcond, rank, | ||
work, lwork, | ||
iwork, info | ||
) | ||
|
||
proc gelsd(m: ptr cint; n: ptr cint; nrhs: ptr cint; a: ptr cdouble; lda: ptr cint; | ||
b: ptr cdouble; ldb: ptr cint; s: ptr cdouble; rcond: ptr cdouble; | ||
rank: ptr cint; work: ptr cdouble; lwork: ptr cint; iwork: ptr cint; | ||
info: ptr cint) {.inline.}= | ||
dgelsd( | ||
m, n, nrhs, | ||
a, m, | ||
b, ldb, | ||
s, rcond, rank, | ||
work, lwork, | ||
iwork, info | ||
) | ||
|
||
|
||
proc least_squares_solver*[T: SOmeReal](a, b: Tensor[T]): | ||
tuple[ | ||
least_square_sol: Tensor[T], | ||
residuals: Tensor[T], | ||
matrix_rank: int, | ||
singular_values: Tensor[T] | ||
] {.noInit.}= | ||
|
||
assert a.rank == 2 and b.rank in {1, 2} and a.shape[0] > 0 and a.shape[0] == b.shape[0] | ||
# We need to copy the input as Lapack will destroy A and replace B with its result. | ||
# Furthermore it expects a column major ordering. | ||
# In the future with move operator we can consider letting moved tensors be destroyed. | ||
|
||
# Note on dealing with row-major without copies: | ||
# http://drsfenner.org/blog/2016/01/into-the-weeds-iii-hacking-lapack-calls-to-save-memory/ | ||
|
||
# LAPACK reference doc: | ||
# http://www.netlib.org/lapack/explore-html/d7/d3b/group__double_g_esolve_ga94bd4a63a6dacf523e25ff617719f752.html | ||
# Intel example for the Fortran API | ||
# https://software.intel.com/sites/products/documentation/doclib/mkl_sa/11/mkl_lapack_examples/dgelsd_ex.c.htm | ||
|
||
let is1d = b.rank == 1 # Axis 1 will be squeezed at the end for the 1d case | ||
|
||
var # Parameters | ||
m = a.shape[0].cint | ||
n = a.shape[1].cint | ||
nrhs = if is1d: 1.cint | ||
else: b.shape[1].cint | ||
# A is row-major so lda = m | ||
ldb = max(m, n).cint | ||
|
||
# Shadowing the inputs | ||
var a = a.clone(colMajor) # Lapack destroys the A matrix during solving | ||
|
||
# Lapack replaces the B matrix by the result during solving | ||
# Furthermore, as we have input B shape M x NRHS and output N x NRHS | ||
# if M < N we must zero the reminder of the tensor | ||
var bstar: Tensor[T] | ||
tensorCpu([ldb.int, nrhs.int], bstar, colMajor) | ||
bstar.storage.Fdata = newSeq[T](bstar.size) | ||
|
||
var bstar_slice = bstar[0 ..< b.shape[0], 0 ..< nrhs] # Workaround because slicing does no produce a var at the moment | ||
apply2_inline(bstar_slice, b): | ||
# paste b in bstar. | ||
# if bstar is larger, the rest is zeros | ||
y | ||
|
||
# Temporary sizes | ||
const smlsiz = 25.cint # equal to the maximum size of the subproblems at the bottom of the computation tree | ||
let | ||
minmn = min(m,n).int | ||
nlvl = max(0, (minmn.T / (smlsiz+1).T).ln.cint + 1) | ||
# Bug in some Lapack, liwork must be determined manually: http://icl.cs.utk.edu/lapack-forum/archives/lapack/msg00899.html | ||
liwork = max(1, 3 * minmn * nlvl + 11 * minmn) | ||
|
||
result.singular_values = newTensorUninit[T](minmn) # will hold the singular values of A | ||
|
||
var # Temporary parameter values | ||
# Condition for a float to be considered 0 | ||
rcond = epsilon(T) * a.shape.max.T * a.max | ||
lwork = max(1, 12 * m + 2 * m * smlsiz + 8 * m * nlvl + m * nrhs + (smlsiz + 1) ^ 2) | ||
work = newSeqUninitialized[T](lwork) | ||
iwork = newSeqUninitialized[cint](liwork) | ||
info, rank: cint | ||
|
||
# Solve the equations A*X = B | ||
gelsd( | ||
m.addr, n.addr, nrhs.addr, | ||
a.get_data_ptr, m.addr, # lda | ||
bstar.get_data_ptr, ldb.addr, | ||
result.singular_values[0].addr, rcond.addr, rank.addr, | ||
work[0].addr, lwork.addr, | ||
iwork[0].addr, info.addr | ||
) | ||
|
||
if info > 0: | ||
# TODO, this should not be an exception, not converging is something that can happen and should | ||
# not interrupt the program. Alternative. Fill the result with Inf? | ||
raise newException(ValueError, "the algorithm for computing the SVD failed to converge") | ||
|
||
if info < 0: | ||
raise newException(ValueError, "Illegal parameter in linear square solver gelsd: " & $(-info)) | ||
|
||
result.matrix_rank = rank.int # This is the matrix_rank not the tensor rank | ||
result.least_square_sol = bstar[0 ..< n, _].squeeze(axis = 1) # Correction for 1d case | ||
|
||
if rank == n and m > n: | ||
result.residuals = (bstar[n .. _, _].squeeze(axis = 1)).fold_axis_inline(Tensor[T], fold_axis = 0): | ||
x = y .^ 2f # initial value | ||
do: | ||
x += y .^ 2f # core loop | ||
do: | ||
x += y # values were stored in a temporary array of size == nb of cores | ||
# to avoid multithreading issues and must be reduced one last time | ||
else: | ||
result.residuals = newTensorUninit[T](0) | ||
# Workaround as we can't create empty tensors for now: | ||
result.residuals.shape[0] = 0 | ||
result.residuals.shape.len = 0 | ||
result.residuals.strides[0] = 0 | ||
result.residuals.shape.len = 0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# Copyright (c) 2018 the Arraymancer contributors | ||
# Distributed under the Apache v2 License (license terms are at http://www.apache.org/licenses/LICENSE-2.0). | ||
# This file may not be copied, modified, or distributed except according to those terms. | ||
|
||
import ./least_squares | ||
|
||
export least_squares |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
# Copyright (c) 2018 the Arraymancer contributors | ||
# Distributed under the Apache v2 License (license terms are at http://www.apache.org/licenses/LICENSE-2.0). | ||
|
||
import ../../src/arraymancer | ||
import unittest, math, fenv | ||
|
||
suite "Linear algebra": | ||
test "Linear equation solver using least squares": | ||
block: # "Single equation" | ||
# Example from Numpy documentation | ||
let A = [ | ||
[0f, 1f], | ||
[1f, 1f], | ||
[2f, 1f], | ||
[3f, 1f] | ||
].toTensor | ||
|
||
let y = [-1f, 0.2, 0.9, 2.1].toTensor | ||
|
||
let (solution, residuals, matrix_rank, singular_values) = least_squares_solver(A, y) | ||
|
||
# From Numpy with double precision and `lstsq` function | ||
let | ||
expected_sol = [1f, -0.95f].toTensor | ||
expected_residuals = [0.05f].toTensor | ||
expected_matrix_rank = 2 | ||
expected_sv = [ 4.10003045f, 1.09075677].toTensor | ||
|
||
check: | ||
mean_relative_error(solution, expected_sol) < 1e-6 | ||
mean_relative_error(residuals, expected_residuals) < 1e-6 | ||
matrix_rank == expected_matrix_rank | ||
mean_relative_error(singular_values, expected_sv) < 1e-6 | ||
|
||
block: # Example from Eigen | ||
# https://eigen.tuxfamily.org/dox/group__LeastSquares.html | ||
let A = [[ 0.68 , 0.597], | ||
[-0.211, 0.823], | ||
[ 0.566, -0.605]].toTensor | ||
let b = [-0.33, 0.536, -0.444].toTensor | ||
|
||
let (solution, _, _, _) = least_squares_solver(A, b) | ||
let expected_sol = [-0.67, 0.314].toTensor | ||
|
||
check: mean_relative_error(solution, expected_sol) < 1e-3 | ||
|
||
block: # "Multiple independant equations" | ||
# Example from Intel documentation: | ||
# https://software.intel.com/sites/products/documentation/doclib/mkl_sa/11/mkl_lapack_examples/dgelsd_ex.c.htm | ||
let a = [ | ||
[ 0.12, -8.19, 7.69, -2.26, -4.71], | ||
[-6.91, 2.22, -5.12, -9.08, 9.96], | ||
[-3.33, -8.94, -6.72, -4.40, -9.98], | ||
[ 3.97, 3.33, -2.74, -7.92, -3.20] | ||
].toTensor | ||
|
||
let b = [ | ||
[ 7.30, 0.47, -6.28], | ||
[ 1.33, 6.58, -3.42], | ||
[ 2.68, -1.71, 3.46], | ||
[-9.62, -0.79, 0.41] | ||
].toTensor | ||
|
||
let (solution, residuals, matrix_rank, singular_values) = least_squares_solver(a, b) | ||
|
||
# From Intel minimum norm solution | ||
let expected_sol = [[-0.69, -0.24, 0.06], | ||
[-0.80, -0.08, 0.21], | ||
[ 0.38, 0.12, -0.65], | ||
[ 0.29, -0.24, 0.42], | ||
[ 0.29, 0.35, -0.30]].toTensor | ||
# No residuals | ||
let expected_matrix_rank = 4 | ||
let expected_sv = [ 18.66, 15.99, 10.01, 8.51].toTensor | ||
|
||
check: | ||
mean_relative_error(solution, expected_sol) < 0.015 | ||
residuals.rank == 0 and residuals.shape[0] == 0 and residuals.strides[0] == 0 | ||
matrix_rank == expected_matrix_rank | ||
mean_relative_error(singular_values, expected_sv) < 1e-03 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
|
||
import ../../src/arraymancer | ||
|
||
block: | ||
let a = [ | ||
[ 0.12f, -8.19, 7.69, -2.26, -4.71], | ||
[-6.91f, 2.22, -5.12, -9.08, 9.96], | ||
[-3.33f, -8.94, -6.72, -4.40, -9.98], | ||
[ 3.97f, 3.33, -2.74, -7.92, -3.20] | ||
].toTensor | ||
|
||
let b = [ | ||
[ 7.30f, 0.47f, -6.28f], | ||
[ 1.33f, 6.58f, -3.42f], | ||
[ 2.68f, -1.71f, 3.46f], | ||
[-9.62f, -0.79f, 0.41f] | ||
].toTensor | ||
|
||
let sol = least_squares_solver(a, b) | ||
echo sol | ||
|
||
block: | ||
let A = [ | ||
[0f, 1f], | ||
[1f, 1f], | ||
[2f, 1f], | ||
[3f, 1f] | ||
].toTensor | ||
|
||
let y = [-1f, 0.2, 0.9, 2.1].toTensor | ||
|
||
let sol = least_squares_solver(A, y) | ||
echo sol |
Oops, something went wrong.