Використовуємо CNN для обробки зображень. Частина друга

💡 Усі статті, обговорення, новини про AI — в одному місці. Приєднуйтесь до AI спільноти!

Привіт усім! Мене звуть Олексій, я працюю Machine Learning Engineer у компанії Svitla Systems. У попередній статті «Використовуємо CNN для обробки зображень» я описав мотивацію розробки CNN, історію їхнього виникнення, різні варіанти CNN та сучасний стан цих мереж. Однак багато аспектів залишаються незрозумілими без глибокого розуміння математики, що стоїть за CNN.

У цій статті розглянемо більш детально процес роботи фільтрів для зображень і утворення згорткових шарів. Я сподіваюсь, що це допоможе вам краще зрозуміти, як CNN використовуються для обробки зображень та покращить ваші навички.

Також нижче будуть приклади коду та алгоритму роботи згорткових нейронних мереж. Ви можете переписати/скопіювати його самостійно для власних потреб, або для зручності використати вже реалізований код в Google Colab.

Математика операції згортки

Основним елементом, що відрізняє CNN від інших типів нейронних мереж, є операція згортки — потужний інструмент, який дозволяє мережі ефективно виявляти корисні ознаки на зображеннях незалежно від їхнього положення.

Згортка в CNN — це процес застосування фільтра (або «ядра згортки») до зображення. Фільтр — це невелика матриця, яку ми перемножуємо з пікселями зображення. Він рухається по зображенню, розглядаючи невеликі ділянки зображення по черзі, що дозволяє аналізувати локальні ознаки.

Приклад операції згортки, де на вхідне зображення (Input) накладається фільтр (Kernel), що означає що їхнє значення перемножуються та складаються разом, утворюючи нові значення для наступного зображення (Output).

Формально процес згортки ми можемо визначити наступним чином:

Де n2 — це половина висоти фільтра, m2 — це половина довжини фільтра, x — позиція стовпчика певного пікселя на зображенні, y — позиція рядка певного пікселя на зображенні.

Так само з визначенням

вище: n — висота фільтра, а m — довжина фільтра h.

За приклад візьмемо зображення I, та фільтр h, які визначені нижче — ми можемо застосувати формулу згортки для них.

Щоб застосовувати згортку, потрібно визначити ще кілька деталей щодо того, як фільтр буде пересуватись по вхідному зображенню. Перше значення, яке потрібно визначити, це крок того, на скільки пікселів будуть переміщатися значення x та y на зображенні. Цей параметр називають stride.

Зображення ліворуч показує переміщення фільтра на один крок, тобто stride=1. На зображенні ліворуч stride=2, тому зелена ділянка для обробки знаходиться на два пікселі праворуч від червоної. Зверніть увагу, що чим більше значення stride, тим менший розмір зображення ми отримаємо.
Джерело: Analytics Vidhya

Друге значення — це кількість додаткових рядків та стовпчиків поза зображенням, або padding. Вони використовуються для того, щоб визначити, скільки інформації з зображення ми хочемо зберегти після згортки.

Вхідне зображення має додані рядки та стовпчики заповнені нулями, тобто padding=1, таstride=1, тому зображення на виході фактично не відрізняється від зображення на вході.
Джерело: Analytics Vidhya

Як ви можете помітити з прикладів вище, розмір та значення в результаті можуть відрізнятись залежно від цих двох параметрів. Тому задля простоти ми будемо враховувати, що значення для параметру stride=1, а для padding=0. Згідно з цими значеннями, ми можемо порахувати розмір вихідного зображення для матриці I, яку ми визначили раніше. Оскільки її розмір 4×4, а фільтр, що будемо до неї застосовувати, розміром 3×3, то вихідна матриця буде мати розміри 2×2. Це значення можна вирахувати за формулою нижче:

Де nin, min — висота та довжина вхідного зображення, nout, mout — висота та довжина вихідного зображення.

Отже, маючи всі необхідні дані, можемо почати згортку, яка буде виконана для елементів I11, I12, I21, I22 так, як це зображено на малюнку нижче:

Нагадаю, що значення для матриці I та матриці h наступні:

Знайдемо значення згортки для елемента Ix=1, y=1 та фільтром h:

Таким чином, ми можемо з упевненістю сказати, що перший елемент дорівнює 12. Відповідно для елементу з координатами x=2, y=1 будемо мати наступний результат:

Таким чином, наступний елемент має значення 21.

Зробивши ту ж саму операцію для елементів Iy=2, x=1, Iy=2, x=2, ми можемо визначити, що їхнє значення дорівнюють 15 та 24 відповідно. В результаті згортки ми отримаємо наступну матрицю:

Далі наводиться приклад коду з реалізацією цього алгоритму згортки на Python:

!wget -O "Lenna_(test_image).png" https://static.wikia.nocookie.net/computervision/images/3/34/Lenna.jpg/revision/latest?cb=20050408081934
# Імпортування необхідних бібліотек
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Функція згортки
def convolve(img, filter_, k, x, y):
    sum = 0
    # Визначаємо початкову точку ядра
    k_start = int(-np.floor(k/2))
    # Проводимо згортку: перемножуємо елементи зображення та фільтра та сумуємо
    for i in range(k):
        for j in range(k):
            a = img[y+i+k_start][x+j+k_start] * filter_[i][j]
            print(f"i[{y+i+k_start},{x+j+k_start}]*f[{i},{j}]={a}  ", end="\t")
            sum += a
        print("")
    # Повертаємо результат згортки для даної точки
    return sum
# Розмір ядра
k = 3
# Фільтр для згортки
filter_ = [
    [0, 1, 0],
    [0, 1, 0],
    [0, 1, 0],
]
# Вхідне зображення
img = [
    [0, 3, 6, 9 ],
    [1, 4, 7, 10],
    [2, 5, 8, 11],
    [3, 6, 9, 12]
]
# Визначаємо висоту і ширину зображення
height = len(img)
width = len(img[0])
# Визначаємо границі, в межах яких буде проводитися згортка
row_start = int(np.floor(k/2))
row_end = height-k+2
col_start = int(np.floor(k/2))
col_end = width-k+2
# Створюємо нове зображення, заповнене нулями, розміром менше вихідного на розмір ядра
new_image = np.zeros((height-k+1, width-k+1))
for y in range(row_start, row_end):
    for x in range(col_start, col_end):
        # Проводимо згортку для кожної точки в області визначеної границями
        new_image[y-row_start][x-col_start] = convolve(img, filter_, k, x, y)
        print(f"process ({x=}, {y=}) image point\n")
# Виводимо результат згортки
new_image

Як ви можете побачити з результатів вище, наші розрахунки цілком збігаються з алгоритмом програми.

Згортка для чорно-білих зображень

Тепер, маючи уявлення про те, що таке згортка, як вона працює та реалізовується, ми можемо її застосувати для реального зображення:

# Функція згортки
def convolve(img, filter_, k, x, y):
    sum = 0
    # Визначаємо початкову точку ядра
    k_start = int(-np.floor(k/2))
    # Проводимо згортку: перемножуємо елементи зображення та фільтра та сумуємо
    for i in range(k):
        for j in range(k):
            sum += img[y+i+k_start][x+j+k_start] * filter_[i][j]
    # Повертаємо результат згортки для даної точки
    return sum
# Функція згортки для всього зображення
def convolve_image(img, filter_):
    # Визначаємо розмір ядра
    k = filter_.shape[0]
    # Визначаємо границі, в межах яких буде проводитися згортка
    row_start = int(np.floor(k/2))
    row_end = height-k+2
    col_start = int(np.floor(k/2))
    col_end = width-k+2
    # Створюємо нове зображення, заповнене нулями, розміром менше вихідного на розмір ядра
    new_image = np.zeros((height-k+1, width-k+1))
    for y in range(row_start, row_end):
        for x in range(col_start, col_end):
            # Проводимо згортку для кожної точки в області визначеної границями
            new_image[y-row_start][x-col_start] = convolve(img, filter_, k, x, y)
    return new_image
# Завантажуємо зображення
img = cv2.imread('Lenna_(test_image).png', 0)
height, width = img.shape
# Створюємо фігуру з двома графіками
fig, axs = plt.subplots(ncols=2, nrows=1, figsize=(14, 6))
# Визначаємо розмір та тип фільтра
"""
При k = 3 фільтр буде виглядати наступним чином:
[[0, 1, 0],
 [0, 1, 0],
 [0, 1, 0]],
"""
k = 15
filter_ = np.zeros((k, k), dtype=int)
filter_[:, k // 2] = 1
# Проводимо згортку зображення
new_image = convolve_image(img, filter_)
# Відображаємо оригінальне та згорнуте зображення
axs[0].imshow(img, cmap='gray')
axs[1].imshow(new_image, cmap='gray')
# Відображаємо графіки
plt.show()

В прикладі вище я обрав розмір для фільтра 15, щоб краще відобразити різницю між оригінальним зображенням та тим, що пройшло згортку. Тому ми маємо результат нижче:

Оскільки зображення на виході менше, ніж зображення на вході, то ми можемо бачити на зображені праворуч чорні смуги (знизу і справа), які насправді є нулями.

Цей код виконує згортку зображення, використовуючи вертикальний 2D-фільтр (фільтр Собеля). Фільтр Собеля використовується для виявлення країв на зображенні.

Цей фільтр в основному виявляє вертикальні краї, оскільки він має ненульові значення в середньому стовпці. Таким чином, після застосування цього фільтра до зображення, вертикальні краї будуть більш виділені.

Результатом виконання цього коду буде два зображення: оригінальне зображення та зображення після згортки. Зображення після згортки буде відображати виявлені краї на оригінальному зображенні.

В прикладі нижче я вирішив показати й інші способи накладання різновидів цього фільтру, щоб у вас краще склалось враження про роботу цього алгоритму.

def convolve(img, filter_, k, x, y):
    """
    Оптимізована функція згортки зображень
    """
    # Визначаємо початкову точку ядра
    k_start = int(-np.floor(k/2))
    # Вирізаємо фрагмент зображення розміром з ядро
    img_slice = img[y+k_start:y+k_start+k, x+k_start:x+k_start+k]
    # Перемножуємо фрагмент зображення та ядро і сумуємо результат
    return np.sum(np.multiply(img_slice, filter_))
# Завантажуємо зображення
img = cv2.imread('Lenna_(test_image).png', 0)
img.shape
# Розмір ядра
k = 25
# Filter 1
"""
[[0, 1, 0],
 [0, 1, 0],
 [0, 1, 0]],
"""
filter1 = np.zeros((k, k), dtype=int)
filter1[:, k//2] = 1
# Filter 2
"""
[[0, 0, 0],
 [1, 1, 1],
 [0, 0, 0]],
"""
filter2 = np.zeros((k, k), dtype=int)
filter2[k//2, :] = 1
# Filter 3
"""
[[1, 0, 0],
 [0, 1, 0],
 [0, 0, 1]],
"""
filter3 = np.zeros((k, k), dtype=int)
np.fill_diagonal(filter3, 1)
# Filter 4
"""
[[0, 0, 1],
 [0, 1, 0],
 [1, 0, 0]],
"""
filter4 = np.zeros((k, k), dtype=int)
np.fill_diagonal(np.fliplr(filter4), 1)
# Filter 5
"""
[[1, 1, 1],
 [1, 0, 1],
 [1, 1, 1]],
"""
filter5 = np.ones((k, k))
if k > 2:
    filter5[1:-1, 1:-1] = 0
# Список з усіма фільтрами
filters = [filter1, filter2, filter3, filter4, filter5]
# Назви фільтрів
filter_names = ['|', '—', '\\', '/', '□' ]
# Створюємо фігуру для відображення графіків
fig, axs = plt.subplots(ncols=3, nrows=2, figsize=(14, 9))
# Відображаємо оригінальне зображення
axs[0, 0].imshow(img, cmap='gray')
axs[0, 0].set_title('Original Image')
# Застосовуємо кожен фільтр до зображення і відображаємо результат
for i, filter_ in enumerate(filters):
    new_image = convolve_image(img, filter_)
    axs[(i+1)//3, (i+1)%3].imshow(new_image, cmap='gray')
    axs[(i+1)//3, (i+1)%3].set_title(f'Filter {i+1}, {filter_names[i]}')
# Показуємо графіки
plt.show()

Приклад застосування вертикальних, горизонтальних та нахилених ліній до зображення.

Цей код застосовує різні фільтри до вхідного зображення і візуалізує результати.

Filter 1 (|): цей фільтр виділяє вертикальні лінії на зображенні. Результатом буде зображення, де вертикальні лінії більш виділені.

Filter 2 (—): цей фільтр виділяє горизонтальні лінії на зображенні. Результатом буде зображення, де горизонтальні лінії більш виділені.

Filter 3 (\): цей фільтр виділяє діагональні лінії, які йдуть з лівого верхнього кута до правого нижнього кута. Результатом буде зображення, де такі діагональні лінії більш виділені.

Filter 4 (/): цей фільтр виділяє діагональні лінії, які йдуть з правого верхнього кута до лівого нижнього кута. Результатом буде зображення, де такі діагональні лінії більш виділені.

Filter 5 (□): цей фільтр виділяє краї об’єктів на зображенні. Результатом буде зображення, де краї об’єктів більш виділені.

Як бачите, різні фільтри можуть отримувати свої ознаки, саме ця концепція лежить в основі побудови сучасних згорткових нейронних мереж.

Пулінг для чорно-білих зображень

Ми розглянули математичний та алгоритмічний аспекти роботи першої частини згорткових мереж, а саме операцію згортки. Наступною задачею є формулювання математичного та алгоритмічного опису роботи другої частини згорткових мереж, тобто операції пулінгу.

Розглянемо, наприклад, операцію максимізації, або макс-пулінгу, яку можна сформулювати наступним чином:

Де:

  • X — вхідна матриця;
  • I та J — індекси вікна пулінгу;
  • Y — вихідна матриця після пулінгу.

Формула бере максимум від усіх елементів вікна пулінгу в X і присвоює це значення відповідному елементу в Y.

Отже, ми розглядаємо кожне вікно вхідного зображення розміром I x J, що починається з позиції (i,j), і вибираємо максимальне значення з цього вікна.

Тепер розгляньмо приклад, як це працює. Наприклад, ми маємо матрицю I, яка виглядає наступним чином:

Тепер можемо застосувати до неї операцію пулінгу. За приклад використаємо вікно пулінгу 2 та stride, тобто крок вікна зі значенням один. Тоді ділянки, для яких буде використана операція макс-пулінгу, будуть виглядати наступним чином:

Як бачимо на зображенні вище, в результаті обробки ми отримаємо матрицю 2×3. Таким чином, матриця Y буде виглядати так:

Реалізуючи цю логіку в алгоритмі після використання згортки ми можемо побачити більш чітке виділення сильних ознак зображення, що демонструється кодом нижче.

def max_pooling(img, k, x, y):
    """
    Функція для виконання макс-пулінгу на вхідному зображенні img
    з ядром розміром k та вихідним значенням x, y
    """
    # Визначаємо початкову точку для ядра
    k_start = int(-np.floor(k/2))
    # Отримуємо зображення з діапазону y+k_start:y+k_start+k, x+k_start:x+k_start+k
    img_slice = img[y+k_start:y+k_start+k, x+k_start:x+k_start+k]
    # Повертаємо максимальне значення цього зображення
    return np.max(img_slice)
def max_pooling_image(img, k, stride=1):
    """
    Функція для виконання макс-пулінгу на всьому зображенні img
    з ядром розміром k та кроком stride
    """
    height, width = img.shape
    # Визначаємо початкові та кінцеві точки для рядків та стовпців
    row_start = int(np.floor(k/2))
    row_end = height-k+2
    col_start = int(np.floor(k/2))
    col_end = width-k+2
    # Ініціалізуємо зображення після пулінгу
    pooled_image = np.zeros((
        np.floor((height-k)/stride).astype("int") + 1,
        np.floor((width-k)/stride).astype("int") + 1
    ))
    # Ітеруємо через кожний піксель зображення
    for i, y in enumerate(range(row_start, row_end, stride)):
        for j, x in enumerate(range(col_start, col_end, stride)):
            # Виконуємо макс-пулінг для поточного вікна
            pooled_image[i][j] = max_pooling(img, k, x, y)
    # Повертаємо зображення після пулінгу
    return pooled_image
# Вхідне зображення
img = np.array([
    [0, 3, 6, 9 ],
    [1, 4, 7, 10],
    [2, 5, 8, 11]
])
height, width = img.shape # Отримуємо розмір вхідного зображення
# Розмір ядра
k=2
# Виконуємо макс-пулінг зображення
pooled_image = max_pooling_image(img, k)
pooled_image # Виводимо зображення після пулінгу

В результаті виконання цього коду отримаємо результат на зображені нижче.

Тепер можемо подивитись, який результат дасть комбінація розглянутих вище функцій, тобто поєднання операції згортки та операції пулінгу для обробки чорно-білих зображень.

# Завантажуємо зображення в чорно-білому форматі
img = cv2.imread('Lenna_(test_image).png', 0)
# Отримуємо розміри завантаженого зображення
height, width = img.shape
# Створюємо об'єкт фігури для відображення зображень
fig, axs = plt.subplots(ncols=3, nrows=1, figsize=(14, 4))
# Встановлюємо розмір ядра для згортки
k_convolve = 15
# Створюємо фільтр для згортки
filter_ = np.zeros((k_convolve, k_convolve), dtype=int)
filter_[:, k // 2] = 1
# Виконуємо згортку зображення зі створеним фільтром
new_image = convolve_image(img, filter_)
# Встановлюємо розмір ядра для пулінгу
k_pooling = 10
# Виконуємо операцію пулінгу над зображенням після згортки
pooled_image = max_pooling_image(new_image, k_pooling, k_pooling)
# Відображаємо оригінальне зображення
axs[0].imshow(img, cmap='gray')
# Відображаємо зображення після згортки
axs[1].imshow(new_image, cmap='gray')
# Відображаємо зображення після пулінгу
axs[2].imshow(pooled_image, cmap='gray')
# Виводимо всі зображення
plt.show()

Виконуючи цей код, отримаємо наступний результат:

На ілюстрації ми бачимо, що перше зображення є оригіналом, друге — оброблене за допомогою згортки, та третє після застосування функції макс-пулінгу.

Як видно з останнього зображення, яке пройшло макс-пулінг, розмірність зображення значно зменшилась через те, що параметр ядра та відступу для операції пулінгу було обрано значення 10. Таким чином, ми виділили ті ознаки, які прорахував фільтр згортки, та склали з них менш деталізоване зображення. Проте, маючи декілька подібних фільтрів для згортки, які отримували не тільки вертикальні, а й горизонтальні ознаки, ми би зберегли більше інформації для подальшої обробки.

Згортка та пулінг для кольорових зображень

Всі приклади, які я продемонстрував раніше, стосувались тільки чорно-білих зображень. Тепер ми розглянемо, як ці операції працюють з кольоровими RGB-зображеннями. Для чорно-білих зображень ми використовували операцію згортки та робили її для певних позицій на зображенні.

Згортку для всього одноканального зображення ми можемо описати як I * h. Згортка для багатоканального зображення буде виглядати наступним чином:

Де Iin — вхідне зображення, Iout — вихідне зображення, Cin — кількість каналів у вхідному зображенні, c — індекс каналу зображення, у випадку RGB, це будуть червоний, зелений або синій канал.

Тобто згідно з формулою вище, передавши на вхід багатоканальне зображення, в результаті ми отримуємо одноканальне зображення. У випадку з кольоровим RGB-зображенням на виході буде чорно-біле зображення. Саме це ми можемо побачити в результаті реалізації цієї формули.

def convolve_RGB_image(img, filter_):
    height, width, channels = img.shape
    # Ініціалізуємо вихідне зображення
    new_image = np.zeros((height, width), dtype=np.float64)
    # Застосовуємо згортку
    for y in range(k//2, height - k//2):
        for x in range(k//2, width - k//2):
            for c in range(channels):
                new_image[y, x] += convolve(img[:,:,c], filter_, k, x, y)
    # Нормалізуємо зображення до 8-бітового формату
    new_image = (new_image / np.max(new_image) * 255).astype('uint8')
    return new_image
# Завантажуємо зображення
img_original = cv2.imread('Lenna_(test_image).png')
img_original_RGB = cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)
# Ініціалізуємо фільтр
k = 25
filter_ = np.zeros((k, k), dtype=int)
filter_[:, k // 2] = 1
new_image = convolve_RGB_image(img_original_RGB, filter_)
# Показуємо оригінальне та згорнуте зображення
fig, axs = plt.subplots(ncols=2, nrows=1, figsize=(14, 6))
axs[0].imshow(img_original_RGB)
axs[0].set_title('Оригінальне зображення')
axs[1].imshow(new_image, cmap="gray")
axs[1].set_title('Зображення після згортки')
plt.show()

В прикладі вище функція convolve_RGB_image містить нормалізацію зображення, яка відбувається для того, щоб отримане одноканальне зображення можна було відобразити в чорно-білому спектрі й отримати наступний результат.

Результат виконання коду, коли кольорове зображення після виконання операції згортки перетворюється в чорно-біле.

Тепер можемо поєднати алгоритм згортки з алгоритмом пулінгу.

Де I0 — вхідне зображення, I1 — зображення після згортки по всім вхідним каналам, I2 — зображення після макс-пулінгу.

# Завантажуємо зображення
img_original = cv2.imread('Lenna_(test_image).png')
img_original_RGB = cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)
# Ініціалізуємо фільтр
k = 25
filter_ = np.zeros((k, k), dtype=int)
filter_[:, k // 2] = 1
# Ініціалізуємо вихідне зображення
convolved_image = convolve_RGB_image(img_original_RGB, filter_)
# Застосовуємо max_pooling
pool_size = 10
pooled_image = max_pooling_image(convolved_image, pool_size, pool_size)
# Показуємо оригінальне, згорнуте та зображення після max_pooling
fig, axs = plt.subplots(ncols=3, nrows=1, figsize=(21, 7))
axs[0].imshow(img_original_RGB)
axs[0].set_title('Оригінальне зображення')
axs[1].imshow(convolved_image, cmap="gray")
axs[1].set_title('Зображення після згортки')
axs[2].imshow(pooled_image, cmap="gray")
axs[2].set_title('Зображення після max_pooling')
plt.show()

Поєднавши алгоритми згортки для кольорового зображення та макс-пулінгу ми отримаємо повноцінне уявлення, яким чином працюють згорткові шари та як вони дозволяють отримати певні ознаки з зображень.

Маючи багато фільтрів з різними значеннями, які обробляють кольорові зображення, ми можемо отримувати різноманітні ознаки характеристик зображень.

Нейронні мережі на базі згортки та пулінгу

Отже, ми розглянули, як виглядає операція згортки для кольорових зображень та операція макс-пулінгу. Маючи всю цю інформацію, тепер ми можемо легко вивести концепцію згорткових нейронних мереж.

По-перше, замість визначеного фільтра h ми можемо використовувати матриці з випадково згенерованими значеннями w або вагами. Чим більше буде таких фільтрів, тим більше різних ознак ми можемо отримати з зображень. Тобто визначивши кількість фільтрів для обробки Cout ми можемо отримати таку ж кількість одноканальних зображень на виході або одне зображення з каналами Cout.

Крім того, деякі ознаки можуть виділятися більш яскраво, ніж інші, створюючи шум, який приховає ознаки, що можуть бути важливими для даної конкретної задачі. Саме тому додається ще вектор bias(Cout), який дозволяє компенсувати схильність згорткового шару до одних ознак та обхід інших (з подробицями можете ознайомитись у документації згорткових шарів бібліотеки PyTorch).

Оскільки ці операції є лінійними, то незалежно від того, наскільки глибока будь-яка згорткова мережа, її можна було б звести до одного єдиного шару, оскільки композиція лінійних функцій є лінійною функцією. Саме тому вводиться поняття функції активації Activation(•). Функції активації вводять нелінійність у модель, дозволяючи нейронній мережі навчатися і моделювати складніші функції та шаблони в даних. Вони визначають вихід вузла (нейрона) в мережі. Застосування функції активації включає перетворення вихідного сигналу нейрона таким чином, що він може бути використаний як вхід для наступного шару нейронів.

В згорткових нейронних мережах часто використовують функцію активації ReLU (Rectified Linear Unit), оскільки вона допомагає моделі швидше сходитися при тренуванні, а також ефективно обробляє проблему вимивання градієнтів, що може виникнути з іншими функціями активації, такими як сигмоїд або гіперболічний тангенс. І вже маючи вихід з цієї функції, ми можемо використати пулінг.

Це і є повна формула для опису одного шару згорткової мережі. Накладаючись, шар за шаром, згорткові нейронні мережі здатні отримувати все складніші та складніші ознаки з зображення. Таким чином будуються глибокі згорткові нейронні мережі.

Першою успішною нейронною мережею, яка була використана для розв’язання реальних задач, а саме для розпізнавання рукописних поштових кодів, була мережа LeNet, названа на честь реалізатора Яна ЛеКуна, розроблена в 1989 році. Приклад її роботи ви можете побачити на відео нижче:

Далі показана архітектура цієї нейронної мережі, яка реалізована за допомогою бібліотеки PyTorch:

#Defining the convolutional neural network
class LeNet5(nn.Module):
    def __init__(self, num_classes):
        super(ConvNeuralNet, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=0),
            nn.BatchNorm2d(6),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer2 = nn.Sequential(
            nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0),
            nn.BatchNorm2d(16),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.fc = nn.Linear(400, 120)
        self.relu = nn.ReLU()
        self.fc1 = nn.Linear(120, 84)
        self.relu1 = nn.ReLU()
        self.fc2 = nn.Linear(84, num_classes)
    def forward(self, x):
        out = self.layer1(x)
        out = self.layer2(out)
        out = out.reshape(out.size(0), -1)
        out = self.fc(out)
        out = self.relu(out)
        out = self.fc1(out)
        out = self.relu1(out)
        out = self.fc2(out)
        return out

Як бачимо, архітектура складається з двох шарів згорткових нейронних мереж та трьох шарів звичайних штучних нейронів. Кожен згортковий шар має зазначені нами раніше функції та збільшує кількість каналів на виході кожного шару. Перший шар перетворює чорно-біле зображення в шестиканальне, а другий з шестиканального зображення в 16-канальне.

Але справжня новизна цієї нейронної мережі була в тому, що фільтри, які були реалізовані, є вагами, які змінювались в процесі навчання. Для них був реалізований алгоритм зворотного проходження, або backpropagation (кому цікаві подробиці — відео за посиланням).

Саме це стало основою для подальшого розвитку і революції всієї області комп’ютерного зору, яка спалахнула у 2012 році з випуском іншої відомої нейронної мережі AlexNet.

Висновок

Ми розглянули головні математичні принципи, які лежать в основі згорткових нейронних мереж, детально розібрали операцію згортки, з’ясували, як вона застосовується до чорно-білих і кольорових зображень, а також розглянули пулінг.

Основна мета цієї статті — допомогти читачам краще зрозуміти, як працюють згорткові нейронні мережі, а не продемонструвати їхню оптимальну реалізацію в сучасних бібліотеках.

Ми розглядали деталі алгоритму backpropagation, ініціалізації фільтрів, деталей стосовно розміру ядер тощо, які також є важливими елементами навчання нейронних мереж.

З усім тим, сподіваюсь, що ця стаття дала вам відчуття основних принципів роботи CNN. Я маю надію, що ви змогли отримати нову та корисну інформацію, яка допоможе вам краще зрозуміти і використати, можливо, навіть свої власні згорткові нейронні мережі.

👍ПодобаєтьсяСподобалось15
До обраногоВ обраному8
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

У фото, яке використовувалось у статті є ціла сторінка на вікі
en.wikipedia.org/wiki/Lenna

Підписатись на коментарі