Layer Shape Propagation
Table of Contents
1. Layer Shape Propagation
1.1. Problem
假设模型输入是图片, 修改 input 的 H,W (不修改 C) 会如何影响后续 layer 的输出尺寸和算力?
模型中的 layer 大约有以下三种情形:
- layer 支持可变的 input shape
- layer 的 input * N 导致 output * N
- input * N 但是 output 不变
- layer 不支持 input shape 改变
1.2. Layer Shape
1.2.1. Convolution/Pooling/Deconvolution
Convolution 和 Pooling 基本属于 case 1.1, 但有一个例外: padding
同样的 padding 搭配不同的 input shape 有时会因为 (input+padding-filter) 无法整除 stride 而出错, 例如:
input=10, padding=1, stride=3, filter=3 时, ouput=(10+2-3)/3+1=4
当 input 变为 11 时, (11+2-3)/3 无法整除, 导致出错.
ps. 某些 framework 如 tensorflow, pytorch 在定义模型时可以通过 `same` 或 `valid` padding 自动计算 padding 值, 但是最终导出的模型里 padding 会变成计算后的结果.
1.2.2. Spatial Pyramid Pooling
case 1.2, 因为它是根据 input shape 计算它的 stride, 以保证 output shape 是确定的. 所以 SPP 会导致 input shape 的变化无法向后传递.
1.2.3. Crop
case 1.2, 它的 output shape 总是由 crop 的参数决定的
1.2.4. LSTM
LSTM Layer 的 input 为 [B, L, C], 输出为 [B, L, H], C 转换为 H 是由多个全连接完成的, 所以它属于 case 2
1.2.5. Inner Product
属于 case 2, 因为它的 input/output shape 是固定的.
即使修改了它的 input shape 使它能与前一层的输出匹配, 它对后续的层也有会类似于 1.2 的效果, 因为的它输出尺寸是固定的.
1.2.6. Batch Normalization
Batch Normalization 都属于 case 1.1, 虽然有的实现 (例如 tensorflow, pytorch) 会包含额外的 alpha, beta 参数, 用来做 scale 和 bias, 但这两个参数的 shape 与 channel 一致, 并不会变化.
1.2.7. Reshape
Reshape 如果 reshape_param 包含 -1, 则属于 case 1.1, 否则属于 case 2
1.2.8. Slice
slice 会把 input 按下标分割为几份, 例如总数为 50, 下标为 10, 20, 30, 40, 就会分割成 5 份, 每份 10 个. 如果 slice 的 axis 是 feature map, 则 slice 可以认为属于 case 2, 否则属于 case 1.1
1.2.9. Eltwise/Softmax/Split/Concat/Tile/Flatten/Activations
case 1.1
1.2.10. Reduction/Argmax
若 axis 为 channel, 则属于 case 1.1, 否则属于 case 1.2
1.2.11. Other
以上列举的是 caffe 的常用算子, 除些以外还有一些自定义或其它框架包含的算子:
ROIFilter/ROIAlign
case 1.2
GolbalAveragePooling
和 SPP 类似, case 1.2
NMS
case 1.2
另外, 有些框架例如 pytorch 在定义模型时看起来像是 case 1.1, 但因为它导出成 torchscript/onnx 时一般通过 trace 方式, 所以导出的模型会变成 case 2, 例如:
for slice in input: output[i] = conv2d(slice)
看起来 input 是支持任意长度, 但通过 trace 导出时会变成这样:
# 假设导出时提供的 input 长度为 3 slice_1 = input[0] slice_2 = input[1] slice_3 = input[2] output_1 = conv2d(slice_1) output_2 = conv2d(slice_2) output_3 = conv2d(slice_3) # ...
1.3. Layer Flops
计算 flops 时一般只考虑 conv/deconv/fc, 其中贡献算力最多的是 conv/deconv, 而 conv/deconv 的 flops 与 H*W 成正比, 如果能保证中间各个 layer 都属于 case 1.1 (以确保 conv 的输入是 case 1.1), 则可以估计最终算力也属于 case 1.1
1.4. Case Study
以 apollo 的 dark_scnn 为例, 这个模型的输入是 640x480, 把它修改成 2560x1920 (H,W 各乘 4) 后, 模型会因为尺寸不匹配而报错, 因为 scnn 模型中包含一个 message_passing 层, 它做的是事情是:
1. 按 H 方向切成 H 个 (C,1,W) 的 slice 2. out[0]=slice[0] 3. out[1]=slice[1]+conv(out[0]) 4. out[2]=slice[2]+conv(out[1]) ...
当输入是 640x480 时, 会分成 60 个 slice, 但是当输入变为 16 倍时, 这里需要分成 240 个 slice 才可以. 修改模型加入 240 个 slice 后, 可以计算出算力是之前的 16 倍, 因为这个模型并不存在 case 1.2 的 layer
1.5. Conclusion
理论上, H*W 增加 N 倍后, 如果只是为了模型中各层的尺寸能匹配而修改模型, 那么最终算力变化多少没有什么估算的依据, 因为有 case 1.2 的存在.
但是在实际应用中, case 1.2 的 layer 有的只对 c 进行操作, 或者与模型的输入尺寸有内在联系, 例如
- faster-rcnn 中的 ROIFilter 层 (类似于 SPP) 用来把 region proposal 通过 pooling 转换成固定大小的 region, 例如 32x32. 当输入图片变大的, region 其实也应该变大, 因为 feature map 的分辨率实际上提高了
- faster-rcnn 的 rpn 生成 region proposal 后会先通过 NMS 和排序的方法只保留 top-N 个 region proposal, 当输入尺寸变大, 保留更多的 region proposal 可能也是有意义的?
另外, 对于 fc, max 这类的 case 1.2 的算子, 可能看不出来它们与模型的输入尺寸的内在联系, 无法估计它们的变化后的`合理`的输出尺寸.
综上, 大约给出一个粗略的结论: 输入面积乘 N 时算力大约也需要乘 N