Masking dan padding dengan Keras

Lihat di TensorFlow.org Jalankan di Google Colab Lihat sumber di GitHub Unduh buku catatan

Mempersiapkan

import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

pengantar

Masking adalah cara untuk memberitahu urut-pengolahan lapisan yang timesteps tertentu dalam input hilang, dan dengan demikian harus dilewati saat memproses data.

Padding adalah bentuk khusus dari masking di mana langkah-langkah bertopeng berada di awal atau akhir berurutan. Padding berasal dari kebutuhan untuk mengkodekan data urutan ke dalam batch yang berdekatan: untuk membuat semua urutan dalam batch sesuai dengan panjang standar yang diberikan, perlu untuk menambahkan atau memotong beberapa urutan.

Mari kita lihat dari dekat.

Data urutan padding

Saat memproses data urutan, sangat umum untuk sampel individu memiliki panjang yang berbeda. Perhatikan contoh berikut (teks yang diberi token sebagai kata-kata):

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

Setelah pencarian kosakata, data mungkin di-vektorkan sebagai bilangan bulat, misalnya:

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

Data adalah daftar bersarang di mana sampel individu memiliki panjang 3, 5, dan 6, masing-masing. Karena input data untuk model pembelajaran yang mendalam harus tensor tunggal (bentuk misalnya (batch_size, 6, vocab_size) dalam hal ini), sampel yang lebih pendek dari item kebutuhan terpanjang untuk empuk dengan beberapa nilai placeholder (alternatif, salah satu mungkin juga memotong sampel panjang sebelum mengisi sampel pendek).

Keras menyediakan fungsi utilitas untuk truncate dan pad daftar Python dengan panjang umum: tf.keras.preprocessing.sequence.pad_sequences .

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

# By default, this will pad using 0s; it is configurable via the
# "value" parameter.
# Note that you could "pre" padding (at the beginning) or
# "post" padding (at the end).
# We recommend using "post" padding when working with RNN layers
# (in order to be able to use the
# CuDNN implementation of the layers).
padded_inputs = tf.keras.preprocessing.sequence.pad_sequences(
    raw_inputs, padding="post"
)
print(padded_inputs)
[[ 711  632   71    0    0    0]
 [  73    8 3215   55  927    0]
 [  83   91    1  645 1253  927]]

penyamaran

Sekarang semua sampel memiliki panjang yang seragam, model harus diinformasikan bahwa beberapa bagian dari data sebenarnya adalah padding dan harus diabaikan. Mekanisme yang masking.

Ada tiga cara untuk memperkenalkan masker input dalam model Keras:

  • Tambahkan keras.layers.Masking lapisan.
  • Mengkonfigurasi keras.layers.Embedding layer dengan mask_zero=True .
  • Lulus mask argumen secara manual saat memanggil lapisan yang mendukung argumen ini (misalnya RNN lapisan).

Lapisan topeng menghasilkan: Embedding dan Masking

Di bawah tenda, lapisan ini akan membuat tensor masker (tensor 2D dengan bentuk (batch, sequence_length) ), dan melampirkannya pada output tensor dikembalikan oleh Masking atau Embedding lapisan.

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

print(masked_output._keras_mask)

masking_layer = layers.Masking()
# Simulate the embedding lookup by expanding the 2D input to 3D,
# with embedding dimension of 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 False False False]
 [ True  True  True  True  True False]
 [ True  True  True  True  True  True]], shape=(3, 6), dtype=bool)
tf.Tensor(
[[ True  True  True False False False]
 [ True  True  True  True  True False]
 [ True  True  True  True  True  True]], shape=(3, 6), dtype=bool)

Seperti yang Anda lihat dari hasil cetak, topeng adalah 2D tensor boolean dengan bentuk (batch_size, sequence_length) , di mana setiap individu False entri menunjukkan bahwa sesuai timestep harus diabaikan selama pemrosesan.

Propagasi mask di Functional API dan Sequential API

Bila menggunakan API Fungsional atau API Sequential, masker dihasilkan oleh Embedding atau Masking lapisan akan disebarkan melalui jaringan untuk setiap lapisan yang mampu menggunakan mereka (misalnya, RNN lapisan). Keras akan secara otomatis mengambil topeng yang sesuai dengan input dan meneruskannya ke lapisan mana pun yang tahu cara menggunakannya.

Misalnya, dalam model Sequential berikutnya, LSTM lapisan akan secara otomatis menerima masker, yang berarti akan mengabaikan nilai-nilai empuk:

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

Ini juga berlaku untuk model API Fungsional berikut:

inputs = 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 = keras.Model(inputs, outputs)

Melewati tensor topeng langsung ke lapisan

Lapisan yang dapat menangani masker (seperti LSTM layer) memiliki mask argumen dalam mereka __call__ metode.

Sementara itu, lapisan yang menghasilkan topeng (misalnya Embedding ) mengekspos compute_mask(input, previous_mask) metode yang dapat Anda menelepon.

Dengan demikian, Anda dapat melewati output dari compute_mask() metode dari layer mask-memproduksi dengan __call__ metode lapisan masker memakan, seperti ini:

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)
        # Note that you could also prepare a `mask` tensor manually.
        # It only needs to be a boolean tensor
        # with the right shape, i.e. (batch_size, timesteps).
        mask = self.embedding.compute_mask(inputs)
        output = self.lstm(x, mask=mask)  # The layer will ignore the masked values
        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([[-3.6287602e-04,  8.8942451e-03, -4.5623952e-03, ...,
         3.6509466e-04, -4.3871473e-03, -1.7532009e-03],
       [ 2.6261162e-03, -2.5420082e-03,  7.6517118e-03, ...,
         5.8210879e-03, -1.5617531e-03, -1.7562184e-03],
       [ 6.8687932e-03,  1.2330032e-03, -1.2028826e-02, ...,
         2.0486799e-03,  5.7172528e-03,  2.6641595e-03],
       ...,
       [-3.4327951e-04,  1.3967649e-03, -1.2102776e-02, ...,
         3.8406218e-03, -2.3374180e-03, -4.9669710e-03],
       [-2.3023323e-03,  1.8474255e-03,  2.7329330e-05, ...,
         6.1798934e-03,  4.2709545e-04,  3.9026213e-03],
       [ 7.4090287e-03,  1.9879336e-03, -2.0261200e-03, ...,
         8.2100276e-03,  8.7051848e-03,  9.9167246e-03]], dtype=float32)>

Mendukung masking di lapisan khusus Anda

Kadang-kadang, Anda mungkin perlu untuk menulis lapisan yang menghasilkan topeng (seperti Embedding ), atau lapisan yang perlu memodifikasi topeng saat ini.

Misalnya, setiap lapisan yang menghasilkan tensor dengan dimensi waktu yang berbeda dari input, seperti Concatenate lapisan yang merangkai pada dimensi waktu, akan perlu memodifikasi topeng saat ini sehingga lapisan hilir akan dapat benar mengambil timesteps bertopeng ke Akun.

Untuk melakukan hal ini, lapisan Anda harus menerapkan layer.compute_mask() metode, yang menghasilkan topeng baru yang diberikan input dan masker saat.

Berikut adalah contoh dari TemporalSplit lapisan yang perlu memodifikasi topeng saat ini.

class TemporalSplit(keras.layers.Layer):
    """Split the input tensor into 2 tensors along the time dimension."""

    def call(self, inputs):
        # Expect the input to be 3D and mask to be 2D, split the input tensor into 2
        # subtensors along the time axis (axis 1).
        return tf.split(inputs, 2, axis=1)

    def compute_mask(self, inputs, mask=None):
        # Also split the mask into 2 if it presents.
        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(
[[False False False]
 [ True  True False]
 [ True  True  True]], shape=(3, 3), dtype=bool)

Berikut ini adalah contoh lain dari CustomEmbedding lapisan yang mampu menghasilkan topeng dari nilai input:

class CustomEmbedding(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  True  True  True  True  True  True  True  True]
 [ True  True  True  True  True False  True  True  True  True]
 [ True  True  True  True  True  True  True  True False  True]], shape=(3, 10), dtype=bool)

Memilih untuk menutupi propagasi pada lapisan yang kompatibel

Sebagian besar lapisan tidak mengubah dimensi waktu, jadi tidak perlu mengubah topeng saat ini. Namun, mereka mungkin masih ingin dapat menyebarkan masker saat ini, tidak berubah, ke lapisan berikutnya. Ini adalah perilaku opt-in. Secara default, lapisan kustom akan menghancurkan topeng saat ini (karena kerangka kerja tidak memiliki cara untuk mengetahui apakah menyebarkan topeng aman untuk dilakukan).

Jika Anda memiliki lapisan khusus yang tidak mengubah dimensi waktu, dan jika Anda ingin untuk dapat menyebarkan masukan topeng saat ini, Anda harus menetapkan self.supports_masking = True di konstruktor lapisan. Dalam hal ini, perilaku default compute_mask() adalah untuk hanya lulus topeng saat melalui.

Berikut adalah contoh lapisan yang masuk daftar putih untuk propagasi topeng:

class MyActivation(keras.layers.Layer):
    def __init__(self, **kwargs):
        super(MyActivation, self).__init__(**kwargs)
        # Signal that the layer is safe for mask propagation
        self.supports_masking = True

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

Anda sekarang dapat menggunakan ini lapisan kustom di-antara layer mask menghasilkan (seperti Embedding ) dan layer mask memakan (seperti LSTM ), dan itu akan berlalu masker bersama sehingga mencapai layer mask memakan.

inputs = keras.Input(shape=(None,), dtype="int32")
x = layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)(inputs)
x = MyActivation()(x)  # Will pass the mask along
print("Mask found:", x._keras_mask)
outputs = layers.LSTM(32)(x)  # Will receive the mask

model = keras.Model(inputs, outputs)
Mask found: KerasTensor(type_spec=TensorSpec(shape=(None, None), dtype=tf.bool, name=None), name='Placeholder_1:0')

Menulis lapisan yang membutuhkan informasi topeng

Beberapa lapisan adalah konsumen mask: mereka menerima mask argumen dalam call dan menggunakannya untuk menentukan apakah akan melewatkan langkah waktu tertentu.

Untuk menulis sebuah layer tersebut, Anda hanya dapat menambahkan mask=None argumen di Anda call tanda tangan. Topeng yang terkait dengan input akan diteruskan ke lapisan Anda kapan pun tersedia.

Berikut adalah contoh sederhana di bawah ini: lapisan yang menghitung softmax selama dimensi waktu (sumbu 1) dari urutan input, sambil membuang langkah waktu bertopeng.

class TemporalSoftmax(keras.layers.Layer):
    def call(self, inputs, mask=None):
        broadcast_float_mask = tf.expand_dims(tf.cast(mask, "float32"), -1)
        inputs_exp = tf.exp(inputs) * broadcast_float_mask
        inputs_sum = tf.reduce_sum(
            inputs_exp * broadcast_float_mask, axis=-1, keepdims=True
        )
        return inputs_exp / inputs_sum


inputs = keras.Input(shape=(None,), dtype="int32")
x = layers.Embedding(input_dim=10, output_dim=32, mask_zero=True)(inputs)
x = layers.Dense(1)(x)
outputs = TemporalSoftmax()(x)

model = keras.Model(inputs, outputs)
y = model(np.random.randint(0, 10, size=(32, 100)), np.random.random((32, 100, 1)))

Ringkasan

Itu saja yang perlu Anda ketahui tentang padding & masking di Keras. Untuk rekap:

  • "Masking" adalah bagaimana lapisan dapat mengetahui kapan harus melewati / mengabaikan langkah waktu tertentu dalam input urutan.
  • Beberapa lapisan yang mask-generator: Embedding dapat menghasilkan topeng dari nilai input (jika mask_zero=True ), dan sehingga dapat dengan Masking lapisan.
  • Beberapa lapisan yang mask-konsumen: mereka mengekspos mask argumen dalam mereka __call__ metode. Ini adalah kasus untuk lapisan RNN.
  • Di Functional API dan Sequential API, informasi mask disebarkan secara otomatis.
  • Bila menggunakan lapisan dalam cara yang mandiri, Anda dapat melewati mask argumen untuk lapisan secara manual.
  • Anda dapat dengan mudah menulis lapisan yang memodifikasi topeng saat ini, yang menghasilkan topeng baru, atau yang menggunakan topeng yang terkait dengan input.