You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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')

问题原因分析

  1. 输出层激活函数错误:Net_Definition的最后一层用了softmax激活,而TestModel用的是relu。Siamese网络的嵌入层需要输出连续的实数值向量,用于计算欧氏距离。softmax会将输出归一化为概率分布,所有元素之和为1,这会限制嵌入向量的数值范围,计算欧氏距离时可能出现异常,甚至在反向传播中引发梯度爆炸/消失,最终导致损失函数出现NaN。

  2. CreateModel函数的BUG:不管传入的name是'test'还是'net_definition',最后都强制把network赋值为Net_Definition(input_shape),这意味着你之前测试的TestModel其实没真正被使用,无法正确切换模型。

  3. 数值稳定性问题:对比损失函数中K.sqrt计算欧氏距离时,如果输入的平方和为0或者极小值,可能引发数值不稳定;另外,当margin - y_pred为负数时,K.maximum会截断为0,但如果y_pred异常大(比如因为嵌入层输出异常),会导致平方项过大,进而引发NaN。


解决方法

  1. 修正Net_Definition的输出层激活:把最后一层的softmax改成relu或者线性激活(不指定激活函数),保持嵌入向量的连续性:
# Net_Definition最后一行修改为
model.add(Dense(128, activation='relu'))
# 或者使用线性激活
# model.add(Dense(128))
  1. 修复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
  1. 优化损失函数的数值稳定性:在欧氏距离计算中加入一个极小值,避免开根号时出现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)
  1. 调整优化器参数:降低RMSprop的学习率,默认学习率0.001对于更深的Net_Definition来说可能过大,容易引发梯度爆炸:
opt = RMSprop(learning_rate=0.0001)

其他适用于Siamese网络的损失函数

  1. 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)
  1. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 14:14:52