Keras VGG16 微调

8

这里有一个关于VGG16微调的例子,可以在keras博客中找到,但我无法复现。

更准确地说,以下是用于初始化没有顶层的VGG16并冻结除最顶部之外的所有块的代码:

WEIGHTS_PATH_NO_TOP = 'https://github.com/fchollet/deep-learning-models/releases/download/v0.1/vgg16_weights_tf_dim_ordering_tf_kernels_notop.h5'
weights_path = get_file('vgg16_weights.h5', WEIGHTS_PATH_NO_TOP)

model = Sequential()
model.add(InputLayer(input_shape=(150, 150, 3)))
model.add(Conv2D(64, (3, 3), activation='relu', padding='same'))
model.add(Conv2D(64, (3, 3), activation='relu', padding='same'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))

model.add(Conv2D(128, (3, 3), activation='relu', padding='same'))
model.add(Conv2D(128, (3, 3), activation='relu', padding='same'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))

model.add(Conv2D(256, (3, 3), activation='relu', padding='same'))
model.add(Conv2D(256, (3, 3), activation='relu', padding='same'))
model.add(Conv2D(256, (3, 3), activation='relu', padding='same'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))

model.add(Conv2D(512, (3, 3), activation='relu', padding='same'))
model.add(Conv2D(512, (3, 3), activation='relu', padding='same'))
model.add(Conv2D(512, (3, 3), activation='relu', padding='same'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))

model.add(Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv1'))
model.add(Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv2'))
model.add(Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv3'))
model.add(MaxPooling2D((2, 2), strides=(2, 2), name='block5_maxpool'))

model.load_weights(weights_path)

for layer in model.layers:
    layer.trainable = False

for layer in model.layers[-4:]:
    layer.trainable = True
    print("Layer '%s' is trainable" % layer.name)  

下一步,创建一个只有单隐藏层的顶层模型:
top_model = Sequential()
top_model.add(Flatten(input_shape=model.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(1, activation='sigmoid'))
top_model.load_weights('top_model.h5')

请注意,它先前是在瓶颈特征上训练的,就像博客文章中所描述的那样。接下来,将此顶部模型添加到基本模型中并进行编译:
model.add(top_model)
model.compile(loss='binary_crossentropy',
              optimizer=SGD(lr=1e-4, momentum=0.9),
              metrics=['accuracy'])

最终,适合猫/狗数据:

batch_size = 16

train_datagen = ImageDataGenerator(rescale=1./255,
                                   shear_range=0.2,
                                   zoom_range=0.2,
                                   horizontal_flip=True)

test_datagen = ImageDataGenerator(rescale=1./255)

train_gen = train_datagen.flow_from_directory(
    TRAIN_DIR,
    target_size=(150, 150),
    batch_size=batch_size,
    class_mode='binary')

valid_gen = test_datagen.flow_from_directory(
    VALID_DIR,
    target_size=(150, 150),
    batch_size=batch_size,
    class_mode='binary')

model.fit_generator(
    train_gen,
    steps_per_epoch=nb_train_samples // batch_size,
    epochs=nb_epoch,
    validation_data=valid_gen,
    validation_steps=nb_valid_samples // batch_size)

但是,当我试图拟合时,出现了以下错误:

ValueError:检查模型目标时出错:期望block5_maxpool有4个维度,但得到形状为(16,1)的数组

因此,似乎基本模型中的最后一个池化层出现了问题。或者可能是在尝试连接基本模型与顶部模型时做错了什么。

有人遇到过类似的问题吗?或者也许有更好的方法来构建这样的“连接”模型?我正在使用带有theano后端的keras==2.0.0。

注意:我使用了gist和applications.VGG16实用程序的示例,但在尝试连接模型时出现了问题,我对keras函数API不太熟悉。因此,我提供的解决方案是最“成功”的解决方案,即仅在拟合阶段失败。


更新 #1

好的,这里是我尝试做的事情的简要解释。首先,我正在生成来自VGG16的瓶颈特征,步骤如下:

def save_bottleneck_features():
    datagen = ImageDataGenerator(rescale=1./255)
    model = applications.VGG16(include_top=False, weights='imagenet')

    generator = datagen.flow_from_directory(
        TRAIN_DIR,
        target_size=(150, 150),
        batch_size=batch_size,
        class_mode=None,
        shuffle=False)    
    print("Predicting train samples..")
    bottleneck_features_train = model.predict_generator(generator, nb_train_samples)
    np.save(open('bottleneck_features_train.npy', 'w'), bottleneck_features_train)

    generator = datagen.flow_from_directory(
        VALID_DIR,
        target_size=(150, 150),
        batch_size=batch_size,
        class_mode=None,
        shuffle=False)
    print("Predicting valid samples..")
    bottleneck_features_valid = model.predict_generator(generator, nb_valid_samples)
    np.save(open('bottleneck_features_valid.npy', 'w'), bottleneck_features_valid)

然后,我创建一个顶层模型,并按以下方式对其进行训练:
def train_top_model():
    train_data = np.load(open('bottleneck_features_train.npy'))
    train_labels = np.array([0]*(nb_train_samples / 2) + 
                            [1]*(nb_train_samples / 2))
    valid_data = np.load(open('bottleneck_features_valid.npy'))
    valid_labels = np.array([0]*(nb_valid_samples / 2) +
                            [1]*(nb_valid_samples / 2))
    model = Sequential()
    model.add(Flatten(input_shape=train_data.shape[1:]))  
    model.add(Dense(256, activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(1, activation='sigmoid'))
    model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['accuracy'])
    model.fit(train_data, train_labels,
              nb_epoch=nb_epoch,
              batch_size=batch_size,
              validation_data=(valid_data, valid_labels),
              verbose=1)
    model.save_weights('top_model.h5')   

基本上,有两个经过训练的模型,具有ImageNet权重的base_model和由瓶颈特征生成的权重的top_model。我想知道如何将它们连接起来?这可能吗,还是我做错了什么?因为我可以看到,@thomas-pinetz的回复认为顶部模型没有单独训练,而是直接附加到模型中。不确定我是否清楚,以下是博客中的引用:

为了进行微调,所有层都应该从合适的训练权重开始:例如,你不应该在预先训练的卷积基础上随意添加随机初始化的全连接网络。这是因为由随机初始化的权重引发的大梯度更新会破坏卷积基础中学习到的权重。在我们的情况下,这就是为什么我们首先训练顶层分类器,然后才开始微调卷积权重。


这个问题/答案对你有帮助吗?链接 - Martin Thoma
3个回答

9
我认为VGG网络描述的权重不适合你的模型,错误源于此。无论如何,使用网络本身的方法更好,如(https://keras.io/applications/#vgg16)所述。
你可以直接使用:
base_model = keras.applications.vgg16.VGG16(include_top=False, weights='imagenet', input_tensor=None, input_shape=None)

要实例化一个预训练的vgg网络。然后您可以冻结层并使用模型类来实例化自己的模型,如下所示:
x = base_model.output
x = Flatten()(x)
x = Dense(your_classes, activation='softmax')(x) #minor edit
new_model = Model(input=base_model.input, output=x)

为了将底部和顶部网络组合起来,您可以使用以下代码片段。使用以下函数(输入层(https://keras.io/getting-started/functional-api-guide/)/ load_model(https://keras.io/getting-started/faq/#how-can-i-save-a-keras-model)以及 keras 的功能 API)。
final_input = Input(shape=(3, 224, 224))
base_model = vgg...
top_model = load_model(weights_file)

x = base_model(final_input)
result = top_model(x)
final_model = Model(input=final_input, output=result)

你知道吗,使用你的代码片段是否可以处理瓶颈特征?我的意思是,在参考的博客中,VGG16使用预训练权重进行初始化,然后用于生成瓶颈特征。然后,使用自定义类创建并在这些特征上进行训练的顶部模型。最后,这两个模型重新初始化其权重,连接并应用于新数据。我理解你的代码也可以实现相同的功能,对吗?我尝试过类似于你的建议,但遇到了一些问题。 - devforfu
瓶颈特征是我在这个片段中使用的Imagenet特征。这里有一个可以满足你需求的代码示例:https://keras.io/applications/。另外,出现了什么错误? - Thomas Pinetz
我已经添加了一些额外的部分到问题中。不确定我尝试做的事情是否与您的建议不同,或者它是否没有太多意义。简而言之,我正在尝试将两个单独的网络连接成一个顺序模型。在您的片段中,“顶层模型”(以功能方式创建)还没有被训练,对吗? - devforfu
根据您的后续问题修改了答案。 - Thomas Pinetz

3
我认为你可以通过像这样的方式连接两个内容:
#load vgg model
vgg_model = applications.VGG16(weights='imagenet', include_top=False, input_shape=(150, 150, 3))
print('Model loaded.')

#initialise top model
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg_model.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(1, activation='sigmoid'))


top_model.load_weights(top_model_weights_path)

# add the model on top of the convolutional base

model = Model(input= vgg_model.input, output= top_model(vgg_model.output))

这个解决方案是关于微调预训练网络的顶层的示例。完整代码可以在这里找到。

1
不要忘记从keras.models导入Model。 - Gowtham Ramesh

1

好的,我想托马斯和高桑发表了正确(更简洁的)答案,但是我想分享一下代码,我已经成功运行:

def train_finetuned_model(lr=1e-5, verbose=True):
    file_path = get_file('vgg16.h5', VGG16_WEIGHTS_PATH, cache_subdir='models')
    if verbose:
        print('Building VGG16 (no-top) model to generate bottleneck features.')

    vgg16_notop = build_vgg_16()
    vgg16_notop.load_weights(file_path)
    for _ in range(6):
        vgg16_notop.pop()
    vgg16_notop.compile(optimizer=RMSprop(lr=lr), loss='categorical_crossentropy', metrics=['accuracy'])    

    if verbose:
        print('Bottleneck features generation.')

    train_batches = get_batches('train', shuffle=False, class_mode=None, batch_size=BATCH_SIZE)
    train_labels = np.array([0]*1000 + [1]*1000)
    train_bottleneck = vgg16_notop.predict_generator(train_batches, steps=2000 // BATCH_SIZE)
    valid_batches = get_batches('valid', shuffle=False, class_mode=None, batch_size=BATCH_SIZE)
    valid_labels = np.array([0]*400 + [1]*400)
    valid_bottleneck = vgg16_notop.predict_generator(valid_batches, steps=800 // BATCH_SIZE)

    if verbose:
        print('Training top model on bottleneck features.')

    top_model = Sequential()
    top_model.add(Flatten(input_shape=train_bottleneck.shape[1:]))
    top_model.add(Dense(4096, activation='relu'))
    top_model.add(Dropout(0.5))
    top_model.add(Dense(4096, activation='relu'))
    top_model.add(Dropout(0.5))
    top_model.add(Dense(2, activation='softmax'))
    top_model.compile(optimizer=RMSprop(lr=lr), loss='categorical_crossentropy', metrics=['accuracy'])
    top_model.fit(train_bottleneck, to_categorical(train_labels),
                  batch_size=32, epochs=10,
                  validation_data=(valid_bottleneck, to_categorical(valid_labels)))

    if verbose:
        print('Concatenate new VGG16 (without top layer) with pretrained top model.')

    vgg16_fine = build_vgg_16()
    vgg16_fine.load_weights(file_path)
    for _ in range(6):
        vgg16_fine.pop()
    vgg16_fine.add(Flatten(name='top_flatten'))    
    vgg16_fine.add(Dense(4096, activation='relu'))
    vgg16_fine.add(Dropout(0.5))
    vgg16_fine.add(Dense(4096, activation='relu'))
    vgg16_fine.add(Dropout(0.5))
    vgg16_fine.add(Dense(2, activation='softmax'))
    vgg16_fine.compile(optimizer=RMSprop(lr=lr), loss='categorical_crossentropy', metrics=['accuracy'])

    if verbose:
        print('Loading pre-trained weights into concatenated model')

    for i, layer in enumerate(reversed(top_model.layers), 1):
        pretrained_weights = layer.get_weights()
        vgg16_fine.layers[-i].set_weights(pretrained_weights)

    for layer in vgg16_fine.layers[:26]:
        layer.trainable = False

    if verbose:
        print('Layers training status:')
        for layer in vgg16_fine.layers:
            print('[%6s] %s' % ('' if layer.trainable else 'FROZEN', layer.name))        

    vgg16_fine.compile(optimizer=RMSprop(lr=1e-6), loss='binary_crossentropy', metrics=['accuracy'])

    if verbose:
        print('Train concatenated model on dogs/cats dataset sample.')

    train_datagen = ImageDataGenerator(rescale=1./255,
                                       shear_range=0.2,
                                       zoom_range=0.2,
                                       horizontal_flip=True)
    test_datagen = ImageDataGenerator(rescale=1./255)
    train_batches = get_batches('train', gen=train_datagen, class_mode='categorical', batch_size=BATCH_SIZE)
    valid_batches = get_batches('valid', gen=test_datagen, class_mode='categorical', batch_size=BATCH_SIZE)
    vgg16_fine.fit_generator(train_batches, epochs=100,
                             steps_per_epoch=2000 // BATCH_SIZE,
                             validation_data=valid_batches,
                             validation_steps=800 // BATCH_SIZE)
    return vgg16_fine 

这种方式有点啰嗦,需要手动完成所有事情(即从预训练层复制权重到连接模型),但它大体上是可行的。

虽然我发布的这段代码存在低准确率的问题(约为70%),但那是另外一个故事。


你是如何决定微调时解冻多少层的?我对VGG16和迁移学习有这个问题。 - skeller88
1
@skeller88 我认为这取决于您的数据集与原始模型所训练的数据集有多大的不同。如果领域相似(如本例中的猫/狗照片),您可以微调最上层的全连接层,以及最后几个卷积层。然而,如果您的领域与ImageNet图像非常不同(即X射线图像、显微镜图像、材料等),则需要进行更多的微调。可能需要逐渐解冻所有层,并使用适当的学习率重新训练它们。 - devforfu

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