0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

画像を拡大・縮小するモデル

Posted at

畳み込みは一般に平行移動に強く、画像が任意量だけ平行移動した際でも畳み込みによって元の画像と同じようにその特徴量を取り出すことが出来る、という事は有名である。

一方、畳み込みは拡大・縮小や回転に対しては万能ではなく、同じ畳み込み重みでは拡大した画像や回転した画像からは同じ特徴量を得る事が出来ない。拡大・縮小や回転した画像にはそのサイズや角度に適した別の畳み込み重みを考える必要がある。
厳密にはData Augmentationではないのだが、モデル内である計算を行う事で画像を任意サイズに拡大・縮小を行う全結合を考えてみたい。

モデル

入力を$(H,W,C)=(224,224,3)$、出力を$(224,224,3)$とする。
$H$方向の全結合重み$H_{in}×H_{out}=(224,224)$
$W$方向の全結合重み$W_{in}×W_{out}=(224,224)$を定義する。
行列の並びを入れ替えつつ$(224,224)$の重みを持つ全結合を二回行う。

from tensorflow.keras import Input
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense, Permute

input = Input(shape=(224, 224, 3))
x = Permute((2,3,1))(input)
x = Dense(224, use_bias=False)(x)
x = Permute((2,3,1))(x)
x = Dense(224, use_bias=False)(x)
y = Permute((2,3,1))(x)
model = Model(input, y)
model.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
input_1 (InputLayer)         [(None, 224, 224, 3)]     0
_________________________________________________________________
permute (Permute)            (None, 224, 3, 224)       0
_________________________________________________________________
dense (Dense)                (None, 224, 3, 224)       50176
_________________________________________________________________
permute_1 (Permute)          (None, 3, 224, 224)       0
_________________________________________________________________
dense_1 (Dense)              (None, 3, 224, 224)       50176
_________________________________________________________________
permute_2 (Permute)          (None, 224, 224, 3)       0
=================================================================
Total params: 100,352
Trainable params: 100,352
Non-trainable params: 0
_________________________________________________________________

恒等変換

モデルの二回の全結合重みに対角行列を与える。
重みは255倍してPIL画像にする。この場合、入力画像とモデルの出力画像は等しい。

import numpy as np
from PIL import Image

model_weight1 = np.eye(224)
model_weight2 = np.eye(224)
model_weight1_img = Image.fromarray((model_weight1*255).astype(np.uint8))
model_weight2_img = Image.fromarray((model_weight2*255).astype(np.uint8))
model.layers[2].set_weights([model_weight1])
model.layers[4].set_weights([model_weight2])

...

img = Image.open('cat_dog.png')
img2 = np.asarray(img)
img2 = img2.reshape([1,224,224,3])
img3 = model.predict(img2)[0]

img3 = Image.fromarray(img3.astype(np.uint8))
img4 = get_concat_h_multi_blank([img,model_weight1_img,model_weight2_img,img3])
img4.show()

test0.png

垂直反転・水平反転

対角行列の並びを反転させると画像が反転する。
一回目の全結合重みを反転させると垂直反転で二回目の全結合重みを反転させると水平反転になる。

flip_vertical = True
flip_horizontal = False
if flip_vertical:
    model_weight1 = model_weight1[:,::-1]
if flip_horizontal:
    model_weight2 = model_weight2[:,::-1]

test1.png
test2.png

拡大・縮小

全結合重みを画像化して横幅を拡大してからクロップする。
この画像を全結合重みに変換して設定する。何故か一部のデータが欠損するが、理由は分からない。例えば、犬の足元の左側の床が黒くなってる。

def crop_center(pil_img, crop_width, crop_height):
    img_width, img_height = pil_img.size
    return pil_img.crop(((img_width - crop_width) // 2,
                         (img_height - crop_height) // 2,
                         (img_width + crop_width) // 2,
                         (img_height + crop_height) // 2))

x_scale = 0.5
y_scale = 0.7
model_weight1 = np.eye(224)
model_weight2 = np.eye(224)

model_weight1_img = Image.fromarray((model_weight1*255).astype(np.uint8))
model_weight2_img = Image.fromarray((model_weight2*255).astype(np.uint8))
model_weight1_img = model_weight1_img.resize((int(224*y_scale), 224), Image.BILINEAR)
model_weight2_img = model_weight2_img.resize((int(224*x_scale), 224), Image.BILINEAR)
model_weight1_img = crop_center(model_weight1_img, 224, 224)
model_weight2_img = crop_center(model_weight2_img, 224, 224)
model_weight1 = np.asarray(model_weight1_img)/255.0
model_weight2 = np.asarray(model_weight2_img)/255.0

model.layers[2].set_weights([model_weight1])
model.layers[4].set_weights([model_weight2])

test3.png

x_scale = 1.2
y_scale = 1.5

test4.png

複数の縮小を並べる

縮小の全結合重みを横に並べて結合すると複数の縮小画像を結合した結果が得られる。縦に三通りの傾きの線が通っているが、三本目の線は非常に薄くて目を凝らさないと見えない。
この結果は重みの平行移動が画像の平行移動にもなっていることを示唆する。

def get_concat_h_blank(im1, im2, color=(0, 0, 0)):
    dst = Image.new('RGB', (im1.width + im2.width, max(im1.height, im2.height)), color)
    dst.paste(im1, (0, 0))
    dst.paste(im2, (im1.width, 0))
    return dst

def get_concat_h_multi_blank(im_list):
    _im = im_list.pop(0)
    for im in im_list:
        _im = get_concat_h_blank(_im, im)
    return _im

model_weight1_img_a = model_weight1_img.resize((int(224*0.6), 224), Image.BILINEAR)
model_weight1_img_b = model_weight1_img.resize((int(224*0.3), 224), Image.BILINEAR)
model_weight1_img_c = model_weight1_img.resize((int(224*0.1), 224), Image.BILINEAR)
model_weight2_img_a = model_weight2_img.resize((int(224*0.6), 224), Image.BILINEAR)
model_weight2_img_b = model_weight2_img.resize((int(224*0.3), 224), Image.BILINEAR)
model_weight2_img_c = model_weight2_img.resize((int(224*0.1), 224), Image.BILINEAR)
model_weight1_img = get_concat_h_multi_blank([model_weight1_img_a, model_weight1_img_b, model_weight1_img_c])
model_weight2_img = get_concat_h_multi_blank([model_weight2_img_a, model_weight2_img_b, model_weight2_img_c])
model_weight1_img = crop_center(model_weight1_img, 224, 224)
model_weight2_img = crop_center(model_weight2_img, 224, 224)

model_weight1 = np.asarray(model_weight1_img)[:,:,0]/255.0
model_weight2 = np.asarray(model_weight2_img)[:,:,0]/255.0

test6.png

Cutout

Data Augmentationで画像の任意領域を黒塗りにする操作をCutoutという。
$x,y$の任意領域をそれぞれ黒塗りにする事は可能だが、両方使うと得られる黒く塗り領域はCutoutのような長方形状ではなく十字領域になる。

model_weight1[150:200,150:200] = 0
model_weight2[50:70,50:70] = 0

test5.png
また、逆のことをやれば、ある任意範囲のみの画像を領域を取り出すこともできる。
元の画像からこの領域を差し引くことでCutoutに相当する任意領域を黒塗りは作成できる。

model_weight1[:150,:150] = 0
model_weight1[200:,200:] = 0
model_weight2[:50,:50] = 0
model_weight2[120:,120:] = 0

test8.png

対角行列がS字の直線

多分意味はないので結果のみ示す。
test10.png

対角行列がカーブする時

多分意味はないので結果のみ示す。
test7.png

離散フーリエ変換(DFT)

W_N^n=e^{-2\pi i(n/N)}

image.png
全結合重みが上記の様な係数を与える行列ならこの全結合演算は$H$または$W$の離散フーリエ変換(DFT)に等しいと見なせる(…筈である)。

model_weight1 = np.zeros((224,224))
model_weight2 = np.zeros((224,224))

for i in range(224):
    for j in range(224):
        model_weight1[i,j] = np.exp(-2*np.pi*1j*i*j/224)
        model_weight2[i,j] = np.exp(-2*np.pi*1j*i*j/224)

test11.png
画像の2次元FFTでよく知る中心に十字の放射状の周波数スペクトルに似てないが、あっちは複素数の絶対値の結果にlogを掛ける強度スペクトルであるからだろう。虚部のモデルも作成すれば以下の様に良く知る周波数スペクトルが得られる。

img3a = model.predict(img2)[0]
img3b = model2.predict(img2)[0]

img3 = img3a + 1j * img3b
img3 = np.fft.fftshift(img3)
img3 = 20 * np.log(np.absolute(img3))

test13.png

ここでDFTの計算量は$O(N^2)=224224(行列積の負荷)$。
一方、FFTは$O(Nlog_2N)$で$2^8=256$で$8$回のバタフライ演算をする事で良く、物理的な計算量が$8*(バタフライ演算)=2568((行列積の負荷+ビットリバースの負荷))$で済む。十分に$N$が大きければ当然FFTは速度的に有利だが、ビットリバースの負荷が無視できない$N$の大きさでは必ずしもFFTはDFTより速くならないはずである。

クロネッカーのデルタ関数

恒等変換に掛けている対角行列がクロネッカーのデルタと同じで以下の定義がある。

\delta_{i,j}=\begin{cases}
1 & ( i = j ) \\
0 & ( i \neq j )
\end{cases}

さて、ここでクロネッカーのデルタを示す関数を$relu$関数を用いて表現してみたい。

f(i,j)=relu(1+(i-j)-2\cdot relu(i-j))

と定義すればこれはクロネッカーのデルタである事が分かる。

f(i,j)=\begin{cases}
relu(1-(i-j))=0 & ( i -j \geqq 1 ) \\
1 & ( i -j = 0 )\\
relu(1+(i-j))=0 & ( i -j \leqq -1 )
\end{cases}

ここでpos_embである$r=(1,2,3...,224)$を与え、$f(r_i,r_j^T)\cdot x$の行列演算を考えれば、これは対角行列を掛ける全結合(恒等変換)に等しい。
ここで$(i-j)→(2i-j)$とすれば傾き1以外の対角行列も作れる。

90°回転

$(H,W,C)$の行列を$(W,H,C)$の行列に軸順番を操作を追加するとy = Permute((2,1,3))(x)、結果を90°回転することが出来る。
一方でこれまでの$H×H$全結合の重みや$W×W$の全結合をいくら考えても回転操作を与える全結合重みは与える事が出来ない。

input = Input(shape=(224, 224, 3))
x = Permute((2,3,1))(input)
x = Dense(224, use_bias=False)(x)
x = Permute((2,3,1))(x)
x = Dense(224, use_bias=False)(x)
x = Permute((2,3,1))(x)
y = Permute((2,1,3))(x)           # 追加。HとWを入れ替え
model = Model(input, y)

test9.png

任意角回転

これは次元を結合して$HW×HW$の全結合重みを与えれば任意角回転に相当する全結合重みを得ることは出来る。しかし、一般に画像サイズが$(224,224)$なら$224\cdot 224 \cdot 224 \cdot224=2.5×10^9$でFP32なら全結合のパラメータに$10GB$のメモリが必要になる。これはあまり現実的ではない。
正直、回転を与える変換がどんな形になるかよく分からない。

MLP-Mixerにおける全結合

画像が$(H,W,C)$で与えらた時、パッチ分割を行うと$(HW/p^2,C\cdot p^2)$という次元になり、
MLP-Mixerではパッチ方向の全結合が$(HW/p^2,HW/p^2)$、チャンネル方向の全結合が$(C\cdot p^2,C\cdot p^2)$である。パッチ方向の全結合は上で議論してきた$H×H$全結合の重みや$W×W$の全結合重みと性質は異なる。むしろ、任意角回転において議論した$HW×HW$の全結合重みに近い。
今回のことを試した動機としてViTのAttentionが入力に依存する任意の画像変換になってたりしないかという事があったのだが、今回のテストはこの疑問の答えには繋がらなかった。

まとめ

畳み込みを行う前に$H×H$全結合の重みや$W×W$の全結合重みを与えた時、その処理は拡大・縮小などの一部のDataAugmentation処理の内容に等しいのではと思った。ただし、今回のように任意の初期全結合重みを与えた場合、その変換は確率的に変化せず、また全ての入力に対し同じ変換しか与えれないためDataAugmentation処理を代替することは出来ない。
利点として挙げられるのは複数の異なるタイプの画像変換が組み合わさった場合(反転、拡大、平行移動)でも、最適化したまとめた全結合重みを作れれば計算は1回で済む(affine変換のように)。

Kerasでのデータ拡張のImageDataGeneratorは今回のように変換をつくる前処理モデルを作って本モデル前に繋げているのだろうか?自前のDataGeneratorの作り方を見る限りはかなり自由に作れるのでそんな感じではないが…。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?