web-dev-qa-db-ja.com

Tensorflowバックエンドを使用したKeras--損失関数のマスキング

TensorflowバックエンドでKerasのLSTMを使用して、シーケンス間タスクを実装しようとしています。入力は可変長の英文です。 2次元形状[batch_number、max_sentence_length]でデータセットを構築するには、行末にEOFを追加し、各文に十分なプレースホルダー(例:#)を埋め込みます。文はワンホットベクトルに変換され、データセットは3D形状[batch_number、max_sentence_length、character_number]になりますLSTMエンコーダーおよびデコーダーレイヤーの後、出力とターゲット間のソフトマックスクロスエントロピーが計算されます。

モデルトレーニングでパディング効果を排除するには、入力および損失関数でマスキングを使用できます。 Kerasのマスク入力は、「layers.core.Masking」を使用して実行できます。 Tensorflowでは、損失関数のマスキングは次のように実行できます。 Tensorflowのカスタムマスク損失関数

ただし、kerasで使用定義された損失関数はパラメータy_trueおよびy_predのみを受け入れるため、Kerasでそれを実現する方法を見つけられません。では、損失関数とマスクに真のsequence_lengthsを入力するにはどうすればいいですか?

また、\ keras\engine\training.pyに関数 "_weighted_masked_objective(fn)"があります。その定義は、「目的関数にマスキングとサンプルの重み付けのサポートを追加します。」しかし、関数はfn(y_true、y_pred)のみを受け入れるようです。この関数を使用して問題を解決する方法はありますか?

具体的には、Yu-Yangの例を変更します。

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の出力は、次のように比較されます。 enter image description here

上に示したように、マスキングはいくつかの種類のレイヤーの後に無効になります。では、これらのレイヤーが追加されたときにケラの損失関数をマスクする方法は?

21
Shuaaai

モデルにマスクがある場合、レイヤーごとに伝搬され、最終的に損失に適用されます。したがって、正しい方法でシーケンスをパディングおよびマスクしている場合、パディングプレースホルダーの損失は無視されます。

いくつかの詳細:

プロセス全体を説明するのは少し複雑なので、いくつかのステップに分けます。

  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
_

_tf.reverse_のaxis引数を_[0,1]_から_[1]_に変更したため、値はあなたのものとは異なります。

17
Yu-Yang

Yu-Yangの答えのようにマスクを使用していない場合は、これを試すことができます。

ターゲットデータ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)

ケラスが損失関数を自動的にマスクするかどうかは誰にもわかりませんか?マスキングレイヤーを提供し、出力については何も言わないので、多分それは自動的に行いますか?

0
Daniel Möller