-
Notifications
You must be signed in to change notification settings - Fork 84
Add PInv Moore-Penrose pseudoinverse #292
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
Comments
You could use Based on Wikipedia, given
It's also useful to note a couple of things:
Here's an implementation with the steps (a) compute if let (Some(u), s, Some(v_h)) = result {
// Determine how many singular values to keep and compute the
// values of `Σ⁺ U^H` (up to `num_keep` rows).
let (num_keep, s_inv_u_h) = {
let mut u_t = u.reversed_axes();
let mut num_keep = 0;
for (&sing_val, mut u_t_row) in s.iter().zip(u_t.rows_mut()) {
if sing_val > threshold {
let sing_val_recip = sing_val.recip();
u_t_row.map_inplace(|u_t| *u_t = A::from_real(sing_val_recip) * u_t.conj());
num_keep += 1;
} else {
break;
}
}
u_t.slice_axis_inplace(Axis(0), Slice::from(..num_keep));
(num_keep, u_t)
};
// Compute `V` (up to `num_keep` columns).
let v = {
let mut v_h_t = v_h.reversed_axes();
v_h_t.slice_axis_inplace(Axis(1), Slice::from(..num_keep));
v_h_t.map_inplace(|x| *x = x.conj());
v_h_t
};
Ok(v.dot(&s_inv_u_h))
} else {
...
} Here's another implementation with the steps (a) compute if let (Some(u), s, Some(v_h)) = result {
// Determine how many singular values to keep and compute the
// values of `V Σ⁺` (up to `num_keep` columns).
let (num_keep, v_s_inv) = {
let mut v_h_t = v_h.reversed_axes();
let mut num_keep = 0;
for (&sing_val, mut v_h_t_col) in s.iter().zip(v_h_t.columns_mut()) {
if sing_val > threshold {
let sing_val_recip = sing_val.recip();
v_h_t_col.map_inplace(|v_h_t| *v_h_t = A::from_real(sing_val_recip) * v_h_t.conj();
num_keep += 1;
} else {
break;
}
}
v_h_t.slice_axis_inplace(Axis(1), Slice::from(..num_keep));
(num_keep, v_h_t)
};
// Compute `U^H` (up to `num_keep` rows).
let u_h = {
let mut u_t = u.reversed_axes();
u_t.slice_axis_inplace(Axis(0), Slice::from(..num_keep));
u_t.map_inplace(|x| *x = x.conj());
u_t
};
Ok(v_s_inv.dot(&u_h))
} else {
...
} Please test these before using them. I haven't verified their correctness, and they may have bugs. |
Thank you for doing this @jturner314 this is awesome. I really like the optimization with the break after the first singular value below the threshold. Is this something we could get integrated into the ndarray-linalg (if I clean it up and make a pull request)? I don't have time tonight but if there is interest in integrating it I will work on it over the weekend. |
Yes, this seems useful to include in Although not strictly necessary, it seems useful to have a method which uses a heuristic to choose the threshold value rather than requiring the user to manually specify a threshold. Wikipedia says, "For example, in the MATLAB, GNU Octave, or NumPy function pinv, the tolerance is taken to be t = ε⋅max(m, n)⋅max(Σ), where ε is the machine epsilon." |
I am working through this, I noticed octave[1] calculates the threshold using ε⋅max(m, n)⋅max(Σ) but NumPy[2] just does rcond⋅max(Σ) where rcond is an argument that defaults to 1e-15, which is about the same as ε for a f64 with a maximum dimension of 10. |
Huh, that's interesting. NumPy's approach of multiplying a user-specified parameter by max(Σ) seems like a good choice. The user could specify rcond = ε⋅max(m, n) to reproduce Octave's behavior. Another option would be to multiply a user-specified parameter by max(m, n)⋅max(Σ). I haven't used pseudoinverses myself, so I'm not familiar enough with the area to have a strong opinion. |
I was thinking of having an optional argument rcond like NumPy that defaults to ε⋅max(m, n) if its not specified but I would really like to know where the 1e-15 came from in NumPy. let rcond = rcond.unwrap_or_else(|| {
let (n, m) = self.dim();
Self::E::epsilon() * Self::E::real(n.max(m))
});
let threshold = rcond * s[0]; |
The plot thickens, numpy.linalg.matrix_rank uses the threshold ε⋅max(m, n)⋅max(Σ) and references W. H. Press, S. A. Teukolsky, W. T. Vetterling and B. P. Flannery, Which says
and Just to add more confusion there is ε⋅0.5*sqrt(m+n+1.)⋅max(Σ) On page 71 The numpy.linalg.matrix_rank documentation also stated that an absolute value might be appropriate in some cases
But I think its probably better not to support this unless there is a compelling need. |
I have implemented tests for various ranks for matrix but it seems like there must be a better way to create them. |
Sorry for bumping, but what is the status of this? |
I was porting some code from NumPy, and needed the pinv[1][2] function (Moore-Penrose pseudoinverse). I have started implementing it but have run into some issues making it generic over real and complex numbers.
At the moment I am getting this error
My understanding this is because I am trying to multiple Scalar::Real with a Scalar, but I am not sure how this should be fixed.
The text was updated successfully, but these errors were encountered: