Попробуем решить задачу классификации с помощью искусственной нейронной сети для известного и уже классического набора данных - Ирисы Фишера. Кто не знает, что это такое, отправляю вас на Википедию, там отличная статья с картинками и самим набором данных.
Набор содержит 150 объектов (собственно, ирисов), которые описываются с помощью 4 вещественных параметров. Каждый ирис принадлежит одному из 3 классов. В этом примере мы обучим сеть на 130 объектах и протестируем полученную сеть на 20 оставшихся объектах.
Для решения задачи будем использовать библиотеку с открытым исходным кодом FANN (Fast Artificial Neural Network Library), которая написана на языке C. Эта библиотека довольно часто выпадала в ответах для моих поисковых запросов, поэтому решил проверить. Ссылка на официальный сайт библиотеки, ссылка на репозиторий GitHub.
Собственно, язык C мы и будем использовать для реализации решения. Из названия библиотеки нам обещают быстродействие - проверим на практике.
Что нам должна ответить нейронная сеть - к какому классу принадлежит объект тестовой выборки.
Приступим к практике. Первым делом разделим наш набор данных на два файла, в одном из которых будет находится 130 объектов для обучения сети, в другом файле будут находится объекты для тестирования сети. Как я понял, FANN требует определенный формат файлов с данными. Выглядеть каждый файл должен так:
1 2 3 4 5 6 7 |
20 4 3 6.1 3 4.6 1.4 0 1 0 6.7 3.1 5.6 2.4 0 0 1 5 3.2 1.2 0.2 1 0 0 |
Где первая строка файла (параметры разделены пробелами) это количество объектов в данном файле, количество параметров объекта (количество входных нейронов сети), количество выходных нейронов сети. Вторая строка это описание параметров объектов. В нашем случае ирис описывается 4-мя вещественными значениями. Третья строка это описание правильной реакции выходных нейронов. В нашем случае, первый объект из примера принадлежит второму классу (0 1 0), второй объект принадлежит третьему классу (0 0 1), третий объект принадлежит первому классу (1 0 0). То есть при тестировании 3 выходных нейрона в идеале должны выдавать именно такие значения. Но такого никогда не будет. Выходные нейроны будут давать результаты в вещественных числах в пределах от 0 до 1.
Для удобства выкладываю данные текстовые файлы с наборами данных для нашего эксперимента. В архиве находится два текстовых файла. Ссылка на загрузку.
Наборы есть, осталось написать рабочую программу. Ниже привожу рабочий вариант программы с подробным описанием, что там происходит.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
#include #include "fann.h" #define IRIS_DATA_TRAIN_FILE "iris_dataset.train" #define IRIS_DATA_TEST_FILE "iris_dataset.test" int main(){ /** * Объявляем основные переменные. */ // количество слоев сети, с учетом входного и выходного слоя. const unsigned int layersNum = 3; // количество нейронов в скрытом слое. const unsigned int hiddenNeuronsNum1 = 65; // указатель на нашу нейронную сеть. struct fann *network; // указатели на структуры данных, где будут находится загруженные наборы данных. struct fann_train_data *trainData, *testData; // указатель для вывода выходных значений выходных нейронов. fann_type *output; /** * Загружаем наборы данных из файлов. */ trainData = fann_read_train_from_file(IRIS_DATA_TRAIN_FILE); testData = fann_read_train_from_file(IRIS_DATA_TEST_FILE); /** * Создаем "стандартную" нейронную сеть с обратным распространением ошибки. * Связи между слоями устанавливаются автоматически с архитектурой "все со всеми". * В функции указываем количество слоев сети, с учетом входного и выходного слоя, * количество входных нейронов, количество нейронов в скрытых слоях, количество выходных нейронов. */ network = fann_create_standard(layersNum, trainData->num_input, hiddenNeuronsNum1, trainData->num_output); /** * Устанавливаем алгоритм обучения для сети. Все алгоритмы описаны в fann_train_enum. * Тут выбран первый и стандартный итерационный алгоритм. */ fann_set_training_algorithm(network, FANN_TRAIN_INCREMENTAL); /** * Устанавливаем функцию активации для скрытых слоев сети. * На выбор реализовано много функций, см. fann_activationfunc_enum. * Тут выбрана сигмоидальная функция активации. */ fann_set_activation_function_hidden(network, FANN_SIGMOID); // можно также установить функцию активации для выходных нейронов //fann_set_activation_function_output(network, FANN_SIGMOID); /** * Запускаем обучение сети. * trainData - наш указатель с данными для обучения. * 5000 - количество эпох обучения. Больше не всегда лучше. * 10 - через сколько эпох делать вывод в консоль о результатах обучения. 0 - без вывода. * 0.001 - желаемое значение ошибки. */ printf("Training network start\n"); fann_train_on_data(network, trainData, 5000, 10, 0.001); /** * Очищаем СКО сети. */ fann_reset_MSE(network); int realClass = 0; /** * Запускаем цикл тестирования сети. * Для этого пройдемся по всем входным значениям - * - нашего тестового набора testData. * fann_run возвращает указатель на специальный тип fann_type, * из которого можно получить значения выходных нейронов сети. * (это просто указатель на массив float). */ for(int i = 0; i < testData->num_data; i++){ output = fann_run(network, testData->input[i]); // находим номер исходного класса объекта, чтобы сравнить с результатами for(int j = 0; j < testData->num_output; j++){ if(testData->output[i][j] > 0){ realClass = j; break; } } printf("\t%d) [Original class: %d] Output neurnos: %f, %f, %f\n", i, realClass, output[0], output[1], output[2]); } /** * Сохраняем нашу обученную сеть в файл. */ fann_save(network, "iris_classifier.net"); /** * Освобождаем занятые ресурсы. * Удаляем нашу сеть. */ fann_destroy_train(trainData); fann_destroy_train(testData); fann_destroy(network); return 0; } |
Как видно, в коде есть места с хардкодом, но в этом примере это не так важно.
Я выбрал простую архитектуру 4-65-3: то есть 4 входных нейрона, 65 нейронов в скрытом слое, и 3 выходных нейрона. В данном случае число нейронов в скрытом слое случайное, поскольку абсолютно точной методики для нахождения нужного количества нейронов в скрытых слоях и количества этих скрытых слоев - нету, поэтому обычно эти значения подбираются, пока не будут удовлетворительные результаты. Существуют разные эмпирические законы для расчета, но они редко бывают точными, поскольку итоговые функции, к которым должна стремиться нейронная сеть, для разных задач разные.
Для оценки насколько качественно сработала нейронная сеть, я не буду считать разные оценки точности, полноты и т.д. Просто приведу примеры, где зрительно можно оценить правильность классификатора. Ниже на картинках в квадратных скобках приведен исходный класс объекта (напомню, всего их 3: 0, 1 и 2, т.е. тут нумерация начинается с 0) и значения выходных нейронов. Первый столбец это значения выходного нейрона для класса 0, второй столбец для класса 1, третий столбец для класса 2. Чем ближе значение к 1, тем больше вероятность принадлежности объекта к классу.
Вот результаты для исходной 4-65-3 архитектуры:
Как видно в целом сеть дает хорошие результаты. Разные классы объектов явно отделены друг от друга разной активностью выходных нейронов, поэтому в целом спутать не получится.
Экспериментировал с разным количеством нейронов в скрытом слое. Получил следующие результаты.
Такие результаты для 90 нейронов:
Такие результаты для 25 нейронов:
Также для улучшения результатов нужно подбирать количество эпох обучения. Но важно понимать, что больше - не всегда лучше, потому что сеть может сильно переобучиться. Но и мало эпох также плохо сказывается на обучение искусственных нейронных сетей.
Классификатор, считаю, в целом удался. Теперь можно спокойно определять где какой ирис Фишера.