備忘録とか日常とか

学んだこととかを書きます。

theanoで局所コントラスト正規化(Local Contrast Normalization)を使う

Deep Learning Tutorial で theanoによる実装,アルゴリズムを勉強中。
研究では主にCNNをtheanoを使っているが、正規化層による効果はどんなものか試してみたくなったので実装する。

theanoはあくまで数値計算ライブラリなので、はじめから正規化のための関数が用意されているわけではない。その他にも機械学習でよく使うような関数が用意されていないこともままある。そこでtheanoをベースとしている機械学習ライブラリであるpylearn2の中から、コードを参考にして実装していく。

参考元は以下
https://github.com/lisa-lab/pylearn2/blob/master/pylearn2/datasets/preprocessing.py
Theano-3D-ConvNet/convnet3d.py at master · lpigou/Theano-3D-ConvNet · GitHub
python - Implementing LeCun Local Contrast Normalization with Theano - Stack Overflow
MIRU2014 tutorial deeplearning



正規化の方法にはいろいろあり、代表的なものを挙げると

  • Global Contrast Normalization(GCN)
  • Local Contrast Normalization(LCN)
  • Local Response Normalization(LRN)
  • ZCA whitening
  • Local mean subtraction

などがある。Deep Learningに限らず、画像認識で一般的に行われる前処理としての正規化はGCNとかが多いらしい。CNNが世に知られるきっかけとなったKrizhevsky氏のImageNetの論文では、RGBの画素値の平均を引くだけにしているので、前処理の必要の有無はタスクによるのかもしれない。Caffeとかでもデフォルトで平均画像を引くようになってたような気がする。


CNN内の正規化層としては、LCNやらLRNが使われる。
お恥ずかしい話ではあるが、つい最近までLCNとLRNをごっちゃにして考えていた・・・。とりあえず違いを確認する。
以下、画像のチャネルを特徴マップと呼ぶ。(元画像はRGBの3チャネルを持つ)

LCNはその名の通り、特徴マップの局所領域内でコントラストを正規化する。この処理は一つの特徴マップ内で完結するので、すべての特徴マップに対して独立して行う。
対してLRNでは、同一位置における異なる特徴マップ間で正規化する。どちらもいくつかのハイパーパラメータはあるが、学習の対象となるパラメータはないので、誤差伝播が容易に可能である。
今回はLCNを実装するのでLRNはまた今度説明することとする。


LCNでは局所領域ごとに平均{\overline{x}_{ij} }を求めて減算、そこから標準偏差{\sigma_{ij} }をとって除算を行う。

単一の特徴マップ{x_{ij} }に対して、画素{(i,j)}を中心とする正方領域{P_{ij}}を考える。
これを局所領域のフィルタとして平均と分散を求める。式で書くと以下のようになる。

{\displaystyle
\overline{x}_{ij} = \sum_{(p,q)\in P_{ij}} w_{pq}x_{i+p,i+q}
}

{\displaystyle
\sigma^2_{ij} = \sum_{(p,q)\in P{ij}} w_{pq}(x_{i+p,j+q}-\overline{x}_{ij})^2
}

ここで、{w_{pq} }は重みであり、上の式では重み付き平均をとっていることになる。
一般的に画像では、注目画素から遠くなるほど輝度変化は大きくなるので、その影響を小さくするために、中心から離れるに連れて低下するような重みを用いる。(要するにガウシアンフィルタ)

上で求めた値を使って正規化する。

{\displaystyle
z_{ij} = \frac{x_{ij}-\overline{x}_{ij}}{\sigma{ij}}
}

平均で引くだけにした場合を減算正規化(Local mean subtractionと同義)、その後標準偏差で割るところまで計算すると除算正規化(LCNと同義)と呼ばれるらしい。
ただし上の計算をそのまますると、コントラストが小さい領域ほど濃淡がはっきり出てしまうことになる。コントラストが大きいところにのみ上記の計算を適用するため、定数{c}を用意して、以下のような計算をする。

{\displaystyle
z_{ij} = \frac{x_{ij} - \overline{x}_{ij}}{max(c, \sigma_{ij})}
}

こうすることで{\sigma_{ij} < c } となるところは濃淡が強調されることを防ぐ。


とりあえず書いてみる。
CNNのうちの一層として使えるようにオブジェクトで定義すると以下のようになる。

class NormLayer(object):
    
    def __init__(self, input, kernel_shape=9, threshold=1e-4, method="lcn"):
        
        input_shape = input.shape
        
        input_shape_X = (input.shape[0]*input.shape[1] , 1 , input.shape[2], input.shape[3])
        X = input.reshape(input_shape_X)
        
        if method=="lcn":
            out = self.lecun_lcn(X, kernel_shape)
        
        self.output = out.reshape(input_shape)
        
        
    
    def lecun_lcn(self, X, kernel_shape, threshold=1e-4):
        """
        Yann LeCun's local contrast normalization
        Orginal code in Theano by: Guillaume Desjardins
        """

        filter_shape = (1, 1, kernel_shape, kernel_shape)
        filters = gaussian_filter(kernel_shape).reshape(filter_shape)
        filters = theano.shared(theano._asarray(filters, dtype=theano.config.floatX), borrow=True)
        
        # 'full' convolution  outputのサイズは  input_shape + filter_shape - 1
        convout = conv.conv2d(input=X,
                              filters=filters,
                              filter_shape=filter_shape,
                              border_mode='full')

        # 各ピクセルから、kernel_size x kernel_size  近傍の平均を減算

        mid = int(numpy.floor(kernel_shape / 2.))
        centered_X = X - convout[:, :, mid:-mid, mid:-mid]

        # kernel_size x kernel_size  近傍の標準偏差で除算
        sum_sqr_XX = conv.conv2d(input=centered_X ** 2,
                                 filters=filters,
                                 filter_shape=filter_shape,
                                 border_mode='full')

        denom = T.sqrt(sum_sqr_XX[:, :, mid:-mid, mid:-mid])
        per_img_mean = denom.mean(axis=[2, 3])
        divisor = T.largest(per_img_mean.dimshuffle(0, 1, 'x', 'x'), denom)
        # 定数c(threshold)で、コントラストが大きい場合のみ標準偏差で除算
        divisor = T.maximum(divisor, threshold)

        new_X = centered_X / divisor

        return new_X


def gaussian_filter(kernel_shape):
    x = numpy.zeros((kernel_shape, kernel_shape),
                    dtype=theano.config.floatX)

    def gauss(x, y, sigma=2.0):
        Z = 2 * numpy.pi * sigma ** 2
        return 1. / Z * numpy.exp(-(x ** 2 + y ** 2) / (2. * sigma ** 2))

    mid = numpy.floor(kernel_shape / 2.)
    for i in xrange(0, kernel_shape):
        for j in xrange(0, kernel_shape):
            x[i, j] = gauss(i - mid, j - mid)

    return x / numpy.sum(x)

使い方としては、画像を[batch_size, channel, height, width]の形の4Dtensorにしてinputに通すだけである。(パラメータは適当に調節する必要あり)
ここのコードはGCNと減算正規化もまとめて実装してあったので、拡張の余地を残すためにmethodという引数を入れといた。

これで一応theanoでLCNが使えるようになる。
正規化が有効かどうかは結局データに依存するのだが、まあ試してみるだけ試す。

次はLRNについて書く。