Розпізнавання образів в R з використанням згорткових нейронних мереж з пакету MXNet

Це детальна інструкція по розпізнаванню образів в R з використанням глибокої сверточной нейронної мережі, що надається пакетом MXNet. У цій статті наведено відтворений приклад, як отримати 97,5% точність в задачі розпізнавання осіб на R.
image

Передмова

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

Вимоги

Я збираюся використовувати і Python 3.x (для отримання і попередньої обробки даних), і R (власне, рішення задачі), тому має сенс встановити обидва. Вимоги до пакетів R такі:
  1. MXNet. Цей пакет надасть модель, яку ми збираємося використовувати в цій статті, власне, глибоку сверточную нейронну мережу. Вам не знадобиться GPU-версія, CPU буде достатньо для цього завдання, хоча вона може працювати повільніше. Якщо це станеться, скористайтесь GPU-версією.
  2. EBImage. Цей пакет має безліч інструментів для роботи з зображеннями. З ним робота з зображеннями — одне задоволення, документація гранично зрозуміла і досить проста.
Що стосується Python 3.x, встановіть і Numpy, Scikit-learn. Можливо, варто також встановити дистрибутив Anaconda, в якому є ряд попередньо встановлених популярних пакетів для аналізу даних та машинного навчання.

Як тільки у вас все це запрацювало, можна приступати.

Набір даних

Я збираюся використовувати набір осіб Olivetti. Цей набір даних — колекція зображень 64 64 пікселя, в 0-256 градаціях сірого.

Набір даних містить 400 зображень 40 людей. З 10 екземплярами для кожної людини зазвичай використовують неконтрольовані або полуконтролируемые алгоритми, але я збираюся постаратися і використовувати конкретний контрольований метод.

Для початку потрібно масштабувати зображення за шкалою від 0 до 1. Це робиться автоматично функцією, яку ми збираємося використовувати для завантаження набору даних, тому не варто про це турбуватися, але потрібно знати, що це вже зроблено. Якщо ви збираєтеся використовувати свої власні зображення, попередньо масштабуйте їх за шкалою від 0 до 1 (або -1;1, хоча перше краще працює з нейронними мережами, виходячи з мого досвіду). Нижче — скрипт на Python, який потрібно виконати, щоб завантажити набір даних. Просто змініть шляху на ваші значення і виконайте з IDE або терміналу.
# -*- coding: utf-8 -*-

# Імпорт
from sklearn.datasets import fetch_olivetti_faces
import numpy as np

# Завантаження набору осіб Olivetti
olivetti = fetch_olivetti_faces()
x = olivetti.images
y = olivetti.target

# Вивід інформації про розмірах і їх зміна при необхідності
print("Original x shape:", x.shape)
X = x.reshape((400, 4096))
print("New x shape:", X. shape)
print("y shape", y.shape)

# Збереження масивів numpy
np.savetxt("C://olivetti_X.csv", X, delimiter = ",")
np.savetxt("C://olivetti_y.csv", y, delimiter = ",", fmt = '%d')

print("\nDownloading and reshaping done!")

################################################################################
# ВИСНОВОК
################################################################################
#
# Original x shape: (400, 64, 64)
# New x shape: (400, 4096)
# y shape (400,)
#
# Downloading and reshaping done!

Фактично, цей шматочок коду робить наступне: завантажує дані, змінює розміри картинок по Х і зберігає масиви numpy в файл .csv.

Масив х є тензором (тензор — красива назва багатовимірної матриці) розміру (400, 64, 64): це означає, що масив х містить 400 примірників матриць 64 64 (лічених зображень). Якщо сумніваєтеся, чи просто виведіть перші елементи тензора і спробуйте розібратися в структурі даних з урахуванням того, що ви вже знаєте. Наприклад, з опису набору даних ми знаємо, що у нас є 400 екземплярів, кожен з яких — зображення 64 64 пікселя. Ми згладжуємо тензор х до матриці розміром 400 на 4096. Тобто, кожна матриця 64 64 (зображення) тепер конвертується (згладжується) горизонтальний вектор довжиною 4096.

Що стосується, то це вже вертикальний вектор розміром 400. Його не потрібно змінювати.

Подивіться на цей файл .csv і переконайтеся, що всі перетворення зрозумілі.

Трохи попередньої обробки R

Тепер ми скористаємося EBImage, щоб змінити розмір зображень до 28 на 28 пікселів, і згенеруємо навчальний і тестовий набори даних. Ви запитаєте, навіщо я зраджую розміри зображень. З якоїсь причини мого комп'ютера не подобаються картинки на 64 64 пікселя, і кожен раз при запуску моделі з даними виникає помилка. Погано. Але терпимо, оскільки ми можемо отримати хороші результати і з меншими картинками (але ви, звичайно, можете спробувати запустити і з 64 64 пікселя, якщо у вас немає такої проблеми). Отже:
# Цей скрипт потрібен для зміни розміру зображень з 64 64 до 28 на 28 пікселів

# Очистити робоче середовище
rm(list=ls())

# Завантажити бібліотеку EBImage
require(EBImage)

# Завантажити дані
X <- read.csv("olivetti_X.csv", header = F)
labels <- read.csv("olivetti_y.csv", header = F)

# Масив даних картинок із зміненим розміром
rs_df <- data.frame()

# Основний цикл: для кожної картинки змінити розмір і перевести в градації сірого
for(i in 1:nrow(X))
{
# Try-catch
result <- tryCatch({
# Зображення (як одновимірний вектор)
img <- as.numeric(X[i])
# Картинка 64x64 (об'єкт EBImage)
img <- Image(img, dim=c(64, 64), colormode = "Grayscale")
# Змінити розмір картинки на 28x28 пікселів
img_resized <- resize(img, w = 28, h = 28)
# Отримати матрицю картинки (тут повинна бути інша функція, щоб зробити це швидше і акуратніше!)
img_matrix <- img_resized@.Data
# Призвести до вектора
img_vector <- as.vector(t(img_matrix))
# Додати теги
label <- labels[i]
vec <- c(label, img_vector)
# Помістити в rs_df з допомогою rbind
rs_df <- rbind(rs_df, vec)
# Вивести статус
print(paste("Done",i,sep = " "))},
# Функція виводу помилок (просто виводить помилку). Але помилок бути не повинно!
error = function(e){print(e)})
}


# Задати імена. Перші стовпці - мітки, решта - пікселі.
names(rs_df) <- c("label", paste("pixel", c(1:784)))

# Розбиття на навчання та тест
#-------------------------------------------------------------------------------
# Просте розділення на навчання та тест. Жодних перехресних перевірок.

# Встановити початкове число для відтворюваності
set.seed(100)

# Перемішаний df
shuffled <- rs_df[sample(1:400),]

# Розбиття на навчання та тест
train_28 <- shuffled[1:до 360 ]
test_28 <- shuffled[361:400, ]

# Зберегти навчальний і тестовий набори даних
write.csv(train_28, "C://train_28.csv", row.names = FALSE)
write.csv(test_28, "C://test_28.csv", row.names = FALSE)

# Готово!
print("Done!")

Ця частина повинна бути достатньо зрозумілою, якщо ви не впевнені, як виглядають вихідні дані, варто поглянути на набір даних rs_df. Це повинен бути масив даних 400x785, приблизно такий:
label, pixel1, pixel2, ..., pixel784
0, 0.2, 0.3,… ,0.1

Побудова моделі

Тепер найцікавіше, давайте побудуємо модель. Нижче скрипт, який був використаний, щоб навчити і протестувати модель. Нижче будуть мої коментарі та пояснення до коду.
# Очистити робочу область
rm(list=ls())

# Завантажити MXNet
require(mxnet)

# Завантаження даних та налаштування
#-------------------------------------------------------------------------------

# Завантажити навчальний і тестовий набори даних
train <- read.csv("train_28.csv")
test <- read.csv("test_28.csv")

# Визначити навчальний і тестовий набори даних
train <- data.matrix(train)
train_x <- t(train[, -1])
train_y <- train[, 1]
train_array <- train_x
dim(train_array) <- c(28, 28, 1, ncol(train_x))

test_x <- t(test[, -1])
test_y <- test[, 1]
test_array <- test_x
dim(test_array) <- c(28, 28, 1, ncol(test_x))

# Задати символьну модель
#-------------------------------------------------------------------------------

data <- mx.symbol.Variable('data')
# Перший шар згортковий
conv_1 <- mx.symbol.Convolution(data = data, kernel = c(5, 5), num_filter = 20)
tanh_1 <- mx.symbol.Activation(data = conv_1, act_type = "tanh")
pool_1 <- mx.symbol.Pooling(data = tanh_1, pool_type = "max", kernel = c(2, 2), stride = c(2, 2))
# Другий шар згортковий
conv_2 <- mx.symbol.Convolution(data = pool_1, kernel = c(5, 5), num_filter = 50)
tanh_2 <- mx.symbol.Activation(data = conv_2, act_type = "tanh")
pool_2 <- mx.symbol.Pooling(data=tanh_2, pool_type = "max", kernel = c(2, 2), stride = c(2, 2))
# Перший повністю зв'язний шар
flatten <- mx.symbol.Flatten(data = pool_2)
fc_1 <- mx.symbol.FullyConnected(data = flatten, num_hidden = 500)
tanh_3 <- mx.symbol.Activation(data = fc_1, act_type = "tanh")
# Другої повністю зв'язний шар
fc_2 <- mx.symbol.FullyConnected(data = tanh_3, num_hidden = 40)
# Висновок. Многопеременный логістичний висновок, т. к. хочемо отримати якісь ймовірності.
NN_model <- mx.symbol.SoftmaxOutput(data = fc_2)

# Налаштування до навчання
#-------------------------------------------------------------------------------

# Встановити початкове значення для відтворюваності
mx.set.seed(100)

# Пристрій. В моєму випадку CPU.
devices <- mx.cpu()

# Навчання
#-------------------------------------------------------------------------------

# Навчити модель
model <- mx.model.FeedForward.create(NN_model,
X = train_array,
y = train_y,
ctx = devices,
num.round = 480,
array.batch.size = 40,
learning.rate = 0.01,
momentum = 0.9,
eval.metric = mx.metric.accuracy,
epoch.end.callback = mx.callback.log.train.metric(100))

# Тестування
#-------------------------------------------------------------------------------

# Передбачити мітки
predicted <- predict(model, test_array)
# Мітки
predicted_labels <- max.col(t(predicted)) - 1
# Отримати точність
sum(diag(table(test[, 1], predicted_labels)))/40

################################################################################
# ВИСНОВОК
################################################################################
#
# 0.975
#

Після завантаження навчальної і тестового набору даних я використовую функцію
data.matrix
, щоб перетворити кожен набір даних в числову матрицю. Пам'ятайте, перший стовпець даних — мітки, пов'язані з кожною картинкою. Переконайтеся, що ви видалили мітки з
train_array
та
test_array
. Після розділення тегів і залежних змінних потрібно вказати MXNet обробити дані. Це я роблю у рядку 19 наступним шматочком коду: «dim(train_array) < — c(28, 28, 1, ncol(train_x))» для навчального набору і в рядку 24 для тестового. Таким чином ми фактично говоримо моделі, що навчальні дані складаються з ncol(train_x) зразків (360 картинок) розміром 28x28. Число 1 вказує, що картинки в градації сірого, тобто, що в них лише 1 канал. Якщо картинки були в RGB, 1 потрібно було б замінити на 3, саме стільки каналів мали б картинки.

Що стосується структури моделі, це варіація моделі LeNet, використовує зворотній гіперболічний тангенс як активационную функцію замість «Relu» (трансформований лінійний вузол), 2 згорткових шару, 2 шару підвибірки, 2 повністю зв'язкових шару і стандартний многопеременный логістичний висновок.

Кожен згортковий шар використовує ядро 5х5 і застосовується до фіксованого набору фільтрів. Перегляньте це прекрасне відео, щоб празобраться зі згортковими шарами. Шари підвибірки використовують класичний підхід «максимального об'єднання».

Мої тести показали, що tanh працює набагато краще, ніж sigmoid і Relu, але ви можете спробувати і інші функції активації, якщо є бажання.

Що стосується гиперпараметров моделі, рівень навчання трохи вище звичайного, але працює нормально, поки кількість періодів — 480. Розмір серії, рівний 40, теж добре працює. Ці гиперпараметры отримані шляхом проб і помилок. Можна було зробити пошук по вікон смугах, але не хотілося переусложнять код, так що я скористався перевіреним методом проб і помилок.

В кінці ви повинні отримати точність 0.975.

Висновок

В цілому, цю модель було досить легко налаштувати і запустити. При запуску на CPU навчання займає 4-5 хвилин: трохи довго, якщо ви хочете поекспериментувати, але все-таки прийнятно для роботи.

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

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

Джерело набору даних — набір осіб Olivetti, створений AT&T Laboratories Cambridge.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.