Как замаскировать функцию потерь в Keras с помощью бэкэнда TensorFlow?

Я пытаюсь реализовать задачу от последовательности к последовательности, используя LSTM от Keras с бэкэндом TensorFlow. Входные данные - английские предложения переменной длины. Чтобы создать набор данных с двумерной формой [batch_number, max_sentence_length], я добавляю EOF в конце строки и дополняю каждое предложение достаточным количеством заполнителей, например #. А затем каждый символ в предложении преобразуется в горячий вектор, так что набор данных имеет трехмерную форму [batch_number, max_sentence_length, character_number]. После уровней кодера и декодера LSTM вычисляется кросс-энтропия softmax между выходом и целью.

Чтобы устранить эффект заполнения при обучении модели, можно использовать маскирование для функций ввода и потерь. Ввод маски в Keras можно выполнить с помощью layers.core.Masking. В TensorFlow маскирование функции потерь можно выполнить следующим образом: настраиваемая функция маскированных потерь в TensorFlow .

Однако я не нашел способа реализовать это в Keras, поскольку определяемая пользователем функция потерь в Keras принимает только параметры y_true и y_pred. Итак, как ввести true sequence_lengths в функцию потерь и маску?

Кроме того, я нахожу функцию _weighted_masked_objective(fn) в \keras\engine\training.py. Его определение

Добавляет поддержку маскирования и взвешивания выборки к целевой функции.

Но похоже, что функция может принимать только fn(y_true, y_pred). Есть ли способ использовать эту функцию для решения моей проблемы?

Чтобы быть конкретным, я модифицирую пример Ю-Янга.

from keras.models import Model
from keras.layers import Input, Masking, LSTM, Dense, RepeatVector, TimeDistributed, Activation
import numpy as np
from numpy.random import seed as random_seed
random_seed(123)

max_sentence_length = 5
character_number = 3 # valid character 'a, b' and placeholder '#'

input_tensor = Input(shape=(max_sentence_length, character_number))
masked_input = Masking(mask_value=0)(input_tensor)
encoder_output = LSTM(10, return_sequences=False)(masked_input)
repeat_output = RepeatVector(max_sentence_length)(encoder_output)
decoder_output = LSTM(10, return_sequences=True)(repeat_output)
output = Dense(3, activation='softmax')(decoder_output)

model = Model(input_tensor, output)
model.compile(loss='categorical_crossentropy', optimizer='adam')
model.summary()

X = np.array([[[0, 0, 0], [0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 1, 0]],
          [[0, 0, 0], [0, 1, 0], [1, 0, 0], [0, 1, 0], [0, 1, 0]]])
y_true = np.array([[[0, 0, 1], [0, 0, 1], [1, 0, 0], [0, 1, 0], [0, 1, 0]], # the batch is ['##abb','#babb'], padding '#'
          [[0, 0, 1], [0, 1, 0], [1, 0, 0], [0, 1, 0], [0, 1, 0]]])

y_pred = model.predict(X)
print('y_pred:', y_pred)
print('y_true:', y_true)
print('model.evaluate:', model.evaluate(X, y_true))
# See if the loss computed by model.evaluate() is equal to the masked loss
import tensorflow as tf
logits=tf.constant(y_pred, dtype=tf.float32)
target=tf.constant(y_true, dtype=tf.float32)
cross_entropy = tf.reduce_mean(-tf.reduce_sum(target * tf.log(logits),axis=2))
losses = -tf.reduce_sum(target * tf.log(logits),axis=2)
sequence_lengths=tf.constant([3,4])
mask = tf.reverse(tf.sequence_mask(sequence_lengths,maxlen=max_sentence_length),[0,1])
losses = tf.boolean_mask(losses, mask)
masked_loss = tf.reduce_mean(losses)
with tf.Session() as sess:
    c_e = sess.run(cross_entropy)
    m_c_e=sess.run(masked_loss)
    print("tf unmasked_loss:", c_e)
    print("tf masked_loss:", m_c_e)

Вывод в Keras и TensorFlow сравнивается следующим образом:

введите описание изображения здесь

Как показано выше, после некоторых типов слоев маскирование отключено. Итак, как замаскировать функцию потерь в Keras при добавлении этих слоев?


person Shuaaai    schedule 01.11.2017    source источник
comment
Вы хотите динамическое маскирование?   -  person Marcin Możejko    schedule 01.11.2017
comment
@ MarcinMożejko Если '' динамическое маскирование означает маскирование функции потерь в соответствии с различными входными данными модели, да, это то, что я хочу.   -  person Shuaaai    schedule 02.11.2017


Ответы (2)


Если в вашей модели есть маска, она будет распространяться слой за слоем и в конечном итоге применяться к потерям. Поэтому, если вы правильно заполняете и маскируете последовательности, потеря заполнителей заполнения будет проигнорирована.

Некоторые детали:

Объяснить весь процесс немного сложно, поэтому я просто разобью его на несколько этапов:

  1. В compile() маска собирается путем вызова compute_mask() и применяется к потерям (нерелевантные строки для ясности игнорируются).
weighted_losses = [_weighted_masked_objective(fn) for fn in loss_functions]

# Prepare output masks.
masks = self.compute_mask(self.inputs, mask=None)
if masks is None:
    masks = [None for _ in self.outputs]
if not isinstance(masks, list):
    masks = [masks]

# Compute total loss.
total_loss = None
with K.name_scope('loss'):
    for i in range(len(self.outputs)):
        y_true = self.targets[i]
        y_pred = self.outputs[i]
        weighted_loss = weighted_losses[i]
        sample_weight = sample_weights[i]
        mask = masks[i]
        with K.name_scope(self.output_names[i] + '_loss'):
            output_loss = weighted_loss(y_true, y_pred,
                                        sample_weight, mask)
  1. Внутри Model.compute_mask() вызывается run_internal_graph().
  2. Внутри run_internal_graph() маски в модели распространяются слой за слоем от входов модели к выходам путем итеративного вызова Layer.compute_mask() для каждого слоя.

Поэтому, если вы используете в своей модели слой Masking, вам не следует беспокоиться о потере заполнителей для заполнения. Потеря этих записей будет замаскирована, как вы, вероятно, уже видели внутри _weighted_masked_objective().

Небольшой пример:

max_sentence_length = 5
character_number = 2

input_tensor = Input(shape=(max_sentence_length, character_number))
masked_input = Masking(mask_value=0)(input_tensor)
output = LSTM(3, return_sequences=True)(masked_input)
model = Model(input_tensor, output)
model.compile(loss='mae', optimizer='adam')

X = np.array([[[0, 0], [0, 0], [1, 0], [0, 1], [0, 1]],
              [[0, 0], [0, 1], [1, 0], [0, 1], [0, 1]]])
y_true = np.ones((2, max_sentence_length, 3))
y_pred = model.predict(X)
print(y_pred)
[[[ 0.          0.          0.        ]
  [ 0.          0.          0.        ]
  [-0.11980877  0.05803877  0.07880752]
  [-0.00429189  0.13382857  0.19167568]
  [ 0.06817091  0.19093043  0.26219055]]

 [[ 0.          0.          0.        ]
  [ 0.0651961   0.10283815  0.12413475]
  [-0.04420842  0.137494    0.13727818]
  [ 0.04479844  0.17440712  0.24715884]
  [ 0.11117355  0.21645413  0.30220413]]]

# See if the loss computed by model.evaluate() is equal to the masked loss
unmasked_loss = np.abs(1 - y_pred).mean()
masked_loss = np.abs(1 - y_pred[y_pred != 0]).mean()

print(model.evaluate(X, y_true))
0.881977558136

print(masked_loss)
0.881978

print(unmasked_loss)
0.917384

Как видно из этого примера, потери на замаскированной части (нули в y_pred) игнорируются, а выход model.evaluate() равен masked_loss.


РЕДАКТИРОВАТЬ:

Если есть повторяющийся слой с return_sequences=False, то маскировка распространяется (т. Е. Возвращается маска None). В RNN.compute_mask():

def compute_mask(self, inputs, mask):
    if isinstance(mask, list):
        mask = mask[0]
    output_mask = mask if self.return_sequences else None
    if self.return_state:
        state_mask = [None for _ in self.states]
        return [output_mask] + state_mask
    else:
        return output_mask

В вашем случае, если я правильно понимаю, вам нужна маска, основанная на y_true, и всякий раз, когда значение y_true равно [0, 0, 1] (горячая кодировка #), вы хотите, чтобы потеря была замаскирована. Если это так, вам нужно замаскировать значения потерь примерно так, как это сделал Дэниел.

Основное отличие - итоговое среднее значение. Среднее значение следует брать по количеству немаскированных значений, которое составляет всего K.sum(mask). Кроме того, y_true можно напрямую сравнить с вектором с горячим кодированием [0, 0, 1].

def get_loss(mask_value):
    mask_value = K.variable(mask_value)
    def masked_categorical_crossentropy(y_true, y_pred):
        # find out which timesteps in `y_true` are not the padding character '#'
        mask = K.all(K.equal(y_true, mask_value), axis=-1)
        mask = 1 - K.cast(mask, K.floatx())

        # multiply categorical_crossentropy with the mask
        loss = K.categorical_crossentropy(y_true, y_pred) * mask

        # take average w.r.t. the number of unmasked entries
        return K.sum(loss) / K.sum(mask)
    return masked_categorical_crossentropy

masked_categorical_crossentropy = get_loss(np.array([0, 0, 1]))
model = Model(input_tensor, output)
model.compile(loss=masked_categorical_crossentropy, optimizer='adam')

Выходные данные приведенного выше кода показывают, что потери вычисляются только для немаскированных значений:

model.evaluate: 1.08339476585
tf unmasked_loss: 1.08989
tf masked_loss: 1.08339

Значение отличается от вашего, потому что я изменил аргумент axis в tf.reverse с [0,1] на [1].

person Yu-Yang    schedule 01.11.2017
comment
Спасибо за ответ. Да, это может работать, когда return_sequences=True в LSTM. Однако в модели кодировщика-декодера LSTM в кодировщике обычно устанавливает return_sequences=False и использует RepeatVector для повторения вывода последнего блока, затем LSTM в декодере принимает его. Чтобы быть конкретным, я модифицирую ваш небольшой пример, чтобы показать проблему. Я покажу это «ответь на мой вопрос» ниже, так как комментарий не может быть слишком длинным. - person Shuaaai; 02.11.2017
comment
@Shuaaai А, под seq2seq, я думал, вы имеете в виду модели, подобные показанной в этом пример. Я обновил ответ. Пожалуйста, посмотрите, хотите ли вы этого. - person Yu-Yang; 02.11.2017
comment
Во-первых, большое вам спасибо. Да, мне нужна маска на основе y_true. Я запускаю ваш обновленный код и выдает ошибку ValueError: Размеры должны быть равны, но равны 5 и 3 для 'Equal' (op: 'Equal') с входными формами: [2,5,3], [3,1 ]. Это вызвано разными версиями или чем-то еще? - person Shuaaai; 02.11.2017
comment
Виноват. Я вставил неправильный код. Теперь он должен работать. - person Yu-Yang; 02.11.2017
comment
По-прежнему возникает ошибка ValueError: начальное_значение должно иметь указанную форму: Tensor (density_1_target: 0, shape = (?,?,?), Dtype = float32). Может я ошибаюсь? - person Shuaaai; 02.11.2017
comment
Умм, какие у вас версии Keras и TF? Я тестирую код на Keras 2.0.9 + TF 1.3.0, и он отлично работает. Не могли бы вы предоставить дополнительную информацию об ошибке? - person Yu-Yang; 02.11.2017
comment
Мой Keras - 2.0.4, а TF - 1.1.0. Я просто вытаскиваю свой код и снимок экрана с ошибкой в ​​github.com/Shuaaai/Mask-loss-function. Не могли бы вы это проверить? Спасибо. - person Shuaaai; 02.11.2017
comment
(1) В вашей функции потерь есть K.variable(y_true), удалите K.variable(). (2) Для Keras 2.0.4 это должно быть K.categorical_crossentropy(y_pred, y_true) вместо K.categorical_crossentropy(y_true, y_pred). В более поздних версиях аргументы меняются местами. (3) Строка mask = tf.reverse(...,[0,1]) должна быть mask = tf.reverse(...,[1]). Вы же не хотите менять сэмплы (ось 0), верно? - person Yu-Yang; 02.11.2017
comment
Да, я так много ошибаюсь ... Большое спасибо! Моя проблема решена. - person Shuaaai; 02.11.2017
comment
Привет, Ю-Ян, я пробовал этот метод маскирования на модели github. com / fchollet / keras / blob / master / examples / add_rnn.py. Я не нахожу явной разницы в потере обучения или потере проверки между исходной моделью и измененной моделью с одним и тем же номером итерации. Есть ли у вас опыт по этому поводу? Спасибо. - person Shuaaai; 05.11.2017
comment
Извините, что у меня нет опыта работы с этой моделью. На первый взгляд, я думаю, что вам, вероятно, не следует маскировать отступы в этом вопросе. Модель должна научиться предсказывать пробелы, если ответ содержит пробелы. Рассмотрим пример "12 + 34 = 46" и "12 + 34 = 468", последнее явно неверно. Модель должна вывести 4, 6 и пробел с учетом входных данных 12 + 34. - person Yu-Yang; 06.11.2017
comment
Другими словами, если модель предсказывает достаточно хорошо, позиции заполнения не должны сильно теряться. Тогда не так важно, замаскировали вы отступы или нет. - person Yu-Yang; 06.11.2017
comment
Да, "12 + 34 = 468" будет неверным предсказанием, данным "12 + 34 = 46". Поэтому я добавляю EOF в ответы (например, 46 #), и модель также будет изучать EOF. Тогда символы перед EOF будут выведены, а пробелы после EOF можно будет игнорировать. Я предполагаю, что положение заполнения могло бы меньше повлиять на модель, если бы модель была достаточно сильной, чтобы справиться с задачей, но я не нахожу подходящего исследования или теоретического объяснения. - person Shuaaai; 06.11.2017
comment
Спасибо за четкий ответ. У меня есть еще один вопрос: кажется, что плотные слои не поддерживают маскировку. Что произойдет, если TimeDistributed (Dense ()) будет добавлен после LSTM (require_sequences = True)? Будет ли маска недействительной? - person soloice; 25.07.2018
comment
Я считаю, что Dense слой поддерживает маскировку. Поскольку в Dense не реализована функция compute_mask, по умолчанию маска должна просто распространяться по слою без изменений. - person Yu-Yang; 25.07.2018
comment
@ Yu-Yang, что может быть эквивалентом этого в TensorFlow? - person DINA TAKLIT; 22.03.2019

Если вы не используете маски, как в ответе Ю-Янга, вы можете попробовать это.

Если у вас есть целевые данные Y с длиной и дополнены значением маски, вы можете:

import keras.backend as K
def custom_loss(yTrue,yPred):

    #find which values in yTrue (target) are the mask value
    isMask = K.equal(yTrue, maskValue) #true for all mask values

    #since y is shaped as (batch, length, features), we need all features to be mask values
    isMask = K.all(isMask, axis=-1) #the entire output vector must be true
        #this second line is only necessary if the output features are more than 1

    #transform to float (0 or 1) and invert
    isMask = K.cast(isMask, dtype=K.floatx())
    isMask = 1 - isMask #now mask values are zero, and others are 1

    #multiply this by the inputs:
       #maybe you might need K.expand_dims(isMask) to add the extra dimension removed by K.all
     yTrue = yTrue * isMask   
     yPred = yPred * isMask

     return someLossFunction(yTrue,yPred)

Если у вас есть отступы только для входных данных или если Y не имеет длины, вы можете иметь свою собственную маску вне функции:

masks = [
   [1,1,1,1,1,1,0,0,0],
   [1,1,1,1,0,0,0,0,0],
   [1,1,1,1,1,1,1,1,0]
]
 #shape (samples, length). If it fails, make it (samples, length, 1). 

import keras.backend as K

masks = K.constant(masks)

Поскольку маски зависят от ваших входных данных, вы можете использовать значение маски, чтобы знать, где ставить нули, например:

masks = np.array((X_train == maskValue).all(), dtype='float64')    
masks = 1 - masks

#here too, if you have a problem with dimensions in the multiplications below
#expand masks dimensions by adding a last dimension = 1.

И сделайте так, чтобы ваша функция принимала маски извне (вы должны воссоздать функцию потерь, если вы измените входные данные):

def customLoss(yTrue,yPred):

    yTrue = masks*yTrue
    yPred = masks*yPred

    return someLossFunction(yTrue,yPred)

Кто-нибудь знает, если keras автоматически маскирует функцию потерь ?? Поскольку он предоставляет маскирующий слой и ничего не говорит о выходных данных, может быть, он делает это автоматически?

person Daniel Möller    schedule 01.11.2017
comment
Дэниел - это действительно плохой ответ. Маски по длине динамически назначаются y_true и y_pred, поэтому вы не можете определить его снаружи - поскольку такие маски меняются. Если вы сделаете это способом, который вы предоставили - это закончится постоянной маской - чего не ожидает OP. - person Marcin Możejko; 01.11.2017
comment
@ MarcinMożejko, большое спасибо. Мой ответ был действительно плохим. - person Daniel Möller; 01.11.2017
comment
Все еще не очень хорошо по сравнению с Yu-Yang, но если они не используют маскирующий слой, он может применяться. - person Daniel Möller; 01.11.2017
comment
Если вы определите настраиваемую потерю внутри функции модели, вы все равно сможете получить доступ к тензору маски. Так что это правильный ответ. - person jonperl; 28.01.2019