Pull to refresh

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

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

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



Код на C#, но думаю к нему лучше относиться как к псевдокоду. Но если кто-то чего-то не понял спрашивайте — объясню.

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

public class cSensor
{
    public event EventHandler 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, Random RND)
        {
            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);
                if (RND.Next(2) == 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; /* Ассоциативное поле*/
      int ACount;
      int SCount;
      public ArrayList AHConnections;
      Random RND = new Random();
 
        public cNeironNet(int argSCount, int argACount)
        {
            ACount = argACount;
            SCount = argSCount;
            SensorsField = new cSensor[SCount];
            for (int i = 0; i < SCount; i++)
            {
                SensorsField[i] = new cSensor();
            }
            AssociationsFiled = new cAssociation[ACount];
            for (int i = 0; i < ACount; i++)
            {
                AssociationsFiled[i] = new cAssociation(SensorsField, SCount, RND);
            }
        }
 
      /*Добавить на обработку новый пример из обучающей выборки*/
      public ArrayList 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;
          }
          // Кинем на сенсоры полученный пример
          for (int i = 0; i < SCount; i++)
          {
              SensorsField[i].State = tPerception[i];
          }
          // Запомним какие А-элементы были активны на этом примере
          AHConnections = new ArrayList();
          for (= 0; i < ACount; i++)
          {
              if (AssociationsFiled[i].ActivationLevel > 0)
              {
                    AHConnections.Add(i);
              }
          }
          // Запомним какая реакция должна быть на этот пример
          SaveReaction(tReaction);
          return AHConnections;
      }
      /* Когда все примеры добавлены, вызывается чтобы перцептрон их выучил*/
      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
        {
            IsError = 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, вес может со временем снова поменять знак пройдя через ноль, но застрять ему в нуле физически не получается.

upd.

Ниже retran предложил для активации А-элементов использовать матрицы и linq от функционального программирования в замен моему коду в событиях согласно более четкой модели по ООП.

    public class Perceptron1
    {
        public int[][] SAMatrix { get; private set; }
        public Perceptron1(int sensorsCount, int associativeElementsCount)
        {
            var random = new Random();
            SAMatrix = new int[associativeElementsCount][];
            for (var i = 0; i < associativeElementsCount; i++)
            {
                SAMatrix[i] = new int[sensorsCount];
                for (var j = 0; j < 10; j++) 
                {
                    var sindex = random.Next(sensorsCount);
                    if (random.Next(2) == 1)
                        if (random.Next(2) == 1)
                            SAMatrix[i][sindex] += 1;
                        else
                            SAMatrix[i][sindex] -= 1; 
                }
            }
        }
        public int Activate(int i, int[] inputs)
        {
            return (SAMatrix[i].Zip(inputs, (w, input) => w * input).Sum() > 0 ? 1 : 0);
        }
    }
 


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

Тестируем так (правда в моем коде выше несколько багов, но их легко поправить, если кто-то этим будет заниматься, + большую часть устранил и обновил код выше):

            Random random = new Random();
            int[] Input = new int[1000];
            int[] AActiv = new int[900];
 
            TimeSpan BeginTime = DateTime.Now.TimeOfDay;
            Perceptron1 P1 = new Perceptron1(1000900);
            for (int i = 0; i < 100; i++)
            {
                for (int j = 0; j < 1000; j++)
                {
                    Input[j] = random.Next(2);
                }
                for (int j = 0; j < 900; j++)
                {
                    AActiv[j] = P1.Activate(j, Input);
                }
            }
            TimeSpan locTime = DateTime.Now.TimeOfDay - BeginTime;
 
            //TimeSpan BeginTime = DateTime.Now.TimeOfDay;
            //Perceptron2 P2 = new Perceptron2(1000, 900);
            //for (int i = 0; i < 10000; i++)
            //{
            //    for (int j = 0; j < 1000; j++)
            //    {
            //        Input[j] = random.Next(2);
            //    }
            //    P2.JoinStimul(Input);
            //}
            //TimeSpan locTime = DateTime.Now.TimeOfDay - BeginTime;
 
            Console.WriteLine(locTime.ToString());
            Console.ReadLine();
 
Tags:
Hubs:
+35
Comments 211
Comments Comments 211

Articles