6.3 Заполнение и шаг
В предыдущем примере (рис. 6.2.1) высота и ширина входа равны , высота и ширина ядра — , а выход имеет размерность . Как мы обобщили в разделе 6.2, если форма входа равна , а форма ядра — , то форма выхода равна . Таким образом, форма выхода свёртки зависит от формы входа и формы ядра.
Что ещё влияет на размер выхода? В этом разделе мы рассмотрим заполнение (padding) и шаг (stride). Рассмотрим такой сценарий. Иногда после нескольких последовательных свёрток выход оказывается заметно меньше входа. Это происходит потому, что высота и ширина ядра обычно больше . Например, изображение пикселей после свёрточных слоёв уменьшится до пикселей. Так граничные пиксели исходного изображения теряют много полезной информации. Заполнение — самый эффективный способ решить эту проблему. Иногда же нам может понадобиться существенно уменьшить высоту и ширину изображения, например если исходное разрешение входа избыточно. В таких случаях помогает шаг (stride).
6.3.1 Заполнение
Как уже говорилось, при последовательном применении нескольких свёрток мы часто теряем пиксели по краям. Поскольку ядра обычно малы, одна свёртка теряет лишь несколько пикселей, но при большом числе последовательных свёрточных слоёв потери накапливаются. Простой способ решить эту проблему — заполнение (padding): по границам входного изображения добавляют элементы, обычно нулевые. Например, на рис. 6.3.1 вход дополнен до , поэтому выход увеличивается до . Затенённая часть — первый элемент выхода и элементы входного и ядерного тензоров, использованные для его вычисления: .
Рис. 6.3.1. Двумерная взаимная корреляция с заполнением
В общем случае, если добавить строк заполнения (примерно половину сверху и половину снизу) и столбцов (примерно половину слева и половину справа), форма выхода будет равна
Это означает, что высота и ширина выхода увеличиваются соответственно на и .
Во многих случаях нужно задать и , чтобы вход и выход имели одинаковую высоту и ширину. Это упрощает предсказание формы выхода каждого слоя при построении сети. Пусть нечётно: тогда мы заполняем по строк с обеих сторон по высоте. Если же чётно, один из вариантов — добавить строк сверху входа и строк снизу. Ширину заполняем аналогично.
Высота и ширина ядер в свёрточных нейронных сетях обычно нечётны, например 1, 3, 5 или 7. Преимущество нечётных размеров в том, что при сохранении пространственных размеров мы можем добавлять одинаковое количество строк сверху и снизу и одинаковое количество столбцов слева и справа.
Кроме того, нечётные размеры ядра и заполнения удобны и в записи. Для любого двумерного тензора X при выполнении условий:
- размер ядра нечётный;
- число строк и столбцов заполнения одинаково с каждой стороны;
- выход имеет ту же высоту и ширину, что и вход;
можно утверждать: выход Y[i, j] получается взаимной корреляцией ядра с окрестностью входа X, центр которой находится в X[i, j].
Например, в примере ниже мы создаём двумерный свёрточный слой с высотой и шириной и добавляем по 1 пикселю заполнения со всех сторон. При входе высотой и шириной выход тоже имеет высоту и ширину .
MXNet:
from mxnet import np, npx
from mxnet.gluon import nn
npx.set_np()
# Для удобства определим функцию, которая вычисляет свёрточный слой.
# Эта функция инициализирует веса свёрточного слоя и добавляет или убирает
# нужные размерности у входа и выхода
def comp_conv2d(conv2d, X):
conv2d.initialize()
# Здесь (1, 1) означает, что размер батча и количество каналов равны 1
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
# Опускаем первые два измерения: размер батча и каналы
return Y.reshape(Y.shape[2:])
# Обратите внимание: с каждой стороны добавлена 1 строка или столбец,
# поэтому всего добавлено 2 строки или 2 столбца
conv2d = nn.Conv2D(1, kernel_size=3, padding=1)
X = np.random.uniform(size=(8, 8))
comp_conv2d(conv2d, X).shape
PyTorch:
import torch
from torch import nn
# Для удобства определим функцию, которая вычисляет свёрточный слой.
# Эта функция инициализирует веса свёрточного слоя и добавляет или убирает
# нужные размерности у входа и выхода
def comp_conv2d(conv2d, X):
# Здесь (1, 1) означает, что размер батча и количество каналов равны 1
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
# Опускаем первые два измерения: размер батча и каналы
return Y.reshape(Y.shape[2:])
# Обратите внимание: с каждой стороны добавлена 1 строка или столбец,
# поэтому всего добавлено 2 строки или 2 столбца
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
comp_conv2d(conv2d, X).shape
TensorFlow:
import tensorflow as tf
# Для удобства определим функцию, которая вычисляет свёрточный слой.
# Эта функция инициализирует веса свёрточного слоя и добавляет или убирает
# нужные размерности у входа и выхода
def comp_conv2d(conv2d, X):
# Здесь (1, 1) означает, что размер батча и количество каналов равны 1
X = tf.reshape(X, (1, ) + X.shape + (1, ))
Y = conv2d(X)
# Опускаем первые два измерения: размер батча и каналы
return tf.reshape(Y, Y.shape[1:3])
# Обратите внимание: с каждой стороны добавлена 1 строка или столбец,
# поэтому всего добавлено 2 строки или 2 столбца
conv2d = tf.keras.layers.Conv2D(1, kernel_size=3, padding='same')
X = tf.random.uniform(shape=(8, 8))
comp_conv2d(conv2d, X).shape
PaddlePaddle:
import warnings
warnings.filterwarnings(action='ignore')
import paddle
from paddle import nn
# Для удобства определим функцию, которая вычисляет свёрточный слой.
# Эта функция инициализирует веса свёрточного слоя и добавляет или убирает
# нужные размерности у входа и выхода
def comp_conv2d(conv2d, X):
# Здесь (1, 1) означает, что размер батча и количество каналов равны 1
X = paddle.reshape(X, [1, 1] + X.shape)
Y = conv2d(X)
return Y.reshape(Y.shape[2:]) # Отбрасываем ненужные первые два измерения: батч и каналы
# Обратите внимание: с каждой стороны добавлена 1 строка или столбец,
# поэтому всего добавлено 2 строки или 2 столбца
conv2d = nn.Conv2D(in_channels=1, out_channels=1, kernel_size=3, padding=1)
X = paddle.rand((8, 8))
comp_conv2d(conv2d, X).shape
Если высота и ширина ядра различаются, мы можем заполнять по высоте и ширине по-разному, чтобы выход и вход имели одинаковые высоту и ширину. В следующем примере используется ядро высотой и шириной ; заполнение по высоте равно , по ширине — .
MXNet:
conv2d = nn.Conv2D(1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape
PyTorch:
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape
TensorFlow:
conv2d = tf.keras.layers.Conv2D(1, kernel_size=(5, 3), padding='same')
comp_conv2d(conv2d, X).shape
PaddlePaddle:
conv2d = nn.Conv2D(in_channels=1, out_channels=1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape
6.3.2 Шаг
При вычислении взаимной корреляции окно свёртки начинает движение из левого верхнего угла входного тензора и сдвигается вниз и вправо. В предыдущих примерах мы по умолчанию сдвигали окно на один элемент за раз. Однако иногда ради эффективности вычислений или для уменьшения числа выборок окно может пропускать промежуточные положения и сдвигаться сразу на несколько элементов.
Количество элементов, на которое окно сдвигается за один шаг, называют шагом (stride). До сих пор мы использовали шаг по высоте и ширине, равный . Как же устроен шаг больше единицы? На рис. 6.3.2 показана двумерная взаимная корреляция с вертикальным шагом и горизонтальным шагом . Затенённая часть — выходные элементы, а также элементы входного и ядерного тензоров, используемые для их вычисления: и .
Видно, что для вычисления второго элемента первого столбца выхода окно свёртки сдвигается вниз на три строки, а для второго элемента первой строки — вправо на два столбца. Когда же окно продолжает сдвигаться вправо ещё на два столбца, выхода нет: входные элементы не могут заполнить окно (если только мы не добавим ещё один столбец заполнения).
Рис. 6.3.2. Двумерная взаимная корреляция с вертикальным шагом и горизонтальным шагом
В общем случае при вертикальном шаге и горизонтальном шаге форма выхода равна
Если задать и , форма выхода упрощается до . Более того, если высота и ширина входа делятся на вертикальный и горизонтальный шаги, форма выхода равна .
Теперь зададим шаг по высоте и ширине равным 2, что уменьшит высоту и ширину входа вдвое.
MXNet:
conv2d = nn.Conv2D(1, kernel_size=3, padding=1, strides=2)
comp_conv2d(conv2d, X).shape
PyTorch:
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape
TensorFlow:
conv2d = tf.keras.layers.Conv2D(1, kernel_size=3, padding='same', strides=2)
comp_conv2d(conv2d, X).shape
PaddlePaddle:
conv2d = nn.Conv2D(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape
Теперь рассмотрим чуть более сложный пример.
MXNet:
conv2d = nn.Conv2D(1, kernel_size=(3, 5), padding=(0, 1), strides=(3, 4))
comp_conv2d(conv2d, X).shape
PyTorch:
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape
TensorFlow:
conv2d = tf.keras.layers.Conv2D(1, kernel_size=(3,5), padding='valid',
strides=(3, 4))
comp_conv2d(conv2d, X).shape
PaddlePaddle:
conv2d = nn.Conv2D(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape
Для краткости, если число заполняемых строк и столбцов по высоте и ширине входа равно и соответственно, будем называть это заполнением . При заполнение равно . Аналогично, если шаги по высоте и ширине равны и , будем говорить о шаге . В частном случае шаг равен . По умолчанию заполнение равно 0, а шаг равен 1. На практике мы редко используем несогласованные шаги или заполнение, то есть обычно и .
Резюме
- Заполнение увеличивает высоту и ширину выхода. Его часто используют, чтобы выход имел ту же высоту и ширину, что и вход.
- Шаг уменьшает высоту и ширину выхода; например, высота и ширина выхода составляют лишь от высоты и ширины входа ( — целое число больше ).
- Заполнение и шаг можно использовать для эффективной настройки размерности данных.
Упражнения
- Для последнего примера этого раздела вычислите форму выхода и проверьте, совпадает ли она с результатом эксперимента.
- В экспериментах этого раздела попробуйте другие комбинации заполнения и шага.
- Что означает шаг для аудиосигнала?
- В чём вычислительное преимущество шага больше ?
MXNet: Обсуждение
PyTorch: Обсуждение
TensorFlow: Обсуждение
PaddlePaddle: Обсуждение