После одной провокационной статьи Перцептрон Розенблатта — что забыто и придумано историей? и одной полностью доказывающей отсутствие проблем в перцептроне Розенблатта, и даже наоборот показывающей некоторые интересные стороны и возможности Какова роль первого «случайного» слоя в перцептроне Розенблатта, я так думаю у некоторых хабражителей появилось желание разобраться, что же это за зверь такой — перцептрон Розенблатта. И действительно, достоверную информацию о нем, кроме как в оригинале, найти не возможно. Но и там достаточно сложно описано как этот перцептрон запрограммировать. Полный код я выкладывать не буду. Но попробуем вместе пройти ряд основ.
Начнем… ах да, предупреждаю, я буду рассказывать не классически, а несколько осовременено…
Код на C#, но думаю к нему лучше относиться как к псевдокоду. Но если кто-то чего-то не понял спрашивайте — объясню.
А начнем мы с сенсоров.
Тут все просто. Сенсор имеет состояние 0 или 1. Как только сенсор получает единицу он посылает событие. Дальше мы имеем синапс.
Он имеет тип — будет возбуждать или тормозить активность сенсора. А также реакцию, на изменение сенсора (ChangeSensorState). Теперь собственно А-элемент в среднем слое.
При создании А-элемента нужно образовать связанные с ним синапсы, пусть их будет 10. Случайно решаем с каким сенсором его соединить и какого типа будет синапс (возбуждающий или тормозящий). И главное подписываемся на смену значения сенсора, чтобы вызывать в этот момент AssumeSinapsSignal. А там мы увеличиваем уровень активации или уменьшаем в зависимости от типа привязанного синапса.
В общем все, все то что так сложно рассказывалось в Какова роль первого «случайного» слоя в перцептроне Розенблатта — мы реализовали. Мы имеем в множестве А-элементов уже гарантированно линейное представление любой произвольной задачи.
Теперь перейдем к обучению методом коррекции ошибки во втором слое. Вначале общий алгоритм, думаю понятен без слов.
Активация R-элементов тоже проста. Суммируем веса от активных А-элементов, и пробрасываем через порог (=0).
Проверка есть ошибка или нет ниже.
Тут сверяем текущую реакцию с той, что есть, и подготавливаем массив для обучения о том какая реакция ReactionError. Теперь остановимся на последнем — собственно обучении.
И усё.
Единственно, меня спрашивают — «видимо, этот алгоритм обучения коррекции с ошибкой тоже застревает как и алгоритм обратного распространения ошибки, если веса нулевые?». Как видим нет, тут само обучение начинается с нулевых весов. Тут нет ни каких математических формул — элементарные инкременты или декременты. Если вес был 0, то при коррекции ошибки он станет или +1 или -1, вес может со временем снова поменять знак пройдя через ноль, но застрять ему в нуле физически не получается.
upd.
Ниже retran предложил для активации А-элементов использовать матрицы и linq от функционального программирования в замен моему коду в событиях согласно более четкой модели по ООП.
Да, конечно код значительно короче. Но менее понятен, в него сложнее вносить изменения если меняются процессы по активации. Но главное он работает более чем в 100 раз медленнее. Вот вам и прелести математического и функционального программирования :)
Тестируем так (правда в моем коде выше несколько багов, но их легко поправить, если кто-то этим будет заниматься, + большую часть устранил и обновил код выше):
Начнем… ах да, предупреждаю, я буду рассказывать не классически, а несколько осовременено…
Код на 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(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, 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 (i = 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 (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
{
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(1000, 900);
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();