みんな大好きResNetのReceptive field(受容野)について調べます。
Receptive field(受容野)
畳み込み演算におけるReceptive field(受容野)とは畳み込み演算の結果の1点と結合する入力画像の幅のことを指します。ある1点の特徴量を計算したい場合、その点を基準にどれくらいマージンをとれば良いかが分かります。
ResNet
2015年にMicrosoft Researchが考案した畳み込みニューラルネットワークのアーキテクチャであり、様々な画像認識タスクで用いられています。
ResNetのアーキテクチャ
論文を参考にResNetのアーキテクチャを確認します。
conv3_x以降のダウンサンプリング(stride=2)するタイミングですが、論文中に
Downsampling is performed by conv3_1, conv4_1, and conv5_1 with a stride of 2.
と書かれおり、conv3_x以降のブロックの先頭で行われていることが分かります。
ResNet50のReceptive field
まずは畳み込みのKernel sizeとStrideがReceptive fieldにどのような影響を与えるかを考えます。
Kernel sizeとReceptive field
畳み込みのKernel sizeをksizeとするとReceptive fieldは(ksize-1)/2増加すると考えることが出来ます。
StrideとReceptive field
stride=2で畳み込んだ場合、次回以降の畳み込みのReceptive fieldの増加量が2倍になると考えることが出来ます。
Receptive field試算
以上を踏まえてResNet50のReceptive fieldを試算するための表を作成します(1x1の畳み込み等を除く)。
block名 | ksize | stride=2 | RF増加分 | RF累積 | |
---|---|---|---|---|---|
(input) | 1 | ||||
conv1 | 7 | ✓ | 3 | ||
conv2_x | max pool | 3 | ✓ | 2 | |
conv2_1 | 3 | 4 | |||
conv2_2 | 3 | 4 | |||
conv2_3 | 3 | 4 | 18 | ||
conv3_x | conv3_1 | 3 | ✓ | 4 | |
conv3_2 | 3 | 8 | |||
conv3_3 | 3 | 8 | |||
conv3_4 | 3 | 8 | 46 | ||
conv4_x | conv4_1 | 3 | ✓ | 8 | |
conv4_2 | 3 | 16 | |||
conv4_3 | 3 | 16 | |||
conv4_4 | 3 | 16 | |||
conv4_5 | 3 | 16 | |||
conv4_6 | 3 | 16 | 134 | ||
conv5_x | conv5_1 | 3 | ✓ | 16 | |
conv5_2 | 3 | 32 | |||
conv5_3 | 3 | 32 | 214 |
実験
500x500ピクセルの画像を上下に分割し、ResNet50に入力して中間層conv4_xでの特徴マップを結合させます。上下分割しない場合の特徴マップを一致させるにはどれくらいのマージンが必要かを考えます。
試算によるとマージンは134ピクセルで良さそうです。但し、畳み込み演算はシフト不変ですが、ダウンサンプリングの影響でconv4_xの場合は16刻みで平行移動させないと計算結果が変わってしまいます。ここは16の倍数で丸めてマージンを144ピクセルとしましょう。128ピクセルでは不一致となるはずです。
ソースコード
Torchvisionのresnet50を使用します。論文中のブロック名「conv4_x」は「layer3」に対応します。
import torch
from torchvision.models import resnet50
from torchvision.models.feature_extraction import create_feature_extractor
def test_margin(margin):
# ResNet50をロード
model = create_feature_extractor(
resnet50(weights="DEFAULT"),
return_nodes=["layer3"]) # conv4_xはlayer3に対応
model.eval()
# 入力画像(ランダム)
input = torch.rand((1,3,500,500))
# 分割しないで推論
with torch.no_grad():
feature = model(input)["layer3"]
# 分割位置(16の倍数にする)
half = input.size(2)//2
half = half - half%16
# マージンを付加して上下分割
input_t = input[:, :, :half+margin]
input_b = input[:, :, half-margin:]
# 分割して推論
with torch.no_grad():
feature_t = model(input_t)["layer3"]
feature_b = model(input_b)["layer3"]
# マージンを付加した分を切り落とす
feature_t = feature_t[: ,:, :-margin//16]
feature_b = feature_b[:, :, margin//16:]
# 上下結合
feature_t_b = torch.cat((feature_t, feature_b), dim=2)
# 誤差を計算
err = (feature - feature_t_b).abs().max()
print(f"margin:{margin} max_err:{err}")
if __name__ == "__main__":
test_margin(112)
test_margin(128)
test_margin(144)
test_margin(160)
実行結果
margin:112 max_err:0.04556167125701904
margin:128 max_err:3.552436828613281e-05
margin:144 max_err:0.0
margin:160 max_err:0.0
期待通りマージン144ピクセルで特徴マップが一致することを確認しました。