Introduction
In this article I’ll present a beginner-oriented framework implementing neural networks in C++. The main goal of this code is to understand the root of neural networks for beginners, it also allows multiple modifications which are usually hard to implement in classical high-level framework (Pytorch, Tensorflow, Keras etc..).
So this code can also be used for some research because you can also do things you couldn’t do in other frameworks.
In this article:
- Features of the framework
- Neural networks from scratch
- Framework design
- Exotic backpropagation-free optimizer: Shaking Tree optimizer
- What I learned doing this
- Github repository
1. Features of the framework
- Graph implementation of neural network (no matrices)
- Neural network, neurons and edges as explicit objects
- Only multilayer perceptrons with Linear/ReLU/Sigmoid activations
- No parallel computations, no optimizations, no convolutional layers
- Exotic backpropagation-free optimizer
2. Neural networks from scratch
Neural networks can be interpreted in two ways. The “graph” way and the “matrix” way. When you begin with neural networks, people usually teach you the “graph” way:
That’s how neural networks are usually represented, vertices(neurons) + oriented edges(dendrites), a directed graph. But in practice they’re used with matrices because computations are much faster on GPUs that way.
For complete beginners, you can try playing with neural network on this webpage. The easy version of how a neural network does the forward pass is as follows:
(0) create a neural network following the above architecture
(1) put values in the input neurons (exp: 3 neurons with value 1, 5 and 3)
(2) trigger the input layer. The values are multiplied with a weight linking each input neuron to each neuron of the 2nd layer (3neurons*4neurons=12scalars/weights/edges). Then sum the 3 inputs*weights for each of the 4 neurons of the 2nd layer. (exp: 1*a1+5*b1+3*c1 for 1st neuron of 2nd layer, then 1*a2+5*b2+3*c2 etc.)
(3) trigger the 2nd layer and do (2) between the 3rd layer and the 2nd layer (the same way it was done between the 2nd and the 1st layer)
(4) read the two values accumulated by the 3rd/output layer
The forward pass is easy. The same computations can be done with matrices. What’s harder to understand and important to re-implement is the backward pass, the method used to learn the right weights. The inputs [1, 5, 3] and outputs of each layer depend on the data we’re processing. The weights [a1, b1, c1, a2..] are here to process all inputs correctly. We’ll focus on finding the good weights.
In this repo, neural networks are implemented in a graph way. Some Github repositories of that kind exist because people — including me — think it’s a great way to be sure you understood how neural networks work, and it’s also a great way to play with neural networks.
This repo is a beginning of neural network framework in C++ because it’s more elaborated than “just the neural-network part” in one file. It’s not even 0.01% of what libs like dlib are, and it’s not meant to be a framework that would serve anything but playing around and understanding neural networks.
3. Framework Design
- Neural: Contains classes related to the neural network
- Dataset: Contains one simple class for reading a dataset
- Optimizer: Contains a generic optimizer class, a classical backpropagation optimizer and an exotic optimizer
- Misc: Contains activation functions and their derivatives
2. Neural Classes
- NeuralNetwork.h/.cpp: the main class containing a model. A NeuralNetwork is composed of multiple layers
- Layer.h/.cpp: A Layer is composed of multiple neurons. It has a type (INPUT/STANDARD/OUTPUT) because it doesn’t work the same way depending on its type and it has an activation function
- Neuron.h/.cpp: A Neuron has a parent layer. A neuron accumulates the output of the edges connected to it (_accumulated), it outputs that input to its edges after processing it with its _activation_function (which you can change on a per-neuron basis). A neuron knows its _next and _previous edges.
- Edge.h/.cpp: An Edge knows its next neuron (_n), the neuron its coming from (_nb), it has a weight (_w), it can memorize how much its weight was shift (_last_shift) last time it was, and it has a backpropagation memory so that it can retain a part of the chain rule
3. Dataset / Optimizer
- Dataset.h/.cpp: It gives the data. One input/output is just a vector (again, no matrices, no images). So the list of all inputs is a vector of vector. The Dataset class is also responsible for doing the train/test split.
- Optimizer.h/.cpp: Abstract class that links the network and the dataset together. Child classes must implement a “minimize” method that is called during the optimization process to minimize the loss.
- Backpropagation.h/.cpp: Implementation of the backpropagation optimizer (though the backpropagation computations are in the neuron class for simplification). This class is mostly there to call these methods in the neuron class and to apply the new computed weights. It uses a batch size, the learning rate is a global variable because it’s common to all implemented optimizers and it’s easier to access it from anywhere.
- ShakingTree.h/.cpp: Exotic optimizer that’ll be detailed later.
Computation example
The algorithm I provide was also made to exactly reproduce the “famous” article from Matt Mazur on his website: https://mattmazur.com/2015/03/17/a-step-by-step-backpropagation-example/
Just use the “mattmazur_step_by_step” branch on git, this branch serves only that purpose and contains older code than the code on “master”, but the backpropagation part works the same way.
You can add logs wherever you want to understand the computations following the blog post from mattmazur. Mostly you can uncomment the cout line in the “getBackpropagationShifts” method in neuron.cpp.
As you can see, the results are exactly the same he got.
Building a Neural Network
It’s pretty much straightforward:
Instanciate the class. Add an input layer, specify the number of neurons (size). Then add hidden layers (standard), specify the number of neurons (size=5 neurons) and an activation function (sigmoid). Finally, add an output layer, its size (1 output value) and an activation function (sigmoid).
Yeah, C++ can have a quite great readability now.
The “autogenerate” adds the edges (connectComplete) so that the layers are fullyconnected, and it also randomizes all the weights:
Predicting a value
That’s also an easy one:
The inputs are set up in the _accumulated variable of the input neurons, then trigger() calls each layer to fire successively, it computes the output of that layer and put it in the input of the neurons in the next layer. Finally the output() just returns the activation_function(accumulated) for each neuron of the last/output layer.
Backpropagation
The chain rule is implemented in the getBackpropagationShifts() methods in neuron.cpp.
___ About backpropagation. Going into details wouldn’t serve much as you mostly only need to read the guide from mattmazur to understand what’s being done in here mathematically.
Put simply, backpropagation is about computing how to alter the weights such that the output of the network corresponds to the data you’re trying to do a prediction on. If we have high values in the output but the values we’re trying to predict are close to zero, backpropagation will make the weights closer to zero.
How? We have the output we made and the right prediction (the target). We can predict the error between these two (error = target — output). And as everything is only a succession of mathematical operations like y = f(x) and is differentiable, we can compute the partial derivative of the error with respect to the weights (basically Δerr/Δw).
Which means we know how influencing a weight can influence the error, for the current data we’re reading and based on the weights we were using.
(new_weight) will be (old_weight — Δerr/Δw).
___ About the code. You need the target (the values we’re trying to learn) to do the computations for the output layer (the “if” block), then you need to keep in memory a part of the computations to apply the chain rule in all other layers (the “else” block).
dw stands for delta_weight, so for each neuron you’re essentially computing a delta to apply to each of its _previous edges, it’s backpropagation starting from the output layer.
The chain rule puts Δerr/Δw in three operations which gives the three d1, d2 and (d3) the previous output you have in the code (the line in if section with dw[i] = …). Basically dw[i] = Δerr/Δw.
Functions are named so that they should be easy to read and to understand.
That’s basic backpropagation. No momentum, no tricks, nothing but gradients. The learning rate applies when we add the delta_weight to the weight (shiftBackWeights method of Neuron calls shiftWeight of Edge).
4. Exotic backpropagation-free optimizer: ShakingTree optimizer
Here I’ll introduce a new kind of exotic optimizer which I named “Shaking Tree optimizer”.
Why “ShakingTree”? Because we’re going to randomly change the weights, like if we were shaking the graph as a tree or if we were randomly shaking some branches so that only the weaker ones are replaced by greater ones.
Theory
The goal is of course to do better than a backpropagation algorithm. How does backprop work? (1) do the forward pass (2) compute the loss (3) do complex computations to evaluate the gradients (4) loop.
Can you change the weights of a neural network without doing a forward pass to compute the loss? Well at some points you need to evaluate what you’re doing and how it impacts the loss (1)+(2), so the main thing I wanted to remove is the gradients computations (3).
But I still need a way to change the weights. Ok, some people tried genetic algorithms, it works but it’s quite random, and of course the more weights, the harder it gets to find the right values. but that’s also true with vanishing gradients. We know that gradients give a great estimation of how to change the weights of a neural network. The main idea of ShakingTree is to try to approximate the gradients (OR something that would be at least as great as gradients) from random values without explicitly computing the gradients.
So, is it worth it to quickly try random values? Or is it better to slowly compute gradients? Is there a way to be quick and compute “random” values that end up being better than the gradients? That’s what I tried to answer.
I tried two methods, a “basic” one and a “complex” one.
- Basic method
- Evaluate the score with current weights on a part of the dataset
- Save current weights
- Apply new weights randomly (amplitude, distribution, number of weights etc.. many hyperparameters)
- Re-evaluate the score on (another?) part of the dataset
- If the new score is worst, re-apply old weights
2. Complex method
- Evaluate the score with current weights
- Generate a delta to be applied to all weights (this delta is meant to act like the gradients)
- Apply the delta weight, evaluate it, save the delta score and the delta weight
- Now technically you can evaluate a kind of f(delta_weight) = delta_score (that’s what the gradients aim to be!)
- Each N iterations, average the delta_weight of the weights that reduced the score (delta_score < 0), and apply that delta
That’s all for the theory, now you know the idea, what are the resuls?
In practice
The dataset contains two 2d-gaussian distribution, so input is size 2 and output is size 1 (<0.5 is first gaussian, ≥ 0.5 is the second one), so that’s far from any real-world use case for deep learning algorithms. I don’t tune hyerparameters much. Meaning I’m pretty sure any of these curves could be higher or lower than the others with tuning but as we’re not in a real-world use case, it doesn’t really matter. The neural network contains five 10-neurons hidden layer with sigmoid activations (again, probably far from real-world use case).
Why is it noisy? Because it’s random and it can sometimes quickly get to bad results. The fact that it goes from bad results to good results partially shows how easy the task is (but changing the final output layer parameters’ can also quickly give bad results and changing them back again can give good results again quickly). minimizeBasicLarger2 and minimizeBasic use the “basic” method of ShakingTree with different hyperparameters. Backpropagation is in blue and the complex version of ShakingTree is in red.
The first hyperparameters I chose for minimizeBasicLarger gave much more random results so I changed them once or twice, giving the green curve. I didn’t change the parameters of the other algorithms on that particular task.
Keeping only the best iteration:
Limitations
Just to be clear, be sure that these algorithms do bad outside of their confort zone, which, again, to be honest, would be the confort zone of many other algorithms that don’t scale. For example, the minimizeBasic algorithm changes one weight per iteration, which is ok in a neural network with 500 weights but you sure can’t use that version for a neural network with 5M parameters. And if you randomly change 500.000 weights, you’re less prone to find a great configuration by chance, meaning you have to use smarter metrics.
These ShakingTree algorithms with their worst hyperparameters could probably look like a random search but the added hyperparameters are there to avoid being too close to a random search. These algorithms could probably also be approximated by genetic algorithms, but the same apply, it’s not exactly the same algorithm.
We’re not doing much better than any algorithm that probably performs well on MNIST and actually proving that these algorithms work on these datasets is far from being able to prove that they work on real-life datasets. But maybe with some hyperparameter tuning and with a more tensor-oriented approach it could be applied to convolutional filters on a more standard framework like Pytorch.
This part of the article is mainly a proposition for a new backpropagation-free algorithm. There are many other backprop-free algorithms that could perform better on small datasets with small networks, but which probably are always worst than backprop on larger datasets with larger networks.
Создание простого ИИ на C# в Unity
Почти любой игре необходим взаимодействующий с пользователем искусственный интеллект (AI), чаще всего в виде враждебной игроку силы. В некоторых случаях ИИ должен помогать игроку, в других — бороться с ним, но у всех управляемых компьютером персонажей существует некоторое сходство. В зависимости от требований проекта ИИ может использовать простые или сложные поведения. Такими требованиями могут быть дипломатия с другим игроком или простое блуждание вперёд-назад по платформе. Как бы то ни было, необходимо сделать так, чтобы ИИ качественно выполнял свою работу.
В этом проекте я продемонстрирую очень простой искусственный интеллект. Допустим, мы создаём игру, в которой игрок должен тайком пробраться рядом с вражеским штабом. Когда игрока замечает камера слежения, рядом создаются враги и в течение короткого промежутка времени преследуют игрока. Именно это мы и реализуем в проекте на простейшем уровне. Завершив проект, вы получите управляемый объект игрока, круг, используемый в качестве камеры врага, и объект врага, который будет преследовать игрока, когда о его присутствии сообщит объект камеры.
Подготовка
Для начала нам нужно создать 3D-проект. Нажмите на кнопку New в верхней части окна после запуска Unity, как это показано на рисунке 1.

Рисунок 1: создание нового проекта
Назовите свой проект AI и убедитесь, что он является 3D-проектом. Выбрав место на компьютере для хранения проекта, нажмите на кнопку Create Project внизу, показанную на рисунке 2.

Рисунок 2: экран настройки проекта
После создания проекта нам первым делом нужно настроить папки в окне Assets, чтобы упорядочить свою работу. Нажмите правой кнопкой на окне Assets и выберите Create → Folder для создания новой папки. Назовите эту папку Materials. Затем создайте вторую папку и назовите её Scripts. На рисунке 3 показано, как это должно выглядеть.

Рисунок 3: создание новой папки
После всего этого окно Assets должно выглядеть так, как показано на рисунке 4.

Рисунок 4: окно Assets.
Далее создадим пол, на котором будут стоять все объекты. В окне Hierarchy выберите Create → 3D Object → Plane, чтобы создать объект-плоскость, которая будет использоваться в качестве пола.

Рисунок 5: создание объекта Plane.
Назовите этот объект Floor и измените его значение X Scale на 7, а значение Z Scale — на 3. После этого окно Inspector с выбранным объектом Floor должно выглядеть так, как показано на рисунке 6.

Рисунок 6: задание свойств объекта Floor.
Теперь нам нужно создать новый материал для Floor, чтобы отличать его от остальных объектов, которые будут размещены в сцене. В папке Materials окна Assets создайте новый материал, нажав правой кнопкой на окно Assets и выбрав Create → Material.

Рисунок 7: создание нового материала
После завершения назовите материал Floor.

Рисунок 8: материал Floor.
В верхней части окна Inspector с выбранным материалом Floor выберите color picker.

Рисунок 9: выбор color picker.
Разумеется, вы можете выбрать для пола любой цвет, но в этом примере я выбрал красно-коричневый цвет, как показано на рисунке 10.

Рисунок 10: color picker.
Выберите объект Floor в окне Hierarchy, и в компоненте Mesh Renderer выберите маленькую стрелку рядом с Materials.

Рисунок 11: подготовка к изменению материала.
Перетащите материал Floor из окна Assets в поле Element 0 компонента Mesh Renderer в окне Inspector.

Рисунок 12: задание материала Floor в качестве материала объекта Floor.
Закончив с объектом Floor, мы дожны создать вокруг области стены, чтобы игрок не мог свалиться с края. Снова заходим в Create → 3D Object → Plane для создания новой плоскости. Назовём эту плоскость Wall и выставим ей те же размеры, что и у Floor, то есть X Scale со значением 7 и Z Scale со значением 3. Затем создадим ещё три стены, выбрав объект и трижды нажав Ctrl + D. После этого разместим стены вокруг пола в соответствии с данными из таблицы.
| Название | Position X | Position Y | Position Z | Rotation X | Rotation Z |
| Wall | -35 | 21 | 0 | 0 | -90 |
| Wall (1) | -1 | 11 | -15 | 90 | 0 |
| Wall (2) | -1 | 11 | 13.5 | -90 | 0 |
| Wall (3) | 34 | 21 | 0 | 0 | 90 |
Таблица 1: позиции и повороты всех объектов Wall.
Завершив всё это, нужно изменить положение камеры, чтобы она смотрела на пол сверху. Выберите объект Main Camera и задайте для Y Position значение 30, для Z Position значение 0, а X Rotation — значение 80.

Рисунок 13: настройка объекта камеры.
Сцена подготовлена, поэтому настало время создания персонажа игрока. В окне Hierarchy нажмите на Create → 3D Object → Sphere, чтобы создать объект-сферу. Назовите этот объект Player, а затем нажмите на кнопку Add Component в нижней части окна Inspector.

Рисунок 14: добавление нового компонента.
Теперь найдите Rigidbody. После этого выберите из списка компонент Rigidbody и добавьте Rigidbody к объекту Player.

Рисунок 15: добавление компонента Rigidbody.
Далее нужно присвоить игроку тэг, который позже пригодится нам в коде. Нажмите на раскрывающееся меню Tag в левом верхнем углу окна Inspector и выберите тэг Player.

Рисунок 16: задание нового тэга.
Нам нужно задать позицию игрока, чтобы он не находился под объектом Floor. В примере я расположил игрока в левом верхнем углу с X position равным 26, Y Position равным 1, и Z position равным -9.

Рисунок 17: размещение игрока.
Чтобы наш будущий код работал правильно, нам, разумеется, нужно прикрепить его к объекту. Снова заходим в окно Hierarchy и на этот раз выбираем Create → 3D Object → Cube. Назовём этот куб Guard, добавим к нему компонент Rigidbody и компонент NavMesh Agent с помощью кнопки Add Component в окне Inspector. Далее поместим его где-нибудь в верхнем левом углу сцены. После этого окно Inspector объекта Guard будет выглядеть следующим образом:

Рисунок 18: объект Guard в окне Inspector.
И этот объект должен быть расположен так:

Рисунок 19: Размещение объекта Guard.
Наконец, нам потребуется объект, используемый в качестве «глаз» объекта Guard, который будет уведомлять Guard о том, что его касается игрок. В последний раз перейдите в окно Hierarchy и выберите Create → 3D Object → Sphere для создания ещё одного объекта-сферы. Назовите этот объект Looker. На этот раз нам не нужно добавлять к нему никаких других компонентов. Однако мы изменим размер объекта. Выбрав Looker, измените следующие переменные компонента Transform в окне Inspector.
- Scale Xна 9.
- Scale Y на 0.5.
- Scale Z на 9.

Рисунок 20: размещение объекта Looker.
Настало подходящее время для того, чтобы придать Looker уникальный материал, чтобы было заметно, что его стоит избегать. В папке Materials окна Assets нажмите правой клавишей мыши и создайте новый материал. Назовите его Looker и задайте ему ярко-красный цвет. После этого назначьте этот материал в качестве материала объекта Looker, чтобы изменить его цвет. После этого сцена должна выглядеть следующим образом:

Рисунок 21: объект Looker с новым материалом.
Единственное, что нам осталось — создать навигационный меш для Guard, по которому он сможет перемещаться. В верхней части редактора Unity есть меню Window. Выберите Window → Navigation, чтобы открыть окно Navigation, показанное на рисунке 22.

Рисунок 22: окно Navigation.
Выберите объект Floor в Hierarchy, а затем в окне Navigation поставьте флажок Navigation Static.

Рисунок 23: Navigation Static.
Далее выберем опцию Bake в верхней части окна.

Рисунок 24: переключение на меню Bake.
Откроется меню Bake, в котором можно изменять свойства навигационного меша, который мы собираемся создать. В нашем примере ничего изменять не требуется. Достаточно нажать на кнопку Bake в правой нижней части.

Рисунок 25: создание нового навигационного меша.
На этом этапе Unity попросит сохранить сцену. Сохраните её, после чего будет создан навигационный меш. Теперь сцена будет выглядеть так:

Рисунок 26: текущая сцена с добавленным навигационным мешем.
Теперь всё в Unity настроено, поэтому настало время для создания скриптов, необходимых для работы проекта. В окне Assets нажмите правой клавишей мыши и выберите Create → C# Script. Назовите этот скрипт Player. Повторите эту операцию ещё два раза, создав скрипты с названиями Guard и Looker.

Рисунок 27: создание нового скрипта.
После этого папка Scripts в окне Assets будет выглядеть так:

Рисунок 28: папка Scripts.
Первым мы начнём писать код скрипта Player. Дважды щёлкните по скрипту Player в окне Assets, чтобы открыть Visual Studio и приступить к созданию кода.
Скрипт Player достаточно прост, всё что он делает — позволяет пользователю перемещать объект-мяч. Под объявлением класса нам нужно получить ссылку на компонент Rigidbody, который мы ранее создали в проекте.
Сразу после этого в функции Start мы прикажем Unity сделать текущий компонент Rigidbody объекта Player значением rb.
После этого скрипт Player будет выглядеть так:

Рисунок 29: скрипт Player на текущий момент.
Теперь, когда значение rb присвоено, нам нужно позволить объекту Player двигаться при нажатии клавиш со стрелками. Для перемещения объекта мы будем использовать физику, применяя силу к объекту при нажатии пользователем клавиш со стрелками. Для этого достаточно добавить в функцию Update следующий код:
На этом мы завершили скрипт Player. Готовый скрипт будет выглядеть следующим образом:

Рисунок 30: готовый скрипт Player.
Сохраните свою работу и вернитесь в Unity. На этот раз выберите в окне Assets скрипт Guard. Чтобы заставить код для Guard работать, нужно добавить в верхнюю часть скрипта конструкцию using.
Далее объявим следующие переменные сразу после объявления класса.
В качестве значения переменной player объекта Guard используется объект Player. Она пригодится нам позже, когда мы прикажем объекту Guard преследовать игрока. Затем объявляется переменная navmesh для получения компонента NavMeshAgent объекта. Её мы используем позже, когда Guard начнёт преследовать игрока после того, как узнает о том, что игрок касается объекта Looker. В функции Start нам нужно задать в качестве значения переменной navmesh компонент NavMesh Agent объекта:
Затем в функции Update мы добавим единственную строку кода:
Эта строка задаёт точку назначения для объекта Guard. В нашем случае она будет брать текущую позицию объекта Player и перемещаться к этой точке. После срабатывания объект будет постоянно преследовать игрока. Вопрос в том, как выполняется процесс срабатывания? Он будет закодирован не в скрипте Guard, а в скрипте Looker. Прежде чем переходить к скрипту Looker, посмотрите на рисунок 31, чтобы сверить свой код скрипта Guard.

Рисунок 31: готовый скрипт Guard.
Внутри Looker нам снова нужно объявить следующие переменные:
После этого закомментируем функцию Start, которая в этом скрипте нам не нужна. Перейдём к функции Update и добавим следующий код:
Именно здесь происходят основные действия проекта, поэтому давайте проанализируем код. Во-первых, в зависимости от значения булевой переменной movingDown, объект, к которому прикреплён этот скрипт, будет двигаться вверх или вниз. Как только он достигнет определённой точки, то изменит направление. Далее Looker снизит значение сброса на основании реального времени. Как только таймер станет меньше нуля, он возьмём скрипт Guard из объекта Guard и отключит его, после чего объект Guard начнёт перемещаться к последней известной до этого момента позиции игрока, а затем остановится. Looker также снова включает его коллайдер, чтобы весь процесс мог начаться заново. Теперь наш скрипт выглядит следующим образом:

Рисунок 32: скрипт Looker.
Кстати о коллайдерах: настало время создать код коллизии, сбрасывающий таймер Looker и включающий скрипт Guard. В функции Update создайте следующий код:
OnCollisionEnter Unity автоматически распознаёт как код коллизии, а поэтому выполняет его при возникновении коллизии с другим объектом. В нашем случае он сначала проверяет, имеет ли столкнувшийся объект тэг Player. Если нет, то он игнорирует остальную часть кода. В противном случае он включает скрипт Guard, задаёт таймеру reset значение 5 (то есть пять секунд), и отключает его коллайдер, чтобы игрок по-прежнему мог двигаться сквозь объект и случайно не застрял в объекте Looker. Функция показана на рисунке 33.

Рисунок 33: код коллизии для Looker.
На этом весь код проекта готов! Можно сделать ещё пару вещей, прежде чем закончить проект. Сохраните всю работу и вернитесь в Unity.
Завершение проекта
Для завершения проекта нам достаточно прикрепить скрипты к соответствующим объектам и задать несколько переменных. Во-первых, перейдите из окна Navigation в окно Inspector:

Рисунок 34: переход в окно Inspector.
После этого начнём с объекта Player. Выберите его в окне Hierarchy, а затем в нижней части окна Inspector нажмите на кнопку Add Component и добавьте скрипт Player. На этом объект Player завершён.

Рисунок 35: компонент скрипта Player.
Далее выберите объект Guard. Как и раньше, прикрепим скрипт Guard к объекту. На этот раз нам понадобится сообщить Guard, кто является игроком. Для этого перетащите объект Player из Hierarchy в поле Player компонента скрипта Guard, как показано на рисунке 36.

Рисунок 36: делаем объект Player значением поля Player.
Также нам нужно отключить скрипт Guard. В нашем проекте Guard будет преследовать игрока после включения его скрипта. Этот скрипт Guard должен включаться только после того, как игрок коснётся объекта Looker. Всё, что нужно сделать — снять флажок рядом с текстом Guard (Script) в компоненте:

Рисунок 37: отключение скрипта Guard.
Наконец, перейдём к объекту Looker и прикрепим к нему скрипт Looker. На этот раз объекту Looker потребуется объект Guard в качестве значения его переменной Guard. Так же, как мы назначали объект Player переменной Player скрипта Guard, мы сделаем то же самое с объектом Guard и скриптом Looker. Перетащите Guard из Hierarchy в поле Guard скрипта Looker. И на этом проект завершён! Нажмите на кнопку Play в верхней части редактора Unity, чтобы проверить свой проект.

Рисунок 38: тестирование проекта.
Попробуйте переместить объект Player в объект Looker (не забывайте, что перемещение выполняется стрелками!). Заметьте, что после этого объект Guard начнёт преследовать игрока. Он будет продолжать преследование примерно 5 секунд, после чего сдастся.

Рисунок 39: полностью готовый проект в действии.
Заключение
Этот ИИ очень прост, но его запросто можно расширить. Допустим, если мы представим, что объект Looker — это камера, а охранник смотрит через неё, чтобы найти вас, то будет логично дать объекту Guard собственную пару глаз. Игрок может проходить рядом с камерами, но они должны учитывать и глаза охранника. Также можно скомбинировать этот проект с концепцией поиска пути: дать охраннику путь, по которому он будет следовать, создав таким образом более интересную для игрока среду.
Подобный простой ИИ можно развить множеством разных способов. Возможно, вы не захотите делать ничего вышеизложенного и решите сделать что-то своё. Советую вам экспериментировать, возможно, у вас появится идея интересного проекта, который стоит довести до конца.
Name already in use
If nothing happens, download GitHub Desktop and try again.
Launching GitHub Desktop
If nothing happens, download GitHub Desktop and try again.
Launching Xcode
If nothing happens, download Xcode and try again.
Launching Visual Studio Code
Your codespace will open once ready.
There was a problem preparing your codespace, please try again.
Latest commit
Git stats
Files
Failed to load latest commit information.
README.md
simple neuero network with C/C++ and QT Code write for educational purposes, it’s not an example of «clean code».

Try it with mnist data
About
simple neuero network with C/C++
Resources
License
Stars
Watchers
Forks
Releases
Packages 0
Languages
Footer
© 2023 GitHub, Inc.
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
C# и нейронные сети. С чего начать и в какую сторону смотреть?
Но я здесь немного уточню что бы было не так расплывчасто.
- Интересует общая сжатая теория. С какой статьи лучше начать?
- Интересуют актуальные библиотеки C#
- Интересует приблизительная реализация уже созданной в нескольких вариациях идеи — скармливаешь нейронке много изображений-пар: большое и маленькое. А нейронка учится делать из маленького большое и потом сравнивает сделанное большое и то большое которое ему скормили что бы понять что сделано не так. Какую нейронку лучше выбрать под даную задачу?
- Не слишком ли сложная и на сколько вообще реальная задачка в 3, если учесть что я вообще ни бум-бум в нейронных сетях?
- Если задача непосильная для новичка в НС, то с каких задач-тренировок лучше начинать?
![]()
![]()
Одна из самых изысканных задач для НС приведена в этой статье, можно понять уровень проблематики и алгоритмов, которые задействуются для решений подобного класса задач и где действительно нужна НС. Примеры на OpenCV, что тоже замечательно.
OpenCV — Библиотека с самой открытой лицензией и исходным кодом, написана на C++. Под C# есть wrapper’ы — EmguCV, OpenCV for Unity (можно и без Unity использовать). Есть адаптация для Visual Studio (C++), Objective C, Android Studio, Python.
Даже для умеренно сложных задач НС обычно не нужна, и не очень даже применима — часто приходится делать обработку видеопотока в реальном времени, а НС не самая быстрая штука далеко. По этой и еще ряду причин большинство разработчиков, уверенных поначалу что им нужна НС, от этой идеи впоследствии отказываются. Благо в OpenCV реализовано огромное количество алгоритмов на все случаи жизни — почти всё, что связано с обработкой видео и изображений там есть.
После редактирования автором вопроса редактирую ответ 🙂
Большинство алгоритмов, реализованных в доступных пакетах, в основном предназначены для down-scaling, спрос на это очевиден — камера снимает в большом разрешении, а в Интернеты лучше постить, предварительно уменьшив фотку — этим пользуются все активные пользователи Интернетов и соцсетей.
Up-scaling задача тоже достаточно хорошо проработана, просто спрос на нее несоизмеримо меньше — тем не менее любой дизайнер отлично знает, что нужно делать, чтобы изображение можно было без проблем увеличивать — взять Corel Draw или другой пакет векторной графики, то есть сделать изображение векторным.
Таким образом, если маленькое растровое изображение преобразовать в векторы, сделать up-scaling, мы скорее всего уже получим что-то интересное — обратите внимание на программу Potrace. В OpenCV готового для raster->vector ничего нет. Хотя из доступных инструментов и можно слепить что-то, врядли получится лучше чем у Potrace — всё-таки там люди занимались этой проблемой специально. Potrace идёт с исходниками и подробным описанием алгоритмов, так что простор для творчества полный 🙂
И самое интересное на десерт — пора взглянуть на ваше увеличенное изображение, и путем сравнения с полученным в результате векторизации и up-scaling результатом выявить отклонения. Выявленные отклонения могут являться материалом для обучения НС. Например, можно пойти путем векторизации большого изображения, выявления соответствия кривых и сравнения их характеристик. По результатам обучения НС начнет выдавать некие рекомендации по коррекции кривых в произвольных изображениях. Может быть даже замахнуться на добавление кривых, типа дополнительных цветовых контуров градиента (хотя градиент в полученном изображении проще будет сгладить с помощью функций Blur и т.п.)