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

image

Начнем с предыстории. Учусь я на первом курсе Новосибирского ВУЗа, подходит летняя сессия и мне пришлось сдавать курсовую работу. Темой курсовой работы я мог выбрать вполне простую и легкую для понимания программу на c++, будь то калькулятор длинных чисел или же клавиатурный тренажер. Но, имея опыт работы webdev, решил что тема моей курсовой работы будет «Простейшие нейронные сети для распознавания образов». Сразу вспомнил про замечательную библиотеку FANN, и, не долго думая приступил к работе. Решил я научить нейронку различать 3 фигуры: круг, квадрат и треугольник.

Дальнейшее свое описание я хотел бы разбить на несколько ключевых пунктов:

  • Мои попытки разобраться с библиотекой
  • Первый удачный опыт
  • Конечный результат
  • Шишки, которые я набил

Мои попытки разобраться с библиотекой


Стоило только установить библиотеку (думаю проблем не у кого не возникает, ибо уроков по установке FANN и так очень много), я начал ее тискать и лапать. Тестировал функции, изучал теорию нейронов и нейросетей в целом.

Единственное, что я смог найти по этой теме — пару уроков на Хабре, которые устарели, так как функции и синтаксис FANN изменился.

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

Первое и самое главное — Вам необходимо понять что такое нейронная сеть. Понять принцип. Я бы с радостью расписал тут все, но есть и так куча уроков на эту тему. Могу посоветовать: Нейронные сети для начинающих.

Второе и немаловажное — нейронные сети одновременно просты и сложны.

Ну, в общем, наверное все. Дальше буду описывать то, как строится нейронная сеть и опишу свой первый удачный опыт.

Первый удачный опыт


Моей радости не было предела, когда моя нейронная сеть дала адекватный ответ. Изначально я создал класс. В этом классе я использовал ф-ию, которая учит нейронную сеть определять фигуру. Точнее просто добавляет ее в «очередь на изучение».

	/**
	 * Добавление фигуры
	 *
	 * @param  string $name    		Название фигуры
	 * @param  string $dir 		 	Папка для обучения
	 * @param  array &$newh 		Указатель на массив
	 *
	 * @return array             	Готовый массив для обучения сети
 	*/

    public function addFigure($name, $dir, &$tonn)
    {
        $j = 0;
        $d = dir($dir);
        while($entry = $d->read()) 
        {
            if ( preg_match("/jpg/", $entry) )
            {
                $im = $this->imageresize($dir . $entry,16,16,75);
                imagefilter($im, IMG_FILTER_GRAYSCALE);
                $cur_array = array();
                $cnt = 0;
                for($y=0; $y<16; $y++)
                {
                    for($x=0; $x < 16; $x++)
                    {
                        $rgb = imagecolorat($im, $x, $y) / 16777215;
                        $rgb = round($rgb, 6);
                        $cur_array[$cnt] = $rgb;
                        $cnt++;
                    }
                }
                imagedestroy($im);
                $tonn[$name][$j] = $cur_array;
                $j++;
            }
        }
        return $tonn;
    }

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

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

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

	/**
	 * Подготовка изображения к проверке
	 *
	 * @param  string $img    		Ссылка на изображение
	 *
	 * @return array             	Массив данных для нейронной сети
 	*/

    public function input($img)
    {
        $Input = array();
        $im = $this->imageresize($img,16,16,75);
        imagefilter($im, IMG_FILTER_GRAYSCALE);
        $cnt = 0;
        for($y=0; $y<16; $y++)
        {
            $count = 0;
            for($x=0; $x < 16; $x++)
            {
                $rgb = imagecolorat($im, $x, $y) / 255;
                $rgb = round($rgb, 6);
                $Input[$cnt] = $rgb;
                $cnt++;
            }
        }
        $count = 0;

        imagedestroy($im);
        return $Input;
    }

Начинается самое интересное — обучение нейронной сети. Я решил реализовать сл. образом:

    public function teach($tonn)
    {
        $num = count($tonn);
        $ann = fann_create_standard_array(3, array(256, 128, $num));
        for ($i=0; $i < 10000; $i++) {
            $j= 0;
            $arout = array();
            foreach ($tonn as $key => $value) {
                for($k = 0; $k<count($tonn);$k++){
                    if($k==$j){
                        $arout[$key][$k] = 1;
                    }else{
                        $arout[$key][$k] = 0;
                    }
                }
                $j++;
                for ($s=0; $s < count($tonn[$key]); $s++) { 
                    fann_train($ann, $tonn[$key][$s], $arout[$key]);
                }
            }

        }
        fann_save($ann,"base/base.ann");
        fann_destroy($ann);
        return "base/base.ann";
    }

Тут разберу поподробнее некоторые моменты.

$ann = fann_create_standard_array(3, array(256, 128, $num));

Мы создаем нейронную сеть с 3 слоями нейронов.

1 слой — 256 нейронов, так сказать входной слой. Почему 256, очень просто — 16пикселей*16пикселей=256пикселей.
2 слой — 128 нейронов, так сказать промежуточный слой.
3 слой — кол-во нейронов, равное кол-ву фигур в «очереди». Это слой, который возвращает нейронная сеть.

Второй момент.

fann_train($ann, $tonn[$key][$s], $arout[$key]) — обучение нейронной сети.

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

Ну, и в итоге мы сохраняем всю нашу «базу» в файл base/base.ann. Это делается для быстрого использования сети в дальнейшем без обучения.

Далее по классу идет ф-ия «think» и ф-ия обработки результатов. Они выглядят вот так:

	/**
	 * Распознование образов
	 *
	 * @param  string $file    		Ссылка к файлу Нейронных Сетей
	 * @param  string $img    		Ссылка на изображение
	 *
	 * @return array             	Массив результатов
 	*/

    public function think($file, $img)
    {
        $ann = fann_create_from_file($file);
        $output = fann_run($ann, $img);
        return $output;
    }

	/**
	 * Обработка результата
	 *
	 * @param  array $output    	Массив результатов
	 *
	 * @return array             	Массив результатов
 	*/

    public function showResult($output)
    {
        foreach($output as $k => $v)
        { 
            if( ! is_numeric($v)) return array();
            
            if( ! isset($max))
            {
                $max = $v;
                continue;
            }
            
            if($v > $max) $max = $v;
        }
        
        $arr = array();
        foreach($output as $k => $v)
        { 
            if($v == $max) $arr[] = $k;
        }
        
        return $arr;
    }

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

Вот так происходит обучение на практике:

$nn = new NeuralNetwork;

/* Обучение нейронных сетей */
if(isset($_GET['teach'])){
	$tonn = array();
    $nn->addFigure("triangle", "teach/triangle/", $tonn);
    $nn->addFigure("circle", "teach/circle/", $tonn);
    $nn->addFigure("square", "teach/square/", $tonn);
    $ann = $nn->teach($tonn);
}

В папки для обучения я загрузил фотографии данных фигур.

Ну, и само распознание изображения, ничего сложного:

if(isset($_POST['checkfigure'])){
    $uploaddir = 'uploads/';
    $uploadfile = $uploaddir . basename($_FILES['getimage']['name']);
    move_uploaded_file($_FILES['getimage']['tmp_name'], $uploadfile);
    copy("uploads/".$_FILES['getimage']['name'], "thumbs/".$_FILES['getimage']['name']);
    $Input = $nn->input("thumbs/".$_FILES['getimage']['name']);
    $output = $nn->think("base/base.ann", $Input);
    $result = $nn->showResult($output);
    switch ($result[0]) {
        case '0':
            $answer = "Треугольник";
            break;
        case '1':
            $answer = "Круг";
            break;
        case '2':
            $answer = "Квадрат";
            break;
        default:
            $answer = "неизвестно";
            break;
    }
    echo $answer;
}

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

Конечный результат


Конечный результат можно увидеть в моем репозитории гитхаба.

Шишки, которые я набил


Нейронные сети выдавали очень смешанный результат, более похожий на рандомные числа.

Как решил: я просто уменьшил изображения и сделал их черно-белыми. Если Ваша нейронная сеть выдает разные числа, значит она не правильно обучилась, как бы банально это не звучало. Алгоритм, по которому вы будете строить массивы, которыми в дальнейшем будете пичкать вашу сеть — придумывать вам.

Нейронные сети выдавали 99% не правильный ответ.

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

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

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

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

Поделиться с друзьями
-->

Комментарии (2)


  1. iiifx
    18.05.2017 12:54

    Спасибо, достаточно простой но и понятный пример. Будет с чего начать знакомство. Разобраться бы еще и как оно там под капотом устроено.


  1. crazy_llama
    18.05.2017 12:55

    Может я чего-то не нашел, но на github очень маленькая выборка. Я бы рекомендовал тот же MNIST (выборку с рукописными цифрами) — данные подготовлены, их много, текущая точность для разных моделей известна.