Напишите свёртку самостоятельно

Здравствуйте и добро пожаловать на занятия по теме «Свёрточные нейронные сети: глубокое обучение на языке Python, часть 3».

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

Я уже наводил псевдокод, чтобы вы могли написать собственную свёртку, так что вы на 100% в состоянии справиться самостоятельно – ведь это всего лишь ряд циклов:

n1, n2 = X.shape

m1, m2 = W.shape

for i in xrange(n1):

    for j in xrange(n2):

        for ii in xrange(m1):

            for jj in xrange(m2):

                y[i,j] = w[ii,jj]*x[i – ii,j – jj]

Я настоятельно рекомендую сначала попробовать сделать всё самостоятельно, и лишь если у вас действительно не будет получаться, вернуться к видео.

Итак, первое, что мы должны обсудить, – каким должен быть размер исходящего сигнала? Обычно когда мы говорим о свёртке, мы говорим о суммах от минус бесконечности до плюс бесконечности. Но, разумеется, мы не можем хранить бесконечно длинный сигнал. Мы знаем, что наш входной сигнал и наш фильтр должны быть конечной величины. Таким образом, вопрос в следующем: можем ли мы определить размер исходящих данных, учитывая размер входящих. И ответ, конечно же, – да. Фокус в том, чтобы рассматривать входные и исходящие данные как бесконечные, хотя в действительности размер каждого сигнала означает просто количество фрагментов в сигнале, не равное нулю.

Один из способов понять это – через картинки. Не забывайте, что мы пытаемся сдвигать фильтр вдоль сигнала. Исходя из этого, мы можем сказать, что ненулевые значения находятся в пределах величины N + M, где N – длина сигнала, а M – длина фильтра. Обратите внимание, что сейчас мы рассматриваем одномерный сигнал – сугубо для того, упростить наглядное представление.

Другой способ – рассмотреть уравнение свёртки и записать результат вручную. Это чисто механическая работа, требующая лишь умения складывать и умножать, что, надеюсь, вы уже вполне умеете.

Рассмотрим простой сигнал, входной размер которого равен 5, а принимаемые значения могут быть произвольными. Поскольку входной размер равен 5, то действительными будут только индексы от 0 до 4 включительно. Размер фильтра пусть будет равным 3, так что действительными будут только индексы от 0 до 2 включительно.

Вычислим y[0]:

y [i] = \sum_J w [j] x [i-j],

y[0] = x [0-0] + x [0-1] + x [0-2] = x[0] + x[-1] + x[-2] = x[0].

Поскольку определено только значение x[0], то и результат будет равен x[0] – не забывайте, что всё, что находится вне диапазона от x[0] до x[4] равно нулю.

Вычислим теперь y[1]:

y[1] = x [1-0] + x [1-1] + x [1-2] = x[1] + x[0] + x[-1] = x [1] + x[0].

Таким образом, теперь у нас два значения x.

Теперь вычислим y[2]:

y[2] = x [2-0] + x [2-1] + x [2-2] = x[2] + x[1] + x[0].

В этом случае все значения x определены.

Этот ряд можно продолжать до y[5], которое начинает зависеть от x[5], которое, в свою очередь, опять-таки равно нулю, и до y[7], которое уже полностью равно нулю:

y[3] = x [3] + x [2] + x [1],

y[4] = x [4] + x [3] + x [3],

y[5] = x [5] + x [4] + x [3] = x[4] + x[3],

y[6] = x [6] + x [5] + x [4] = x[4],

y[7] = x [7] + x [6] + x [5] = 0.

Таким образом, как вы можете убедиться, лишь значения от y[0] до y[6] могут иметь ненулевые значения. Всё остальное следует считать равным нулю, но сугубо для полноты картины вычислим и y[-1], чтобы показать, что и оно равно нулю:

y[-1] = x [-1] + x [-2] + x [-3] = 0.

Таким образом, величины от y[0] до y[6] имеют вероятные ненулевые значения, а всё остальное равно нулю. Размер нашего y составляет 7, что равно 5 + 3 – 1. Этот ответ совпадает с предыдущим примером, когда результат должен был находиться в пределах N + M.

Переведём теперь всё это в программный код.

Я уже кое-что написал – просто взял код из нашего примера с размытием, заключил в комментарии импортировавшуюся ранее функцию convolve2d и написал несколько своих вариантов реализации этой функции. Если вы хотите просмотреть код без самостоятельного его написания, зайдите в репозитарий и найдите файл custom_blur.py.

Итак, первое, что я сделал, – это прямую реализацию свёртки согласно её определению. Как вы сможете убедиться, она работает куда медленнее, чем соответствующая функция SciPy, поскольку содержит четыре цикла. Не забывайте, что вы должны стремиться векторизовать свой код где только можно, поэтому в данной версии у нас есть n1 и n2, являющиеся размерностями x, и m1 и m2, являющиеся размерностями фильтра. В данном случае мы используем непосредственное уравнение свёртки без какой-либо оптимизации.

import numpy as np

# from scipy.signal import convolve2d

import matplotlib.pyplot as plt

import matplotlib.image as mpimg

from datetime import datetime

 

def convolve2d(X, W):

    t0 = datetime.now()

    n1, n2 = X.shape

    m1, m2 = W.shape

    Y = np.zeros((n1 + m1 – 1, n2 + m2 – 1))

    for i in xrange(n1 + m1 – 1):

        for ii in xrange(m1):

            for j in xrange(n2 + m2 – 1):

                for jj in xrange(m2):

                    if i >= ii and j >= jj and i – ii < n1 and j – jj < n2:

                        Y[i,j] += W[ii,jj]*X[i – ii,j – jj]

    print “elapsed time:”, (datetime.now() – t0)

    return Y

Запустим программу и посмотрим, сколько это займёт времени.

Итак, у нас есть первоначальное изображение, чёрно-белое изображение и фильтр. Теперь подождём результата.

Мы получили размытое изображение, что заняло почти шесть минут.

Теперь заметьте, что существует лёгкая возможность оптимизировать программу. Мы знаем, что два цикла из числа наведенных лишь умножают x[i,j] на каждую часть w и добавляют результат к суб-изображению Y. Это, в свою очередь, является лишь скалярным умножением матриц, что очень легко реализовать в Numpy. Рассмотрим функцию, реализующую такой вариант:

def convolve2d(X, W):

    t0 = datetime.now()

    n1, n2 = X.shape

    m1, m2 = W.shape

    Y = np.zeros((n1 + m1 – 1, n2 + m2 – 1))

    for i in xrange(n1):

        for j in xrange(n2):

            Y[i:i+m1,j:j+m2] += X[i,j]*W

    print “elapsed time:”, (datetime.now() – t0)

    return Y

Таким образом, теперь у нас лишь два цикла вместо четырёх, так что программа будет работать быстрее. Попробуем запустить её ещё раз.

Имеем первоначальное изображение, чёрно-белое, фильтр и размытое изображение. Обратите внимание, насколько быстрее выполнилась программа – всего за 2,2 секунды.

Вы также могли заметить чёрную кромку по краю изображения. Как вы помните, когда мы вручную делали свёртку, на краях изображения мы брали всего по несколько пикселей из x. Почему так? Потому что согласно уравнению свёртки, остальные исходящие данные относятся к отрицательным индексам x, которые считаются равными нулю и потому не учитываются. Получается, что при этом умножается лишь малое число значений из первоначального изображения на малое число значений w, а результат оказывается очень близким к нулю, что соответствует чёрному цвету. Theano и TensorFlow позволяют сделать конечный результат свёртки того же размера, что и входные данные, и мы используем эту возможность позже при построении свёрточной нейронной сети, а сейчас давайте попробуем сделать это самостоятельно.

Уберём в комментарии второй вариант функции и попробуем третий. Попытайтесь самостоятельно удостовериться, что переход от M/2 до -M/2 + 1 от первоначальных исходящих данных даст в результате тот же размер, что и размер входных данных.

# same size as input

def convolve2d(X, W):

    n1, n2 = X.shape

    m1, m2 = W.shape

    Y = np.zeros((n1 + m1 – 1, n2 + m2 – 1))

    for i in xrange(n1):

        for j in xrange(n2):

            Y[i:i+m1,j:j+m2] += X[i,j]*W

    ret = Y[m1/2:-m1/2+1,m2/2:-m2/2+1]

    assert(ret.shape == X.shape)

    return ret

Запустим этот вариант и посмотрим, что получится.

Опять видим первоначальное изображение, опять чёрно-белое, опять фильтр и, наконец, отфильтрованное изображение. Как мы видим, чёрные области в основном исчезли, но некоторая их часть всё же осталась.

Есть одно интересное обстоятельство: по умолчанию Theano делает так называемую «узкую» свёртку, когда конечный результат в действительности меньше, чем входные данные. Точная длина исходящих данных при заданном размере входных данных N и фильтре размера M будет равняться NM + 1. Попробуем проделать то же самое, ориентируясь на центр первоначального изображения.

Обратимся к последнему варианту функции convolve2d. Убедитесь самостоятельно, что переход от M – 1 до –M + 1 в первоначальных исходящих данных даёт нам новые исходящие данные размером NM + 1.

# smaller than input

def convolve2d(X, W):

    n1, n2 = X.shape

    m1, m2 = W.shape

    Y = np.zeros((n1 + m1 – 1, n2 + m2 – 1))

    for i in xrange(n1):

        for j in xrange(n2):

            Y[i:i+m1,j:j+m2] += X[i,j]*W

    ret = Y[m1-1:-m1+1,m2-1:-m2+1]

    return ret

Запустим этот вариант и посмотрим на результат.

Первоначальное изображение, чёрно-белое, фильтр… И размытое изображение. Как вы можете видеть, в этой версии на исходящем изображении нет чёрной кромки. Так что если вы удивлялись несколько странному размеру исходящих данных в Theano, это, надеюсь, всё объясняет.

Понравилась статья? Поделить с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: