| // Copyright ©2013 The Gonum Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package mat |
| |
| import ( |
| "gonum.org/v1/gonum/blas/blas64" |
| "gonum.org/v1/gonum/lapack" |
| "gonum.org/v1/gonum/lapack/lapack64" |
| ) |
| |
| const badRcond = "mat: invalid rcond value" |
| |
| // SVD is a type for creating and using the Singular Value Decomposition |
| // of a matrix. |
| type SVD struct { |
| kind SVDKind |
| |
| s []float64 |
| u blas64.General |
| vt blas64.General |
| } |
| |
| // SVDKind specifies the treatment of singular vectors during an SVD |
| // factorization. |
| type SVDKind int |
| |
| const ( |
| // SVDNone specifies that no singular vectors should be computed during |
| // the decomposition. |
| SVDNone SVDKind = 0 |
| |
| // SVDThinU specifies the thin decomposition for U should be computed. |
| SVDThinU SVDKind = 1 << (iota - 1) |
| // SVDFullU specifies the full decomposition for U should be computed. |
| SVDFullU |
| // SVDThinV specifies the thin decomposition for V should be computed. |
| SVDThinV |
| // SVDFullV specifies the full decomposition for V should be computed. |
| SVDFullV |
| |
| // SVDThin is a convenience value for computing both thin vectors. |
| SVDThin SVDKind = SVDThinU | SVDThinV |
| // SVDFull is a convenience value for computing both full vectors. |
| SVDFull SVDKind = SVDFullU | SVDFullV |
| ) |
| |
| // succFact returns whether the receiver contains a successful factorization. |
| func (svd *SVD) succFact() bool { |
| return len(svd.s) != 0 |
| } |
| |
| // Factorize computes the singular value decomposition (SVD) of the input matrix A. |
| // The singular values of A are computed in all cases, while the singular |
| // vectors are optionally computed depending on the input kind. |
| // |
| // The full singular value decomposition (kind == SVDFull) is a factorization |
| // of an m×n matrix A of the form |
| // A = U * Σ * Vᵀ |
| // where Σ is an m×n diagonal matrix, U is an m×m orthogonal matrix, and V is an |
| // n×n orthogonal matrix. The diagonal elements of Σ are the singular values of A. |
| // The first min(m,n) columns of U and V are, respectively, the left and right |
| // singular vectors of A. |
| // |
| // Significant storage space can be saved by using the thin representation of |
| // the SVD (kind == SVDThin) instead of the full SVD, especially if |
| // m >> n or m << n. The thin SVD finds |
| // A = U~ * Σ * V~ᵀ |
| // where U~ is of size m×min(m,n), Σ is a diagonal matrix of size min(m,n)×min(m,n) |
| // and V~ is of size n×min(m,n). |
| // |
| // Factorize returns whether the decomposition succeeded. If the decomposition |
| // failed, routines that require a successful factorization will panic. |
| func (svd *SVD) Factorize(a Matrix, kind SVDKind) (ok bool) { |
| // kill previous factorization |
| svd.s = svd.s[:0] |
| svd.kind = kind |
| |
| m, n := a.Dims() |
| var jobU, jobVT lapack.SVDJob |
| |
| // TODO(btracey): This code should be modified to have the smaller |
| // matrix written in-place into aCopy when the lapack/native/dgesvd |
| // implementation is complete. |
| switch { |
| case kind&SVDFullU != 0: |
| jobU = lapack.SVDAll |
| svd.u = blas64.General{ |
| Rows: m, |
| Cols: m, |
| Stride: m, |
| Data: use(svd.u.Data, m*m), |
| } |
| case kind&SVDThinU != 0: |
| jobU = lapack.SVDStore |
| svd.u = blas64.General{ |
| Rows: m, |
| Cols: min(m, n), |
| Stride: min(m, n), |
| Data: use(svd.u.Data, m*min(m, n)), |
| } |
| default: |
| jobU = lapack.SVDNone |
| } |
| switch { |
| case kind&SVDFullV != 0: |
| svd.vt = blas64.General{ |
| Rows: n, |
| Cols: n, |
| Stride: n, |
| Data: use(svd.vt.Data, n*n), |
| } |
| jobVT = lapack.SVDAll |
| case kind&SVDThinV != 0: |
| svd.vt = blas64.General{ |
| Rows: min(m, n), |
| Cols: n, |
| Stride: n, |
| Data: use(svd.vt.Data, min(m, n)*n), |
| } |
| jobVT = lapack.SVDStore |
| default: |
| jobVT = lapack.SVDNone |
| } |
| |
| // A is destroyed on call, so copy the matrix. |
| aCopy := DenseCopyOf(a) |
| svd.kind = kind |
| svd.s = use(svd.s, min(m, n)) |
| |
| work := []float64{0} |
| lapack64.Gesvd(jobU, jobVT, aCopy.mat, svd.u, svd.vt, svd.s, work, -1) |
| work = getFloats(int(work[0]), false) |
| ok = lapack64.Gesvd(jobU, jobVT, aCopy.mat, svd.u, svd.vt, svd.s, work, len(work)) |
| putFloats(work) |
| if !ok { |
| svd.kind = 0 |
| } |
| return ok |
| } |
| |
| // Kind returns the SVDKind of the decomposition. If no decomposition has been |
| // computed, Kind returns -1. |
| func (svd *SVD) Kind() SVDKind { |
| if !svd.succFact() { |
| return -1 |
| } |
| return svd.kind |
| } |
| |
| // Rank returns the rank of A based on the count of singular values greater than |
| // rcond scaled by the largest singular value. |
| // Rank will panic if the receiver does not contain a successful factorization or |
| // rcond is negative. |
| func (svd *SVD) Rank(rcond float64) int { |
| if rcond < 0 { |
| panic(badRcond) |
| } |
| if !svd.succFact() { |
| panic(badFact) |
| } |
| s0 := svd.s[0] |
| for i, v := range svd.s { |
| if v <= rcond*s0 { |
| return i |
| } |
| } |
| return len(svd.s) |
| } |
| |
| // Cond returns the 2-norm condition number for the factorized matrix. Cond will |
| // panic if the receiver does not contain a successful factorization. |
| func (svd *SVD) Cond() float64 { |
| if !svd.succFact() { |
| panic(badFact) |
| } |
| return svd.s[0] / svd.s[len(svd.s)-1] |
| } |
| |
| // Values returns the singular values of the factorized matrix in descending order. |
| // |
| // If the input slice is non-nil, the values will be stored in-place into |
| // the slice. In this case, the slice must have length min(m,n), and Values will |
| // panic with ErrSliceLengthMismatch otherwise. If the input slice is nil, a new |
| // slice of the appropriate length will be allocated and returned. |
| // |
| // Values will panic if the receiver does not contain a successful factorization. |
| func (svd *SVD) Values(s []float64) []float64 { |
| if !svd.succFact() { |
| panic(badFact) |
| } |
| if s == nil { |
| s = make([]float64, len(svd.s)) |
| } |
| if len(s) != len(svd.s) { |
| panic(ErrSliceLengthMismatch) |
| } |
| copy(s, svd.s) |
| return s |
| } |
| |
| // UTo extracts the matrix U from the singular value decomposition. The first |
| // min(m,n) columns are the left singular vectors and correspond to the singular |
| // values as returned from SVD.Values. |
| // |
| // If dst is empty, UTo will resize dst to be m×m if the full U was computed |
| // and size m×min(m,n) if the thin U was computed. When dst is non-empty, then |
| // UTo will panic if dst is not the appropriate size. UTo will also panic if |
| // the receiver does not contain a successful factorization, or if U was |
| // not computed during factorization. |
| func (svd *SVD) UTo(dst *Dense) { |
| if !svd.succFact() { |
| panic(badFact) |
| } |
| kind := svd.kind |
| if kind&SVDThinU == 0 && kind&SVDFullU == 0 { |
| panic("svd: u not computed during factorization") |
| } |
| r := svd.u.Rows |
| c := svd.u.Cols |
| if dst.IsEmpty() { |
| dst.ReuseAs(r, c) |
| } else { |
| r2, c2 := dst.Dims() |
| if r != r2 || c != c2 { |
| panic(ErrShape) |
| } |
| } |
| |
| tmp := &Dense{ |
| mat: svd.u, |
| capRows: r, |
| capCols: c, |
| } |
| dst.Copy(tmp) |
| } |
| |
| // VTo extracts the matrix V from the singular value decomposition. The first |
| // min(m,n) columns are the right singular vectors and correspond to the singular |
| // values as returned from SVD.Values. |
| // |
| // If dst is empty, VTo will resize dst to be n×n if the full V was computed |
| // and size n×min(m,n) if the thin V was computed. When dst is non-empty, then |
| // VTo will panic if dst is not the appropriate size. VTo will also panic if |
| // the receiver does not contain a successful factorization, or if V was |
| // not computed during factorization. |
| func (svd *SVD) VTo(dst *Dense) { |
| if !svd.succFact() { |
| panic(badFact) |
| } |
| kind := svd.kind |
| if kind&SVDThinV == 0 && kind&SVDFullV == 0 { |
| panic("svd: v not computed during factorization") |
| } |
| r := svd.vt.Rows |
| c := svd.vt.Cols |
| if dst.IsEmpty() { |
| dst.ReuseAs(c, r) |
| } else { |
| r2, c2 := dst.Dims() |
| if c != r2 || r != c2 { |
| panic(ErrShape) |
| } |
| } |
| |
| tmp := &Dense{ |
| mat: svd.vt, |
| capRows: r, |
| capCols: c, |
| } |
| dst.Copy(tmp.T()) |
| } |
| |
| // SolveTo calculates the minimum-norm solution to a linear least squares problem |
| // minimize over n-element vectors x: |b - A*x|_2 and |x|_2 |
| // where b is a given m-element vector, using the SVD of m×n matrix A stored in |
| // the receiver. A may be rank-deficient, that is, the given effective rank can be |
| // rank ≤ min(m,n) |
| // The rank can be computed using SVD.Rank. |
| // |
| // Several right-hand side vectors b and solution vectors x can be handled in a |
| // single call. Vectors b are stored in the columns of the m×k matrix B and the |
| // resulting vectors x will be stored in the columns of dst. dst must be either |
| // empty or have the size equal to n×k. |
| // |
| // The decomposition must have been factorized computing both the U and V |
| // singular vectors. |
| // |
| // SolveTo returns the residuals calculated from the complete SVD. For this |
| // value to be valid the factorization must have been performed with at least |
| // SVDFullU. |
| func (svd *SVD) SolveTo(dst *Dense, b Matrix, rank int) []float64 { |
| if !svd.succFact() { |
| panic(badFact) |
| } |
| if rank < 1 || len(svd.s) < rank { |
| panic("svd: rank out of range") |
| } |
| kind := svd.kind |
| if kind&SVDThinU == 0 && kind&SVDFullU == 0 { |
| panic("svd: u not computed during factorization") |
| } |
| if kind&SVDThinV == 0 && kind&SVDFullV == 0 { |
| panic("svd: v not computed during factorization") |
| } |
| |
| u := Dense{ |
| mat: svd.u, |
| capRows: svd.u.Rows, |
| capCols: svd.u.Cols, |
| } |
| vt := Dense{ |
| mat: svd.vt, |
| capRows: svd.vt.Rows, |
| capCols: svd.vt.Cols, |
| } |
| s := svd.s[:rank] |
| |
| _, bc := b.Dims() |
| c := getWorkspace(svd.u.Cols, bc, false) |
| defer putWorkspace(c) |
| c.Mul(u.T(), b) |
| |
| y := getWorkspace(rank, bc, false) |
| defer putWorkspace(y) |
| y.DivElem(c.slice(0, rank, 0, bc), repVector{vec: s, cols: bc}) |
| dst.Mul(vt.slice(0, rank, 0, svd.vt.Cols).T(), y) |
| |
| res := make([]float64, bc) |
| if rank < svd.u.Cols { |
| c = c.slice(len(s), svd.u.Cols, 0, bc) |
| for j := range res { |
| col := c.ColView(j) |
| res[j] = Dot(col, col) |
| } |
| } |
| return res |
| } |
| |
| type repVector struct { |
| vec []float64 |
| cols int |
| } |
| |
| func (m repVector) Dims() (r, c int) { return len(m.vec), m.cols } |
| func (m repVector) At(i, j int) float64 { |
| if i < 0 || len(m.vec) <= i || j < 0 || m.cols <= j { |
| panic(ErrIndexOutOfRange.string) // Panic with string to prevent mat.Error recovery. |
| } |
| return m.vec[i] |
| } |
| func (m repVector) T() Matrix { return Transpose{m} } |
| |
| // SolveVecTo calculates the minimum-norm solution to a linear least squares problem |
| // minimize over n-element vectors x: |b - A*x|_2 and |x|_2 |
| // where b is a given m-element vector, using the SVD of m×n matrix A stored in |
| // the receiver. A may be rank-deficient, that is, the given effective rank can be |
| // rank ≤ min(m,n) |
| // The rank can be computed using SVD.Rank. |
| // |
| // The resulting vector x will be stored in dst. dst must be either empty or |
| // have length equal to n. |
| // |
| // The decomposition must have been factorized computing both the U and V |
| // singular vectors. |
| // |
| // SolveVecTo returns the residuals calculated from the complete SVD. For this |
| // value to be valid the factorization must have been performed with at least |
| // SVDFullU. |
| func (svd *SVD) SolveVecTo(dst *VecDense, b Vector, rank int) float64 { |
| if !svd.succFact() { |
| panic(badFact) |
| } |
| if rank < 1 || len(svd.s) < rank { |
| panic("svd: rank out of range") |
| } |
| kind := svd.kind |
| if kind&SVDThinU == 0 && kind&SVDFullU == 0 { |
| panic("svd: u not computed during factorization") |
| } |
| if kind&SVDThinV == 0 && kind&SVDFullV == 0 { |
| panic("svd: v not computed during factorization") |
| } |
| |
| u := Dense{ |
| mat: svd.u, |
| capRows: svd.u.Rows, |
| capCols: svd.u.Cols, |
| } |
| vt := Dense{ |
| mat: svd.vt, |
| capRows: svd.vt.Rows, |
| capCols: svd.vt.Cols, |
| } |
| s := svd.s[:rank] |
| |
| c := getWorkspaceVec(svd.u.Cols, false) |
| defer putWorkspaceVec(c) |
| c.MulVec(u.T(), b) |
| |
| y := getWorkspaceVec(rank, false) |
| defer putWorkspaceVec(y) |
| y.DivElemVec(c.sliceVec(0, rank), NewVecDense(rank, s)) |
| dst.MulVec(vt.slice(0, rank, 0, svd.vt.Cols).T(), y) |
| |
| var res float64 |
| if rank < c.Len() { |
| c = c.sliceVec(rank, c.Len()) |
| res = Dot(c, c) |
| } |
| return res |
| } |