Статья посвящена введению в нейронные сети и примеру их реализации. В первой части дано небольшое теоретическое введение в нейронные сети на примере нейронной сети Хопфилда. Показано, как осуществляется обучение сети и как описывается ее динамика. Во второй части показано, как можно реализовать алгоритмы, описанные в первой части при помощи языка С++. Разработанная программа наглядно показывает способность нейронной сети очищать от шума ключевой образ. В конце статьи есть ссылка на исходный код проекта.
Теоретическое описание
Введение
Для начала, необходимо определить, что такое нейрон. В биологии нейрон — специализированная клетка, из которой состоит нервная система. Биологический нейрон имеет строение, показанное на рис.1.
Рис.1 Схема нейрона
Нейронную сеть можно ввести как совокупность нейронов и их взаимосвязей. Следовательно, для того, чтобы определить искусственную (не биологическую) нейронную сеть необходимо:
- Задать архитектуру сети;
- Определить динамику отдельных элементов сети — нейронов;
- Определить правила, по которым нейроны будут взаимодействовать между собой;
- Описать алгоритм обучения, т.е. формирования связей для решения поставленной задачи.
В качестве архитектуры нейронной сети будет использоваться сеть Хопфилда. Данная модель, видимо, является наиболее распространенной математической моделью в нейронауке. Это обусловлено ее простотой и наглядность. Сеть Хопфилда показывает, каким образом может быть организована память в сети из элементов, которые не являются очень надежными. Экспериментальные данные показывают, что при увеличении количества вышедших из строя нейронов до 50%, вероятность правильного ответа крайне близка к 100%. Даже поверхностное сопоставление нейронной сети (например,
Формальное описание сети Хопфилда
Сеть состоит из N искусственных нейронов, аксон каждого нейрона связан с дендритами остальных нейронов, образуя обратную связь. Архитектура сети изображена на рис. 2.
Рис.2 Архитектура нейронной сети Хопфилда
Каждый нейрон может находиться в одном из 2-х состояний:
где — состояние нейрона в момент . «Возбуждению» нейрона соответствует , а «торможению» . Дискретность состояний нейрона отражает нелинейный, пороговый характер его функционирования и известный в нейрофизиологи как принцип «все или ничего».
Динамика состояния во времени -ого нейрона в сети из нейронов описывается дискретной динамической системой:
где — матрица весовых коэффициентов, описывающих взаимодействие дендритов -ого нейрона с аксонами -ого нейрона.
Стоит отметить, что и случай не рассматриваются.
Обучение и устойчивость к шуму
Обучение сети Хопфилда выходным образам сводится к вычислению значений элементов матрицы . Формально можно описать процесс обучения следующим образом: пусть необходимо обучить нейронную сеть распознавать образов, обозначенных . Входной образ представляет собой: где — шум, наложенный на исходный образ .
Фактически, обучение нейронной сети — определение нормы в пространстве образов . Тогда, очистка входного образа от шума можно описать как минимизацию этого выражения.
Важной характеристикой нейронной сети является отношение числа ключевых образов , которые могут быть запомнены, к числу нейронов сети : . Для сети Хопфилда значение не больше 0.14.
Вычисление квадратной матрицы размера для ключевых образов производится по правилу Хебба:
где означает -ый элемент образа .
Стоит отметить, что в силу коммутативности операции умножения, соблюдается равенство
Входной образ, который предъявляется для распознавания соответствует начальным данным для системы, служащий начальным условием для динамической системы (2):
Уравнений (1), (2), (3), (4) достаточно для определения искусственной нейронной сети Хопфилда и можно перейти к ее реализации.
Реализация нейронной сети Хопфилда
Реализация нейронной сети Хопфилда, определенной выше будет производиться на языке C++. Для упрощения экспериментов, добавим основные определения типов, напрямую связанных с видом нейрона и его передаточной функции в класс simple_neuron, а производные определим далее.
Самыми основными типами, напрямую связанными с нейроном являются:
- тип весовых коэффициентов (выбран float);
- тип, описывающий состояния нейрона (введен перечислимый тип с 2 допустимыми значениями).
На основе этих типов можно ввести остальные базовые типы:
- тип, описывающий состояние сети в момент (выбран стандартный контейнер vector);
- тип, описывающий матрицу весовых коэффициентов связей нейронов (выбран контейнер vector контейнеров vector).
struct simple_neuron {
enum state {LOWER_STATE=-1, UPPER_STATE=1};
typedef float coeff_t; <<(1)
typedef state state_t; <<(2)
...
};
typedef simple_neuron neuron_t;
typedef neuron_t::state_t state_t;
typedef vector<state_t> neurons_line; <<(3)
typedef vector<vector<neuron_t::coeff_t>> link_coeffs; <<(4)
Обучение сети, или, вычисление элементов матрицы в соответствии с (3) производится функцией learn_neuro_net, принимающей на вход список обучающих образов и возвращающей объект типа link_coeffs_t. Значения вычисляются только для нижнетреугольных элементов. Значения верхнетреугольных элементов вычисляются в соответствии с (4). Общий вид метода learn_neuro_net показан в листинге 2.
link_coeffs learn_neuro_net(const list<neurons_line> &src_images) {
link_coeffs result_coeffs;
size_t neurons_count = src_images.front().size();
result_coeffs.resize(neurons_count);
for (size_t i = 0; i < neurons_count; ++i) {
result_coeffs[i].resize(neurons_count, 0);
}
for (size_t i = 0; i < neurons_count; ++i) {
for (size_t j = 0; j < i; ++j) {
neuron_t::coeff_t val = 0;
val = std::accumulate(
begin(src_images),
end(src_images),
neuron_t::coeff_t(0.0),
[i, j] (neuron_t::coeff_t old_val, const neurons_line &image) -> neuron_t::coeff_t{
return old_val + (image[i] * image[j]);
});
result_coeffs[i][j] = val;
result_coeffs[j][i] = val;
}
}
return result_coeffs;
}
Обновление состояний нейронов реализовано с помощью функтора neuro_net_system. Аргументом метода _do функтора является начальное состояние , являющееся распознаваемых образом (в соответствии с (5)) — ссылка на объект типа neurons_line.
Метод функтора модифицирует передаваемый объект типа neurons_line до состояния нейронной сети в момент времени . Значение жестко не фиксировано и определяется выражением:
т.е., когда состояние каждого нейрона не изменилось за 1 «такт».
Для вычисления (2) применены 2 алгоритма STL:
- std::inner_product для вычисления суммы произведений весовых коэффициентов и состояний нейронов (т.е. вычисление (2) для определенного );
- std::transform для вычисления новых значений для каждого нейрона (т.е. вычисление пункта выше для каждого возможного )
Исходный код функтора neurons_net_system и метода calculate класса simple_neuron показан в листинге 3.
struct simple_neuron {
...
template <typename _Iv, typename _Ic>
static state_t calculate(_Iv val_b, _Iv val_e, _Ic coeff_b) {
auto value = std::inner_product(
val_b,
val_e,
coeff_b,
coeff_t(0)
);
return value > 0 ? UPPER_STATE : LOWER_STATE;
}
};
struct neuro_net_system {
const link_coeffs &_coeffs;
neuro_net_system(const link_coeffs &coeffs): _coeffs(coeffs) {}
bool do_step(neurons_line& line) {
bool value_changed = false;
neurons_line old_values(begin(line), end(line));
link_coeffs::const_iterator it_coeffs = begin(_coeffs);
std::transform(
begin(line),
end(line),
begin(line),
[&old_values, &it_coeffs, &value_changed] (state_t old_value) -> state_t {
auto new_value = neuron_t::calculate(
begin(old_values),
end(old_values),
begin(*it_coeffs++)
);
value_changed = (new_value != old_value) || value_changed;
return new_value;
});
return value_changed;
}
size_t _do(neurons_line& line) {
bool need_continue = true;
size_t steps_done = 0;
while (need_continue) {
need_continue = do_step(line);
++steps_done;
}
return steps_done;
}
};
Для вывода в консоль входных и выходных образов создан тип neurons_line_print_descriptor, который хранит ссылку на образ и формат форматирования (ширину и высоту прямоугольника, в который будет вписан образ). Для этого типа переопределен оператор <<. Исходный код типа neurons_line_print_descriptor и оператора вывода в поток показан в листинге 4.
struct neurons_line_print_descriptor {
const neurons_line &_line;
const size_t _width;
const size_t _height;
neurons_line_print_descriptor (
const neurons_line &line,
size_t width,
size_t height
): _line(line),
_width(width),
_height(height)
{}
};
template <typename Ch, typename Tr>
std::basic_ostream<Ch, Tr>&
operator << (std::basic_ostream<Ch, Tr>&stm, const neurons_line_print_descriptor &line) {
neurons_line::const_iterator it = begin(line._line), it_end = end(line._line);
for (size_t i = 0; i < line._height; ++i) {
for (size_t j = 0; j < line._width; ++j) {
stm << neuron_t::write(*it);
++it;
}
stm << endl;
}
return stm;
}
Пример работы нейронной сети
Для проверки работоспособности реализации, нейронная сеть была обучена 2 ключевым образам:
Рис.3 Ключевые образы
На вход подавались искаженные образы. Нейронная сеть корректно распознала исходные образы. Искаженные образы и распознанные образы показаны на рис.4, 5
Рис.4 Распознавание образа 1
Рис.5 Распознавание образа 2
Запуск программы производится из командной строки строчкой вида: AppName WIDTH HEIGHT SOURCE_FILE [LEARNE_FILE_N], где:
AppNaame - название исполняемого файла; WIDTH, HEIGHT - ширина и высота прямоугольника, в который будут вписываться выходной и ключевые образы; SOURCE_FILE - исходный файл с начальным образом; [LEARNE_FILE_N] - один или несколько файлов с ключывыми образами (через пробел).
Исходный код выложен на GitHub -> https://github.com/RainM/hopfield_neuro_net
В репозитории проект CMake, из которого можно сгенерировать проект Visual Studio (VS2015 компилирует проект успешно) или обычные Unix Makefile’ы.
Использованная литература
- Г.Г. Малинецкий. Математические основы синергетики. Москва, URSS, 2009.
- Статья «Нейронная_сеть_Хопфилда» на Википедии.
Автор: RainM