Свёртки и устройство CNN¶
Автор: Максим Банцевич
Дополнение к статье 2.3. Нужен только NumPy. Выполните ячейки сверху вниз. Изображения синтетические, для проверки вычислений. Обучения и сравнения точности в этом примере нет.
Один фильтр¶
Считаем взаимную корреляцию: шаг 1, без дополнения, одно смещение на весь фильтр.
import numpy as np
def conv2d(x, kernel, bias=0.0):
x = np.asarray(x, dtype=float)
kernel = np.asarray(kernel, dtype=float)
if x.ndim != 2 or kernel.ndim != 2:
raise ValueError("Нужны две двумерные таблицы")
h, w = x.shape
kh, kw = kernel.shape
if min(kh, kw) < 1 or kh > h or kw > w:
raise ValueError("Ядро должно помещаться во входе")
out = np.empty((h-kh+1, w-kw+1))
for i in range(out.shape[0]):
for j in range(out.shape[1]):
out[i, j] = (x[i:i+kh, j:j+kw] * kernel).sum() + bias
return out
x = np.tile([0, 0, 1, 1], (4, 1))
kernel = np.array([[-1, 1], [-1, 1]])
y = conv2d(x, kernel)
print(y)
assert np.array_equal(y, np.tile([0, 2, 0], (3, 1)))
[[0. 2. 0.] [0. 2. 0.] [0. 2. 0.]]
Сдвиг и направление границы¶
Перед запуском предскажите, как изменится ответ. Проверим также однородную картинку, минимальный вход и слишком большое ядро.
shifted = np.tile([0, 0, 0, 1], (4, 1))
assert np.array_equal(conv2d(shifted, kernel), np.tile([0, 0, 2], (3, 1)))
assert np.array_equal(conv2d(1-x, kernel), -y)
assert np.array_equal(conv2d(np.ones((4, 4)), kernel), np.zeros((3, 3)))
assert conv2d([[2]], [[3]], bias=1).item() == 7
try:
conv2d([[1]], np.ones((2, 2)))
except ValueError:
print("Слишком большое ядро отклонено")
else:
raise AssertionError("Нужна проверка размера ядра")
print(conv2d(shifted, kernel))
Слишком большое ядро отклонено [[0. 0. 2.] [0. 0. 2.] [0. 0. 2.]]
Размер карты и область видимости¶
Проверим три случая из рисунка 7 и проследим, какой участок исходного изображения влияет на одно значение после двух свёрток.
def size(n, k, p=0, s=1):
return (n + 2*p - k)//s + 1
for padding, stride, expected in [(0, 1, 3), (1, 1, 5), (0, 2, 2)]:
n = size(5, 3, p=padding, s=stride)
assert n == expected
print(f"Рамка: {padding}, шаг: {stride}, карта: {n} × {n}")
image = np.arange(49).reshape(7, 7)
kernel3 = np.ones((3, 3))
first = conv2d(image, kernel3)
second = conv2d(first, kernel3)
assert first.shape == (5, 5)
assert second.shape == (3, 3)
# Пиксель в углу участка 5 × 5 влияет на первое значение второго слоя.
changed = image.copy()
changed[4, 4] += 1
assert conv2d(conv2d(changed, kernel3), kernel3)[0, 0] != second[0, 0]
# Пиксель за пределами этого участка на первое значение не влияет.
changed = image.copy()
changed[5, 5] += 1
assert conv2d(conv2d(changed, kernel3), kernel3)[0, 0] == second[0, 0]
print("Для первого значения второго слоя область видимости — участок 5 × 5")
Рамка: 0, шаг: 1, карта: 3 × 3 Рамка: 1, шаг: 1, карта: 5 × 5 Рамка: 0, шаг: 2, карта: 2 × 2 Для первого значения второго слоя область видимости — участок 5 × 5
Самостоятельно¶
- Поверните фильтр для поиска горизонтальной границы. Вход: две чёрные строки над двумя белыми. Ожидайте среднюю строку из двоек.
- Уберите рамку нулей вокруг изображения 5 × 5. Как изменится карта при окне 3 × 3 и шаге 1? Проверьте ответ функцией
size. - Добавьте третью свёртку с окном 3 × 3 и шагом 1. Предскажите размер результата для исходного изображения 7 × 7 и размер области видимости единственного выходного значения. Затем проверьте результат кодом.
Два фильтра на одном изображении¶
Фильтр А сравнивает левую и правую части окна, фильтр Б — верхнюю и нижнюю. Оба работают с тем же изображением 4 × 4, с шагом 1, без дополнения и смещения.
В окне на вертикальной границе верхняя и нижняя строки совпадают: 0, 1. Поэтому фильтр А даёт отклик 2, а фильтр Б — 0. Проверьте это вручную перед запуском.
image = np.tile([0, 0, 1, 1], (4, 1))
kernel_v = np.array([[-1, 1], [-1, 1]])
kernel_h = np.array([[-1, -1], [1, 1]])
map_v = conv2d(image, kernel_v)
map_h = conv2d(image, kernel_h)
assert np.array_equal(map_v, np.tile([0, 2, 0], (3, 1)))
assert np.array_equal(map_h, np.zeros((3, 3)))
patch = image[:2, 1:3]
assert np.array_equal(patch, [[0, 1], [0, 1]])
assert (patch * kernel_v).sum() == 2
assert (patch * kernel_h).sum() == 0
print("Изображение:", image, sep="\n")
print("Фильтр А:", kernel_v, "Карта А:", map_v, sep="\n")
print("Фильтр Б:", kernel_h, "Карта Б:", map_h, sep="\n")
# На горизонтальной границе отклики меняются местами.
assert np.array_equal(conv2d(image.T, kernel_v), np.zeros((3, 3)))
assert np.array_equal(conv2d(image.T, kernel_h), [[0, 0, 0], [2, 2, 2], [0, 0, 0]])
Изображение: [[0 0 1 1] [0 0 1 1] [0 0 1 1] [0 0 1 1]] Фильтр А: [[-1 1] [-1 1]] Карта А: [[0. 2. 0.] [0. 2. 0.] [0. 2. 0.]] Фильтр Б: [[-1 -1] [ 1 1]] Карта Б: [[0. 0. 0.] [0. 0. 0.] [0. 0. 0.]]
Дополнительная задача: вычисление свёртки¶
Это необязательное упражнение для тех, кто хочет реализовать рассмотренный приём.
Условие. Даны таблица целых чисел и фильтр 2 × 2. Вычислите карту откликов с шагом 1, без дополнения и смещения. В каждом положении умножьте соответствующие элементы и сложите произведения.
Вход. В первой строке записаны высота H и ширина W. Далее идут H строк по W чисел, затем две строки по два числа — веса фильтра.
Ограничения. 2 ≤ H, W ≤ 30; значения изображения и фильтра — целые числа от −9 до 9.
Выход. H − 1 строк по W − 1 чисел — полученная карта.
Пример входа:
3 3
0 0 1
0 0 1
0 0 1
-1 1
-1 1
Ответ:
0 2
0 2
Сначала проверьте пример вручную. Затем рассмотрите минимальное изображение 2 × 2: на выходе будет одно число. Проверьте также нулевой фильтр и отрицательные значения.
Это подготовленное условие; задача на платформе ещё не опубликована.
Самопроверка¶
1. Сколько чисел описывают один пиксель RGB-изображения и что они означают?¶
A. Одно число — общая яркость.
B. Три числа — красная, зелёная и синяя составляющие.
C. Два числа — высота и ширина.
D. Четыре числа — значения соседних пикселей.
2. Почему при перемещении фильтра не нужно задавать новые веса?¶
A. В каждом положении используются случайные веса.
B. Весами служат значения текущего окна.
C. Одна таблица весов применяется во всех положениях.
D. Веса нужны только для первого окна.
3. Чему равен отклик нашего фильтра на окне, в котором все значения равны 1?¶
A. 0
B. 1
C. 2
D. 4
4. В ручном примере границу сдвинули на один пиксель вправо. Какой станет верхняя строка карты?¶
A. $(2,0,0)$
B. $(0,2,0)$
C. $(2,2,2)$
D. $(0,0,2)$
5. Слой применяет к одному изображению несколько фильтров. Должны ли их карты совпасть?¶
A. Да, карты всегда совпадают.
B. Нет, разные веса могут давать разные карты.
C. Да, если размеры фильтров одинаковы.
D. Нет, размеры карт обязательно различаются.
6. Что означает шаг 2 при перемещении ядра свёртки?¶
A. Ядро содержит две строки.
B. Изображение содержит два канала.
C. При каждом перемещении ядро сдвигается на два пикселя.
D. Для каждого окна нужно два фильтра.
7. Изображение $5\times5$ дополнили рамкой из нулей толщиной в одну клетку. Какой размер карты даст фильтр $3\times3$ с шагом 1?¶
A. $5\times5$
B. $3\times3$
C. $7\times7$
D. $2\times2$
8. На строке из пяти клеток окно шириной 3 перемещается с шагом 2, без дополнения. Сколько положений допустимо?¶
A. Одно.
B. Два.
C. Три.
D. Пять.
9. Два свёрточных слоя подряд используют фильтры $3\times3$ с шагом 1. Какова область видимости одного значения второго слоя вдали от границ изображения?¶
A. $3\times3$
B. $6\times6$
C. $9\times9$
D. $5\times5$
10. Гарантирует ли применение свёрток, что сеть одинаково распознает изображение после любого поворота?¶
A. Да, при любом повороте.
B. Да, если все веса фильтра положительны.
C. Нет, поворот меняет расположение пикселей в окне.
D. Да, если изображение состоит из одного канала.
Подсказки и решения¶
Вопрос 1¶
Подсказка
Вспомните названия каналов.
Решение
B. Три числа — красная, зелёная и синяя составляющие. Вместе они задают цвет одного пикселя.
Вопрос 2¶
Подсказка
Меняется участок изображения.
Решение
C. Один фильтр использует одну таблицу весов во всех положениях. Это позволяет искать одну деталь в разных частях изображения.
Вопрос 3¶
Подсказка
Сложите два отрицательных и два положительных слагаемых.
Решение
A. $-1+1-1+1=0$. На однородном участке отклики этого фильтра равны нулю.
Вопрос 4¶
Подсказка
Проверьте три положения окна.
Решение
D. $(0,0,2)$ вместо $(0,2,0)$. Отклик переместился вслед за границей.
Вопрос 5¶
Подсказка
У фильтров могут различаться веса.
Решение
B. Нет, разные фильтры могут давать разные карты. Каждый из них реагирует на своё сочетание значений в окне.
Вопрос 6¶
Подсказка
Вспомните, чем измеряется длина сдвига ядра.
Решение
C. Ядро сдвигается на два пикселя за один шаг. Размер самого ядра при этом не меняется.
Вопрос 7¶
Подсказка
Сначала посчитайте клетки вместе с рамкой, затем — допустимые положения окна.
Решение
A. Получится карта $5\times5$. В дополненном изображении по семь клеток на каждой стороне, поэтому окно помещается в пяти положениях по горизонтали и по вертикали.
Вопрос 8¶
Подсказка
Отметьте начало окна в первой и третьей клетках.
Решение
B. Два положения. Следующее окно вышло бы за границу строки.
Вопрос 9¶
Подсказка
Участки исходного изображения для соседних значений первого слоя перекрываются.
Решение
D. Область видимости имеет размер $5\times5$. Окно второго слоя объединяет три соседних положения по каждой стороне; каждое следующее положение сдвинуто на один пиксель.
Вопрос 10¶
Подсказка
Подумайте, что изменится внутри окна.
Решение
C. Нет. При повороте расположение пикселей меняется, и те же фильтры могут дать другие отклики.