Siamese神经网络训练时contrastive_loss返回NaN的问题求助
问题:Siamese网络训练时Net_Definition架构下损失函数返回NaN
我在训练用于人脸识别的Siamese神经网络,使用的对比损失函数如下:
def contrastive_loss(y_true, y_pred): margin = 1 return K.mean(y_true * K.square(y_pred) + (1 - y_true) * K.square(K.maximum(margin - y_pred, 0)))
TestModel架构训练完全正常,但Net_Definition架构下该损失函数返回NaN,导致模型无法训练。完整代码如下:
Models.py代码
from keras.models import Sequential, Model from keras.layers import Input, Conv2D, MaxPooling2D, Dense, Dropout, Flatten, Lambda, BatchNormalization, Activation from keras.optimizers import RMSprop from keras import backend as K def euclidean_distance(vects): x, y = vects return K.sqrt(K.sum(K.square(x - y), axis=1, keepdims=True)) def eucl_dist_output_shape(shapes): shape1, shape2 = shapes return (shape1[0], 1) def contrastive_loss(y_true, y_pred): margin = 1 return K.mean(y_true * K.square(y_pred) + (1 - y_true) * K.square(K.maximum(margin - y_pred, 0))) def accuracy(y_true, y_pred): return K.mean(K.equal(y_true, K.cast(y_pred < 0.5, y_true.dtype))) def TestModel(input_shape): model = Sequential() model.add(Conv2D(filters=96, kernel_size=3, strides=3, activation='relu', input_shape=input_shape, padding='valid')) model.add(MaxPooling2D(pool_size=2)) model.add(Dropout(.25)) model.add(Conv2D(filters=256, kernel_size=3, strides=3, activation='relu', padding='valid')) model.add(MaxPooling2D(pool_size=2)) model.add(Dropout(.25)) model.add(Flatten()) model.add(Dense(512, activation='relu')) model.add(Dropout(0.1)) model.add(Dense(128, activation='relu')) return model def Net_Definition(input_shape): model = Sequential() model.add(Conv2D(filters=96, kernel_size=7, strides=4, activation='relu', padding='valid', input_shape=input_shape)) model.add(MaxPooling2D(pool_size=3, strides=2, padding='valid')) model.add(BatchNormalization()) model.add(Conv2D(filters=256, kernel_size=5, strides=1, activation='relu', padding='same')) model.add(MaxPooling2D(pool_size=3, strides=2, padding='valid')) model.add(BatchNormalization()) model.add(Conv2D(filters=384, kernel_size=3, strides=1, activation='relu', padding='same')) model.add(MaxPooling2D(pool_size=3, strides=2, padding='valid')) model.add(Flatten()) model.add(Dense(512, activation='relu')) model.add(Dropout(.5)) model.add(Dense(512, activation='relu')) model.add(Dropout(.5)) model.add(Dense(128, activation='softmax')) return model def CreateModel(name, input_shape): global network if name == 'test': network = TestModel(input_shape) elif name == 'net_definition': network = Net_Definition(input_shape) else: print('Invalid model name!') exit(0) network = Net_Definition(input_shape) input_a = Input(shape=input_shape) input_b = Input(shape=input_shape) processed_a = network(input_a) processed_b = network(input_b) distance = Lambda(euclidean_distance, output_shape=eucl_dist_output_shape)([processed_a, processed_b]) model = Model(inputs=[input_a, input_b], outputs=distance) opt = RMSprop() model.compile(loss=contrastive_loss, optimizer=opt, metrics=[accuracy]) return model
训练代码
from keras.utils import Sequence import numpy as np import Models from keras.callbacks import CSVLogger class MyGenerator(Sequence): def __init__(self, filenames, labels, batch_size): self.filenames = filenames self.labels = labels self.batch_size = batch_size def __len__(self): return (np.ceil(len(self.filenames) / float(self.batch_size))).astype(np.int32) def __getitem__(self, item): batch_x = self.filenames[item * self.batch_size:(item + 1) * self.batch_size] batch_y = self.labels[item * self.batch_size:(item + 1) * self.batch_size] x1 = [] x2 = [] for i, files in enumerate(batch_x): pair = np.load(files).astype(np.float32) x1.append(pair[0]/255) x2.append(pair[1]/255) x1 = np.asarray(x1) x2 = np.asarray(x2) return (x1, x2), np.array(batch_y).astype(np.float32) # path_to_folder = 'Datasets/test/pairs/224/' path_to_folder = 'Datasets/6. Pairs/224/' input_shape = (224, 224, 3) batch_size = 128 x_train_file = open(path_to_folder + 'X_Train.txt', 'r') y_train_file = open(path_to_folder + 'Y_Train.txt', 'r') x_val_file = open(path_to_folder + 'X_Val.txt', 'r') y_val_file = open(path_to_folder + 'Y_Val.txt', 'r') x_train = x_train_file.read().splitlines() y_train = y_train_file.read().splitlines() x_val = x_val_file.read().splitlines() y_val = y_val_file.read().splitlines() csv_logger = CSVLogger('logs.log') train_generator = MyGenerator(x_train, y_train, batch_size) val_generator = MyGenerator(x_val, y_val, batch_size) model = Models.CreateModel('test', input_shape) history = model.fit(train_generator, epochs=10, verbose=1, validation_data=val_generator, callbacks=[csv_logger]) model.save_weights('my_checkpoint')
问题原因分析
输出层激活函数错误:Net_Definition的最后一层用了
softmax激活,而TestModel用的是relu。Siamese网络的嵌入层需要输出连续的实数值向量,用于计算欧氏距离。softmax会将输出归一化为概率分布,所有元素之和为1,这会限制嵌入向量的数值范围,计算欧氏距离时可能出现异常,甚至在反向传播中引发梯度爆炸/消失,最终导致损失函数出现NaN。CreateModel函数的BUG:不管传入的name是'test'还是'net_definition',最后都强制把
network赋值为Net_Definition(input_shape),这意味着你之前测试的TestModel其实没真正被使用,无法正确切换模型。数值稳定性问题:对比损失函数中
K.sqrt计算欧氏距离时,如果输入的平方和为0或者极小值,可能引发数值不稳定;另外,当margin - y_pred为负数时,K.maximum会截断为0,但如果y_pred异常大(比如因为嵌入层输出异常),会导致平方项过大,进而引发NaN。
解决方法
- 修正Net_Definition的输出层激活:把最后一层的
softmax改成relu或者线性激活(不指定激活函数),保持嵌入向量的连续性:
# Net_Definition最后一行修改为 model.add(Dense(128, activation='relu')) # 或者使用线性激活 # model.add(Dense(128))
- 修复CreateModel的赋值BUG:删除强制赋值的
network = Net_Definition(input_shape)行,确保根据传入的name选择对应的模型:
def CreateModel(name, input_shape): global network if name == 'test': network = TestModel(input_shape) elif name == 'net_definition': network = Net_Definition(input_shape) else: print('Invalid model name!') exit(0) # 删掉下面这行错误赋值 # network = Net_Definition(input_shape) input_a = Input(shape=input_shape) input_b = Input(shape=input_shape) processed_a = network(input_a) processed_b = network(input_b) distance = Lambda(euclidean_distance, output_shape=eucl_dist_output_shape)([processed_a, processed_b]) model = Model(inputs=[input_a, input_b], outputs=distance) opt = RMSprop() model.compile(loss=contrastive_loss, optimizer=opt, metrics=[accuracy]) return model
- 优化损失函数的数值稳定性:在欧氏距离计算中加入一个极小值,避免开根号时出现0的情况:
def euclidean_distance(vects): x, y = vects # 加入1e-8防止sqrt(0)引发数值异常 return K.sqrt(K.sum(K.square(x - y), axis=1, keepdims=True) + 1e-8)
- 调整优化器参数:降低RMSprop的学习率,默认学习率0.001对于更深的Net_Definition来说可能过大,容易引发梯度爆炸:
opt = RMSprop(learning_rate=0.0001)
其他适用于Siamese网络的损失函数
- Triplet Loss:通过锚点、正样本、负样本三元组训练,拉近锚点与正样本的距离,拉远锚点与负样本的距离,非常适合人脸识别任务:
def triplet_loss(y_true, y_pred, alpha=0.2): anchor, positive, negative = y_pred[:,0], y_pred[:,1], y_pred[:,2] pos_dist = K.sum(K.square(anchor - positive), axis=1) neg_dist = K.sum(K.square(anchor - negative), axis=1) basic_loss = pos_dist - neg_dist + alpha loss = K.maximum(basic_loss, 0.0) return K.mean(loss)
- Binary Cross-Entropy Loss:将Siamese网络的输出改为相似度分数(比如用Dense层+sigmoid激活),直接分类是否为同一人脸:
# 修改CreateModel函数的输出部分 def CreateModel(name, input_shape): # ... 前面代码不变 ... processed_a = network(input_a) processed_b = network(input_b) # 拼接两个嵌入向量后用全连接层输出相似度 concat = Concatenate()([processed_a, processed_b]) similarity = Dense(1, activation='sigmoid')(concat) model = Model(inputs=[input_a, input_b], outputs=similarity) model.compile(loss='binary_crossentropy', optimizer=opt, metrics=['accuracy']) return model
内容的提问来源于stack exchange,提问作者Inki
相关产品推荐
相关产品推荐

