Algoritmo robusto per SVD


26

Che cos'è un semplice algoritmo per il calcolo dell'SVD di matrici 2×2 ?

Idealmente, vorrei un algoritmo numericamente robusto, ma mi piacerebbe vedere implementazioni sia semplici che non così semplici. Codice C accettato.

Qualche riferimento a documenti o codice?


5
Wikipedia elenca una soluzione a forma chiusa 2x2, ma non ho idea delle sue proprietà numeriche.
Damien,

Come riferimento, "Ricette numeriche", Press et al., Cambridge Press. Libro abbastanza costoso ma vale ogni centesimo. Oltre alle soluzioni SVD troverai molti altri algoritmi utili.
Jan Hackenberg,

Risposte:


19

Vedi /math/861674/decompose-a-2d-arbitrary-transform-into-only-scaling-and-rotation (scusate, l'avrei inserito in un commento ma mi sono registrato solo per pubblicare questo, quindi non posso ancora pubblicare commenti).

Ma dal momento che lo scrivo come risposta, scriverò anche il metodo:

E=m00+m112;F=m00-m112;sol=m10+m012;H=m10-m012Q=E2+H2;R=F2+sol2SX=Q+R;Sy=Q-Run'1=un'tun'n2(sol,F);un'2=un'tun'n2(H,E)θ=un'2-un'12;φ=un'2+un'12

That decomposes the matrix as follows:

M=(m00m01m10m11)=(cosφ-peccatoφpeccatoφcosφ)(SX00Sy)(cosθ-peccatoθpeccatoθcosθ)

L'unica cosa da evitare con questo metodo è che sol=F=0 o H=E=0 per atan2.Dubito che possa essere più robusto di così( Aggiornamento: vedi la risposta di Alex Eftimiades!).

Il riferimento è: http://dx.doi.org/10.1109/38.486688 (fornito da Rahul lì) che viene dal fondo di questo post del blog: http://metamerist.blogspot.com/2006/10/linear-algebra -per-grafica-geek-svd.html

Aggiornamento: Come notato da @VictorLiu in un commento, sy può essere negativo. Ciò accade se e solo se anche il determinante della matrice di input è negativo. Se è così e vuoi i valori singolari positivi, prendi semplicemente il valore assoluto di sy .


1
Sembra che possa essere negativo se Q < R . Questo non dovrebbe essere possibile. syQ<R
Victor Liu,

@VictorLiu Se la matrice di input viene invertita, l'unica posizione che può essere riflessa è nella matrice di ridimensionamento, poiché le matrici di rotazione non possono invertire la tendenza. Basta non alimentare le matrici di input che si girano. Non ho ancora fatto i calcoli ma scommetto che il segno del determinante della matrice di input determinerà se o R è maggiore. QR
Pedro Gimeno

@VictorLiu Ho fatto la matematica ora e ho confermato che in effetti, semplifica a m 00 m 11 - m 01 m 10, ovvero il determinante della matrice di input. Q2R2m00m11m01m10
Pedro Gimeno,

9

@Pedro Gimeno

"Dubito che possa essere più robusto di così."

Sfida accettata.

Ho notato che l'approccio abituale è quello di utilizzare funzioni trig come atan2. Intuitivamente, non dovrebbe esserci la necessità di utilizzare le funzioni di trigger. In effetti, tutti i risultati finiscono come seni e coseni di arctan, che possono essere semplificati in funzioni algebriche. Ci è voluto un po 'di tempo, ma sono riuscito a semplificare l'algoritmo di Pedro per utilizzare solo le funzioni algebriche.

Il seguente codice Python fa il trucco.

da intorpidito importar 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
Il codice sembra errato. Considera la matrice di identità 2x2. Quindi y1= 0, x1= 0, h1= 0 e t1= 0/0 = NaN.
Hugues,

8

La GSL ha uno SVD 2-by-2 risolutore sottostante la parte QR decomposizione dell'algoritmo SVD principale gsl_linalg_SV_decomp. Vedi il svdstep.cfile e cerca la svd2funzione. La funzione ha alcuni casi speciali, non è esattamente banale e sembra fare molte cose per essere numericamente attenti (ad esempio, usando hypotper evitare traboccamenti).


1
Questa funzione ha documentazione? Vorrei sapere quali sono i suoi parametri di input.
Victor Liu,

@VictorLiu: Purtroppo non ho visto altro che i magri commenti nel file stesso. C'è un po 'nel ChangeLogfile se scarichi GSL. E puoi guardare svd.cper i dettagli dell'algoritmo generale. L'unica vera documentazione sembra essere per le funzioni di alto livello richiamabili dall'utente, ad es gsl_linalg_SV_decomp.
Horchler,

7

Quando diciamo "numericamente robusto", di solito intendiamo un algoritmo in cui facciamo cose come il pivot per evitare la propagazione degli errori. Tuttavia, per una matrice 2x2, è possibile scrivere il risultato in termini di formule esplicite, ovvero scrivere le formule per gli elementi SVD che indicano il risultato solo in termini di input , piuttosto che in termini di valori intermedi precedentemente calcolati . Ciò significa che potresti avere la cancellazione ma nessuna propagazione dell'errore.

Il punto è semplicemente che per i sistemi 2x2 non è necessario preoccuparsi della robustezza.


Può dipendere dalla matrice. Ho visto un metodo che trova gli angoli sinistro e destro separatamente (ciascuno tramite arctan2 (y, x)) che generalmente funziona bene. Ma quando i valori singolari sono vicini, ognuno di questi arctan tende a 0/0, quindi il risultato può essere impreciso. Nel metodo fornito da Pedro Gimeno, il calcolo di a2 sarà ben definito in questo caso, mentre a1 diventa mal definito; hai ancora un buon risultato perché la validità della decomposizione è sensibile solo a theta + phi quando le s.vals sono vicine tra loro, non a theta-phi.
Greggo

5

Questo codice è basato su carta di Blinn , carta Ellis , SVD conferenza e ulteriori calcoli. Un algoritmo è adatto per matrici reali regolari e singolari. Tutte le versioni precedenti funzionano al 100% così come questa.

#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

Avevo bisogno di un algoritmo che ha

  • piccola ramificazione (speriamo CMOV)
  • nessuna chiamata di funzione trigonometrica
  • elevata precisione numerica anche con float a 32 bit

Vogliamo calcolare e σ 2 come segue:c1,s1,c2,s2,σ1σ2

A=USV

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

The main idea is to find a rotation matrix V that diagonalizes ATA, that is VATAVT=D is diagonal.

Recall that

USV=A

US=AV1=AVT (since V is orthogonal)

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

Multiplying both sides by S1 we get

(STST)UTU(SS1)=UTU=STDS1

Since D is diagonal, setting S to D will give us UTU=Identity, meaning U is a rotation matrix, S is a diagonal matrix, V is a rotation matrix and USV=A, just what we are looking for.

Calculating the diagonalizing rotation can be done by solving the following equation:

t22βαγt21=0

where

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

and t2 is the tangent of angle of V. This can be derived by expanding VATAVT and making its off-diagonal elements equal to zero (they are equal to each other).

The problem with this method is that it loses significant floating point precision when calculating βα and γ for certain matrices, because of the subtractions in the calculations. The solution for this is to do an RQ decomposition (A=RQ, R upper triangular and Q orthogonal) first, then use the algorithm to factorize USV=R. This gives USV=USVQ=RQ=A. Notice how setting d to 0 (as in R) eliminates some of the additions/subtractions. (The RQ decomposition is fairly trivial from the expansion of the matrix product).

The algorithm naively implemented this way has some numerical and logical anomalies (e.g. is S +D or D), which I fixed in the code below.

I threw about 2000 million randomized matrices at the code, and the largest numerical error produced was around 6107 (with 32 bit floats, error=||USV-M||/||M||). L'algoritmo funziona in circa 340 cicli di clock (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);
}

Idee da:
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

Ho usato la descrizione su http://www.lucidarme.me/?p=4624 per creare questo codice C ++. Le matrici sono quelle della libreria Eigen, ma da questo esempio puoi facilmente creare la tua struttura di dati:

UN=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);

Con la funzione di segno standard

double signum(double value)
{
    if(value > 0)
        return 1;
    else if(value < 0)
        return -1;
    else
        return 0;
}

Ciò si traduce esattamente negli stessi valori di Eigen::JacobiSVD(consultare 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

Ho puro codice C per il vero SVD 2x2 qui . Vedere la riga 559. In sostanza calcola gli autovalori diUNTUNrisolvendo un quadratico, quindi non è necessariamente il più robusto, ma sembra funzionare bene nella pratica per casi non troppo patologici. È relativamente semplice.


Non credo che il tuo codice funzioni quando gli autovalori della matrice sono negativi. Prova [[1 1] [1 0]] e u * s * vt non è uguale a m ...
Carlos Scheidegger,

2

Per mia necessità personale, ho cercato di isolare il calcolo minimo per uno svd 2x2. Immagino sia probabilmente una delle soluzioni più semplici e veloci. Puoi trovare i dettagli sul mio blog personale: http://lucidarme.me/?p=4624 .

Vantaggi: semplice, veloce e puoi calcolare solo una o due delle tre matrici (S, U o D) se non hai bisogno delle tre matrici.

Svantaggio utilizza atan2, che potrebbe essere inaccurato e potrebbe richiedere una libreria esterna (tip. Math.h).


3
Poiché i collegamenti sono raramente permanenti, è importante sintetizzare l'approccio piuttosto che fornire semplicemente un collegamento come risposta.
Paolo

Inoltre, se hai intenzione di pubblicare un link sul tuo blog, per favore (a) rivelare che è il tuo blog, (b) ancora meglio sarebbe in realtà riassumere o tagliare e incollare il tuo approccio (le immagini delle formule possono essere tradotto in LaTeX grezzo e renderizzato usando MathJax). Le risposte migliori per questo tipo di formule di stato delle domande, forniscono citazioni per tali formule e quindi elencano cose come svantaggi, casi limite e potenziali alternative.
Geoff Oxberry,

1

Ecco un'implementazione di una risoluzione SVD 2x2. L'ho basato sul codice di Victor Liu. Il suo codice non funzionava per alcune matrici. Ho usato questi due documenti come riferimento matematico per la risoluzione: pdf1 e pdf2 .

Il setDatametodo matrix è nell'ordine delle righe principali. Internamente, rappresento i dati della matrice come un array 2D fornito da 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
            );
        }
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.