Нейронная Сеть Своими Руками С Нуля. Часть 2. Реализация

Как я сказал во введении к первая часть , я фронтенд-разработчик, и мой родной язык — JavaScript, на нем мы и будем реализовывать нашу нейросеть в этой статье.

Сначала несколько слов о конструкции.

За исключением различных вычисляемых свойств и методов, объект нейронной сети будет содержать массив слоев, каждый слой будет содержать массив нейронов, а каждый нейрон, в свою очередь, будет содержать массив «входов» — связей с нейронами входы предыдущего слоя.

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

Пойдем от частного к общему и сначала опишем входной класс для нейрона.

  
  
  
  
  
  
   

class Input { constructor(neuron, weight) { this.neuron = neuron; this.weight = weight; } }

Здесь все довольно просто.

Каждый вход имеет числовой вес и ссылку на нейрон предыдущего слоя.

Давайте двигаться дальше.

Опишем класс самого нейрона.



class Neuron { constructor(layer, previousLayer) { this._layer = layer; this.inputs = previousLayer ? previousLayer.neurons.map((neuron) => new Input(neuron, Math.random() - 0.5)) : [0]; } get $isFirstLayerNeuron() { return !(this.inputs[0] instanceof Input) } get inputSum() { return this.inputs.reduce((sum, input) => { return sum + input.neuron.value * input.weight; }, 0); } get value() { return this.$isFirstLayerNeuron ? this.inputs[0] : this._layer._network.activationFunction(this.inputSum); } set input(val) { if (!this.$isFirstLayerNeuron) { return; } this.inputs[0] = val; } set error(error) { if (this.$isFirstLayerNeuron) { return; } const wDelta = error * this._layer._network.derivativeFunction(this.inputSum); this.inputs.forEach((input) => { input.weight -= input.neuron.value * wDelta * this._layer._network.learningRate; input.neuron.error = input.weight * wDelta; }); } }

Давайте разберемся, что здесь происходит. Мы можем передать в конструктор нейрона два параметра: слой, на котором находится этот нейрон и, если это не входной слой нейронной сети, ссылку на предыдущий слой.

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

Если это входной слой сети, то массив входов будет состоять из одного числового значения, того самого, которое мы передадим на вход. $isFirstLayerNeuron — вычисляемый флаг, который сообщит нам, что это нейрон входного слоя.

Некоторые операции выполняются во всех слоях, кроме входного, поэтому нам это пригодится в дальнейшем.

входная сумма — свойство readonly, предоставляющее нам сумму всех входных сигналов (значений на выходе нейронов предыдущего слоя) с примененными к ним весами.

ценить - выходное значение нейрона.

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

И самое интересное - это сеттер ошибка .

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

Таким образом, установив ошибку на выходе сети, мы инициируем пересчет весов всех нейронных связей.

Теперь давайте посмотрим на класс слоя нейронной сети.

Здесь все гораздо проще.



class Layer { constructor(neuronsCount, previousLayer, network) { this._network = network; this.neurons = []; for (let i = 0; i < neuronsCount; i++) { this.neurons.push(new Neuron(this, previousLayer)); } } get $isFirstLayer() { return this.neurons[0].

$isFirstLayerNeuron; } set input(val) { if (!this.$isFirstLayer) { return; } if (!Array.isArray(val)) { return; } if (val.length !== this.neurons.length) { return; } val.forEach((v, i) => this.neurons[i].

input = v); } }

Конструктору передается число — количество нейронов, в соответствии с ним мы заполним массив нейронов слоя, ссылку на предыдущий слой для передачи ее конструктору нейронов и ссылку на саму сеть.

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

Это необходимо для заполнения входного слоя значениями.

И наконец, опишем класс самой нейронной сети.



class Network { static sigmoid(x) { return 1 / (1 + Math.exp(-x)); } static sigmoidDerivative(x) { return Network.sigmoid(x) * (1 - Network.sigmoid(x)); } constructor(inputSize, outputSize, hiddenLayersCount = 1, learningRate = 0.5) { this.activationFunction = Network.sigmoid; this.derivativeFunction = Network.sigmoidDerivative; this.learningRate = learningRate; this.layers = [new Layer(inputSize, null, this)]; for (let i = 0; i < hiddenLayersCount; i++) { const layerSize = Math.min(inputSize * 2 - 1, Math.ceil((inputSize * 2 / 3) + outputSize)); this.layers.push(new Layer(layerSize, this.layers[this.layers.length - 1], this)); } this.layers.push(new Layer(outputSize, this.layers[this.layers.length - 1], this)); } set input(val) { this.layers[0].

input = val; } get prediction() { return this.layers[this.layers.length - 1].

neurons.map((neuron) => neuron.value); } trainOnce(dataSet) { if (!Array.isArray(dataSet)) { return; } dataSet.forEach((dataCase) => { const [input, expected] = dataCase; this.input = input; this.prediction.forEach((r, i) => { this.layers[this.layers.length - 1].

neurons[i].

error = r - expected[i]; }); }); } train(dataSet, epochs = 100000) { return new Promise(resolve => { for (let i = 0; i < epochs; i++) { this.trainOnce(dataSet); } resolve(); }); } }

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

Сеттер вход - просто сахар, по сути вызывает одноименный установщик входного слоя.

Вычисляемое свойство прогноз - для этого все это и делалось.

Дает нам прогноз нейронной сети в виде массива значений нейронов выходного слоя на основе заданных входных данных.

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

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

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

Разработано как обещание только для хорошего синтаксиса.

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

then, даже если весь наш код уже будет выполнен в основном потоке.

Теперь у нас есть все, чтобы проверить нашу нейросеть в действии.

Давайте сделаем это на классическом примере — попробуем обучить сеть выполнять операцию XOR. Сначала давайте создадим экземпляр сети с двумя входными нейронами и одним выходным нейроном.



const network = new Network(2, 1);

Давайте установим набор обучающих данных:

const data = [ [[0, 0], [0]], [[0, 1], [1]], [[1, 0], [1]], [[1, 1], [0]], ];

Начнем процесс обучения, а затем проверим результат на тестовых данных.



network.train(data).

then(() => { const testData = [ [0, 0], [0, 1], [1, 0], [1, 1], ]; testData.forEach((input, index) => { network.input = input; console.log(`${input[0]} XOR ${input[1]} = ${network.prediction}`) }); });



Нейронная сеть своими руками с нуля.
</p><p>
 Часть 2. Реализация

Что ж, наша сеть дает правильный результат с очень хорошей точностью для двоичной операции.

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

Теги: #Популярная наука #программирование #нейронные сети #JavaScript #ml

Вместе с данным постом часто просматривают: