Запрограммируем перцептрон Розенблатта?

в 6:31, , рубрики: Алгоритмы, искусственные нейронные сети, искусственный интеллект, исходный код, Перцептрон, Программирование, метки: , ,

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

Начнем… ах да, предупреждаю, я буду рассказывать не классически, а несколько осовременено…

А начнем мы с сенсоров.

public class cSensor
{
    public event EventHendler ChangeState;
    private sbyte state = 0;
    public sbyte State
    {
        get { return state; }
        set 
        {
            state = value;
            if (state == 1 && ChangeState != null)
            {
                ChangeState(thisnew EventArgs());
            }
        }
    }
}
 

Тут все просто. Сенсор имеет состояние 0 или 1. Как только сенсор получает единицу он посылает событие. Дальше мы имеем синапс.

public delegate void BackCommunication(sbyte Type);
class cSinaps
{
    private sbyte type;
    private BackCommunication hBackCommunication;
    public cSinaps(sbyte tType,BackCommunication tBackCommunication)
    {
        type = tType;
        hBackCommunication = tBackCommunication;
    }
    public void ChangeSensorState(object sourse, EventArgs e)
    {
        hBackCommunication(type);
    }
}
 

Он имеет тип — будет возбуждать или тормозить активность сенсора. А также реакцию, на изменение сенсора (ChangeSensorState). Теперь собственно А-элемент в среднем слое.

public class cAssociation
{
    // Уровень активации А-элемента
    public int ActivationLevel = 0;
    // Синапсы соединенные с этим А-элементом 
    private cSinaps[] oSinaps;
 
    public cAssociation(cSensor[] sensorsField, int SCount, int ID)
    {
        int SinapsCount = 10;
        oSinaps = new cSinaps[SinapsCount];
 
        int tSinapsNumber=0;
        int tmpSensorNumber=0; 
        sbyte tmpSensorType=0;
 
        for (int j = 1; j < SinapsCount + 1; j++)
        {
            tmpSensorNumber = RND.Next(SCount) + 1;
            int x = RND() * 4;
            if (== 1 || x == 0) tmpSensorType = 1; else tmpSensorType = -1;
 
            oSinaps[tSinapsNumber] = new cSinaps(tmpSensorType, AssumeSinapsSignal);
 
            sensorsField[tmpSensorNumber].ChangeState += 
                    new EventHandler(oSinaps[tSinapsNumber].ChangeSensorState);
            tSinapsNumber++;
        }
    }
 
    void AssumeSinapsSignal(sbyte Type)
    {
        ActivationLevel += Type;
    }
}
 

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

В общем все, все то что так сложно рассказывалось в Какова роль первого «случайного» слоя в перцептроне Розенблатта — мы реализовали. Мы имеем в множестве А-элементов уже гарантированно линейное представление любой произвольной задачи.

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

public class cNeironNet
{
     public cSensor[] SensorsField; /* Сенсорное поле*/
     public cAssociation[] AssociationsFiled; /* Ассоциативное поле*/
 
     /*Добавить на обработку новый пример из обучающей выборки*/
     public int JoinStimul(int[] tPerception, int[] tReaction)
     {
         for (int i = 1; i < ACount + 1; i++)
         {
             AssociationsFiled[i].ActivationLevel = 0;
         }
         for (int i = 1; i < SCount + 1; i++)
         {
             SensorsField[i].State = 0;
         }
         // Кинем на сенсоры полученный пример
         SetSConnection(tPerception);
         // Запомним какие А-элементы были активны на этом примере
         for (= 1; i < ACount + 1; i++)
         {
             if (AssociationsFiled[i].ActivationLevel > 0)
             {
                 AHConnections[ReactionNumber].Count++;
                 AHConnections[ReactionNumber].Value[AHConnections.Count] = i;
             }
         }
         // Запомним какая реакция должна быть на этот пример
         SaveReaction(tReaction);
     }
     /* Когда все примеры добавлены, вызывается чтобы перцептрон их выучил*/
     private void Storing()
     {
         // Делаем очень много итераций
         for (int n = 1; n < 100000 + 1; n++)
         {
             // За каждую итерацию прокручиваем все примеры из обучающей выборки
             for (int i = 1; i < StimulCount + 1; i++)
             {
                 // Активизируем R-элементы, т.е. рассчитываем выходы
                 RAktivization(i);
                 // Узнаем ошибся перцептрон или нет, если ошибся отправляем на обучение
                 bool e = GetError(i);
                 if (e)
                 {
                     LearnedStimul(i);
                     Error++; // Число ошибок, если в конце итерации =0, то выскакиваем из обучения.
                 }
             }
         }
     }
}

Активация R-элементов тоже проста. Суммируем веса от активных А-элементов, и пробрасываем через порог (=0).

private void RAktivization(int ReactionNumber)
{
     int[] Summa = new int[RCount + 1];
     for (int j = 1; j < RCount + 1; j++)
     {
         for (= 1; i < AHConnections[ReactionNumber].Count + 1; i++)
         {
             Summa[j] += Weight[AHConnections[ReactionNumber].Value[i]].Value[j];
         }
     }
     for (int i = 1; i < RCount + 1; i++)
     {
         if (Summa[i] > 0) Reactions[i] = 1;
         if (Summa[i] <= 0) Reactions[i] = -1;
     }
}

Проверка есть ошибка или нет ниже.

private int GetError(int ReactionNumber)
{
    int IsError = 0;
    for (int i = 1; i < RCount + 1; i++)
    {
        if (Reactions[i] == NecessaryReactions[ReactionNumber].Value[i])
        {
            ReactionError[i] = 0;
        }
        else
        {
            ErrorCount = 1;
            ReactionError[i] = NecessaryReactions[ReactionNumber].Value[i];
        }
    }
    return IsError;
}
 

Тут сверяем текущую реакцию с той, что есть, и подготавливаем массив для обучения о том какая реакция ReactionError. Теперь остановимся на последнем — собственно обучении.

private void LearnedStimul(int ReactionNumber)
{
    for (int j = 1; j < RCount + 1; j++)
    {
        for (int i = 1; i < AHConnections[ReactionNumber].Count + 1; i++)
        {
            Weight[AHConnections[ReactionNumber].Value[i]].Value[j] += ReactionError[j];
        }
    }
}
 

И усё.

Единственно, меня спрашивают — «видимо, этот алгоритм обучения коррекции с ошибкой тоже застревает как и алгоритм обратного распространения ошибки, если веса нулевые?». Как видим нет, тут само обучение начинается с нулевых весов. Тут нет ни каких математических формул — элементарные инкременты или декременты. Если вес был 0, то при коррекции ошибки он станет или +1 или -1, вес может со временем снова поменять знак пройдя через ноль, но застрять ему в нуле физически не получается.

Автор: tac

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


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