-
Notifications
You must be signed in to change notification settings - Fork 548
Implement af::pinverse() #2279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement af::pinverse() #2279
Conversation
src/api/c/pinverse.cpp
Outdated
// sVec produced by svd() has minimal dim length (no extra zeroes). | ||
// Thus s+ produced by diagCreate() will have minimal dims as well, | ||
// and v could have an extra dim0 or u* could have an extra dim1 | ||
if (v.dims()[1] > sPinvCast.dims()[0]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the way, I'm wondering if this is the fastest implementation of what I'm trying to do in this section. At first, actually, I thought of avoiding creating new arrays (createSubArray()
) since this might be expensive especially when the arrays are big. Instead, a reference to only the needed section of the arrays might be better to use - although this could also have the disadvantage of making the data access pattern jump (and thus induce cache misses) near the cropped-out section of the arrays during matmul()
.
Another way could be padding sVec
with 0's instead to match v and u*'s dims for matmul. This creates a new array once (and a smaller one compared to the other arrays), but will waste multiplication and adding with 0's during matmul.
I've been benchmarking this vs just |
b75a6f4
to
89c357d
Compare
src/api/c/pinverse.cpp
Outdated
Array<T> v = transpose(vT, true); | ||
|
||
// Round down small values to zero to avoid large reciprocals later | ||
Array<Tr> eps = createValueArray<Tr>(sVec.dims(), scalar<Tr>(1e-6)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change tolerance to match t = ε⋅max(m,n)⋅max(Σ)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented that relative tolerance calculation but epsilon will be 1e-6 by default instead of machine epsilon. There's an issue with SVD that prevents me from lowering this for float. I should probably write about that issue, but basically if I SVD a given array, make one of the singular values really small (1e-12 perhaps), multiply them back together, and SVD it again, the really small singular value isn't so small anymore (becomes something on the order of 1e-5). Thus I think our SVD cannot produce really small singular values and so I set the default tolerance that high.
src/api/c/pinverse.cpp
Outdated
} | ||
if (uT.dims()[0] > sPinvCast.dims()[1]) { | ||
std::vector<af_seq> seqs = { | ||
{0., static_cast<double>(sPinvCast.dims()[1]), 1.}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inclusive! Make exclusive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
include/af/lapack.h
Outdated
@@ -200,6 +200,25 @@ namespace af | |||
*/ | |||
AFAPI array inverse(const array &in, const matProp options = AF_MAT_NONE); | |||
|
|||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#if AF_API_VERSION >= 37
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding this now
src/api/c/pinverse.cpp
Outdated
#include <svd.hpp> | ||
#include <transpose.hpp> | ||
|
||
using af::dim4; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
using std::vector
using std::swap
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding this now too
src/api/c/pinverse.cpp
Outdated
} | ||
|
||
double validTol = tol; | ||
if (validTol < 0.) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ARG_ASSERT?
User should probably know their tolerance is bad...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@mark-poscablo @syurkevi Try to implement this for non-square matrices inside inverse rather than adding a new function. |
@pavanky The extra parameters are not necessary for the regular inverse. We can make this part of the regular inverse and pick good defaults so that inverse will also perform a pseudo inverse if the matrices are not square. |
@pavanky I was actually planning to do this at first, but @syurkevi advised me early on that the user most probably wants to explicitly differentiate the pseudoinverse from the regular inverse. But true, as @umar456 said, we could add it as a fallback for regular inverse in case the matrices aren't square - if we do this though I think we should add a boolean parameter to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking great. I have made a couple of comments regarding the documentation and style.
docs/details/lapack.dox
Outdated
|
||
\brief Pseudo-invert a matrix | ||
|
||
This function calculates the Moore-Penrose pseudoinverse of a matrix \f$A\f$, using \ref af::svd at its core. If \f$A\f$ is of size **M x N**, then its pseudoinverse \f$A^+\f$ will be of size **N x M**. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please limit lines to 80 characters per line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup I followed suit on the other functions' existing docs, thinking that maybe doxygen somehow requires everything on a single line or else an unnecessary line break will occur. I'll change it to 80 chars per line though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright it's good, it automatically adds a space for every new line i add on the .dox file.
include/af/lapack.h
Outdated
\param[in] options determining various properties of matrix \p in | ||
\returns \p x, the inverse of the input matrix | ||
|
||
\note \p tol is not the actual lower threshold, but it is passed in as a parameter to the calculation of the actual threshold relative to the shape and contents of \p in. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
80 characters per line. You can tab to the first character of the comment to make it more readable:
\note \p tol is not the actual lower threshold, but it is passed in as a
parameter to the calculation of the actual threshold relative to the
shape and contents of \p in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same case as above for this file, just added line breaks now (to pinverse and af_pinverse)
src/api/c/pinverse.cpp
Outdated
@@ -0,0 +1,170 @@ | |||
/******************************************************* | |||
* Copyright (c) 2014, ArrayFire |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a new file so you should set this to 2018
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, I just changed this one and test/pinverse.cpp
src/api/c/pinverse.cpp
Outdated
Array<T> pinverseSvd(const Array<T> &in, const double tol) | ||
{ | ||
// Moore-Penrose Pseudoinverse | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Extra line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here I intentionally put an extra line because the comment doesn't apply only to the code block right below it, but rather the whole function. Maybe I should put this comment above the signature instead eh?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better to apply that comment to the whole function. Maybe move it above the function
src/api/c/pinverse.cpp
Outdated
} | ||
|
||
ARG_ASSERT(1, i_info.isFloating()); // Only floating and complex types | ||
ARG_ASSERT(2, tol >= 0.); // Only floating and complex types |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment is incorrect here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops copy paste. Just changed the comment.
test/pinverse.cpp
Outdated
|
||
// Test Moore-Penrose conditions | ||
// See https://en.wikipedia.org/wiki/Moore%E2%80%93Penrose_inverse#Definition | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Extra line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same reasoning as in api/c/pinverse.cpp but I just removed the extra line here and clarified that this comment just applies to the first 4 following tests
docs/details/lapack.dox
Outdated
|
||
This function calculates the Moore-Penrose pseudoinverse of a matrix \f$A\f$, using \ref af::svd at its core. If \f$A\f$ is of size **M x N**, then its pseudoinverse \f$A^+\f$ will be of size **N x M**. | ||
|
||
This calculation can be batched if the input array is three-dimensional (**M x N x P**). Each **M x N** slice along the third dimension will have its own pseudoinverse, for a total of **P** pseudoinverses in the output array (**N x M x P**). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup I was on the fence about this one, since other functions' docs (like af::svd) just usually put bold. But it's probably better to be consistent within a doc page. I'll change this.
include/af/lapack.h
Outdated
\param[in] in is the input matrix | ||
\param[in] tol defines the lower threshold for singular values from SVD | ||
\param[in] options determining various properties of matrix \p in | ||
\returns \p x, the inverse of the input matrix |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see an x value in the function signature. just use
\returns the inverse of the input matrix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, let me take it out.
include/af/lapack.h
Outdated
|
||
\param[in] in is the input matrix | ||
\param[in] tol defines the lower threshold for singular values from SVD | ||
\param[in] options determining various properties of matrix \p in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Singular option.
Perhaps state that this must be AF_MAT_NONE
\param[in] option must be AF_MAT_NONE. For future use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup just changed it.
src/api/c/pinverse.cpp
Outdated
{0., static_cast<double>(inArray.dims()[1] - 1), 1.}, | ||
{static_cast<double>(i), static_cast<double>(i), 1.} | ||
}; | ||
Array<T> inSlice = createSubArray<T>(inArray, seqs); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you make sure that this function is indeed making a subarray and not allocating and performing a copy? If it's copying an array then we should move this operation into the svd function and then iterate over the arrays there. You can just create a TODO if we are doing a copy and we can fix it at a later time. I suspect it is working as expected but I just want to make sure
2964698
to
8d1a81c
Compare
1ed87a2
to
d592c34
Compare
…rrectly write into subarrays (arrayfire#2279)
d592c34
to
cc90985
Compare
Array<T> vT = createValueArray<T>(dim4(N, N, P, Q), scalar<T>(0)); | ||
Array<Tr> sVec = createValueArray<Tr>(dim4(min(M, N), 1, P, Q), scalar<Tr>(0)); | ||
for (uint j = 0; j < Q; ++j) { | ||
for (uint i = 0; i < P; ++i) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if it will effect performance, but this double-loop can be merged into a single loop in this case.
…rrectly write into subarrays (arrayfire#2279) (cherry picked from commit 6fc326f)
…rrectly write into subarrays (arrayfire#2279) (cherry picked from commit 6fc326f)
…rrectly write into subarrays (arrayfire#2279) (cherry picked from commit 6fc326f)
SVD-based Moore-Penrose pseudoinverse.
This has the same type restrictions as
af::inverse()
, but lifts the dims restriction of requiring the array to be square. The third dimension is also allowed for batching, although this uses naive batching for now (loop through and successively process each slice along the third dimension). No backend-specific code were added, since all the necessary pieces for the SVD-based approach are already implemented in arrayfire (thus all the code is insrc/api/c/pinverse.cpp
).The tests include checking if all the Moore-Penrose conditions hold (see https://en.wikipedia.org/wiki/Moore%E2%80%93Penrose_inverse#Definition), for all the the appropriate types (f32, f64, c32, c64).
The CI tests might fail initially until the corresponding PR on arrayfire-data is approved and I update the git submodule reference here.
This addresses #2074, #2077, and #2143 (this last one indirectly).