スパース符号化#

Sparse codingと生成モデル#

Sparse codingモデル [Olshausen and Field, 1996] [Olshausen and Field, 1997]はV1のニューロンの応答特性を説明する線形生成モデル (linear generative model)である.まず,画像パッチ x が基底関数(basis function) Φ=[ϕj] のノイズを含む線形和で表されるとする (係数は r=[rj] とする).

(1)x=jrjϕj+ϵ=Φr+ϵ

ただし,ϵN(0,σ2I) である.このモデルを神経ネットワークのモデルと考えると, Φ は重み行列,係数 r は入力よりも高次の神経細胞の活動度を表していると解釈できる.ただし,rj は負の値も取るので単純に発火率と捉えられないのはこのモデルの欠点である.

Sparse codingでは神経活動 r が潜在変数の推定量を表現しているという仮定の下,少数の基底で画像 (や目的変数)を表すことを目的とする.要は上式において,ほとんどが0で,一部だけ0以外の値を取るという疎 (=sparse)な係数rを求めたい.

確率的モデルの記述#

入力される画像パッチ xi (i=1,,N) の真の分布を pdata(x) とする.また,x の生成モデルを p(x|Φ) とする.さらに潜在変数 r の事前分布 (prior)を p(r), 画像パッチ x の尤度 (likelihood)を p(x|r,Φ) とする.このとき,

(2)p(x|Φ)=p(x|r,Φ)p(r)dr

が成り立つ.p(x|r,Φ)は,(1)式においてノイズ項をϵN(0,σ2I)としたことから,

(3)p(x| r,Φ)=N(x| Φr,σ2I)=1Zσexp(xΦr22σ2)

と表せる.ただし,Zσは規格化定数である.

事前分布の設定#

事前分布p(r)としては,0においてピークがあり,裾の重い(heavy tail)を持つsparse distributionあるいは super-Gaussian distribution (Laplace 分布やCauchy分布などGaussian分布よりもkurtoticな分布)を用いるのが良い.このような分布では,rの各要素riはほとんど0に等しく,ある入力に対しては大きな値を取る.p(r)は一般化して式(4), (5)のように表記する.

(4)p(r)=jp(rj)(5)p(rj)=1Zβexp[βS(rj)]

ただし,βは逆温度(inverse temperature), Zβは規格化定数 (分配関数) である1S(x)と分布の関係をまとめた表が以下となる (cf. Harpur, 1997).

S(r)

dS(r)dr

p(r)

分布名

尖度(kurtosis)

r2

2r

1α2πexp(r22α2)

Gaussian 分布

0

|r|

sign(r)

12αexp(|r|α)

Laplace 分布

3.0

ln(α2+r2)

2rα2+r2

απ1α2+r2=απexp[ln(α2+r2)]

Cauchy 分布

-

分布p(r)S(r)を描画すると次のようになる.

using PyPlot

x = range(-5, 5, length=300)
figure(figsize=(7,3))
subplot(1,2,1)
title(L"$p(x)$")
plot(x, 1/sqrt(2pi)*exp.(-(x.^2)/2), color="black", linestyle="--",label="Gaussian")
plot(x, 1/2*exp.(-abs.(x)), label="Laplace")
plot(x, 1 ./ (pi*(1 .+ x.^2)), label="Cauchy")
xlim(-5, 5); 
xlabel(L"$x$")
legend()

subplot(1,2,2)
title(L"S(x)")
plot(x, x.^2, color="black", linestyle="--",label="Gaussian")
plot(x, abs.(x), label="Laplace")
plot(x, log.(1 .+ x.^2), label="Cauchy")
xlim(-5, 5); ylim(0, 5)
xlabel(L"$x$")

tight_layout()
../_images/sparse-coding_5_0.png

目的関数の設定と最適化#

最適な生成モデルを得るために,入力される画像パッチの真の分布 pdata(x)xの生成モデル p(x|Φ)を近づける.このために,2つの分布のKullback-Leibler ダイバージェンス DKL(pdata(x) p(x|Φ))を最小化したい.しかし,真の分布は得られないので,経験分布

(6)p^data(x):=1Ni=1Nδ(xxi)

を近似として用いる (δ() はDiracのデルタ関数である).ゆえにDKL(p^data(x) p(x|Φ))を最小化する.

DKL(p^data(x) p(x|Φ))=p^data(x)logp^data(x)p(x|Φ)dx=Ep^data[lnp^data(x)p(x|Φ)](7)=Ep^data[lnp^data(x)]Ep^data[lnp(x|Φ)]

が成り立つ.(7)式の1番目の項は一定なので,DKL(p^data(x) p(x|Φ)) を最小化するにはEp^data[lnp(x|Φ)]を最大化すればよい.ここで,

(8)Ep^data[lnp(x|Φ)]=i=1Np^data(xi)lnp(xi|Φ)=1Ni=1Nlnp(xi|Φ)

が成り立つ.また,(2)式より

lnp(x|Φ)=lnp(x|r,Φ)p(r)dr

が成り立つので,近似として p(x|r,Φ)p(r)drp(x|r,Φ)p(r)(=p(x,r|Φ)) で評価する.これらの近似の下,最適なΦ=Φは次のようにして求められる.

Φ=argminΦminrDKL(p^data(x)p(x|Φ))=argmaxΦmaxrEp^data[lnp(x|Φ)]=argmaxΦi=1Nmaxrilnp(xi|Φ)argmaxΦi=1Nmaxrilnp(xi|ri,Φ)p(ri)(9)=argminΦi=1Nminri E(xi,ri|Φ)

ただし,xiに対する神経活動を riとした.また,E(x,r|Φ)はコスト関数であり,次式のように表される.

E(x,r|Φ):=lnp(x|r,Φ)p(r)(10)=xΦr2preserve information+λjS(rj)sparseness of rj

ただし,λ=2σ2βは正則化係数2であり,1行目から2行目へは式(3), (4), (5)を用いた.ここで,第1項が復元損失,第2項が罰則項 (正則化項)となっている.

式(9)で表される最適化手順を最適なrΦを求める過程に分割しよう.まず, Φを固定した下でE(xn,ri|Φ)を最小化するri=r^iを求める (11.1.3).

r^i=argminriE(xi,ri|Φ) (=argmaxrip(ri|xi))

これは r について MAP推定 (maximum a posteriori estimation)を行うことに等しい.次にr^を用いて

Φ=argminΦi=1NE(xi,r^i|Φ) (=argmaxΦi=1Np(xi|r^i,Φ))

とすることにより,Φを最適化する (11.1.4).こちらは Φ について 最尤推定 (maximum likelihood estimation)を行うことに等しい.

Locally competitive algorithm (LCA)#

rの勾配法による更新則は,Eの微分により次のように得られる.

drdt=ηr2Er=ηr[Φ(xΦr)λ2S(r)]

ただし,ηrは学習率である.この式によりrが収束するまで最適化するが,単なる勾配法ではなく,[Olshausen and Field, 1996]では共役勾配法 (conjugate gradient method)を用いている.しかし,共役勾配法は実装が煩雑で非効率であるため,より効率的かつ生理学的な妥当性の高い学習法として,LCA (locally competitive algorithm)が提案されている [Rozell et al., 2008].LCAは側抑制 (local competition, lateral inhibition)と閾値関数 (thresholding function)を用いる更新則である.LCAによる更新を行うRNNは通常のRNNとは異なり,コスト関数(またはエネルギー関数)を最小化する動的システムである.このような機構はHopfield networkで用いられているために,OlshausenはHopfield trickと呼んでいる.

軟判定閾値関数を用いる場合 (ISTA)#

S(x)=|x|とした場合の閾値関数を用いる手法としてISTA(Iterative Shrinkage Thresholding Algorithm)がある.ISTAはL1-norm正則化項に対する近接勾配法で,要はLasso回帰に用いる勾配法である.

解くべき問題は次式で表される.

r=arg minr{xΦr22+λr1}

詳細は後述するが,次のように更新することで解が得られる.

  1. r(0)を要素が全て0のベクトルで初期化:r(0)=0

  2. r(t+1)=r(t)+ηrΦ(xΦr(t))

  3. r(t+1)=Θλ(r(t+1))

  4. rが収束するまで2と3を繰り返す

ここでΘλ()軟判定閾値関数 (Soft thresholding function)と呼ばれ,次式で表される.

Θλ(y)={yλ(y>λ)0(λyλ)y+λ(y<λ)

Θλ()を関数として定義すると次のようになる.また,ReLU (ランプ関数)はmax(x, 0)で実装できる.この点から考えればReLUを軟判定非負閾値関数 (soft nonnegative thresholding function)と捉えることもできる [Papyan et al., 2018]

# thresholding function of S(x)=|x|
soft_thres(x, λ) = max(x - λ, 0) - max(-x - λ, 0)
soft_nonneg_thres(x, λ) = max(x - λ, 0) # relu(x-λ)
soft_nonneg_thres (generic function with 1 method)

次にΘλ()を描画すると次のようになる.

xmin, xmax = -5, 5
x = range(xmin, xmax, length=100)
y = soft_thres.(x, 1)

figure(figsize=(4,4.5))
subplot(2,2,1)
title(L"$S(x)=|x|$")
plot(x, abs.(x))
xlim(xmin, xmax); ylim(0, 10)
hlines(y=xmax, xmin=xmin, xmax=xmax, color="k", alpha=0.2)
vlines(x=0, ymin=0, ymax=xmax*2, color="k", alpha=0.2)

subplot(2,2,2)
title(L"$\frac{\partial S(x)}{\partial x}$")
plot(x, x, "k--")
plot(x, sign.(x))
xlim(xmin, xmax); ylim(xmin, xmax)
hlines(y=0, xmin=xmin, xmax=xmax, color="k", alpha=0.2)
vlines(x=0, ymin=xmin, ymax=xmax, color="k", alpha=0.2)

subplot(2,2,3)
title(L"$f_\lambda(x)=x+\lambda\cdot\frac{\partial S(x)}{\partial x}$")
plot(x, x, "k--")
plot(x, x + 1*sign.(x))
xlabel(L"$x$")
xlim(-5, 5); ylim(-5, 5)
hlines(y=0, xmin=xmin, xmax=xmax, color="k", alpha=0.2)
vlines(x=0, ymin=xmin, ymax=xmax, color="k", alpha=0.2)

subplot(2,2,4)
title(L"$\Theta_\lambda(x)$")
plot(x, x, "k--")
plot(x, y)
xlabel(L"$x$")
xlim(-5, 5); ylim(-5, 5)
hlines(y=0, xmin=xmin, xmax=xmax, color="k", alpha=0.2)
vlines(x=0, ymin=xmin, ymax=xmax, color="k", alpha=0.2)

tight_layout()
../_images/sparse-coding_11_0.png

なお,軟判定閾値関数は次の目的関数Cを最小化するxを求めることで導出できる.

C=12(yx)2+λ|x|

ただし,x,y,λはスカラー値とする.|x|が微分できないが,これは場合分けを考えることで解決する.x0を考えると,(6)式は

C=12(yx)2+λx={x(yλ)}2+λ(yλ)

となる.(7)式の最小値を与えるxは場合分けをして考えると,yλ0のとき二次関数の頂点を考えてx=yλとなる. 一方でyλ<0のときはx0において単調増加な関数となるので,最小となるのはx=0のときである.同様の議論をx0に対しても行うことで (5)式が得られる.

なお,閾値関数としては軟判定閾値関数だけではなく,硬判定閾値関数やy=xtanh(x) (Tanh-shrink)など様々な関数を用いることができる.

重み行列の更新則#

rが収束したら勾配法によりΦを更新する.

Δϕi(x)=ηEΦ=η[([xΦr)r]

Sparse coding networkの実装#

ネットワークは入力層を含め2層の単純な構造である.今回は,入力はランダムに切り出した16×16 (=256)の画像パッチとし,これを入力層の256個のニューロンが受け取るとする.入力層のニューロンは次層の100個のニューロンに投射するとする.100個のニューロンが入力をSparseに符号化するようにその活動および重み行列を最適化する.

画像データの読み込み#

データはhttp://www.rctn.org/bruno/sparsenet/からダウンロードできる 3IMAGES_RAW.matは10枚の自然画像で,IMAGES.matはそれを白色化したものである.matファイルの読み込みにはMAT.jlを用いる.

using MAT
#using PyPlot
# datasets from http://www.rctn.org/bruno/sparsenet/
mat_images_raw = matopen("../_static/datasets/IMAGES_RAW.mat")
imgs_raw = read(mat_images_raw, "IMAGESr")

mat_images = matopen("../_static/datasets/IMAGES.mat")
imgs = read(mat_images, "IMAGES")

close(mat_images_raw)
close(mat_images)

画像データを描画する.

figure(figsize=(8, 3))
subplots_adjust(hspace=0.1, wspace=0.1)
for i=1:10
    subplot(2, 5, i)
    imshow(imgs_raw[:,:,i], cmap="gray")
    axis("off")
end
suptitle("Natural Images", fontsize=12)
subplots_adjust(top=0.9)  
../_images/sparse-coding_19_0.png

モデルの定義#

必要なパッケージを読み込む.

using Base: @kwdef
using Parameters: @unpack # or using UnPack
using LinearAlgebra, Random, Statistics, ProgressMeter
Random.seed!(0)
rc("axes.spines", top=false, right=false)

モデルを定義する.

@kwdef struct OFParameter{FT}
    lr_r::FT = 1e-2 # learning rate of r
    lr_Phi::FT = 1e-2 # learning rate of Phi
    λ::FT = 5e-3 # regularization parameter
end

@kwdef mutable struct OlshausenField1996Model{FT}
    param::OFParameter = OFParameter{FT}()
    num_inputs::Int32
    num_units::Int32
    batch_size::Int32
    r::Array{FT} = zeros(batch_size, num_units) # activity of neurons
    Phi::Array{FT} = randn(num_inputs, num_units) .* sqrt(1/num_units)
end

パラメータを更新する関数を定義する.今回はより生理学的に妥当にするため,軟判定非負閾値関数を用いる.

function updateOF!(variable::OlshausenField1996Model, param::OFParameter, inputs::Array, training::Bool)
    @unpack num_inputs, num_units, batch_size, r, Phi = variable
    @unpack lr_r, lr_Phi, λ = param

    # Updates                
    error = inputs .- r * Phi'
    r_ = r +lr_r .* error * Phi

    #r[:, :] = soft_thres.(r_, λ)
    r[:, :] = soft_nonneg_thres.(r_, λ)

    if training 
        error = inputs - r * Phi'
        dPhi = error' * r
        Phi[:, :] += lr_Phi * dPhi
    end
    
    return error
end
updateOF! (generic function with 1 method)

行ごとに正規化する関数を定義する.

function normalize_rows(A::Array)
    return A ./ sqrt.(sum(A.^2, dims=1) .+ 1e-8)
end
normalize_rows (generic function with 1 method)

損失関数を定義する.

function calculate_total_error(error, r, λ)
    recon_error = mean(error.^2)
    sparsity_r = λ*mean(abs.(r)) 
    return recon_error + sparsity_r
end
calculate_total_error (generic function with 1 method)

シミュレーションを実行する関数を定義する.外側のfor loopでは画像パッチの作成とrの初期化を行う.内側のfor loopではrが収束するまで更新を行い,収束したときに重み行列Phiを更新する.

function run_simulation(imgs, num_iter, nt_max, batch_size, sz, num_units, eps)
    H, W, num_images = size(imgs)
    num_inputs = sz^2

    model = OlshausenField1996Model{Float32}(num_inputs=num_inputs, num_units=num_units, batch_size=batch_size)
    errorarr = zeros(num_iter) # Vector to save errors    
    
    # Run simulation
    @showprogress "Computing..." for iter in 1:num_iter
        # Get the coordinates of the upper left corner of clopping image randomly.
        beginx = rand(1:W-sz, batch_size)
        beginy = rand(1:H-sz, batch_size)

        inputs = zeros(batch_size, num_inputs)  # Input image patches

        # Get images randomly
        for i in 1:batch_size        
            idx = rand(1:num_images)
            img = imgs[:, :, idx]
            clop = img[beginy[i]:beginy[i]+sz-1, beginx[i]:beginx[i]+sz-1][:]
            inputs[i, :] = clop .- mean(clop)
        end

        model.r = zeros(batch_size, num_units) # Reset r states
        model.Phi = normalize_rows(model.Phi) # Normalize weights
        # Input image patches until latent variables are converged 
        r_tm1 = zeros(batch_size, num_units)  # set previous r (t minus 1)

        for t in 1:nt_max
            # Update r without update weights 
            error = updateOF!(model, model.param, inputs, false)

            dr = model.r - r_tm1 

            # Compute norm of r
            dr_norm = sqrt(sum(dr.^2)) / sqrt(sum(r_tm1.^2) + 1e-8)
            r_tm1 .= model.r # update r_tm1

            # Check convergence of r, then update weights
            if dr_norm < eps
                error = updateOF!(model, model.param, inputs, true)
                errorarr[iter] = calculate_total_error(error, model.r, model.param.λ) # Append errors
                break
            end

            # If failure to convergence, break and print error
            if t >= nt_max-1
                print("Error at patch:", iter_, dr_norm)
                errorarr[iter] = calculate_total_error(error, model.r, model.param.λ) # Append errors
                break
            end
        end
        # Print moving average error
        if iter % 100 == 0
            moving_average_error = mean(errorarr[iter-99:iter])
            println("iter: ", iter, "/", num_iter, ", Moving average error:", moving_average_error)
        end
    end
    return model, errorarr
end
run_simulation (generic function with 1 method)

r_tm1 .= model.rの部分は,要素ごとのコピーを実行している.r_tm1 = copy(model.r)でもよいが,新たなメモリ割り当てが生じるので避けている.@. r_tm1 = model.rとしてもよい.

シミュレーションの実行#

# Simulation constants
num_iter = 500 # number of iterations
nt_max = 1000 # Maximum number of simulation time
batch_size = 250 # Batch size

sz = 16 # image patch size
num_units = 100 # number of neurons (units)
eps = 1e-2 # small value which determines convergence

model, errorarr = run_simulation(imgs, num_iter, nt_max, batch_size, sz, num_units, eps);

訓練中の損失の描画#

訓練中の損失の変化を描画してみよう.損失が低下し,学習が進行したことが分かる.

# Plot error
figure(figsize=(4, 2))
ylabel("Error")
xlabel("Iterations")
plot(1:num_iter, errorarr)
tight_layout()
../_images/sparse-coding_36_0.png

重み行列 (受容野)の描画#

学習後の重み行列 Phi (Φ)を可視化してみよう.

# Plot Receptive fields
figure(figsize=(4.2, 4))
subplots_adjust(hspace=0.1, wspace=0.1)
for i in 1:num_units
    subplot(10, 10, i)
    imshow(reshape(model.Phi[:, i], (sz, sz)), cmap="gray")
    axis("off")
end
suptitle("Receptive fields", fontsize=14)
subplots_adjust(top=0.925)
../_images/sparse-coding_38_0.png

白色がON領域(興奮),黒色がOFF領域(抑制)を表す.Gaborフィルタ様の局所受容野が得られており,これは一次視覚野(V1)における単純型細胞(simple cells)の受容野に類似している.

画像の再構成#

学習したモデルを用いて入力画像が再構成されるか確認しよう.

H, W, num_images = size(imgs)
num_inputs = sz^2

# Get the coordinates of the upper left corner of clopping image randomly.
beginx = rand(1:W-sz, batch_size)
beginy = rand(1:H-sz, batch_size)

inputs = zeros(batch_size, num_inputs)  # Input image patches

# Get images randomly
for i in 1:batch_size        
    idx = rand(1:num_images)
    img = imgs[:, :, idx]
    clop = img[beginy[i]:beginy[i]+sz-1, beginx[i]:beginx[i]+sz-1][:]
    inputs[i, :] = clop .- mean(clop)
end

model.r = zeros(batch_size, num_units) # Reset r states

# Input image patches until latent variables are converged 
r_tm1 = zeros(batch_size, num_units)  # set previous r (t minus 1)

for t in 1:nt_max
    # Update r without update weights 
    error = updateOF!(model, model.param, inputs, false)

    dr = model.r - r_tm1 

    # Compute norm of r
    dr_norm = sqrt(sum(dr.^2)) / sqrt(sum(r_tm1.^2) + 1e-8)
    r_tm1 .= model.r # update r_tm1

    # Check convergence of r, then update weights
    if dr_norm < eps
        break
    end
end;

神経活動 rがスパースになっているか確認しよう.

figure(figsize=(3, 2))
hist(model.r[:], bins=50)
xlim(0, 0.5)
tight_layout()
../_images/sparse-coding_43_0.png

要素がほとんど0のスパースなベクトルになっていることがわかる.次に画像を再構成する.

reconst = model.r * model.Phi'
println(size(reconst))
(250, 256)

再構成した結果を描画する.

figure(figsize=(7.5, 3))
subplots_adjust(hspace=0.1, wspace=0.1)
num_show = 5
for i in 1:num_show
    subplot(2, num_show, i)
    imshow(reshape(inputs[i, :], (sz, sz)), cmap="gray")
    xticks([]); yticks([]); 
    if i == 1
        ylabel("Input\n images")
    end

    subplot(2, num_show, num_show+i)
    imshow(reshape(reconst[i, :], (sz, sz)), cmap="gray")
    xticks([]); yticks([]); 
    if i == 1
        ylabel("Reconstructed\n images")
    end
end
../_images/sparse-coding_47_0.png

上段が入力画像,下段が再構成された画像である.差異はあるものの,概ね再構成されていることがわかる.

参考文献#

OF96(1,2)

B A Olshausen and D J Field. Emergence of simple-cell receptive field properties by learning a sparse code for natural images. Nature, 381(6583):607–609, June 1996.

OF97

B A Olshausen and D J Field. Sparse coding with an overcomplete basis set: a strategy employed by v1? Vision Res., 37(23):3311–3325, December 1997.

PRSE18

Vardan Papyan, Yaniv Romano, Jeremias Sulam, and Michael Elad. Theoretical foundations of deep learning via sparse representations: a multilayer sparse model and its connection to convolutional neural networks. IEEE Signal Process. Mag., 35(4):72–89, July 2018.

RJBO08

Christopher J Rozell, Don H Johnson, Richard G Baraniuk, and Bruno A Olshausen. Sparse coding via thresholding and local competition in neural circuits. Neural Comput., 20(10):2526–2563, October 2008.


1

これらの用語は統計力学における正準分布 (ボルツマン分布)から来ている.

2

この式から逆温度βが正則化の度合いを調整するパラメータであることがわかる.

3

これはアメリカ北西部で撮影された自然画像であり,van Hateren’s Natural Image Datasetから取得されたものである.