takminの書きっぱなし備忘録 @はてなブログ

主にコンピュータビジョンなど技術について、たまに自分自身のことや思いついたことなど

scikit-learnでSparse CodingとDictionary Learning -実践編-

この記事は@naoya_tさん主催の機械学習アドベントカレンダーの12月25日分の記事です。そして昨日のコンピュータビジョンアドベントカレンダーScikit-LearnでSparse CodingとDictionary Learning -理論編-」の続きです。

機械学習ACのサイトから来た方に簡単に自己紹介をすると、私はコンピュータビジョンや画像処理の分野でフリーランスをしているid:takminと言います。プロフィールはここにまとまってます。


ここではscikit-Learnを使ってSparse CodingとDictionary Learningを実装してみようと思います。Sparse CodingとDictionary Learningについて知らない方は、こちらの記事を先に読まれることをお勧めします。


scikit-learn(Pythonベースの機械学習ライブラリ)でSparse CodingとDictionary Learningを使用するにあたって、参考になるサイトは以下の通りです。


scikit-learnでのSparse CodingとDictionary Learning概説
http://scikit-learn.org/stable/modules/decomposition.html#dictionary-learning


SparseCoderクラス解説
http://scikit-learn.org/stable/modules/generated/sklearn.decomposition.SparseCoder.html


Sparse Codingサンプル
http://scikit-learn.org/stable/auto_examples/decomposition/plot_sparse_coding.html


DictionaryLearningクラス解説
http://scikit-learn.org/stable/modules/generated/sklearn.decomposition.DictionaryLearning.html


Dictionary Learningサンプル
http://scikit-learn.org/stable/auto_examples/decomposition/plot_image_denoising.html


ここでは、以下のようなグレースケールの画像とノイズを含んだ画像からの画像再構成に挑戦したいと思います。

辞書の学習

辞書は8x8ピクセルの画像パッチ約3万枚から作成しました。
これらの画像パッチはCaltech256という顔、飛行機、犬など、256カテゴリの物体の画像を集めた、画像認識用のデータセットからランダムに切り取って使用しました。切り取った画像のサンプルは以下の様な感じです。


尚、上のパッチはカラーですが、グレースケールになおしてから学習を行いました。


辞書の学習にはsklearn.decomposition.DictionaryLearningクラスを使用します。が、3万枚の画像を学習するのは非常に時間がかかるため、ここではsklearn.decomposition.MiniBatchDictionaryLearningという簡易版のクラスを使用しました。このクラスは学習速度はDictionaryLearningクラスよりも早いが精度はやや劣るとのこと。


以下、辞書作成のサンプルコードになります。

import Image
import ImageOps
import numpy as np
from time import time
from sklearn.decomposition import MiniBatchDictionaryLearning

#画像リスト読み込み
def ImageListFile2Array(filename):
    imgArray = None
    image_list = open(filename)
    for image_file in image_list:
        #改行を削除
        image_file =  image_file.rstrip()
        print image_file
        
        #画像を読み込み   
        im = Image.open(image_file)
        
        #グレースケール変換
        gray_im = ImageOps.grayscale(im)
        
        data = np.asarray(gray_im)
        data = data.reshape(1,data.size)
        if imgArray is None:
            imgArray = data
        else:
            imgArray = np.vstack( (imgArray, data) )
    return imgArray

    
#画像パッチのサイズ
patch_size = (8,8)

#基底の数
num_basis = 100

#画像リスト読み込み
imgArray = ImageListFile2Array('patchlist.txt')
        
# 辞書クラスの初期化
print 'Learning the dictionary... '
t0 = time()
dico = MiniBatchDictionaryLearning(n_atoms=num_basis, alpha=1.0, transform_algorithm = 'lasso_lars', transform_alpha=1.0, fit_algorithm = 'lars', n_iter=500)

#平均を0、標準偏差を1にする(白色化)
M = np.mean(imgArray, axis = 0)[numpy.newaxis,:]
whiteArray = imgArray - M
whiteArray /= np.std(whiteArray, axis = 0)

#辞書を計算
V = dico.fit(whiteArray).components_

#処理時間を出力
dt = time() - t0
print 'done in %.2fs.' % dt

#辞書を保存
np.save('Dictionaries.npy', V)

上のコードでは、パッチ画像ファイルパスを改行で区切ったテキストファイル(patchlist.txt)を事前に用意しておき、それを読み込んで辞書の作成を行なっています。
MiniBatchDictionaryLearning()でインスタンスを作成する際に、作成する基底の数(n_atoms)、辞書のアップデートやSparse Coding時それぞれの最適化アルゴリズム(fit_algorithm、transform_algorithm)およびそのパラメータ(alpha, transform_alpha, etc)を指定できます。ここでは基底の数を100としています。


上記のコードで生成した辞書を可視化したのが、下の画像になります。


余談ですが、どこかでDictionary Learningで学習した基底は独立成分分析(ICA)で取得した基底と似たものが得られる、と聞いたことがあったので、実際に試してみました。ICAでの基底の求め方はこちらを参考にして下さい。


うーん、あまり似てないですね(笑)。まあ、ICAとDictionary Learningでは目的とするところが違うので当然の結果なのかもしれませんが。
ICAの方が小さい周波数の基底を取っているように見えます。

Sparse Codingによる画像の再構成

では続いて、Sparse Codingによる画像の再構成にチャレンジします。

import Image
import ImageOps
import numpy as np
from time import time
from sklearn.decomposition import SparseCoder

#グレースケールの配列をRGBに変換
def GrayArray2RGB(gray_array):
    gray_shape = gray_array.shape
    rgb_data = np.zeros*1
im.save(dst_file, 'PNG')
print 'save image as ' + dst_file

以下の通りSparseCoder()でパラメータを指定してインスタンスを生成し、transform()関数でスパースな基底の係数を求めます。上の例ではOrthogonal Matching Pursuit(OMP)アルゴリズムを用いて、10または30個の基底とその係数を求めています。

結果はこんな感じです。


ノイズ無し(L0ノルム:10, 30)


ノイズ有り(L0ノルム:10, 30)


当然ですが、L0ノルムの数(基底の数)を増やした方が再構成の精度は高いですが、ノイズの影響が受けやすいことがわかります。


次に、学習を行わずにランダムに生成した辞書を元に、再構成を行なってみました。


ノイズ無し(L0ノルム:10, 30)


ノイズ有り(L0ノルム:10, 30)


ランダムな辞書で十分みたいな話を聞いた気がしたのですが、この結果を見る限り辞書を学習させた方が再構成の精度的にもノイズの強さ的にも良いみたいです。

感想

というわけで理論編と実践編の2回に分けて、scikit-learnでSparse CodingとDictionary Learningを実装しました。どこか見落としている点やコメントなどありましたら、ぜひ御指摘下さい。
今回は簡単に試して使い方を確認した程度ですが、辞書数と誤差の関係やどんな最適化アルゴリズムを選ぶかなど色々と調整の仕方がありそうだと感じました。

*1:gray_shape[0], gray_shape[1], 3), dtype=gray_array.dtype) for c in range(3): rgb_data[:,:,c] = gray_array[:,:,0] return rgb_data #復元する画像 src_file = 'lena_noise.png' #保存する画像 dst_file = 'recon_img.png' #画像パッチのサイズ patch_size = (8,8) #作成した辞書をロード V = np.load('Dictionaries.npy') #画像を読み込み im = Image.open(src_file) #グレースケール変換 gray_im = ImageOps.grayscale(im) #出力画像を初期化 dst_array = np.zeros( (gray_im.size[1], gray_im.size[0]) ) #画像をpatch_sizeで分割して処理 w = gray_im.size[0] - patch_size[0] h = gray_im.size[1] - patch_size[1] y = 0 while y <= h: x = 0 while x <= w: #パッチサイズの領域を切り取り box = (x,y,x+patch_size[0],y+patch_size[1]) crop_im = gray_im.crop(box) #arrayに格納 data = np.asarray(crop_im) data = data.reshape(1,data.size) #Sparse Coding coder = SparseCoder(dictionary=V, transform_algorithm='omp', transform_n_nonzero_coefs=10) u = coder.transform(data) #信号を復元 s = np.dot(u, V) #復元した画像をコピー s = s.reshape(patch_size[1],patch_size[0]) dst_array[y:y+patch_size[1], x:x+patch_size[0]] = s x+=patch_size[0] y+=patch_size[1] #再構成した画像を保存 dst_array = dst_array.reshape(gray_im.size[1],gray_im.size[0], 1) rgb_data = GrayArray2RGB(dst_array) im = Image.fromarray(np.uint8(rgb_data