Русский
Русский
English
Статистика
Реклама

Создание нейронной сети Хопфилда на JavaScript

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

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

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

Исходникина Github и демо.

Дляреализациипонадобится:

  • Браузер

  • Базовоепониманиенейросетей

  • БазовыезнанияJavaScript/HTML

Немноготеории

Нейронная сеть Хопфилда (англ. Hopfield network) полносвязная нейронная сеть ссимметричной матрицей связей. Такая сеть может быть использована для организации ассоциативной памяти, как фильтр, атакже для решения некоторых задач оптимизации.

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

Структурная схема нейросети ХопфилдаСтруктурная схема нейросети Хопфилда

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

Алгоритм работы сети:

  1. Инициализация
    Веса нейронов устанавливаются по следующей формуле:

    w_{ij}=\left\{\begin{matrix} \sum_{k=1}^{m} x_{i}^{k} * x_{j}^{k} & i \neq j \\0, & i=j \end{matrix}\right.

    где m количество образов
    x_{i}^{k}, x_{j}^{k} i - ый и j - ый элементы вектора k - ого образца.

  2. Навходы сети подается неизвестный сигнал. Фактически его ввод осуществляется непосредственной установкой значений выходов:
    y_{j}(0) = x_{j}

  3. Рассчитывается выход сети (новое состояние нейронов иновые значения выходов):

    y_{j}(t+1)=f\left ( \sum_{i=1}^{n} w_{ij}*y_{i}(t)\right )

    где f пороговая активационная функция собластью значений [-1; 1];
    t номер итерации;
    j = 1...n; n количество входов инейронов.

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

Разработка

Визуальная часть

Для начала посмотрим как работает итоговый проект.

Демонстрация работы программыДемонстрация работы программы

Онсостоит издвух элементов Canvas итрех кнопок. Это простейший HTML иCSS код, ненуждающийся впояснении (можете скопировать сгитхаба).

Левый элемент Canvas нужен для рисования изображений, которые затем будут использованы для обучения (кнопка Запомнить) или распознавания нейросети. Направом элементе отображается результат распознавания сигнала, находящегося наданный момент налевом Canvas (вданном случае сеть вспомнила буквуТ наоснове искаженного сигнала).

Здесь нужно обратить внимание нато, что область для рисования представлена сеткой 1010 ипозволяет закрашивать клетки только черным цветом. Так как всети Хопфилда число нейронов равно числу входов, количество нейронов будет равно длине входного сигнала, тоесть 100 (унас всего 100 клеток наэкране). Входной сигнал при этом будет двоичным массив, состоящий из1и1, где 1 это белый, а1 черный цвет.

Наконец-то приступим кнаписанию кода, сначала инициализируем необходимые переменные.

Код инициализации
// Размер сетки установим равным 10 для простоты тестированияconst gridSize = 10;// Размер одного квадрата в пикселяхconst squareSize = 45;// Размер входного сигнала (100)const inputNodes = gridSize * gridSize;// Массив для хранения текущего состояния картинки в левом канвасе,// он же является входным сигналом сетиlet userImageState = [];// Для обработки движений мыши по канвасуlet isDrawing = false;// Инициализация состоянияfor (let i = 0; i < inputNodes; i += 1) {    userImageState[i] = -1;  }// Получаем контекст канвасов:const userCanvas = document.getElementById('userCanvas');const userContext = userCanvas.getContext('2d');const netCanvas = document.getElementById('netCanvas');const netContext = netCanvas.getContext('2d');

Реализуем функцию рисования сетки, используя инициализированные ранее переменные.

Функция отрисовки сетки
// Функция принимает контекст канваса и рисует// сетку в 100 клеток (gridSize * gridSize)const drawGrid = (ctx) => {  ctx.beginPath();  ctx.fillStyle = 'white';  ctx.lineWidth = 3;  ctx.strokeStyle = 'black';  for (let row = 0; row < gridSize; row += 1) {    for (let column = 0; column < gridSize; column += 1) {      const x = column * squareSize;      const y = row * squareSize;      ctx.rect(x, y, squareSize, squareSize);      ctx.fill();      ctx.stroke();    }  }  ctx.closePath();};

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

Обработчики движений мыши
// Обработка клика мышиconst handleMouseDown = (e) => {  userContext.fillStyle = 'black';  // Рисуем залитый прямоугольник в позиции x, y  // размером squareSize х squareSize (45х45 пикселей)  userContext.fillRect(    Math.floor(e.offsetX / squareSize) * squareSize,    Math.floor(e.offsetY / squareSize) * squareSize,    squareSize, squareSize,  );  // На основе координат вычисляем индекс,  // необходимый для изменения состояния входного сигнала  const { clientX, clientY } = e;  const coords = getNewSquareCoords(userCanvas, clientX, clientY, squareSize);  const index = calcIndex(coords.x, coords.y, gridSize);  // Проверяем необходимо ли изменять этот элемент сигнала  if (isValidIndex(index, inputNodes) && userImageState[index] !== 1) {    userImageState[index] = 1;  }  // Изменяем состояние (для обработки движения мыши)  isDrawing = true;};// Обработка движения мыши по канвасуconst handleMouseMove = (e) => {  // Если не рисуем, т.е. не было клика мыши по канвасу, то выходим из функции  if (!isDrawing) return;  // Далее код, аналогичный функции handleMouseDown  // за исключением последней строки isDrawing = true;  userContext.fillStyle = 'black';  userContext.fillRect(    Math.floor(e.offsetX / squareSize) * squareSize,    Math.floor(e.offsetY / squareSize) * squareSize,    squareSize, squareSize,  );  const { clientX, clientY } = e;  const coords = getNewSquareCoords(userCanvas, clientX, clientY, squareSize);  const index = calcIndex(coords.x, coords.y, gridSize);  if (isValidIndex(index, inputNodes) && userImageState[index] !== 1) {    userImageState[index] = 1;  }};

Как вымогли заметить, обработчики использует некоторые вспомогательные функции, такие как getNewSquareCoords, calcIndex иisValidIndex. Ниже код этих функций скомментариями.

Вспомогательные функции
// Вычисляет индекс для изменения в массиве// на основе координат и размера сеткиconst calcIndex = (x, y, size) => x + y * size;// Проверяет, помещается ли индекс в массивconst isValidIndex = (index, len) => index < len && index >= 0;// Генерирует координаты для закрашивания клетки в пределах // размера сетки, на выходе будут значения от 0 до 9const getNewSquareCoords = (canvas, clientX, clientY, size) => {  const rect = canvas.getBoundingClientRect();  const x = Math.ceil((clientX - rect.left) / size) - 1;  const y = Math.ceil((clientY - rect.top) / size) - 1;  return { x, y };};

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

Функция очистки сетки
const clearCurrentImage = () => {  // Чтобы убрать закрашенные клетки, просто заново отрисовываем   // всю сетку и сбрасываем массив входного сигнала  drawGrid(userContext);  drawGrid(netContext);  userImageState = new Array(gridSize * gridSize).fill(-1);};

Теперь можно переходить кразработке мозга программы.

Реализация алгоритма нейросети

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

Инициализация весов сети
...const weights = [];  // Массив весов сетиfor (let i = 0; i < inputNodes; i += 1) {  weights[i] = new Array(inputNodes).fill(0); // Создаем пустой массив и заполняем его 0  userImageState[i] = -1;}...

Так как каждый нейрон всети Хопфилда связан совсеми остальными нейронами, веса сети представлены двумерным массивом, каждый элемент которого является одномерным массивом размером inputNodes элементов. Витоге мыполучаем 100нейронов, укаждого изкоторых по100связей.

Теперь реализуем обработку входного сигнала (изменение весов) нейросетью согласно формуле изпервого шага алгоритма. Данный процесс происходит понажатию накнопку Запомнить. Запомненные образы будут является эталонами для последующего восстановления.

Код обработки входного сигнала
const memorizeImage = () => {  for (let i = 0; i < inputNodes; i += 1) {    for (let j = 0; j < inputNodes; j += 1) {      if (i === j) weights[i][j] = 0;      else {        // Напоминаю, что входной сигнал находится в массиве userImageState и является        // набором -1 и 1, где -1 - это белый, а 1 - черный цвет клеток на канвасе        weights[i][j] += userImageState[i] * userImageState[j];      }    }  }};

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

Функция распознавания искаженного сигнала
// Где-то в html подключаем библиотеку lodash:<script src="http://personeltest.ru/aways/cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>...const recognizeSignal = () => {  let prevNetState;  // На вход сети подается неизвестный сигнал. Фактически   // его ввод осуществляется непосредственной установкой значений выходов  // (2 шаг алгоритма), просто копируем массив входного сигнала  const currNetState = [...userImageState];  do {    // Копируем текущее состояние выходов, // т.е. теперь оно становится предыдущим состоянием    prevNetState = [...currNetState];    // Рассчитываем выход сети согласно формуле 3 шага алгоритма    for (let i = 0; i < inputNodes; i += 1) {      let sum = 0;      for (let j = 0; j < inputNodes; j += 1) {        sum += weights[i][j] * prevNetState[j];      }      // Рассчитываем выход нейрона (пороговая ф-я активации)      currNetState[i] = sum >= 0 ? 1 : -1;    }    // Проверка изменения выходов за последнюю итерацию    // Сравниваем массивы при помощи ф-ии isEqual  } while (!_.isEqual(currNetState, prevNetState));  // Если выходы стабилизировались (не изменились), отрисовываем восстановленный образ  drawImageFromArray(currNetState, netContext);};

Здесь для сравнения выходов сети напредыдущем итекущем шаге используется функция isEqual избиблиотеки lodash.

Для отрисовки полученного сигнала используется функция drawImageFromArray. Она преобразует выходной сигнал сети вдумерный массив иотрисовывает его направом канвасе.

Функция отрисовки изображения из массива точек
const drawImageFromArray = (data, ctx) => {  const twoDimData = [];  // Преобразуем одномерный массив в двумерный  while (data.length) twoDimData.push(data.splice(0, gridSize));  // Предварительно очищаем сетку  drawGrid(ctx);  // Рисуем изображение по координатам (индексам массива)  for (let i = 0; i < gridSize; i += 1) {    for (let j = 0; j < gridSize; j += 1) {      if (twoDimData[i][j] === 1) {        ctx.fillStyle = 'black';        ctx.fillRect((j * squareSize), (i * squareSize), squareSize, squareSize);      }    }  }};

Финальные приготовления

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

Привязываем функции к HTML элементам
const resetButton = document.getElementById('resetButton');const memoryButton = document.getElementById('memoryButton');const recognizeButton = document.getElementById('recognizeButton');// Вешаем слушатели на кнопкиresetButton.addEventListener('click', () => clearCurrentImage());memoryButton.addEventListener('click', () => memorizeImage());recognizeButton.addEventListener('click', () => recognizeSignal());// Вешаем слушатели на канвасыuserCanvas.addEventListener('mousedown', (e) => handleMouseDown(e));userCanvas.addEventListener('mousemove', (e) => handleMouseMove(e));// Перестаем рисовать, если кнопка мыши отпущена или вышла за пределы канвасаuserCanvas.addEventListener('mouseup', () => isDrawing = false);userCanvas.addEventListener('mouseleave', () => isDrawing = false);// Отрисовываем сеткуdrawGrid(userContext);drawGrid(netContext);

Демонстрация работы нейросети

Обучим сеть двум ключевым образам, буквам Т и Н:

Эталонные образы для обучения сетиЭталонные образы для обучения сети

Теперь проверим работу сети на искаженных образах:

Попытка распознать искаженный образ буквы НПопытка распознать искаженный образ буквы НПопытка распознать искаженный образ буквы ТПопытка распознать искаженный образ буквы Т

Программа работает! Сеть успешно восстановила исходные образы.

В заключение стоит отметить, что для сети Хопфилда число запоминаемых образов mнедолжно превышать величины, примерно равной 0.15 * n(где n размерность входного сигнала иколичество нейронов). Кроме того, если образы имеют сильное сходство, то они, возможно, будут вызывать усети перекрестные ассоциации, тоесть предъявление навходы сети вектораА приведет кпоявлению наеевыходах вектораБ инаоборот.

Исходникина Github и демо.

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

Источник: habr.com
К списку статей
Опубликовано: 05.06.2021 18:10:05
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Javascript

Программирование

Обработка изображений

Машинное обучение

Neural networks

Сеть хопфилда

Гайд

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru