Глибоке навчання для новачків: розпізнаємо рукописні цифри

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



Інтенсивний розвиток технологій машинного навчання призвело до появи декількох досить зручних фреймворків, які дозволяють швидко проектувати і будувати прототипи наших моделей, а також надають необмежений доступ до наборів даних, застосовуваним для тестування алгоритмів навчання (таким, як названі вище). Середовище розробки, яку ми будемо використовувати тут, називається Keras; я знайшов її найбільш зручною і інтуїтивно, але в той же час володіє виражальними можливостями, достатніми для того, щоб при необхідності вносити правки в модель.
В кінці цього уроку ви будете розуміти принцип роботи простий моделі глибокого навчання, званої «багатошаровий перцептрон» (MLP), а також навчитеся будувати її в Keras, отримуючи гідну ступінь точності на MNIST. На наступному уроці ми розглянемо методи розв'язування більш складних задач класифікації зображень (таких, як CIFAR-10).
(Штучні) нейрони
Хоча термін «глибоке навчання» можна розуміти і в більш широкому сенсі, в більшості випадків він застосовується в області (штучних) нейронних мереж. Ідея цих конструкцій запозичені з біології: нейронні мережі імітують процес обробки нейронами головного мозку сприймає з навколишнього середовища образів і участь цих нейронів у прийнятті рішень. Принцип роботи окремо взятого штучного нейрона в сутності дуже простий: він обчислює зважену суму всіх елементів вхідного вектора \inline\vec{_x_}, використовуючи вектор ваг \inline\vec{_w_}(а також адитивну складову зміщення {_w__0}), а потім до результату може застосовуватися функція активації σ.

Серед найбільш популярних функцій активації
  • Функція тотожності (Identity): σ(z)=z;
  • Сигмоїдальна функція, а саме, логістична функція (Logistic): \sigma(z)={1\over(1 + exp(-z))і гіперболічний тангенс (Tanh): \sigma(z)=tanh(z)
  • Полулинейная функція (Rectified linear, ReLU) \sigma(z)=max(0, z)

Спочатку (з 1950-х) моделі перцептронов були повністю лінійними, тобто в якості функції активації служило тільки тотожність. Але незабаром стало зрозуміло, що основні завдання частіше мають нелінійну природу, що призвело до появи інших функцій активації. Сигмоїдальні функції (зобов'язані своїй назві характерному S-образним графіком) добре моделюють початкову «невизначеність» нейрона щодо бінарного рішення, коли z близько до нуля, в поєднанні з швидким насиченням при зміщенні z в якому-небудь напрямі. Дві функції, представлені тут, дуже схожі, але вихідні значення гіперболічного тангенса належать відрізку [-1, 1], а область значень логістичної функції — [0, 1] (таким чином, логістична функція зручніше для представлення ймовірностей).
В останні роки в глибокому навчанні отримали широке поширення полулинейные функції і їх варіації — вони з'явилися в якості простого способу зробити модель нелінійної («якщо значення негативно, обнулим його»), але врешті-решт виявилися успішнішими, ніж історично більш популярні сигмоїдальні функції, до того ж вони більше відповідають тому, як біологічний нейрон передає електричний імпульс. З цієї причини в рамках цього уроку ми зосередимо увагу на півлінійних функціях (ReLU).
Кожен нейрон однозначно визначається його ваговим вектором \inline\vec{_w_}, і головна мета навчається алгоритму — на основі навчальної вибірки відомих пар вхідних і вихідних даних присвоїти нейрона набір ваг таким чином, щоб мінімізувати помилки передбачення. Типовий приклад такого алгоритму — метод градієнтного спуску (gradient descent), який для певної функції втрат E(\vec{w})змінює вектор ваг в напрямку найбільшої убування цієї функції:

\vec{w}\leftarrow\vec{w}-\eta {\partial E(\vec{w}) \over \partial \vec{w}}
де η — позитивний параметр, званий темпом навчання (learning rate).
Функція втрат відображає наше уявлення про те, наскільки неточний нейрон у прийнятті рішень при поточному значенні параметрів. Найбільш простий вибір функції втрат, який при цьому хороший для більшості завдань квадратична функція; для заданої навчальної вибірки (\vec{x}, y)вона визначається як квадрат різниці цільового значення y і фактичного вихідного значення нейрона при цьому вхідному \vec{x}:
E(\vec{w}) = (y - \sigma(w_0 + \sum_{i=1}^{n} w_ix_i))^2
У мережі є велика кількість навчальних курсів, що розглядають алгоритми градієнтного спуску більш поглиблено. У нашому ж випадку про всю оптимізації для нас подбає фреймворк, тому я не буду приділяти їй багато уваги в подальшому.
Введення в нейронні мережі (і глибоке навчання)
Тепер, коли ми ввели поняття нейрона, стає можливо з'єднати вихід одного нейрона з входом іншого, таким чином поклавши початок нейронної мережі. В цілому ми зосередимо нашу увагу на нейронних мережах прямого поширення, в яких нейрони формують шари так, що нейрони одного шару обробляють вихідні дані попереднього шару. У найбільш потужною з таких архітектур (багатошарові перцептрони, MLP) всі вихідні дані одного шару з'єднані з усіма нейронами наступного шару, як на схемі нижче.

Для зміни ваг вихідних нейронів може бути безпосередньо використаний описаний вище метод градієнтного спуску з заданою функцією втрат, для інших нейронів необхідно поширити ці втрати у зворотному напрямку (застосовуючи правило диференціювання складної функції), таким чином поклавши початок алгоритмом зворотного поширення помилок (backpropagation). Так само, як і з методом градієнтного спуску, я не буду приділяти увагу математичного обгрунтування алгоритму, так як всі обчислення проводити наш фреймворк.
За універсальною теоремі апроксимації Цибенко досить широкий багатошаровий перцептрон з одним прихованим шаром сигмоїдальних нейронів може апроксимувати будь-яку неперервну функцію дійсних змінних на заданому інтервалі. Доказ цієї теореми не має практичного застосування і не пропонує ефективного навчального алгоритму для подібних структур. Відповідь дає глибоке навчання: замість ширини збільшуйте глибину; за визначенням будь-яка нейронна мережа з більш, ніж одним прихованим шаром, вважається глибокої.
Переміщення в глибину також дозволяє нам подавати на вхід нейронної мережі необроблені вхідні дані: у минулому одношаровим мереж на вхід подавалися ключові ознаки (features), які виділяли з вхідних даних за допомогою спеціальних функцій. Це означало, що для різних класів задач, наприклад, комп'ютерного зору, розпізнавання мови або обробки природних мов, потрібні різні підходи, що перешкоджало науковому співробітництву між цими областями. Але коли мережа містить кілька прихованих шарів, вона набуває здатність сама навчатися виділяти ключові ознаки, які найкращим чином описують вхідні дані, таким чином знаходячи застосування end-to-end learning (без традиційних програмованих обробок між входом і виходом), а також дозволяючи використовувати одну і ту ж мережу для широкого спектра завдань, так як більше немає необхідності виводити функції для отримання ключових ознак. Я наведу графічне підтвердження вищесказаного у другій частині лекції, коли ми будемо розглядати згорткові нейронні мережі.
Застосування глибокого MLP до MNIST
Тепер реалізуємо найпростішу можливу глибоку нейронну мережу — MLP з двома прихованими шарами — і застосуємо її до задачі розпізнавання рукописних цифр з набору даних MNIST.
Необхідні наступні імпорти:
from keras.datasets import mnist # підпрограм for fetching the MNIST dataset
from keras.models import Model # basic class for specifying and training a neural network
from keras.layers import Input, Dense # the two types of neural network layer we will be using
from keras.utils import np_utils # utilities for one hot encoding of ground truth values

Потім визначимо деякі параметри нашої моделі. Ці параметри часто називають гиперпараметрами, так як передбачається, що вони будуть уточнені ще до початку навчання. В даному посібнику ми візьмемо заздалегідь підібрані значення, процесу же їх уточнення приділимо більше уваги в наступних уроках.
зокрема, ми визначимо
batch_size — кількість навчальних зразків, що обробляються одночасно за одну ітерацію алгоритму градієнтного спуску
num_epochs — кількість ітерацій навчального алгоритму по всьому навчального безлічі
hidden_size — кількість нейронів у кожному з двох прихованих шарів MLP
batch_size = 128 # in each iteration, we consider 128 training examples at once
num_epochs = 20 # we iterate twenty times over the entire training set
hidden_size = 512 # there will be 512 neurons in both hidden layers

Настав час завантажити MNIST і провести попередню обробку. З допомогою Keras це робиться дуже просто: він просто зчитує дані з віддаленого сервера безпосередньо в масиви бібліотеки NumPy.
Щоб підготувати дані, спершу ми представимо зображення у вигляді одновимірних масивів (так як вважаємо кожен піксель окремим вхідним ознакою), а потім розділимо значення інтенсивності кожного пікселя на 255, щоб нове значення потрапляло у відрізок [0, 1]. Це дуже простий спосіб нормалізувати дані, ми обговоримо інші способи в наступних уроках.
Хорошим підходом до задачі класифікації є імовірнісна класифікація, при якій у нас є один вихідний нейрон для кожного класу, видає ймовірність того, що вхідний елемент належить даного класу. Це передбачає необхідність перетворення навчальних вихідних даних пряме кодування: наприклад, якщо бажаний вихідний клас — 3, а всього п'ять класів (і вони пронумеровані від 0 до 4), то відповідне пряме кодування— [0, 0, 0, 1, 0]. Повторюся, що Keras пропонує нам всю цю функціональність «з коробки».
num_train = 60000 # there are 60000 training examples in MNIST
num_test = 10000 # there are 10000 test examples in MNIST

height, width, depth = 28, 28, 1 # MNIST images are 28x28 and greyscale
num_classes = 10 # there are 10 classes (1 per digit)

(X_train, y_train), (X_test, y_test) = mnist.load_data() # fetch MNIST data

X_train = X_train.reshape(num_train, height * width) # Flatten data to 1D
X_test = X_test.reshape(num_test, height * width) # Flatten data to 1D
X_train = X_train.astype('float32') 
X_test = X_test.astype('float32')
X_train /= 255 # Normalise data to [0, 1] range
X_test /= 255 # Normalise data to [0, 1] range

Y_train = np_utils.to_categorical(y_train, num_classes) # One-hot encode the labels
Y_test = np_utils.to_categorical(y_test, num_classes) # One-hot encode the labels

А тепер настав момент визначити нашу модель! Для цього ми скористаємося стеком з трьох Dense шарів, який відповідає полносвязному MLP, де всі виходи одного шару пов'язані з усіма входами наступного. Будемо використовувати ReLU для нейронів перших двох шарів, і softmax для останнього шару. Ця функція активації розроблена, щоб перетворити будь-який вектор з реальними значеннями вектор ймовірностей і визначається для j-го нейрона наступним чином:

\sigma(\vec{z})_j = {exp(z_j) \over \sum_iexp(z_i)}
Чудова риса Keras, яка відрізняє його від інших фреймворків (наприклад, від TansorFlow) — це автоматичний розрахунок розмірів шарів; нам достатньо лише вказати розмірність вхідного шару, а Keras автоматично проинициализирует всі інші шари. Коли всі шари визначено, нам треба просто задати вхідні і вихідні дані, як це зроблено нижче.
inp = Input(shape=(висота * ширина,)) # Our input is a 1D vector of size 784
hidden_1 = Dense(hidden_size, activation='relu')(inp) # First hidden ReLU layer
hidden_2 = Dense(hidden_size, activation='relu')(hidden_1) # Second hidden ReLU layer
out = Dense(num_classes, activation='softmax')(hidden_2) # Output softmax layer

model = Model(input=inp, output=out) # To define a model, just specify its input and output layers

Тепер нам залишилося тільки визначити функцію втрат, алгоритм оптимізації і метрики, які ми буде збирати.
Коли ми маємо справу з ймовірнісної класифікацією, в якості функції втрат краще всього використовувати не визначену вище квадратичну помилку, а перехресне ентропію. Для певного вихідного імовірнісного вектора \vec{y}, порівнюваного з фактичним вектором \vec\hat{y}, втрата (для k-го класу) буде визначатися як


\mathcal{L}(\vec{y}, \vec\hat{y}) = -\sum_{i=1}^k\hat{y}_i\log{y_i}
Втрати будуть менші для імовірнісних задач (наприклад, з логістичної/softmax функцією для вихідного шару), в основному з-за того, що дана функція призначена для максимізації впевненості моделі в правильному визначенні класу, і її не турбує розподіл ймовірностей попадання зразка в інші класи (в той час як функція квадратичної помилки прагне до того, щоб ймовірність попадання в інші класи була якомога ближче до нуля).
Використовуваний алгоритм оптимізації буде нагадувати якусь форму алгоритму градієнтного спуску, відмінність буде лише в тому, як вибирається темп навчання η. Прекрасний огляд цих підходів представлений тут, а зараз ми будемо використати оптимізатор Адама, який зазвичай показує хорошу продуктивність.
Так як наші класи збалансовані (кількість рукописних цифр, що належать кожному класу, однаково), підходящою метрикою буде точність (accuracy) — частка вхідних даних, віднесених до правильного класу.
model.compile(loss='categorical_crossentropy', # using the cross-entropy loss function
optimizer='adam', # using the Adam optimiser
metrics=['accuracy']) # reporting the accuracy

Нарешті ми запускаємо навчальний алгоритм. Хорошою практикою буде відкласти деяку підмножину даних для перевірки, що наш алгоритм (все ще) вірно розпізнає дані — ці дані ще називають валідаційних набором (validation set); тут ми відокремлюємо для цієї цілі 10%.
Ще одна приємна особливість Keras — деталізація: він виводить детальний логування всіх кроків алгоритму.
model.fit(X_train, Y_train, # Train the model using the training set...
batch_size=batch_size, nb_epoch=num_epochs,
verbose=1, validation_split=0.1) # ...holding out 10% of the data for validation
model.evaluate(X_test, Y_test, verbose=1) # Evaluate the trained model on the test set!

Train on 54000 samples, validate on 6000 samples
Epoch 1/20
54000/54000 [==============================] - 9s - loss: 0.2295 - acc: 0.9325 - val_loss: 0.1093 - val_acc: 0.9680
Epoch 2/20
54000/54000 [==============================] - 9s - loss: 0.0819 - acc: 0.9746 - val_loss: 0.0922 - val_acc: 0.9708
Epoch 3/20
54000/54000 [==============================] - 11s - loss: 0.0523 - acc: 0.9835 - val_loss: 0.0788 - val_acc: 0.9772
Epoch 4/20
54000/54000 [==============================] - 12s - loss: 0.0371 - acc: 0.9885 - val_loss: 0.0680 - val_acc: 0.9808
Epoch 5/20
54000/54000 [==============================] - 12s - loss: 0.0274 - acc: 0.9909 - val_loss: 0.0772 - val_acc: 0.9787
Epoch 6/20
54000/54000 [==============================] - 12s - loss: 0.0218 - acc: 0.9931 - val_loss: 0.0718 - val_acc: 0.9808
Epoch 7/20
54000/54000 [==============================] - 12s - loss: 0.0204 - acc: 0.9933 - val_loss: 0.0891 - val_acc: 0.9778
Epoch 8/20
54000/54000 [==============================] - 13s - loss: 0.0189 - acc: 0.9936 - val_loss: 0.0829 - val_acc: 0.9795
Epoch 9/20
54000/54000 [==============================] - 14s - loss: 0.0137 - acc: 0.9950 - val_loss: 0.0835 - val_acc: 0.9797
Epoch 10/20
54000/54000 [==============================] - 13s - loss: 0.0108 - acc: 0.9969 - val_loss: 0.0836 - val_acc: 0.9820
Epoch 11/20
54000/54000 [==============================] - 13s - loss: 0.0123 - acc: 0.9960 - val_loss: 0.0866 - val_acc: 0.9798
Epoch 12/20
54000/54000 [==============================] - 13s - loss: 0.0162 - acc: 0.9951 - val_loss: 0.0780 - val_acc: 0.9838
Epoch 13/20
54000/54000 [==============================] - 12s - loss: 0.0093 - acc: 0.9968 - val_loss: 0.1019 - val_acc: 0.9813
Epoch 14/20
54000/54000 [==============================] - 12s - loss: 0.0075 - acc: 0.9976 - val_loss: 0.0923 - val_acc: 0.9818
Epoch 15/20
54000/54000 [==============================] - 12s - loss: 0.0118 - acc: 0.9965 - val_loss: 0.1176 - val_acc: 0.9772
Epoch 16/20
54000/54000 [==============================] - 12s - loss: 0.0119 - acc: 0.9961 - val_loss: 0.0838 - val_acc: 0.9803
Epoch 17/20
54000/54000 [==============================] - 12s - loss: 0.0073 - acc: 0.9976 - val_loss: 0.0808 - val_acc: 0.9837
Epoch 18/20
54000/54000 [==============================] - 13s - loss: 0.0082 - acc: 0.9974 - val_loss: 0.0926 - val_acc: 0.9822
Epoch 19/20
54000/54000 [==============================] - 12s - loss: 0.0070 - acc: 0.9979 - val_loss: 0.0808 - val_acc: 0.9835
Epoch 20/20
54000/54000 [==============================] - 11s - loss: 0.0039 - acc: 0.9987 - val_loss: 0.1010 - val_acc: 0.9822
10000/10000 [==============================] - 1s 

[0.099321320021623111, 0.9819]

Як видно, наша модель досягає точності приблизно 98.2% на тестовому наборі даних, це цілком гідно для такої простої моделі, незважаючи на те що її далеко перевершили надсучасні підходи, описані здесь.
Я закликаю вас ще поекспериментувати з цією моделлю: спробувати різні гиперпараметры, алгоритми оптимізації, функції активації, додати прихованих шарів, і т. д. В кінці кінців у вас повинно вийти досягти точності вище 99%.
Висновок
У цьому пості ми розглянули основні поняття глибокого навчання, успішно реалізували простий двошаровий глибокий MLP з допомогою фреймворку Keras, застосували його до набору даних MNIST — і все це менше 30 рядках коду.
наступного разу ми розглянемо згорткові нейронні мережі (CNN), які вирішують деякі проблеми, що виникають при застосуванні MLP до зображень великих обсягів (таких, як CIFAR-10).
Джерело: Хабрахабр

0 коментарів

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