machine learning
masked tensors
loss functions
tensor operations
deep learning

\`Loss\` on masked tensors

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Masked tensors are common when sequence lengths vary, labels are partially missing, or some regions of an input should not contribute to optimization. Examples include padded NLP batches, invalid pixels in segmentation masks, and recommender data with unknown targets. If you compute loss on all elements blindly, gradients will reflect noise from padded or irrelevant values, and training quality drops.

The right approach is to compute per-element loss, apply a mask, and normalize by the number of valid entries. This keeps gradient magnitude consistent across batches and prevents long padded samples from dominating updates. In TensorFlow and Keras, you can implement this manually or rely on built-in masking features when they match your use case.

Core Sections

Compute masked loss explicitly

Start from unreduced loss (reduction='none'), then weight by mask.

python
1import tensorflow as tf
2
3# y_true and y_pred shape: [batch, time]
4y_true = tf.constant([[1., 0., 1.], [1., 1., 0.]])
5y_pred = tf.constant([[0.9, 0.1, 0.7], [0.8, 0.4, 0.2]])
6mask   = tf.constant([[1., 1., 0.], [1., 1., 1.]])
7
8bce = tf.keras.losses.BinaryCrossentropy(reduction="none")
9per_step = bce(y_true, y_pred)            # shape [batch, time]
10masked = per_step * mask
11loss = tf.reduce_sum(masked) / tf.reduce_sum(mask)

This formula ensures only valid positions contribute. The denominator should be valid count, not total tensor size.

Keep batch normalization stable with variable masks

If valid counts vary heavily per batch, gradient scale can jump. Track the valid count and clip when needed.

python
1def masked_mean(values, mask, eps=1e-8):
2    num = tf.reduce_sum(values * mask)
3    den = tf.reduce_sum(mask)
4    return num / tf.maximum(den, eps)

Using a small epsilon avoids division-by-zero on edge batches where every entry is masked.

Integrate with custom training loops

In GradientTape, pass mask from dataset and compute loss consistently.

python
1optimizer = tf.keras.optimizers.Adam()
2
3@tf.function
4def train_step(x, y, mask, model):
5    with tf.GradientTape() as tape:
6        pred = model(x, training=True)
7        per_element = tf.keras.losses.mean_squared_error(y, pred)
8        loss = tf.reduce_sum(per_element * mask) / tf.maximum(tf.reduce_sum(mask), 1.0)
9
10    grads = tape.gradient(loss, model.trainable_variables)
11    optimizer.apply_gradients(zip(grads, model.trainable_variables))
12    return loss

Keeping masking in one utility function reduces inconsistencies between training and validation.

Use Keras sample weights when suitable

For many models, sample_weight can implement masking without custom loops.

python
1model.compile(optimizer="adam", loss="binary_crossentropy")
2model.fit(
3    x_train,
4    y_train,
5    sample_weight=mask_train,
6    epochs=5,
7    batch_size=32,
8)

This is simpler, but verify shape semantics. sample_weight can be per-sample or per-timestep depending on output rank and loss configuration.

Sequence models and built-in masks

Embedding layers can produce masks (mask_zero=True), and compatible recurrent layers propagate them automatically.

python
1inputs = tf.keras.Input(shape=(None,), dtype="int32")
2x = tf.keras.layers.Embedding(input_dim=10000, output_dim=128, mask_zero=True)(inputs)
3x = tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64))(x)
4outputs = tf.keras.layers.Dense(1, activation="sigmoid")(x)
5model = tf.keras.Model(inputs, outputs)

Built-in masking is convenient, but it only helps layers that support mask propagation. Custom ops still need explicit handling.

Common Pitfalls

  • Reducing loss before masking, which makes masked positions influence gradients even if you multiply later.
  • Dividing by full tensor size instead of valid-element count, causing under-training when padding is large.
  • Letting all-zero masks pass through without safeguards, resulting in NaN or unstable updates.
  • Assuming Keras automatic masking applies to every custom layer or manual tensor operation.
  • Using integer masks with unintended broadcasting shapes that silently zero or keep wrong dimensions.

Summary

Masked loss is fundamentally a weighting problem: compute unreduced loss, zero invalid positions, then normalize by valid count. Whether you implement this with a custom training step or sample_weight, consistency is critical across train and evaluation paths. Add safeguards for empty masks and shape checks so masking logic cannot fail silently. With correct masked-loss design, models learn from real signal instead of padding artifacts and converge more reliably on variable-length or partially observed data.

In practice, log both masked loss and valid-element count per batch during debugging. Sudden drops in valid count often explain unstable training far better than model changes alone. Observability around masking helps catch upstream data issues before they become expensive model-quality problems.


Course illustration
Course illustration

All Rights Reserved.