Почему нейронная сеть со временем не развивается, и результаты не улучшаются
Как говорится, длиннопост...
Вступление
Основываясь на этом, а также некоторых видео и статьях, пытаюсь понять систему работы простейших нейронных сетей, которые со временем развиваются и выдают все более точные результаты с каждой итерацией.
У меня есть платформа; сфера ("игрок"); и — слева и справа от нее — две "еды" — побольше и поменьше. Задача нейронной сети научиться правильно выбирать еду. Нужна та, которая больше.
Что у меня есть:
- Скрипт еды: содержит 2 публичные переменные: saturation - размер еды, isBenefit - является ли эта еда более предпочтительной перед другой (их, как уже сказал, две: слева и справа от сферы).
- Скрипт NeuralNetwork - нейронная сеть. Я его писал сам, однако смысл его такой же, что и у оригинального. Даже больше скажу: я вставил оригинальный заместо моего, но нейронка все равно не развивалась.
- Скрипт Sphere - сфера. Выглядит он так (по максимуму прокомментировал, чтобы вам было легче читать, лишнее убрал):
public class Sphere : MonoBehaviour
{
public NeuralNetwork Brain { get; set; } // Мозг: нейронная сеть сферы
public float fitness { get; private set; } // Приспособленность: -0.005 каждый кадр, +0.1 за неверный выбор, +1 за верный выбор еды
public bool didChooseBenefit { get; private set; } // Правильно ли выбрал еду
private bool isPlaying; // Бежит ли за едой, или уже прибежал
private bool hasDecided; // Принял ли уже решение, в какую сторону бежать
private int direction; // Направление бега
public void SetBrain (NeuralNetwork myBrain)
{
Brain = myBrain;
isPlaying = true;
fitness = 0;
}
private void FixedUpdate()
{
if (isPlaying) // Если еще не нашел еду и играет
{
if (!hasDecided) // Если еще не принял решение
{
CalculateDistances(); // Осмотреться влево-вправо
UseBrain(); // Принять решение с помощью нейронки
hasDecided = true; // Ставим, что уже принял решение
}
rb.AddForce(Vector3.right * direction * speed * Time.deltaTime, ForceMode.Force); // Бежим в выбранном с помощью нейронки направлении
fitness -= 0.005f; // Ежекадровый штраф (penalty)
}
}
private void UseBrain () // Функция использования нейронки
{
float[] inputs = CalculateDistances(); // Осматриваемся влево-вправо, inputs - массив из 2 элементов: размер еды слева и справа
float[] output = Brain.FireNeurons(input); // Пропускаем сквозь нейронную сеть. Получаем output
direction = output[0]>0 ? 1 : -1; // Направление влево или вправо, при выводе нейронной сети меньше/больше 0.
}
private float[] CalculateDistances() // Функция Осмотреться влево-вправо рейкастами
{
Vector3 pos = transform.position; // Кэшируем
float RayLength = 5; // Длина луча
RaycastHit hitLeft, hitRight;
Physics.Raycast(pos, -Vector3.right, out hitLeft, RayLength); // Стреляем влево
Physics.Raycast(pos, Vector3.right, out hitRight, RayLength); // Стреляем вправо
float[] saturations = {
hitLeft.transform.GetComponent<Food>().Saturation, // Размер еды слева
hitRight.transform.GetComponent<Food>().Saturation // Размер еды справа
};
return saturations;
}
private void OnTriggerEnter(Collider other)
{
if (other.tag == "Food") // Наткнулся на еду
{
if (other.GetComponent<Food>().isBenefit) // Если бОльшая
{
fitness += 1; // Большой приз
didChooseBenefit = true; // Выбрал правильно
GetComponent<MeshRenderer>().material.color = Color.green; // Подсветка
}
else // Если наткнулся на малую еду
{
fitness += 0.1f; // Малый приз
GetComponent<MeshRenderer>().material.color = Color.red; // Подсветка
}
isPlaying = false; // Остановка всех действий сферы
GameObject.Find("GameManager").GetComponent<GameManager>().OnSphereFoundFood(); // Запускаем метод проверки: все уже нашли еду (т.е. запускаем новую генерацию) или еще пока нет.
Destroy(other.gameObject); // Уничтожаем еду
}
}
}
- Скрипт GameManager - контроллер игры. Также прокомментировал по максимуму для удобства. Лишние перемнные убрал.
public class GameManager : MonoBehaviour
{
[Range(1, 100)] public int GenerationSize; // Кол-во сфер ("игроков") в итерации
[Range(0.01f, 0.1f)] public float MutationChance; // Шанс мутации каждого участника нового поколения
private int GenerationNumber; // Номер генерации
private int countPlaying; // Количество сфер, которые все еще не нашли еду
private int[] Layers; // Слои нейронки.
private List<Sphere> SpheresList; // Список сфер
private List<NeuralNetwork> PreviousGeneration; // Список сер предыдущего поколения
private void Start()
{
GenerationNumber = 0; // Обнуляем
Layers = new int[] { 2, 6, 6, 1 }; // Размеры слоев: 2 входных данных (размер еды слева И справа); 2 скрытых слоя по 6 нейронов; 1 выходной нейрон - число от -1 до 1, и если <0 - идти за едой влево, иначе вправо
StartGeneration(); // Запускаем первую генерацию
}
private void StartGeneration () // Запуск генерации
{
GenerationNumber++; // Инкрементируем номер генерации
SpheresList = new List<Sphere>(); // Пока список пустой
InstantiateSpheres(); // Создаем сферы
InstantiateFood(); // Создаем еду
}
private IEnumerator EndGeneration () // Оканчиваем генерацию
{
yield return new WaitForSeconds(1);
SpheresList.Sort(SortByFitness); // Отсортируем сферы по приспособленности
SaveGenerationAsPrevious(); // Сохраним мозги наших сфер как, теперь уже, предыдущее поколение
DestroySpheres(); // Уничтожим наши серы
DestroyFood(); // Уничтожим оставшуюся еду
// Creating
yield return new WaitForSeconds(0.2f);
StartGeneration(); // Запускаем новую генерацию
}
private void InstantiateSpheres () // Создаем сфер
{
SpheresList.Clear(); // Очищаем
for (int i = 0; i < GenerationSize; i++) // Цикл на нужное количество сфер
{
Sphere newSphere = InstantiateSphere(i); // Создаем
if (GenerationNumber == 1) // Если это первая генерация, то...
newSphere.SetBrain(new NeuralNetwork(Layers)); // ... вставляем мозги
else // Если не первая генерация, то...
newSphere.SetBrain(MutateBrain(i)); // ...Вставляем мозги, которые, возможно, мутировали
}
countPlaying = GenerationSize; // Определяем кол-во активных сфер
}
private Sphere InstantiateSphere (int index) // Создание одной сферы
{
GameObject newSphere = Instantiate(SpherePrefab, new Vector3(0, 1, findPosZ(index)), Quaternion.identity, SpheresContainer); // Спавним
SpheresList.Add(newSphere.GetComponent<Sphere>()); // Добавляем в список
return newSphere.GetComponent<Sphere>(); // Возвращаем
}
private void InstantiateFood () // Создаем еду
{
/* В этой функции я создаю еду: спавню в нужной позиции, задаю цвет, рандомный размер,
* определяю какая из вдух "ед" более выгодная: левая или правая.
* Здесь, на мой взгляд, нет ничего особенного, здесь я никак не зацепляю ни нейронку, ни сферы
*/
}
private void SaveGenerationAsPrevious () // Сохраняю поколение как предыдущее
{
PreviousGeneration = new List<NeuralNetwork>(); // Обнуляем
for (int i = 0; i < GenerationSize; i++)
{
PreviousGeneration.Add(SpheresList[i].Brain); // По одному добавляем (именно мозги, а не всю сферу, т.к. потом мы ее удалим)
}
}
private void DestroySpheres () { // Уничтожаем сферы
/* Здесь я циклом и Destory() уничтожаю еду, потом очищаю массив SpheresList.Clear() */ }
private void DestroyFood () { // Уничтожаем еду
/* Ищу по тегам, уничтожаю Destroy() */ }
private float findPosZ (int index) // Вспомогательная Функция для вычисления позиции по Z
{
return 74f - index * 1.5f;
}
public void OnSphereFoundFood () // Когда сфера находим еду
{
countPlaying--; // Становится на 1 меньше играющих сфер
if (countPlaying == 0) // Если все сверф уже нашли еду
{
StartCoroutine(EndGeneration()); // Оканчиваем генерацию
}
}
private NeuralNetwork MutateBrain (int index) // Мутирование мозгов
{
if (index == 0) // Самый первый в новом поколении...
return new NeuralNetwork(PreviousGeneration[0], 0); // ... прямой наследник победителя в предыдущем поколении
if (index < GenerationSize * 0.33f) // Первая треть нового поколения - это...
return new NeuralNetwork(PreviousGeneration[index], MutationChance); // ... мутировавший тот же самый, но в предыдущем поколении
if (index < GenerationSize * 0.66f) // Вторая треть нового поколения - это...
return new NeuralNetwork(PreviousGeneration[0], MutationChance); // ... мутировавший победитель предыдущего поколения
else // Третья треть - это...
return new NeuralNetwork(PreviousGeneration[1], MutationChance); // ... мутировавшее второе место предыдущего поколения
}
private int SortByFitness (Sphere a, Sphere b) // Сортировка по приспособлености
{
return -a.fitness.CompareTo(b.fitness); // Сравниваем по приспособленности
}
}
Как это все работает:
Да никак это не работает!
Нейронка просто не прогрессирует. Я подключил запись количество сфер, которые правильно выбрали еду, в XML документ с помощью System.XmlSerialization, после чего построил в Excel график, и не заметил ни малейшего повышения вверх, хотя дал нейронке прогнать более 800 итераций.
Вопрос:
Что у меня не так? Почему нет улучшений, если в оригинале все работало: птички спокойно пролетали сквозь трубы, причем в первой итерации не пролетел никто, а уже в 20-й летели почти без проблем. Почему у меня не работает?
Понимаю, что мало кому захочется разбираться в моем коде, однако, знаю, есть в нашем сообществе люди, которые любят провести обшиное исследование чужой проблемы:) Надеюсь на помощь, хотя думаю, кода в вопросе слишком много, поэтмоу потратил много времени, чтобы весь его раскомментировать:)