Skip to content
This repository was archived by the owner on Apr 26, 2021. It is now read-only.

Commit 44dd5ab

Browse files
committed
Basic implementation of GK SVD algorithm
1 parent 8c73bbf commit 44dd5ab

File tree

6 files changed

+377
-5
lines changed

6 files changed

+377
-5
lines changed

LICENSE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The GenericSVD.jl package is licensed under the MIT "Expat" License:
22

3-
> Copyright (c) 2016: Simon Byrne.
3+
> Copyright (c) 2016: Simon Byrne, Andreas Noack.
44
>
55
> Permission is hereby granted, free of charge, to any person obtaining
66
> a copy of this software and associated documentation files (the

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1-
# GenericSVD
1+
# GenericSVD.jl
22

33
[![Build Status](https://travis-ci.org/simonbyrne/GenericSVD.jl.svg?branch=master)](https://travis-ci.org/simonbyrne/GenericSVD.jl)
4+
5+
Implements Singular Value Decomposition for generic number types, such as `BigFloat` and `Complex{BigFloat}`. It internally overloads several Base functions such that existing methods (`svd`, `svdfact` and `svdvals`) should work directly.
6+
7+
It uses a Golub-Kahan 2-stage algorithm of bidiagonalization with Householder reflections, followed by an implicit QR with shift.

src/GenericSVD.jl

Lines changed: 241 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,245 @@
11
module GenericSVD
22

3-
# package code goes here
3+
import Base: SVD, svdvals!, svdfact!
4+
5+
include("utils.jl")
6+
include("bidiagonalize.jl")
7+
8+
9+
10+
function svdfact!(X; sorted=true, thin=true)
11+
m,n = size(X)
12+
m >= n || error("Generic SVD requires more rows than columns.")
13+
B,P = bidiagonalize_tall!(X)
14+
U,Vt = full(P,thin=thin)
15+
U,S,Vt = svd!(B,U,Vt)
16+
for i = 1:n
17+
if signbit(S[i])
18+
S[i] = -S[i]
19+
for j = 1:n
20+
Vt[i,j] = -Vt[i,j]
21+
end
22+
end
23+
end
24+
if sorted
25+
I = sortperm(S,rev=true)
26+
S = S[I]
27+
U = U[:,I]
28+
Vt = Vt[I,:]
29+
end
30+
SVD(U,S,Vt)
31+
end
32+
33+
function svdvals!(X; sorted=true)
34+
B,P = bidiagonalize_tall!(copy(X))
35+
S = svd!(B)[2]
36+
for i = eachindex(S)
37+
if signbit(S[i])
38+
S[i] = -S[i]
39+
end
40+
end
41+
sorted ? sort!(S,rev=true) : S
42+
end
43+
44+
45+
"""
46+
Tests if the B[i-1,i] element is approximately zero, using the criteria
47+
```math
48+
|B_{i-1,i}| < ɛ*(|B_{i-1,i-1}| + |B_{i,i}|)
49+
```
50+
"""
51+
offdiag_approx_zero(B::Bidiagonal,i,ɛ) =
52+
abs(B.ev[i-1]) < ɛ*(abs(B.dv[i-1]) + abs(B.dv[i]))
53+
54+
55+
"""
56+
This finds the lowest strictly-bidiagonal submatrix, i.e. n₁, n₂ such that
57+
58+
```
59+
[ d ? ]
60+
[ d 0 ]
61+
n₁ [ d e ]
62+
[ d e ]
63+
n₂ [ d 0 ]
64+
[ d 0 ]
65+
```
66+
"""
67+
function svd!{T<:Real}(B::Bidiagonal{T}, U=nothing, Vt=nothing, ɛ::T = eps(T))
68+
n = size(B, 1)
69+
n₂ = n
70+
71+
if istriu(B)
72+
while true
73+
while offdiag_approx_zero(B,n₂,ɛ)
74+
n₂ -= 1
75+
if n₂ == 1
76+
return U,B.dv,Vt
77+
end
78+
end
79+
n₁ = n₂ - 1
80+
while n₁ > 1 && !offdiag_approx_zero(B,n₁,ɛ)
81+
n₁ -= 1
82+
end
83+
84+
85+
86+
# TODO: check for diagonal zeros
87+
# if n₂ == n₁ + 1 # 2x2 block
88+
89+
# B.dv[n₁] = s₂
90+
# B.dv[n₂] = s₁
91+
# B.eb[n₁] = 0
92+
93+
# # TODO: apply op
94+
# end
95+
96+
d₁ = B.dv[n₂-1]
97+
d₂ = B.dv[n₂]
98+
e = B.ev[n₂-1]
99+
100+
s₁, s₂ = svdvals2x2(d₁, d₂, e)
101+
# use singular value closest to
102+
h = hypot(d₂,e)
103+
shift = abs(s₁-h) < abs(s₂-h) ? s₁ : s₂
104+
svd_gk!(B, U, Vt, n₁, n₂, shift)
105+
end
106+
else
107+
throw(ArgumentError("lower bidiagonal version not implemented yet"))
108+
end
109+
end
110+
111+
112+
113+
"""
114+
Applies a Golub-Kahan SVD step.
115+
116+
A Givens rotation is applied to the top 2x2 matrix, and the resulting "bulge" is "chased" down the diagonal to the bottom of the matrix.
117+
"""
118+
function svd_gk!{T<:Real}(B::Bidiagonal{T},U,Vt,n₁,n₂,shift)
119+
120+
if istriu(B)
121+
122+
d₁′ = B.dv[n₁]
123+
e₁′ = B.ev[n₁]
124+
d₂′ = B.dv[n₁+1]
125+
126+
G, r = givens(d₁′ - abs2(shift)/d₁′, e₁′, n₁, n₁+1)
127+
A_mul_B!(G, Vt)
128+
129+
# [d₁,e₁] = [d₁′,e₁′] * G
130+
# [b ,d₂] [0 ,d₂′]
131+
132+
133+
d₁ = d₁′*G.c + e₁′*G.s
134+
e₁ = -d₁′*G.s + e₁′*G.c
135+
b = d₂′*G.s
136+
d₂ = d₂′*G.c
137+
138+
for i = n₁:n₂-2
139+
140+
# [. ,e₁′,b′ ] = G * [d₁,e₁,0 ]
141+
# [0 ,d₂′,e₂′] [b ,d₂,e₂]
142+
143+
e₂ = B.ev[i+1]
144+
145+
G, r = givens(d₁, b, i, i+1)
146+
A_mul_Bc!(U, G)
147+
148+
B.dv[i] = G.c*d₁ + G.s*b
149+
150+
e₁′ = G.c*e₁ + G.s*d₂
151+
d₂′ = -G.s*e₁ + G.c*d₂
152+
153+
b′ = G.s*e₂
154+
e₂′ = G.c*e₂
155+
156+
# [. ,0 ] = [e₁′,b′ ] * G
157+
# [d₁,e₁] [d₂′,e₂′]
158+
# [b ,d₂] [0 ,d₃′]
159+
160+
d₃′ = B.dv[i+2]
161+
162+
G, r = givens(e₁′, b′, i+1, i+2)
163+
A_mul_B!(G, Vt)
164+
165+
B.ev[i] = e₁′*G.c + b′*G.s
166+
167+
d₁ = d₂′*G.c + e₂′*G.s
168+
e₁ = -d₂′*G.s + e₂′*G.c
169+
170+
b = d₃′*G.s
171+
d₂ = d₃′*G.c
172+
end
173+
174+
# [. ,.] = G * [d₁,e₁]
175+
# [0 ,.] [b ,d₂]
176+
177+
G, r = givens(d₁,b,n₂-1,n₂)
178+
A_mul_Bc!(U, G)
179+
180+
B.dv[n₂-1] = G.c*d₁ + G.s*b
181+
182+
B.ev[n₂-1] = G.c*e₁ + G.s*d₂
183+
B.dv[n₂] = -G.s*e₁ + G.c*d₂
184+
else
185+
throw(ArgumentError("lower bidiagonal version not implemented yet"))
186+
end
187+
188+
return B,U,Vt
189+
end
190+
191+
192+
"""
193+
The singular values of the matrix
194+
```
195+
B = [ f g ;
196+
0 h ]
197+
```
198+
(i.e. the sqrt-eigenvalues of `B'*B`).
199+
200+
This is a direct translation of LAPACK [DLAS2](http://www.netlib.org/lapack/explore-html/d8/dfd/dlas2_8f.html).
201+
"""
202+
function svdvals2x2(f, h, g)
203+
fa = abs(f)
204+
ga = abs(g)
205+
ha = abs(h)
206+
207+
fhmin = min(fa,ha)
208+
fhmax = max(fa,ha)
209+
210+
if fhmin == 0
211+
ssmin = zero(f)
212+
if fhmax == 0
213+
ssmax = zero(f)
214+
else
215+
ssmax = max(fhmax,ga)*sqrt(1+(min(fhmax,ga)/max(fhmax,ga))^2)
216+
end
217+
else
218+
if ga < fhmax
219+
as = 1 + fhmin/fhmax
220+
at = (fhmax-fhmin)/fhmax
221+
au = (ga/fhmax)^2
222+
c = 2/(sqrt(as^2 + au) + sqrt(at^2+au))
223+
ssmin = fhmin*c
224+
ssmax = fhmax/c
225+
else
226+
au = fhmax / ga
227+
if au == 0
228+
ssmin = (fhmin*fhmax)/ga
229+
ssmax = ga
230+
else
231+
as = 1+fhmin/fhmax
232+
at = (fhmax-fhmin)/fhmax
233+
c = 1/(sqrt(1 + (as*au)^2) + sqrt(1 + (at*au)^2))
234+
ssmin = 2((fhmin*c)*au)
235+
ssmax = ga/(2c)
236+
end
237+
end
238+
end
239+
ssmin,ssmax
240+
end
241+
242+
243+
4244

5245
end # module

src/bidiagonalize.jl

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""
2+
Packed storage of bidiagonalizing QR reflectors.
3+
"""
4+
immutable PackedUVt{T}
5+
A::Matrix{T}
6+
end
7+
8+
9+
"""
10+
Bidiagonalize a tall matrix `A` into `B`. Both arguments are overwritten.
11+
"""
12+
function bidiagonalize_tall!{T}(A::Matrix{T},B::Bidiagonal)
13+
m, n = size(A)
14+
# tall case: assumes m >= n
15+
# upper bidiagonal
16+
17+
for i = 1:n
18+
x = slice(A, i:m, i)
19+
τi = LinAlg.reflector!(x)
20+
B.dv[i] = real(A[i,i])
21+
LinAlg.reflectorApply!(x, τi, slice(A, i:m, i+1:n))
22+
A[i,i] = τi # store reflector in diagonal coefficient
23+
24+
# for Real, this only needs to be n-2
25+
# needed for Complex to ensure superdiagonal is Real
26+
if i <= n-1
27+
x = slice(A, i, i+1:n)
28+
conj!(x)
29+
τi = LinAlg.reflector!(x)
30+
B.ev[i] = real(A[i,i+1])
31+
LinAlg.reflectorApply!(slice(A, i+1:m, i+1:n), x, τi)
32+
A[i,i+1] = τi
33+
end
34+
end
35+
B.isupper = true
36+
37+
B, PackedUVt(A)
38+
end
39+
40+
function bidiagonalize_tall!{T}(A::Matrix{T})
41+
m,n = size(A)
42+
R = real(T)
43+
B = Bidiagonal(Array(R,n),Array(R,n-1),true)
44+
bidiagonalize_tall!(A,B)
45+
end
46+
47+
function Base.full{T}(P::PackedUVt{T};thin=true)
48+
A = P.A
49+
m,n = size(A)
50+
51+
# U = Q_1 ... Q_n I_{m,n}
52+
w = thin ? n : m
53+
U = eye(T,m,w)
54+
for i = n:-1:1
55+
τi = A[i,i]
56+
x = slice(A, i:m, i)
57+
LinAlg.reflectorApply!(x, τi', slice(U, i:m, i:w))
58+
end
59+
60+
# Vt = P_{n_2} ... P_1
61+
Vt = eye(T,n,n)
62+
for i = n-1:-1:1
63+
τi = A[i,i+1]
64+
x = slice(A, i, i+1:n)
65+
LinAlg.reflectorApply!(slice(Vt, i:n, i+1:n), x, τi')
66+
end
67+
U,Vt
68+
end

src/utils.jl

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import Base.LinAlg: reflectorApply!
2+
@inline function reflectorApply!(A::StridedMatrix, x::AbstractVector, τ::Number) # apply conjugate transpose reflector from right.
3+
m, n = size(A)
4+
if length(x) != n
5+
throw(DimensionMismatch("reflector must have same length as second dimension of matrix"))
6+
end
7+
@inbounds begin
8+
for i = 1:m
9+
Aiv = A[i, 1]
10+
for j = 2:n
11+
Aiv += A[i, j]*x[j]
12+
end
13+
Aiv = Aiv*τ
14+
A[i, 1] -= Aiv
15+
for j = 2:n
16+
A[i, j] -= Aiv*x[j]'
17+
end
18+
end
19+
end
20+
return A
21+
end
22+
23+
24+
import Base: A_mul_B!, A_mul_Bc!, A_ldiv_B!
25+
26+
A_mul_B!(G::LinAlg.Givens, ::Void) = nothing
27+
A_mul_Bc!(::Void, G::LinAlg.Givens) = nothing
28+
29+
function A_ldiv_B!{Ta,Tb}(A::SVD{Ta}, B::StridedVecOrMat{Tb})
30+
T = promote_type(Ta,Tb)
31+
k = length(find(A.S .> eps(real(T))*maximum(A.S)))
32+
A.Vt[1:k,:]' * (A.S[1:k] .\ (A.U[:,1:k]' * B))
33+
end

0 commit comments

Comments
 (0)