用于 Keras 中句子相似性的具有 LSTM 的连体网络定期给出相同的结果

Posted

技术标签:

【中文标题】用于 Keras 中句子相似性的具有 LSTM 的连体网络定期给出相同的结果【英文标题】:Siamese Network with LSTM for sentence similarity in Keras gives periodically the same result 【发布时间】:2018-03-10 00:17:54 【问题描述】:

我是 Keras 的新手,我正在尝试在 Keras 中使用 NN 解决句子相似性的任务。 我使用 word2vec 作为词嵌入,然后使用 Siamese Network 来预测两个句子的相似程度。 Siamese 网络的基础网络是 LSTM,为了合并这两个基础网络,我使用了带有余弦相似度度量的 Lambda 层。 作为数据集,我使用的是 SICK 数据集,它为每对句子打分,从 1(不同)到 5(非常相似)。

我创建了网络并且它运行了,但我有很多疑问: 首先,我不确定我用句子喂 LSTM 的方式是否合适。我对每个单词进行 word2vec 嵌入,每个句子只创建一个数组,用零填充到 seq_len 以获得相同的长度数组。然后我以这种方式重塑它:data_A = embedding_A.reshape((len(embedding_A), seq_len, feature_dim))

此外,我不确定我的 Siamese Network 是否正确,因为不同对的很多预测都是相等的,并且损失没有太大变化(从 0.3300 到 0.2105 的 10 个时期,并且变化不大在 100 个 epoch 中更多)。

有人可以帮我找出并理解我的错误吗? 非常感谢(抱歉我的英语不好)

对我的代码感兴趣的部分

def cosine_distance(vecs):
    #I'm not sure about this function too
    y_true, y_pred = vecs
    y_true = K.l2_normalize(y_true, axis=-1)
    y_pred = K.l2_normalize(y_pred, axis=-1)
    return K.mean(1 - K.sum((y_true * y_pred), axis=-1))

def cosine_dist_output_shape(shapes):
    shape1, shape2 = shapes
    print((shape1[0], 1))
    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 create_base_network(feature_dim,seq_len):

    model = Sequential()  
    model.add(LSTM(100, batch_input_shape=(1,seq_len,feature_dim),return_sequences=True))
    model.add(Dense(50, activation='relu'))    
    model.add(Dense(10, activation='relu'))
    return model


def siamese(feature_dim,seq_len, epochs, tr_dataA, tr_dataB, tr_y, te_dataA, te_dataB, te_y):    

    base_network = create_base_network(feature_dim,seq_len)

    input_a = Input(shape=(seq_len,feature_dim,))
    input_b = Input(shape=(seq_len,feature_dim))

    processed_a = base_network(input_a)
    processed_b = base_network(input_b)

    distance = Lambda(cosine_distance, output_shape=cosine_dist_output_shape)([processed_a, processed_b])

    model = Model([input_a, input_b], distance)

    adam = Adam(lr=0.0001, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0)
    model.compile(optimizer=adam, loss=contrastive_loss)
    model.fit([tr_dataA, tr_dataB], tr_y,
              batch_size=128,
              epochs=epochs,
              validation_data=([te_dataA, te_dataB], te_y))


    pred = model.predict([tr_dataA, tr_dataB])
    tr_acc = compute_accuracy(pred, tr_y)
    for i in range(len(pred)):
        print (pred[i], tr_y[i])


    return model


def padding(max_len, embedding):
    for i in range(len(embedding)):
        padding = np.zeros(max_len-embedding[i].shape[0])
        embedding[i] = np.concatenate((embedding[i], padding))

    embedding = np.array(embedding)
    return embedding

def getAB(sentences_A,sentences_B, feature_dim, word2idx, idx2word, weights,max_len_def=0):
    #from_sentence_to_array : function that transforms natural language sentences 
    #into vectors of real numbers. Each word is replaced with the corrisponding word2vec 
    #embedding, and words that aren't in the embedding are replaced with zeros vector.  
    embedding_A, max_len_A = from_sentence_to_array(sentences_A,word2idx, idx2word, weights)
    embedding_B, max_len_B = from_sentence_to_array(sentences_B,word2idx, idx2word, weights)

    max_len = max(max_len_A, max_len_B,max_len_def*feature_dim)

    #padding to max_len
    embedding_A = padding(max_len, embedding_A)
    embedding_B = padding(max_len, embedding_B)

    seq_len = int(max_len/feature_dim)
    print(seq_len)

    #rashape
    data_A = embedding_A.reshape((len(embedding_A), seq_len, feature_dim))
    data_B = embedding_B.reshape((len(embedding_B), seq_len, feature_dim))

    print('A,B shape: ',data_A.shape, data_B.shape)

    return data_A, data_B, seq_len



FEATURE_DIMENSION = 100
MIN_COUNT = 10
WINDOW = 5

if __name__ == '__main__':

    data = pd.read_csv('data\\train.csv', sep='\t')
    sentences_A = data['sentence_A']
    sentences_B = data['sentence_B']
    tr_y = 1- data['relatedness_score']/5

    if not (os.path.exists(EMBEDDING_PATH)  and os.path.exists(VOCAB_PATH)):    
        create_embeddings(embeddings_path=EMBEDDING_PATH, vocab_path=VOCAB_PATH,  size=FEATURE_DIMENSION, min_count=MIN_COUNT, window=WINDOW, sg=1, iter=25)
    word2idx, idx2word, weights = load_vocab_and_weights(VOCAB_PATH,EMBEDDING_PATH)

    tr_dataA, tr_dataB, seq_len = getAB(sentences_A,sentences_B, FEATURE_DIMENSION,word2idx, idx2word, weights)

    test = pd.read_csv('data\\test.csv', sep='\t')
    test_sentences_A = test['sentence_A']
    test_sentences_B = test['sentence_B']
    te_y = 1- test['relatedness_score']/5

    te_dataA, te_dataB, seq_len = getAB(test_sentences_A,test_sentences_B, FEATURE_DIMENSION,word2idx, idx2word, weights, seq_len) 

    model = siamese(FEATURE_DIMENSION, seq_len, 10, tr_dataA, tr_dataB, tr_y, te_dataA, te_dataB, te_y)


    test_a = ['this is my dog']
    test_b = ['this dog is mine']
    a,b,seq_len = getAB(test_a,test_b, FEATURE_DIMENSION,word2idx, idx2word, weights, seq_len)
    prediction  = model.predict([a, b])
    print(prediction)

部分结果:

my prediction | true label 
0.849908 0.8
0.849908 0.8
0.849908 0.74
0.849908 0.76
0.849908 0.66
0.849908 0.72
0.849908 0.64
0.849908 0.8
0.849908 0.78
0.849908 0.8
0.849908 0.8
0.849908 0.8
0.849908 0.8
0.849908 0.74
0.849908 0.8
0.849908 0.8
0.849908 0.8
0.849908 0.66
0.849908 0.8
0.849908 0.66
0.849908 0.56
0.849908 0.8
0.849908 0.8
0.849908 0.76
0.847546 0.78
0.847546 0.8
0.847546 0.74
0.847546 0.76
0.847546 0.72
0.847546 0.8
0.847546 0.78
0.847546 0.8
0.847546 0.72
0.847546 0.8
0.847546 0.8
0.847546 0.78
0.847546 0.8
0.847546 0.78
0.847546 0.78
0.847546 0.46
0.847546 0.72
0.847546 0.8
0.847546 0.76
0.847546 0.8
0.847546 0.8
0.847546 0.8
0.847546 0.8
0.847546 0.74
0.847546 0.8
0.847546 0.72
0.847546 0.68
0.847546 0.56
0.847546 0.8
0.847546 0.78
0.847546 0.78
0.847546 0.8
0.852975 0.64
0.852975 0.78
0.852975 0.8
0.852975 0.8
0.852975 0.44
0.852975 0.72
0.852975 0.8
0.852975 0.8
0.852975 0.76
0.852975 0.8
0.852975 0.8
0.852975 0.8
0.852975 0.78
0.852975 0.8
0.852975 0.8
0.852975 0.78
0.852975 0.8
0.852975 0.8
0.852975 0.76
0.852975 0.8

【问题讨论】:

【参考方案1】:

您看到连续相等的值是因为函数 cosine_distance 的输出形状错误。当您使用不带 axis 参数的 K.mean(...) 时,结果是一个标量。要修复它,只需在 cosine_distance 中使用 K.mean(..., axis=-1) 替换 K.mean(...)

更详细的解释:

model.predict()被调用时,输出数组pred首先被预分配,然后被批量预测填充。来自源代码training.py:

if batch_index == 0:
    # Pre-allocate the results arrays.
    for batch_out in batch_outs:
        shape = (num_samples,) + batch_out.shape[1:]
        outs.append(np.zeros(shape, dtype=batch_out.dtype))
for i, batch_out in enumerate(batch_outs):
    outs[i][batch_start:batch_end] = batch_out

在您的情况下,您只有一个输出,因此pred 只是上面代码中的outs[0]。当batch_out 是标量时(例如,结果中看到的0.847546),上面的代码等效于pred[batch_start:batch_end] = 0.847576。由于model.predict() 的默认批量大小为 32,因此您可以看到 32 个连续的 0.847576 值出现在您发布的结果中。


另一个可能更大的问题是标签错误。您通过tr_y = 1- data['relatedness_score']/5 将相关性分数转换为标签。现在如果两个句子“非常相似”,则关联度得分为 5,因此这两个句子的tr_y 为 0。

然而,在对比损失中,当y_true为零时,术语K.maximum(margin - y_pred, 0)实际上意味着“这两个句子应该有一个余弦距离>= margin”。这与您希望模型学习的内容相反(而且我认为您在损失中不需要K.square)。

【讨论】:

非常感谢您的帮助。我更改了余弦函数并且它起作用了:) 但我仍然不明白为什么我的标签是错误的。在 LeCun 论文 (link) 中,关于对比度损失,写成“让 Y 是分配给这对的二进制标签。如果 X1 和 X2 被认为相似,则 Y = 0,如果认为它们不相似,则 Y = 1”,并且这就是我使用那个标签的原因。我错了吗? 您可以比较方程式。 4 与您的contrastive_loss 功能。如果希望 Y = 0 表示与论文中相似的对,则需要交换 contrastive_loss 中的 y_true(1 - y_true) 的位置。 当然,你是对的,现在我明白了!感谢您的帮助和耐心【参考方案2】:

只是为了在某处的答案中捕获它(我在已接受答案的 cmets 中看到它),您的对比损失函数应该是:

loss = K.mean((1 - y) * k.square(d) + y * K.square(K.maximum(margin - d, 0)))

您的 (1 - y) * ...y * ... 混淆了,这可能会使那些以您的示例为起点的人望而却步。否则,这是一个很好的起点。

关于命名的说明:您使用了y_truey_pred 而不是yd。我使用yd 因为y 是您的标签,应该是0 或1,但d 不一定在同一范围内(d 实际上在余弦距离的0 和2 之间) .这并不是y 值的真正预测。您只想在两个输入相似时最小化您的距离测量d,并在它们不同时最大化它(或将其推到您的边距之外)。基本上对比损失不是试图让d预测y,只是试图让d在相同时变小,在不同时变大。

【讨论】:

以上是关于用于 Keras 中句子相似性的具有 LSTM 的连体网络定期给出相同的结果的主要内容,如果未能解决你的问题,请参考以下文章

如何使用Keras LSTM与word嵌入来预测单词id

LSTM 句子相似度分析

如何使用带有词嵌入的 Keras LSTM 来预测词 id

原创:Siamese LSTM解决句子相似度(理论篇)

用于序列二进制分类的 Keras LSTM 模型

读:Instance-aware Image and Sentence Matching with Selective Multimodal LSTM