Чтобы обработать, например, черно-белое изображение размером 24x24, мы должны были бы превратить матричное представление изображения в вектор, который содержит 24x24 =576 элементов. Как можно вдуматься, с таким преобразованием мы теряем важный атрибут – взаимное расположение пикселей в вертикальном и горизонтальном направлении осей, а также, наверное, в большинстве случаев пиксел, находящийся в верхнем левом углу изображения вряд ли имеет какое-то логически объяснимое влияние на пиксел в нижнем правом углу.
Для исключения этих недостатков – для обработки изображений используют сверточные слои (convolutional layer, CNN).
Основным назначением CNN является выделение из исходного изображения малых частей, содержащих опорные (характерные) признаки (features), такие как ребра, контуры, дуги или грани. На следующих уровнях обработки из этих ребер можно распознать более сложные повторяемые фрагменты текстур (окружности, квадратные фигуры и др.), которые дальше могут сложиться в еще более сложные текстуры (часть лица, колесо машины и др.).
Например, рассмотрим классическую задачу – распознавание изображения цифр. Каждая цифра имеет свой набор характерных для них фигур (окружности, линии). В тоже самое время каждую окружность или линию можно составить из более мелких ребер (рисунок 1)
1. Сверточные слои нейронной сети (convolutional layer)
Основным элементом CNN является фильтр (матричной формы), который продвигается вдоль изображения c шагом в один пиксел (ячейку) вдоль горизонтальной и вертикальной осях, начиная от левого верхнего угла и заканчивая нижним правым. На каждом шаге – CNN выполняет вычисление с целью оценки – на сколько похожа часть изображения попавшиеся в окно фильтра с паттерном самого фильтра.
Предположим, что мы имеем фильтр размерностью 2x2 (матрица K) и он делает проекцию на исходное изображение, которая обязательно тоже имеет размерность 2x2 (матрица N), тогда значение выходного слоя вычисляется следующим образом:
Мы перемножаем каждый пиксел в паттерне фильтра с соответствующим пикселем части исходного изображения, попавшего в проекцию фильтра на текущем шаге и суммируем эти значения.
Обратите внимание, что данное выражение имеет похожую форму записи операции суммирования в полносвязанные слоях (fully-connected, dense layers):
Единственным существенным отличием является то, что в полносвязанных слоях операции выполняются над векторо-подобными структурами, а в случае сверточных слоях – это матрично-подобные структуры, которые учитывают влияние близко расположенных пикселов в вертикальных и горизонтальных осях и при этом игнорируют влияние далеко расположенных пикселов (вне окна фильтра).
Общий подход к обработке в сверточном слое вы можете увидеть на рисунке 2. Обратите внимание тут, что на втором изображении сканируемая часть изображения полностью совпадает с паттерном фильтра, и ожидаемо, что значение ячейки в выходном слое будет иметь максимальное значение.
Размерность ядра фильтра (kernel size) обычно выбирают квадратной формы и с нечетным количеством элементов вдоль осей матрицы – 3, 5, 7.
Если мы имеем форму ядра фильтра (kernel) [kh, kw], а входное изображение размерностью [nh, nw], то размерность выходного сверточного слоя будет (рисунок 3):
Когда мы выбираем достаточно малое значение ядра фильтра, то потеря в размерности выходного слоя не велика. Если же это значение увеличивается, то и размерность выходного слоя уменьшается по отношению к оригинальному размеру входного изображения. Это может стать проблемой, особенно если в модели нейронной сети подключено последовательно много сверточных слоев.
Чтобы избежать потери разрешений выходных изображений, в настройках сверточных слоев используют дополнительный параметр – отступ (padding). Это расширяет исходное изображения по краям, заполняя эти ячейки нулевыми значениями. Предположим, что мы добавляем ph и pw ячеек к исходному изображению, тогда размер выходного сверточного слоя будет:
Обычно, размерность отступов задают таким образом, чтобы размерности исходного изображения и выходного сверточного слоя совпадали, тогда они могут быть вычислены следующим образом:
Как вы уже может обратили внимание - сверочный фильтр начинает свое движение с верхнего левого угла изображения и продвигается вдоль горизонтальной и вертикальной осей на одну ячейку вдоль направления движения. Однако иногда для снижения времени вычисления или же для уменьшения размерности выходного слоя, перемещение сверточного слоя может происходить вдоль направления движения больше чем на одну ячейку (stride). Это еще один из параметров сверточного слоя – шаг перемещения (stride).
Предполагая, что размер шага вдоль горизонтальной и вертикальной осей равны соответственно sw, sh, тогда размер выходного сверточного слоя составит:
Так же стоит отметить, что сверточный слой может содержать один и более фильтров (каждый фильтр – это аналог скрытого слоя с нейронами в полносвязанном слое нейронной сети). Каждый фильтр будет ответственен за извлечение из изображения своих специфичных паттернов (признаков). Представим, что на вход первого сверточного слоя (CONV1) было подано изображение размерностью 9x9x1 (изображение с одним цветовым каналом – черно-белое изображение), а сверточный слой имеет 2 фильтра с шагом перемещения ядра 1x1 (stride) и отступ (padding) подобран таким образом, чтобы выходной слой сохранял туже размерность, что и входной. Тогда размерность выходного слоя будет 9x9x2 где 2 – это количество фильтров (см. рисунок 6). На следующем шаге в CONV2 сверточном слое, обратите внимание, что при задании размерности фильтра 2x2, его глубина определяется глубиной входного слоя, которой равен 2, тогда ядро будет размерностью 2x2x2. Выходной слой после второго сверточного слоя (CONV2) тогда будет 9x9x4, где 4 – количество фильтров в сверточном слое.
Обратите тут внимание, что во всех фреймворках мы будем задавать kw и kh для фильтра, однако если при этом этот на сверточный слой было подано изображение размерностью nw x nh x nd, где nd - количество цветовых каналов изображения или же количество характеристик, извлеченных на предыдущем сверточном слое, то размерность фильтра будет kw x kh x nd (смотри рисунок 6, CONV2).
На рисунке 7 изображен подход при вычислениях, если на сверточный слой подано цветное изображение с тремя каналами RGB, а сверточный слой задан матрицей 3x3. Как было указано выше, так как глубина входного изображения равна трем (3 канала), то фильтр будет иметь размерность 3x3x3.
АПИ TensorFlow.js для сверточного слоя
Для создания сверточного слоя, необходимо воспользоваться следующим методом: tf.layers.conv2d, который принимает один аргумент – это объект параметров, среди которых важны к рассмотрению следующие: - filter – number – число фильтров в слое
- kernelSize – number | number[] – размерность фильтра, если задана number, то размерность фильтра принимает квадратную форму, если задана массивом – то высота и ширина могут отличаться
- strides – number | number[] - шаг продвижения, не обязательный параметр и по умолчанию задан как [1,1], в горизонтальном и вертикальном направлениях окно фильтра будет продвигаться на одну ячейку.
- padding – ‘same’, ‘valid’ – настройка нулевого отступа, по умолчанию ‘valid’
Рассмотрим режимы задания нулевого отступа.
Режим 'same'
Сверточный слой будет использовать нулевые отступы при необходимости, при котором выходной слой будет всегда иметь размерность, которая вычисляется делением входной ширины (длины) на шаг передвижения фильтра (stride) с округлением в большую сторону. Например, входная ширина слоя вдоль одной из осей - 11 ячеек, шаг перемещения окна фильтра – 5, тогда выходной слой будет иметь размерность 13/5=2.6, с учетом округления в большое сторону – это будет 3 (рисунок 8).
В случае если stride=1, то размерность выходного слоя будет равна размерности входного слоя (рисунок 9), фреймворк добавит столько нулевых отступов, сколько необходимо (рисунок 8).
Режим 'valid'
В этом режиме фреймворк не добавляет нулевые отступы, в котором в зависимости от strides будут игнорироваться некоторые ячейки входного изображения, как показано на рисунке 8.
Практическое использование сверточного слоя в TensorFlow.js
Давайте ближе познакомимся с АПИ и попробуем к загруженному изображению применить сверточный слой, фильтр которого извлекают из изображения вертикальные или горизонтальные линии. Эти фильтры имеют следующий вид:
- для извлечения вертикальных линий:
- для извлечения горизонтальных линий:
В первую очередь понадобится АПИ, который преобразует загруженное изображение в тензор, для этого будет использован tf.browser.fromPixels. Первым аргументом является источник изображения, это может быть указатель на img или canvas элемент в разметке.
<img src="./sources/itechart.png" alt="Init image" id="target-image"/> <canvas id="output-image-01"></canvas> <script> const imgSource = document.getElementById('target-image'); const image = tf.browser.fromPixels(imgSource, 1); </script>
Далее, соберем модель, которая состоит из одного сверточного слоя, содержащей один фильтр размерностью 3x3, с режимом отступа “same” и активационной функцией ‘relu’:
const model = tf.sequential({ layers: [ tf.layers.conv2d({ inputShape: image.shape, filters: 1, kernelSize: 3, padding: 'same', activation: 'relu' }) ] });
Данная модель в качестве входных параметров принимает тензор формой [NUM_SAMPLES, WIDTH, HEIGHT,CHANNEL], однако tf.browser.fromPixel возвращает тензор размерностью [WIDTH, HEIGHT, CHANNEL], то поэтому нам надо модифицировать форму тензора и добавить еще одну ось – там где располагаются образцы изображений (в нашем случае будет только один, так как одно изображение):
const input = image.reshape([1].concat(image.shape));
По умолчанию значения весов фильтра в слое будет инициализироваться в произвольном порядке. Чтобы задать собственные веса, воспользуемся методом setWeights класса Layer, нужный слой может быть получен у экземпляра модели по его индексу:
model.getLayer(null, 0).setWeights([ tf.tensor([ 1, 1, 1, 0, 0, 0, -1, -1, -1 ], [3, 3, 1, 1]), tf.tensor([0]) ]);
Далее пропустим исходное изображение через модель, а также нормализуем выходной тензор таким образом, чтобы все значения были в промежутке 0-255, а также уберем ось NUM_SAMPLES:
const output = model.predict(input); const max = output.max().arraySync(); const min = output.min().arraySync(); const outputImage = output.reshape(image.shape) .sub(min) .div(max - min) .mul(255) .cast('int32');
Чтобы отобразить тензор на плоскости canvas, воспользуемся функцией tf.browser.toPixels:
tf.browser.toPixels(outputImage, document.getElementById('output-image-01'));
Тут приведен результат работы с применением двух разных фильтров:
2. Подвыборочный слой (pooling layer)
Обычно для обработки изображения, последовательно подключается несколько сверточных слоев (обычно это десять или даже сотня слоев), при этом каждый сверточный слой увеличивает размерность выходного изображения, что в свою очередь приводит к увеличению времени обучения такой сети. Для того, чтобы его снизить, после сверточных слоев применяют подвыборочный слой (pooling layer, subsample layer), которые уменьшают размерность выходного изображения. Обычно применяется MaxPooling операция.
Данный слой также снижает чувствительность к точному пространственному расположению признаков изображения, потому что например любая написанная цифра от руки может быть смещена от центра абсолютно в любом направлении.
В целом преобразования в этом слое похожи на преобразования в сверточном слое. Этот слой также имеет ядро (kernel) определенного размера, который скользит вдоль изображения с задаваемым шагом (stride) и в отличии от сверточного слоя в фреймворках по умолчанию он равен не 1x1, а равен размеру задаваемого ядра. В выходном изображении, каждая ячейка будет содержать максимальное значение пикселов в проекции ядра на определённом шаге (см. рисунок 10).
Как видим, при исходном изображении размерностью 4x4, подвыборочный слой с размерностью фильтра 2x2 и размерностью шага (stride) по умолчанию, равному размерностью фильтра 2x2, выходное изображение имеет размерность в два раза меньше входного.
Также покажем наглядно, что этот слой сглаживает пространственные смещения (рисунок 11) во входном слое. Обратите внимание, что на втором рисунке изображение смещено на один пиксел влево относительно первого изображения, тем не менее выходные изображения для обоих случаев после MaxPooling слоев идентичные. Это еще называют пространственной инверсией (translation invariance). На третьем рисунке, выходной слой уже не идентичен первым двум, но тем не менее пространственная инверсия составляет 50%. Тут в примере рассматривались смещения вдоль вертикальной, горизонтальных осях, но MaxPooling также толерантен к небольшим вращением изображениям также.
В некоторых источниках преобразование подвыборочного слоя сопоставляют с работой программ по сжатию изображений с целью снизить размер файла, но при этом не потеряв важные характеристики изображения.
Кстати, тут ради справедливости стоит сказать, что некоторые источники предлагают не использовать этот тип слоя вовсе и предлагают для снижения разрешения выходного изображения – манипулировать значением шага перемещения фильтра в сверточном слое (stride).
Также помимо MaxPooling иногда используется AveragePooling, с той лишь разницей, что в этом случае считается среднее взвешенное значение пикселей, попавшие в проекции фильтра. Этот фильтр ранее был популярным, но сейчас все же отдают предпочтение MaxPooling. С одной стороны AveragePooling теряет меньше информации, так как учитывает средневзвешенное значение всех пикселов, в тоже время MaxPooling выкидывает из рассмотрения менее значимые пикселы в окне.
АПИ TensorFlow.js для подвыборочного слоя (pooling layer)
В зависимости от слоя вы можете выбрать или tf.layers.maxPooling2d или tf.layers.averagePooling2d. Оба метода имеют одинаковую сигнатуру и принимают один аргумент – объект параметров, среди которых важны к рассмотрению следующие:
- poolSize – number | number[] – размерность фильтра, если задана number, то размерность фильтра принимает квадратную форму, если задана массивом – то высота и ширина могут отличаться
- strides – number | number[] - шаг продвижения, не обязательный параметр и по умолчанию имеет туже размерность, что и заданный poolSize.
- padding – ‘same’, ‘valid’ – настройка нулевого отступа, по умолчанию ‘valid’