以下の内容はhttps://genkaiphd.hatenablog.comより取得しました。


Pythonで点の軌跡のアニメーション

Toy実験ように適当な軌跡のアニメーションが欲しかったので作った.

各frameをつくる:

import numpy as np
import math
import itertools
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
PI = math.pi

# img_size is 2N x 2N
(N, N) = (30, 30)

def current_point(r, ang):
    x = r * math.cos( ang )
    y = r * math.sin( ang )
    return x, y

def get_bar(cr, cang, dr, dang):
    fill_range = []
    for r in np.arange(cr-dr, cr+dr, 0.02):
        for ang in np.arange(cang-dang, cang+dang, 0.02):
            (x,y) = current_point(r, ang)
            fill_range.append((x,y))
    return fill_range

def get_frames_for_circle(rad=0.65, lor=0.1):
    frames = []
    for (k,t) in enumerate(np.arange(-2*PI,2*PI,0.1)):
        img_np = np.ones((2*N+1,2*N+1))
        
        past_range = [
            get_bar(rad, t-0.0, 0.10, lor),
            get_bar(rad, t-0.1, 0.09, lor),
            get_bar(rad, t-0.2, 0.08, lor),
            get_bar(rad, t-0.3, 0.07, lor),
            get_bar(rad, t-0.4, 0.06, lor),
            get_bar(rad, t-0.5, 0.05, lor),
            get_bar(rad, t-0.6, 0.04, lor),
            get_bar(rad, t-0.7, 0.03, lor),
            get_bar(rad, t-0.8, 0.02, lor),
            get_bar(rad, t-0.9, 0.01, lor),
            get_bar(rad, t-1.0, 0.005, lor),
            get_bar(rad, t-1.1, 0.005, lor),
            get_bar(rad, t-1.2, 0.005, lor),
            get_bar(rad, t-1.3, 0.005, lor),
        ]
    
        kmax = len(past_range)
        for (k,fill_range) in enumerate(past_range):
            for (x, y) in fill_range:
                x_int = np.clip(round(N*x + N),0, 2*N)
                y_int = np.clip(round(N*y + N),0, 2*N)
                img_np[x_int,y_int] = 1/kmax*k
        frames.append(img_np)
    return frames

つくったフレームからgifアニメーションをつくる

frames = get_frames_for_circle(rad=0.75, lor=0.05)
% ↓画像としてほしかったらこれをコメントアウトして
% plt.imshow(frames[-1]) 

fig, ax = plt.subplots()
img = ax.imshow(frames[0], cmap='gray')
def update(frame):
    img.set_array(frame)
    return [img]
ani = FuncAnimation(fig, update, frames=frames, interval=50)
ani.save('animation.gif', writer='imagemagick')

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 言語 テンソルリング分解による欠損補完

この論文に出てくる PTRC-RW を実装しました.

https://ieeexplore.ieee.org/document/9158539

Folding と Unfolding が肝で,よく分かんないので,stackoverflow の強い人に助けてもらいました.

arrays - The inverse operation of tensor circular unfolding in Julia - Stack Overflow

Stridedを使うとちょっと早くなります.本当は,permutedims は新しいテンソルを作っていて,実は view さえあれば計算はできると思うので,もうちょっと最適化できるかもしれないです.このあたりはややこしくて苦手です.ただ,実行時間は論文の図9くらいにはなったので,matlab と julia の環境の差はあれど,まぁ許容できるくらいには最適化できたのかなと納得して進むことにしました.

とりあえず,TCUとその逆作用.

using LinearAlgebra
using TensorToolbox
using Tullio
using Distributions
using Printf
using TransmuteDims
using Strided

function TCU(X,d,k)
    N = ndims(X)
    @assert d < N
    if d <= k
        a = k-d+1
    else
        a = k-d+1+N
    end
    @strided tmp = permutedims(X,circshift(1:N,-a+1))
    tenmat(tmp,row=1:d)
end

function invTCU(M,d,k, presize)
    N = length(presize)
    a = d<=k ? k-d+1 : k-d+1+N
    X = reshape(M,Tuple(circshift(collect(presize),1-a)))
    @strided permutedims(X,circshift(1:N,a-1))
end

アルゴリズム本体は以下.RWfalseにすると論文中のアルゴリズム1(PTRC)になります. Xが入力テンソルで,Mが欠損の位置を表すバイナリテンソルです.alphadはハイパラです.alphaは総和が1で大きさがテンソルのオーダーと同じベクトルです.dテンソルのオーダーよりも小さい整数です.

function PTRCRW!(X, M, R, alpha, d;RW=true, iter_max=1000, verbose=true, verbose_inval=5, Xgt=NaN, tol=1.0e-5)
    idxs_missing = findall( M .== 0 )
    N = ndims(X)
    J = size(X)

    Xd = Vector{Matrix{Float64}}(undef,N)
    W = Vector{Matrix{Float64}}(undef,N)
    H = Vector{Matrix{Float64}}(undef,N)
    if RW
        B = Vector{Matrix{Float64}}(undef,N)
    end

    for k = 1:N
        a = get_a(d,k,N)
        if a == 1
            s1 = R[k]*R[N]
        else
            s1 = R[k]*R[a-1]
        end

        if a == 1
            s2 = prod(J[k+1:N])
        elseif k+1 <= a-1
            s2 = prod(J[k+1:a-1])
        else
            s2 = prod(vcat(J[k+1:N]...,J[1:a-1]...))
        end
        H[k] = rand( s1, s2 )

        if RW
            Wdk = TCU(M,d,k)
            omega_kd = sum( Wdk )
            beta = zeros(size(Wdk)[1])
            for i = 1:size(Wdk)[1]
                omega_kdi = sum( Wdk[i,:] )
                beta[i] = max( omega_kdi/omega_kd ,1.0e-16)
            end
            B[k] = diagm(beta)
        end
    end

    X_pre = zeros( J )
    for iter = 1:iter_max
        for k = 1:N
            if iter == 1
                Xd[k] = TCU(X,d,k)
                W[k] = Xd[k] * H[k]'
            else
                Xd[k] .= TCU(X,d,k)
                W[k] .= Xd[k] * H[k]'
            end

            if RW
                H[k] .= ( W[k]'*B[k]*W[k] ) \ W[k]' * B[k] * Xd[k]
            else
                H[k] .= ( W[k]'*W[k] ) \ W[k]' * Xd[k]
            end

            Xd[k] .= W[k]*H[k]
        end

        foldM = zeros(J)
        for k = 1:N
            foldM .+= ( alpha[k] .* invTCU(Xd[k],d,k,J) )
        end
        X[idxs_missing] .= foldM[idxs_missing]

        if iter > 1
            diff_X = norm( X_pre .- X ) / norm(X_pre)

            if verbose && iter > 1
                if mod(iter, verbose_inval) == 0
                    if Xgt isa Array
                        rms = norm(X .- Xgt)/norm(Xgt)
                    else
                        rms = NaN
                    end
                    @printf("%4d %5f %5f \n", iter, diff_X, rms)
                end
            end

            if diff_X < tol
                return X
            end

            if iter == iter_max
                println("PTRCRW was not converged")
            end
        end
        X_pre .= X
    end

    return X
end

適当にダウンロードしてきた画像ファイルに欠損を90%与えてみたらこんな感じになりました.

using FileIO
function main()
    T = load("../../data/SIPI/tensor_SIPI.jld2")
    Xgt = T["Baboon"]
    #Xgt = reshape(Xgt, 16,16,16,16,3)
    Xgt = reshape(Xgt, 4,4,4,4,4,4,4,4,3)
    W = generate_weight(Xgt, sr=10)
    Xin = init_missing_val!(deepcopy(Xgt), W)

    D = ndims(Xin)
    alpha = ones(D)/D
    R = 5*ones(Int64,D)
    d = 3
    Xpre = PTRCRW!(deepcopy(Xin), W, R, alpha, d, RW=true, tol=1.0e-3, iter_max=1000, verbose=true, verbose_inval=20, Xgt=Xgt)

end

RSE(a,b) = norm(a - b) / norm(a)
main()

下がってます,下がってます.

iter     cost       RMSE
  20 0.007605 0.261927
  40 0.003536 0.215563
  60 0.002055 0.198852
  80 0.001495 0.190946
 100 0.001139 0.186659

使った,init_missing_val!generate_weightの定義はこちらをご参照ください.

https://genkaiphd.hatenablog.com/entry/2023/01/05/232531genkaiphd.hatenablog.com

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 言語 テンソル低ランク補完 HaLRTC, SiLRTC

テンソルに低タッカーランク性を課して,欠損値を補完する手法 HaLRTC を実装しました.

参考にした論文はこれ.Algorithm 4 です.

pubmed.ncbi.nlm.nih.gov

引用数すげえええ.こういう論文をかけるようになりたいものですね.再現するのは図6の実験です.

アルゴリズム本体はこんな感じ.特に苦労もなくかけました. ただ,YとMの初期値をどう決めたらよいか論文には書いてないように思います.とりあえず,全部ゼロで初期化してます. https://ieeexplore.ieee.org/document/6138863

function Dtau(X,tau)
    D = svd(X)
    return D.U * diagm( max.(D.S .- tau, 0) ) * D.Vt
end

"""
HaLRTC: High Accuracy Low Rank Tensor Completion
See Algorithm 4 in the
[original paper](https://ieeexplore.ieee.org/document/6138863)

# Aruguments
- 'X' : input tensor
- 'W' : binary tensor if X_ijk is missing then W_ijk = 0, otherwise 1
- 'rho' : hyper parameter
- 'Xgt' : ground truth tensor for printing verbose
"""
function HaLRTC(X, W;rho=1.0e-5, iter_max=1000, verbose=true,Xgt=NaN)
    idxs_missing = findall( W .== 0 )
    Xhat = X
    D = ndims(X)
    J = size(X)
    Y = Vector{Array{Float64,D}}(undef,D)
    M = Vector{Array{Float64,D}}(undef,D)
    for d = 1:D
        Y[d] = zeros( J )
        M[d] = zeros( J )
    end
    alpha = 1.0/D

    for iter = 1:iter_max

        # #################
        # update M tensors
        # #################

        for d = 1:D
            Xd = tenmat(X,d)
            Ydd = tenmat(Y[d],d)
            M[d] = matten( Dtau( Xd .+ Ydd/rho, alpha/rho ), d, [J...])
        end

        tmp = ( sum(M) .- sum(Y)./rho ) ./ D
        Xhat .= (1 .- W) .* tmp + W .* X

        # update Y tensors
        for d = 1:D
            Y[d] .-= rho.*( M[d] .- X )
        end

        if verbose
            if mod(iter,5) == 0
                rms = norm(Xhat .- Xgt)/norm(Xgt)
                @show (iter, rms)
            end
        end
    end
    return Xhat
end

実験用に欠損を含む低タッカーランクをつくります.Jがテンソルのサイズ,Rがタッカーランクです.総和がテンソルの要素数と同じになるように規格化しておきます.Wは重みテンソルで,欠損してるインデックスは0,それ以外は1のバイナリテンソルです.sr が観測率ですね.sr=0 は全て欠損,sr=100は欠損なしを意味します.

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] = ( -0.5 .+ rand(J[d],R[d]) )
    end
    T = ttm(G, [U...], [1:D;])
    T =  T .* ( prod(J) ./ sum(T))
    return T
end

function generate_weight(T;sr=30)
    prob = zeros(100)
    prob[1:sr] .= 1
    W = rand(prob, size(T))
    return W
end

欠損している部分を平均μ,分散1の正規分布で初期値を決めておきます. μは欠損していない部分の平均.これは論文に書いてあった初期位置の決め方です.

function init_missing_val!(X,W)
    mu = sum( W .* X ) / sum(W)
    prob = Normal(mu, 1)
    idxs_missing = findall( W .== 0 )
    X[idxs_missing] = rand(prob,length(idxs_missing))
    return X
end

実行コードは以下.論文の問題設定に合わせて観測率20%にしておきます.

function main()
    r = 2
    Xgt = get_low_tucker_tensor([50,50,50,50],[r,r,r,r])
    W = generate_weight(Xgt, sr=20)
    Xin = init_missing_val!(deepcopy(Xgt), W)
    for rho in [1.0e-6, 1.0e-7, 1.0e-8, 1.0e-9]
        Xpre = HaLRTC(Xin, W, rho=rho, iter_max=50, verbose=true, Xgt=Xgt)
        @show (r, rho, RSE(Xgt,Xpre))
    end

end
RSE(a,b) = norm(a - b) / norm(a)

main()

今回は学習の様子を知りたいので,HaLRTCにXgt(ground truth)を渡してます.これは普通の問題設定では無理なので注意(verboseでスコアを出す以外では使ってない).実行結果は以下.

% rho = 1.0e-6
(iter, rms) = (5, 0.8869150573563238)
(iter, rms) = (10, 0.8787654368905992)
(iter, rms) = (15, 0.8706546764827426)
(iter, rms) = (20, 0.8625800836366997)
(iter, rms) = (25, 0.8545393221335581)
(iter, rms) = (30, 0.8465303461954946)
(iter, rms) = (35, 0.8385513502138886)
(iter, rms) = (40, 0.8306007297171744)
(iter, rms) = (45, 0.822677050591317)
(iter, rms) = (50, 0.8147790244567812)
(r, rho, RSE(Xgt, Xpre)) = (2, 1.0e-6, 0.8147790244567812)

% rho = 1.0e-7
(iter, rms) = (5, 0.7370755291251271)
(iter, rms) = (10, 0.6611151830246284)
(iter, rms) = (15, 0.5865472507974067)
(iter, rms) = (20, 0.5132093749028765)
(iter, rms) = (25, 0.4410342512627796)
(iter, rms) = (30, 0.3700098409582638)
(iter, rms) = (35, 0.3001540041695475)
(iter, rms) = (40, 0.2315108108075831)
(iter, rms) = (45, 0.16415926359343516)
(iter, rms) = (50, 0.09823642969477969)
(r, rho, RSE(Xgt, Xpre)) = (2, 1.0e-7, 0.09823642969477969)

% rho = 1.0e-8
(iter, rms) = (5, 0.11760666100628855)
(iter, rms) = (10, 0.10256307545489725)
(iter, rms) = (15, 0.03393611809759228)
(iter, rms) = (20, 0.013397645459977311)
(iter, rms) = (25, 0.019970917994870968)
(iter, rms) = (30, 0.010004052083471028)
(iter, rms) = (35, 0.0013604313052622036)
(iter, rms) = (40, 0.0033711415897143713)
(iter, rms) = (45, 0.002407736831070906)
(iter, rms) = (50, 0.00060861557570968)
(r, rho, RSE(Xgt, Xpre)) = (2, 1.0e-8, 0.00060861557570968)

% rho = 1.0e-9
(iter, rms) = (5, 0.8434343422817917)
(iter, rms) = (10, 0.30375723857457604)
(iter, rms) = (15, 0.16121070156092238)
(iter, rms) = (20, 0.09762730684576809)
(iter, rms) = (25, 0.0447844565537104)
(iter, rms) = (30, 0.014794610766144974)
(iter, rms) = (35, 0.01559292614989365)
(iter, rms) = (40, 0.011395916029972048)
(iter, rms) = (45, 0.0062374608175992325)
(iter, rms) = (50, 0.004374149421615887)
(r, rho, RSE(Xgt, Xpre)) = (2, 1.0e-9, 0.004374149421615887)

まぁちゃんと下がってますし,良いんじゃないでしょうか.どうも rho=1.0e-6 のときはうまくいきませんね.論文では10回繰り返した,と書いてあったのですが,これはきっと10回繰り返して最もよかったものをプロットした,ということだと思うので,まぁ入力テンソルを変えれば,Fig.6 くらい(0.55くらい?)までは下がるのかな.

ついでにこないだの SiLRTC も貼っておきます.

"""
SiLRTC: Simple Low Rank Tensor Completion
See Algorithm 1 in the
[original paper](https://ieeexplore.ieee.org/document/6138863)

# Aruguments
- 'X' : input tensor
- 'W' : binary tensor if X_ijk is missing then W_ijk = 0, otherwise 1
- 'tau' : hyper parameter
- 'Xgt' : ground truth tensor for printing verbose
"""
function SiLRTC(X, W, tau=1.0e2; iter_max=1000, verbose=false,Xgt=NaN)
    idxs_missing = findall( W .== 0 )
    D = ndims(X)
    beta = ones(D)

    sizeX = size(X)
    sum_beta = sum(beta)
    M = Vector{Matrix{Float64}}(undef,D)
    for iter = 1:iter_max

        # ##################
        # update matrices M
        # ##################

        for d = 1 : D
            Xd = tenmat(X,d) #unfolding X on mode-d, matricization
            M[d] = Dtau(Xd, tau)
        end

        foldM = zeros(size(X))
        for d = 1:D
            foldM .+= ( beta[d] .* matten( M[d], d, [size(X)...] ))
        end
        foldM = foldM ./ sum_beta

        X[idxs_missing] .= foldM[idxs_missing]
        if verbose
            if iter % 200 == 0
                rms = norm(X .- Xgt)/norm(Xgt)
                @show (iter, rms)
            end
        end
    end
    return X
end


function main()
    r = 10
    Xgt = get_low_tucker_tensor([50,50,50],[r,r,r])
    W = generate_weight(Xgt, sr=30)
    Xin = init_missing_val!(deepcopy(Xgt), W)
    for tau in [10,100,1000]
        Xpre = SiLRTC(deepcopy(Xin), W, tau, iter_max=2000, verbose=true, Xgt=Xgt)
        @show (r, tau, RSE(Xgt,Xpre))
    end
end

実行結果は以下.tau が小さいと(モデルが大きくて,最適化するパラメータ数が多いので)収束するのにもっともっと iter が必要ということですな.

(iter, rms) = (200, 0.8306453721340777)
(iter, rms) = (400, 0.8247437581588134)
(iter, rms) = (600, 0.8188849564191533)
(iter, rms) = (800, 0.8130680035360011)
(iter, rms) = (1000, 0.8072919985188617)
(iter, rms) = (1200, 0.8015560976208085)
(iter, rms) = (1400, 0.7958595097055471)
(iter, rms) = (1600, 0.7902014920666872)
(iter, rms) = (1800, 0.7845813466478869)
(iter, rms) = (2000, 0.7789984166179046)
(r, tau, RSE(Xgt, Xpre)) = (10, 10, 0.7789984166179046)

(iter, rms) = (200, 0.7789920277327693)
(iter, rms) = (400, 0.7250842431553487)
(iter, rms) = (600, 0.6743908099006761)
(iter, rms) = (800, 0.6266246907129802)
(iter, rms) = (1000, 0.5815967124956913)
(iter, rms) = (1200, 0.5391698153982848)
(iter, rms) = (1400, 0.4992340492634431)
(iter, rms) = (1600, 0.4616920768892733)
(iter, rms) = (1800, 0.42645050231781445)
(iter, rms) = (2000, 0.3934146886184567)
(r, tau, RSE(Xgt, Xpre)) = (10, 100, 0.3934146886184567)

(iter, rms) = (200, 0.39309150992974745)
(iter, rms) = (400, 0.16002034419257943)
(iter, rms) = (600, 0.03314117313566921)
(iter, rms) = (800, 0.0224660080703799)
(iter, rms) = (1000, 0.022466003544853275)
(iter, rms) = (1200, 0.022466003544851655)
(iter, rms) = (1400, 0.0224660035448515)
(iter, rms) = (1600, 0.022466003544851582)
(iter, rms) = (1800, 0.02246600354485149)
(iter, rms) = (2000, 0.022466003544851818)
(r, tau, RSE(Xgt, Xpre)) = (10, 1000, 0.022466003544851818)

関連して Julia の実装を見つけたのではっておきます.

github.com

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 言語 Tensor Ring Decomposition

SVDによるテンソルリング分解を実装しました.テンソルリング分解についてはこちらの論文を参照.テンソルリング分解では,ランクはベクトルです.このベクトルの各要素には自然数がはいっていて,要素数は入力テンソルのオーダーに一致します.

arxiv.org

こちらにも実装の詳細があって,結構助かりました.

arxiv.org

オリジナルの論文では,リングランクを指定するのではなくて,許容する誤差εを入力に与えています.自分の研究ではリングランクを決め打ちする実装も必要になりそうでした.そこで,TR()の二つ目の引数にベクトルを渡したら,ランクを決め打ちで分解,floatを渡したら誤差ε以下で分解,という実装にしました.多重ディスパッチ最高.

自然数の約数を求めるアルゴリズムと,誤差を指定してtruncated SVD が必要だったので,こちらを先に用意しておきます.約数を全部求めるアルゴリズムお気楽 Julia プログラミング超入門から拝借しました.ありがとうございます。

"""
Provided by http://www.nct9.ne.jp/m_hiroi/light/juliaa08.htmll
"""
function divisor(n)
    # pq の約数を求める
    divisor_sub(p, q) = [p ^ i for i = 0 : q]

    xs = factorization(n)
    ys = divisor_sub(xs[1]...)
    for p = xs[2 : end]
        ys = [x * y for x = divisor_sub(p...) for y = ys]
    end
    sort(ys)
end

function factorSub(n, m)
    c = zero(n)
    while n % m == 0
        c += 1
        n = div(n, m)
    end
    c, n
end

function factorization(n)
    x::typeof(n) = 2
    xs = Pair{typeof(n), typeof(n)}[]
    c, m = factorSub(n, x)
    if c > 0
        push!(xs, x => c)
    end
    x = 3
    while x * x <= m
        c, m = factorSub(m, x)
        if c > 0
            push!(xs, x => c)
        end
        x += 2
    end
    if m > 1
        push!(xs, m => one(n))
    end
    xs
end

truncated SVD については,いままでいろいろ実装してきましたが,全てランクを先に決め打ちするものでした.今回はランクを決め打ちするものと,許容誤差をあたえておく低ランク近似の両方の関数を用意しておきます.

function lowrank_app_th(M,δ)
    m = rank(M)
    U,Σ,V = svd(M)
    sorted_Σ = sort(Σ)
    err = cumsum(sorted_Σ[begin:end-1].^2)
    reverse!(err)
    
    r = m
    for l = 1:m-1
        if err[l] < δ
            r = l 
            break
        end
    end
    
    Ur = U[:,1:r]
    Σr = Σ[1:r]
    Vr = V[:,1:r]
    # reconstraction is Ur*diagm(Σr)*Vr'
    return Ur,diagm(Σr),Vr,r
end

function lowrank_app(M,r::Int)
    U,Σ,V = svd(M)
    Ur = U[:,1:r]
    Σr = Σ[1:r]
    Vr = V[:,1:r]
    # reconstraction is Ur*diagm(Σr)*Vr'
    return Ur,diagm(Σr),Vr
end

ここからが本体.分解したコアテンソルから元のテンソルを再構成する関数reconst()も用意しておきます. どうもreconst()では結構時間がかかるので,もっとよい実装があるはず...。

using LinearAlgebra
using Glob
using Plots
using Images
using Statistics

"""
Tensor Ring Decomposition based on SVD
proposed by [Q Zhao (2016)](https://arxiv.org/abs/1606.05535)

[Matlab code](https://github.com/oscarmickelin/tensor-ring-decomposition)
is also available,

# Aruguments
- 'T' : input tensor
- 'r' : target rank, which should be vector

example:
    T = randn(20,20,30)
    r = (5,3,10)
    ring_cores = TR(T,r)
    size.(ring_cores)
    # 3-element Vector{Tuple{Int64, Int64, Int64}}:
    # (5, 20, 3)
    # (3, 20, 10)
    # (10, 30, 5)

    # We can obtain low-ring rank tensor by 'reconst'
    Tr = reconst(ring_cores)
"""
function TR(T,r::Vector)
    n = size(T)
    d = length(r)
    @assert r[1]*r[2] <= n[1] "r1*r2 should be smaller than n[1]"
    @assert d == ndims(T) "the length of ranks should be same as the dims of tensor"
    r0 = r[1]*r[2]
    T1 = reshape(T,(n[1],prod(n[2:end])))
    U,Σ,V = lowrank_app(T1,r0)
    G = []
    G1 = permutedims(reshape(U,(n[1],r[1],r[2])),[2,1,3])
    C  = permutedims(reshape(Σ*V',(r[1],r[2],prod(n[2:d]))),[2,3,1])
    push!(G,G1)
    for k=2:d-1
        C = reshape(C,(r[k]*n[k],prod(n[k+1:end])*r[1]))
        U,Σ,V = lowrank_app(C,r[k+1])
        Gk = reshape(U,(r[k],n[k],r[k+1]))
        push!(G,Gk)
        C = reshape(Σ*V',(r[k+1],:,r[1]))
    end
    Gd = reshape(C,(r[d],n[d],r[1]))
    push!(G,Gd)
    return G
end

"""
Tensor ring decomposition with prescribed error value ε.
The reconstraction error becomes smaller than ε.
See more details in the [paper](https://arxiv.org/abs/1606.05535)

# Aruguments
- 'T' : input tensor
- 'ε' : prescribed error, which should be float64

example:
    Tr = reconst(ring_cores)
    T = rand(10,20,7)
    ε = 0.3
    G,r = TR(T,ε);
    Tre = reconst(G)
    @show norm(T - Tre)/norm(T) < ε
    # true

    Gnew = TR(T,r);
    for l = 1:length(r)
        @show Gnew[l] ≈ G[l]
        # true
    end
"""
function TR(T,ε::Float64)
    n = size(T)
    d = ndims(T)
    r = zeros(Int,d)
    G = []
    
    δ1 = sqrt(2/d)*ε*norm(T)
    δ = δ1/sqrt(2)
    T1 = reshape(T,(n[1],prod(n[2:end])))
    U,Σ,V, r0 = lowrank_app_th(T1,δ1)
    factors = divisor(r0)
    r[1] = factors[floor(Int,length(factors)/2)]
    r[2] = Int( r0 / r[1] )
    
    G1 = permutedims(reshape(U,(n[1],r[1],r[2])),[2,1,3])
    C  = permutedims(reshape(Σ*V',(r[1],r[2],prod(n[2:d]))),[2,3,1])
    push!(G,G1)
    for k=2:d-1
        C = reshape(C,(r[k]*n[k],prod(n[k+1:end])*r[1]))
        U,Σ,V, rnew = lowrank_app_th(C,δ)
        r[k+1] = rnew
        Gk = reshape(U,(r[k],n[k],r[k+1]))
        push!(G,Gk)
        C = reshape(Σ*V',(r[k+1],:,r[1]))
    end
    Gd = reshape(C,(:,n[d],r[1]))
    r[d] = size(Gd)[1]
    push!(G,Gd)
    return G, r
end

"""
Reconstract the original tensor from ring cores.
# Arguments
- 'G' : cores tensors
"""
function reconst(G)
    d = length(G)
    I = zeros(Int,d)
    for n = 1:d
        _, In, _ = size(G[n])
        I[n] = In
    end
    reconst_T = zeros(I...)

    for idx in CartesianIndices((tuple(I...)))
        reconst_T[idx] = tr(prod([G[n][:,idx[n],:] for n=1:d]))
    end
    return reconst_T
end
テスト

適当なテンソルを用意して,テンソルリングを大きくしていくとちゃんと誤差が小さくなることを確かめてみます.

T = rand(25,25,25)
for r in [1,2,3,4,5]
    G = TR(T,[r,r,r])
    Tre = reconst(G)
    @show norm(T-Tre)
end
# norm(T - Tre) = 36.07195656407602
# norm(T - Tre) = 35.709229547710216
# norm(T - Tre) = 35.17180322846426
# norm(T - Tre) = 34.43064761988541
# norm(T - Tre) = 33.56737731452813

問題なさそうです.

決め打ちした誤差以下になっているか確かめてみます.

for ε in [10.3, 9.4, 3.3, 1.2, 0.8, 0.01, 0.001]
    G, r = TR(T,ε);
    Tre = reconst(G);
    err = norm(T - Tre)/norm(Tre);
    @show err < ε
end

全部trueになります.問題なさそうです. 最後に適当に画像を再構成してみます.ORL顔画像データセットをダウンロードして,/dataに置いておきます.410枚のグレースケール顔画像を32×27にリサイズして,まとめた32×27×410テンソルTをつくります.とりあえず1枚目の顔画像T[:,:,1]をプロットさせてみます.

names = glob("data/*")
L = length(names) #410
H = 32
W = 27
T = zeros(H,W,L)
l = 1
for name in names
    img = load(name)
    img = Gray.(img)
    img = imresize(img, (H, W))
    img_mtx = convert(Array{Float64}, img)
    T[:,:,l] = img_mtx 
    l += 1
end
plot(Gray.(T[:,:,1]))

このテンソルTを分解して,ε=0.2で再構成して,1枚目の顔画像を復元した様子がこちら.

G, r =TR(T,0.2);
T_reconst = reconst(G);
plot(Gray.(T_reconst[:,:,1]))
@show r
#(4,7,162)

このときランクは(4,7,162)でした.

ランクを小さくすると当然再構成が雑になっていきます.順に第三リングランクを50と10にしてみた様子です.

G=TR(T,[4,7,50]);
T_reconst = reconst(G);
plot(Gray.(T_reconst[:,:,1]))

G=TR(T,[4,7,50]);
T_reconst = reconst(G);
plot(Gray.(T_reconst[:,:,1]))

reconst関数のもっと速い実装

Lirimyさん(@LirimyDh)に次のような高速な実装を教わりました.すごい速くなりました.ありがとうございます.以下,コードの一部をこのページから引用します.私もこういうコードがかけるようになりたいものです.

# 3次元配列を行列の配列に変換する
"Just specify that [an element of] the argument is an array of matrices"
struct MatrixArray end

"""
    R = matrixarray(S::AbstractArray{T,3})

`R[k][i, j] = S[i, k, j]`
"""
matrixarray(S) = [SMatrix{size(S[:, i, :])...}(S[:, i, :]) for i in axes(S, 2)]
#matrixarray(S) = [S[:, i, :] for i in axes(S, 2)] # without StaticArrays


# 行列積の最終段を省き、トレースを直接求める
reconst(G) = reconst(MatrixArray(), matrixarray.(G))
reconst(::MatrixArray, G) = _reconst(G...)

# Gs を真ん中で分けないと遅くなる
function _reconst(Gs...)
    h = length(Gs) ÷ 2
    _reconst(reduce(eachprod, Gs[begin:h]), reduce(eachprod, Gs[h+1:end]))
end

#_reconst(G1, G2, Gs...) = _reconst(eachprod(G1, G2), Gs...) # 遅い
_reconst(G1, G2) = trprod.(G1, expanddim(G2, G1))

"""
    trprod(A, B)

Returns `tr(A * B)`
"""
trprod(A, B) = dot(vec(A'), vec(B))

"""
    C = eachprod(A, B)

`C[i, j, k] = A[i, j] * B[k]`

`A, B, C :: Array{Matrix}`
"""
eachprod(A, B) = A .* expanddim(B, A)


"""
    Bx = expanddim(B, A)

`Bx = reshape(B, (1, 1, 1, m, n))` where `ndims(A) == 3`, `size(B) == (m, n)`
"""
expanddim(B, A) = reshape(B, (ntuple(_ -> 1, ndims(A))..., size(B)...))

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)

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




以上の内容はhttps://genkaiphd.hatenablog.comより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14