计算矩阵的SVD的简单算法是什么?
理想情况下,我希望使用数值上健壮的算法,但我希望同时看到简单和不太简单的实现。接受C代码。
对文件或代码有任何参考吗?
计算矩阵的SVD的简单算法是什么?
理想情况下,我希望使用数值上健壮的算法,但我希望同时看到简单和不太简单的实现。接受C代码。
对文件或代码有任何参考吗?
Answers:
参见/math/861674/decompose-a-2d-arbitrary-transform-into-only-scaling-and-rotation(对不起,我会在评论中输入,但我已经注册了只是发布此内容,这样我还不能发表评论)。
但是,由于我将其作为答案来编写,因此我还将编写该方法:
分解矩阵如下:
此方法唯一需要注意的是atan2的或。我怀疑它会比这更强大(更新:请参见Alex Eftimiades的答案!)。
参考是:http : //dx.doi.org/10.1109/38.486688(由Rahul提供),来自此博客文章的底部:http : //metamerist.blogspot.com/2006/10/linear-algebra -for-graphics-geeks-svd.html
更新: @VictorLiu在评论中指出,可能为负。当且仅当输入矩阵的行列式也为负时,才会发生这种情况。如果是这样,并且您想要正的奇异值,则只需取的绝对值即可。
@佩德罗·吉梅诺
“我怀疑它会比这更强大。”
接受挑战。
我注意到通常的方法是使用像atan2这样的trig函数。直观上,不需要使用trig函数。实际上,所有结果最终都是反正弦的正弦和余弦,可以将其简化为代数函数。花了相当长的时间,但我设法简化了Pedro的算法,只使用代数函数。
以下python代码可以解决问题。
从numpy import asarray,diagdef svd2(m):
y1, x1 = (m[1, 0] + m[0, 1]), (m[0, 0] - m[1, 1]) y2, x2 = (m[1, 0] - m[0, 1]), (m[0, 0] + m[1, 1]) h1 = hypot(y1, x1) h2 = hypot(y2, x2) t1 = x1 / h1 t2 = x2 / h2 cc = sqrt((1 + t1) * (1 + t2)) ss = sqrt((1 - t1) * (1 - t2)) cs = sqrt((1 + t1) * (1 - t2)) sc = sqrt((1 - t1) * (1 + t2)) c1, s1 = (cc - ss) / 2, (sc + cs) / 2, u1 = asarray([[c1, -s1], [s1, c1]]) d = asarray([(h1 + h2) / 2, (h1 - h2) / 2]) sigma = diag(d) if h1 != h2: u2 = diag(1 / d).dot(u1.T).dot(m) else: u2 = diag([1 / d[0], 0]).dot(u1.T).dot(m) return u1, sigma, u2
y1
= 0,x1
= 0,h1
= 0和t1
= 0/0 = NaN
。
的GSL具有2乘2 SVD解算器的主要SVD算法的QR分解部底层gsl_linalg_SV_decomp
。查看svdstep.c
文件并查找svd2
功能。该函数有一些特殊情况,并非完全无关紧要,并且看起来在做一些事情在数字上要小心(例如,使用hypot
以避免溢出)。
ChangeLog
如果您下载的是GSL ,则文件中有些内容。您可以查看svd.c
整个算法的详细信息。唯一真正的文档似乎是关于高级用户可调用函数的,例如gsl_linalg_SV_decomp
。
当我们说“数值鲁棒性”时,通常指的是一种算法,其中我们进行诸如枢转之类的操作以避免错误传播。但是,对于2x2矩阵,您可以按照显式公式写下结果-即写下SVD元素的公式,这些公式仅根据输入而不是先前计算的中间值来陈述结果。这意味着您可能有取消但没有错误传播。
重点仅在于对于2x2系统,不必担心鲁棒性。
该代码基于Blinn的论文,Ellis的论文,SVD讲座和其他 计算。一种算法适用于规则和奇异实数矩阵。所有以前的版本都可以与此版本100%兼容。
#include <stdio.h>
#include <math.h>
void svd22(const double a[4], double u[4], double s[2], double v[4]) {
s[0] = (sqrt(pow(a[0] - a[3], 2) + pow(a[1] + a[2], 2)) + sqrt(pow(a[0] + a[3], 2) + pow(a[1] - a[2], 2))) / 2;
s[1] = fabs(s[0] - sqrt(pow(a[0] - a[3], 2) + pow(a[1] + a[2], 2)));
v[2] = (s[0] > s[1]) ? sin((atan2(2 * (a[0] * a[1] + a[2] * a[3]), a[0] * a[0] - a[1] * a[1] + a[2] * a[2] - a[3] * a[3])) / 2) : 0;
v[0] = sqrt(1 - v[2] * v[2]);
v[1] = -v[2];
v[3] = v[0];
u[0] = (s[0] != 0) ? (a[0] * v[0] + a[1] * v[2]) / s[0] : 1;
u[2] = (s[0] != 0) ? (a[2] * v[0] + a[3] * v[2]) / s[0] : 0;
u[1] = (s[1] != 0) ? (a[0] * v[1] + a[1] * v[3]) / s[1] : -u[2];
u[3] = (s[1] != 0) ? (a[2] * v[1] + a[3] * v[3]) / s[1] : u[0];
}
int main() {
double a[4] = {1, 2, 3, 6}, u[4], s[2], v[4];
svd22(a, u, s, v);
printf("Matrix A:\n%f %f\n%f %f\n\n", a[0], a[1], a[2], a[3]);
printf("Matrix U:\n%f %f\n%f %f\n\n", u[0], u[1], u[2], u[3]);
printf("Matrix S:\n%f %f\n%f %f\n\n", s[0], 0, 0, s[1]);
printf("Matrix V:\n%f %f\n%f %f\n\n", v[0], v[1], v[2], v[3]);
}
我需要一个算法
我们要计算和,如下所示:
,可以扩展为:
主要思想是找到对角化的旋转矩阵,即是对角线。
回顾
(因为是正交的)
将双方都乘以我们得到
由于是对角线,设置到会给我们,这意味着是一个旋转矩阵,是对角矩阵,是一个旋转矩阵和,正是我们正在寻找对于。
可以通过求解以下方程式来计算对角线旋转:
哪里
和是角的正切值。这可以通过扩展并使它的非对角元素等于零(它们彼此相等)来得出。
该方法的问题在于,由于计算中的减法,在为某些矩阵计算和时,它将失去显着的浮点精度。此解决方案是做一个RQ分解(,上三角和正交)第一,然后使用算法因式分解。这使。请注意,将设置为0(如)是如何消除某些加法/减法的。(RQ分解从矩阵乘积的扩展来看是微不足道的)。
以这种方式天真的实现的算法存在一些数值和逻辑异常(例如或),我在下面的代码中对此进行了修复。
我在该代码上抛出了约2000万个随机矩阵,并且产生的最大数值误差约为(具有32位浮点数,)。该算法运行大约340个时钟周期(MSVC 19,Ivy Bridge)。
template <class T>
void Rq2x2Helper(const Matrix<T, 2, 2>& A, T& x, T& y, T& z, T& c2, T& s2) {
T a = A(0, 0);
T b = A(0, 1);
T c = A(1, 0);
T d = A(1, 1);
if (c == 0) {
x = a;
y = b;
z = d;
c2 = 1;
s2 = 0;
return;
}
T maxden = std::max(abs(c), abs(d));
T rcmaxden = 1/maxden;
c *= rcmaxden;
d *= rcmaxden;
T den = 1/sqrt(c*c + d*d);
T numx = (-b*c + a*d);
T numy = (a*c + b*d);
x = numx * den;
y = numy * den;
z = maxden/den;
s2 = -c * den;
c2 = d * den;
}
template <class T>
void Svd2x2Helper(const Matrix<T, 2, 2>& A, T& c1, T& s1, T& c2, T& s2, T& d1, T& d2) {
// Calculate RQ decomposition of A
T x, y, z;
Rq2x2Helper(A, x, y, z, c2, s2);
// Calculate tangent of rotation on R[x,y;0,z] to diagonalize R^T*R
T scaler = T(1)/std::max(abs(x), abs(y));
T x_ = x*scaler, y_ = y*scaler, z_ = z*scaler;
T numer = ((z_-x_)*(z_+x_)) + y_*y_;
T gamma = x_*y_;
gamma = numer == 0 ? 1 : gamma;
T zeta = numer/gamma;
T t = 2*impl::sign_nonzero(zeta)/(abs(zeta) + sqrt(zeta*zeta+4));
// Calculate sines and cosines
c1 = T(1) / sqrt(T(1) + t*t);
s1 = c1*t;
// Calculate U*S = R*R(c1,s1)
T usa = c1*x - s1*y;
T usb = s1*x + c1*y;
T usc = -s1*z;
T usd = c1*z;
// Update V = R(c1,s1)^T*Q
t = c1*c2 + s1*s2;
s2 = c2*s1 - c1*s2;
c2 = t;
// Separate U and S
d1 = std::hypot(usa, usc);
d2 = std::hypot(usb, usd);
T dmax = std::max(d1, d2);
T usmax1 = d2 > d1 ? usd : usa;
T usmax2 = d2 > d1 ? usb : -usc;
T signd1 = impl::sign_nonzero(x*z);
dmax *= d2 > d1 ? signd1 : 1;
d2 *= signd1;
T rcpdmax = 1/dmax;
c1 = dmax != T(0) ? usmax1 * rcpdmax : T(1);
s1 = dmax != T(0) ? usmax2 * rcpdmax : T(0);
}
想法来自:http:
//www.cs.utexas.edu/users/inderjit/public_papers/HLA_SVD.pdf
http://www.math.pitt.edu/~sussmanm/2071Spring08/lab09/index.html
http:// www.lucidarme.me/singular-value-decomposition-of-a-2x2-matrix/
我使用http://www.lucidarme.me/?p=4624上的描述来创建此C ++代码。矩阵是本征库的矩阵,但是您可以从此示例轻松创建自己的数据结构:
#include <cmath>
#include <Eigen/Core>
using namespace Eigen;
Matrix2d A;
// ... fill A
double a = A(0,0);
double b = A(0,1);
double c = A(1,0);
double d = A(1,1);
double Theta = 0.5 * atan2(2*a*c + 2*b*d,
a*a + b*b - c*c - d*d);
// calculate U
Matrix2d U;
U << cos(Theta), -sin(Theta), sin(Theta), cos(Theta);
double Phi = 0.5 * atan2(2*a*b + 2*c*d,
a*a - b*b + c*c - d*d);
double s11 = ( a*cos(Theta) + c*sin(Theta))*cos(Phi) +
( b*cos(Theta) + d*sin(Theta))*sin(Phi);
double s22 = ( a*sin(Theta) - c*cos(Theta))*sin(Phi) +
(-b*sin(Theta) + d*cos(Theta))*cos(Phi);
// calculate S
S1 = a*a + b*b + c*c + d*d;
S2 = sqrt(pow(a*a + b*b - c*c - d*d, 2) + 4*pow(a*c + b*d, 2));
Matrix2d Sigma;
Sigma << sqrt((S1+S2) / 2), 0, 0, sqrt((S1-S2) / 2);
// calculate V
Matrix2d V;
V << signum(s11)*cos(Phi), -signum(s22)*sin(Phi),
signum(s11)*sin(Phi), signum(s22)*cos(Phi);
具有标准标志功能
double signum(double value)
{
if(value > 0)
return 1;
else if(value < 0)
return -1;
else
return 0;
}
结果得出的值与完全相同Eigen::JacobiSVD
(请参见https://eigen.tuxfamily.org/dox-devel/classEigen_1_1JacobiSVD.html)。
S2 = hypot( a*a + b*b - c*c - d*d, 2*(a*c + b*d))
我在这里有2x2真正SVD的纯C代码。参见559行。它本质上是通过求解二次函数来计算的特征值,因此它不一定是最健壮的,但对于不太病理的情况,它似乎在实践中效果很好。这比较简单。
出于我的个人需求,我尝试隔离2x2 svd的最小计算量。我想这可能是最简单,最快的解决方案之一。您可以在我的个人博客中找到详细信息:http : //lucidarme.me/?p=4624。
优点:简单,快速,并且如果不需要这三个矩阵,则只能计算三个或三个矩阵(S,U或D)中的一个或两个。
缺点是使用atan2,后者可能不准确,可能需要外部库(典型的math.h)。
这是2x2 SVD解决方案的实现。我基于Victor Liu的代码。他的代码不适用于某些矩阵。我将这两个文档用作求解的数学参考:pdf1和pdf2。
矩阵setData
方法按行优先。在内部,我将矩阵数据表示为给出的2D数组data[col][row]
。
void Matrix2f::svd(Matrix2f* w, Vector2f* e, Matrix2f* v) const{
//If it is diagonal, SVD is trivial
if (fabs(data[0][1] - data[1][0]) < EPSILON && fabs(data[0][1]) < EPSILON){
w->setData(data[0][0] < 0 ? -1 : 1, 0, 0, data[1][1] < 0 ? -1 : 1);
e->setData(fabs(data[0][0]), fabs(data[1][1]));
v->loadIdentity();
}
//Otherwise, we need to compute A^T*A
else{
float j = data[0][0]*data[0][0] + data[0][1]*data[0][1],
k = data[1][0]*data[1][0] + data[1][1]*data[1][1],
v_c = data[0][0]*data[1][0] + data[0][1]*data[1][1];
//Check to see if A^T*A is diagonal
if (fabs(v_c) < EPSILON){
float s1 = sqrt(j),
s2 = fabs(j-k) < EPSILON ? s1 : sqrt(k);
e->setData(s1, s2);
v->loadIdentity();
w->setData(
data[0][0]/s1, data[1][0]/s2,
data[0][1]/s1, data[1][1]/s2
);
}
//Otherwise, solve quadratic for eigenvalues
else{
float jmk = j-k,
jpk = j+k,
root = sqrt(jmk*jmk + 4*v_c*v_c),
eig = (jpk+root)/2,
s1 = sqrt(eig),
s2 = fabs(root) < EPSILON ? s1 : sqrt((jpk-root)/2);
e->setData(s1, s2);
//Use eigenvectors of A^T*A as V
float v_s = eig-j,
len = sqrt(v_s*v_s + v_c*v_c);
v_c /= len;
v_s /= len;
v->setData(v_c, -v_s, v_s, v_c);
//Compute w matrix as Av/s
w->setData(
(data[0][0]*v_c + data[1][0]*v_s)/s1,
(data[1][0]*v_c - data[0][0]*v_s)/s2,
(data[0][1]*v_c + data[1][1]*v_s)/s1,
(data[1][1]*v_c - data[0][1]*v_s)/s2
);
}
}
}