我使用了Resnet50进行迁移学习。我基于Keras提供的预训练模型(’imagenet’)创建了一个新模型。
训练我的新模型后,我按以下方式保存它:
# 保存Siamese网络结构siamese_model_json = siamese_network.to_json()with open("saved_model/siamese_network_arch.json", "w") as json_file: json_file.write(siamese_model_json)# 保存Siamese网络模型权重siamese_network.save_weights('saved_model/siamese_model_weights.h5')
之后,我按以下方式重新加载模型以进行一些预测:
json_file = open('saved_model/siamese_network_arch.json', 'r')loaded_model_json = json_file.read()json_file.close()siamese_network = model_from_json(loaded_model_json)# 将权重加载到新模型中siamese_network.load_weights('saved_model/siamese_model_weights.h5')
然后,我检查权重是否合理(来自其中一层):
print("bn3d_branch2c:\n", siamese_network.get_layer('model_1').get_layer('bn3d_branch2c').get_weights())
如果我只训练我的网络1个epoch,我会看到合理的值…
但是,如果我训练我的模型18个epoch(由于我的电脑非常慢,这需要5-6个小时),我只会看到NaN值如下:
bn3d_branch2c: [array([nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, ...
这里有什么技巧吗?
补充1:
这是我创建模型的方式。
在这里,我有一个triplet_loss函数,稍后我会用到它。
def triplet_loss(inputs, dist='euclidean', margin='maxplus'): anchor, positive, negative = inputs positive_distance = K.square(anchor - positive) negative_distance = K.square(anchor - negative) if dist == 'euclidean': positive_distance = K.sqrt(K.sum(positive_distance, axis=-1, keepdims=True)) negative_distance = K.sqrt(K.sum(negative_distance, axis=-1, keepdims=True)) elif dist == 'sqeuclidean': positive_distance = K.sum(positive_distance, axis=-1, keepdims=True) negative_distance = K.sum(negative_distance, axis=-1, keepdims=True) loss = positive_distance - negative_distance if margin == 'maxplus': loss = K.maximum(0.0, 2 + loss) elif margin == 'softplus': loss = K.log(1 + K.exp(loss)) returned_loss = K.mean(loss) return returned_loss
以下是我从头到尾构建模型的方式。我提供完整的代码以给出准确的图景。
model = ResNet50(weights='imagenet')# 移除最后一层(为了稍后能够创建Siamese网络模型)model.layers.pop()# 首先冻结ResNet50的所有层。将应用迁移学习。for layer in model.layers: layer.trainable = False# 所有批量归一化层仍然需要可训练,以便使用新的训练数据更新“均值”和“标准差(std)”参数model.get_layer('bn_conv1').trainable = Truemodel.get_layer('bn2a_branch2a').trainable = Truemodel.get_layer('bn2a_branch2b').trainable = Truemodel.get_layer('bn2a_branch2c').trainable = Truemodel.get_layer('bn2a_branch1').trainable = Truemodel.get_layer('bn2b_branch2a').trainable = Truemodel.get_layer('bn2b_branch2b').trainable = Truemodel.get_layer('bn2b_branch2c').trainable = Truemodel.get_layer('bn2c_branch2a').trainable = Truemodel.get_layer('bn2c_branch2b').trainable = Truemodel.get_layer('bn2c_branch2c').trainable = Truemodel.get_layer('bn3a_branch2a').trainable = Truemodel.get_layer('bn3a_branch2b').trainable = Truemodel.get_layer('bn3a_branch2c').trainable = Truemodel.get_layer('bn3a_branch1').trainable = Truemodel.get_layer('bn3b_branch2a').trainable = Truemodel.get_layer('bn3b_branch2b').trainable = Truemodel.get_layer('bn3b_branch2c').trainable = Truemodel.get_layer('bn3c_branch2a').trainable = Truemodel.get_layer('bn3c_branch2b').trainable = Truemodel.get_layer('bn3c_branch2c').trainable = Truemodel.get_layer('bn3d_branch2a').trainable = Truemodel.get_layer('bn3d_branch2b').trainable = Truemodel.get_layer('bn3d_branch2c').trainable = Truemodel.get_layer('bn4a_branch2a').trainable = Truemodel.get_layer('bn4a_branch2b').trainable = Truemodel.get_layer('bn4a_branch2c').trainable = Truemodel.get_layer('bn4a_branch1').trainable = Truemodel.get_layer('bn4b_branch2a').trainable = Truemodel.get_layer('bn4b_branch2b').trainable = Truemodel.get_layer('bn4b_branch2c').trainable = Truemodel.get_layer('bn4c_branch2a').trainable = Truemodel.get_layer('bn4c_branch2b').trainable = Truemodel.get_layer('bn4c_branch2c').trainable = Truemodel.get_layer('bn4d_branch2a').trainable = Truemodel.get_layer('bn4d_branch2b').trainable = Truemodel.get_layer('bn4d_branch2c').trainable = Truemodel.get_layer('bn4e_branch2a').trainable = Truemodel.get_layer('bn4e_branch2b').trainable = Truemodel.get_layer('bn4e_branch2c').trainable = Truemodel.get_layer('bn4f_branch2a').trainable = Truemodel.get_layer('bn4f_branch2b').trainable = Truemodel.get_layer('bn4f_branch2c').trainable = Truemodel.get_layer('bn5a_branch2a').trainable = Truemodel.get_layer('bn5a_branch2b').trainable = Truemodel.get_layer('bn5a_branch2c').trainable = Truemodel.get_layer('bn5a_branch1').trainable = Truemodel.get_layer('bn5b_branch2a').trainable = Truemodel.get_layer('bn5b_branch2b').trainable = Truemodel.get_layer('bn5b_branch2c').trainable = Truemodel.get_layer('bn5c_branch2a').trainable = Truemodel.get_layer('bn5c_branch2b').trainable = Truemodel.get_layer('bn5c_branch2c').trainable = True# 编译Siamese网络时使用def identity_loss(y_true, y_pred): return K.mean(y_pred - 0 * y_true) # 创建Siamese网络x = model.get_layer('flatten_1').output # 层'flatten_1'是模型的最后一层model_out = Dense(128, activation='relu', name='model_out')(x)model_out = Lambda(lambda x: K.l2_normalize(x,axis=-1))(model_out)new_model = Model(inputs=model.input, outputs=model_out)anchor_input = Input(shape=(224, 224, 3), name='anchor_input')pos_input = Input(shape=(224, 224, 3), name='pos_input')neg_input = Input(shape=(224, 224, 3), name='neg_input')encoding_anchor = new_model(anchor_input)encoding_pos = new_model(pos_input)encoding_neg = new_model(neg_input)loss = Lambda(triplet_loss)([encoding_anchor, encoding_pos, encoding_neg])siamese_network = Model(inputs = [anchor_input, pos_input, neg_input], outputs = loss) # 注意,模型的输出是上面triplet_loss函数的返回值siamese_network.compile(optimizer=Adam(lr=.0001), loss=identity_loss)
需要注意的是,我使所有批量归一化层“可训练”,以便BN相关的参数可以使用我的训练数据进行更新。这产生了很多行,但我找不到更简短的解决方案。
回答:
解决方案的灵感来自于@Gurmeet Singh的推荐。
显然,在训练过程中一段时间后,可训练层的权重变得非常大,所有这些权重都被设置为NaN,这让我以为我保存和重新加载模型的方式有问题,但问题实际上是梯度爆炸。
我在github讨论中也看到了类似的问题,可以在这里查看:github.com/keras-team/keras/issues/2378 在github该线程的底部,建议使用较低的学习率来避免这个问题。
在这个链接中(Keras ML库:如何在梯度更新后进行权重裁剪?TensorFlow后端),讨论了两种解决方案:- 使用优化器中的clipvalue参数,它简单地按配置切割计算出的梯度值。但这不是推荐的解决方案。(在另一个线程中解释。)- 第二种方法是使用clipnorm参数,当计算出的梯度值的L2范数超过用户设定的值时,它会简单地裁剪这些值。
我也考虑过使用输入归一化(以避免梯度爆炸),但后来发现这已经在preprocess_input(..)函数中完成了。(详细信息请查看此链接:https://www.tensorflow.org/api_docs/python/tf/keras/applications/resnet50/preprocess_input)虽然可以将mode参数设置为“tf”(否则默认设置为“caffe”),这可能会进一步帮助(因为mode=”tf”设置将像素缩放到-1到1之间),但我没有尝试过。
总结一下,我在编译将要训练的模型时更改了两个东西:
更改的行如下:
更改前:
siamese_network.compile(optimizer=Adam(**lr=.0001**), loss=identity_loss)
更改后:
siamese_network.compile(optimizer=Adam(**lr=.00004**, **clipnorm=1.**), loss=identity_loss)
1) 使用较小的学习率,使梯度更新稍微小一些2) 使用clipnorm参数来归一化计算出的梯度并进行裁剪。
然后我再次训练了我的网络10个epoch。损失按预期降低,但现在速度更慢了。并且在保存和存储我的模型时我没有遇到任何问题。(至少在10个epoch之后(在我的电脑上需要时间)。)
请注意,我将clipnorm的值设置为1。这意味着首先计算梯度的L2范数,如果计算出的归一化梯度超过“1”的值,则裁剪梯度。我认为这是一个可以优化的超参数,它影响训练模型所需的时间,同时有助于避免梯度爆炸问题。