Свёртка в глубоком обучении. Обзор и примеры

Что такое свёртка?

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

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

Итак, что такое свёртка?

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

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

Покажем эффект эхо наглядно. Нужный нам аудиосигнал представлен треугольником. Не забывайте, что нам необходимо сделать две вещи – мы должны слышать наш сигнал в дальнейшем, что обычно изображается сдвигом его вправо, и этот сигнал по амплитуде должен быть ниже, чем первоначальный. Это и изображено на рисунке. Последняя операция – свести эти действия вместе.

Обратите внимание, что «ширина» сигнала остаётся неизменной, поскольку её увеличение или уменьшение привело бы к изменению высоты тона.

Как теперь выразить это математически? Мы можем представить изменение амплитуды в виде весовых коэффициентов, обозначаемых w, причём для данного конкретного эхо-фильтра мы должны убедиться, что каждый последующий коэффициент меньше предыдущего:

x(t) + w_1x (t - t_1) + w_2x (t - t_2) + ... \;,

где w1 < 1, w2 < w1 и так далее.

В общем случае уже не будет такого ограничения по весовым коэффициентам – сами коэффициенты будут определять фильтр, а саму операцию можно представить в виде суммы:

 \sum w_mx (t - t_m).

Ещё более обобщим эту операцию с помощью интеграла:

\int w(m) x (t -m) dm.

Если вы боитесь интегралов – не волнуйтесь, далее мы вернёмся к суммам, когда будем работать с изображениями, так как они дискретны. Обратите внимание, что мы можем ввести новую переменную – например, m’ вместо m:

m' = t - m/m = t - m'.

Подставив её, получим точно такое же выражение:

\int w(t - m')x(m') dm'.

Это значит, что не имеет значения, какая функция рассматривается в качестве сигнала, а какая – в качестве фильтра.

Рассмотрим теперь собственно определение свёртки, обычно обозначаемой звёздочкой. Это можно сделать как для непрерывной независимой переменной:

x(t)*w(t) = \int x(\tau)w(t - \tau) d\tau = \int w(\tau) x (t - \tau) d\tau,

так и для дискретной независимой переменной:

x[n]*w[n] = \sum x[m] w [n-m]= \sum w[m] x [n-m].

Вы можете рассматривать это как скольжение фильтра вдоль сигнала путём изменения значения m. Ещё раз подчеркну, что не имеет значения, сдвигаем ли мы фильтр вдоль сигнала или сдвигаем сигнал вдоль фильтра, поскольку результат будет один и тот же.

Одним из важнейших фильтров является дельта-функция. В непрерывном случае она определяется как производная ступенчатой функции:

\delta (t) = \frac {du}{dt}.

Ступенчатая функция определяется равной единице, если t > 0, и равной нулю, если t < 0. В точке t = 0 существует разрыв, поэтому в данной точке производная функции стремится к бесконечности. Поскольку дельта-функция является производной от ступенчатой, интеграл дельта-функции для любого t > 0 равен единице.

В дискретном случае дельта-функция принимает значение 1 в нуле, в противном случае она равна 0.

Другое название дельта-функции – импульс. Ответ в виде импульса получается, если взять свёртку между фильтром и функцией импульса:

x(t)*\delta(t)= \int x(t-\tau) \delta (\tau) d\tau = x(t),

x[n]*\delta[n]= \sum \delta [m]x[n-m] = \sum \delta [0]x[n] = x[n].

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

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

Таким образом, для каждого входящего x мы должны выводить точное значение y – выглядит как задача машинного обучения, правда?

Поскольку мы будем работать с изображениями, мы должны рассмотреть двухмерную свёртку, ведь изображения являются двухмерными сигналами. Из формулы видно, что это всего лишь две независимые свёртки, проводимые по каждому направлению:

x[n,m]*w[n,m]= \sum x[n-i, \;m-j] w [i,j].

Я написал часть псевдокода, чтобы показать, как это можно реализовать в коде:

def convolve(x, w):

 y = np.zeros(x.shape)

for n in xrange(x.shape[0]):

 for m in xrange(x.shape[1]):

 for i in xrange(x.shape[0]):

for j in xrange(x.shape[1]):

   y[n,m] += x[n-i, m-j]*w[i,j]

Обратите внимание, что тут есть одна сложность – если i больше n или j больше m, то мы выйдем за допустимый предел. Это значит, что размер Y в действительности больше, чем X. В некоторых случаях мы будем просто игнорировать дополнительные части и считать размер Y равным в точности X. Вы увидите, что при использовании Theano и TensorFlow можно управлять методом, в котором определяется размер исходящих данных.

Пример свёртки с эффектом эха

В этом разделе поговорим о том, как создать простое эхо в аудиофайле при помощи Numpy. Если вы не хотите сами писать код, а лишь посмотреть уже написанный, найдите на Github файл echo.py.

Итак, начнём с импорта необходимых библиотек – Matplotlib, чтобы выводить графики, Numpy – нам всегда нужен Numpy! – а также модули Python’а wave, позволяющий работать с wav-файлами, и sys. Кроме того, библиотека SciPy имеет модуль, позволяющий записывать массивы NumPy в wav-файл, что очень удобно, так что импортируем и его.

import matplotlib.pyplot as plt

import numpy as np

import wave

import sys

from scipy.io.wavfile import write

Прежде всего импортируем wav-файл. Вы можете воспроизвести его в своём любимом аудиопроигрывателе, чтобы услышать его содержимое.

spf = wave.open(‘helloworld.wav’, ‘r’)

Отмечу, что если вы глянете на атрибуты, или свойства, файла – вы можете сделать это в Windows, Linux и Mac правым щелчком мыши – то увидите, что его частота дискретизации 16 килогерц, а размер аудиообразца равен 16 битам.

Частота дискретизации отображает квантование звукового сигнала во времени – если в реальном мире звук непрерывен, то в компьютере он представлен в виде 16 000 фрагментов в секунду.

Второй тип квантования происходит по амплитуде. Опять же, в реальном мире звук, представляющий собой волну давления воздуха, может принимать любое действительное значение, тогда как в компьютере он представлен 16 битами, то есть 216 различными значениями.

Чтобы преобразовать сигнал в массив Numpy, нам необходимо вызвать функцию readframes, а затем вызвать функцию fromstring, указав, что сигнал закодирован в виде 16-битного целого числа. После этого выведем соответствующую информацию на экран вместе с графиком сигнала, чтобы увидеть, как он выглядит.

signal = spf.readframes(-1)

signal = np.fromstring(signal, ‘Int16’)

print(“numpy signal:”, signal.shape)

plt.plot(signal)

plt.title(“Hello world without echo”)

plt.show()

Далее мы используем свёртку с помощью дельта-функции, чтобы увидеть, как это изменит первоначальный сигнал.

delta = np.array([1., 0., 0.])

noecho = np.convolve(signal, delta)

print “noecho signal:”, noecho.shape

assert(np.abs(noecho[:len(signal)] – signal).sum() < 0.000001)

Последняя строка добавлена, чтобы вы убедились, что новый и старый сигналы различаются по размеру. Сохраним новый сигнал, чтобы воспроизвести его позже.

Обратите внимание, что после свёртки сигнал необходимо преобразовать в виде 16-битного целого числа. Сделать это нужно обязательно, иначе всё, что вы услышите, – это очень громкий шум.

noecho = noecho.astype(np.int16)

write(‘noecho.wav’, 16000, noecho)

Затем создадим фильтр для эха. Создадим фильтр длительностью 16 000 фрагментов, что представляет собой одну секунду, с уменьшающимся на каждом интервале сигналом. Повторять первоначальный сигнал будем через каждые 4 000 фрагментов – то есть каждую четверть секунды. Это и есть наше эхо.

filt = np.zeros(16000)

filt[0] = 1

filt[4000] = 0.6

filt[8000] = 0.3

filt[12000] = 0.2

filt[15999] = 0.1

out = np.convolve(signal, filt)

И не забывайте вновь преобразовать сигнал в 16-битное целое число.

out = out.astype(np.int16)

write(‘out.wav’, 16000, out)

Запустим нашу программу.

Мы видим график звукового сигнала. Обратите внимание, как изменилась длительность звука после свёртки.

Прослушаем наши файлы – первоначальный helloworld.wav, файл без эха noecho.wav и файл с эхом out.wav.

Пример свёртки с гауссовым размытием изображения

Если вы когда-либо занимались редактированием изображений в программах наподобие Photoshop, вы, вероятно, уже знакомы с фильтром размытия. Иногда его называют гауссовым размытием и скоро вы поймёте, почему. Если вы не хотите сами писать код, а лишь просмотреть уже написанный, откройте на Github файл blur.py.

Идея тут та же, что и со звуком, – мы возьмём сигнал и разложим его, только на этот раз вместо заданных задержек мы разложим сигнал в форме двухмерного гауссиана.

Итак, начнём с импорта необходимых библиотек.

import numpy as np

from scipy.signal import convolve2d

import matplotlib.pyplot as plt

import matplotlib.image as mpimg

Далее импортируем наши данные – знаменитое изображение lena.png. Оно используется практически во всех областях обработки изображений и компьютерного распознавания образов. Сразу выведем его на экран, чтобы вы могли на него посмотреть.

img = mpimg.imread(‘lena.png’)

plt.imshow(img)

plt.show()

Вот оно – знаменитое изображение Лены.

Теперь преобразуем его в чёрно-белый формат, поскольку двухмерная свёртка определена только для двухмерных матриц. Выведем его вновь на экран, чтобы убедиться, что всё правильно.

bw = img.mean(axis=2)

plt.imshow(bw, cmap=’gray’)

plt.show()

Теперь наше изображение стало чёрно-белым.

Далее создадим наш гауссов фильтр. Пусть это будет фильтр 20х20, хотя можно подставить и другие величины. Остальные вычисления сделаем тут же, вручную, после чего выведем наш фильтр на экран, чтобы посмотреть, что получилось.

W = np.zeros((20, 20))

for i in xrange(20):

    for j in xrange(20):

        dist = (i – 9.5)**2 + (j – 9.5)**2

        W[i, j] = np.exp(-dist / 50)

plt.imshow(W, cmap=’gray’)

plt.show()

Вот так выглядит двухмерное гауссово изображение.

Теперь сделаем свёртку с выводом результат на экран.

out = convolve2d(bw, W)

plt.imshow(out, cmap=’gray’)

plt.show()

Как вы можете убедится, первоначальное изображение несколько размылось.

Теперь выведем на экран форму конечных данных.

out.shape

Интересно тут то, что конечные данные могут иметь другую форму, нежели входные, но вы всегда можете подставить соответствующие параметры в функцию свёртки, чтобы сделать их одинаковыми.

Так и сделаем. Используем функцию convolve2d с дополнительным параметром mode.

out = convolve2d(bw, W, mode=’same’)

plt.imshow(out, cmap=’gray’)

plt.show()

По сути получилось то же самое, но без чёрных краёв на изображении.

Проверим размерность входных и конечных данных.

out.shape

bw.shape

Теперь немного усложним задачу и посмотрим, что получится, если запустить один и тот же фильтр по всем трём цветовым каналам. Сделаем это отдельно для каждого канала, после чего выведем результат на экран.

out3 = np.zeros(img.shape)

for i in xrange(3):

    out3[:,:,i] = convolve2d(img[:,:,i], W, mode=’same’)

plt.imshow(out3)

plt.show()

Получилось нечто непонятное.

Пример свёртки с распознаванием контура

Распознавание контуров – это ещё одна важная операция в компьютерном распознавании образов. Если вы не хотите сами писать код, а лишь посмотреть уже написанный, зайдите на Github и найдите файл edge.py.

Продолжим с уже импортированными библиотеками и с тем же изображением.

import numpy as np

from scipy.signal import convolve2d

import matplotlib.pyplot as plt

import matplotlib.image as mpimg

img = mpimg.imread(‘lena.png’)

bw = img.mean(axis=2)

Мы используем оператор Собеля. Оператор Собеля определяется для двух направлений X и Y и аппроксимирует градиент для каждой точки изображения – обозначим их через Hx и Hy. Hx определяется следующим образом:

Hx = np.array([

    [-1, 0, 1],

    [-2, 0, 2],

    [-1, 0, 1],

], dtype=np.float32)

Это всего лишь транспонированное Hx:

Hy = Hx.T

Теперь применим свёртки для двух переменных Gx и Gy, где G означает градиент, и сразу будем выводить изображение на экран.

Gx = convolve2d(bw, Hx)

plt.imshow(Gx, cmap=’gray’)

plt.show()

Это должно определить горизонтальные контуры.

Теперь вычислим Gy и выведем изображение:

Gy = convolve2d(bw, Hy)

plt.imshow(Gy, cmap=’gray’)

plt.show()

Это определяет вертикальные контуры.

Gx и Gy можно рассматривать как своего рода векторы, так что мы можем вычислить их величину и направление. Так, величина вычисляется так:

G = np.sqrt(Gx*Gx + Gy*Gy)

plt.imshow(G, cmap=’gray’)

plt.show()

Как вы можете убедиться, контуры были действительно обнаружены.

Мы также можем вычислить и вывести на экран направления градиентов:

theta = np.arctan2(Gy, Gx)

plt.imshow(theta, cmap=’gray’)

plt.show()

Итак, каковы же выводы из всех этих лекций о свёртке?

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

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

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

Share via
Copy link