SVD的鲁棒算法


26

计算矩阵的SVD的简单算法是什么?2×2

理想情况下,我希望使用数值上健壮的算法,但我希望同时看到简单和不太简单的实现。接受C代码。

对文件或代码有任何参考吗?


5
Wikipedia列出了2x2封闭形式的解决方案,但是我不知道其数值属性。
Damien

作为参考,“数字食谱”,Press等,剑桥出版社。非常昂贵的书,但值得每一分。除了SVD解决方案之外,您还会发现很多其他有用的算法。
Jan Hackenberg

Answers:


19

参见/math/861674/decompose-a-2d-arbitrary-transform-into-only-scaling-and-rotation(对不起,我会在评论中输入,但我已经注册了只是发布此内容,这样我还不能发表评论)。

但是,由于我将其作为答案来编写,因此我还将编写该方法:

E=m00+m112;F=m00m112;G=m10+m012;H=m10m012Q=E2+H2;R=F2+G2sx=Q+R;sy=QRa1=atan2(G,F);a2=atan2(H,E)θ=a2a12;ϕ=a2+a12

分解矩阵如下:

M=(m00m01m10m11)=(cosϕsinϕsinϕcosϕ)(sx00sy)(cosθsinθsinθcosθ)

此方法唯一需要注意的是atan2的或。G=F=0H=E=0我怀疑它会比这更强大更新:请参见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在评论中指出,可能为负。当且仅当输入矩阵的行列式也为负时,才会发生这种情况。如果是这样,并且您想要正的奇异值,则只需取的绝对值即可。sysy


1
如果,似乎可以为负。这应该是不可能的。 Q < řsyQ<R
维克多·刘

@VictorLiu如果输入矩阵翻转,则唯一可以反映的地方是缩放矩阵,因为旋转矩阵不可能翻转。只是不给它输入翻转的输入矩阵。我还没有完成数学运算,但是我敢打赌,输入矩阵行列式的符号将确定还是更大。[RQR
Pedro Gimeno 2014年

@VictorLiu我现在已经完成了数学运算,并且确认确实,简化为即输入矩阵的行列式。m 00 m 11m 01 m 10Q2R2m00m11m01m10
Pedro Gimeno 2014年

9

@佩德罗·吉梅诺

“我怀疑它会比这更强大。”

接受挑战。

我注意到通常的方法是使用像atan2这样的trig函数。直观上,不需要使用trig函数。实际上,所有结果最终都是反正弦的正弦和余弦,可以将其简化为代数函数。花了相当长的时间,但我设法简化了Pedro的算法,只使用代数函数。

以下python代码可以解决问题。

从numpy import asarray,diag

def 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


1
该代码似乎不正确。考虑2x2恒等矩阵。然后y1= 0,x1= 0,h1= 0和t1= 0/0 = NaN
Hugues

8

GSL具有2乘2 SVD解算器的主要SVD算法的QR分解部底层gsl_linalg_SV_decomp。查看svdstep.c文件并查找svd2功能。该函数有一些特殊情况,并非完全无关紧要,并且看起来在做一些事情在数字上要小心(例如,使用hypot以避免溢出)。


1
此功能有任何文档吗?我想知道它的输入参数是什么。
维克多·刘

@VictorLiu:可悲的是,除了文件本身微不足道的注释之外,我什么都没有看到。ChangeLog如果您下载的是GSL ,则文件中有些内容。您可以查看svd.c整个算法的详细信息。唯一真正的文档似乎是关于高级用户可调用函数的,例如gsl_linalg_SV_decomp
霍希勒2013年

7

当我们说“数值鲁棒性”时,通常指的是一种算法,其中我们进行诸如枢转之类的操作以避免错误传播。但是,对于2x2矩阵,您可以按照显式公式写下结果-即写下SVD元素的公式,这些公式仅根据输入而不是先前计算中间值来陈述结果。这意味着您可能有取消但没有错误传播。

重点仅在于对于2x2系统,不必担心鲁棒性。


它可以取决于矩阵。我看过一种方法,该方法可以分别找到左角和右角(每个通过arctan2(y,x)),通常效果很好。但是,当奇异值彼此接近时,这些反正切趋于0/0,因此结果可能不准确。在佩德罗·吉梅诺(Pedro Gimeno)给出的方法中,这种情况下a2的计算将得到很好的定义,而a1的定义将变得不明确;您仍然会得到很好的结果,因为当s.val靠近时,分解的有效性仅对theta + phi敏感,而对theta-phi不敏感。
greggo

5

该代码基于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]);
}

5

我需要一个算法

  • 小分支(希望是CMOV)
  • 没有三角函数调用
  • 即使使用32位浮点数也具有很高的数值精度

我们要计算和,如下所示:c1,s1,c2,s2,σ1σ2

A=USV,可以扩展为:

[abcd]=[c1s1s1c1][σ100σ2][c2s2s2c2]

主要思想是找到对角化的旋转矩阵,即是对角线。VATAVATAVT=D

回顾

USV=A

US=AV1=AVT(因为是正交的)V

VATAVT=(AVT)TAVT=(US)TUS=STUTUS=D

将双方都乘以我们得到S1

(STST)UTU(SS1)=UTU=STDS1

由于是对角线,设置到会给我们,这意味着是一个旋转矩阵,是对角矩阵,是一个旋转矩阵和,正是我们正在寻找对于。DSDUTU=IdentityUSVUSV=A

可以通过求解以下方程式来计算对角线旋转:

t22βαγt21=0

哪里

ATA=[acbd][abcd]=[a2+c2ab+cdab+cdb2+d2]=[αγγβ]

和是角的正切值。这可以通过扩展并使它的非对角元素等于零(它们彼此相等)来得出。t2VVATAVT

该方法的问题在于,由于计算中的减法,在为某些矩阵计算和时,它将失去显着的浮点精度。此解决方案是做一个RQ分解(,上三角和正交)第一,然后使用算法因式分解。这使。请注意,将设置为0(如)是如何消除某些加法/减法的。(RQ分解从矩阵乘积的扩展来看是微不足道的)。βαγA=RQRQUSV=RUSV=USVQ=RQ=AdR

以这种方式天真的实现的算法存在一些数值和逻辑异常(例如或),我在下面的代码中对此进行了修复。S +DD

我在该代码上抛出了约2000万个随机矩阵,并且产生的最大数值误差约为(具有32位浮点数,)。该算法运行大约340个时钟周期(MSVC 19,Ivy Bridge)。6107error=||USVM||/||M||

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/


3

我使用http://www.lucidarme.me/?p=4624上的描述来创建此C ++代码。矩阵是本征库的矩阵,但是您可以从此示例轻松创建自己的数据结构:

A=UΣVT

#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)。


1
S2 = hypot( a*a + b*b - c*c - d*d, 2*(a*c + b*d))
greggo

2

在这里有2x2真正SVD的纯C代码。参见559行。它本质上是通过求解二次函数来计算的特征值,因此它不一定是最健壮的,但对于不太病理的情况,它似乎在实践中效果很好。这比较简单。ATA


我认为矩阵的特征值为负时,您的代码不起作用。尝试[[1 1] [1 0]],并且u * s * vt不等于m ...
Carlos Scheidegger 2014年

2

出于我的个人需求,我尝试隔离2x2 svd的最小计算量。我想这可能是最简单,最快的解决方案之一。您可以在我的个人博客中找到详细信息:http : //lucidarme.me/?p=4624

优点:简单,快速,并且如果不需要这三个矩阵,则只能计算三个或三个矩阵(S,U或D)中的一个或两个。

缺点是使用atan2,后者可能不准确,可能需要外部库(典型的math.h)。


3
由于链接很少是永久的,因此总结方法而不是简单地提供链接作为答案很重要。
保罗

另外,如果您要发布指向自己博客的链接,请(a)披露它是您的博客,(b)最好是总结或剪切粘贴您的方法(公式的图像可以被翻译成原始LaTeX并使用MathJax渲染)。此类问题状态公式的最佳答案,为所述公式提供引用,然后列出诸如缺陷,边缘情况和潜在替代方案之类的内容。
Geoff Oxberry 2014年

1

这是2x2 SVD解决方案的实现。我基于Victor Liu的代码。他的代码不适用于某些矩阵。我将这两个文档用作求解的数学参考:pdf1pdf2

矩阵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
            );
        }
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.