Нейросети для чайников. Часть 2 — Перцептрон

в 10:30, , рубрики: Алгоритмы, нейрон, нейронные сети, Программирование, метки: , ,

image

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

Язык программирования, на этот раз — C#.
Заинтересовавшихся прошу под кат.

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

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

Нейросети для чайников. Часть 2 — Перцептрон

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

Т.к. подаваемая на вход картинка у нас черно-белая, то на входе аксона может быть только 1 или 0:
Нейросети для чайников. Часть 2 — Перцептрон
А на выходе либо значение веса, либо 0.
Грубо говоря, если на входе что-то есть — ножка начинает «дёргаться», говоря нейрону, что на ней есть информация. От того, как сильно она «дёргается», будет зависеть принимаемое сетью решение.
Количество аксонов соответствует числу элементов входного массива. В данной статье я буду использовать в качестве входной информации изображения размерами 3х5 пикселей. Соответственно, число связей, приходящих в нейрон будет 3 х 5 = 15.

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

Нейросети для чайников. Часть 2 — Перцептрон

Перейдем к программной реализации.
Создадим класс нейрона:

class Web
        {
            public int[,] mul; // Тут будем хранить отмасштабированные сигналы
            public int[,] weight; // Массив для хранения весов
            public int[,] input; // Входная информация
            public int limit = 9; // Порог - выбран экспериментально, для быстрого обучения
            public int sum ; // Тут сохраним сумму масштабированных сигналов

            public Web(int sizex, int sizey,int[,] inP) // Задаем свойства при создании объекта
            {
                weight = new int[sizex, sizey]; // Определяемся с размером массива (число входов)
                mul = new int[sizex, sizey];

                input = new int[sizex, sizey];
                input = inP; // Получаем входные данные
            }

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

Масштабирование:

            public void mul_w()
            {
                for (int x = 0; x <= 2; x++)
                {
                    for (int y = 0; y <= 4; y++) // Пробегаем по каждому аксону
                    {
                        mul[x, y] = input[x,y]*weight[x,y]; // Умножаем его сигнал (0 или 1) на его собственный вес и сохраняем в массив.
                    }
                }
            }

Сложение:

            public void Sum()
            {
                sum = 0;
                for (int x = 0; x <= 2; x++)
                {
                    for (int y = 0; y <= 4; y++)
                    {
                        sum += mul[x, y];
                    }
                }
            }

Сравнение:

            public bool Rez()
            {
                if (sum >= limit)
                    return true;
                else return false;
            }

Программа будет открывать файл-картинку такого вида:

Нейросети для чайников. Часть 2 — Перцептрон

Пробегать по всем пикселям и пытаться определить, является ли эта цифра той, которую её научили распознавать.
Т.к. у нас нейрон только один, то и распознавать мы сможем только один символ. Я выбрал цифру 5. Иными словами, программа будет говорить нам — является скормленная ей картинка изображением цифры 5 или нет.

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

        private void Form1_Load(object sender, EventArgs e)
        {
           

            NW1 = new Web(3, 5,input); // Создаем экземпляр нашего нейрона

            openFileDialog1.Title = "Укажите файл весов";
            openFileDialog1.ShowDialog();
            string s = openFileDialog1.FileName;
            StreamReader sr = File.OpenText(s);  // Загружаем файл весов
            string line;
            string[] s1;
            int k = 0;
            while ((line = sr.ReadLine()) != null)
            {
               
                s1 = line.Split(' ');
                for (int i = 0; i < s1.Length; i++)
                {
                    listBox1.Items.Add("");
                    if (k < 5)
                    {
                        NW1.weight[i, k] = Convert.ToInt32(s1[i]); // Назначаем каждой связи её записанный ранее вес
                        listBox1.Items[k] += Convert.ToString(NW1.weight[i, k]); // Выводим веса, для наглядности
                    }

                }
                k++;

            }
            sr.Close();
        }

Далее, необходимо сформировать массив входных данных:

            Bitmap im = pictureBox1.Image as Bitmap;
            for (var i = 0; i <= 5; i++) listBox1.Items.Add(" ");

            for (var x = 0; x <= 2; x++)
            {
                for (var y = 0; y <= 4; y++)
                {
                  
                    int n = (im.GetPixel(x, y).R);
                    if (n >= 250) n = 0; // Определяем, закрашен ли пиксель
                    else n = 1;
                    listBox1.Items[y] = listBox1.Items[y] + "  " + Convert.ToString(n);

                    input[x, y] = n; // Присваиваем соответствующее значение каждой ячейке входных данных

                }

            }

Распознаем символ, вызывая описанные выше методы класса:

        public void recognize()
        {
            NW1.mul_w();
            NW1.Sum();
            if (NW1.Rez()) listBox1.Items.Add(" - True, Sum = "+Convert.ToString(NW1.sum));
            else listBox1.Items.Add( " - False, Sum = "+Convert.ToString(NW1.sum));
        }

Все. Теперь наша программа уже может называться нейронной сетью и что-то определять.
Однако, она совсем глупа и всегда выдает False.
Необходимо обучить её. Обучать будем по простейшему алгоритму.

Если сеть выдает правильный ответ — радуемся и ничего не делаем.
А если ошибается — наказываем её соответствующим образом:

— Если её неправильный ответ False, то прибавляем значения входов к весам каждой ножки (к ножке 1 — значение в точке [0,0] картинки и т.д.):

            public void incW(int[,] inP)
            {
                for (int x = 0; x <= 2; x++)
                {
                    for (int y = 0; y <= 4; y++)
                    {
                        weight[x, y] += inP[x, y];
                    }
                }
            }

— Если её неправильный ответ True, то вычитаем значения входов из веса каждой ножки:

            public void decW(int[,] inP)
            {
                for (int x = 0; x <= 2; x++)
                {
                    for (int y = 0; y <= 4; y++)
                    {
                        weight[x, y] -= inP[x, y];
                    }
                }
            }

Затем сохраняем полученные изменения в массиве весов и продолжаем обучение.
Как я уже сказал, я взял для образца цифру 5: Нейросети для чайников. Часть 2 — Перцептрон

Кроме того, я приготовил остальные цифры и несколько вариантов самой цифры 5:

Нейросети для чайников. Часть 2 — Перцептрон

Вполне ожидаемо, что самой проблемной будет цифра 6, однако и это мы преодолеем с помощью обучения.
Создаем текстовый файл, заполненный нулями 3х5 — чистая память нашей сети:

000
000
000
000
000

Запускаем программу и указываем ей на этот файл.
Загружаем картинку цифры 5:
Нейросети для чайников. Часть 2 — Перцептрон
Естественно, ответ неверный. Нажимаем «Не верно».
Происходит перерасчет весов (можете посмотреть результат в файле):

1 1 1
1 0 0
1 1 1
0 0 1
1 1 1

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

1 2 1
1 0 -4
1 2 1
-4 0 1
1 1 0

Нейросети для чайников. Часть 2 — Перцептрон

Вот и шестерка определяется корректно.

Исходные коды программы, исполняемый файл, файлы весов и картинок 3х5 можно взять отсюда.
Если хотите поиграться с другими символами или обучить сеть заново — не забудьте обнулить все цифры в файле w.txt

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

За сим откланяюсь, спасибо за чтение.

Автор: Paul_Smith

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js