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=m00−m112;G=m10+m012;H=엠10−엠012큐=이자형2+H2;아르 자형=에프2+지2에스엑스=큐+아르 자형;에스와이=큐−아르 자형에이1=에이티에이엔2(지,에프);에이2=에이티에이엔2(H,이자형)θ=에이2−에이12;ϕ=에이2+에이12

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

엠=(엠00엠01엠10엠11)=(코사인⁡ϕ−죄⁡ϕ죄⁡ϕ코사인⁡ϕ)(에스엑스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, 즉 입력 행렬의 결정자를 단순화 함을 확인했습니다 . Q2−R2m00m11−m01m10
— 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=에이

유에스=에이V−1=에이V티V

V에이티에이V티=(에이V티)티에이V티=(유에스)티유에스=에스티유티유에스=디

에스−1

(에스−티에스티)유티유(에스에스−1)=유티유=에스−티디에스−1

디에스디유티유=나는디이자형엔티나는티와이유에스V유에스V=에이 입니다.

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

티22−β−αγ티2−1=0

어디에

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

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

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

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

코드에서 약 2 억 개의 무작위 행렬을 던졌고 가장 큰 수치 오류는 약 6⋅10−7 (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의 코드를 기반으로했습니다. 그의 코드는 일부 행렬에서 작동하지 않았습니다. 이 두 문서를 해결을위한 수학적 참조로 사용했습니다 : 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
            );
        }
    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.