SVD를 위한 강력한 알고리즘


26

2×2 행렬 의 SVD를 계산하는 간단한 알고리즘은 무엇입니까 ?

이상적으로는 수치 적으로 강력한 알고리즘을 원하지만 단순하고 단순하지 않은 구현을 모두보고 싶습니다. C 코드가 허용되었습니다.

논문이나 코드에 대한 언급이 있습니까?


5
Wikipedia 에는 2x2 폐쇄 형 솔루션이 나열되어 있지만 수치 속성은 모릅니다.
Damien

"Numerical Recipes", Press 등, Cambridge Press를 참조한다. 꽤 비싼 책이지만 매 센트의 가치가 있습니다. SVD 솔루션 외에도 다른 유용한 알고리즘이 많이 있습니다.
Jan Hackenberg

답변:


19

https://math.stackexchange.com/questions/861674/decompose-a-2d-arbitrary-transform-into-only-scaling-and-rotation을 참조 하십시오 (죄송합니다, 코멘트에 넣었을 수도 있지만 등록했습니다 댓글을 올릴 수 없으므로이 글을 게시하십시오.)

그러나 답변으로 작성했기 때문에 방법도 작성합니다.

E=m00+m112;F=m00m112;G=m10+m012;H=10012=이자형2+H2;아르 자형=에프2+2에스엑스=+아르 자형;에스와이=아르 자형에이1=에이에이2(,에프);에이2=에이에이2(H,이자형)θ=에이2에이12;ϕ=에이2+에이12

그러면 다음과 같이 행렬이 분해됩니다.

=(00011011)=(코사인ϕϕϕ코사인ϕ)(에스엑스00에스와이)(코사인θθθ코사인θ)

이 방법으로 보호해야 할 유일한 것은 atan2의 경우 =에프=0 또는 H=이자형=0 입니다.나는 그것이 그보다 더 강력 할 수 있는지 의심합니다.( 업데이트 : Alex Eftimiades의 답변 참조!).

기준은 다음과 같습니다 http://dx.doi.org/10.1109/38.486688 이 블로그 게시물의 하단에서 온다 (라훌이 주어진다) http://metamerist.blogspot.com/2006/10/linear-algebra -for-graphics-geeks-svd.html

업데이트 : 의견에 @VictorLiu가 언급했듯이 에스와이 는 음수 일 수 있습니다. 입력 행렬의 행렬식이 음수 인 경우에만 발생합니다. 이 경우 양의 특이 값을 원하면 에스와이 의 절대 값을 사용하십시오 .


1
그 보인다 음의 경우 일 수 Q < R . 불가능합니다. 에스와이<아르 자형
Victor Liu

@VictorLiu 입력 행렬이 뒤집 히면 회전 행렬이 뒤집을 수 없으므로 반영 할 수있는 유일한 위치는 스케일링 행렬입니다. 뒤집는 입력 행렬을 공급하지 마십시오. 나는 아직 수학을하지 않았지만 입력 행렬의 결정 요인의 부호가 또는 R 이 더 큰지를 결정할 것이라고 내기했다 . 아르 자형
Pedro Gimeno

@VictorLiu 이제 수학을 수행했고 실제로 m 00 m 11 - m 01 m 10, 즉 입력 행렬의 결정자를 단순화 함을 확인했습니다 . Q2R2m00m11m01m10
Pedro Gimeno

9

@ 페드로 기 메노

"나는 그것보다 더 강력 할 수 있을지 의심 스럽다."

도전은 받아 들였다.

일반적인 접근 방식은 atan2와 같은 삼각 함수를 사용하는 것입니다. 직관적으로 삼각 함수를 사용할 필요는 없습니다. 실제로 모든 결과는 대수 함수로 단순화 될 수있는 아크 탄의 사인과 코사인으로 끝납니다. 꽤 오랜 시간이 걸렸지 만 대수 함수 만 사용하도록 Pedro의 알고리즘을 단순화했습니다.

다음 파이썬 코드가 트릭을 수행합니다.

numpy import asarray, diag에서

데프 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은 주된 SVD 알고리즘의 QR 분해 부 밑에 솔버 2 바이 2 SVD를 갖는다 gsl_linalg_SV_decomp. 참고 항목 svdstep.c파일을하고 찾는 svd2기능. 이 함수에는 몇 가지 특별한 경우가 있으며 정확하게 사소하지는 않으며 숫자로주의 해야하는 몇 가지 작업을 수행하는 것처럼 보입니다 (예 : hypot오버플로를 피하기 위해 사용 ).


1
이 기능에 문서가 있습니까? 입력 매개 변수가 무엇인지 알고 싶습니다.
Victor Liu

@VictorLiu : 슬프게도 나는 파일 자체에서 빈약 한 의견 외에는 아무것도 보지 못했습니다. ChangeLogGSL을 다운로드하면 파일에 비트가 있습니다 . svd.c전체 알고리즘 에 대한 자세한 내용을 볼 수 있습니다 . 유일한 진정한 문서는 높은 수준의 사용자 호출 가능 기능에 대한 것 같습니다 gsl_linalg_SV_decomp.
horchler

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 비트 플로트에서도 높은 수치 정확도

기음1,에스1,기음2,에스2,σ1σ2

에이=에스V

[에이기음]=[기음1에스1에스1기음1][σ100σ2][기음2에스2에스2기음2]

주요 아이디어는 하는 회전 행렬 를 찾는 것입니다.V에이에이V에이에이V= 는 대각선입니다.

기억해

에스V=에이

에스=에이V1=에이VV

V에이에이V=(에이V)에이V=(에스)에스=에스에스=

에스1

(에스에스)(에스에스1)==에스에스1

에스=나는이자형나는와이에스V에스V=에이 입니다.

대각선 회전 계산은 다음 방정식을 해결하여 수행 할 수 있습니다.

22βαγ21=0

어디에

에이에이=[에이기음][에이기음]=[에이2+기음2에이+기음에이+기음2+2]=[αγγβ]

2 각도의 접선 V. 이것은 확장하여 파생 될 수 있습니다V에이에이V 대각선 이외의 요소를 0으로 만듭니다 (서로 같음).

이 방법의 문제점은 계산할 때 부동 소수점 정밀도가 크게 떨어진다는 것입니다 βαγ계산에서 빼기 때문에 특정 행렬의 경우 이에 대한 해결책은 RQ 분해를 수행하는 것입니다.에이=아르 자형, 아르 자형 상단 삼각형 직교) 먼저 알고리즘을 사용하여 분해 에스V'=아르 자형. 이것은 준다에스V=에스V'=아르 자형=에이. 설정 방법에 주목 ~ 0 아르 자형)는 일부 덧셈 / 뺄셈을 제거합니다. (RQ 분해는 매트릭스 제품의 확장으로 인해 사소한 것입니다).

이 방법으로 순진하게 구현 된 알고리즘에는 수치 적, 논리적 이상이 있습니다 (예 : 에스 + 또는 ), 아래 코드에서 수정했습니다.

코드에서 약 2 억 개의 무작위 행렬을 던졌고 가장 큰 수치 오류는 약 6107 (32 비트 수레로, 이자형아르 자형아르 자형영형아르 자형=||에스V||/||||). 이 알고리즘은 약 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/


3

이 C ++ 코드를 작성하기 위해 http://www.lucidarme.me/?p=4624 의 설명을 사용했습니다 . 행렬은 Eigen 라이브러리의 행렬이지만이 예제에서 고유 한 데이터 구조를 쉽게 만들 수 있습니다.

에이=ΣV

#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 행을 참조하십시오. 기본적으로 고유 값을 계산합니다.에이에이이차 문제를 해결함으로써 반드시 가장 강건한 것은 아니지만 병리학 적이 지 않은 경우에는 실제로 잘 작동하는 것 같습니다. 비교적 간단합니다.


행렬의 고유 값이 음수 일 때 코드가 작동하지 않는다고 생각합니다. 시도 [1] [1 0], 및 U m 동일하지 * S * ... VT
카를로스 Scheidegger

2

개인적인 필요를 위해 2x2 svd에 대한 최소 계산을 분리하려고했습니다. 아마도 가장 간단하고 빠른 솔루션 중 하나 일 것입니다. 내 개인 블로그 ( http://lucidarme.me/?p=4624) 에서 세부 정보를 찾을 수 있습니다 .

장점 : 간단하고 빠르며 세 개의 매트릭스가 필요하지 않은 경우 세 개의 매트릭스 (S, U 또는 D) 중 하나 또는 두 개만 계산할 수 있습니다.

단점 은 atan2를 사용하는데, 이는 부정확하고 외부 라이브러리 (일반적으로 math.h)가 필요할 수 있습니다.


3
링크는 영구적이지 않기 때문에 링크를 단순히 답변으로 제공하는 것이 아니라 접근 방식을 요약하는 것이 중요합니다.
Paul

또한 자신의 블로그에 대한 링크를 게시하려는 경우 (a) 블로그임을 밝히십시오. (b) 접근 방식을 실제로 요약하거나 잘라내어 붙여 넣는 것이 더 좋습니다 (공식 이미지는 원시 LaTeX로 번역되고 MathJax를 사용하여 렌더링됩니다). 이러한 종류의 질문 상태 수식에 대한 최상의 답변은 해당 수식에 대한 인용을 제공 한 다음 단점, 엣지 사례 및 잠재적 대안과 같은 항목을 나열합니다.
Geoff Oxberry

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
            );
        }
    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.