Керівництво хакера по нейронних мереж. Схеми реальних значень. Шаблони в «зворотному» потоці. Приклад «Один нейрон»

ЗмістЧастина 1:
Вступ
Глава 1: Схеми реальних значень
Базовий сценарій: Простий логічний елемент у схемі
Мета
Стратегія №1: Довільний локальний пошук

Частина 2
Стратегія №2: Числовий градієнт

Частина 3:
Стратегія №3: Аналітичний градієнт

Частина 4:
Схеми з декількома логічними елементами
Зворотне поширення помилки

Частина 5:
Шаблони в «зворотному» потоці 
Приклад "Один нейрон"



Давайте знову подивимося на наш приклад схеми з введеними числами. Перша схема показує нам «сирі» значення, а друга — градієнти, які повертаються до початкових значень, як обговорювалося раніше. Зверніть увагу, що градієнт завжди зводиться до +1. Це стандартний поштовх для схеми, у якій має збільшитися значення.

image

Через якийсь час ви почнете помічати шаблони в тому, як градієнти повертаються за схемою. Наприклад, логічний елемент + завжди піднімає градієнт і просто передає його на всі вихідні значення (зверніть увагу, в прикладі з -4 він був просто переданий на обидва вихідних значення логічного елемента +). Це тому, що його власна похідна для вихідних значень дорівнює +1, незалежно від того, чому дорівнюють фактичні значення вихідних даних, тому в ланцюговому правилі градієнт зверху просто множиться на 1 і залишається таким же.

Теж саме відбувається, наприклад, з логічним елементом max(x,y). Так як градієнт елемента max(x,y) по відношенню до своїм вихідним значенням дорівнює +1 для того значення x або y, яке більше, і 0 для другого, цей логічний елемент у процесі зворотного розподілу помилки ефективно використовується тільки в якості «перемикача» градієнта: він бере градієнт зверху і «направляє» його до вихідного значення, яке виявляється вище при зворотному проході.

Перевірка числового градієнта

Перш ніж ми закінчимо з цим розділом, давайте просто переконаємося, що аналітичний градієнт, який ми вирахували для зворотного поширення помилки, правильний. Давайте згадаємо, що ми можемо зробити це, просто розрахувавши числовий градієнт, і переконавшись, що отримаємо [-4, -4, 3] для x,y,z. Ось код:

// стартові умови
var x = -2, y = 5, z = -4;

// перевірка числового градієнта
var h = 0.0001;
var x_derivative = (forwardCircuit(x+h,y,z) - forwardCircuit(x,y,z)) / h; // -4
var y_derivative = (forwardCircuit(x,y+h,z) - forwardCircuit(x,y,z)) / h; // -4
var z_derivative = (forwardCircuit(x,y,z+h) - forwardCircuit(x,y,z)) / h; // 3


Приклад: Один нейрон

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

image

У цьому виразі σ являє собою сигмоидную функцію. Її можна описати як «стискає функцію», так як вона бере початкове значення і стискає його, щоб воно виявилося між нулем і одиницею: Вкрай негативні значення стискаються в бік нуля, а позитивні значення стискаються в бік одиниці. Наприклад, у нас є вираз sig(-5) = 0.006, sig(0) = 0.5, sig(5) = 0.993. Сигмоидная функція визначається наступним чином:

image

Градієнт по відношенню до його єдиного вихідного значення, як зазначено у Вікіпедії (або, якщо ви розбираєтеся в методах розрахунку, можете обчислити його самостійно), має вигляд наступного виразу:

image

Наприклад, якщо вихідним значенням сигмоидного логічного елемента є x = 3, логічний елемент буде обчислювати результат рівняння f = 1.0 / (1.0 + Math.exp(-x)) = 0.95, після чого (локальний) градієнт за його вихідного значення буде мати наступний вигляд: dx= (0.95) * (1 — 0.95) = 0.0475.

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

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

1. Значення, яке вона має при прямому проході
2. Градієнт (тобто поштовх), який проходить назад по ній при зворотному проході

Давайте створимо просту структуру сегмента (Unit), яка буде зберігати ці два значення по кожній лінії. Наші логічні елементи не будуть працювати поверх сегментів: вони будуть приймати їх в якості вихідних значень і створювати їх у вигляді вихідних значень.

// кожен сегмент відповідає лінії на графіках
var Unit = function(value, grad) {
// значення, розраховане при передньому проході
this.value = value; 
// похідна результату схеми по відношенню до цього сегменту, розрахована при зворотному проході
this.grad = grad; 
}



Крім сегментів, нам також потрібні три логічних елемента: +, * і sig (сигмоїда). Давайте почнемо з застосування логічного елемента множення. Тут я використовую Javascript, який цікаво симулює класи за допомогою функцій. Якщо ви не знайомі з Javascript, те, що тут відбувається — це визначення класу, у якого є певні властивості (доступ до яких виходить за допомогою ключового слова this), і деякі методи (які Javascript поміщені в прототип функції). Просто запам'ятайте їх як класові методи. Також не забувайте, що спосіб, за допомогою якого ми будемо їх використовувати, полягає в тому, що ми спочатку передамо (forward) всі логічні елементи по одному, а потім повернемо їх назад (backward) у зворотному порядку. От реалізація цього:

var multiplyGate = function(){ };
multiplyGate.prototype = {
forward: function(u0, u1) {
// зберігаємо покажчики для введення сегментів u0 і u1 і вихідного сегмента utop
this.u0 = u0; 
this.u1 = u1; 
this.utop = new Unit(u0.value * u1.value, 0.0);
return this.utop;
},
backward: function() {
// беремо градієнт у вихідному сегменті і пов'язуємо його з 
// локальними показниками, які ми диференціювали для логічного елемента множення заздалегідь 
// після цього прописуємо ці градієнти в цих сегментах.
this.u0.grad += this.u1.value * this.utop.grad;
this.u1.grad += this.u0.value * this.utop.grad;
}
}



Логічний елемент множення бере два сегменти, кожен з яких містить значення, і створює сегмент, який зберігає його результат. Градієнту присвоюється в якості початкового значення нуль. Потім, зверніть увагу, що при виклику функції backward ми отримуємо градієнт з результату сегмента, який ми створили в процесі переднього проходу (який тепер, сподіваюся, буде мати свій заповнений градієнт) і множимо його на локальний градієнт для цього логічного елемента (ланцюгове правило!). Цей логічний елемент виконує множення (u0.value * u1.value) при передньому проході, тому згадуємо, що градієнт по відношенню до дорівнює u0 u1.value і по відношенню до u1 дорівнює u0.value. Також зверніть увагу, що ми використовуємо += для додавання до градієнту функції при backward. Це, можливо, дозволить нам використовувати результат одного логічного елемента кілька разів (уявіть собі це як розгалуженні лінії), так як виявляється, що градієнти за цим гілкам просто підсумовуються при розрахунку остаточного градієнта по відношенню до результату схеми. Інші два логічних елемента визначаються аналогічним чином:

var addGate = function(){ };
addGate.prototype = {
forward: function(u0, u1) {
this.u0 = u0; 
this.u1 = u1; // зберігаємо покажчики для введення сегментів
this.utop = new Unit(u0.value + u1.value, 0.0);
return this.utop;
},
backward: function() {
// логічний елемент додавання. Похідна по відношенню до обох результатами дорівнює 1
this.u0.grad += 1 * this.utop.grad;
this.u1.grad += 1 * this.utop.grad;
}
}
var sigmoidGate = function() { 
// допоміжна функція
this.sig = function(x) { return 1 / (1 + Math.exp(-x)); };
};
sigmoidGate.prototype = {
forward: function(u0) {
this.u0 = u0;
this.utop = new Unit(this.sig(this.u0.value), 0.0);
return this.utop;
},
backward: function() {
var s = this.sig(this.u0.value);
this.u0.grad += (s * (1 - s)) * this.utop.grad;
}
}


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

s.grad = 1.0;
sg0.backward(); // записує градієнт в axpbypc
addg1.backward(); // записує градієнт в axpby та c
addg0.backward(); // записує градієнт в ax by і
mulg1.backward(); // записує градієнт в b і y
mulg0.backward(); // записує градієнт в a і x


Зверніть увагу, що перший рядок встановлює градієнт на виході (самий останній сегмент) значення 1.0 для запуску ланцюга градієнта. Це можна інтерпретувати, як поштовх на останній логічний елемент з силою дорівнює +1. Іншими словами, ми тягне всю схему, змушуючи її прикладати сили, які збільшать вихідне значення. Якщо б ми не задали йому значення 1, всі градієнти розраховувалися б як нульові зважаючи множення згідно ланцюгового правилом. Зрештою, давайте змусимо вихідні значення реагувати на розраховані градієнти і перевіримо, що функція збільшилася:

var step_size = 0.01;
a.value += step_size * a.grad; // a.grad одно -0.105
b.value += step_size * b.grad; // b.grad одно 0.315
c.value += step_size * c.grad; // c.grad одно 0.105
x.value += step_size * x.grad; // x.grad одно 0.105
y.value += step_size * y.grad; // y.grad одно 0.210

forwardNeuron();
console.log('circuit output after one backprop: ' + s.value); // виводиться результат 0.8825


Успіх! 0.8825 вище, ніж попереднє значення, 0.8808. Нарешті, давайте перевіримо, що ми правильно виконали зворотне поширення помилки, перевіривши числовий градієнт:

var forwardCircuitFast = function(a,b,c,x,y) { 
return 1/(1 + Math.exp( - (a*x + b*y + c))); 
};
var a = 1, b = 2, c = -3, x = -1, y = 3;
var h = 0.0001;
var a_grad = (forwardCircuitFast(a+h,b,c,x,y) - forwardCircuitFast(a,b,c,x,y))/h;
var b_grad = (forwardCircuitFast(a,b+h,c,x,y) - forwardCircuitFast(a,b,c,x,y))/h;
var c_grad = (forwardCircuitFast(a,b,c+h,x,y) - forwardCircuitFast(a,b,c,x,y))/h;
var x_grad = (forwardCircuitFast(a,b,c,x+h,y) - forwardCircuitFast(a,b,c,x,y))/h;
var y_grad = (forwardCircuitFast(a,b,c,x,y+h) - forwardCircuitFast(a,b,c,x,y))/h;



Таким чином, усе це дає ті ж значення, що і градієнти зворотного поширення помилки[-0.105, 0.315, 0.105, 0.105, 0.210]. Відмінно!

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

Джерело: Хабрахабр

0 коментарів

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