Garry's Mod

Garry's Mod

Not enough ratings
Пишем свою нейросеть на С++ | Перцептрон
By .infinity
НЕ душное руководство о том, как создать свою первую нейросеть на С++.
   
Award
Favorite
Favorited
Unfavorite
Введение
Привет всем читателям!
Я оооочень давно не выпускал руководства, и решил выкатить вам руководство по нейросетям!
Большейнство гайдов про нейросети такие:
  • Берём tensorflow(на питоне)
  • Делаем нейросеть.

А информация о том, как на самом деле работает нейросеть - разбросана по всему интернету.
И вот, я решил собрать всю информацию воедино, и создать гайд о нейросетях.

С этим гайдом мы сами напишем свою простую нейросеть на С++, с нуля.
К слову, данная нейросеть будет очень простой, и просто будет решать логические задачи линейной алгебры, типа AND, NAND и тп.

P.s. Вопрос о том, почему я пишу на С++ а не на питоне или JS краткий - я люблю скорость, и мне в целом нравится С++, когда будет время, напишу гайд для питона.

Ну штош, давайте начнем! :3
Основы С++
Самое главное - знать С++!
Вы можете легко и быстро изучть С++ с моим гайдом!
https://gtm.steamproxy.vip/sharedfiles/filedetails/?id=3390154519
Теория
Перед началом создания нейросети мы обсудим о том, что такое нейросеть.

Нейронные сети мы позаимствовали у природы. А точнее у живых существ.

Для чего нужны нейросети
Нейросети встречаются везде. Основная их функция - это управление различными частями организма в зависимости от изменения окружающих условий. В качестве примера можно рассмотреть механизм реакции на боль.

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

Эти болевые сенсоры - вход нашей нейронной сети.
А мышцы пальца - выход нейронной сети.

Таких механизмов очень много в теле (почти) любого живого существа.

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

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

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

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

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

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

Но есть одна проблема. Если все нейроны будут получать одинаковый сигнал, то они все будут отдавать одинаковое значение. Таким образом мы бы могли заменить всю систему на один нейрон.
Чтобы избавиться от данной проблемы, мы присвоим входу каждого нейрона определяющий вес. Этот вес будет определять, на сколько важен нейрону сигнал, получаемый от другого нейрона. И тут начинается самое интересное.

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

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

Включаем математику
В качестве активационной функции мы будем использовать сигмоиду. Она существует на отрезке от минус бесконечности, до плюс бесконечности и плавно меняет значение от 0 до 1.
Выглядит вот так:

sigmoid(x) = 1/(1 + exp(x))

Таким образом наш нейрон может принимать любую сумму сигналов входных нейроном и на выходе давать значение от 0 до 1.



Давайте теперь рассмотрим простой перцептрон, изображённый на картинке выше.
У нас есть нейросеть с 3 входными нейронами, 2 скрытыми и 1 выходным.

Входные нейроны будут иметь значения ИСТИНА, ЛОЖЬ, ИСТИНА.
Так как нейросеть у нас ещё не обучена, для весов мы будем брать случайные значения от -0.5 до 0.5.

Таким образом сумма входных значений первого нейрона скрытого слоя будет равна

1 * 0,43 + 0 * 0,18 + 1 * -0,21 = 0,22

Передав это значение в активационную функцию, мы получим значение, которое наш нейрон передаст далее по сети в следующий слой.

sigmoid(0,22) = 1 / (1 + e^-0,22) = 0,55

Аналогичные операции произведём для второго нейрона скрытого слоя и получим значение 0,60.

И, наконец, повторим эти операции для единственного нейрона в выходном слое нашей нейросети и получим значение 0,60, что мы условились считать как истину.

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

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

error = 0.60 - 0 = 0.60

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

sigmoid(x)dx = sigmoid(x)(1 / sigmoid(x))

Таким образом наша дельта весов будет равна

delta = 0.60 * (1 - 0.60) = 0.24

Новый вес для входа нейрона рассчитывается по формуле
weight = weight - output * delta * learning rate

Где weight - текущий вес, output - значение на выходе предыдущего нейрона, delta - дельта весов, которую мы рассчитали ранее и learning rate - значение, подбираемое экспериментально, от которого зависит скорость обучения нейросети. Если оно будет слишком маленьким - нейросеть будет более чувствительна к деталям, но будет обучаться слишком медленно и наоборот. Для примера возьмем learning rate равным 0,3. Итак новый вес для первого входа выходного нейрона будет равен:
w = 0,22 - 0,55 * 0,24 * 0,3 = 0,18

Аналогичным образом рассчитаем новый вес для второго входа выходного нейрона:

w = 0.47 - 0.60 * 0.24 * 0.3 = 0.43

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

error = 0.18 * 0.24 = 0.04

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

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

Практика
Как я говорил, мы напишем свою нейросеть на С++ с нуля.
Это простая нейросеть, и мы не будем гнаться за высокой эффективностью, так как это вообще никак не повлияет на скорость этой лёгкой нейросети.

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

Он сможет решать простые линейные функции по типу AND и NAND.
Но такие сложные нелинейные функции как XOR он уже выполнить не сможет. Именно данная проблема привела к "зиме" нейросетей, длившейся 20 лет.

Полный код вы можете посмотреть в главе "Полный код"

Для начала просто создайте базовую программу на С++. Создайте файл main.cpp, и перепишите этот код:
#include <iostream> using namespace std; int main() { return 0; }

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

Теперь подключите несколько базовых библиотек через макрос #include, а именно:
  • iostream - для вывода информации.
  • vector - для работы с векторами и матрицами.
  • cmath - для функции экспоненты.
  • ctime - для настройки рандома.

#include <iostream> #include <vector> #include <cmath> #include <ctime>

Основой нашего перцептрона будет одноимённый класс Perceptron. В нём будет конструктор.
class Perceptron { public: Perceptron() { } }

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

Perceptron(int inputs) : weights(inputs) { for(auto& w : weights) w = (float)rand()/RAND_MAX; }

Теперь добавим к этому классу два метода:

  • float predict() - для предсказания.
  • void train() - для обучения нейросети.

Выглядят они следующим образом:
float predict(const vector<float>& x) {
float sum = bias;
for(size_t i = 0; i < x.size(); i++)
sum += x[i] * weights[i];
return 1/(1 + exp(-sum));
}

void train(const vector<vector<float>>& data, const vector<float>& labels, float lr = 0.1, int epochs = 1000) {
for(int e = 0; e < epochs; e++)
for(size_t i = 0; i < data.size(); i++) {
float err = labels[i] - predict(data[i]);
for(size_t j = 0; j < weights.size(); j++)
weights[j] += err * lr * data[i][j];
bias += err * lr;
}
}

Понимаю! Выглядит страшно и непонятно. Щас разберём.



float predict() - делает предсказание на основе входных данных. На вход принимает вектор из чисел с плавающей точкой. По сути, это и есть наш 1 нейрон в нейросети. На основе его она как то работает.
В теоретической части я рассказывал про функцию. Тут мы находим сумму всех входных нейронов и применяем сигмоиду.

void train() - обучает нейросеть. Здесь есть простая реализация градиентного спуска. Теперь о данных, которая она принимает.
  • const vector<vector<float>>& data - матрица из чисел с плавающей точкой. Данные, на которых мы будем обучать нейросеть.
  • const vector<float>& labels - вектор чисел с плавающей точкой. Также данные для обучения нейросети. Но с их помощью мы будем корректирвоать веса.
  • float lr = 0.1 - скорость обучения. Меньше - медленнее, но точнее.
  • int epochs = 1000 - кол-во эпох обучений. Чем больше - тем дольше обучение, но точнее. Только если эпох слишком много, нейросеть может переобучиться. А при переобучении нейросеть не способна работать с данными, с которыми она никогда не работала.

Тут есть 2 цикла. Первый цикл - мы проходим по всем эпохам, обучая нейросеть. Это и есть цикл обучения. Повторяем его epochs раз.
Второй цикл - проходим по слоям.

Потом, находясь в слое, мы считаем ошибку.
err = правильный ответ - предсказание.

Обновляет веса:
Каждый вес weights[j] меняется на ошибка × скорость обучения × входное значение

Обновляет смещение bias:
Корректирует на ошибка × скорость обучения.

Всё это, чтобы уменьшить ошибку предсказания.

Ну, и самое главное - как использовать нейросеть?

Достаточно вписать это в функцию main()
srand(time(0)); Perceptron p(2); p.train({{0,0}, {0,1}, {1,0}, {1,1}}, {0,0,0,1}); for(auto& x : vector<vector<float>>{{0,0}, {0,1}, {1,0}, {1,1}}) cout << x[0] << " AND " << x[1] << " = " << p.predict(x) << endl;

Здесь мы создаем объект класса, задаем ему данные для обучения. {{0,0}, {0,1}, {1,0}, {1,1}} - все возможные бинарные комбинации. Решения к данным задачам - {0,0,0,1}, обычная задача AND. Только помните! Решает только линейные задачи.

Вывод после компиляции примерно такой:
0 AND 0 = 0.000183593 0 AND 1 = 0.0482138 1 AND 0 = 0.0484906 1 AND 1 = 0.933592

Как видим, задачу AND решает довольно уверенно.
Полный код
#include <iostream>
#include <vector>
#include <cmath>
#include <ctime>

using namespace std;

class Perceptron {
vector<float> weights;
float bias = 0;

public:
Perceptron(int inputs) : weights(inputs) {
for(auto& w : weights)
w = (float)rand()/RAND_MAX;
}

float predict(const vector<float>& x) {
float sum = bias;
for(size_t i = 0; i < x.size(); i++)
sum += x[i] * weights[i];
return 1/(1 + exp(-sum));
}

void train(const vector<vector<float>>& data, const vector<float>& labels, float lr = 0.1, int epochs = 1000) {
for(int e = 0; e < epochs; e++)
for(size_t i = 0; i < data.size(); i++) {
float err = labels[i] - predict(data[i]);
for(size_t j = 0; j < weights.size(); j++)
weights[j] += err * lr * data[i][j];
bias += err * lr;
}
}
};

int main() {
srand(time(0));
Perceptron p(2);
p.train({{0,0}, {0,1}, {1,0}, {1,1}}, {0,0,0,1});

for(auto& x : vector<vector<float>>{{0,0}, {0,1}, {1,0}, {1,1}})
cout << x[0] << " AND " << x[1] << " = " << p.predict(x) << endl;}

Вывод после компиляции примерно такой:
0 AND 0 = 0.000183593 0 AND 1 = 0.0482138 1 AND 0 = 0.0484906 1 AND 1 = 0.933592
Конец
Итог: Мы узнали что такое нейронные сети, и создали свою протсую нейронную сеть на С++ с нуля.
Конечно, не самая крутая нейросеть, но она хорошо демонстрирует работу нейросети.

Надеюсь, что вам понравилось данное руководство, и вы оцените его!
Желаю вам успехов в изучении и создании своих нейросетей!

И самое главное! Если вы поддержите меня хорошей оценкой к этому руководству, я создам ещё несколько гайдов по нейросетям, где мы будем разбирать многослойные нейронные сети и другие архитектуры, и сами создадим их на разных языках.


Другие мои руководства:
https://gtm.steamproxy.vip/sharedfiles/filedetails/?id=3390154519
https://gtm.steamproxy.vip/sharedfiles/filedetails/?id=3395692013
https://gtm.steamproxy.vip/sharedfiles/filedetails/?id=3400164684
https://gtm.steamproxy.vip/sharedfiles/filedetails/?id=3401446439
https://gtm.steamproxy.vip/sharedfiles/filedetails/?id=3389441554
7 Comments
[NN]Tekirinka 24 Jul @ 7:38am 
Привет! Решил наконец разобраться что такое нейросети и с чем их едят и мне попалось твое руководство!! СПАСИБО за то что уделил время и выложил такой гайдик для тех кто начинает только свое знакомство с нейросетями, знай, что еще много людей может его увидеть и как раз с этого начать свой путь. От души благодарю, удачи тебе)
.infinity  [author] 18 Feb @ 7:56am 
Я работал над ним 3 дня... Но ладно:steamsad:
Cpycь 18 Feb @ 7:55am 
лан заебись руководство
Cpycь 18 Feb @ 7:54am 
Как всегда дресня, брат!:steamthumbsup:
Tofca 16 Feb @ 11:26pm 
копай снег, найди траву
.infinity  [author] 16 Feb @ 7:08am 
У меня на улице 10см снега, какая трава блять
Tofca 16 Feb @ 7:04am 
Хватит гнить в своих руководствах, иди потрогай траву:steammocking: