Padding и маскирование последовательностей

В работе с последовательными данными, такими как текст или временные ряды, часто возникает необходимость обработки входов различной длины. Модели нейронных сетей, особенно рекуррентные (RNN, LSTM, GRU) и трансформеры, требуют единообразных размеров входных тензоров. Padding и маскирование служат для решения этой задачи.


1. Концепция Padding

Padding — это добавление специальных значений к последовательностям для приведения их к одинаковой длине. В TensorFlow.js обычно используют 0 или любое другое значение, которое не встречается в реальных данных.

Пример последовательностей:

const sequences = [
  [1, 2, 3],
  [4, 5],
  [6]
];

Для выравнивания до длины 3 добавляется нулевой padding:

const paddedSequences = [
  [1, 2, 3],
  [4, 5, 0],
  [6, 0, 0]
];

Основные методы добавления padding в TensorFlow.js:

  1. tf.pad — универсальный метод для добавления значений к тензору.
const tf = require('@tensorflow/tfjs');

const tensor = tf.tensor2d([[1, 2], [3, 4]]);
const padded = tf.pad(tensor, [[0, 1], [0, 2]], 0);
padded.print();

Аргументы [[0, 1], [0, 2]] обозначают добавление 0 в начале и конце каждой размерности: [top, bottom] для строк, [left, right] для столбцов.

  1. tf.keras.preprocessing.sequence.padSequences (через Keras API) — удобный способ для последовательностей в NLP:
const tf = require('@tensorflow/tfjs');
const { padSequences } = require('@tensorflow/tfjs-layers/dist/keras_format/utils');

const sequences = [[1, 2, 3], [4, 5], [6]];
const padded = padSequences(sequences, { maxlen: 4, padding: 'pre', value: 0 });
console.log(padded);

Опции padSequences:

  • maxlen — желаемая длина последовательности.
  • padding'pre' (в начало) или 'post' (в конец).
  • truncating'pre' или 'post', определяет, с какой стороны обрезать длинные последовательности.
  • value — значение padding (по умолчанию 0).

2. Маскирование последовательностей

Маскирование (masking) позволяет модели игнорировать добавленные элементы padding при обучении и вычислении градиентов. Это критично для правильной работы RNN и трансформеров, чтобы не учитывать искусственные нули.

Создание маски в TensorFlow.js:

const tf = require('@tensorflow/tfjs');

const sequences = tf.tensor2d([
  [1, 2, 3],
  [4, 5, 0],
  [6, 0, 0]
]);

const mask = sequences.notEqual(tf.scalar(0));
mask.print();

Результат — булевый тензор:

[[true, true, true],
 [true, true, false],
 [true, false, false]]

Этот маска-тензор можно передавать слоям, поддерживающим маскирование, например tf.layers.lstm:

const model = tf.sequential();
model.add(tf.layers.embedding({ inputDim: 10, outputDim: 8, inputLength: 3, maskZero: true }));
model.add(tf.layers.lstm({ units: 16 }));
  • Параметр maskZero: true у Embedding автоматически создает маску, игнорируя элементы с нулевым padding.
  • LSTM слой использует эту маску для пропуска ненужных шагов при вычислении скрытых состояний.

3. Padding и маскирование в трансформерах

Трансформеры требуют строгой маски для attention, чтобы не учитывать padding при вычислении весов внимания:

function createPaddingMask(seq) {
  const mask = tf.cast(seq.equal(tf.scalar(0)), 'float32');
  return mask.expandDims(1).expandDims(2); // [batch, 1, 1, seq_len]
}

Использование маски в Multi-Head Attention:

const attentionOutput = tf.layers.multiHeadAttention({
  numHeads: 8,
  keyDim: 64
}).apply([query, key, value, createPaddingMask(inputSequence)]);
  • Маска имеет значение 1 для padding и 0 для реальных элементов.
  • Attention слой автоматически вычитает бесконечно большое значение из весов внимания, чтобы игнорировать padding.

4. Практические рекомендации

  • Всегда использовать одинаковую длину последовательностей при пакетной обработке данных.
  • Значение padding должно однозначно отличаться от допустимых данных.
  • Для NLP лучше использовать maskZero: true в Embedding.
  • Для временных рядов можно использовать tf.pad с собственными значениями padding.
  • Маски должны синхронизироваться с длиной последовательностей и batch размером.

5. Пример полного пайплайна

const tf = require('@tensorflow/tfjs');
const { padSequences } = require('@tensorflow/tfjs-layers/dist/keras_format/utils');

const sequences = [[2, 3, 4], [5, 6], [7]];

// Шаг 1: Padding
const padded = padSequences(sequences, { maxlen: 4, padding: 'post', value: 0 });

// Шаг 2: Создание маски
const tensor = tf.tensor2d(padded);
const mask = tensor.notEqual(tf.scalar(0));

// Шаг 3: Создание модели с маскирующим Embedding
const model = tf.sequential();
model.add(tf.layers.embedding({ inputDim: 10, outputDim: 8, inputLength: 4, maskZero: true }));
model.add(tf.layers.lstm({ units: 16 }));
model.add(tf.layers.dense({ units: 1, activation: 'sigmoid' }));

// Шаг 4: Компиляция и обучение
model.compile({ optimizer: 'adam', loss: 'binaryCrossentropy' });

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