Хабрахабр

[Перевод] Создаём простую нейросеть

Перевод Making a Simple Neural Network

Мы попробуем создать простую и совсем маленькую нейронную сеть, которую мы объясним и научим что-нибудь различать. Что мы будем делать? При этом не будем вдаваться в историю и математические дебри (такую информацию найти очень легко) — вместо этого постараемся объяснить задачу (не факт, что удастся) вам и самим себе рисунками и кодом.
Многие из терминов в нейронных сетях связаны с биологией, поэтому давайте начнем с самого начала:


Мозг — штука сложная, но и его можно разделить на несколько основных частей и операций:

Возбудитель может быть и внутренним (например, образ или идея):

А теперь взглянем на основные и упрощенные части мозга:


Мозг вообще похож на кабельную сеть.

Нейрон — основная единица исчислений в мозге, он получает и обрабатывает химические сигналы других нейронов, и, в зависимости от ряда факторов, либо не делает ничего, либо генерирует электрический импульс, или Потенциал Действия, который затем через синапсы подает сигналы соседним связанным нейронам:

Сны, воспоминания, саморегулируемые движения, рефлексы да и вообще все, что вы думаете или делаете — все происходит благодаря этому процессу: миллионы, или даже миллиарды нейронов работают на разных уровнях и создают связи, которые создают различные параллельные подсистемы и представляют собой биологическую нейронную сеть.

Разумеется, это всё упрощения и обобщения, но благодаря им мы можем описать простую
нейронную сеть:

И описать её формализовано с помощью графа:

Кружки — это нейроны, а линии — это связи между ними,
и, чтобы не усложнять на этом этапе, взаимосвязи представляют собой прямое передвижение информации слева направо. Тут требуются некоторые пояснения. Также мы присвоили ему число (1 — если он работает, 0 — если нет). Первый нейрон в данный момент активен и выделен серым. Числа между нейронами показывают вес связи.

Графы выше показывают момент времени сети, для более точного отображения, нужно разделить его на временные отрезки:

В качестве примера возьмем кролика (тестового кролика) и поставим его в условия классического эксперимента. Для создания своей нейронной сети нужно понимать, как веса влияют на нейроны и как нейроны обучаются.

Когда на них направляют безопасную струю воздуха, кролики, как и люди, моргают:

Эту модель поведения можно нарисовать графами:

Помимо этого мы вычисляем, срабатывает ли второй нейрон, основываясь на значении веса. Как и в предыдущей схеме, эти графы показывают только тот момент, когда кролик чувствует дуновение, и мы таким образом кодируем дуновение как логическое значение. Если он равен 1, то сенсорный нейрон срабатывает, мы моргаем; если вес меньше 1, мы не моргаем: у второго нейрона предел — 1.

Введем еще один элемент — безопасный звуковой сигнал:

Мы можем смоделировать заинтересованность кролика так:

Теперь научим кролика моргать по команде, смешивая
раздражители (звуковой сигнал и дуновение): Основное отличие в том, что сейчас вес равен нулю, поэтому моргающего кролика мы не получили, ну, пока, по крайней мере.

Важно, что эти события происходят в разные временные эпохи, в графах это будет выглядеть так:

Сам по себе звук ничего не делает, но воздушный поток по-прежнему заставляет кролика моргать, и мы показываем это через веса, умноженные на раздражители (красным).

Обучение сложному поведению можно упрощённо выразить как постепенное изменение веса между связанными нейронами с течением времени.

Чтобы обучить кролика, повторим действия:

Для первых трех попыток схемы будут выглядеть так:

30, но число может быть каким угодно, даже отрицательным. Обратите внимание, что вес для звукового раздражителя растет после каждого повтора (выделено красным), это значение сейчас произвольное — мы выбрали 0. После третьего повтора вы не заметите изменения в поведении кролика, но после четвертого повтора произойдет нечто удивительное — поведение изменится.

Объяснить это поведение может наша последняя схемка: Мы убрали воздействие воздухом, но кролик все еще моргает, услышав звуковой сигнал!

Мы обучили кролика реагировать на звук морганием.


В условиях реального эксперимента такого рода может потребоваться более 60 повторений для достижения результата.

Для начала попробуем сделать простую задачу. Теперь мы оставим биологический мир мозга и кроликов и попробуем адаптировать всё, что
узнали, для создания искусственной нейросети.

Задача — узнать, какая кнопка выдает вознаграждение: Допустим, у нас есть машина с четырьмя кнопками, которая выдает еду при нажатии правильной
кнопки (ну, или энергию, если вы робот).

Мы можем изобразить (схематично), что делает кнопка при нажатии следующим образом:

Такую задачу лучше решать целиком, поэтому давайте посмотрим на все возможные результаты, включая правильный:


Нажмите на 3-ю кнопку, чтобы получить свой ужин.

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

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


Обратите внимание, что все веса равны 0, поэтому нейронная сеть, как младенец, совершенно пуста, но полностью взаимосвязана.

Оно может совпадать или не совпадать с реальностью, но это мы пока проигнорируем и начнем описывать задачу понятным компьютеру способом. Таким образом мы сопоставляем внешнее событие с входным слоем нейронной сети и вычисляем значение на ее выходе. Начнем с ввода весов (будем использовать JavaScript):

var inputs = [0,1,0,0];
var weights = [0,0,0,0]; // Для удобства эти векторы можно назвать

Следующий шаг — создание функции, которая собирает входные значения и веса и рассчитывает значение на выходе:

function <b>evaluateNeuralNetwork</b>(inputVector, weightVector)); return (result.toFixed(2)); } // Может казаться комплексной, но все, что она делает — это сопоставляет пары вес/ввод и добавляет результат

Как и ожидалось, если мы запустим этот код, то получим такой же результат, как в нашей модели или графике…

evaluateNeuralNetwork(inputs, weights); // 0.00

Живой пример: Neural Net 001.

Следующим шагом в усовершенствовании нашей нейросети будет способ проверки её собственных выходных или результирующих значений сопоставимо реальной ситуации,
давайте сначала закодируем эту конкретную реальность в переменную:

Чтобы обнаружить несоответствия (и сколько их), мы добавим функцию ошибки:

Error = Reality - Neural Net Output

С ней мы можем оценивать работу нашей нейронной сети:

Но что более важно — как насчет ситуаций, когда реальность дает положительный результат?

А здорово это потому, что теперь мы можем использовать функцию ошибки для управления нашим обучением. Теперь мы знаем, что наша модель нейронной сети не работает (и знаем, насколько), здорово! Но всё это обретет смысл в том случае, если мы переопределим функцию ошибок следующим образом:

Error = <b>Desired Output</b> - Neural Net Output

Неуловимое, но такое важное расхождение, молчаливо показывающее, что мы будем
использовать ранее полученные результаты для сопоставления с будущими действиями
(и для обучения, как мы потом увидим). Это существует и в реальной жизни, полной
повторяющихся паттернов, поэтому оно может стать эволюционной стратегией (ну, в
большинстве случаев).

Далее в наш пример кода мы добавим новую переменную:

var input = [0,0,1,0];
var weights = [0,0,0,0];
var desiredResult = 1;

И новую функцию:

function <b>evaluateNeuralNetError</b>(desired,actual) { return (desired — actual);
}
// After evaluating both the Network and the Error we would get:
// "Neural Net output: 0.00 Error: 1"

Живой пример: Neural Net 002.

Мы начали с задачи, сделали её простую модель в виде биологической нейронной сети и получили способ измерения её производительности по сравнению с реальностью или желаемым результатом. Подведем промежуточный итог. Теперь нам нужно найти способ исправления несоответствия — процесс, который как и для компьютеров, так и для людей можно рассматривать как обучение.

Как обучать нейронную сеть?

Начнем с
обучающих алгоритмов. Основа обучения как биологической, так и искусственной нейронной сети — это повторение
и алгоритмы обучения, поэтому мы будем работать с ними по отдельности.

В природе под алгоритмами обучения понимаются изменения физических или химических
характеристик нейронов после проведения экспериментов:

Поэтому давайте добавим переменную для обозначения степени облегчения жизни: Драматическая иллюстрация того, как два нейрона меняются по прошествии времени в коде и нашей модели «алгоритм обучения» означает, что мы просто будем что-то менять в течение какого-то времени, чтобы облегчить свою жизнь.

var learningRate = 0.20;
// Чем больше значение, тем быстрее будет процесс обучения :)

И что это изменит?

Это изменит веса (прям как у кролика!), особенно вес вывода, который мы хотим получить:

Как кодировать такой алгоритм — ваш выбор, я для простоты добавляю коэффициент обучения к весу, вот он в виде функции:

function <b>learn</b>(inputVector, weightVector) { weightVector.forEach(function(weight, index, weights) { if (inputVector[index] > 0) { <b>weights[index] = weight + learningRate;</b> } });
}

При использовании эта обучающая функция просто добавит наш коэффициент обучения к вектору веса активного нейрона, до и после круга обучения (или повтора) результаты будут такими:

// Original weight vector: [0,0,0,0]
// Neural Net output: 0.00 Error: 1
<b>learn(input, weights);</b>
// New Weight vector: [0,0.20,0,0]
// Neural Net output: 0.20 Error: 0.8
// Если это не очевидно, вывод нейронной сети близок к 1 (выдача курицы) — то, чего мы и
хотели, поэтому можно сделать вывод, что мы движемся в правильном направлении

Живой пример: Neural Net 003.

Окей, теперь, когда мы движемся в верном направлении, последней деталью этой головоломки будет внедрение повторов.

Это не так уж и сложно, в природе мы просто делаем одно и то же снова и снова, а в коде мы просто указываем количество повторов:

var trials = 6;

И внедрение в нашу обучающую нейросеть функции количества повторов будет выглядеть так:

function <b>train</b>(trials) {
for (i = 0; i < trials; i++) { neuralNetResult = evaluateNeuralNetwork(input, weights); learn(input, weights);
}
}

Ну и наш окончательный отчет:

Neural Net output: 0.00 Error: 1.00 Weight Vector: [0,0,0,0]
Neural Net output: 0.20 Error: 0.80 Weight Vector: [0,0,0.2,0]
Neural Net output: 0.40 Error: 0.60 Weight Vector: [0,0,0.4,0]
Neural Net output: 0.60 Error: 0.40 Weight Vector: [0,0,0.6,0]
Neural Net output: 0.80 Error: 0.20 Weight Vector: [0,0,0.8,0]
Neural Net output: 1.00 Error: 0.00 Weight Vector: [0,0,1,0]
// Chicken Dinner !

Живой пример: Neural Net 004.

Теперь у нас есть вектор веса, который даст только один результат (курицу на ужин), если входной вектор соответствует реальности (нажатие на третью кнопку).

Так что же такое классное мы только что сделали?

В этом конкретном случае наша нейронная сеть (после обучения) может распознавать входные данные и говорить, что приведет к желаемому результату (нам всё равно нужно будет программировать конкретные ситуации):

Мы смогли узнать что-то новое о машинном обучении, нейронных сетях и искусственном интеллекте. Кроме того, это масштабируемая модель, игрушка и инструмент для нашего с вами обучения.

Предостережение пользователям:

  • Механизм хранения изученных весов не предусмотрен, поэтому данная нейронная сеть забудет всё, что знает. При обновлении или повторном запуске кода нужно не менее шести успешных повторов, чтобы сеть полностью обучилась, если вы считаете, что человек или машина будут нажимать на кнопки в случайном порядке… Это займет какое-то время.
  • Биологические сети для обучения важным вещам имеют скорость обучения 1, поэтому нужен будет только один успешный повтор.
  • Существует алгоритм обучения, который очень напоминает биологические нейроны, у него броское название: правило widroff-hoff, или обучение widroff-hoff.
  • Пороги нейронов (1 в нашем примере) и эффекты переобучения (при большом количестве повторов результат будет больше 1) не учитываются, но они очень важны в природе и отвечают за большие и сложные блоки поведенческих реакций. Как и отрицательные веса.

Заметки и список литературы для дальнейшего чтения

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

Лучше начинать с Neuroscience (Purves) и Cognitive Neuroscience (Gazzaniga). Биологическое строение мозга — тема не простая, отчасти из-за неточности, отчасти из-за его сложности. Я изменил и адаптировал пример с кроликом из Gateway to Memory (Gluck), которая также является прекрасным проводником в мир графов.

Еще один шикарный ресурс An Introduction to Neural Networks (Gurney), подойдет для всех ваших нужд, связанных с ИИ.

Спасибо Илье Андшмидту за предоставленную версию на Python: А теперь на Python!

inputs = [0, 1, 0, 0]
weights = [0, 0, 0, 0]
desired_result = 1
learning_rate = 0.2
trials = 6 def evaluate_neural_network(input_array, weight_array): result = 0 for i in range(len(input_array)): layer_value = input_array[i] * weight_array[i] result += layer_value print("evaluate_neural_network: " + str(result)) print("weights: " + str(weights)) return result def evaluate_error(desired, actual): error = desired - actual print("evaluate_error: " + str(error)) return error def learn(input_array, weight_array): print("learning...") for i in range(len(input_array)): if input_array[i] > 0: weight_array[i] += learning_rate def train(trials): for i in range(trials): neural_net_result = evaluate_neural_network(inputs, weights) learn(inputs, weights) train(trials)

А теперь на GO! За эту версию благодарю Кирана Мэхера.

package main import ( "fmt" "math"
) func main() { fmt.Println("Creating inputs and weights ...") inputs := []float64{0.00, 0.00, 1.00, 0.00} weights := []float64{0.00, 0.00, 0.00, 0.00} desired := 1.00 learningRate := 0.20 trials := 6 train(trials, inputs, weights, desired, learningRate)
} func train(trials int, inputs []float64, weights []float64, desired float64, learningRate float64) { for i := 1; i < trials; i++ { weights = learn(inputs, weights, learningRate) output := evaluate(inputs, weights) errorResult := evaluateError(desired, output) fmt.Print("Output: ") fmt.Print(math.Round(output*100) / 100) fmt.Print("\nError: ") fmt.Print(math.Round(errorResult*100) / 100) fmt.Print("\n\n") } } func learn(inputVector []float64, weightVector []float64, learningRate float64) []float64 { for index, inputValue := range inputVector { if inputValue > 0.00 { weightVector[index] = weightVector[index] + learningRate } } return weightVector
} func evaluate(inputVector []float64, weightVector []float64) float64 { result := 0.00 for index, inputValue := range inputVector { layerValue := inputValue * weightVector[index] result = result + layerValue } return result
} func evaluateError(desired float64, actual float64) float64 { return desired - actual
}

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»