扩展算子的实现可以在Caffe、TensorFlow、MindSpore深度学习框架中实现,扩展算子中的第一种是基于Caffe框架进行扩展实现的,如Normalize和Upsample层等,因此有公开的prototxt标准定义。而扩展算子中的第二种并不是在Caffe框架下实现的,对于这类网络中的自定义算子,也需要给出prototxt的标准定义,用于在prototxt中定义网络中相应的算子。
在指定的轴上反序,如输入为[1,2,3],其反序为[3,2,1]。
算子定义如下:
message LayerParameter {
...
optional ReverseParameter reverse_param = 157;
...
}message ReverseParameter{
repeated int32 axis = 1;
}Upsample层为Pooling层的逆操作,其中每个Upsample层均与网络之前一个对应大小输入、输出Pooling层一一对应,完成feature map在spatial维度上的扩充。
Upsample层需要对caffe.proto文件进行扩展,定义UpsampleParameter,扩展方式示例如下所示。 定义的参数包括stride,即输出与输入的尺寸比例,如2。
message LayerParameter {
...
optional UpsampleParameter upsample_param = 160;
...
}message UpsampleParameter{
optional float scale = 1[default = 1];
optional int32 stride = 2[default = 2];
optional int32 stride_h = 3[default = 2];
optional int32 stride_w = 4[default=2];
}基于上述原则,Upsample在prototxt中定义的代码样例如下:
layer {
name: "layer86-upsample"
type: "Upsample"
bottom: "some_input"
top: "some_output"
upsample_param {
scale: 1
stride: 2
}
}
Normalize层为SSD网络中的一个归一化层,主要作用是将空间或者通道内的元素归一化到0到1之间,其进行的操作为对于一个c*h*w的三维tensor,输出是同样大小的tensor,其中间计算为每个元素以channel方向的平方和的平方根求 normalize,其具体计算公式为:

其中分母位置的平方和的累加向量为同一h与w位置的所有c方向的向量,如图1中的橙色区域。
经过上述计算归一化后,再对每个feature map做scale,每个channel对应一个scale值。
Normalize层需要对caffe.proto文件进行扩展,定义NormalizeParameter,扩展方式如下所示。定义的参数包括:
normalize的计算公式转换为:

算子定义如下:
message LayerParameter {
...
optional NormalizeParameter norm_param = 206;
...
}message NormalizeParameter {
optional bool across_spatial = 1 [default = true];
// Initial value of scale. Default is 1.0 for all
optional FillerParameter scale_filler = 2;
// Whether or not scale parameters are shared across channels.
optional bool channel_shared = 3 [default = true];
// Epsilon for not dividing by zero while normalizing variance
optional float eps = 4 [default = 1e-10];
}基于上述原则,Normalize在prototxt中定义的代码样例如下:
layer {
name: "normalize_layer"
type: "Normalize"
bottom: ""some_input"
top: "some_output"
norm_param {
across_spatial: false
scale_filler {
type: "constant"
value: 20
}
channel_shared: false
}
}
Reorg算子在昇腾AI处理器内部以PassThrough算子呈现,将通道数据转移到平面上,或反过来操作。
PassThrough层为Yolo v2中的一个自定义层,由于Yolo v2并不是使用Caffe框架实 现,因此对于该层没有标准的定义。该层实现的功能为将feature map在spatial维度上的数据展开到channel维度上,原始在channel维度上连续的元素在展开后的feature map中依然是连续的。
算子定义如下:
message LayerParameter {
...
optional ReorgParameter reorg_param = 155;
...
}message ReorgParameter{
optional uint32 stride = 2 [default = 2];
optional bool reverse = 1 [default = false];
}基于上述原则,Reorg在prototxt中定义的代码样例如下:
layer {
bottom: "some_input"
top: "some_output"
name: "reorg"
type: "Reorg"
reorg_param {
stride: 2
}
}
ROIAlign 是在Mask-RCNN论文里提出的一种区域特征聚集方式, 与ROIPooling算法进行改进:用双线性插值替换ROIPooling操作中两次量化,以解决ROIPooling造成的区域不匹配的问题,提高检测准确性。
ROIAlign算子是从feature map中获取ROI(range of interest),分为pooled_w x pooled_h 个单元格,每个单元格均分为sampling_ratio*sampling_ratio个小方格,每个小方格的中心点就是采样点。如图2所示,虚线部分表示feature map,实线表示ROI,这里将ROI切分成2x2的单元格。如果采样点数是4,则首先将每个单元格子均分成四个小方格(如红色线所示),每个小方格中心就是采样点。由于采样点的坐标通常是浮点数,所以需要对采样点像素进行双线性插值(如图2中的四个箭头所示),就可以得到该像素点的值了。然后对每个单元格内的四个采样点取均值,就可以得到最终的ROIAlign的结果。
算子定义如下:
message LayerParameter {
...
optional ROIAlignParameter roi_align_param = 154;
...
}message ROIAlignParameter {
// Pad, kernel size, and stride are all given as a single value for equal
// dimensions in height and width or as Y, X pairs.
optional uint32 pooled_h = 1 [default = 0]; // The pooled output height
optional uint32 pooled_w = 2 [default = 0]; // The pooled output width
// Multiplicative spatial scale factor to translate ROI coords from their
// input scale to the scale used when pooling
optional float spatial_scale = 3 [default = 1];
optional int32 sampling_ratio = 4 [default = -1];
optional int32 roi_end_mode = 5 [default = 0];
}
根据上述类型以及属性用户可以自定义prototxt。
ShuffleChannel是把channel维度分为[group, channel/Group],然后再在channel维度中进行数据转置[channel/Group,group]。
例如对于channel=4,group=2,则执行此算子后,就是把channel[1]、channel[2]的数据交换位置。
算子定义如下:
message LayerParameter {
...
optional ShuffleChannelParameter shuffle_channel_param = 159;
...
}message ShuffleChannelParameter{
optional uint32 group = 1[default = 1]; // The number of group
}基于上述原则,ShuffleChannel在prototxt中定义的代码样例如下:
layer {
name: "layer_shuffle"
type: "ShuffleChannel"
bottom: "some_input"
top: "some_output"
shuffle_channel_param {
group: 3
}
}
YOLO算子出现在YOLO V2网络,且目前仅在YOLO V2、V3网络中使用,对数据做sigmoid和softmax操作。
输入数据格式为Tensor(n, coords+backgroup+classes,l.h,l.w),其中n是anchor box的数量,coords表示x,y,w,h。
算子定义如下:
message LayerParameter {
...
optional YoloParameter yolo_param = 199;
...
}message YoloParameter {
optional int32 boxes = 1 [default = 3];
optional int32 coords = 2 [default = 4];
optional int32 classes = 3 [default = 80];
optional string yolo_version = 4 [default = "V3"];
optional bool softmax = 5 [default = false];
optional bool background = 6 [default = false];
optional bool softmaxtree = 7 [default = false];
}基于上述原则,Yolo在prototxt中定义的代码样例如下:
layer {
bottom: "layer82-conv"
top: "yolo1_coords"
top: "yolo1_obj"
top: "yolo1_classes"
name: "yolo1"
type: "Yolo"
yolo_param {
boxes: 3
coords: 4
classes: 80
yolo_version: "V3"
softmax: true
background: false
}
}
根据输入的参数,生成prior box。
下面以conv7_2_mbox_priorbox为例,根据对应的参数生成prior box的数量。定义如下:
layer{
name:"conv7_2_mbox_priorbox"
type:"PriorBox"
bottom:"conv7_2"
bottom:"data"
top:"conv7_2_mbox_priorbox"
prior_box_param{
min_size:162.0
max_size:213.0
aspect_ratio:2
aspect_ratio:3
flip:true
clip:false
variance:0.1
variance:0.1
variance:0.2
variance:0.2
img_size:300
step:64
offset:0.5
}
}
因此,num_priors(prior box的数量)= min_size的数量+aspect_ratio的数量(这里为4)*min_size的数量(这里为1)+max_size的数量 (max_size的数量和min_size 的数量一一对应)。
算子定义如下:
message LayerParameter {
...
optional PriorBoxParameter prior_box_param = 203;
...
}message PriorBoxParameter {
// Encode/decode type.
enum CodeType {
CORNER = 1;
CENTER_SIZE = 2;
CORNER_SIZE = 3;
}
// Minimum box size (in pixels). Required!
repeated float min_size = 1;
// Maximum box size (in pixels). Required!
repeated float max_size = 2;
// Various of aspect ratios. Duplicate ratios will be ignored.
// If none is provided, we use default ratio 1.
repeated float aspect_ratio = 3;
// If true, will flip each aspect ratio.
// For example, if there is aspect ratio "r",
// we will generate aspect ratio "1.0/r" as well.
optional bool flip = 4 [default = true];
// If true, will clip the prior so that it is within [0, 1]
optional bool clip = 5 [default = false];
// Variance for adjusting the prior bboxes.
repeated float variance = 6;
// By default, we calculate img_height, img_width, step_x, step_y based on
// bottom[0] (feat) and bottom[1] (img). Unless these values are explicitely
// provided.
// Explicitly provide the img_size.
optional uint32 img_size = 7;
// Either img_size or img_h/img_w should be specified; not both.
optional uint32 img_h = 8;
optional uint32 img_w = 9;
// Explicitly provide the step size.
optional float step = 10;
// Either step or step_h/step_w should be specified; not both.
optional float step_h = 11;
optional float step_w = 12;
// Offset to the top left corner of each cell.
optional float offset = 13 [default = 0.5];
}基于上述原则,PriorBox在prototxt中定义的代码样例如下:
layer {
name: "layer_priorbox"
type: "PriorBox"
bottom: "some_input"
bottom: "some_input"
top: "some_output"
prior_box_param {
min_size: 30.0
max_size: 60.0
aspect_ratio: 2
flip: true
clip: false
variance: 0.1
variance: 0.1
variance: 0.2
variance: 0.2
step: 8
offset: 0.5
}
}
该算子计算过程其实做了一个仿射变换:仿射变换的参数,可以是在prototxt固定的,多个batch使用一份;也可以是层的第二个输入,每个batch使用不一样的参数。
计算步骤:

计算代码如下:
Dtype* data = output_grid.mutable_cpu_data();
for(int i=0; i< output_H_ * output_W_; ++i) {
data[3 * i] = (i / output_W_) * 1.0 / output_H_ * 2 - 1;
data[3 * i + 1] = (i % output_W_) * 1.0 / output_W_ * 2 - 1;
data[3 * i + 2] = 1;
}计算代码如下:
caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasTrans, output_H_ * output_W_, 2, 3, (Dtype)1.,
output_grid_data, full_theta_data + 6 * i, (Dtype)0., coordinates);Dtype x = (px + 1) / 2 * H;
Dtype y = (py + 1) / 2 * W;
if(debug) std::cout<<prefix<<"(x, y) = ("<<x<<", "<<y<<")"<<std::endl;
for(int m = floor(x); m <= ceil(x); ++m)
for(int n = floor(y); n <= ceil(y); ++n) {
if(debug) std::cout<<prefix<<"(m, n) = ("<<m<<", "<<n<<")"<<std::endl;
if(m >= 0 && m < H && n >= 0 && n < W) {
res += (1 - abs(x - m)) * (1 - abs(y - n) * pic[m * W + n]);
if(debug) std::cout<<prefix<<" pic[m * W + n]= "<<std::endl;
}
}
算子定义如下:
message LayerParameter {
...
optional SpatialTransformParameter spatial_transform_param = 153;
...
}message SpatialTransformParameter {
optional uint32 output_h = 1 [default = 0];
optional uint32 output_w = 2 [default = 0];
optional float border_value = 3 [default = 0];
repeated float affine_transform = 4;
enum Engine {
DEFAULT = 0;
CAFFE = 1;
CUDNN = 2;
}
optional Engine engine = 15 [default = DEFAULT];
}基于上述原则,SpatialTransform层在prototxt中定义的代码样例如下:
layer {
name: "st_1"
type: "SpatialTransformer"
bottom: "data"
bottom: "theta"
top: "transformed"
st_param {
to_compute_dU: false
theta_1_1: -0.129
theta_1_2: 0.626
theta_2_1: 0.344
theta_2_2: 0.157
}
}