7.2. Свёртки для изображений¶
Теперь, когда мы понимаем, как работают свёрточные слои в теории, мы готовы рассмотреть их работу на практике. Исходя из нашей мотивации, связанной с тем, что свёрточные нейронные сети являются эффективными архитектурами для исследования структуры в данных изображений, мы продолжаем использовать изображения в качестве примера.
import torch
from torch import nn
from d2l import torch as d2l
7.2.1. Операция кросс-корреляции¶
Напомним, что строго говоря, свёрточные слои — это не совсем корректное название, поскольку операции, которые они выполняют, точнее описываются как перекрёстные корреляции. Исходя из наших описаний свёрточных слоёв в Section 7.1, в таком слое входной тензор и тензор ядра комбинируются для получения выходного тензора посредством операции перекрёстной корреляции.
Давайте пока отложим в сторону каналы и посмотрим, как это работает с двумерными данными и скрытыми представлениями. На рисунке Fig. 7.2.1 входные данные представлены в виде двумерного тензора высотой 3 и шириной 3. Обозначим форму тензора как \(3 \times 3\) или (\(3\), \(3\)). Высота и ширина ядра равны 2. Форма окна свёртки (или свёрточного окна) определяется высотой и шириной ядра (в данном случае это \(2 \times 2\)).
Fig. 7.2.1 Двумерная операция кросс-корреляции. Заштрихованные области обозначают первый элемент выходного тензора, а также элементы входного тензора и ядра, использованные для вычисления этого элемента: \(0\times0+1\times1+3\times2+4\times3=19\).¶
В двумерной операции кросс-корреляции мы начинаем с размещения свёрточного окна в верхнем левом углу входного тензора и перемещаем его по всему входному тензору слева направо и сверху вниз. Когда окно сдвигается в определённую позицию, подтензор входных данных, находящийся в этом окне, и тензор ядра поэлементно перемножаются, а полученный тензор суммируется, в результате чего получается единственный скаляр. Этот результат определяет значение выходного тензора в соответствующей позиции. Таким образом, выходной тензор имеет высоту 2 и ширину 2, а его четыре элемента получены в результате двумерной операции кросс-корреляции:
Обратите внимание, что размер выхода по каждой оси немного меньше размера входа. Поскольку ширина и высота ядра больше \(1\), мы можем корректно вычислять кросс-корреляцию только для тех положений, где ядро полностью помещается в изображение. Размер выхода определяется размером входа \(n_\textrm{h} \times n_\textrm{w}\) минус размер ядра свёртки \(k_\textrm{h} \times k_\textrm{w}\) следующим образом:
Это связано с тем, что нам необходимо достаточно места для «сдвига» ядра
свёртки по изображению. Далее мы рассмотрим, как сохранить размер
неизменным, дополнив изображение нулями по границе, чтобы было
достаточно места для сдвига ядра. Далее мы реализуем этот процесс в
функции corr2d, которая принимает входной тензор X и тензор ядра
K и возвращает выходной тензор Y.
def corr2d(X, K): #@save
"""Compute 2D cross-correlation."""
h, w = K.shape
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
return Y
Мы можем создать входной тензор X и тензор ядра K из
Fig. 7.2.1, чтобы проверить вывод вышеописанной
реализации двумерной операции кросс-корреляции.
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
corr2d(X, K)
7.2.2. Свёрточные слои¶
Свёрточный слой выполняет операцию взаимной корреляции между входными данными и ядром и добавляет скалярное смещение для получения результата. Два параметра свёрточного слоя — это ядро и скалярное смещение. При обучении моделей на основе свёрточных слоёв мы обычно инициализируем ядра случайным образом, так же как и в случае с полносвязным слоем.
Теперь мы готовы реализовать двумерный свёрточный слой на основе функции
corr2d, описанной выше. В конструкторе __init__ мы объявляем
weight и bias в качестве двух параметров модели. Метод прямого
распространения вызывает функцию corr2d и добавляет смещение.
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super().__init__()
self.weight = nn.Parameter(torch.rand(kernel_size))
self.bias = nn.Parameter(torch.zeros(1))
def forward(self, x):
return corr2d(x, self.weight) + self.bias
В \(h \times w\) свёртке или в \(h \times w\) ядре свёртки высота и ширина ядра свёртки равны \(h\) и \(w\) соответственно. Мы также называем слой свёртки с ядром свёртки размера \(h \times w\) просто \(h \times w\)-свёрточным слоем.
7.2.3. Обнаружение границ объектов на изображениях¶
Давайте разберем простое применение свёрточного слоя: обнаружение границы объекта на изображении путем определения местоположения изменения пикселей. Сначала мы создадим «изображение» из \(6\times 8\) пикселей. Четыре центральных столбца будут черными (\(0\)), а остальные — белыми (\(1\)).
X = torch.ones((6, 8))
X[:, 2:6] = 0
X
Далее мы создадим ядро K высотой 1 и шириной 2. При выполнении
операции кросс-корреляции с входными данными, если горизонтально
соседние элементы одинаковы, результат равен нулю. В противном случае
результат отличен от нуля. Обратите внимание, что данное ядро является
частным случаем оператора конечной разности. В позиции \((i,j)\) оно
вычисляет \(x_{i,j} - x_{(i+1),j}\), то есть разность значений
горизонтально соседних пикселей. Это дискретное приближение первой
производной по горизонтальному направлению. Ведь для функции
\(f(i,j)\) её производная определяется как
\(-\partial_i f(i,j) = \lim_{\epsilon \to 0} \frac{f(i,j) - f(i+\epsilon,j)}{\epsilon}\).
Давайте посмотрим, как это работает на практике.
K = torch.tensor([[1.0, -1.0]])
Мы готовы выполнить операцию кросс-корреляции с аргументами X (наши
входные данные) и K (наше ядро). Как видно, мы обнаруживаем
\(1\) для границы от белого к черному и \(-1\) для границы от
черного к белому. Все остальные выходные значения принимают значение
\(0\).
Y = corr2d(X, K)
Y
Теперь мы можем применить ядро к транспонированному изображению. Как и
ожидалось, оно исчезает. Ядро K обнаруживает только вертикальные
границы.
corr2d(X.t(), K)
7.2.4. Обучение ядра¶
Разработка детектора краёв с помощью конечных разностей [1, -1]
выглядит привлекательно, если мы точно знаем, что именно это нам и
нужно. Однако при рассмотрении более крупных ядер и последовательных
слоёв свёрток может оказаться невозможным вручную определить, что именно
должен делать каждый фильтр.
Теперь посмотрим, сможем ли мы научиться восстанавливать ядро, которое
сгенерировало Y из X, используя только пары «вход–выход».
Сначала мы создадим свёрточный слой и инициализируем его ядро случайным
тензором. Затем на каждой итерации мы будем использовать квадратичную
ошибку для сравнения Y с выходом свёрточного слоя. После этого мы
можем вычислить градиент для обновления ядра. Для простоты в дальнейшем
мы будем использовать встроенный класс для двумерных свёрточных слоёв и
игнорировать смещение.
# Construct a two-dimensional convolutional layer with 1 output channel and a
# kernel of shape (1, 2). For the sake of simplicity, we ignore the bias here
conv2d = nn.LazyConv2d(1, kernel_size=(1, 2), bias=False)
# The two-dimensional convolutional layer uses four-dimensional input and
# output in the format of (example, channel, height, width), where the batch
# size (number of examples in the batch) and the number of channels are both 1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 3e-2 # Learning rate
for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
conv2d.zero_grad()
l.sum().backward()
# Update the kernel
conv2d.weight.data[:] -= lr * conv2d.weight.grad
if (i + 1) % 2 == 0:
print(f'epoch {i + 1}, loss {l.sum():.3f}')
Обратите внимание, что ошибка упала до небольшого значения после 10 итераций. Теперь мы рассмотрим тензор ядра, который мы выучили.
conv2d.weight.data.reshape((1, 2))
Действительно, изученный тензор ядра удивительно близок к тензору ядра
K, который мы определили ранее.
7.2.5. Кросс-корреляция и свёртка¶
Вспомним наше наблюдение из Section 7.1 о соответствии между операциями кросс-корреляции и свёртки. Теперь продолжим рассмотрение двумерных свёрточных слоёв. Что, если такие слои выполняют строгие операции свёртки, определённые в (7.1.6), а не кросс-корреляции? Чтобы получить результат строгой операции свёртки, нам нужно лишь отразить двумерный тензор ядра по горизонтали и вертикали, а затем выполнить операцию кросс-корреляции с входным тензором.
Стоит отметить, что поскольку ядра в глубоком обучении обучаются на основе данных, выходы свёрточных слоёв остаются неизменными независимо от того, выполняют ли эти слои строгие операции свёртки или операции кросс-корреляции.
Для наглядности предположим, что свёрточный слой выполняет перекрёстную корреляцию и обучает ядро в Fig. 7.2.1, которое здесь обозначено как матрица \(\mathbf{K}\). Если предположить, что остальные условия остаются неизменными, то при выполнении этим слоем строгой свёртки обученное ядро \(\mathbf{K}'\) будет таким же, как \(\mathbf{K}\), после того как \(\mathbf{K}'\) будет отражено как по горизонтали, так и по вертикали. Иными словами, когда свёрточный слой выполняет строгую свёртку для входных данных в Fig. 7.2.1 и \(\mathbf{K}'\), будет получен тот же самый выход в Fig. 7.2.1 (перекрёстная корреляция входных данных и \(\mathbf{K}\)).
В соответствии со стандартной терминологией, принятой в литературе по глубокому обучению, мы будем продолжать называть операцию перекрёстной корреляции свёрткой, хотя, строго говоря, она немного отличается. Кроме того, мы используем термин элемент для обозначения компонента (или составляющей) любого тензора, представляющего собой либо представление слоя, либо ядро свёртки.
7.2.6. Карта признаков и рецептивное поле¶
Как описано в Section 7.1.4, выход свёрточного слоя в Fig. 7.2.1 иногда называют картой признаков, поскольку его можно рассматривать как изученные представления (признаки) в пространственных измерениях (например, ширину и высоту) для последующего слоя. В свёрточных нейронных сетях для любого элемента \(x\) некоторого слоя его рецептивное поле включает все элементы (из всех предыдущих слоёв), которые могут влиять на вычисление значения \(x\) в процессе прямого распространения. Обратите внимание, что рецептивное поле может быть больше фактического размера входных данных.
Продолжим использовать Fig. 7.2.1 для объяснения рецептивного поля. Учитывая \(2 \times 2\) ядро свёртки, рецептивное поле затенённого выходного элемента (со значением \(19\)) — это четыре элемента в затенённой части входных данных. Теперь обозначим \(2 \times 2\) выход как \(\mathbf{Y}\) и рассмотрим более глубокую CNN с дополнительным \(2 \times 2\) свёрточным слоем, который принимает \(\mathbf{Y}\) в качестве входа и выдаёт один элемент \(z\). В этом случае рецептивное поле \(z\) на \(\mathbf{Y}\) включает все четыре элемента \(\mathbf{Y}\), в то время как рецептивное поле на входе включает все девять элементов входных данных. Таким образом, когда любому элементу карты признаков требуется большее рецептивное поле для обнаружения признаков входных данных на более широкой области, мы можем построить более глубокую сеть.
Название «рецептивные поля» происходит из нейрофизиологии. Серия экспериментов на различных животных с использованием разных стимулов (Hubel and Wiesel, 1959, Hubel and Wiesel, 1962, Hubel and Wiesel, 1968) изучала реакцию так называемой зрительной коры на эти стимулы. В целом было установлено, что нижние уровни реагируют на края и связанные с ними формы. Позднее Field (1987) продемонстрировали этот эффект на естественных изображениях с помощью того, что можно назвать только свёрточными ядрами. Мы приводим ключевую иллюстрацию из Fig. 7.2.2, чтобы показать поразительное сходство.
Fig. 7.2.2 Рисунок и подпись взяты из Field (1987): пример кодирования с шестью различными каналами. (Слева) примеры шести типов сенсоров, соответствующих каждому каналу. (Справа) свёртка изображения из (в центре) с шестью сенсорами, показанными на (слева). Отклик отдельных сенсоров определяется путём дискретизации этих отфильтрованных изображений с шагом, пропорциональным размеру сенсора (показано точками). На этой диаграмме показан отклик только чётно-симметричных сенсоров.¶
Как выяснилось, эта связь сохраняется и для признаков, вычисляемых более глубокими слоями сетей, обученных на задачах классификации изображений, что продемонстрировано, например, в Kuzovkin et al. (2018). Достаточно сказать, что свёртки оказались невероятно мощным инструментом в компьютерном зрении — как в биологии, так и в программной реализации. Поэтому неудивительно (задним числом), что именно они предвещали недавний успех глубокого обучения.
7.2.7. Итоги¶
Основной вычислительной операцией, необходимой для свёрточного слоя, является операция кросс-корреляции. Мы видели, что для вычисления её значения достаточно простого вложенного цикла for. Если у нас есть несколько входных и несколько выходных каналов, мы выполняем матрично-матричную операцию между каналами. Как видно, вычисления просты и, что самое важное, высоко локальны. Это позволяет существенно оптимизировать аппаратное обеспечение, и многие недавние достижения в области компьютерного зрения стали возможны именно благодаря этому. В конце концов, это означает, что разработчики чипов могут инвестировать в быстрые вычисления, а не в память, когда речь идет об оптимизации свёртки. Хотя это может не привести к оптимальным решениям для других приложений, это открывает путь к повсеместному и доступному компьютерному зрению.
Что касается самих свёрток, то они могут использоваться для множества задач, например, для обнаружения краёв и линий, размытия изображений или повышения их резкости. Самое главное заключается в том, что статистик (или инженер) не обязан изобретать подходящие фильтры вручную. Вместо этого мы можем просто научить их на основе данных. Это заменяет эвристики конструирования признаков статистикой, основанной на доказательствах. Наконец, и это весьма замечательно, эти фильтры не только полезны для построения глубоких сетей, но и соответствуют рецептивным полям и картам признаков в мозге. Это вселяет уверенность в том, что мы движемся в правильном направлении.
7.2.8. Упражнения¶
Создайте изображение
Xс диагональными ребрами.Что произойдет, если применить ядро
Kиз этого раздела к нему?Что произойдет, если транспонировать
X?Что произойдет, если транспонировать
K?
Разработайте несколько ядер вручную.
Задан направленный вектор \(\mathbf{v} = (v_1, v_2)\). Выведите ядро обнаружения краёв, которое выявляет края, ортогональные направлению \(\mathbf{v}\), то есть края в направлении \((v_2, -v_1)\).
Выведите оператор конечных разностей для второй производной. Каков минимальный размер свёрточного ядра, соответствующего ему? Какие структуры на изображениях реагируют на него наиболее сильно?
Как бы вы спроектировали ядро размытия? Зачем может понадобиться использовать такое ядро?
Каков минимальный размер ядра, необходимый для получения производной порядка \(d\)?
Когда вы попытаетесь автоматически найти градиент для класса
Conv2D, который мы создали, какое сообщение об ошибке вы увидите?Как представить операцию кросс-корреляции в виде матричного умножения, преобразовав тензоры входных данных и ядра?