После одной провокационной статьи Перцептрон Розенблатта — что забыто и придумано историей? и одной полностью доказывающей отсутствие проблем в перцептроне Розенблатта, и даже наоборот показывающей некоторые интересные стороны и возможности Какова роль первого «случайного» слоя в перцептроне Розенблатта, я так думаю у некоторых читателей появилось желание разобраться, что же это за зверь такой — перцептрон Розенблатта. И действительно, достоверную информацию о нем, кроме как в оригинале, найти не возможно. Но и там достаточно сложно описано как этот перцептрон запрограммировать. Полный код я выкладывать не буду. Но попробуем вместе пройти ряд основ.
Начнем… ах да, предупреждаю, я буду рассказывать не классически, а несколько осовременено…
А начнем мы с сенсоров.
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(this, new 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 (x == 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 (i = 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 (i = 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