Керівництво хакера по нейронних мереж. Глава 2: Машинне навчання. Навчання мережі на основі методу опорних векторів (SVM)

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

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

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

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

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

Частина 6:
Стаємо майстром зворотного поширення помилки


Розділ 2: Машинне навчанняЧастина 7:
Бінарна класифікація

Частина 8:
Навчання мережі на основі методу опорних векторів (SVM)



В якості конкретного прикладу давайте розглянемо SVM. SVM — це дуже популярний лінійний класифікатор. Його функціональна форма має саме такий самий вигляд, як я описував в попередньому розділі — f(x,y)=ax+by+c. На даному етапі, якщо ви бачили опис SVM, ви напевно чекаєте, що я буду визначати функцію втрат SVM і занурюватися в пояснення вільних змінних, геометричних понять великих полів, ядер, двоїстості та ін. Але тут я би хотів скористатися іншим підходом.

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

«Характеристика сили:
Якщо ми пропускаємо позитивну точку вводу даних через схему SVM, і вихідне значення менше 1, потрібно потягнути схему з силою +1. Це позитивний приклад, тому нам потрібно, щоб його значення було вище.

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

Крім натягу вгору, завжди потрібно додавати невелику натяг для параметрів a,b (зверніть увагу, не на c!), яке буде тягнути їх у напрямку нуля. Можете уявити, ніби a,b прив'язані до фізичної пружині, яка прив'язана до нуля. Як і у випадку з фізичної пружиною, це зробить натяг пропорційних значенню кожного елемента a,b (закон Гука у фізиці, хто-небудь знає?). Наприклад, якщо a приймає занадто високе значення, воно буде відчувати сильне натяг з величиною |a| назад в бік нуля. Це натяг — це те, що ми називаємо регуляризацией, і воно забезпечує, щоб ні один з наших параметрів a або b не став непропорційно великою. Це може бути небажано, так як обидва параметра a,b множаться на характеристики вводу x,y (згадуємо, що наше рівняння має вигляд a*x + b*y + c), тому, якщо який-небудь з них має дуже високе значення, наш класифікатор буде занадто чутливим по відношенню до цих параметрів. Це не дуже хороша властивість, так як характеристики можуть часто бути неточними на практиці, тому нам потрібно, щоб наш класифікатор змінювався відносно гладко, якщо вони розгойдуються в сторони.

Давайте швидко пройдемося по невеликому, але конкретного прикладу. Припустимо, що ми почали з установки довільного параметра, скажімо, a = 1, b = -2, c = -1. Тоді, якщо ми подаємо точку [1.2, 0.7],SVM обчислить значення 1 * 1.2 + (-2) * 0.7 — 1 = -1.2. Ця точка позначена як +1 в навчальних даних, тому ми хочемо, щоб значення було більше ніж 1. Градієнт у верхній частині схеми буде в такому випадку позитивним: +1, і буде виконувати зворотне поширення помилки до a,b,c. Крім того, буде мати місце регуляризационное натяг на a з силою -1 (щоб зробити його менше) і регуляризационное натяг на b з силою +2, щоб зробити його більше, у напрямку нуля.

Замість цього припустимо, що ми подаємо на SVM точку введення даних [-0.3, 0.5]. Вона обчислює 1 * (-0.3) + (-2) * 0.5 — 1 = -2.3. Мітка для цієї точки має значення -1, а так як -2.3 менше, ніж -1, ми розуміємо, що відповідно до нашої характеристикою сили SVM повинна радіти: обчислене значення буде вкрай негативним і буде відповідати від'ємної позначки в цьому прикладі. Наприкінці схеми не буде натягу (тобто воно буде нульовим), так як зміни не потрібні. Однак, все одно буде присутній регуляризационное натяг на a з силою -1 і на b з силою +2.

Загалом, дуже багато тексту. Давайте напишемо код SVM і скористаємося перевагами механізму схеми, розглянутого у Главі 1:

// Схема бере 5 елементів (x,y,a,b,c), а видає один елемент
// Вона також може розрахувати градієнт по відношенню до власних початкових значень
var Circuit = function() {
// створюємо декілька логічних елементів
this.mulg0 = new multiplyGate();
this.mulg1 = new multiplyGate();
this.addg0 = new addGate();
this.addg1 = new addGate();
};
Circuit.prototype = {
forward: function(x,y,a,b,c) {
this.ax = this.mulg0.forward(a, x); // a*x
this.by = this.mulg1.forward(b, y); // b*y
this.axpby = this.addg0.forward(this.ax, this.by); // a*x + b*y
this.axpbypc = this.addg1.forward(this.axpby, c); // a*x + b*y + c
return this.axpbypc;
},
backward: function(gradient_top) { // приймає натяг зверху
this.axpbypc.grad = gradient_top;
this.addg1.backward(); // встановлює градієнт в axpby та c
this.addg0.backward(); // встановлює градієнт в ax by і
this.mulg1.backward(); // встановлює градієнт в b і y
this.mulg0.backward(); // встановлює градієнт в a і x
}
}


Це схема, яка просто обчислює a*x + b*y + c і може також обчислити градієнт. У ній використовується код логічних елементів, який ми створили в Главі 1. Тепер давайте запишемо SVM, яка не залежить від фактичної схеми. Для неї важливі не тільки значення, які виходять з неї, і вона підтягує схему.

// SVM клас
var SVM = function() {

// довільні значення початкового параметра
this.a = new Unit(1.0, 0.0); 
this.b = new Unit(-2.0, 0.0);
this.c = new Unit(-1.0, 0.0);

this.circuit = new Circuit();
};
SVM.prototype = {
forward: function(x, y) { // уявімо, що x і y є сегментами
this.unit_out = this.circuit.forward(x, y, this.a, this.b, this.c);
return this.unit_out;
},
backward: function(label) { // мітка дорівнює +1 або -1

// скидаємо натяг на a,b,c
this.a.grad = 0.0; 
this.b.grad = 0.0; 
this.c.grad = 0.0;

// обчислюємо натяг на підставі того, яким був результат схеми
var pull = 0.0;
if(label === 1 && this.unit_out.value < 1) { 
pull = 1; // значення було надто низьким: підтягуємо вгору
}
if(label === -1 && this.unit_out.value > -1) {
pull = -1; // значення було надто високим для позитивного прикладу, підтягуємо вниз
}
this.circuit.backward(pull); // записує градієнт в x,y,a,b,c

// додаємо регуляризационное натяг для параметрів: в бік нуля і пропорційно значенню 
this.a.grad += -this.a.value;
this.b.grad += -this.b.value;
},
learnFrom: function(x, y, label) {
this.forward(x, y); // прохід вперед (встановлюємо .value в усі сегменти)
this.backward(label); // зворотний прохід (встановлюємо .grad у всі сегменти)
this.parameterUpdate(); // параметри відповідають на поштовх 
},
parameterUpdate: function() {
var step_size = 0.01;
this.a.value += step_size * this.a.grad;
this.b.value += step_size * this.b.grad;
this.c.value += step_size * this.c.grad;
}
};


Тепер давайте використаємо SVM з допомогою спуску стохастичного градієнта:
var data = []; var labels = [];
data.push([1.2, 0.7]); labels.push(1);
data.push([-0.3, -0.5]); labels.push(-1);
data.push([3.0, 0.1]); labels.push(1);
data.push([-0.1, -1.0]); labels.push(-1);
data.push([-1.0, 1.1]); labels.push(-1);
data.push([2.1, -3]); labels.push(1);
var svm = new SVM();

// функція, яка розраховує точність класифікації
var evalTrainingAccuracy = function() {
var num_correct = 0;
for(var i = 0; i < data.length; i++) {
var x = new Unit(data[i][0], 0.0);
var y = new Unit(data[i][1], 0.0);
var true_label = labels[i];

// дивимося, чи збігається прогноз з наявною міткою
var predicted_label = svm.forward(x, y).value > 0 ? 1 : -1;
if(predicted_label === true_label) {
num_correct++;
}
}
return num_correct / data.length;
};

// петля навчання
for(var iter = 0; iter < 400; iter++) {
// обираємо довільну точку введення даних
var i = Math.floor(Math.random() * data.length);
var x = new Unit(data[i][0], 0.0);
var y = new Unit(data[i][1], 0.0);
var label = labels[i];
svm.learnFrom(x, y, label);

if(iter % 25 == 0) { // кожні 10 повторень... 
console.log('training accuracy at iter ' + iter + ': ' + evalTrainingAccuracy());
}
}


Цей код виводить наступний результат:

training accuracy at iteration 0: 0.3333333333333333
training accuracy at iteration 25: 0.3333333333333333
training accuracy at iteration 50: 0.5
training accuracy at iteration 75: 0.5
training accuracy at iteration 100: 0.3333333333333333
training accuracy at iteration 125: 0.5
training accuracy at iteration 150: 0.5
training accuracy at iteration 175: 0.5
training accuracy at iteration 200: 0.5
training accuracy at iteration 225: 0.6666666666666666
training accuracy at iteration 250: 0.6666666666666666
training accuracy at iteration 275: 0.8333333333333334
training accuracy at iteration 300: 1
training accuracy at iteration 325: 1
training accuracy at iteration 350: 1
training accuracy at iteration 375: 1 


Ми бачимо, що спочатку точність навчання нашого класифікатора була всього 33%, але до кінця навчання приклади правильно класифікуються по мірі того, як параметри a,b,c налаштовують свої значення у відповідності з прикладеною силою натягу. Ми тільки що навчили SVM! Але будь ласка, ніколи не використовуйте цей код на ділі:) Ми побачимо, як можна зробити все набагато ефективніше, коли розберемося, що, по суті, відбувається.

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

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

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

var a = 1, b = -2, c = -1; // початкові параметри
for(var iter = 0; iter < 400; iter++) {
// обираємо довільну точку введення даних
var i = Math.floor(Math.random() * data.length);
var x = data[i][0];
var y = data[i][1];
var label = labels[i];

// розраховуємо натяг
var score = a*x + b*y + c;
var pull = 0.0;
if(label === 1 && score < 1) pull = 1;
if(label === -1 && score > -1) pull = -1;

// обчислюємо градієнт і оновлюємо параметри
var step_size = 0.01;
a += step_size * (x * pull - a); // -a отримано в результаті регуляризації
b += step_size * (y * pull - b); // -b отримано в результаті регуляризації
c += step_size * (1 * pull);
}


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

Зробимо невеликі позначки з цього приводу: Можливо, ви помітили, що натяг завжди дорівнює 1,0 або -1. Ви можете уявити собі інші варіанти, наприклад, натяг пропорційно тому, наскільки серйозною була помилка. Це призводить до варіації на SVM, яку деякі люди називають квадратною кусково-лінійною функцією втрат SVM, чому — ви зрозумієте трохи пізніше. В залежності від різних характеристик вашого набору даних, які вона може працювати краще або гірше. Наприклад, якщо у вас дуже погані показники даних, наприклад, негативна точка введення даних, яка має значення +100, її вплив на класифікатор буде відносно незначним, так як ми будемо просто тягнути з силою -1 незалежно від того, наскільки серйозною була помилка. На практиці ми сприймаємо це властивість класифікатора як стійкість до показників.

Підіб'ємо підсумки. Ми ввели проблему бінарної класифікації, де нам дано N D-мірні вектори і мітка +1/-1 для кожного. Ми побачили, що ми можемо комбінувати ці характеристики з набором параметрів всередині схеми з реальними значеннями (наприклад, схеми Машини опорних векторів, як у нашому прикладі). Потім ми можемо багаторазово проводити наші дані через схему і кожен раз змінювати параметри таким чином, щоб вихідне значення схеми відповідало зазначеним мітках. Зміна залежить, що особливо важливо, від нашої здатності виконувати зворотне поширення помилки градієнтів через схему. Зрештою, остаточну схему можна використовувати для прогнозування значень невідомих прикладів!

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

0 коментарів

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