Перейти к основному содержимому

Инструкции по реализации функционала

Для начала давайте добавим функцию прогноза нейронной сети в ранее написанный скрипт ai.js:

к сведению

Следующая часть кода добавляем в конец файла ai.js.

const normalizationConstant = 705;
// Предсказание нейронной сеткой
const runONNX = async () => {
use_bot.busy = true;
console.time("onnx");
var inp = Float32Array.from([
ball.x / normalizationConstant,
ball.y / normalizationConstant,
rightPaddle.y / normalizationConstant
]);
let input = new onnx.Tensor(inp, "float32", [1, 3]);
let output = (await onnxSess.run([input])).get("output").data;
const actionId = indexOfMax(output);
if (actionId === 2) {
// up
keyPresses["up"] = 1;
keyPresses["down"] = 0;
keyPresses["nothing"] = 0;
rightPaddle.dy = -rightPaddle.paddleSpeed;
} else if (actionId === 1) {
// down
keyPresses["up"] = 0;
keyPresses["down"] = 1;
keyPresses["nothing"] = 0;
rightPaddle.dy = rightPaddle.paddleSpeed;
} else {
//nothing
keyPresses["up"] = 0;
keyPresses["down"] = 0;
keyPresses["nothing"] = 1;
rightPaddle.dy = 0;
}
console.timeEnd("onnx");
use_bot.busy = false;
return output;
};

Разберём по шагам, что тут происходит. Вначале мы вводим константу normalizationConstant, которую использовали при нормализации данных в ноутбуке, когда обучали нейронную сеть на собранном наборе данных. Её значение на самом деле не превышает координату правой ракетки, поскольку все вылеты за пределы поля удаляются из набора данных при его сборе. Поэтому вместо такого "магического числа" (вы, может, удивитесь, но это реальный термин у программистов) хорошо было бы написать rightPaddle.x. И на практике так и стоит поступать. Потому что если мы захотим изменить размеры холста, то это скажется и на горизонтальной координате правой ракетки, и на координатах в обучающем наборе. Затем при обучении модели мы отшкалируем диапазон координат мячика от 0 до 1, поделив на максимальную его координату из набора. А эта координата уже будет отличаться от зашитой нами сейчас в коде. В результате данные будут неправильно подготавливаться перед подачей их на вход в нейронную сеть, в результате чего предсказания модели могут быть катастрофически неправильными. А догадаться вы об этом сходу не сможете, если давно писали этот участок кода и на выявление ошибки может уйти продолжительное время. Вот почему сообщество программистов зачастую так не любит "магические числа" в коде.

Но вернёмся к нашим баранам. После объявления нормирующей константы объявляется функция прямого прохода по нейронной сети runONNX. Она асинхронная, что можно понять по приставке async, потому что это одно из необходимых требований для работы с нейросетевым фреймворком ONNX.js. Затем мы выставляем флаг занятости бота в истинное значение. Зачем мы это делаем? Всё потому, что ранее созданная сессия, в которую был загружен граф нейронной сети, не может обрабатывать больше одного асинхронного запроса на прогноз в один момент времени. Кроме того, это лишняя нагрузка на процессор, поскольку нет необходимости делать прогнозы с такой большой частотой.

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

Затем создаётся типизированный входной массив из нормализованных значений координат мячика и вертикальной координаты правой ракетки. Этот типизированный массив подаётся на вход в специальную структуру данных фреймворка ONNX.js – Tensor. На вход эта структура принимает типизированный массив, тип массива и его размерность. Размерность массива это число примеров на число признаков. Поэтому указывается [1, 3]. На следующей строчке и происходит всё волшебство предсказания при помощи функции run. На вход ей подаётся входная структура Tensor, она её прогоняет через нейронную сеть и "выплёвывает" значения нейронов выходного слоя. Поскольку нам не интересны сами числа, а интересен лишь номер нейрона-победителя, то мы передаём результат в функцию indexOfMax. Она возвращает номер нейрона с максимальным значением. Это нестандартная функция из JavaScript и её надо реализовать:

к сведению

Следующая часть кода добавляется перед const normalizationConstant = 705; файла ai.js.

// Получение позиции максимального элемента в массиве
const indexOfMax = (arr) => {
if (arr.length === 0) {
return -1;
}

var max = arr[0];
var maxIndex = 0;

for (var i = 1; i < arr.length; i++) {
if (arr[i] > max) {
maxIndex = i;
max = arr[i];
}
}
return maxIndex;
};

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

  • Номер 2 – устанавливаем отрицательное (с учётом, что рост вертикальной координаты идёт сверху вниз) значение вертикальной скорости;
  • Номер 1 – устанавливаем положительное (с учётом, что рост вертикальной координаты идёт сверху вниз) значение вертикальной скорости;
  • Номер 0 – устанавливаем нулевое (с учётом, что рост вертикальной координаты идёт сверху вниз) значение вертикальной скорости;

Вместе с этим не забываем обновлять информацию в словарике для логирования keyPresses.

Наконец, останавливаем таймер при помощи метода console.timeEnd. Только после этого в консоль разработчика выведется информация о затраченных миллисекундах. И последним штрихом возвращаем свободный статус боту, установив соответствующее значение в словаре.

Написанная функция runONNX нигде пока не вызывается. Давайте исправим это. Обновим функцию waitUntil:

к сведению

Следующая часть кода изменяет функцию waitUntil файла ai.js.

async function waitUntil(condition, func) {
let frame = 0;
return await new Promise((resolve) => {
condition.intervalId = setInterval(() => {
if (!condition.state) {
resolve("a");
} else {
if (!condition.busy && !isPaused) {
func();
if (frame === 0) {
botImages[0].style.display = "block";
botImages[1].style.display = "none";
const tmp = botImages[0];
botImages[0] = botImages[1];
botImages[1] = tmp;
}
frame = (frame + 1) % 10;
}
}
}, 50);
});
}

А также функцию обратного вызова при подключении файла с моделью:

к сведению

Следующая часть кода изменяет часть кода обратного вызова на событие document.getElementsByClassName("modelFile")[0].onchange в файле ai.js.

    reader.onloadend = async function () {
onnxSess = new onnx.InferenceSession();
await onnxSess.loadModel(reader.result);
use_bot.state = true;
waitUntil(use_bot, runONNX);
};
к сведению

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