Сиамская сеть с LSTM для схожести предложений в Керасе периодически дает один и тот же результат

Я новичок в Керасе, и я пытаюсь решить задачу подобия предложений, используя NN в Керасе. Я использую word2vec для встраивания слов, а затем Siamese Network, чтобы предсказать, насколько похожи два предложения. Базовая сеть для сиамской сети - это LSTM, и для объединения двух базовых сетей я использую слой лямбда с метрикой сходства косинуса. В качестве набора данных я использую набор данных SICK, который дает оценку каждой паре предложений от 1 (разные) до 5 (очень похожие).

Я создал сеть, и она работает, но у меня много сомнений: во-первых, я не уверен, что то, как я кормлю LSTM предложениями, подходит. Я использую встраивание word2vec для каждого слова и создаю только один массив для каждого предложения, дополняя его нулями до seq_len, чтобы получить массивы одинаковой длины. А потом переделываю его так: data_A = embedding_A.reshape((len(embedding_A), seq_len, feature_dim))

Кроме того, я не уверен, что моя сиамская сеть верна, потому что многие прогнозы для разных пар равны, а потери не сильно меняются (от 0,3300 до 0,2105 за 10 эпох, и не сильно изменится за 100). эпох).

Кто-нибудь может помочь мне найти и понять мои ошибки? Большое спасибо (и извините за мой плохой английский)

Интересующая часть моего кода

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

person MiVe93    schedule 28.09.2017    source источник


Ответы (2)


Вы видите последовательные равные значения, потому что форма вывода функции cosine_distance неверна. Когда вы берете K.mean(...) без аргумента axis, результатом является скаляр. Чтобы исправить это, просто используйте K.mean(..., axis=-1) в cosine_distance для замены 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 в убытке).

person Yu-Yang    schedule 30.09.2017
comment
Спасибо большое за вашу помощь. Я изменил функцию косинуса, и она сработала :) Но я до сих пор не понимаю, почему мои метки неправильные. В статье LeCun (ссылка) о Записывается Contrastive Loss. Пусть Y - двоичная метка, присвоенная этой паре. Y = 0, если X1 и X2 считаются похожими, и Y = 1, если они считаются несходными, и поэтому я использовал эти ярлыки. Я ошибся? - person MiVe93; 05.10.2017
comment
Вы можете сравнить уравнение. 4 с вашей функцией contrastive_loss. Если вы хотите, чтобы Y = 0 обозначало похожие пары, как в документе, вам нужно поменять местами y_true и (1 - y_true) в contrastive_loss. - person Yu-Yang; 05.10.2017
comment
Конечно, ты прав, теперь я понял! Спасибо за вашу помощь и терпение - person MiVe93; 05.10.2017

Чтобы это было где-то зафиксировано в ответе (я вижу это в комментариях к принятому ответу), ваша функция контрастных потерь должна быть:

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

Ваши (1 - y) * ... и y * ... были перепутаны, что может отпугнуть людей, использующих ваш пример в качестве отправной точки. В остальном это отличная отправная точка.

Примечание по номенклатуре: вы использовали y_true и y_pred вместо y и d. Я использую y и d, потому что y - ваши метки, которые должны быть либо 0, либо 1, но d не обязательно находится в этом же диапазоне (d фактически находится между 0 и 2 для косинусного расстояния). На самом деле это не прогноз значения y. Вы просто хотите минимизировать вашу меру расстояния d, когда два входа похожи, и максимизировать ее (или выдвинуть ее за пределы вашего поля), когда они разные. По сути, контрастирующая потеря - это не попытка заставить d предсказать y, просто попытка добиться d быть маленьким, когда оно одинаковое, и большим, когда разное.

person Engineero    schedule 18.04.2018