Posts Tagged ‘код’

Сериализация ИНС

Вторник, Январь 26th, 2010

Стандартный для НЛ родительский шаблонный класс ИНС обеспечивает универсальный гибкий механизм сериализации ИНС.
Для того, чтобы данный тип ИНС можно было сохранить или загрузить через пользовательский интерфейс, ее класс должен быть зарегистрирован как поддерживающий сериализацию. Для этого создается глобальный объект SerializableBrainClassRegistrator с данными о ИНС – с именем класса ИНС и другими. При выборе операции открытия ИНС из файла, сначала считывается имя класса ИНС, затем, если класс с таким именем зарегистрирован как поддерживающий загрузку, происходит создание экземпляра данного класса, и вызывается метод загрузки. Аналогично и с сохранением. Механизм разрешения нужен для того, чтобы не вводить в заблуждение пользователей – так как в научно-любительской программе далеко не все ИНС могут поддерживать сериализацию, из-за неактуальности операции для ее программирования.
При необходимости сериализации, в классах нейронов и связей пользовательской ИНС нужно переопределить функции сохранения и загрузки:

  1. virtual void save(QDataStream & s)const;
  2. virtual bool load(QDataStream & s);

В этих функциях, основываясь на примере реализации в других ИНС, провести сериализацию данных нейрона и связи, специфических для данного класса, и вызвать родительскую версию.
Универсальный алгоритм в упрошенном описании действует так. Проводит сериализацию описания формата и в случае загрузки проверяет, что формат поддерживается.
Сериализует количество нейронов, которое надо загрузить или сохранить. Далее, если нейронов более одного, происходит сериализация тела нейрона. После этого идет код подстраховки – сериализуется проверка на размер данных сериализованного нейрона. Если проверка пройдена, сериализуются и остальные нейроны.
Аналогично происходит сериализация связей. Так как все нейроны уже созданы, то связи можно создавать напрямую. После сериализации первой связи идет код проверки размера связи в сериализованном состоянии.
Также сериализуются: комментарии, точки останова, нейронные группы, и т. д. После данных каждого типа вставляется код проверки. Для сериализации данных, специфических для конкретной ИНС, вместо переопределения функции всей сериализации, удобнее переопределить лишь функцию serializeBrainSpecificData. Если нужно, то можно переопределять и другие функции.

Высокоуровневый счетчик циклов

Пятница, Декабрь 18th, 2009

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

Но бывает полезно вводить высокоуровневые счетчики. Не «плюс 53 цикла от начала», а «обработка второй буквы в сенсорном вводе». Полностью заменить таким высокоуровневым счетчиком обычный счетчик циклов крайне нежелательно – потеряются все преимущества, описанные выше. Можно ввести отдельную переменную. Тогда то по ней также можно будет создавать условия точек останова. Можно будет использовать ее в коде обработки нейронов и в других целях. Но тогда возникает вопрос, нужно ли добавлять эту информацию о более высокоуровневом счетчике во многие структуры и окна – логирования операций с нейронными контурами, время создания каждого нейрона и связи, каждого кластера и объекта нейронной цепочки, вывода диагностических сообщения, истории ввода/вывода.

В Нейролаборатории принят промежуточный подход. Основной счетчик, который запоминается в большинстве структур – низкоуровневый, как и прежде. Но при этом добавлен высокоуровневый счетчик, который влияет на первый. А именно, периодически можно увеличивать его значение. Тогда значение низкоуровневого счетчика округляется вверх до некоторой величины. И значение низкоуровневого счетчика становится равным величине (величина_округления*количество_высокоуровневых_итераций), после чего этот счетчик растет обычным способом до очередного округления вверх.

Величина округления взята равной 1000. Эта величина выбрана по нескольким соображениям. Во-первых, количество увеличений счетчика в типичной высокоуровневой итерации не превышает 10-20. Например, одна итерация сна сейчас занимает 16. Тогда величина округления должна быть больше этого значения, и с запасом – например, 40 или 60. Второе соображение – по значению низкоуровневого счетчика, который и будет наблюдаться в большинстве окон, должно быть легко визуально определять основную итерацию и низкоуровневые шаги. Это значит, что величина округления должна быть кратной десяти в целой степени – тогда в десятичной системе счисления в правой части фиксированное количество цифр будет занимать счетчик низкоуровневых шагов. И третье – желательно разделять эти две части, например, нулем, чтобы было еще легче. Если бы величина округления была равна 100, то двузначные значения подитераций не оставляли бы места для нуля. А со значением 1000, ноль остается. Теперь можно легко визуально определить, к какой итерации относится та или иная запись. Номер высокоуровневой итерации будет равен количеству тысяч в общем счетчике.

В то же время, возникает опасение на счет слишком быстрого пожирания величины циклов. Но циклы хранятся в восьмибайтовом целом беззнаковом числе. Это примерно четыре миллиарда в квадрате. Если поделить на тысячу, то останется четыре миллиарда на четыре миллиона. Если система будет делать четыре миллиона операций, которые логируются, в секунду, то на оставшиеся значения смогут увеличиваться без переполнения несколько десятилетий (как время по юниксу). Но четыре миллиона циклов в секунду может делать только очень небольшая нейросеть, разработкой которых я не занимаюсь. Значит, времени предостаточно.

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

ASSERTоподобные точки останова

Пятница, Ноябрь 27th, 2009

Обычные инструкции типа ASSERT, как и обычные точки останова, останавливают исполнение в отладчике. Нейронные точки останова останавливают работу нейросети в Нейроредакторе без прерывания программы. Но устанавливать однотипные точки останова после каждого запуска – ненужная рутина. Поэтому их можно перед запуском создавать программно. Но программное создание такой же точки останова, как и в интерфейсе Нейроредактора, которая может редактироваться через ГИП, не тривиально, и занимает несколько строк. Для некоторых стандартных случаев типа остановки каждые несколько циклов созданы функции-обертки. Но можно пойти еще одним путем – использовать упрощенные точки останова, которые не отображаются в интерфейсе, и которые невозможно изменить. Условно говоря – вызвать функцию типа maybeStopNeuroNet(bool) с условием проверки точки останова. Еще удобнее использовать макрос: если условие сработает – нейросеть остановится после завершения прохода по всем нейронам, а условие, которое сработало, будет выведено в журнал исполнения. Булевское условие можно писать любое, даже такое, которое не входит в список полей Д3, по которым только и работают обычные точки останова Нейролаборатории. Например, можно объявить локальную переменную, и по ней останавливать ИНС.
Еще один вариант – использовать QtScript, и код точек останова вводить в виде JavaScript в Нейролаборатории. Но этот способ требует существенного усложнения кода, и пока не нужен.

Оптимизация ИНС

Воскресенье, Ноябрь 15th, 2009

Оптимизация будет осуществляться при выпуске промышленных версий, или когда будет не хватать памяти. Желательно автоматически.
Нормализация по кластеру. (далее…)

Эволюция реализации внутрикластерных виртуальных связей

Вторник, Ноябрь 3rd, 2009

Вначале использовались самые простые решения. Для получения нейрона заданного типа использовалась функция кластера
neuron* getNeuronByType(NeuronType ntype);
Типичная реализация основывалась на switch(ntype), который возвращал то или иное поле указателя на нейрон. В случае размера кластера, превышающего 4 нейрона, требовалось более двух сравнений – если switch оптимизируется в двоичный поиск. Иначе, может потребоваться значительно больше сравнений.
Для того, чтобы избавиться от нескольких сравнений, нужно было использовать получение нейрона сразу. Для этого использовались шаблонные функции, в качестве шаблонного параметра принимающие тип нейрона, но со специализацией по этому параметру. Версия по умолчанию ничего не делала, только выдавала предупреждения:

  1. template <NeuronType ntype>
  2. neuron* getNeuronByType()
  3. {
  4.         ASSERT(0);//используй специализацию!
  5.         return 0;
  6. }
  7.  
  8. template <>
  9. neuron* getNeuronByType<NTypeOut>()
  10. {
  11.         return this->nOut;
  12. }
  13. template <>
  14. neuron* getNeuronByType<NTypeIn>()
  15. {
  16.         return this->nIn;
  17. }
  18. /*Для использования таких функций, нужно было, чтобы тип нейрона при операциях с объектами нейронных контуров был известен на этапе компиляции. Поэтому, нейронные контуры также дополнялись шаблонным параметром «тип нейрона»: */
  19. NeuroCircuit<NTypeOut> nOut;
  20. NeuroCircuit<NTypeIn> nIn;

При частом изменении моделей ИНС, добавлении/удалении/переименовании нейронных контуров, однотипные изменения нужно было производить в слишком многих местах. Нужно было автоматизировать хотя бы часть работы. Я пошел по тому же пути, что и в таблицах «D3» – создание таблиц метаданных с информацией о полях. В такой таблице хранились записи вида «тип нейрона; смещение на указатель нейрона от начала объекта нейрокластера». Различия были следующими. Так как производительность более важна, то таблицы основывались не на QList, а на обычных массивах – итерации по ним осуществляются быстро даже в debug-версии (с которой и происходит большая часть моей теперешней работы), так как не нужно вызывать кучу функций типа конструкторов итераторов. Конечно же, такую таблицу удобно создавать макросами, типа:

  1. BEGIN_NEURON_MEMBERS_TABLE(NcB2)
  2.         ADD_PNEURON_MEMBER(nIn,         BrainB2::NTypeIn        )
  3.         ADD_PNEURON_MEMBER(nOut,        BrainB2::NTypeOut       )
  4.         …
  5. END_NEURON_MEMBERS_TABLE()

Используя эту таблицу, имея объект нейрокластера, можно в цикле пройтись по всем полям типа «нейрон» нейрокластера. Тут же были переписаны несколько функций:
* isActiveClusterInAnyCycle (возвращает true, если хотя бы один нейрон в кластере активен – используется для визуализации в Нейролаборатории)
* установки комментария на все нейроны кластера
* добавления кластера в группу нейронов (синие квадраты на карте нейронов)
* обнуление полей при инициализации
* связывание нейронов с нейрокластером
* функция getNeuronByType
Из-за последнего пункта, теперь можно было устранить шаблонный параметр «тип нейрона» в объектах нейронных контуров. Но что с производительностью? Снова итерации по таблице для получения одного нейрона? В таком случае, можно было оптимизировать построение таблицы – не первые места вынести нейроны с тем типом, который наиболее часто используется в виртуальных связях, например, nOut. Но не обязательно делать поиск по таблице для каждого кластера. В операциях с нейронными контурами, в одном вызове функции с использованием виртуальных связей, для всех нейронов по циклу используется только один тип виртуальной связи. Поэтому можно перед циклом по всем нейронам один раз пройтись по таблице, найти смещение поля нейрона, и затем использовать его. Останется лишь одна операция сложения – сложение адреса текущего нейрокластера со смещением поля «указатель на нейрон» – то же самое, что и при доступе к полю по объекту в тех шаблонных функциях. Могут быть практически несущественные отличия в производительности из-за того, что в случае шаблонных функций смещение прошито на уровне кода, а тут берется из регистра.

Нейролаборатория: обновление

Пятница, Октябрь 2nd, 2009


Выделение сенсорного ввода показало нейроны, в которых он сохранен


Чтобы не создавать новые мозги по любому поводу, произошла универсализация механизма ввода/вывода. Ввод и вывод производится при помощи абстракции «строковой идентификатор». По каждому новому уникальному идентификатору ИНС может создавать сенсорный нейрокластер. Например, в качестве идентификатора может выступать:
* отдельная буква, причем только одна за цикл – в текстовых мозгах типа Б2
* любая строка, причем в любом количестве за один цикл – в том же Б2, но для целей более общего тестирования алгоритмов
* простейшие визуальные стимулы по типу описанного Б3 также могут конвертироваться в строковые идентификаторы – типа «номер_пикселя=номер_цвета»
* отдельные команды типа «пауза исполнения», «переход в сон»
* элементы DOM-дерева при анализе xml/html документов
Описание реализации. Главный класс:

  1. class IdBrainInputAbstract
  2. {
  3. public:
  4.         class Command
  5.         {
  6.                 …
  7.         };
  8.         virtual void readNext(OUT QList<Command> & listCommands)=0;
  9. };

(далее…)

Биологические основы выделения обобщений и кратной активации

Четверг, Сентябрь 24th, 2009

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

Проверка в коде была вида activation>=2*threshold

Но чтобы писать код с использованием объектов «нейронный контур», нужно было ввести отдельную функцию для такой проверки. В дополнение к функции neuron::isActive, с именем типа isActivatedDoubly. Более красивое решение – использовать шаблонную функцию с параметром «кратность активации». Но так как С++ не поддерживает шаблонные параметры из нецелых чисел, то пришлось коэффициент кратности делать целым. И тогда в функцию обработки нейроконтура можно было передать neuron::isActive<2>, надеясь, что компилятор вместо преобразования типов данных догадается подставить только инкремент степенной части числа с плавающей запятой.

Но у этого подхода есть концептуальные недостатки. Как известно, потенциал действия биологических нейронов при разрядке всегда один и тот же. Накопление на мембране «двойного заряда» не подходит. Повышение порога гуморальными механизмами – слишком медленно (а люди просыпаются достаточно быстро). Можно рассмотреть изменение потенциала на мембранах вспомогательной нейросетью. Можно в режиме выделения тормозить ею нейроны, так что активироваться смогут только нейроны с двойной порцией активации. Или можно во время обычной работы дополнительно активировать нейроны, а в режиме выделения – прекращать поставлять помощь. Можно комбинировать активацию и торможение. Но мне более вероятным кажется использование времени активации в качестве варьируемого параметра. Так как посылка активации в природных нейросетях в режиме высокочастотного возбуждения происходит не единовременно, а множеством спайков, то нейрон, получающий двойную порцию активации (от двух цепочек), будет активирован примерно вдвое быстрее, чем в нормальных условиях. Вариантов реализации механизма выделения нейронов с двойной активацией тогда остается два. В одном варианте, нейроны цепочек шлют вдвое меньше спайков, чем обычно, и активируются только нейроны, входящие в обе цепочки. Мне более вероятным представляется случай, при котором отдельная нейросеть детектирует нейроны, активировавшиеся вдвое быстрее, чем обычно. Вдвое быстрее, конечно, после предыдущего такта нейросети «посылка к признакам». Тот же Бужаки говорил, что есть нейроны, очень точно реагирующие на время активации.

После того, как концептуальные вопросы решены, можно было оставить код, как есть, лишь помня о несколько другом смысле «двойной активации». Понятие двойной активации возникает, так как спайки в моих нейросетях моделируются одним целым, и нет возможности обнаружить изменение времени активации. Можно было бы разделить спайки на два – и вместо одного квазиспайка, всегда слать два квазиспайка. В результате, схема обнаружения общих признаков работала бы так, как и гипотетический природный аналог. Но есть более эффективный подход – слать половинную активацию. Вместо умножения порога или деления активации каждого нейрона при сравнении, лучше послать половинную активацию лишь у тех, кто активирован (с нейронов nDn->* ). Этого можно всего добиться, если понизить порог нейронов контура nDn вдвое, и в зависимости от режима вместо VA(nX, nDn) писать что-то типа VA(nX, nDn, 0.5f), или VA(nX, nDn, 1.f)

Мозгокод vs C++

Среда, Сентябрь 23rd, 2009

Тот случай, когда функции на одну строку бывают очень удобны. Функции с использованием объектов «нейронный контур» вызываются в разных случаях, но для двух самых распространенных случаев использования в каждом мозге полезно вводить следующие функции:

  1. //by Virtual link, Activate for next cycle
  2. //static
  3. void BrainB2::VA(NeuroCircuit & n1, NeuroCircuit & n2, NeuroThread* thread)
  4. {
  5.         n1.VLinkNN< &neuron::isActive, &neuron::activateNext>(n2, thread);
  6. }
  7.  
  8. //send signals if neuron is active
  9. //static
  10. UINT64 BrainB2::send(NeuroCircuit & n, NeuroThread* thread)
  11. {
  12.         UINT64 ret = n.forallNS< &neuron::isActive, &SendSignalsStandard>(thread);
  13.         return ret;
  14. }
  15.  
  16. //n1->VA n2 на мозгокоде теперь будет соответствовать
  17. VA(n1, n2, thread)
  18.  
  19. //n1->* на мозгокоде теперь будет соответствовать
  20. send(n1, thread)

Портирование в Linux

Понедельник, Сентябрь 7th, 2009

Нейролаборатрия портирована в Linux.
Исходная среда разработки – MS Windows XP, MS Visual Studio 2008, Qt 4.5
Среда назначения – Kubuntu Linux, Qt Creator 1.2, Qt 4.5


Нейролаборатория, портированная в Linux


В код пришлось внести сотни изменений. Выработал следующий порядок портирования: (далее…)

Упрощение сравнений активаций нейронов

Суббота, Август 1st, 2009

Для примера, возьмем обработку одного из режимов сна:

  1. //Псевдокод, похожий на Мозгокод:
  2. //Для краткости записи, (neuron.activation>const) будет записываться как (neuron>const),
  3. //(neuron.activation>neuron.threshold) будет записываться как (neuron>t)
  4. для всех нейрокластеров
  5. {
  6.         switch(neuroCluster)
  7.         {
  8.                 case (nIn>0 && nEn>0)режим_выделения; break;
  9.                 case (nIn>t && nEn>t) ->VA nOut; break;
  10.                 case (nIn>0 && nEn==0)->V nFd; break;
  11.         }
  12. }

При попытке реализации этого псевдокода на С++, возникает много инструкций сравнения, причем более громоздких. Вместо case (nIn>0 && nEn>0){} нужно будет писать либо if(0 < nIn->activation && nIn->activation < nIn->thrNorm && 0 < nEn->activation){…}, либо предусмотреть, чтобы сценарий с активацией нейрона, превышающей порог, проверялся раньше, чем на превышение только нуля. Можно использовать макросы, например

  1. #define ACTIVATED_BEFORE_THRESHOLD(n) (0 < n->activation && n->activation < n->thrrNorm)

Но тогда к if-ам нужно будет везде ставить else – иначе компилятор может решить, что при исполнении внутренних инструкций активация и порог могли измениться, и начать производить сравнения снова. Кроме того, при изменении названий полей активации и порога у нейронов разных типов, придется применять другие макросы, например, с префиксом типа мозга. Код становится более «мозго-зависимым». Можно использовать inline-функции класса нейрона, называемые одинаково у нейронов разных типов. Но желательно еще и не делать лишних сравнений. Компилятор не знает, что порог всегда будет не меньше нуля, и не сможет оптимизировать исполнение сокращением количества сравнений. Попытка сделать оптимизацию вручную усложнит код. Код теряет наглядность. Решение – явно разбить активацию на отрезки с применением inline-функций, одинаковых в разных мозгах:

  1. //Threshold Type
  2. enum ThrType
  3. {
  4.         EqLess0 = 0,//activation is Equal or Less 0
  5.         Greater0 = 1,//activation is greater than 0 and less than threshold
  6.         GreaterT = 2,//activation is greater or equal to threshold
  7. };
  8.  
  9. class neuronB2: public CommonNeuron
  10. {
  11. public:
  12.         operator ThrType()const
  13.         {
  14.                 const signal_t & a = …;//активация за текущий цикл
  15.                 const signal_t & t = thrNorm;//нормализированный порог
  16.  
  17.                 ThrType ret = EqLess0;
  18.                 if(a>0)
  19.                 {
  20.                         if(a>=t)
  21.                                 ret = GreaterT;
  22.                         else
  23.                                 ret = Greater0;
  24.                 }
  25.                 return ret;
  26.         }
  27.         …
  28. }
  29.  
  30. void BrainB2::someFunc()
  31. {
  32.         for all clusters()
  33.         {
  34.                 …
  35.                 const ThrType tNin = *nIn;
  36.                 const ThrType tNEn = *nEn;
  37.                
  38.                 if(tIn==GreaterT && tEn==GreaterT)      {}
  39.                 if(tIn==Greater0 && tEn==EqLess0)       {}
  40.                 if(tIn==Greater0 && tEn==Greater0)      {}
  41.         }
  42. }

Конечно, код с использованием ThrType целесообразно использовать только в случае нескольких сложных проверок, иначе он становится менее эффективным, чем простой вызов inline-функции типа neuron::isActive() с единственным сравнением порога с активацией.