Skip to content

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

Merged
merged 2 commits into from
Sep 20, 2018
Merged

Implement af::pinverse() #2279

merged 2 commits into from
Sep 20, 2018

Conversation

mark-poscablo
Copy link
Contributor

@mark-poscablo mark-poscablo commented Aug 14, 2018

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 in src/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).

@mark-poscablo mark-poscablo requested a review from umar456 August 14, 2018 19:15
// 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]) {
Copy link
Contributor Author

@mark-poscablo mark-poscablo Aug 14, 2018

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.

@mark-poscablo
Copy link
Contributor Author

mark-poscablo commented Aug 14, 2018

I've been benchmarking this vs just af::svd() and af::svdInPlace(), and I'm thinking of adding an in-place option for pinverse() too, especially since af::svdInPlace() is actually really fast on CUDA (this could be on another PR though). However, it has a couple of issues - I'll write about it in a separate issue.

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));
Copy link
Contributor

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(Σ)

Copy link
Contributor Author

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.

}
if (uT.dims()[0] > sPinvCast.dims()[1]) {
std::vector<af_seq> seqs = {
{0., static_cast<double>(sPinvCast.dims()[1]), 1.},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inclusive! Make exclusive.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -200,6 +200,25 @@ namespace af
*/
AFAPI array inverse(const array &in, const matProp options = AF_MAT_NONE);

/**
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this now

#include <svd.hpp>
#include <transpose.hpp>

using af::dim4;
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this now too

}

double validTol = tol;
if (validTol < 0.) {
Copy link
Contributor

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...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@pavanky
Copy link
Member

pavanky commented Aug 25, 2018

@mark-poscablo @syurkevi Try to implement this for non-square matrices inside inverse rather than adding a new function.

@umar456
Copy link
Member

umar456 commented Aug 25, 2018

@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.

@mark-poscablo
Copy link
Contributor Author

mark-poscablo commented Aug 27, 2018

@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 af::inverse that decides whether to allow fallback to pseudoinverse or not, setting it default to false to keep the original API's behavior. Reason is that the user might still want to be alerted that the input matrix isn't square, versus quietly selecting pseudoinverse if that's the case. At least with that extra parameter, the user will have to intentionally set it to true if they really want that quiet selection of pseudoinverse.

Copy link
Member

@umar456 umar456 left a 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.


\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**.
Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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.

\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.
Copy link
Member

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.

Copy link
Contributor Author

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)

@@ -0,0 +1,170 @@
/*******************************************************
* Copyright (c) 2014, ArrayFire
Copy link
Member

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

Copy link
Contributor Author

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

Array<T> pinverseSvd(const Array<T> &in, const double tol)
{
// Moore-Penrose Pseudoinverse

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Extra line

Copy link
Contributor Author

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?

Copy link
Member

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

}

ARG_ASSERT(1, i_info.isFloating()); // Only floating and complex types
ARG_ASSERT(2, tol >= 0.); // Only floating and complex types
Copy link
Member

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.

Copy link
Contributor Author

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 Moore-Penrose conditions
// See https://en.wikipedia.org/wiki/Moore%E2%80%93Penrose_inverse#Definition

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Extra line

Copy link
Contributor Author

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


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**).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of making the M x N x P statement bold perhaps use \f$M \times N \times P$\f.

Copy link
Contributor Author

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.

\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
Copy link
Member

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

Copy link
Contributor Author

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.


\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
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup just changed it.

{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);
Copy link
Member

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

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) {
Copy link
Member

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.

@umar456 umar456 merged commit 6fc326f into arrayfire:master Sep 20, 2018
umar456 pushed a commit that referenced this pull request Sep 20, 2018
umar456 pushed a commit to umar456/arrayfire that referenced this pull request Nov 2, 2018
…rrectly

write into subarrays (arrayfire#2279)

(cherry picked from commit 6fc326f)
umar456 pushed a commit to umar456/arrayfire that referenced this pull request Nov 2, 2018
…rrectly

write into subarrays (arrayfire#2279)

(cherry picked from commit 6fc326f)
umar456 pushed a commit to umar456/arrayfire that referenced this pull request Nov 3, 2018
…rrectly

write into subarrays (arrayfire#2279)

(cherry picked from commit 6fc326f)
9prady9 pushed a commit that referenced this pull request Nov 3, 2018
…rrectly

write into subarrays (#2279)

(cherry picked from commit 6fc326f)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants