-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Enhancement] Add singular value decomposition #16
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,22 @@ | ||
const AbstractBlockSparseMatrix{T} = AbstractBlockSparseArray{T,2} | ||
|
||
# SVD is implemented by trying to | ||
# 1. Attempt to find a block-diagonal implementation by permuting | ||
# 2. Fallback to AbstractBlockArray implementation via BlockedArray | ||
function svd( | ||
A::AbstractBlockSparseMatrix; full::Bool=false, alg::Algorithm=default_svd_alg(A) | ||
) | ||
T = LinearAlgebra.eigtype(eltype(A)) | ||
A′ = try_to_blockdiagonal(A) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found it confusing that this is call |
||
|
||
if isnothing(A′) | ||
# not block-diagonal, fall back to dense case | ||
Adense = eigencopy_oftype(A, T) | ||
return svd!(Adense; full, alg) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this output an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (I see from the definition below that it does.) |
||
end | ||
|
||
# compute block-by-block and permute back | ||
A″, (I, J) = A′ | ||
F = svd!(eigencopy_oftype(A″, T); full, alg) | ||
return SVD(F.U[Block.(I), Block.(J)], F.S, F.Vt) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually it looks like that is already the case so it seems like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a bit confused by this code, I would have though it would be something like: SVD(F.U[I, :], F.S, F.Vt[:, J]) i.e. |
||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# type alias for block-diagonal | ||
using LinearAlgebra: Diagonal | ||
|
||
const BlockDiagonal{T,A,Axes,V<:AbstractVector{A}} = BlockSparseMatrix{ | ||
T,A,Diagonal{A,V},Axes | ||
} | ||
|
||
function BlockDiagonal(blocks::AbstractVector{<:AbstractMatrix}) | ||
return BlockSparseArray( | ||
Diagonal(blocks), (blockedrange(size.(blocks, 1)), blockedrange(size.(blocks, 2))) | ||
) | ||
end | ||
|
||
# Cast to block-diagonal implementation if permuted-blockdiagonal | ||
function try_to_blockdiagonal_perm(A) | ||
inds = map(x -> Int.(Tuple(x)), vec(collect(block_stored_indices(A)))) | ||
I = first.(inds) | ||
allunique(I) || return nothing | ||
J = last.(inds) | ||
p = sortperm(J) | ||
Jsorted = J[p] | ||
allunique(Jsorted) || return nothing | ||
return Block.(I[p], Jsorted) | ||
end | ||
|
||
""" | ||
try_to_blockdiagonal(A) | ||
|
||
Attempt to find a permutation of blocks that makes `A` blockdiagonal. If unsuccesful, | ||
returns nothing, otherwise returns both the blockdiagonal `B` as well as the permutation `I, J`. | ||
""" | ||
function try_to_blockdiagonal(A::AbstractBlockSparseMatrix) | ||
perm = try_to_blockdiagonal_perm(A) | ||
isnothing(perm) && return perm | ||
I = first.(Tuple.(perm)) | ||
J = last.(Tuple.(perm)) | ||
diagblocks = map(invperm(I), J) do i, j | ||
return A[Block(i, j)] | ||
end | ||
return BlockDiagonal(diagblocks), perm | ||
end | ||
|
||
# SVD implementation | ||
function eigencopy_oftype(A::BlockDiagonal, S) | ||
diag = map(Base.Fix2(eigencopy_oftype, S), A.blocks.diag) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should make a proper function/API to access Some ideas are:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think ideally we would have |
||
return BlockDiagonal(diag) | ||
end | ||
|
||
function svd(A::BlockDiagonal; kwargs...) | ||
return svd!(eigencopy_oftype(A, LinearAlgebra.eigtype(eltype(A))); kwargs...) | ||
end | ||
function svd!(A::BlockDiagonal; full::Bool=false, alg::Algorithm=default_svd_alg(A)) | ||
# TODO: handle full | ||
F = map(a -> svd!(a; full, alg), blocks(A).diag) | ||
Us = map(Base.Fix2(getproperty, :U), F) | ||
Ss = map(Base.Fix2(getproperty, :S), F) | ||
Vts = map(Base.Fix2(getproperty, :Vt), F) | ||
return SVD(BlockDiagonal(Us), mortar(Ss), BlockDiagonal(Vts)) | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
using LinearAlgebra: | ||
LinearAlgebra, Factorization, Algorithm, default_svd_alg, Adjoint, Transpose | ||
using BlockArrays: AbstractBlockMatrix, BlockedArray, BlockedMatrix, BlockedVector | ||
using BlockArrays: BlockLayout | ||
|
||
# Singular Value Decomposition: | ||
# need new type to deal with U and V having possible different types | ||
# this is basically a carbon copy of the LinearAlgebra implementation. | ||
# additionally, by default we implement a fallback to the LinearAlgebra implementation | ||
# in hope to support as many foreign types as possible that chose to extend those methods. | ||
|
||
# TODO: add this to MatrixFactorizations | ||
# TODO: decide where this goes | ||
# TODO: decide whether or not to restrict types to be blocked. | ||
""" | ||
SVD <: Factorization | ||
|
||
Matrix factorization type of the singular value decomposition (SVD) of a matrix `A`. | ||
This is the return type of [`svd(_)`](@ref), the corresponding matrix factorization function. | ||
|
||
If `F::SVD` is the factorization object, `U`, `S`, `V` and `Vt` can be obtained | ||
via `F.U`, `F.S`, `F.V` and `F.Vt`, such that `A = U * Diagonal(S) * Vt`. | ||
The singular values in `S` are sorted in descending order. | ||
|
||
Iterating the decomposition produces the components `U`, `S`, and `V`. | ||
|
||
# Examples | ||
```jldoctest | ||
julia> A = [1. 0. 0. 0. 2.; 0. 0. 3. 0. 0.; 0. 0. 0. 0. 0.; 0. 2. 0. 0. 0.] | ||
4×5 Matrix{Float64}: | ||
1.0 0.0 0.0 0.0 2.0 | ||
0.0 0.0 3.0 0.0 0.0 | ||
0.0 0.0 0.0 0.0 0.0 | ||
0.0 2.0 0.0 0.0 0.0 | ||
|
||
julia> F = svd(A) | ||
SVD{Float64, Float64, Matrix{Float64}, Vector{Float64}} | ||
U factor: | ||
4×4 Matrix{Float64}: | ||
0.0 1.0 0.0 0.0 | ||
1.0 0.0 0.0 0.0 | ||
0.0 0.0 0.0 1.0 | ||
0.0 0.0 -1.0 0.0 | ||
singular values: | ||
4-element Vector{Float64}: | ||
3.0 | ||
2.23606797749979 | ||
2.0 | ||
0.0 | ||
Vt factor: | ||
4×5 Matrix{Float64}: | ||
-0.0 0.0 1.0 -0.0 0.0 | ||
0.447214 0.0 0.0 0.0 0.894427 | ||
0.0 -1.0 0.0 0.0 0.0 | ||
0.0 0.0 0.0 1.0 0.0 | ||
|
||
julia> F.U * Diagonal(F.S) * F.Vt | ||
4×5 Matrix{Float64}: | ||
1.0 0.0 0.0 0.0 2.0 | ||
0.0 0.0 3.0 0.0 0.0 | ||
0.0 0.0 0.0 0.0 0.0 | ||
0.0 2.0 0.0 0.0 0.0 | ||
|
||
julia> u, s, v = F; # destructuring via iteration | ||
|
||
julia> u == F.U && s == F.S && v == F.V | ||
true | ||
``` | ||
""" | ||
struct SVD{T,Tr,M<:AbstractArray{T},C<:AbstractVector{Tr},N<:AbstractArray{T}} <: | ||
Factorization{T} | ||
U::M | ||
S::C | ||
Vt::N | ||
function SVD{T,Tr,M,C,N}( | ||
U, S, Vt | ||
) where {T,Tr,M<:AbstractArray{T},C<:AbstractVector{Tr},N<:AbstractArray{T}} | ||
Base.require_one_based_indexing(U, S, Vt) | ||
return new{T,Tr,M,C,N}(U, S, Vt) | ||
end | ||
end | ||
function SVD(U::AbstractArray{T}, S::AbstractVector{Tr}, Vt::AbstractArray{T}) where {T,Tr} | ||
return SVD{T,Tr,typeof(U),typeof(S),typeof(Vt)}(U, S, Vt) | ||
end | ||
function SVD{T}(U::AbstractArray, S::AbstractVector{Tr}, Vt::AbstractArray) where {T,Tr} | ||
return SVD( | ||
convert(AbstractArray{T}, U), | ||
convert(AbstractVector{Tr}, S), | ||
convert(AbstractArray{T}, Vt), | ||
) | ||
end | ||
|
||
function SVD{T}(F::SVD) where {T} | ||
return SVD( | ||
convert(AbstractMatrix{T}, F.U), | ||
convert(AbstractVector{real(T)}, F.S), | ||
convert(AbstractMatrix{T}, F.Vt), | ||
) | ||
end | ||
LinearAlgebra.Factorization{T}(F::SVD) where {T} = SVD{T}(F) | ||
|
||
# iteration for destructuring into components | ||
Base.iterate(S::SVD) = (S.U, Val(:S)) | ||
Base.iterate(S::SVD, ::Val{:S}) = (S.S, Val(:V)) | ||
Base.iterate(S::SVD, ::Val{:V}) = (S.V, Val(:done)) | ||
Base.iterate(::SVD, ::Val{:done}) = nothing | ||
|
||
function Base.getproperty(F::SVD, d::Symbol) | ||
if d === :V | ||
return getfield(F, :Vt)' | ||
else | ||
return getfield(F, d) | ||
end | ||
end | ||
|
||
function Base.propertynames(F::SVD, private::Bool=false) | ||
return private ? (:V, fieldnames(typeof(F))...) : (:U, :S, :V, :Vt) | ||
end | ||
|
||
Base.size(A::SVD, dim::Integer) = dim == 1 ? size(A.U, dim) : size(A.Vt, dim) | ||
Base.size(A::SVD) = (size(A, 1), size(A, 2)) | ||
|
||
function Base.show(io::IO, mime::MIME{Symbol("text/plain")}, F::SVD) | ||
summary(io, F) | ||
println(io) | ||
println(io, "U factor:") | ||
show(io, mime, F.U) | ||
println(io, "\nsingular values:") | ||
show(io, mime, F.S) | ||
println(io, "\nVt factor:") | ||
return show(io, mime, F.Vt) | ||
end | ||
|
||
Base.adjoint(usv::SVD) = SVD(adjoint(usv.Vt), usv.S, adjoint(usv.U)) | ||
Base.transpose(usv::SVD) = SVD(transpose(usv.Vt), usv.S, transpose(usv.U)) | ||
|
||
# Conversion | ||
Base.AbstractMatrix(F::SVD) = (F.U * Diagonal(F.S)) * F.Vt | ||
Base.AbstractArray(F::SVD) = AbstractMatrix(F) | ||
Base.Matrix(F::SVD) = Array(AbstractArray(F)) | ||
Base.Array(F::SVD) = Matrix(F) | ||
SVD(usv::SVD) = usv | ||
SVD(usv::LinearAlgebra.SVD) = SVD(usv.U, usv.S, usv.Vt) | ||
|
||
# functions default to LinearAlgebra | ||
# ---------------------------------- | ||
""" | ||
svd!(A; full::Bool = false, alg::Algorithm = default_svd_alg(A)) -> SVD | ||
|
||
`svd!` is the same as [`svd`](@ref), but saves space by | ||
overwriting the input `A`, instead of creating a copy. See documentation of [`svd`](@ref) for details. | ||
""" | ||
svd!(A; kwargs...) = SVD(LinearAlgebra.svd!(A; kwargs...)) | ||
|
||
""" | ||
svd(A; full::Bool = false, alg::Algorithm = default_svd_alg(A)) -> SVD | ||
|
||
Compute the singular value decomposition (SVD) of `A` and return an `SVD` object. | ||
|
||
`U`, `S`, `V` and `Vt` can be obtained from the factorization `F` with `F.U`, | ||
`F.S`, `F.V` and `F.Vt`, such that `A = U * Diagonal(S) * Vt`. | ||
The algorithm produces `Vt` and hence `Vt` is more efficient to extract than `V`. | ||
The singular values in `S` are sorted in descending order. | ||
|
||
Iterating the decomposition produces the components `U`, `S`, and `V`. | ||
|
||
If `full = false` (default), a "thin" SVD is returned. For an ``M | ||
\\times N`` matrix `A`, in the full factorization `U` is ``M \\times M`` | ||
and `V` is ``N \\times N``, while in the thin factorization `U` is ``M | ||
\\times K`` and `V` is ``N \\times K``, where ``K = \\min(M,N)`` is the | ||
number of singular values. | ||
|
||
`alg` specifies which algorithm and LAPACK method to use for SVD: | ||
- `alg = DivideAndConquer()` (default): Calls `LAPACK.gesdd!`. | ||
- `alg = QRIteration()`: Calls `LAPACK.gesvd!` (typically slower but more accurate) . | ||
|
||
!!! compat "Julia 1.3" | ||
The `alg` keyword argument requires Julia 1.3 or later. | ||
|
||
# Examples | ||
```jldoctest | ||
julia> A = rand(4,3); | ||
|
||
julia> F = svd(A); # Store the Factorization Object | ||
|
||
julia> A ≈ F.U * Diagonal(F.S) * F.Vt | ||
true | ||
|
||
julia> U, S, V = F; # destructuring via iteration | ||
|
||
julia> A ≈ U * Diagonal(S) * V' | ||
true | ||
|
||
julia> Uonly, = svd(A); # Store U only | ||
|
||
julia> Uonly == U | ||
true | ||
``` | ||
""" | ||
svd(A; kwargs...) = | ||
SVD(svd!(eigencopy_oftype(A, LinearAlgebra.eigtype(eltype(A))); kwargs...)) | ||
|
||
LinearAlgebra.svdvals(usv::SVD{<:Any,T}) where {T} = (usv.S)::Vector{T} | ||
|
||
# Added here to avoid type-piracy | ||
eigencopy_oftype(A, S) = LinearAlgebra.eigencopy_oftype(A, S) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I see this is designed around a private
BlockSparseArrays.svd
function, is the plan to overloadLinearAlgebra.svd
? Probably we should define this as:(I would like to make the
AbstractArrayInterface
interface types take anndims
parameter so it could beAbstractBlockSparseMatrixInterface
but that isn't supported yet) and then derive that function forLinearAlgebra.svd(::AnyAbstractBlockSparseMatrix)
in the style ofDerive.jl
.