Caffe转Keras:分组卷积的转换

4
我正在尝试从一个非常简单的Caffe模型中获取权重并将其解释为完全功能的Keras模型。
这是在Caffe中的原始模型定义,我们称之为“simple.prototxt”:
input: "im_data"
input_shape {
  dim: 1
  dim: 3
  dim: 1280
  dim: 1280
}
layer {
  name: "conv1"
  type: "Convolution"
  bottom: "im_data"
  top: "conv1"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 96
    kernel_size: 11
    pad: 5
    stride: 4
  }
}
layer {
  name: "relu1"
  type: "ReLU"
  bottom: "conv1"
  top: "conv1"
}
layer {
  name: "pool1"
  type: "Pooling"
  bottom: "conv1"
  top: "pool1"
  pooling_param {
    pool: MAX
    kernel_size: 3
    pad: 0
    stride: 2
  }
}
layer {
  name: "norm1"
  type: "LRN"
  bottom: "pool1"
  top: "norm1"
  lrn_param {
    local_size: 5
    alpha: 0.0001
    beta: 0.75
  }
}
layer {
  name: "conv2"
  type: "Convolution"
  bottom: "norm1"
  top: "conv2"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  convolution_param {
    num_output: 256
    kernel_size: 5
    pad: 2
    group: 2
  }
}
layer {
  name: "relu2"
  type: "ReLU"
  bottom: "conv2"
  top: "conv2"
}

Caffe中的层定义可能看起来很复杂,但它只是将一个尺寸为1280x1280x3的图像传递给卷积层,然后进行最大池化并将其传递到最终的卷积层。
以下是其在Keras中的实现,它更加简单:
from keras.models import Model
from keras.layers import Input, BatchNormalization, 
from keras.activations import relu, softmax

im_data = Input(shape=(1280, 1280, 3),
                   dtype='float32',
                   name='im_data')
conv1 = Conv2D(filters=96,
               kernel_size=11,
               strides=(4, 4),
               activation=relu,
               padding='same',
               name='conv1')(im_data)

pooling1 = MaxPooling2D(pool_size=(3, 3),
                        strides=(2, 2),
                        padding='same',
                        name='pooling1')(conv1)
normalized1 = BatchNormalization()(pooling1)  # https://stats.stackexchange.com/questions/145768/importance-of-local-response-normalization-in-cnn

conv2 = Conv2D(filters=256,
               kernel_size=5,
               activation=relu,
               padding='same',
               name='conv2')(normalized1)
model = Model(inputs=[im_data], outputs=conv2)  

问题:
尽管两个模型在每一层的参数似乎相似,但问题在于它们的权重形状不相等。我知道Caffe和Keras有不同的形状顺序,但这里并非关注点。
问题在于Keras的最后一个卷积层在第三维上的值与Caffe中的最后一个卷积层不同。请参见下面。
Caffe的权重形状:
>>> net = caffe.net('simple.prototxt', 'premade_weights.caffemodel', caffe.TEST)
>>> for i in range(len(net.layers)):
...     if len(net.layers[i].blobs) != 0:  # if layer has no weights
...         print(("name", net._layer_names[i]))
...         print("weight_shapes", [v.data.shape for v in net.layers[i].blobs])
('name', 'conv1')
('weight_shapes', [(96, 3, 11, 11), (96,)])
('name', 'conv2')
('weight_shapes', [(256, 48, 5, 5), (256,)])

Keras的权重形状:
>>> for layer in model.layers:
...     if len(layer.get_weights()) != 0:
...         print(("name", layer.name))
...         print(("weight_shapes", [w.shape for w in layer.get_weights()]))  
('name', 'conv1')
('weight_shapes', [(11, 11, 3, 96), (96,)])
('name', 'conv2')
('weight_shapes', [(5, 5, 96, 256), (256,)])

这似乎是奇怪的行为。如您所见,Caffe和Keras中conv1的形状相同(忽略顺序)。但在Caffe中,conv2的形状为[(256, 48, 5, 5), (256,)]),而在Keras中'conv2'的形状为[(5, 5, 96, 256), (256,)],请注意,48*2=96
此外,请注意,conv2层直接在最大池化层之后,因此Keras中的最大池化层可能存在问题。
问题:
我是否正确地将模型定义从Caffe解释为Keras?特别是最大池化层及其参数?
非常感谢!
1个回答

6
请注意你的conv2定义中的group: 2字段。这意味着你有一个分组卷积(Caffe: What does the group param mean?)。从技术上讲,这意味着你有两个滤波器,每个的形状为(128, 48, 5, 5)。第一个将与前48个通道进行卷积,并产生前128个输出,第二个则是用于剩余的通道。然而,Caffe将这两个权重存储在一个单一的blob中,这就是为什么它的形状是(128x2, 48, 5, 5)
在Keras Conv2D层中没有这样的参数,但被广泛采用的解决方法是使用Lambda层拆分输入特征图,用两个不同的卷积层处理它们,然后合并回单个特征图。
from keras.layers import Concatenate

normalized1_1 = Lambda(lambda x: x[:, :, :, :48])(normalized1)
normalized1_2 = Lambda(lambda x: x[:, :, :, 48:])(normalized1)

conv2_1 = Conv2D(filters=128,
                 kernel_size=5,
                 activation=relu,
                 padding='same',
                 name='conv2_1')(normalized1_1)

conv2_2 = Conv2D(filters=128,
                 kernel_size=5,
                 activation=relu,
                 padding='same',
                 name='conv2_2')(normalized1_2)

conv2 = Concatenate(name='conv_2_merge')([conv2_1, conv2_2])

我没有检查代码的正确性,但是这个想法一定是这样的。
关于你的任务:从Caffe转换网络到Keras可能会很棘手。为了获得完全相同的结果,你必须遇到许多微妙的问题,例如卷积中的不对称填充不同的最大池化行为。这就是为什么如果你从Caffe导入权重,你可能不能用batchnorm替换LRN层。幸运的是,Keras中有LRN的实现,例如这里

是的,昨天我后来发现了这一点。从Caffe官方卷积文档中得知,一旦将group参数改回“1”,一切都正常了。但我一直在寻找确切的实现方法,最终在这里找到了。我也关注填充和局部响应归一化,之前我使用了ZeroPadding2D,但不对称填充似乎有所不同。我注意到Caffe的“padding”不影响输出形状,你知道为什么吗?非常感谢您的帮助!我很感激! - ShellRox
1
Caffe卷积层中的“pad”参数必须影响输出形状,您是什么意思?在某些条件下(stride>1,padding='same'),TensorFlow中可能会发生不对称填充,为了避免这种情况,您可以使用tf.pad + Conv2D的组合,并使用填充“valid”。请参见https://programtalk.com/vs2/python/12827/models/slim/nets/resnet_utils.py中的conv2d_same函数示例。 - Dmytro Prylipko
这是另一种奇怪的行为吗?例如,让我们以第一层conv1pad: 5参数为例,并将其设置为pad: 10,则该层的形状仍然保持不变(96, 3, 11, 11)。这是正常的行为吗?如果不是,我想这是另一个答案的问题。 - ShellRox
抱歉,我有点困惑,我读错了权重形状,而不是实际的输出形状。填充确实会像通常应该的那样影响输出。我想我会使用 tf.pad"symmetric" 选项创建自己的层。谢谢! - ShellRox

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接