Маскирование и дополнение данных с Keras

Смотрите на TensorFlow.org Запустите в Google Colab Изучайте код на GitHub Скачайте ноутбук

Установка

from __future__ import absolute_import, division, print_function, unicode_literals

import numpy as np

try:
  # %tensorflow_version only exists in Colab.
  %tensorflow_version 2.x
except Exception:
  pass
import tensorflow as tf

from tensorflow.keras import layers

Дополнение последовательных данных

При обработке последовательных данных очень часто отдельные примеры имеют разную длину. Рассмотрите следующий пример (текст разбитый на слова):

[
  ["The", "weather", "will", "be", "nice", "tomorrow"],
  ["How", "are", "you", "doing", "today"],
  ["Hello", "world", "!"]
]

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

[
  [83, 91, 1, 645, 1253, 927],
  [73, 8, 3215, 55, 927],
  [71, 1331, 4231]
]

Данные являются двумерным списком где длина отдельных примеров равна 6, 5, и 3 соответственно. Поскольку входные данные для модели глубокого обучения должны быть единым тензором (размерности напр. (batch_size, 6, vocab_size) в этом случае), элементы которые короче самого длинного элемента нужно дополнить некоторым значением по умолчанию (в качестве альтернативы, можно также обрезать длинные элементы перед заполнением коротких).

Keras предоставляет API позволяющий легко обрезать и дополнять последовательности до общей длины: tf.keras.preprocessing.sequence.pad_sequences.

raw_inputs = [
  [83, 91, 1, 645, 1253, 927],
  [73, 8, 3215, 55, 927],
  [711, 632, 71]
]

# По умолчанию это заполнится нулями; настраивать можно с помощью
# параметра "value".
# Заметьте что вы можете "пре" заполнить (добавив нули в начале) или
# "пост" заполнить (добавив в конце).
# Мы рекомендуем использовать "пост" заполнение при работе со слоями RNN
# (для того чтобы иметь возможность использовать 
# CuDNN реализацию слоев).
padded_inputs = tf.keras.preprocessing.sequence.pad_sequences(raw_inputs,
                                                              padding='post')

print(padded_inputs)
[[  83   91    1  645 1253  927]
 [  73    8 3215   55  927    0]
 [ 711  632   71    0    0    0]]

Masking

Сейчас когда у все примеров одна длина, модели нужно сообщить, что некоторая часть данных на самом деле является дополнением и должна быть проигнорирована. Это можно сделать с помощью маски.

Существует три способа ввода масок ввода в модели Keras.:

  • Добавить слой keras.layers.Masking.
  • Добавить mask_zero=True в конфигурацию слоя keras.layers.Embedding.
  • Передать аргумент mask вручную при вызове слоев с поддержкой этого аргумента (напр. слои RNN).

Генерирующие маски слои: Embedding и Masking

Под капотом эти слои создадут тензор-маску (2D тензор размерности (batch, sequence_length)), и приложат его к тензору возвращаемому на выходе слоем Masking или Embedding.

embedding = layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)
masked_output = embedding(padded_inputs)

print(masked_output._keras_mask)
tf.Tensor(
[[ True  True  True  True  True  True]
 [ True  True  True  True  True False]
 [ True  True  True False False False]], shape=(3, 6), dtype=bool)

masking_layer = layers.Masking()
# Смоделируем поиск вложенния, расширив 2D входные данные до 3D,
# с размерностью вложения равной 10.
unmasked_embedding = tf.cast(
    tf.tile(tf.expand_dims(padded_inputs, axis=-1), [1, 1, 10]),
    tf.float32)

masked_embedding = masking_layer(unmasked_embedding)
print(masked_embedding._keras_mask)
tf.Tensor(
[[ True  True  True  True  True  True]
 [ True  True  True  True  True False]
 [ True  True  True False False False]], shape=(3, 6), dtype=bool)

Как видно из напечатанного результата, маска это двумерный булев тензор рамерности (batch_size, sequence_length), где каждая отдельная запись False указывает на то, что соответствующий временной шаг нужно игнорировать во время работы.

Распространение маски в Functional API и Sequential API

При использовании Functional API или Sequential API, маска генерируемая слоем Embedding или Masking распространится по всей сети, по всем слоям которые могут ее использовать (напр. слои RNN). Keras автоматически извлечет маску соответствующую входу и передаст ее любому слою который знает, как ее использовать.

Заметьте что в методе call субклассированной модели или слоя, маски не распространяются автоматически, поэтому вам нужно вручную передать аргумент mask в каждый слой, который в этом нуждается. См. секцию ниже для деталей.

Например, в следующей Sequential модели, слой LSTM автоматически получит маску, что означает, что он проигнорирует добавленные значения:

model = tf.keras.Sequential([
  layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True),
  layers.LSTM(32),
])

Это так же относится к следующей модели Functional API:

inputs = tf.keras.Input(shape=(None,), dtype='int32')
x = layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)(inputs)
outputs = layers.LSTM(32)(x)

model = tf.keras.Model(inputs, outputs)

Передача тензоров масок напрямую в слои

У слоев которые могут обрабатывать маски (такие как LSTM) есть аргумент mask в их методе __call__.

Между тем, слои в которых создается маска (напр. Embedding) предлагают метод compute_mask(input, previous_mask) который вы можете вызвать.

Поэтому вы можете сделать что-то наподобие этого:

class MyLayer(layers.Layer):

  def __init__(self, **kwargs):
    super(MyLayer, self).__init__(**kwargs)
    self.embedding = layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)
    self.lstm = layers.LSTM(32)

  def call(self, inputs):
    x = self.embedding(inputs)
    # Отметим, что вы можете также приготовить `mask` тензор вручную.
    # Необходимо только чтобы он был булевым тензором
    # правильной размерности, т.е. (batch_size, timesteps).
    mask = self.embedding.compute_mask(inputs)
    output = self.lstm(x, mask=mask)  # Слой будет игнорировать маскированные значения
    return output

layer = MyLayer()
x = np.random.random((32, 10)) * 100
x = x.astype('int32')
layer(x)
<tf.Tensor: shape=(32, 32), dtype=float32, numpy=
array([[ 0.00975671,  0.00219537,  0.00844547, ...,  0.01338787,
         0.00583278,  0.00155192],
       [-0.00459427, -0.00471689,  0.00070982, ..., -0.00107169,
         0.01253186,  0.00050222],
       [-0.00779894,  0.00060803, -0.00232897, ..., -0.00369722,
         0.00160114,  0.00101396],
       ...,
       [ 0.00424753,  0.00043241, -0.00019018, ..., -0.00014108,
         0.00369125, -0.00049732],
       [ 0.00302717,  0.00083464,  0.00347353, ...,  0.0025048 ,
        -0.00198104,  0.00172616],
       [-0.00694957,  0.00138061, -0.00989567, ..., -0.00200742,
        -0.00213604, -0.00819204]], dtype=float32)>

Поддержка масок в ваших кастомных слоях

Вам может понадобиться написать слои, которые генерируют маску (например,Embedding), или слои, которым нужно изменить текущую маску.

Например, любой слой производящщий тензор с отличающейся от входа измерением времени, такой как слой Concatenate который соединяет по времени, будет нуждаться в модификации текущей маски чтобы нижележащие слои могли должным образом учитывать маскированные временные шаги.

Чтобы сделать это ваш слой должен реализовать метод layer.compute_mask(), который производит новую маску с учетом входных данных и текущей маски.

Большинство слоев не меняет размерность времени, так что не нужно переживать о масках. По умолчанию, в таких случаях compute_mask() просто передает дальше текущую маску.

Вот пример слоя TemporalSplit требующего модификации текущей маски.

class TemporalSplit(tf.keras.layers.Layer):
  """Разобьем входной тензор на 2 тензора по временной оси."""

  def call(self, inputs):
    # На ожидается трехмерный вход, а маска должна быть двумерной, разобьем входной тензор на 2
    # подтензора вдоль временной оси (ось 1).
    return tf.split(inputs, 2, axis=1)

  def compute_mask(self, inputs, mask=None):
    # Также разобьем маску на 2 если она есть.
    if mask is None:
      return None
    return tf.split(mask, 2, axis=1)

first_half, second_half = TemporalSplit()(masked_embedding)
print(first_half._keras_mask)
print(second_half._keras_mask)
tf.Tensor(
[[ True  True  True]
 [ True  True  True]
 [ True  True  True]], shape=(3, 3), dtype=bool)
tf.Tensor(
[[ True  True  True]
 [ True  True False]
 [False False False]], shape=(3, 3), dtype=bool)

Вот еще один пример слоя CustomEmbedding способного генерировать маску из входных значений:

class CustomEmbedding(tf.keras.layers.Layer):

  def __init__(self, input_dim, output_dim, mask_zero=False, **kwargs):
    super(CustomEmbedding, self).__init__(**kwargs)
    self.input_dim = input_dim
    self.output_dim = output_dim
    self.mask_zero = mask_zero

  def build(self, input_shape):
    self.embeddings = self.add_weight(
      shape=(self.input_dim, self.output_dim),
      initializer='random_normal',
      dtype='float32')

  def call(self, inputs):
    return tf.nn.embedding_lookup(self.embeddings, inputs)

  def compute_mask(self, inputs, mask=None):
    if not self.mask_zero:
      return None
    return tf.not_equal(inputs, 0)


layer = CustomEmbedding(10, 32, mask_zero=True)
x = np.random.random((3, 10)) * 9
x = x.astype('int32')

y = layer(x)
mask = layer.compute_mask(x)

print(mask)
tf.Tensor(
[[ True  True False  True  True  True  True False  True  True]
 [ True  True  True False  True False  True  True  True  True]
 [ True  True  True  True False  True  True  True  True  True]], shape=(3, 10), dtype=bool)

Написание слоев которые нуждаются в информации о маске

Некоторые слои являются потребителями масок: у них есть аргумент mask в call используемый для определения временных шагов которые нужно пропустить.

Для написания такого слоя, вы можете просто добавить аргумент mask=None в сигнатуру вызова call. Маска связанная с входными данными будет передана вашему слою, когда она будет доступной.

class MaskConsumer(tf.keras.layers.Layer):

  def call(self, inputs, mask=None):
    ...

Повторение

Это все что вам нужно знать о масках в Keras. Повторим:

  • "Маскировка" это то, как слои могут узнавать когда пропускать / игнорировать конкретные временные шаги в последовательных временных данных.
  • Некоторые слои являются генераторами масок: Embedding может генерировать маску из входных данных (если mask_zero=True), как и слой Masking.
  • Некоторые слои являются потребителями масок: они используют аргумент mask в своем методе __call__. Это относится к слоям RNN.
  • В Functional API и Sequential API, информация маски распространяется автоматически.
  • При написании субклассированных моделей или при использованнии слоев в автономном режиме, передайте аргументы mask слоям вручную.
  • Вы можете легко написать слои изменяющие текущую маску, генерирующие новую маску, или использующие маску связанную с входными данными.