用于带有预编码输入的二进制分类的连体网络

Siamese Network for binary classification with pre-encoded inputs

我想训练一个孪生网络来比较向量的相似性。

我的数据集由向量对和目标列组成,如果它们相同则为“1”,否则为“0”(二进制 class化):

import pandas as pd

# Define train and test sets.
X_train_val = pd.read_csv("train.csv")
print(X_train_val.head())

y_train_val = X_train_val.pop("class")
print(y_train_val.value_counts())

# Keep 50% of X_train_val in validation set.
X_train, X_val = X_train_val[:991], X_train_val[991:]
y_train, y_val = y_train_val[:991], y_train_val[991:]
del X_train_val, y_train_val

# Split our data to 'left' and 'right' inputs (one for each side Siamese).
X_left_train, X_right_train = X_train.iloc[:, :200], X_train.iloc[:, 200:]
X_left_val, X_right_val = X_val.iloc[:, :200], X_val.iloc[:, 200:]

assert X_left_train.shape == X_right_train.shape

# Repeat for test set.
X_test = pd.read_csv("test.csv")
y_test = X_test.pop("class")

print(y_test.value_counts())

X_left_test, X_right_test = X_test.iloc[:, :200], X_test.iloc[:, 200:]

returns

         v0        v1        v2  ...       v397      v398      v399  class
0  0.003615  0.013794  0.030388  ...  -0.093931  0.106202  0.034870    0.0
1  0.018988  0.056302  0.002915  ...  -0.007905  0.100859 -0.043529    0.0
2  0.072516  0.125697  0.111230  ...  -0.010007  0.064125 -0.085632    0.0
3  0.051016  0.066028  0.082519  ...   0.012677  0.043831 -0.073935    1.0
4  0.020367  0.026446  0.015681  ...   0.062367 -0.022781 -0.032091    0.0

1.0    1060
0.0     923
Name: class, dtype: int64

1.0     354
0.0     308
Name: class, dtype: int64

我的脚本的其余部分如下:

import keras
import keras.backend as K
from keras.layers import Dense, Dropout, Input, Lambda
from keras.models import Model


def euclidean_distance(vectors):
    """
    Find the Euclidean distance between two vectors.
    """
    x, y = vectors
    sum_square = K.sum(K.square(x - y), axis=1, keepdims=True)
    # Epsilon is small value that makes very little difference to the value of the denominator, but ensures that it isn't equal to exactly zero.
    return K.sqrt(K.maximum(sum_square, K.epsilon()))


def contrastive_loss(y_true, y_pred):
    """
    Distance-based loss function that tries to ensure that data samples that are semantically similar are embedded closer together.

    See:
    * https://gombru.github.io/2019/04/03/ranking_loss/
    """
    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):
    """
    Compute classification accuracy with a fixed threshold on distances.
    """
    return K.mean(K.equal(y_true, K.cast(y_pred < 0.5, y_true.dtype)))


def create_base_network(input_dim: int, dense_units: int, dropout_rate: float):
    input1 = Input(input_dim, name="encoder")
    x = input1
    x = Dense(dense_units, activation="relu")(x)
    x = Dropout(dropout_rate)(x)
    x = Dense(dense_units, activation="relu")(x)
    x = Dropout(dropout_rate)(x)
    x = Dense(dense_units, activation="relu", name="Embeddings")(x)
    return Model(input1, x)


def build_siamese_model(input_dim: int):
    shared_network = create_base_network(input_dim, dense_units=128, dropout_rate=0.1)

    left_input = Input(input_dim)
    right_input = Input(input_dim)

    # Since this is a siamese nn, both sides share the same network.
    encoded_l = shared_network(left_input)
    encoded_r = shared_network(right_input)

    # The euclidean distance layer outputs close to 0 value when two inputs are similar and 1 otherwise.
    distance = Lambda(euclidean_distance, name="Euclidean-Distance")([encoded_l, encoded_r])

    siamese_net = Model(inputs=[left_input, right_input], outputs=distance)
    siamese_net.compile(loss=contrastive_loss, optimizer="RMSprop", metrics=[accuracy])

    return siamese_net


model = build_siamese_model(X_left_train.shape[1])

es_callback = keras.callbacks.EarlyStopping(monitor="val_loss", patience=3, verbose=0)
history = model.fit(
    [X_left_train, X_right_train],
    y_train,
    validation_data=([X_left_val, X_right_val], y_val),
    epochs=100,
    callbacks=[es_callback],
    verbose=1,
)

我绘制了对比损失与时代以及模型精度与时代的对比:

验证线几乎是平坦的,这对我来说很奇怪(过度拟合?)。

将共享网络的dropout从0.1改为0.5后,得到如下结果:

不知何故它看起来更好,但也会产生错误的预测。

我的问题是:

编辑:

在遵循@PlzBePython 的建议后,我提出了以下基础网络:

distance = Lambda(lambda tensors: K.abs(tensors[0] - tensors[1]), name="L1-Distance")([encoded_l, encoded_r])
output = Dense(1, activation="linear")(distance)
siamese_net = Model(inputs=[left_input, right_input], outputs=output)
siamese_net.compile(loss=contrastive_loss, optimizer="RMSprop", metrics=[accuracy])

感谢您的帮助!

这不是一个答案,更多的是写下我的想法,希望他们能帮助找到答案。


总的来说,我觉得你所做的一切都很合理。 关于您的问题:

1:

嵌入或特征提取层从来都不是必须的,但几乎总能让学习目标变得更容易。你可以把它们想象成为你的距离模型提供一个句子的综合摘要而不是它的原始单词。这也使您的模型不依赖于单词的位置。在您的情况下,创建句子的 summary/important 特征并将相似的句子彼此靠近嵌入是由同一个网络完成的。当然,这也是可行的,我什至不认为这是一个糟糕的做法。但是,我可能会增加网络大小。

2:

在我看来,这两个损失函数并没有太大区别。二元交叉熵定义为:

而对比损失(margin = 1)是:

所以您基本上是将对数函数换成了铰链函数。 唯一真正的区别来自距离计算。你可能被建议使用某种 L1 距离,因为 L2 距离应该在更高的维度(see for example here)下表现更差,而你的维度是 128。就你个人而言,我宁愿在你的情况下使用 L1,但我不不要认为这是一个交易破坏者。


我会尝试的是:

  • 增加保证金参数。 “1”总是在误报情况下导致相当低的损失。这通常会减慢训练速度
  • 尝试嵌入到 [-inf, inf] space(将最后一层嵌入激活更改为“线性”)
  • 将“binary_crossentropy”损失更改为“keras.losses.BinaryCrossentropy(from_logits=True)”并将最后激活从“sigmoid”更改为“线性”。这实际上应该没有什么区别,但我对 keras 二元交叉熵函数有一些奇怪的体验,from_logits 有时似乎有帮助
  • 增加参数

最后,90% 的验证准确率对我来说实际上已经很不错了。请记住,当在第一个时期计算验证准确度时,模型已经完成了大约 60 次权重更新 (batch_size = 32)。这意味着,尤其是在第一集中,验证准确度高于训练准确度(在训练期间计算)是可以预料的。此外,这有时会导致误认为训练损失的增加速度快于验证损失。

编辑

我建议在最后一层使用“线性”,因为 tensorflow recommends it(“from_logits”=True 需要 [-inf, inf] 中的值)用于二元交叉熵。根据我的经验,它收敛得更好。