まぃふぇいばりっと

機械学習やってます.Julia大好きです.勉強したことの殴り書きです.

Python 行列の列をソートする.

ある行に着目して,ソートする方法なんかはぐぐればでてくるんです.これとか

stackoverflow.com

でも,例えば

array([[1, 2, 1],
       [0, 2, 1],
       [0, 0, 0],
       [0, 1, 0],
       [2, 2, 2],
       [2, 1, 0],
       [1, 2, 2],
       [2, 2, 1]])

array([[0, 0, 0],
       [0, 1, 0],
       [0, 2, 1],
       [1, 2, 1],
       [1, 2, 2],
       [2, 1, 0],
       [2, 2, 1],
       [2, 2, 2]])

とするにはどうしたよいだろうか.結構簡単で,こうすればできる.

data = np.array([[1,2,1],
                   [0,2,1],
                   [0,0,0],
                   [0,1,0],
                   [2,2,2],
                   [2,1,0],
                   [1,2,2],
                   [2,2,1]])
sort_keys = tuple(data[:, col] for col in range(data.shape[1]-1, -1, -1))
sort_indices = np.lexsort(sort_keys)
sorted_data = data[sort_indices]

Julia言語 ボルツマンマシンからのサンプリング

メトロポリス法で,ボルツマンマシンからサンプリングするコードを書きました.

とりあえず,状態xを与えたら,エネルギーを返してくれる関数を用意しておきます.

using Plots
using Random

function energy(x,b,w)
    N = length(b)
    term_1_body = sum( x .* b )
    term_2_body = sum( w[i,j] * x[i] * x[j] for i = 1:N for j = i:N)
    E = term_1_body + term_2_body
    return E
end

function delta_energy(x,y,b,w)
    return energy(y,b,w) - energy(x,b,w)
end

次に標準的なメトロポリス法を実装します.

function sample_BM(b,w;beta=1, max_iter=5000)
    N = length(b) # number of spins
    x = rand([-1,1],N) # initial spins
    
    x_history = []
    for iter = 1:max_iter
        trial_state = copy(x)
        
        # Select one spin
        target_spin_idx = rand(1:N)
        # Flip the spin
        if trial_state[target_spin_idx] == 1 
            #trial_state[target_spin_idx] = 0
            trial_state[target_spin_idx] = -1
        else
            trial_state[target_spin_idx] = 1 
        end
        
        del_ene = delta_energy(trial_state,x,b,w)
        if del_ene < 0
            x = trial_state
        elseif del_ene > 0
            a = rand()
            if a < exp(-beta * del_ene)
                x = trial_state
            end
        end
        if iter % 1000 == 0
            @show iter
            push!(x_history,x)
        end
    end
    
    # you can see the state as 
    # display( reshape(x, (L,M))')
    return x, x_history
end

あとは,相互作用wと磁場bを与えたら動きます. 2D Ising ならこんな感じです.

function define_b_w(M,L,J,H)
    N = M * L
    b = H*ones(N)
    
    w = zeros(N,N)
    for m = 0:M-1
        for l = 1:L
            if l < L
                w[m*L+l, m*L+l+1] = J
            end
            if 1 < l
                w[m*L+l, m*L+l-1] = J
            end
            if 0 < m
                w[m*L+l, m*L+l-L] = J
            end
            if m < M-1
                w[m*L+l, m*L+l+L] = J
            end
        end
    end
    return b,w
end

実行してみます.betaは逆温度で,Jが相互作用の強さ,Hが磁場の強さですね.

M = 40; L = 40; J = 1; H = 0.1; beta=0.1;
b, w = define_b_w(M,L,J,H);

x, x_history = sample_BM(b,w;beta=beta,max_iter=5000)
x = reshape(x, (L,M))';
plt=heatmap(x, aspect_ratio=:equal, axis=false, grid=false, size=(400,400), cbar=false)

ちなみに相互作用を大きくした低温だとこんな感じ.

Julia言語 CPランクが落ちているランダムテンソルをえる

CPランクが落ちているテンソルが欲しい場合

outer(v...) = reshape(kron(reverse(v)...),length.(v))
function get_low_CP_tensor(J, R)
    D = length(J)
    v = Vector{Array}(undef, D)
    T = zeros(J...)
    for r = 1 : R
        for d = 1 : D
            v[d] = rand(J[d])
        end
        T .+= outer(v...)
    end
    return T
end

Julia 言語 非負テンソルトレイン分解の実装

研究で必要だったので実装しました. 参考にした論文はこちらです.

link.springer.com

このアルゴリズムでは,NMFが必要になるので取り急ぎ用意しておきます.

using LinearAlgebra
using Random

function KL(A, B)
    n, m = size(A)
    kl = 0.0
    for i = 1:n
        for j = 1:m
            kl += A[i,j] * log( A[i,j] / B[i,j] ) - A[i,j] + B[i,j]
        end
    end
    return kl
end

function nmf_euc(X, r ; max_iter=200, tol = 0.0001)
    m, n = size(X)
    W = rand(m, r)
    H = rand(r, n)

    error_at_init = norm(X - W*H)
    previous_error = error_at_init
    for iter in 1:max_iter
        H .=  H .* ( W' * X ) ./ ( W' * W * H  )
        W .=  W .* ( X  * H') ./ ( W  * H * H' )

        if tol > 0 && iter % 10 == 0
            error = norm(X - W*H)
            if (previous_error - error) / error_at_init < tol
                break
            end
            previous_error = error
        end
    end

    return W, H
end

function nmf_kl(X, r ; max_iter=200, tol = 0.0001)
    m, n = size(X)
    W = rand(m, r)
    H = rand(r, n)
    one_mn = ones(m, n)

    error_at_init = KL(X, W*H)
    previous_error = error_at_init
    for iter in 1:max_iter
        println(KL(X, W*H))
        H .=  H .* ( W' * ( X ./ (W*H))) ./ ( W' * one_mn )
        W .=  W .* ( (X ./ (W*H) ) * H') ./ ( one_mn * H' )

        if tol > 0 && iter % 10 == 0
            error = KL(X, W*H)
            if (previous_error - error) / error_at_init < tol
                break
            end
            previous_error = error
        end
    end

    return W, H
end

次に論文中のアルゴリズム1を実装します.

function NNTF(A, r; max_iter=200, tol = 0.0001)
    C = A
    r0 = 1
    d = ndims(A)
    n = size(A)
    G = Vector{Array{Float64}}(undef,d)
    for i = 1:(d-1)
        if i == 1
            C = reshape(C, (r0*n[i],:))
        else
            C = reshape(C, (r[i-1]*n[i],:))
        end
        W, H = nmf_euc(C, r[i], max_iter=max_iter, tol=tol)
        H .= norm(W).* H
        W .= W ./ norm(W)
        if i == 1
            G[i] = reshape(W, (1, n[i], r[i]))
        else
            G[i] = reshape(W, (r[i-1], n[i], r[i]))
        end
        C = H
    end
    G[d] = reshape(C, (r[d-1],n[d],1))
   
    return G
end

これで分解はOKなのですが,コアテンソル(train)からテンソルを再構成するための関数を用意しておきます.

outer(v...) = reshape(kron(reverse(v)...),length.(v))
function reconst_train(G)
    D = length(G)
    sizesG = size.(G)
    R = zeros(Int64,D)
    J = zeros(Int64,D)
    for d = 1:D
        R[d] = sizesG[d][1]
        J[d] = sizesG[d][2]
    end
    rs = ( (1:R[d]) for d in 1:D)
    
    term = zeros(Float64, J...)
    for r in product(rs...)
        v = Vector{Array{Float64}}(undef, D)
        for d = 1:D
            if d == 1
                v[d] = G[1][ 1, :, r[2]]
            elseif d == D
                v[d] = G[D][ r[D], :, 1]
            else
                v[d] = G[d][ r[d], :, r[d+1]]
            end
        end
        term += outer(v...)
    end

    return term
end

上の outer(v...)というのは手っ取り速い実装が分からなかったので,stackoverflowの強い人に教えてもらいました.

stackoverflow.com

実験

とりあえず人工データでランクを大きくしていきます.

A = rand(4,5,6,3)
G = NNTF(A,[1,1,1,1]);
@show norm(A - reconst_train(G)) / norm(A)

G = NNTF(A,[2,2,2,2]);
@show norm(A - reconst_train(G)) / norm(A)

G = NNTF(A,[5,5,5,5]);
@show norm(A - reconst_train(G)) / norm(A)

G = NNTF(A,[10,10,10,10]);
@show norm(A - reconst_train(G)) / norm(A)

G = NNTF(A,[25,25,25,25]);
@show norm(A - reconst_train(G)) / norm(A)

#norm(A - reconst_train(G)) / norm(A) = 0.503555751152826
#norm(A - reconst_train(G)) / norm(A) = 0.46242914539607927
#norm(A - reconst_train(G)) / norm(A) = 0.36833865940558996
#norm(A - reconst_train(G)) / norm(A) = 0.261642993995869
#norm(A - reconst_train(G)) / norm(A) = 0.018216782089987324

ランクを大きくすると徐々に再構成誤差が下がっているので,問題なさそうですね.

次に,実データでも実験してみます.とりあえず SIPIデータセットの Baboon を使います.オリジナルのデータサイズは 3×256×256 ですが,これを reshape して (3,16,16,16,16) にして,ターゲットランクを(R,R,R,R,R)としてトレイン分解します.Rを大きくしていくと元の画像が良く復元できていることが分かります.ちなみにR=25でRMSE=0.20くらいでした.

Windows 11 + IJulia + jypterlab + jupyterlab_vim

windows 11 に julia をインストールしたのですが,インストーラの最後の「パスを通す」というチェックボックスにチェックを忘れてしまいました.なので,以下のパスを環境変数名の編集から追加しておきます.

C:\Users\%UserName%\AppData\Local\Programs\Julia-1.8.5\bin

%UserName% の部分はユーザー名にかえてください.たとえば,ユーザー名がhogeなら,C:\Users\hoge\AppData\Local\Programs\Julia-1.8.5\bin です.

REPLを立ち上げて,add IJuliaをして,

using IJulia
jupyterlab()

とするとjypterlabがインストールされるのですが,これだとjypterlabのパスが通ってません.このままだとコマンドプロンプトやパワーシェルから,jupyterlabが呼べないです. デフォルトでは

C:\Users\%UserName%\.julia\conda\3\Scripts

にjypterはあるので,ここにパスを通します.最後に拡張機能が使えるように nodejs をインストールしておく必要があります.以下にあるように,Windows用の nodejs インストーラをダウンロードして,インストールして,再起動します.

stackoverflow.com

これで,

jupyter labextension install jupyterlab_vim

コマンドプロンプトいれておけばjupyterlab_vimをインストールできます.

Julia 言語 Ket Augumentation (KA)

行列をreshapeしてテンソルにすることあるじゃないですか.いろいろな reshape が定義できますが,ビジョン系の研究するときは,Ket Augumentation という reshape をすることがよくあるっぽいです.

この論文の第五章を読んで,KA実装しました.

https://dl.acm.org/doi/abs/10.1109/TIP.2017.2672439

入力行列は 2depth × 2depth として,これを 4×4×...×4 のテンソルにreshape します. (depth は自然数)

例えば,depth が 3,つまり 8×8 の行列 を 4×4×4 に reshape する例を考えます.図のような感じで8×8の入力 (i, j) の各要素がreshape 後のテンソルのどの要素になっているかを表した表が以下です.

例: 入力行列の(3,3)成分は reshape して得たテンソルの (1,4,1) 成分. 入力行列の(5,3)成分は reshape して得たテンソルの (1,2,3) 成分.

Julia 実装はこんな感じ.

function get_Fn(depth)
    Fp = [1 2 ; 3 4]
    Fn = 0
    for n = 1 : depth-1
        Fn_1 = Fp
        Fn_2 = Fn_1 .+ 4^n
        Fn_3 = Fn_2 .+ 4^n
        Fn_4 = Fn_3 .+ 4^n
        Fn = [Fn_1 Fn_2; Fn_3 Fn_4] 
        Fp = Fn
    end
    return Fn
end

function get_Tn(depth)
    tensor_size = 4 * ones(Int,depth)
    Tn = zeros(Int, tensor_size...)
    c = 1
    for idx in CartesianIndices(Tn)
        Tn[idx] = c
        c += 1
    end
    return Tn
end

# InvFn[ Fn[i,j] ] = (i,j)
function get_InvFn(depth)
    InvFn = Array{CartesianIndex}(undef, 4^depth);
    for i = 1:2^depth
        for j = 1:2^depth
            c = Fn[i,j]
            InvFn[c] = CartesianIndex((i,j))
        end
    end
    return InvFn
end
#InvTn[ Tn[i,j,k,l]] = (i,j,k,l)
function get_InvTn(depth)
    InvTn = Array{CartesianIndex}(undef, 4^depth);
    for idx in CartesianIndices(Tn)
        c = Tn[idx]
        InvTn[c] = idx
    end
    return InvTn
end

# tensor == reshape_to_tensor(reshape_to_mtx(tensor,InvTn, Fn),Tn,InvFn)
function reshape_to_tensor(mtx, Tn, InvFn)
    depth = Int(log(2,size(mtx)[1]))
    high_order_tensor = zeros(4*ones(Int,depth)...);
    for idx in CartesianIndices(high_order_tensor)
        m = Tn[idx]
        n = InvFn[m]
        high_order_tensor[idx] = mtx[n]
    end
    return high_order_tensor
end

# mtx == reshape_to_mtx(reshape_to_tensor(mtx,Tn,InvFn), InvTn, Fn)
function reshape_to_mtx(tensor, InvTn, Fn)
    depth = ndims(tensor)
    mtx = zeros(2^depth, 2^depth)
    for i = 1:2^depth
        for j = 1:2^depth
            m = Fn[i,j]
            idx = InvTn[m]
            mtx[i,j] = tensor[idx]
        end
    end
    return mtx
end

適当な行列をつくって,reshape してみます

depth = 3
mtx = rand(1:9,2^depth, 2^depth)
# 8×8 Matrix{Int64}:
# 8  2  4  2  5  6  6  2
# 3  7  2  9  2  5  8  1
# 3  9  9  6  4  5  3  4
# 1  1  9  7  9  8  7  2
# 6  1  6  7  9  7  1  4
# 4  5  4  9  3  4  5  8
# 3  4  8  8  1  3  8  4
# 5  7  8  5  6  1  6  5

Fn = get_Fn(depth);
Tn = get_Tn(depth);
InvFn = get_InvFn(depth);
InvTn = get_InvTn(depth);
high_order_tensor = reshape_to_tensor(mtx, Tn, InvFn)

#4×4×4 Array{Float64, 3}:
#[:, :, 1] =
# 8.0  4.0  3.0  9.0
# 2.0  2.0  9.0  6.0
# 3.0  2.0  1.0  9.0
# 7.0  9.0  1.0  7.0

#[:, :, 2] =
# 5.0  6.0  4.0  3.0
# 6.0  2.0  5.0  4.0
# 2.0  8.0  9.0  7.0
# 5.0  1.0  8.0  2.0

#[:, :, 3] =
# 6.0  6.0  3.0  8.0
# 1.0  7.0  4.0  8.0
# 4.0  4.0  5.0  8.0
# 5.0  9.0  7.0  5.0

#[:, :, 4] =
# 9.0  1.0  1.0  8.0
# 7.0  4.0  3.0  4.0
# 3.0  5.0  6.0  6.0
# 4.0  8.0  1.0  5.0

あってそうか,いくつかの要素をみてみる.

high_order_tensor[1,1,1] == mtx[1,1]
# true
high_order_tensor[4,1,1] == mtx[2,2]
# true
high_order_tensor[1,4,1] == mtx[3,3]
# true
high_order_tensor[1,4,1] == mtx[3,3]
# true
high_order_tensor[3,2,3] == mtx[6,3]
# true

大丈夫そう!

テンソルを reshape 前の行列に戻したかったら,

reshape_to_mtx(high_order_tensor, InvTn, Fn)

とすれば ok.

Julia 言語 タッカーランクが低いランダムテンソルを生成する

ランダムな低ランクテンソルが欲しいときってありますよね. テンソルのランクっていろいろありますが,とりあえずタッカーランクがひくいランダムテンソルを手に入れるコードをかきました.

もっとよい方法があったら知りたいです.

using LinearAlgebra
using TensorToolbox
function get_low_tucker_tensor(J,R)
    G = rand(R...)
    D = length(J)
    U = Vector{Matrix{Float64}}(undef,D)
    for d = 1:D
        U[d] = rand(J[d],R[d])
    end
    T = ttm(G, [U...], [1:D;])
    return T
end

ここで J が欲しいテンソルのサイズで,Rが欲しいタッカーランクです.どちらもベクトルで渡してください.たとえば,タッカーランクが(2,2,2)の3×4×5のテンソルが欲しい場合は以下のようにします.また,mrankを使うとタッカーランクが確認できます.

julia > J = [3,4,5]; R=[2,2,2];
julia > Y = get_low_tucker_tensor(J,R);
julia > mrank(Y)
(2,2,2)

Julia 言語 TensorToolbox で CP 分解して,再構成する.

Julia の TensorToolbox を使うと気軽に CP 分解ができます.

github.com

例えば,ランダムな4×4×4×4テンソルXをCP分解するなら

using TensorToolbox
X = rand(4,4,4,4)
F = cp_als(X, 3)

でOKです.ここで,ターゲットランクは3にしました.これで,F.lambda固有値もどき(テンソル分解なので行列の意味での固有値ではない)が格納され,F.fmat[1],F.fmat[2],F.fmat[3],F.fmat[4] には因子が格納されます.これらの因子から元のテンソルを復元すると,(ターゲットランクが十分に大きければ)それは入力テンソルXの近似になっているはずです.

ということで,F.matF.lambdaから元のテンソルを再構成するコードをかきました.もっとよい方法があったら教えて下さい.

using LinearAlgebra
using TensorToolbox

function reconst(F)
    D = ndims(F)
    R = length(F.lambda) 
    F1 = F.lambda[1] * ttt(F.fmat[1][:,1], F.fmat[2][:,1])
    for d = 3:D
        F1 = ttt(F1, F.fmat[d][:,1])
    end
    for r = 2:R
        Fr = F.lambda[r] * ttt(F.fmat[1][:,r], F.fmat[2][:,r])
        for d = 3:D
            Fr = ttt(Fr, F.fmat[d][:,r])
        end
        F1 .= F1 .+ Fr
    end
    return F1
end

これで reconst(F) は X を近似するはず.

julia> X = rand(3,3,3,3);

julia> for r=1:5:30
           @show r,norm( reconst(cp_als(X,r)) - X ) / norm(X)
       end
(r, norm(reconst(cp_als(X, r)) - X) / norm(X)) = (1, 0.4363688254611265)
(r, norm(reconst(cp_als(X, r)) - X) / norm(X)) = (6, 0.29372126298825085)
(r, norm(reconst(cp_als(X, r)) - X) / norm(X)) = (11, 0.19977496289421565)
(r, norm(reconst(cp_als(X, r)) - X) / norm(X)) = (16, 0.06927809613284029)
(r, norm(reconst(cp_als(X, r)) - X) / norm(X)) = (21, 0.022644681361821475)
(r, norm(reconst(cp_als(X, r)) - X) / norm(X)) = (26, 7.873258786342794e-6)

確かにターゲットランク r を大きくするとどんどん再構成誤差(を入力テンソルの総和で割った値)が小さくないって行く様子がわかる.一応,テストとして,

mrank( reconst( cp_als(X,r) ) ) == (r,r,r,r)

になっている.

しかし,CP分解って,こんなにターゲットランクが大きくないと再構成うまくいかないのか???

Julia言語 Nyström method による高速なカーネル回帰

カーネル回帰においてデータ数が多くてカーネル行列が巨大になると,逆行列の計算がボトルネックになる.そういう時は random feature map を用いて前処理行列を構成して線型方程式を反復法で高速に解く処方箋(この文献のアルゴリズム1参照)もあるみたい.ここでは,カーネル行列を Nyström 法で低ランク近似して,Sherman-Morrison-Woodbury 公式で効率的に逆行列を近似する方法を実装した.参考にしたのは以下の文献.

・以下文献の式(36)-(38) proceedings.mlr.press

・以下文献の式(5)

ojs.aaai.org

とりあえずカーネル行列を使えるようにkernes.jlと名付けられた次のファイルを同一ディレクトリにいれておく.

function kernel_function(kernel)
    if kernel == "rbf"
        return (x,y,c,deg) -> exp( -c*norm(x.-y)^2 )
    elseif kernel == "lapras"
        return (x,y,c,deg) -> exp(-c*sum( abs.(x .- y)))
    elseif kernel == "poly"
        return (x,y,c,deg) -> (c+x'*y)^deg
    elseif kernel == "linear"
        return (x,y,c,deg) -> (0+x'*y)^1
    elseif kernel == "periodic"
        return (x,y,c,deg) -> exp(deg* cos(c * norm(x .- y)))
    else
        error("Kernel $kernel is not defined")
    end
end

function Kernel(X::Matrix, Y::Matrix; c=1.0, deg=3.0, kernel="rbf")
    kf = kernel_function(kernel)
    d, n = size(X)
    d, m = size(Y)
    K = Matrix{Float64}(undef, m, n)
    for j = 1:n
        for i = 1:m
           K[i,j] = kf(Y[:,i],X[:,j],c,deg)
        end
    end
    return K
end

function Kernel(X::Matrix; c=1.0, deg=3.0, kernel="rbf")
    kf = kernel_function(kernel)
    d, n = size(X)
    K = Matrix{Float64}(undef, n, n)
    for j = 1:n
        for i = 1:j
           K[i,j] = kf(X[:,i],X[:,j],c,deg)
        end
    end
    return Symmetric(K)
end

次に本体とnystrom法.

using LinearAlgebra
include("kernels.jl")

"""
low-rank approximation based on random sampling.
See the following reference,
[Petros Drineas et al.](https://www.jmlr.org/papers/v6/drineas05a.html)
# Aruguments
- 'K' : input positive definite matrix.
- 'p' : target rank

example:
    A = randn(100,100)
    K = A*A'
    for p in [1,5,10,20,30,50,100]
        Kr = nystrom(K, p)
        @show p, norm(K-Kr)
    end
"""
function nystrom(K,p)
    n, n = size(K)
    idxs = sample(1:n, p, replace=false, ordered=false)
    C = K[:,idxs]
    W = K[idxs,idxs]
    invWct = W \ C'
    return C, invWct
end

function Kreg(X_train, Y_train, X_test;lambda=1, c=1, deg=3.0, kernel="rbf", sampling=false, p=10)
    K_train_train = Kernel(X_train, c=c, deg=deg, kernel=kernel)
    k_train_test = Kernel(X_train, X_test, c=c, deg=deg, kernel=kernel)

    # number of test data
    M = size(X_test)[2]
    # number of target variables
    N = size(Y_train)[1]

    Y_pred = zeros(N,M)
    for n = 1:N
        if sampling
            C, invWCt = nystrom(K_train_train, p)
            alpha = 1.0 / lambda .* ( I - C* ((lambda*I + invWCt*C) \ invWCt) ) * Y_train[n,:]
        else
            alpha = (K_train_train + lambda * I) \ Y_train[n,:]
        end
        Y_pred[n,:] .= k_train_test * alpha
    end
    return Y_pred
end


main()

適当な3次元ベクトル場を学習してみる.とりあえずデータ数13000でやってみよう.

using Distributions
using BenchmarkTools

function make_vector_field(L)
    f(x,y,z) = [x+y+z, x*cos(y)-y, -y+x+z]
    dist = Uniform(-2, 2)
    x = rand(dist,(3, L))
    y = Matrix{Float64}(undef, 3, L)
    for i = 1:L
        y[:,i] = f(x[:,i]...)
    end
    return x, y
end

function eval(y_test, y_pred)
    L = length(y_test)
    cost = norm(y_test[:] .- y_pred[:])^2 / L
    return cost
end

function main()
    x_train, y_train = make_vector_field(13000)
    x_test, y_test = make_vector_field(100)

    y_pred_0 = Kreg(x_train, y_train, x_test, kernel="rbf" ,lambda=1, c=1, sampling=false)
    @show norm(y_pred_0 - y_test)
    @btime Kreg($x_train, $y_train, $x_test, kernel="rbf" ,lambda=1, c=1, sampling=false)
    println("")
    ps = [5,10,20,30,40,50,100]
    for p in ps
        y_pred = Kreg(x_train, y_train, x_test, kernel="rbf" ,lambda=1, c=1, sampling=true, p=p)
        @show p, norm(y_pred - y_test)
        @btime Kreg($x_train, $y_train, $x_test, kernel="rbf" ,lambda=1, c=1, sampling=true, p=$p)
        println("")
    end
end

nystrom法を使わないと56秒. nystrom法を使うとpが十分に小さければ,37秒.そんなに大差ないですね.誤差もでかいし.

norm(y_pred_0 - y_test) = 0.6526581055689884
  56.314 s (429032544 allocations: 38.25 GiB)

(p, norm(y_pred - y_test)) = (5, 12984.632037633266)
  37.790 s (429032592 allocations: 42.01 GiB)

(p, norm(y_pred - y_test)) = (10, 9540.59736297309)
  37.849 s (429032592 allocations: 42.02 GiB)

(p, norm(y_pred - y_test)) = (20, 4353.575356488931)
  37.820 s (429032601 allocations: 42.03 GiB)

(p, norm(y_pred - y_test)) = (30, 2527.110361708636)
  37.764 s (429032601 allocations: 42.04 GiB)

(p, norm(y_pred - y_test)) = (40, 1576.9397341759382)
  37.998 s (429032601 allocations: 42.04 GiB)

(p, norm(y_pred - y_test)) = (50, 1096.2351214109729)
  37.746 s (429032616 allocations: 42.05 GiB)

(p, norm(y_pred - y_test)) = (100, 270.4954692388678)
  38.273 s (429032616 allocations: 42.10 GiB)

試しにデータ数を130でpを1から130まで動かしてみる.

norm(y_pred_0 - y_test) = 12.090595437011599
  5.766 ms (107610 allocations: 8.76 MiB)

(p, norm(y_pred - y_test)) = (5, 100.97716320731487)
  5.538 ms (107652 allocations: 9.00 MiB)

(p, norm(y_pred - y_test)) = (10, 55.38732928606096)
  5.636 ms (107643 allocations: 9.06 MiB)

(p, norm(y_pred - y_test)) = (20, 35.78078677983344)
  5.855 ms (107652 allocations: 9.18 MiB)

(p, norm(y_pred - y_test)) = (30, 17.860651613527118)
  6.004 ms (107652 allocations: 9.33 MiB)

(p, norm(y_pred - y_test)) = (40, 12.800274289390913)
  6.218 ms (107652 allocations: 9.50 MiB)

(p, norm(y_pred - y_test)) = (50, 10.539272651439447)
  6.371 ms (107667 allocations: 9.69 MiB)

(p, norm(y_pred - y_test)) = (100, 11.428565046455178)
  7.411 ms (107667 allocations: 11.00 MiB)

(p, norm(y_pred - y_test)) = (130, 12.090595437011602)
  8.118 ms (107667 allocations: 12.06 MiB)

うーん,やっぱり微妙.target rank のpがかなり小さくないと高速化の効果はでなそう.ボトルネックになっているのは, alpha = 1.0 / lambda .* ( I - C* ((lambda*I + invWCt*C) \ invWCt) ) * Y_train[n,:] の計算っぽい. で,pが小さいと近似精度はひくすぎる.

いまはランダムサンプリングで,nystrom法を使っているけど,もっと効果的なsamplingの方法があるので,そちらを実装する必要があるかもしれない.

Julia言語 Nyström method による行列の低ランク近似

ようやく実装できた.参考にした論文は以下の2つ.

proceedings.neurips.cc

ojs.aaai.org

それにしても,こんなに著名な手法のjulia実装でどこにもあがってないのはちょっと驚き. サンプリングの方法はいろいろあるんだけど,最も古典的(?)と思われるランダムサンプリングにしている.

using LinearAlgebra
using StatsBase

"""
low-rank approximaion based on random sampling.
See the following reference,
[Petros Drineas et al.](https://www.jmlr.org/papers/v6/drineas05a.html)
# Aruguments
- 'K' : input positive definite matrix.
- 'p' : target rank

example:
    A = randn(100,100)
    K = A*A'
    for p in [1,5,10,20,30,50,100]
        Kr = nystrom(K, p, reconst=true)
        @show p, norm(K-Kr)
    end
"""

function nystrom(K,p;reconst=false)
    n, n = size(K)
    idxs = sample(1:n, p, replace=false, ordered=false)
    C = K[:,idxs]
    W = K[idxs,idxs]

    if reconst == true
        return C*( W \ C')
    else
        return C, inv(W)
    end
end

テストがてら,適当な正定値行列Kをつくって,ランクp近似をしてみる. pを大きくしていくにつれて,再構成誤差がさがるはず...

A = randn(100,100)
K = A*A'
for p in [1,5,10,20,30,50,100]
    Kr = nystrom(K, p, reconst=true)
    @show p, norm(K-Kr)
end

#(p, norm(K - Kr)) = (1, 1383.25284972177)
#(p, norm(K - Kr)) = (5, 1296.5715363623078)
#(p, norm(K - Kr)) = (10, 1211.8022048540815)
#(p, norm(K - Kr)) = (20, 1005.0471242087337)
#(p, norm(K - Kr)) = (30, 812.8200435737806)
#(p, norm(K - Kr)) = (50, 506.67283549630315)
#(p, norm(K - Kr)) = (100, 1.4370668157705365e-8)

pを大きくすると,どんどん再構成誤差が小さくなっている.問題なく実装できてそう.

Julia言語 多値のカーネル回帰

通常,カーネル重回帰では入力がベクトル,出力がスカラーの関数を学習する訳ですが,入力も出力もベクトルにしたいことがあります.そんなときに使える手法を次の文献で勉強しました.

link.springer.com

ieeexplore.ieee.org

いろいろ調べても実装している人がいないっぽいので自分で実装した.丸一日かかってしまった...

using LinearAlgebra

function kernel_function(kernel)
    if kernel == "rbf"
        return (x,y,c,deg) -> exp( -c*norm(x.-y)^2 )
    elseif kernel == "lapras"
        return (x,y,c,deg) -> exp(-c*sum( abs.(x .- y)))
    elseif kernel == "poly"
        return (x,y,c,deg) -> (c+x'*y)^deg
    elseif kernel == "linear"
        return (x,y,c,deg) -> (0+x'*y)^1
    elseif kernel == "periodic"
        return (x,y,c,deg) -> exp(deg* cos(c * norm(x .- y)))
    else
        error("Kernel $kernel is not defined")
    end
end

function Kernel(X::Matrix, Y::Matrix; c=1.0, deg=3.0, kernel="rbf")
    kf = kernel_function(kernel)
    d, n = size(X)
    d, m = size(Y)
    K = Matrix{Float64}(undef, m, n)
    for j = 1:n
        for i = 1:m
           K[i,j] = kf(Y[:,i],X[:,j],c,deg)
        end
    end
    return K
end

function Kernel(X::Matrix; c=1.0, deg=3.0, kernel="rbf")
    kf = kernel_function(kernel)
    d, n = size(X)
    K = Matrix{Float64}(undef, n, n)
    for j = 1:n
        for i = 1:j
           K[i,j] = kf(X[:,i],X[:,j],c,deg)
        end
    end
    return Symmetric(K)
end


function vecKernel(X::Matrix, dim_output::Int; c=1, deg=3.0, omega=0.5, kernel="rbf", block_kernel="ones")
    kf = kernel_function(kernel)
    _, n = size(X)
    K = Matrix{Float64}(undef, n*dim_output, n*dim_output)
    for j = 1:n
        for i = 1:j
            K[(i-1)*dim_output+1:i*dim_output,(j-1)*dim_output+1:j*dim_output] = (
                kf(X[:,i], X[:,j],c,deg) *
                block_kernel_A(X[:,i], X[:,j], dim_output, block_kernel, c, omega)
               )
        end
    end
    return Symmetric(K)
end

function vecKernel(X::Matrix, Y::Matrix; c=1, deg=3.0, omega=0.5, kernel="rbf", block_kernel="ones")
    kf = kernel_function(kernel)
    dim_input,  n = size(X)
    dim_output, m = size(Y)
    K = Matrix{Float64}(undef, m*dim_output, n*dim_output)
    for j = 1:n
        for i = 1:m
            K[(i-1)*dim_output+1:i*dim_output,(j-1)*dim_output+1:j*dim_output] = (
                kf(Y[:,i], X[:,j],c,deg) *
                block_kernel_A(Y[:,i], X[:,j], dim_output, block_kernel, c, omega)
               )
        end
    end
    return K
end

"""
The matrix `A` represent the relation between
output elements. If `A` is `I`, the outputs are independent.
"""
function block_kernel_A(xi::Vector, xj::Vector, dim_output::Int, block_kernel, c, omega)
    if block_kernel == "id"
        A = Matrix{Float64}(I,dim_output,dim_output)
    elseif block_kernel == "ones"
        A = ones(Float64, dim_output, dim_output)
    elseif block_kernel == "ones_id"
        A = ( omega*ones(Float64, dim_output, dim_output)
             .+ (1-omega*dim_output)*Matrix{Float64}(I, dim_output, dim_output))
    elseif block_kernel == "df"
        @assert dim_output == size(xi)[1]
            "df is available only when dim of input and output are same"
        A = block_kernel_df(xi,xj,c=c)
    elseif block_kernel == "cf"
        @assert dim_output == size(xi)[1]
            "cf is available only when dim of input and output are same"
        A = block_kernel_cf(xi,xj,c=c)
    else
        error("block kernel $block_kernel is not defined")
    end
    return A
end

"""
Divergence-free kernel.
The dim of output variable and input variable have to be same.
The variance 1/σ^2 = c
"""
function block_kernel_df(xi::Vector, xj::Vector; c=1)
    m = size(xi)[1]
    dx = xi .- xj
    normdx_2 = norm(dx)^2
    A = 2*c*(2*c*(dx*dx')+(m-1-2*c*normdx_2)*I)
    return A
end

"""
Curl-free kernel.
The dim of output variable and input variable have to be same.
The variance 1/σ^2 = c
"""
function block_kernel_cf(xi::Vector, xj::Vector; c=1)
    m = size(xi)[1]
    dx = xi .- xj
    A = 2*c*( Matrix{Float64}(I,m,m) - 2 * c * (dx * dx'))
    return A
end

"""
Vector-valud function learning with kernel method.
See the following references,
[Mauricio Alvarez et al.](https://ieeexplore.ieee.org/document/8187598)
[Luca Baldassare et al.](https://link.springer.com/article/10.1007/s10994-012-5282-y)

Learn the projection f(x) : x -> y, where both of x and y are vectorvalued.
Let us assume x is in R^k, y is in R^d.
n is the number of the training data.
k is the number of the test data.
# Arguments
- 'X_train' : k \times n matrix, training data of input
- 'Y_train' : d \times n matrix, training data of output
- 'X_test' : d \times m matrix, test data of input.
- 'kernel' : 'rbf','lapras','poly','linear','periodic'
- 'block_kernel' : 'id', 'ones', 'df', 'cf'. if you chouse 'id', the elements of output are independent each other.
- 'lambda' : the value of regularization paramter
"""
function multiKreg(X_train, Y_train, X_test; kernel="rbf", block_kernel="df", lambda=1.0, c=1.0, deg=3.0, omega=0.5)
    # dim of output variable
    d = size(X_train)[1]
    # number of test data
    n = size(X_test)[2]

    K_train_train = vecKernel(X_train, d, c=c, deg=deg, omega=omega, kernel=kernel, block_kernel=block_kernel)
    K_train_test = vecKernel(X_train, X_test, c=c, deg=deg, omega=omega, kernel=kernel, block_kernel=block_kernel)

    # in the original paper, they define
    # c_vec = (K_train_train + lambda*n*I) \ Y_train[:]
    c_vec = (K_train_train + lambda*I) \ Y_train[:]
    Y_pred = K_train_test * c_vec
    Y_pred = reshape(Y_pred, (:,n))
    return Y_pred
end

k(xi,xj)は,入力ベクトル間の内積で,block_kernel_Aで求めておく行列Aは,出力変数間の関係性の強さを表している. A単位行列(=id)だと,出力変数は全て独立になる.性質の良いAとして,Divergence-freeのものと,Curl-freeのものが知られているのでこれも実装しておいた. この2つは,入力ベクトルと出力ベクトルの次元が同じじゃないと使えないので注意. ones_idは以下の論文の5-2-2節で出てくるもので,単位行列と1行列の和.よく使われるのかは分からないけど,とりあえず実装しておいた.

www.sciencedirect.com

普通のカーネル法を出力する各変数について独立に適用して学習するカーネル回帰はこちら.

using LinearAlgebra

function Kreg(X_train, Y_train, X_test;lambda=1, c=1, deg=3.0, kernel="rbf")
    K_train_train = Kernel(X_train, c=c, deg=deg, kernel=kernel)
    k_train_test = Kernel(X_train, X_test, c=c, deg=deg, kernel=kernel)

    # number of test data
    M = size(X_test)[2]
    # number of target variables
    N = size(Y_train)[1]

    Y_pred = zeros(N,M)
    for n = 1:N
        alpha = (K_train_train + lambda * I) \ Y_train[n,:]
        Y_pred[n,:] .= k_train_test * alpha
    end
    return Y_pred
end

適当なベクトル場を作って,学習してみる.学習に使った要素は1000個.テストには500個つかった. evalはコスト関数.カーネルの種類をかえてテストしてみる.

using Distributions
function eval(y_test, y_pred)
    L = length(y_test)
    cost = norm(y_test[:] .- y_pred[:])^2 / L
    return cost
end

function make_vector_field(L)
    f(x,y) = [x+y, x*cos(y)]
    dist = Uniform(-2, 2)
    x = rand(dist,(2, L))
    y = Matrix{Float64}(undef, 2, L)
    for i = 1:L
        y[:,i] = f(x[:,i]...)
    end
    return x, y
end

x_train, y_train = make_vector_field(1000)
x_test, y_test = make_vector_field(500)
y_pred = multiKreg(x_train, y_train, x_test, kernel="rbf", block_kernel="ones_id" ,lambda=1, c=1, omega=0.5)
@show eval(y_pred, y_test)
#eval(y_pred, y_test) = 0.5828929693931735

y_pred = multiKreg(x_train, y_train, x_test, kernel="rbf", block_kernel="df" ,lambda=1, c=1, omega=0)
@show eval(y_pred, y_test)
#eval(y_pred, y_test) = 0.31864270183369753

y_pred = multiKreg(x_train, y_train, x_test, kernel="rbf", block_kernel="cf" ,lambda=1, c=1, omega=0)
@show eval(y_pred, y_test)
#eval(y_pred, y_test) = 0.052348540778465624

なお,ones_idomega=0とした場合は,A単位行列になるので,各出力変数について独立に学習することに等しい. 実際,Kregと比較してみるとコスト関数は全く同じになる.

y_pred = multiKreg(x_train, y_train, x_test, kernel="rbf", block_kernel="one_id" ,lambda=1, c=1, omega=0)
@show eval(y_pred, y_test)
#eval(y_pred, y_test) = 0.003875811228195186

y_pred = multiKreg(x_train, y_train, x_test, kernel="rbf", block_kernel="id" ,lambda=1, c=1, omega=0)
@show eval(y_pred, y_test)
#eval(y_pred, y_test) = 0.003875811228195186

y_pred = Kreg(x_train, y_train, x_test, lambda=1, c=1, kernel="rbf")
@show eval(y_pred, y_test)
#eval(y_pred, y_test) = 0.003875811228195175

なお,学習したベクトル場を描画したいときは次のようにするとよい.

using Plots
using GRUtils
y_pred = multiKreg(x_train, y_train, x_test, kernel="rbf", block_kernel="cf" ,lambda=1, c=1, omega=0.0)
@show eval(y_pred, y_test)

GRUtils.quiver(x_test[1,:], x_test[2,:], y_pred[1,:], y_pred[2,:], arrowscale=1)
GRUtils.savefig("predict.png")

GRUtils.quiver(x_test[1,:], x_test[2,:], y_test[1,:], y_test[2,:], arrowscale=1)
GRUtils.savefig("test.png")

これで推測されたベクトル場predict.pngと,正解のベクトル場test.pngが求まった.

推測されたベクトル場(predict.png)

正解のベクトル場(test.png)

まぁよく学習できてそうである.

Julia 言語 いろいろな方法で固有値をr個,大きい順に計算する関数を用意した.

よく研究で使うのでメモがてら,ここにおいておこう.

行列K固有値を大きい順にrank個求める. 計算手法としては,lapack, lapack_sym, arpack, nys, randomizedの5種類. lapackarpack以外は入力行列が対称であるという条件を課しているので注意. nysは入力に正定値性を課しているので注意.

using Arpack
using LinearAlgebra
function get_eigen_system(K, rank; eigen_method="lapack")
    n = size(K)[1]
    if eigen_method == "lapack"
        f = eigen(K, sortby=-)
        lambda = f.values
        alpha = f.vectors
    elseif eigen_method == "lapack_sym"
        lambda, alpha = eigen(K, n-rank+1:n)
        lambda .= lambda[end:-1:1]
        alpha .= alpha[:, end:-1:1]
    elseif eigen_method == "arpack"
        lambda, alpha = eigs(K, nev=rank, which=:LR)
    elseif eigen_method == "nys"
        lambda, alpha = evdnys(K, rank, reconst=false)
    elseif eigen_method == "randomized"
        lambda, alpha = reigen(K, rank, reconst=false)
    else
        error("Eigenvalue Decomposion method $eigen_method is not defined")
    end
    return lambda, alpha
end

何度も載せているが,nysrandomizedの実装はこちら.

using LinearAlgebra

"""
Randomized eigenvalue decomposition via Nystrom Method.
See Fig 6 in the [original paper](https://arxiv.org/abs/1607.01649)
or See Algorithm 5.5 in the [original paper](https://epubs.siam.org/doi/10.1137/090771806)
This method is available for only a positive definite matrix.

# Aruguments
- 'A' : input positive definite matrix
- 'k' : target rank, number of wanted eigenvalues
- 'p' : over-sampling parameter, it should be 10 or 2k.
- 'reconst' : if ture, the output is reconstracted matrix which approximates 'A'

example:
    # make a positive definite matrix `A`.
    n = 5000; m = 25000; x = randn(m,n)
    demean_x = x .- mean(x, dims=1)
    A = demean_x'*demean_x ./(m-1)

    # run evdnys
    lowrank_mtx = evdnys(A,10, reconst=true)
"""
function evdnys(A::Symmetric, k::Int;p=10, reconst=false)
    n, n = size(A)
    G = randn(n, k+p)
    Y = A * G
    Q = qr(Y).Q*Matrix(I,n,k+p)
    B1 = A*Q
    B2 = Q'*B1
    C = cholesky(Symmetric(B2))
    #F = B1 * inv(C.U)
    F = B1 / C.U
    U, Σ, _ = svd(F)

    if reconst
        return U*diagm(Σ.^2)*U'
    else
        return Σ.^2, U
    end
end

"""
Randomized eigenvalue decomposition proposed by N.halko et al.
See Algorithm 5.6. in the
[original paper](https://epubs.siam.org/doi/10.1137/090771806).
This method is effective matrices with decaying eigenvalues.

# Aruguments
- 'A' : input positive definite matrix
- 'r' : target rank, number of wanted eigenvalues
- 'p' : over-sampling parameter, it should be 10 or 2r.
- 'reconst' : if ture, the output is reconstracted matrix which approximates 'A'

example:
    # make a matrix with decaying eigenvalues
    n = 150; A = rand(n,n); B = zeros(n,n)
    e, v = eigen(Symmetric(A))
    for i in 1:n
        B += e[i] * v[:,i] * v[:,i]' * (i>n-100 ? 1 : 1e-3)
    end

    # run reigen
    lowrank_mtx = reigen(A, 10, reconst=true)
"""
function reigen(A, r ;p=10, twopass=false, reconst=false)
    n = size(A)[1]
    m = r + p

    Ω = randn(n, m)
    Y = A*Ω

    if twopass
        Q = qr!(Y).Q * Matrix(I,n,m)
        T = Q' * A * Q
    else
        Q = qr(Y).Q * Matrix(I,n,m)
        T = (Q' * Y) / ( Q' * Ω )
    end
    λ, S = eigen(T)
    U = Q*S'
    if reconst
        Λ = Diagonal(λ)
        return U*Λ*U'
    else
        return λ,U
    end
end

Julia言語 逆行列の高速な計算のために,右除算演算子と左除算演算子を利用しよう.

数値計算逆行列の計算を避けましょうというのはもはや常識と思います.例えば,givenの行列ABについて,C = inv(A) * B を計算したいときは,左除算演算子\を使って

using LinearAlgebra
C = A \ B

とした方が速いです.実際に

using BenchmarkTools

n = 6000
A = rand(n,n)
B = rand(n,n)
@btime inv($A) * $B
# 3.458 s (8 allocations: 552.29 MiB)
@btime $A \ $B 
# 2.206 s (6 allocations: 552.29 MiB)

念のため,inv(A) * BA \ B がほとんど同じ出力であることを確認しておきます.

norm( inv(A) * B - A \ B)
#2.2633599357010303e-9

一方で,C = B * inv(A)を高速に計算したいときはどうしたらよいのでしょうか.こういう時は,実は右除算演算子/を使えばよいらしいです.知らなかったので,早速比較実験してみると

@btime $B * inv($A)
# 3.474 s (8 allocations:980.53 MiB)
@btime $B / $A 
# 2.717 s (8 allocations: 976.62 MiB)

有意差があるように思います.念のため B * inv(A)B/A が同じことを確認します.

norm( B * inv(A) - B/A)
#4.5818497409971097e-8

一緒ですね!

ということで,右除算演算子と左除算演算子を駆使して,今後はinvは使わないようにしましょう~.

追記

最近書いてた,Randomizedな固有値分解の実装にinvが含まれていたので,改めてinvが含まれていない形に書き直しておいた.

using LinearAlgebra

"""
Randomized eigenvalue decomposition via Nystrom Method.
See Fig 6 in the [original paper](https://arxiv.org/abs/1607.01649)
or See Algorithm 5.5 in the [original paper](https://epubs.siam.org/doi/10.1137/090771806)
This method is available for only a positive definite matrix.

# Aruguments
- 'A' : input positive definite matrix
- 'k' : target rank, number of wanted eigenvalues
- 'p' : over-sampling parameter, it should be 10 or 2k.
- 'reconst' : if ture, the output is reconstracted matrix which approximates 'A'

example:
    # make a positive definite matrix `A`.
    n = 5000; m = 25000; x = randn(m,n)
    demean_x = x .- mean(x, dims=1)
    A = demean_x'*demean_x ./(m-1)

    # run evdnys
    lowrank_mtx = evdnys(A,10, reconst=true)
"""
function evdnys(A::Symmetric, k::Int;p=10, reconst=false)
    n, n = size(A)
    G = randn(n, k+p)
    Y = A * G
    Q = qr(Y).Q*Matrix(I,n,k+p)
    B1 = A*Q
    B2 = Q'*B1
    C = cholesky(Symmetric(B2))
    #F = B1 * inv(C.U)
    F = B1 / C.U
    U, Σ, _ = svd(F)

    if reconst
        return U*diagm(Σ.^2)*U'
    else
        return U, Σ.^2
    end
end

"""
Randomized eigenvalue decomposition proposed by N.halko et al.
See Algorithm 5.6. in the
[original paper](https://epubs.siam.org/doi/10.1137/090771806).
This method is effective matrices with decaying eigenvalues.

# Aruguments
- 'A' : input positive definite matrix
- 'r' : target rank, number of wanted eigenvalues
- 'p' : over-sampling parameter, it should be 10 or 2r.
- 'reconst' : if ture, the output is reconstracted matrix which approximates 'A'

example:
    # make a matrix with decaying eigenvalues
    n = 150; A = rand(n,n); B = zeros(n,n)
    e, v = eigen(A)
    for i in 1:n
        B += e[i] * v[:,i] * v[:,i]' * (i>n-100 ? 1 : 1e-3)
    end

    # run reigen
    lowrank_mtx = reigen(A, 10, reconst=true)
"""
function reigen(A, r ;p=10, twopass=false, reconst=false)
    n = size(A)[1]
    m = r + p

    Ω = randn(n, m)
    Y = A*Ω

    
    if twopass
        Q = qr!(Y).Q * Matrix(I,n,m)
        T = Q' * A * Q
    else
        Q = qr(Y).Q * Matrix(I,n,m)
        T = (Q' * Y) / ( Q' * Ω)
    end
    λ, S = eigen(T)
    U = Q*S'
    if reconst
        Λ = Diagonal(λ)
        return U*Λ*U'
    else
        return U, λ
    end
end

Julia言語 Randomizedな方法での固有値計算の高速化のためにSRFTをやってみたけどうまくいかなかった

いままで実装してきたRandomizedな方法での固有値計算(RED)では,アルゴリズム中にランダム行列と入力行列との積を計算する必要がある.これにはO(nmr)かかる訳だけど,SRFTとかいう方法でO(nm log r)に高速化できるらしい.SRFTというのは特別な条件を満たす行列っぽくて,FFTとか使って行列積の計算が高速化できるらしい.アルゴリズムを調べてもよくわからなかったんだけれども,LowRankApprox.jlのモジュールにSRFTという名前のついた関数がたくさんあったので,必要な部分をコピペして,いままで実装してきたものにくっつけて動かしてみた.

github.com

具体的にはこの部分. 結論から言うとうまくいかなかった.

やっぱりもっとちゃんと勉強して,どうやって高速化するのかを自分で考えてみようと思う. 以下,実験に使ったコード.

using LinearAlgebra
using Plots
using Statistics
import FFTW: plan_r2r!, R2HC,r2rFFTWPlan

"""
We cited srft functions from [LowRankApprox library](https://github.com/JuliaMatrices/LowRankApprox.jl)
"""
function srft_apply!(
    y::AbstractVecOrMat{T}, X::AbstractMatrix{T}, idx::AbstractVector,
    r2rplan!::r2rFFTWPlan) where T<:Real
  l, m = size(X)
  n = l*m
  k = length(idx)
  mul!(X, r2rplan!, X)
  wn = exp(-2im*pi/n)
  wm = exp(-2im*pi/m)
  nnyq = div(n, 2)
  cnyq = div(l, 2)
  i = 1
  @inbounds while i <= k
    idx_ = idx[i] - 1
    row = fld(idx_, l) + 1
    col = rem(idx_, l) + 1
    w = wm^(row - 1)*wn^(col - 1)

    # find indices of real/imag parts
    col_ = col - 1
    cswap = col_ > cnyq
    ia = cswap ? l - col_ : col_
    ib = col_ > 0 ? l - ia : 0

    # initialze next entry to fill
    y[i] = 0
    s = one(T)

    # compute only one entry if purely real or no more space
    if in == 0 || in == nnyq || i == k
      for j = 1:m
        a = X[ia+1,j]
        b = ib == 0 || ib == ia ? zero(T) : X[ib+1,j]
        b = cswap ? -b : b
        y[i] += real(s*(a + b*im))
        s *= w
      end

    # else compute one entry each for real/imag parts
    else
      y[i+1] = 0
      for j = 1:m
        a = X[ia+1,j]
        b = ib == 0 || ib == ia ? zero(T) : X[ib+1,j]
        b = cswap ? -b : b
        z = s*(a + b*im)
        y[i  ] += real(z)
        y[i+1] += imag(z)
        s *= w
      end
      i += 1
    end
    i += 1
  end
end

function srft_reshape!(X::AbstractMatrix, d::AbstractVector, x::AbstractVecOrMat)
    l, m = size(X)
    i = 0
    @inbounds for j = 1:l, k=1:m
        i += 1
        X[j,k] = d[i] * x[i]
    end
end

function srft_rand(::Type{T},n::Integer) where T<:Real
    x = rand(n)
    @simd for i = 1:n
        @inbounds x[i] = 2*(x[i] > 0.5) - 1
    end
    x
end

function srft_init(::Type{T}, n::Integer, k::Integer) where T<:Real
    l = k
    while n % l > 0
        l -= 1
    end
    m = div(n,l)
    X = Array{T}(undef, l, m)
    d = srft_rand(T,n)
    idx = rand(1:n,k)
    r2rplan! = plan_r2r!(X, R2HC, 1)
    X, d, idx, r2rplan!
end

"""
Get a product between m \times n matrix 'A' and 'n \times k` srft.
"""
function get_Y(A::AbstractMatrix{T}, k) where T
    m, n = size(A)
    Y = Matrix{Float64}(undef,m,k)
    X, d, idx, fftplan! = srft_init(T, n, k)
    for i=1:m
        srft_reshape!(X, d, view(A,i,:))
        srft_apply!(view(Y,i,:),X,idx,fftplan!)
    end
    return Y
end

"""
Randomized eigenvalue decomposition via Nystrom Method.
See Fig 6 in the [original paper](https://arxiv.org/abs/1607.01649)
or See Algorithm 5.5 in the [original paper](https://epubs.siam.org/doi/10.1137/090771806)
This method is available for only a positive definite matrix.

# Aruguments
- 'A' : input positive definite matrix
- 'k' : target rank, number of wanted eigenvalues
- 'p' : over-sampling parameter, it should be 10 or 2k.
- 'use_srft' : if ture, use subsampled Fourier transformation.
- 'reconst' : if ture, the output is reconstracted matrix which approximates 'A'

example:
    # make a positive definite matrix `A`.
    n = 5000; m = 25000; x = randn(m,n)
    demean_x = x .- mean(x, dims=1)
    A = demean_x'*demean_x ./(m-1)

    # run evdnys
    lowrank_mtx = evdnys(A,10, reconst=true)
"""
function evdnys(A::Symmetric, k::Int;p=10, use_srft=false, reconst=false)
    n, n = size(A)
    if use_srft
        Y = get_Y(A,k+p)
    else
        G = randn(n, k+p)
        Y = A * G
    end
    Q = qr(Y).Q*Matrix(I,n,k+p)
    B1 = A*Q
    B2 = Q'*B1
    C = cholesky(Symmetric(B2))
    F = B1 * inv(C.U)
    U, Σ, _ = svd(F)

    if reconst
        return U*diagm(Σ.^2)*U'
    else
        return U, Σ.^2
    end
end

"""
Randomized eigenvalue decomposition proposed by N.halko et al.
See Algorithm 5.6. in the
[original paper](https://epubs.siam.org/doi/10.1137/090771806).
This method is effective matrices with decaying eigenvalues.

# Aruguments
- 'A' : input positive definite matrix
- 'r' : target rank, number of wanted eigenvalues
- 'p' : over-sampling parameter, it should be 10 or 2k.
- 'use_srft' : if ture, use subsampled Fourier transformation.
- 'reconst' : if ture, the output is reconstracted matrix which approximates 'A'

example:
    # make a matrix with decaying eigenvalues
    n = 1500; A = rand(n,n); B = zeros(n,n)
    e, v = eigen(A)
    for i in 1:n
        B += e[i] * v[:,i] * v[:,i]' * (i>n-100 ? 1 : 1e-3)
    end

    # run reigen
    lowrank_mtx = reigen(A, 10, reconst=true)
"""
function reigen(A, r ;p=10, twopass=false, use_srft=false, reconst=false)
    n = size(A)[1]
    m = r + p

    if use_srft
        Y = get_Y(A,m)
    else
        Ω = randn(n, m)
        Y = A*Ω
    end

    Q = qr!(Y).Q * Matrix(I,n,m)
    if twopass
        T = Q' * A * Q
    else
        if use_srft
            T = (Q' * Y) * ( (get_Y(Q',m))\I)
        else
            T = (Q' * Y) * ( (Q' * Ω)\I)
        end
    end
    λ, S = eigen(T)
    U = Q*S'
    if reconst
        Λ = Diagonal(λ)
        return U*Λ*U'
    else
        return U, λ
    end
end

SRFTを使うか,lapackを使うかで,45000×45000の正定値行列Aの固有値をk個取り出すのに要した時間をプロット.横軸がk.縦軸が計算時間(sec).下の図はreigenの場合について.うーん,全然はやくない.普通にlapack使った方がいい.

とりあえず,行列積だけでも計算が速くなっているか確認した.Y = get_Y(A,m)Y = A*randn(n,k)に必要な時間の差を調べた.うーん,やっぱり普通に計算するのが良さそう??

とりあえず,StackExchangeに投げてみた.なぜ低評価がついているか分からないが,誰か答えてくれると嬉しいです.

scicomp.stackexchange.com

Julia言語 Randomized eigenvalue decomposition via Nystrom Method を実装した.

n×nの正定値対称行列A固有値を大きい順にr個求めたいです.これを高速に計算するアルゴリズムとしてRandomized eigenvalue decomposition via Nystrom Methodを実装しました.以下の論文のFig.6.を実装しました.

arxiv.org

前に実装したREDは固有値を大きい順にr個だけ高速に計算できましたが,固有値が減衰していない行列については,(Fノルムで評価する低ランク近似はうまくできても)固有値の近似がよくないアルゴリズムでした.今回のRED via Nystrom Methodは入力が正定値対称行列に縛られますが,高速にかつより正確に固有値を大きい順に求めることができます.

とくに困ったところはなく,半日くらいで順調に実装できました.

using LinearAlgebra
function evdnys(A::Symmetric, k::Int;p=10)
    n, n = size(A)
    G = randn(n, k+p)
    Y = A * G
    Q = qr(Y).Q*Matrix(I,n,k+p)
    B1 = A*Q
    B2 = Q'*B1
    C = cholesky(Symmetric(B2))
    F = B1 * inv(C.U)
    U, Σ, _ = svd(F)
   #reconstract is U*diagm(Σ.^2)*U'
    return U, Σ.^2
end

サンプリングパラメータのpは適当に10にしていますが,p=2rにするのがよい,といっている文献もどこかにありました.

計算精度について

実験用にとりあえず,適当な正定値対称行列Aをつくります.

using Statistics
m = 50
n = 100
x = randn(n,m)
demean_x = x .- mean(x, dims=1)
A = demean_x'*demean_x ./(n-1)
isposdef(A) #true

比較のために前回実装した,EVDを用意しておきます.

function reigen(A,r;p=10, twopass=false)
    n = size(A)[1]
    m = r + p
    Ω = randn(n, m)
    n, m = size(Ω)
    Y = A*Ω
    if twopass
        Q = qr!(Y).Q * Matrix(I,n,m)
        T = Q' * A * Q
    else
         Q = qr(Y).Q * Matrix(I,n,m)
         T = (Q' * Y) * ( (Q' * Ω)\I)
    end
    λ, S = eigen(T)
    Λ = Diagonal(λ)
    U = Q*S'
    return U, λ
end

まず,lapackで行列Aの固有値を大きい順に求めて,10個表示させてみます.

result_lapack = eigen(A).values
reverse(result_lapack)[1:10]
# 2.499093654863234
# 2.449174582223532
# 2.3513196905628972
# 2.2851514992705284
# 2.1091186508409545
# 2.069503593052392
# 1.9434150310107634
# 1.804034622747044
# 1.7192090439026784
# 1.6285715863497319

適当にr=20にして,EVDで行列Aの固有値を大きい順に10個求めてみます. 結構激しくずれてます.

r=20
U, values = reigen(A,r)
reverse(values)[1:10]

# 2.3648795631492248
# 2.2652936494283664
# 2.065460162647662
# 2.053243332161027
# 2.0015998322530044
# 1.9269963048738135
# 1.8328635753401925
# 1.6750657308299814
# 1.611119258198799
# 1.542245495794801

では,今回実装したRED via Nystrom Methodで行列Aの固有値を大きい順に10個求めてみます.

r = 20
_, eigenvalues = evdnys(Symmetric(A),r)
display(eigenvalues[1:10])

# 2.499093654863234
# 2.449174582223532
# 2.3513196905628972
# 2.2851514992705284
# 2.1091186508409545
# 2.069503593052392
# 1.9434150310107634
# 1.804034622747044
# 1.7192090439026784
# 1.6285715863497319

lapackの結果をよく近似できているように思います.

計算速度について

既存の手法との比較をしてみました.

function evdnys(A::Symmetric, k::Int;p=10)
    n, n = size(A)
    G = randn(n, k+p)
    Y = A * G
    Q = qr(Y).Q*Matrix(I,n,k+p)
    B1 = A*Q
    B2 = Q'*B1
    C = cholesky(Symmetric(B2))
    F = B1 * inv(C.U)
    U, Σ, _ = svd(F)
    Λ = diagm(Σ.^2)
    return U, Σ.^2
end

function reigen(A,r;p=10, twopass=false)
    n = size(A)[1]
    m = r + p
    Ω = randn(n, m)
    n, m = size(Ω)
    Y = A*Ω
    if twopass
        Q = qr!(Y).Q * Matrix(I,n,m)
        T = Q' * A * Q
    else
         Q = qr(Y).Q * Matrix(I,n,m)
         T = (Q' * Y) * ( (Q' * Ω)\I)
    end
    λ, S = eigen(T)
    Λ = Diagonal(λ)
    U = Q*S'
    return U, λ
end

using Statistics
m = 2000
n = 10000
x = randn(n,m)
demean_x = x .- mean(x, dims=1)
A = demean_x'*demean_x ./(n-1)
isposdef(A) #Aは正定値対称行列

p0 = 5
rs = [ p0*2^(n-1) for n = 1:7];
for r in rs
    result_lapack, runtime_lapack = @timed eigen(Symmetric(A), m-r+1:m);
    result_arpack, runtime_arpack = @timed eigs(A, nev=r, which=:LR);
    result_krylov, runtime_krylov = @timed eigsolve(A, r, :LM, krylovdim=r+10);
    result_lobpcg, runtime_lobpcg = @timed lobpcg(A, true, r);
    result_reigen1, runtime_reigen2 = @timed reigen(A, r, twopass=false);
    result_reigen2, runtime_reigen2 = @timed reigen(A, r, twopass=true);
    result_evdnys, runtime_evdnys = @timed evdnys(Symmetric(A), r);
    println(
       "r $r
        lapack  $runtime_lapack \t arpack $runtime_arpack \t 
        krylov $runtime_krylov \t lobpcg $runtime_lobpcg
        reigen1 $runtime_reigen1 \t reigen2 $runtime_reigen2 \t evdnys $runtime_evdnys")
    println("******")
end

結果は以下の通り.やっぱりEVDviaNystromをはじめ,Randomizedな方法は速いですね.

r 5
        lapack  0.8177911    arpack 0.4960746    krylov 0.3070053    lobpcg 0.3421401
        reigen1 0.0058875    reigen2 0.0104825   **evdnys 0.011699**
******
r 10
        lapack  0.8837821    arpack 0.576648     krylov 0.3857683    lobpcg 0.4529197
        reigen1 0.0101869    reigen2 0.0106938   **evdnys 0.0127273**
******
r 20
        lapack  0.8577545    arpack 0.590889     krylov 0.4786105    lobpcg 0.5592837
        reigen1 0.0080609    reigen2 0.0101375   **evdnys 0.0151632**
******
r 40
        lapack  1.1575799    arpack 0.7349361    krylov 0.722302     lobpcg 0.8956097
        reigen1 0.0125024    reigen2 0.0159989   **evdnys 0.028382**
******
r 80
        lapack  0.9327568    arpack 1.1262472    krylov 1.2329233    lobpcg 2.6357616
        reigen1 0.0242507    reigen2 0.028978    **evdnys 0.051772**
******
r 160
        lapack  1.1156746    arpack 2.0878638    krylov 2.3862248    lobpcg 4.6012995
        reigen1 0.0991418    reigen2 0.1222233   **evdnys 0.1450164**
******
r 320
        lapack  1.4092244    arpack 5.3995158    krylov 4.1578276    lobpcg 15.1888958
        reigen1 0.3209115    reigen2 0.7082315   **evdnys 0.4270121**
******

reigen1,reigen2,evdnysでは,最初にランダム行列と入力行列の積を計算しているんですけど,これには,O(nmr)くらいの計算量が必要です.しかし,N.halko論文この論文によると,O(nmlogr)に減らせるらしい.fast Johnson-Lindenstrauss transforms とか,SRFT とかよばれる技術を使うらしいがよくわからない.具体的にどうしたら計算量がへるのか分からないので,とりあえず StackOverflowに投げた.

stackoverflow.com

これをちゃんと実装できれば,reigen1,reigen2,evdnysの計算速度はさらに半分くらいになりそう..誰か教えてくれ...涙