Вы находитесь на странице: 1из 40

МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ

САНКТ-ПЕТЕРБУРГСКИЙ ГОСУДАРСТВЕННЫЙ
ЭЛЕКТРОТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ
«ЛЭТИ» ИМ. В.И. УЛЬЯНОВА (ЛЕНИНА)

ОТЧЕТ
по проекту
по дисциплине «Комбинаторика и теория графов»
Тема: «Нейронная сеть для определения цикла в графах вершинной
размерности 5 и 10»

Стукалов С.С.
Студенты гр. 0375 Надводнюк Я.О.

Преподаватель Чухнов А.С.

Санкт-Петербург
2021
Оглавление
ОСНОВНЫЕ ПОНЯТИЯ 3
КОМПОНЕНТЫ НЕЙРОННОЙ СЕТИ 4
Постановка задачи 4
Архитектура нашей нейронной сети. 4
О скрытых слоях и их назначении 5
Описание прямого распространения между любыми двумя слоями 6
Масштабирование активации до интервала [0, 1] в выходном слое 6
Описание нейросети в терминах линейной алгебры 8
Уточнение об активации нейронов 8
ОБУЧЕНИЕ НЕЙРОННОЙ СЕТИ ДЛЯ РАСПОЗНАВАНИЯ ЦИКЛА В ГРАФЕ 11
Структура программы, генерирующей тренировочные данные 11
Скорость работы классического алгоритма нахождения цикла 16
Функция потерь (loss) 17
Задание функции потерь для распознавания цикла 17
Градиентный спуск 18
Компоненты градиентного спуска 19
МЕТОД ОБРАТНОГО РАСПРОСТРАНЕНИЯ ОШИБКИ 21
Управление активацией нейрона 21
Варианты настройки нейросети 21
Обратное распространение 22
Классический градиентный спуск 23
Стохастический градиентный спуск 23
Adam 24
СТРУКТУРА ПРОГРАММЫ ДЛЯ СОЗДАНИЯ И ОБУЧЕНИЯ НЕЙРОННОЙ СЕТИ 26
Нормализация базы тренировочных данных 26
Обучение нейронной сети и его статистика 27
Статистика обучения 28
Точность моделей нейронных сетей для тестовой выборки 30
СКОРОСТЬ РАБОТЫ НЕЙРОННОЙ СЕТИ 30
ВЫВОДЫ 31
ПРИЛОЖЕНИЕ 32

2
ОСНОВНЫЕ ПОНЯТИЯ
Нейронная сеть — математическая модель, а также её программное
или аппаратное воплощение, построенная по принципу организации и
функционирования биологических нейронных сетей — сетей нервных клеток
живого организма.

Многослойный перцептрон — базисная (но уже достаточно сложная)


модель для понимания любых более современных вариантов нейронных
сетей.

Рисунок 1 – многослойный перцептрон.

Нейронные сети не программируются в привычном смысле этого


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

3
КОМПОНЕНТЫ НЕЙРОННОЙ СЕТИ
Цель: показать, что собой представляет нейронная сеть.

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

С позиций вычислительной техники удивительно, насколько легко мозг


осуществляет операцию нахождения цикла. Поэтому задача в таком
контексте воспринимается как простая.

Но если бы вам предложили написать программу, которая принимает


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

Как можно предположить из названия, устройство нейросети в чем-то


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

Архитектура нашей нейронной сети.


Так как наши графы будут состоять из 5 (или 10 – условие второй
задачи с другими данными будет далее записано в скобках) вершин, пусть
есть 25 (или 100) элементов в матрице смежности, содержащей различные
числа от 0 до 1: существование дуги отмечено единицей, отсутствие - нулём.
Тогда будем называть вектор из последовательно записанных строчек
матрицы входным слоем.

Необходимость скрытого слоя обосновываем тем, что выход (наличие


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

4
входных данных (как они уже будут происходить – решает нейронная сеть.
Подробнее про назначение скрытых слоев - далее).

Пусть в скрытом слою будет 25 (или 100) нейронов, каждый из


которых связан с каждым элементом входного слоя.

На самом деле с числом нейронов в скрытом слою можно


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

Последний слой содержит один нейрон, который идентифицирует


наличие цикла в графе (1 – да, 0 – нет).

Принцип работы нейросети состоит в том, что активация в одном слое


определяет активацию в следующем. Возбуждаясь, некоторая группа
нейронов вызывает возбуждение другой группы.

О скрытых слоях и их назначении


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

Перцептроны, состоящие только из входных узлов и выходных


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

Конкретное количество слоев и нейронов в них выбирается в


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

Наличие скрытого слоя делает обучение немного более сложным,


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

Описание прямого распространения между любыми двумя слоями


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

6
Масштабирование активации до интервала [0, 1] в выходном слое
На выходном слое необходимо нормализовать данные в данном
интервале, так как, вычислив взвешенную сумму, мы можем получить любое
число в широком диапазоне значений. Для того, чтобы оно попадало в
необходимый диапазон активаций от 0 до 1, разумно использовать функцию,
которая бы «сжимала» весь диапазон до интервала [0, 1].

Часто для такого масштабирования используется логистическая


функция сигмоиды:

1
−x . (1)
1+ e

Рисунок 2 – Сигмоида.

Чем больше абсолютное значение отрицательного входного числа, тем


ближе выходное значение сигмоиды к нулю, положительного – к единице.

Таким образом, активация нейрона — это, по сути, мера того,


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

7
Описание нейросети в терминах линейной алгебры

Рисунок 3 – Визуальное представление прямого распространения.


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

Очевидно, что вместо столбцов и матриц, как это принято в линейной


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

Уточнение об активации нейронов


Настало время уточнить то упрощение, с которого мы начали.
Нейронам соответствуют не просто числа – активации, а функции активации,
принимающие значения со всех нейронов предыдущего слоя и вычисляющие
выходные значения (если это выходной слой нейронной сети, то в интервале
от 0 до 1).

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

Затронем кратко тему функций, используемых для «сжатия» интервала


значений активаций. Функция сигмоиды является примером, подражающим
биологическим нейронам и она использовалась в ранних работах о
нейронных сетях для всех слоёв нейронной сети, однако сейчас для скрытых
слоёв чаще используется более простая ReLU функция (рисунок 3),
облегчающая обучение нейросети.

Рисунок 4 – ReLU функция.

Но сигмоида также имеет место быть, только для выходного слоя, а


именно для нормализации выходных данных. Конкретно в нашем случае
сочетание ReLU в скрытом слое и сигмоиды в выходном дало наилучший
результат.

Функция ReLU соответствует биологической аналогии того, что


нейроны могут находиться в активном состояни либо нет. Если пройден

9
определенный порог, то срабатывает функция, а если не пройден, то нейрон
просто остается неактивным, с активацией равной нулю.

Оказалось, что для глубоких многослойных сетей функция ReLU


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

10
ОБУЧЕНИЕ НЕЙРОННОЙ СЕТИ ДЛЯ РАСПОЗНАВАНИЯ
ЦИКЛА В ГРАФЕ
В общем виде алгоритм нахождения соответствующих весов и сдвигов
только исходя из полученных данных состоит в том, чтобы показать
нейросети множество тренировочных данных. В результате обучения
нейросеть должна правильным образом различать примеры из ранее не
представленных, тестовых данных.

Откуда берутся данные для обучения? Они сгенерированы специально


написанной нами программой, которая заполняет и анализирует матрицу
смежности графа соответствующего размера и на выход выдаёт 1 или 0 в
зависимости от наличия цикла.

Структура программы, генерирующей тренировочные данные


Для начала объявим класс Graph с соответствующей функцией
нахождения цикла “findCycle”, возвращающей bool значение (1, если есть,
или 0, если нет). Функция добавления ребра служит как вспомогательная для
преобразования матрицы смежности в список смежности. Делаем это ввиду
того, что со списком работать просто удобнее:

class Graph {
private:
    // Number of vertices
    int V;
    // Pointer to adjacency list
    list<int>* adj;
    // DFS recursive helper functions
    bool DFS_findCycle(int s, int* visited, vector<int>* cycle);
    // Detect cycle
    int flag = 0;

public:
    //Default constructor
    Graph();
    // Constructor prototype
    Graph(int v);
    // Destructor prototype
    ~Graph();

    // Method to add an edge

11
    void addEdge(int v, int w);

    // Adjancacy matrix to list in the graph


    void matrixToList(int** adjMatrix, int vertexSize);

    // Method for BFS traversal give a source "s"


    bool findCycle(int* cycle);
};
Что касается функции нахождения цикла, то мы ее сделали на основе
алгоритма обхода графа в глубину, так как она часто используется в жизни:

// Perform DFS given a starting vertex 0


bool Graph::findCycle(int* cycle) {
    // Start with all vertices as not visited
    int* visited = new int[V];
    vector<int> path;

    for (int i = 0; i < V; i++) {


        visited[i] = 0; cycle[i] = 0;
    }

    for (int j = 0; j < this->V; j++) {

        if (!visited[j]) {

            if (DFS_findCycle(j, visited, &path)) {

                int i = path.size() - 2;
                cycle[path.at(path.size() - 1)] = 1;

                while (path.at(i) != path.at(path.size() - 1)) {


                    cycle[path.at(i)] = 1;
                    i--;
                }
                delete[] visited;
                return true;
            }
        }
    }

    delete[] visited;
    return false;
}

bool Graph::DFS_findCycle(int s, int* visited, vector<int>* path) {


    if (this->flag == 1) return true;
    else {
        // Visited but did not leave
        visited[s] = 1;
        path->push_back(s);
        // Go through the adjacency list
        for (auto i = adj[s].begin(); i != adj[s].end(); i++) {
            // If not visited, travel through that vertex
12
            if (!visited[*i]) {
                DFS_findCycle(*i, visited, path);
            }
            // If visited but did not leave - cycle
            if (visited[*i] == 1 && *i != s && !this->flag) {
                path->push_back(*i);
                this->flag = 1; return true;
            }
            if (this->flag) return true;
        }
        visited[s] = 2;
        path->pop_back();
        return false;
    }
В принципе, всё логично и давно всем понятно. Однако самое
интересное начинается далее.
Чтобы сгенерировать тренировочные данные, нельзя просто бездумно
строить матрицу смежности графа. Тогда, при обучении, нейронная сеть
будет воспринимать данные как беспорядочный шум. Она будет находить
общее, но в итоге выстроит коэффициенты весов так, чтобы ошибка была
минимальной для любого из зашумленных данных. То есть, по сути, ответ
для совершенно разных случаев был бы просто один и тот же.
Это изначально мы не предусмотрели и вводили огромное количество
тренировочных данных, просто присваивая каждой дуге в матрице
смежности случайное значение (0 или 1).
Обучение нейронной сети схоже с обучением человека. Нельзя просто
взять и показать миллион зашумленных картинок с двумя видами предметов
(в нашем случае это матрицы смежности графа с циклом и без). Это
неэффективно и, как выяснилось, бесполезно. Нужно давать как чистые
картины этих предметов, так и зашумленные, чтобы человек смог научиться
их распознавать в обеих ситуациях. Подобный тренировочный сет мы и
создали с помощью данного кода (будем рассматривать построение базы
тренировочных данных для 10 вершин. Для 5 все строится аналогично):

    srand(time(NULL));

    fstream fs;
    fs.open("train10.csv", fstream::out | fstream::in | fstream::app);
13
    if (!fs.is_open()) {
        cerr << "ERROR: File is not open..." << endl;
    }
    fs << "aa,ab,ac,ad,ae,af,ag,ah,ai,aj,"
        << "ba,bb,bc,bd,be,bf,bg,bh,bi,bj,"
        << "ca,cb,cc,cd,ce,cf,cg,ch,ci,cj,"
        << "da,db,dc,dd,de,df,dg,dh,di,dj,"
        << "ea,eb,ec,ed,ee,ef,eg,eh,ei,ej,"
        << "fa,fb,fc,fd,fe,ff,fg,fh,fi,fj,"
        << "ga,gb,gc,gd,ge,gf,gg,gh,gi,gj,"
        << "ha,hb,hc,hd,he,hf,hg,hh,hi,hj,"
        << "ia,ib,ic,id,ie,if,ig,ih,ii,ij,"
        << "ja,jb,jc,jd,je,jf,jg,jh,ji,jj,"
        << "isCycle:" << endl;

    /* CODE_FOR_GENERATING_TRAIN5
    fs << "aa,ab,ac,ad,ae,"
        << "ba,bb,bc,bd,be,"
        << "ca,cb,cc,cd,ce,"
        << "da,db,dc,dd,de,"
        << "ea,eb,ec,ed,ee,"
        << "isCycle:" << endl;
    */
    for (int k = 0; k < 1000000; k++) {

        // Graph init
        int size = 10;
        Graph g(size);
        int** matrix = new int* [size];
        for (int i = 0; i < size; i++) {
            matrix[i] = new int[size];
            for (int j = 0; j < size; j++)
                matrix[i][j] = 0;
        }

        if (k % 7 == 0) {
            // Random adj matrix
            for (int i = 0; i < size; i++) {
                for (int j = 0; j < size; j++)
                    matrix[i][j] = rand() % 2;
            }
        }
        else {
            for (int i = 0; i < size; i++) {
                int j = rand() % size, m = rand() % size;
                matrix[j][m] = 1;
            }
        }

        // Reading matrix
        g.matrixToList(matrix, size);
        int* cycle = new int[size];
        // Find cycle:

14
        bool isCycle = g.findCycle(cycle);

        // Output matrix
        for (int i = 0; i < size; i++)
            for (int j = 0; j < size; j++)
                fs << matrix[i][j] << ",";

        // Output boolean (isCycle?):


        fs << isCycle << endl;

        // Free memory:
        for (int i = 0; i < size; i++)
            delete[] matrix[i];
        delete[] matrix;
    }
    return 0;
}
Разберём его по частям. Сначала впишем в файл заголовки столбцов
базы данных, для дальнейшей обработки их при обучении:
srand(time(NULL));

    fstream fs;
    fs.open("train10.csv", fstream::out | fstream::in | fstream::app);

    if (!fs.is_open()) {
        cerr << "ERROR: File is not open..." << endl;
    }
    fs << "aa,ab,ac,ad,ae,af,ag,ah,ai,aj,"
        << "ba,bb,bc,bd,be,bf,bg,bh,bi,bj,"
        << "ca,cb,cc,cd,ce,cf,cg,ch,ci,cj,"
        << "da,db,dc,dd,de,df,dg,dh,di,dj,"
        << "ea,eb,ec,ed,ee,ef,eg,eh,ei,ej,"
        << "fa,fb,fc,fd,fe,ff,fg,fh,fi,fj,"
        << "ga,gb,gc,gd,ge,gf,gg,gh,gi,gj,"
        << "ha,hb,hc,hd,he,hf,hg,hh,hi,hj,"
        << "ia,ib,ic,id,ie,if,ig,ih,ii,ij,"
        << "ja,jb,jc,jd,je,jf,jg,jh,ji,jj,"
        << "isCycle:" << endl;

    /* CODE_FOR_GENERATING_TRAIN5
    fs << "aa,ab,ac,ad,ae,"
        << "ba,bb,bc,bd,be,"
        << "ca,cb,cc,cd,ce,"
        << "da,db,dc,dd,de,"
        << "ea,eb,ec,ed,ee,"
        << "isCycle:" << endl;
    */

После, будем вводить миллион строчек в базу данных. Таких, что


каждый 7-й раз (логики в выборе частоты «шумных» данных нет, просто не
слишком большое и не слишком малое число) будет подаваться «шумная»
15
матрица смежности, а во всех остальных случаях будем генерировать
матрицу, у которой рёбер будет максимально вершинной размерности графа.
Таким образом появятся данные с более различимыми циклами в матрице
смежности, а также данные без каких-либо циклов вообще.

for (int k = 0; k < 1000000; k++) {


        // Graph init
        int size = 10;
        Graph g(size);
        int** matrix = new int* [size];
        for (int i = 0; i < size; i++) {
            matrix[i] = new int[size];
            for (int j = 0; j < size; j++)
                matrix[i][j] = 0;
        }

        if (k % 7 == 0) {
            // Random adj matrix
            for (int i = 0; i < size; i++) {
                for (int j = 0; j < size; j++)
                    matrix[i][j] = rand() % 2;
            }
        }
        else {
            for (int i = 0; i < size; i++) {
                int j = rand() % size, m = rand() % size;
                matrix[j][m] = 1;
            }
        }
Да, можно ещё больше оптимизировать тренировочные данные, но эти
тоже дают неплохие результаты при обучении. Так что оставили их.
Скорость работы классического алгоритма нахождения цикла
После создания баз данных, вычислили скорость работы для графа из 5
вершин:
time_t t1 = time(NULL);
for (int i = 0; i < 1000000; i++) {
bool isCycle = g.findCycle(cycle);
}
time_t t2 = time(NULL);

cout << static_cast<double>(t2 - t1) / 1000000 << " seconds" << endl;

Итого: 6e-06 seconds (0,000006 секунды).

16
Функция потерь (loss)
Концептуально задача обучения нейросети сводится к нахождению
минимума определенной функции – функции потерь. Опишем что она собой
представляет.

Как вы помните, каждый нейрон следующего слоя соединен с


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

Чтобы обучать нейросеть, введем функцию потерь (англ. loss function).

Задание функции потерь для распознавания цикла


Существует множество подобных функций. В данном случае мы
решили использовать наиболее часто встречающуюся, а именно функцию
MSE (mean squared error - cреднеквадратическая ошибка):
n
1
MSE= ∑ ( y −^y i )2
n i=1 i

Математически эта функция представляет сумму квадратов разностей


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

Таким образом, чтобы впоследствии определить, насколько хорошо


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

17
Градиентный спуск
Для начала вместо того, чтобы представлять функцию с множеством
входных значений, начнем с функции одной переменной С(w). Для того
чтобы найти минимум функции, нужно взять производную.

Однако форма функции может быть очень сложной, и одна из гибких


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

У функции возможно множество локальных минимумов (рисунок 5), и


то, в каком локальном минимуме окажется алгоритм, зависит от выбора
начальной точки. Нет никакой гарантии, что найденный минимум является
минимальным возможным значением функции потерь. Кроме того, чтобы не
«проскочить» значение локального минимума, нужно менять величину шага
пропорционально наклону функции.

Рисунок 5 – Градиентный спуск и визуальное представление


минимумов функции в виде «впадин» на графике.

Чуть усложняя эту задачу, вместо функции одной переменной можно


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

Рисунок 6 – Градиент функции потерь по параметрам нейронной сети


(весам и сдвигам).

Описанная идея носит название градиентного спуска и может быть


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

Компоненты градиентного спуска


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

19
Для градиентного спуска важно, чтобы выходные значения функции
потерь изменялись плавным образом. Именно поэтому значения активации
имеют не просто бинарные значения 0 и 1, а представляют действительные
числа и находятся в интервале между этими значениями.

Каждый компонент градиента сообщает нам две вещи. Знак


компонента указывает на направление изменения, а абсолютная величина –
на влияние этого компонента на конечный результат: одни веса вносят
больший вклад в функцию потерь, чем другие.

Тогда возникает вопрос: «Каким образом рассчитывается градиент


функции потерь?». Ответ на него: с помощью модели вычислительного
графа. Алгоритм обратного распространения, по сути, работает на основе
свойства данной модели, позволяющее легко вычислять частные
производные функции. О нём расскажем далее.

Рисунок 7 – Пример нахождения градиента функции с помощью


вычислительного графа из 4-й лекции Стэнфордского курса.

20
МЕТОД ОБРАТНОГО РАСПРОСТРАНЕНИЯ ОШИБКИ
Обратное распространение — это ключевой алгоритм обучения
нейронной сети. Обсудим, в чем заключается метод.

Управление активацией нейрона


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

Варианты настройки нейросети


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

Таким образом, чтобы повысить значение этой активации, мы можем:

1. Увеличить сдвиг b.
2. Увеличить веса wi.
3. Поменять активации предыдущего слоя ai.

Из формулы взвешенной суммы можно заметить, что наибольший


вклад в активацию нейрона оказывают веса, соответствующие связям с
наиболее активированными нейронами. Стратегия, близкая к биологическим
нейросетям заключается в том, чтобы увеличивать веса wi пропорционально
величине активаций ai соответствующих нейронов предыдущего слоя.
Получается, что наиболее активированные нейроны соединяются с тем
нейроном, который мы только хотим активировать наиболее "прочными"
связями.

21
Другой близкий подход заключается в изменении активаций нейронов
предыдущего слоя ai пропорционально весам wi. Мы не можем изменять
активации нейронов, но можем менять соответствующие веса и сдвиги и
таким образом влиять на активацию нейронов.

Обратное распространение
Если говорить не математическим языком, то данный алгоритм можно
описать так:

Предпоследний слой нейронов можно рассматривать аналогично


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

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

Если говорить математическим языком, то мы применяем модель


вычислительного графа, рассчитывая частные производные каждого веса
подобным образом (в данном случае a – активация нейрона какого-то слоя, z
– взвешенная сумма, поступающая в нейрон какого-то слоя, w – вес какого-то
слоя):

22
Рисунок 8 – Расчет частной производной функции потерь по весу из
первого слоя с помощью обратного распространения для трех скрытых слоёв.

Делаем именно так, поскольку это намного проще с точки зрения


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

Вычисления простых производных выполняет библиотека Tensorflow,


как и последующее вычисление градиента функции потерь с помощью
алгоритма обратного распространения.

Далее приведём варианты обучения и их оптимизаторы (для ускорения


обучения).

Классический градиентный спуск


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

Стохастический градиентный спуск


Рассмотрение всей совокупности обучающей выборки для расчета
единичного шага замедляет процесс градиентного спуска. Поэтому обычно
делается следующее:

23
Случайным образом данные обучающей выборки перемешиваются и
разделяются на подгруппы. Далее алгоритм рассчитывает шаг градиентного
спуска для каждой подгруппы.

Это не в точности настоящий градиент для функции стоимости,


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

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

Адам отличается от классического стохастического градиентного


спуска: стохастический градиентный спуск поддерживает единую скорость
обучения (называемую альфа) для всех обновлений веса, и скорость
обучения не изменяется во время тренировки.

В Адаме скорость обучения поддерживается для каждого параметра


сети и отдельно адаптируется по мере развития обучения. Метод вычисляет
индивидуальные адаптивные скорости обучения для различных параметров
из оценок первого и второго моментов градиентов. Это алгоритм
унаследовал от другого алгоритма – RMSProp (root mean square propagation).

Авторы описывают Адама как объединение преимуществ двух других


расширений стохастического градиентного спуска. Про RMSProp уже
указали. Второй:

24
SGD с импульсом: импульс накапливает экспоненциально затухающую
скользящую среднюю прошлых градиентов и продолжает двигаться в их
направлении. Благодаря этому мы можем «проскочить» локальный минимум,
словно по инерции, чтобы двигаться дальше и искать другой, возможно
более эффективный, локальный минимум.

Собственно, Адам является популярным алгоритмом в области


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

25
СТРУКТУРА ПРОГРАММЫ ДЛЯ СОЗДАНИЯ И ОБУЧЕНИЯ
НЕЙРОННОЙ СЕТИ
Далее будем рассматривать структуру программы формирования
нейронной сети для графа из 10 вершин (для 5 вершин все будет аналогично).

Нормализация базы тренировочных данных


Как уже известно, входной слой нейронной сети должен принимать на
вход 1 или 0 в зависимости от наличия дуги в матрице смежности графа.
Потому необходимо нормализовать тренировочные данные до вида массива
последовательно записанных строчек матрицы смежности графа (в
библиотеке keras передаётся на вход нейросети тензор в виде вектора,
который автоматически преобразуется в таковой, если передавать в функцию
обучения массив библиотеки numpy).

Собственно, эта часть кода считывает данные из тренировочного


набора и нормализует их:

# Чтение БД, созданной собственноручно.


data_frame = pd.read_csv("train10.csv")

input_names = ["aa","ab","ac","ad","ae","af","ag","ah","ai","aj",
               "ba","bb","bc","bd","be","bf","bg","bh","bi","bj",
               "ca","cb","cc","cd","ce","cf","cg","ch","ci","cj",
               "da","db","dc","dd","de","df","dg","dh","di","dj",
               "ea","eb","ec","ed","ee","ef","eg","eh","ei","ej",
               "fa","fb","fc","fd","fe","ff","fg","fh","fi","fj",
               "ga","gb","gc","gd","ge","gf","gg","gh","gi","gj",
               "ha","hb","hc","hd","he","hf","hg","hh","hi","hj",
               "ia","ib","ic","id","ie","if","ig","ih","ii","ij",
               "ja","jb","jc","jd","je","jf","jg","jh","ji","jj"]

output_names = ["isCycle:"]

# Нормализация данных.

# Формирование словаря столбцов тренировочного набора


def data_frame_to_dict(df):
    result = dict()
    for column in df.columns:
        values = data_frame[column].values
        result[column] = values
    return result

26
# Разделения входного вектора тренировочных данных и выхода
def make_supervised(df):
    raw_input_data = data_frame[input_names]
    raw_output_data = data_frame[output_names]
    return{"inputs":data_frame_to_dict(raw_input_data),
           "outputs":data_frame_to_dict(raw_output_data)}
# Делаем каждый элемент списком, чтобы потом соединить их в строчки входных и
# выходных данных из базы данных с помощью функции zip
def encode(data):
    vectors = []
    for data_name, data_values in data.items():
        encoded = list(map(lambda dn: [dn], data_values))
        vectors.append(encoded)
       
    formatted = []
    for vector_raw in list(zip(*vectors)):
        vector = []
        for element in vector_raw:
            for e in element:
                vector.append(e)
        formatted.append(vector)
    return formatted
# Разделение получившегося словаря на массивы входных и выходных данных
# (которые уже нормализовали)
supervised = make_supervised(data_frame)
encoded_inputs = np.array(encode(supervised["inputs"]))
encoded_outputs = np.array(encode(supervised["outputs"]))

Обучение нейронной сети и его статистика


Создадим нейронную сеть с описанной ранее архитектурой и обучим ее
по определенным параметрам, выбор которых обосновали ранее:

# Как видно, в нашей нейронной сети 2 слоя соответственно с 100 и 1 нейроном,


# Функция активации первого слоя - relu. Второго, для нормализации выхода,
# sigmoid. Метод вычисления ошибки - mse (средн. кв. ошибки). Оптимизатор
# обучения - adam. Метрики - accuracy (точность).
model = k.Sequential()
model.add(k.layers.Dense(units = 100, activation="relu"))
model.add(k.layers.Dense(units = 1, activation="sigmoid"))
model.compile(loss = "mse", optimizer="adam", metrics=["accuracy"])

# Обучение.
fit_result = model.fit(x = train_x, y = train_y, epochs = 30, validation_split =
0.2)

Выведем статистику обучения:

# Вывод статистики обучения.


plt.title("Losses train/validation")
plt.plot(fit_result.history["loss"], label = "Train")
plt.plot(fit_result.history["val_loss"], label = "Validation")
27
plt.legend()
plt.show()

plt.title("Accuracies train/validation")
plt.plot(fit_result.history["accuracy"], label = "Train")
plt.plot(fit_result.history["val_accuracy"], label = "Validation")
plt.legend()
plt.show()

Важный момент: валидация – это результат обучения данных на


определенной части выборки тренировочного сета. Наглядно видно на
подобном примере (рисунок 9).

Статистика обучения
Важный момент: валидация – это результат обучения данных на
определенной части выборки тренировочного сета. Наглядно видно на
подобном примере (рисунок 9).

Рисунок 9 – Разделение данных для отображения статистики обучения

28
Рисунок 10 – Статистика нейронной сети для графа размерности 10.

Рисунок 11 – Статистика нейронной сети для графа размерности 5.

29
Точность моделей нейронных сетей для тестовой выборки
Благодаря огромной базе тренировочных данных, точность нейронной
сети для графа из 5 вершин стремится к 100%:

# Точность модели с неизвестными данными


y = model.predict(test_x)
y2 = tf.cast(tf.round(y), dtype = np.int32).numpy()
test_acc = len(test_y[test_y == y2]) / test_y.shape[0]*100
print(test_acc)

Вывод: 99.8

Что касается нейронной сети для графа из 10 вершин, то у нее тоже


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

# Точность модели с неизвестными данными


y = model.predict(test_x)
y2 = tf.cast(tf.round(y), dtype = np.int32).numpy()
test_acc = len(test_y[test_y == y2]) / test_y.shape[0]*100
print(test_acc)

Вывод: 90.8

СКОРОСТЬ РАБОТЫ НЕЙРОННОЙ СЕТИ


import time
start_time = time.time()
model.predict(x)
end_time = time.time() - start_time
print((time.time() - start_time), "seconds")

Итого: 0.04967904090881348 seconds (0,05 секунды, что в тысячи раз


больше времени работы классического алгоритма).

30
ВЫВОДЫ
Таким образом, цель данной работы выполнена. Стоит заметить, что
скорость работы нейронной сети в тысячи раз меньше скорости работы
классического алгоритма из-за аппаратных особенностей (нет особенных
вычислительных блоков, на которых вычисления выполнялись бы
параллельно), а также из-за особенностей языка, на котором все выполнялось
(python в сотни раз медленнее C++ по различным причинам).

Теоретически, можно сделать так, чтобы нейронная сеть выполнялась


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

31
ПРИЛОЖЕНИЕ
GenTrainsWithDFS.cpp
// Author: Stukalov Sergey (Willem White).
#include <iostream>
#include <fstream>
#include <ctime>
#include <list>
#include <vector>

using namespace std;

class Graph {
private:
    // Number of vertices
    int V;
    // Pointer to adjacency list
    list<int>* adj;
    // DFS recursive helper functions
    bool DFS_findCycle(int s, int* visited, vector<int>* cycle);
    // Detect cycle
    int flag = 0;

public:
    //Default constructor
    Graph();
    // Constructor prototype
    Graph(int v);
    // Destructor prototype
    ~Graph();

    // Method to add an edge


    void addEdge(int v, int w);

    // Adjancacy matrix to list in the graph


    void matrixToList(int** adjMatrix, int vertexSize);

    // Method for BFS traversal give a source "s"


    bool findCycle(int* cycle);
};

Graph::Graph() {
    this->V = 0;
    this->flag = 0;
    this->adj = nullptr;
}

// Constructer with number of vertices


Graph::Graph(int v) {
    // Set number of vertices
    this->V = v;
    this->flag = 0;

32
    // Create new adjacency list
    v ? adj = new list<int>[v] : this->adj = nullptr;
}

// Constructer with number of vertices


Graph::~Graph() {
    // Deleting adjacency list
    adj->~list();
}

// Implementation of method to add edges


void Graph::addEdge(int v, int w) {
    this->adj[v].push_back(w);
}

void Graph::matrixToList(int** adjMatrix, int vertexSize) {


    if (!adjMatrix)
        return;
    if (!vertexSize)
        return;
    if (!adj) adj = new list<int>[vertexSize];

    for (int i = 0; i < vertexSize; i++) {


        for (int j = 0; j < vertexSize; j++) {
            if (adjMatrix[i][j]) this->addEdge(i, j);
        }
    }
    this->V = vertexSize;
}

bool Graph::DFS_findCycle(int s, int* visited, vector<int>* path) {


    if (this->flag == 1) return true;
    else {
        // Visited but did not leave
        visited[s] = 1;
        path->push_back(s);
        // Go through the adjacency list
        for (auto i = adj[s].begin(); i != adj[s].end(); i++) {
            // If not visited, travel through that vertex
            if (!visited[*i]) {
                DFS_findCycle(*i, visited, path);
            }
            // If visited but did not leave - cycle
            if (visited[*i] == 1 && *i != s && !this->flag) {
                path->push_back(*i);
                this->flag = 1; return true;
            }
            if (this->flag) return true;
        }
        visited[s] = 2;
        path->pop_back();
        return false;
    }
}

33
// Perform DFS given a starting vertex 0
bool Graph::findCycle(int* cycle) {
    // Start with all vertices as not visited
    int* visited = new int[V];
    vector<int> path;

    for (int i = 0; i < V; i++) {


        visited[i] = 0; cycle[i] = 0;
    }

    for (int j = 0; j < this->V; j++) {

        if (!visited[j]) {

            if (DFS_findCycle(j, visited, &path)) {

                int i = path.size() - 2;
                cycle[path.at(path.size() - 1)] = 1;

                while (path.at(i) != path.at(path.size() - 1)) {


                    cycle[path.at(i)] = 1;
                    i--;
                }
                delete[] visited;
                return true;
            }
        }
    }

    delete[] visited;
    return false;
}

int main() {
    /* TEST_CODE
    int size = 5;
    Graph g(size);

    int* mat1 = new int[5]{ 0,1,0,0,0 };


    int* mat2 = new int[5]{ 0,0,1,0,0 };
    int* mat3 = new int[5]{ 0,0,0,1,0 };
    int* mat4 = new int[5]{ 1,0,0,0,0 };
    int* mat5 = new int[5]{ 0,0,0,0,0 };
    int** matrix = new int* [5]{ mat1,mat2,mat3,mat4,mat5 };

    // Reading matrix
    g.matrixToList(matrix, size);
    int* cycle = new int[size];
    bool isCycle = g.findCycle(cycle);

    for (int i = 0; i < size; i++)


        cout << cycle[i] << " ";

34
    */
    srand(time(NULL));

    fstream fs;
    fs.open("train10.csv", fstream::out | fstream::in | fstream::app);

    if (!fs.is_open()) {
        cerr << "ERROR: File is not open..." << endl;
    }
    fs << "aa,ab,ac,ad,ae,af,ag,ah,ai,aj,"
        << "ba,bb,bc,bd,be,bf,bg,bh,bi,bj,"
        << "ca,cb,cc,cd,ce,cf,cg,ch,ci,cj,"
        << "da,db,dc,dd,de,df,dg,dh,di,dj,"
        << "ea,eb,ec,ed,ee,ef,eg,eh,ei,ej,"
        << "fa,fb,fc,fd,fe,ff,fg,fh,fi,fj,"
        << "ga,gb,gc,gd,ge,gf,gg,gh,gi,gj,"
        << "ha,hb,hc,hd,he,hf,hg,hh,hi,hj,"
        << "ia,ib,ic,id,ie,if,ig,ih,ii,ij,"
        << "ja,jb,jc,jd,je,jf,jg,jh,ji,jj,"
        << "isCycle:" << endl;

    /* CODE_FOR_GENERATING_TRAIN5
    fs << "aa,ab,ac,ad,ae,"
        << "ba,bb,bc,bd,be,"
        << "ca,cb,cc,cd,ce,"
        << "da,db,dc,dd,de,"
        << "ea,eb,ec,ed,ee,"
        << "isCycle:" << endl;
    */
    for (int k = 0; k < 1000000; k++) {

        // Graph init
        int size = 10;
        Graph g(size);
        int** matrix = new int* [size];
        for (int i = 0; i < size; i++) {
            matrix[i] = new int[size];
            for (int j = 0; j < size; j++)
                matrix[i][j] = 0;
        }

        if (k % 7 == 0) {
            // Random adj matrix
            for (int i = 0; i < size; i++) {
                for (int j = 0; j < size; j++)
                    matrix[i][j] = rand() % 2;
            }
        }
        else {
            for (int i = 0; i < size; i++) {
                int j = rand() % size, m = rand() % size;
                matrix[j][m] = 1;
            }
        }

35
        // Reading matrix
        g.matrixToList(matrix, size);
        int* cycle = new int[size];
        // Find cycle:
        bool isCycle = g.findCycle(cycle);

        // Output matrix
        for (int i = 0; i < size; i++)
            for (int j = 0; j < size; j++)
                fs << matrix[i][j] << ",";

        // Output boolean (isCycle?):


        fs << isCycle << endl;

        // Free memory:
        for (int i = 0; i < size; i++)
            delete[] matrix[i];
        delete[] matrix;
    }
    return 0;
}

Neuron5.ipynb
# Импортируем всё необходимое.
import tensorflow as tf
import keras as k
from keras.models import load_model as l_m
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# Чтение БД, созданной собственноручно.


data_frame = pd.read_csv("train5.csv")

input_names = ["aa","ab","ac","ad","ae",
               "ba","bb","bc","bd","be",
               "ca","cb","cc","cd","ce",
               "da","db","dc","dd","de",
               "ea","eb","ec","ed","ee"]

output_names = ["isCycle:"]

# Нормализация данных.
def data_frame_to_dict(df):
    result = dict()
    for column in df.columns:
        values = data_frame[column].values
        result[column] = values
    return result

def make_supervised(df):
36
    raw_input_data = data_frame[input_names]
    raw_output_data = data_frame[output_names]
    return{"inputs":data_frame_to_dict(raw_input_data),
           "outputs":data_frame_to_dict(raw_output_data)}

def encode(data):
    vectors = []
    for data_name, data_values in data.items():
        encoded = list(map(lambda dn: [dn], data_values))
        vectors.append(encoded)
       
    formatted = []
    for vector_raw in list(zip(*vectors)):
        vector = []
        for element in vector_raw:
            for e in element:
                vector.append(e)
        formatted.append(vector)
    return formatted

supervised = make_supervised(data_frame)
encoded_inputs = np.array(encode(supervised["inputs"]))
encoded_outputs = np.array(encode(supervised["outputs"]))

# Определение входных и выходных данных, тестов, а также самой нейронной сети.


train_x = encoded_inputs[:999500]
train_y = encoded_outputs[:999500]

test_x = encoded_inputs[999500:]
test_y = encoded_outputs[999500:]

# Как видно, в нашей нейронной сети 2 слоя соответственно с 25 и 1 нейроном,


# Функция активации первого слоя - relu. Второго, для нормализации выхода,
# sigmoid. Метод вычисления ошибки - mse (средн. кв. ошибки). Оптимизатор
# обучения - adam. Метрики - accuracy (точность).
model = k.Sequential()
model.add(k.layers.Dense(units = 25, activation="relu"))
model.add(k.layers.Dense(units = 1, activation="sigmoid"))
model.compile(loss = "mse", optimizer="adam", metrics=["accuracy"])

# Обучение.
fit_result = model.fit(x = train_x, y = train_y, epochs = 30, validation_split =
0.2)

# Вывод статистики обучения.


plt.title("Losses train/validation")
plt.plot(fit_result.history["loss"], label = "Train")
plt.plot(fit_result.history["val_loss"], label = "Validation")
plt.legend()
plt.show()

plt.title("Accuracies train/validation")
plt.plot(fit_result.history["accuracy"], label = "Train")

37
plt.plot(fit_result.history["val_accuracy"], label = "Validation")
plt.legend()
plt.show()

# Пример поведения с неизвестными данными.


# (последние 500 не обрабатывали, потому и проверяем тут)
predicted_test = model.predict(test_x)
real_data = data_frame.iloc[999500:][input_names + output_names]
real_data["isCycle"] = predicted_test
print(real_data)

# Точность модели с неизвестными данными


y = model.predict(test_x)
y2 = tf.cast(tf.round(y), dtype = np.int32).numpy()
test_acc = len(test_y[test_y == y2]) / test_y.shape[0]*100
print(test_acc)

# Пример использования нейронной сети.


model = l_m('find_cycle_in_5x5.h5')
example = np.array([1,0,0,0,0,
                    0,1,0,0,0,
                    0,0,1,0,0,
                    0,0,0,1,0,
                    0,0,0,0,1])
x = tf.reshape(tf.cast(example, tf.float32), [-1, 25])
print(model.predict(x))

# Сохранение обученной нейронной сети(используется при обучении):


model.save('find_cycle_in_5x5.h5')

Neuron10.ipynb

# Импортируем всё необходимое.


import tensorflow as tf
import keras as k
from keras.models import load_model as l_m
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# Чтение БД, созданной собственноручно.


data_frame = pd.read_csv("train10.csv")

input_names = ["aa","ab","ac","ad","ae","af","ag","ah","ai","aj",
               "ba","bb","bc","bd","be","bf","bg","bh","bi","bj",
               "ca","cb","cc","cd","ce","cf","cg","ch","ci","cj",
               "da","db","dc","dd","de","df","dg","dh","di","dj",
               "ea","eb","ec","ed","ee","ef","eg","eh","ei","ej",
               "fa","fb","fc","fd","fe","ff","fg","fh","fi","fj",
               "ga","gb","gc","gd","ge","gf","gg","gh","gi","gj",
               "ha","hb","hc","hd","he","hf","hg","hh","hi","hj",
               "ia","ib","ic","id","ie","if","ig","ih","ii","ij",

38
               "ja","jb","jc","jd","je","jf","jg","jh","ji","jj"]

output_names = ["isCycle:"]

# Нормализация данных.
def data_frame_to_dict(df):
    result = dict()
    for column in df.columns:
        values = data_frame[column].values
        result[column] = values
    return result

def make_supervised(df):
    raw_input_data = data_frame[input_names]
    raw_output_data = data_frame[output_names]
    return{"inputs":data_frame_to_dict(raw_input_data),
           "outputs":data_frame_to_dict(raw_output_data)}

def encode(data):
    vectors = []
    for data_name, data_values in data.items():
        encoded = list(map(lambda dn: [dn], data_values))
        vectors.append(encoded)
       
    formatted = []
    for vector_raw in list(zip(*vectors)):
        vector = []
        for element in vector_raw:
            for e in element:
                vector.append(e)
        formatted.append(vector)
    return formatted

supervised = make_supervised(data_frame)
encoded_inputs = np.array(encode(supervised["inputs"]))
encoded_outputs = np.array(encode(supervised["outputs"]))

# Обпределение входных и выходных данных, тестов, а также самой нейронной сети.


train_x = encoded_inputs[:999500]
train_y = encoded_outputs[:999500]

test_x = encoded_inputs[999500:]
test_y = encoded_outputs[999500:]

# Как видно, в нашей нейронной сети 2 слоя соответственно с 100 и 1 нейроном,


# Функция активации первого слоя - relu. Второго, для нормализации выхода,
# sigmoid. Метод вычисления ошибки - mse (средн. кв. ошибки). Оптимизатор
# обучения - adam. Метрики - accuracy (точность).
model = k.Sequential()
model.add(k.layers.Dense(units = 100, activation="relu"))
model.add(k.layers.Dense(units = 1, activation="sigmoid"))

39
model.compile(loss = "mse", optimizer="adam", metrics=["accuracy"])

# Обучение.
fit_result = model.fit(x = train_x, y = train_y, epochs = 30, validation_split =
0.2)
# Вывод статистики обучения.
plt.title("Losses train/validation")
plt.plot(fit_result.history["loss"], label = "Train")
plt.plot(fit_result.history["val_loss"], label = "Validation")
plt.legend()
plt.show()

plt.title("Accuracies train/validation")
plt.plot(fit_result.history["accuracy"], label = "Train")
plt.plot(fit_result.history["val_accuracy"], label = "Validation")
plt.legend()
plt.show()

# Пример поведения с неизвестными данными.


# (последние 500 не обрабатывали, потому и проверяем тут)
predicted_test = model.predict(test_x)
real_data = data_frame.iloc[999500:][input_names + output_names]
real_data["isCycle"] = predicted_test
print(real_data)

# Точность модели с неизвестными данными


y = model.predict(test_x)
y2 = tf.cast(tf.round(y), dtype = np.int32).numpy()
test_acc = len(test_y[test_y == y2]) / test_y.shape[0]*100
print(test_acc)

# Пример использования нейронной сети.


model = l_m('find_cycle_in_10x10.h5')
example = np.array([0,0,0,0,1,0,0,0,0,0,
                    0,0,0,0,0,0,0,0,0,0,
                    0,0,0,0,0,0,0,0,1,0,
                    0,0,0,0,0,0,0,0,0,0,
                    0,0,1,0,0,0,0,0,0,0,
                    0,0,0,0,0,0,0,1,0,0,
                    0,0,0,0,0,0,0,0,0,0,
                    0,1,0,0,0,0,0,0,0,0,
                    0,0,0,0,0,0,0,0,0,0,
                    0,0,0,0,0,0,0,0,0,0])
x = tf.reshape(tf.cast(example, tf.float32), [-1, 100])
print(model.predict(x))

# Сохранение обученной нейронной сети(используется при обучении):


model.save('find_cycle_in_10x10.h5')

40

Вам также может понравиться