DropBlockを読んで試したかったかも

タイトル通りDropBlockという論文読みますた
どういう内容かを雑にいえば、CNNのパラメータに対して cutout と呼ばれる一定のサイズの矩形でマスクする操作を行い、ドロップアウトと似た正則化の効果を得るみたいな話だったはず

[1810.12890] DropBlock: A regularization method for convolutional networks

正則化でそこそこ見かけるドロップアウトは全結合層に対しては効果があるけれど、畳み込み層に対してはあまり効果的ではないと述べられていた。というのは畳み込み層の一部のパラメータを欠落させても、その隣接しているパラメータからドロップアウトなしとほぼ同じ情報量が次に伝搬されてしまうとか
これは論文に載っている次の図から納得がいった

f:id:Owatank:20181220170416p:plain

(b)の図が畳み込みのパラメータをドロップアウトで一部欠落させたものと解釈する。もしこの一部が黒で塗りつぶされた画像を渡されて、緑の領域を復元してほしいと言われたら確かにその隣接しているピクセル情報からおおよそ見当がつくし復元もできそう
一方で(c)の方を渡されたら、(b)よりは復元が難しそうだ。できたとしても(b)より復元の精度は落ちてしまう自分なら自信がある

ドロップアウトが畳み込み層に対して効果が微妙なのは上の図のようにランダムに情報を落としてしまうから。だから畳み込み層に対して効果のあるドロップアウトの方法を考えよって流れはわからんでもないような

そして提案されたDropBlockは図(c)のように連続した一定の領域をドロップアウトさせる方法になっている
ちなみに SpatialDropout と呼ばれるドロップアウトは畳み込みがもつパラメータの一部フィルターを丸ごとドロップアウトさせる方法でこれは畳み込みに対して効果があったそう
RNNにも独自のドロップアウトあるらしい。無知な自分にはRelated Workはお宝の山やでほんま

DropBlockは block_size と γ の2つをパラメータで持っている
block_sizeはドロップアウトさせる領域のサイズのために、γはドロップアウトさせる箇所の選択に用いられる

f:id:Owatank:20181220172731p:plain

論文の図でいえば赤の箇所の選択にγが、γで選ばれた箇所を中心にblock_sizeでマスク領域を作る

まずはマスクの中心位置となる赤の箇所を決める。Dropoutはkeep_probの値をパラメータに持つベルヌーイ分布に従ってランダムに選んでいた。同様にDropBlockでもγをパラメータに持つベルヌーイ分布に従って赤の箇所を決めるけど、γは次の式に従って計算して決める

{ \displaystyle \gamma = \frac{1 - \verb|keep_prob|}{\verb|block_size|^2} \frac{\verb|feat_size|^2}{ {( \verb|feat_size - block_size| + 1 ) }^2} }

やべえアンダースコア打てないのバレる
keep_prob はDropoutで使われている各ユニットを保持するか欠落させるかの確率の値で、 feat_size は特徴マップのサイズの値をしている

なんでγはこんな計算しないといけないかというと、DropBlockはDropoutと違って欠落させるユニットを中心にマスク領域を作るため、ポンポン欠落させる箇所が増えるとマスク領域で特徴マップ全体が欠落してしまう可能性が考えられるのでγは慎重に選ばねばならないのだ・・・

適当に値をそれぞれ入れてみると、 keep_probの値が小さい、またはfeat_sizeが大きいとγの値は大きくなって(赤の箇所が多く選ばれる)、逆にblock_sizeの値が大きいとγの値は小さくなった

うまく設計されてるなと思いつつ、一つ疑問に思ったのがこの計算式によって { \displaystyle 0 \leq \gamma \leq 1 } になる保証ってあるんだろうか?ということ
いや自分が計算式見て感じ取れなかっただけなんですけど・・・ { \displaystyle 1 - \verb|keep_prob| } はいいけど、{ \displaystyle { ( \verb|feat_size - block_size| + 1 ) }^2 }は何故差分とって割ってるんだろうここが保証している部分なのか

なんかこう距離とはいえずとも指標になるような空間ができているんだろうな多分・・・めっちゃ気になるしわからんままだとムズムズするがな

話は戻ってγが計算できたら、γをパラメータに持つベルヌーイ分布に従ってマスクの行列(マスクを適用する入力と同じサイズ)を作る。上の図のようにそのマスクの行列はどっかしらの要素が 0 (赤の箇所) になっているはずなので、その要素の位置を中心に block_sizeの大きさに従ってマスク領域を作る。図(b)のように赤の箇所を中心に黒の部分を作る

ブロックマスクができたのであとは入力に適用して、最後のその入力の特徴量を正規化すればDropBlockの操作は終わり

ちなみにDropBlockはγ間接的にはkeep_probを入力として受け取るけども、keep_probの値は固定値よりかは 0.95 あたりから徐々に減らしていく方が効果があるそう。レベルが上がると難易度が上がるパズルゲームじゃん

実際に試してみたくなる。というわけでPyTorchで実装、テストを試みる

一度もPyTorchのモジュール使ってオリジナルの最適化とか活性化関数とか作ったことがないので、どないすればか悩む。
dropoutの実装コードでも見てみようかと探したら次の実装が見つかった

github.com

DropBlockというクラスを作ってforwardを定義すればなんとかできそう
次に悩んだのがマスク領域の作成。なんかこの操作って画像処理でのDilationって操作に似てるなと思いそれを参考に実装した

DropBlockの実装は次のようになった

import numpy as np
import torch
import torch.nn as nn

class DropBlock(nn.Module):
    def __init__(self, keep_prob, block_size):
        super(DropBlock, self).__init__()
        self.keep_prob = keep_prob
        self.block_size = block_size
        self.gamma = 0.

    def forward(self, x):
        if(self.training==False):
            return x
        gamma = self.calc_gamma(x)
        init_mask = torch.zeros((x.shape[0], x.shape[2], x.shape[3])).add(1. - gamma)
        init_mask = init_mask.to(x.device)
        init_mask = torch.bernoulli(init_mask)
        mask = self.create_block_mask(init_mask, self.block_size//2)
        output = x * mask[:,None,:,:]
        output = (output * output.shape[0] * output.shape[2] * output.shape[3]) / output.sum()
        return output
    def calc_gamma(self, x):
        gamma = ((1. - self.keep_prob) * x.shape[2] ** 2) / (self.block_size ** 2 * (x.shape[2] - self.block_size + 1)**2 )
        return gamma

    def create_block_mask(self, m, kernel_size):
        width = m.shape[1]
        height = m.shape[2]
        out = m.clone()
        for n in range(0, m.shape[0]):
            for i in range(0,width):
                for j in range(0,height):
                    if(m[n, i, j] < 1):
                        out[n, np.maximum(0,i-kernel_size):np.minimum(width,i+kernel_size),
                         np.maximum(0,j-kernel_size):np.minimum(height,j+kernel_size)] = 0
        return out

output = x * mask[:,None,:,:]この部分は x は nチャネル持ってるけど、そのnチャネルに対して1チャネルのマスクをそれぞれ適用させたかったのでこの方法で適当した。

python - How does numpy.newaxis work and when to use it? - Stack Overflow

実験に使うデータはMNISTより複雑のがいいかなと思いcifar10を使った。Dropout(keep_prob=0.2) と DropBlock(keep_prob=0.95, block_size=3) で精度を比較してみる
学習データをbatch_size = 100、100step = 1epochで20epochまで回す

実験のモデルの詳細はこっちに置いておく
github.com

訓練の1epochごとのlossはこんな感じだった

f:id:Owatank:20181220221528p:plain

DropBlock悪い方向に学習してはいないんじゃないかと思ったけど精度でダメだった

Dropoutが 10000 test images: 36 % だったのに対して DropBlock は 10000 test images: 10 %だった。グエー
追い打ちで致命的だったのが計算時間 Dropoutありで1epochにかかる時間はcolab上の環境だと2秒ほど。DropBlockありだと1epoch何秒かかったかというと約300秒かかった

f:id:Owatank:20181220222539j:plain

倍プッシュってレベルじゃないよ使いもんにならねえだろ・・・lossの下がり方を見るに正則化のとしての役割をトンチキに実装してしまったとは思いたくはないんだけれども
ブロックマスクを作る処理が効率悪い書き方なために遅くなってるのかなあ。各ピクセルを見ていくんじゃなくて各行sum()でチェックすべきだったか
だから時給600円なんだよお前

うーんDilationの操作にこだわりすぎてて、似た感じでブロックマスク生成できてもっと早い操作とかがあるのかもしれない