Posts Tagged ‘код’

Параллелизация нейросетей – 2

Пятница, Июнь 4th, 2010

Параллелизация нейросетей – 2
В прошлой заметке описывалась довольно сложный способ параллелизации вычислений. Нейроны (нейрокластеры) делились на блоки и распределялись между потоками, некоторые данные дублировались, данные о связях также распределялись по потокам.
Но вот вычислительных ядер становится все больше и больше. Может оказаться, что лучше написать более вычислительно сложный код, но написать его раньше. Идея из функционального программирования – никаких особых синхронизаций не нужно, главное, чтобы функция не давала никаких побочных эффектов. Идею вспомнил во время чтения http://graphics.cs.williams.edu/archive/SweeneyHPG2009/TimHPG2009.pdf, стр 44 (ссылка с хабра). Каждая итерация каждого потока модифицирует данные только одного нейрона.
Код превращается во что-то типа:
* Создать несколько потоков
В каждом потоке:
* выбрать очередной необработанный нейрон
* обработать
В целях ускорения, очередность выборки нейронов потоками можно ставить в зависимости от потока.
Дальнейшая оптимизация: после завершения обработки «своих» нейронов, поток может перейти на обработку «чужих». Чтобы синхронизация проводилась не на каждом нейроне, распределять нейроны можно небольшими группами. Величину групп можно варьировать в зависимости от количества связей в нейронах. Пересчет границ групп можно проводить редко.
Недостаток подхода – лишний доступ к памяти при проверке «не нужно ли добавить активацию с этого нейрона», если нейрон отправки не активен. В некоторых ИНС это может увеличивать количество необходимой работы в 10-100 раз. Конечно, если потоков 100-1000, то и такой поворот событий сулит выигрыш :) Если в ИНС много пересылок «подпороговых» сигналов (подпороговых к высокочастотной активации), то такой способ более эффективен. Другой способ:
В каждом потоке выделяется интервал идентификаторов нейронов, который он имеет право менять. Для каждого потока выполняется функция:
Если данный нейрон должен модифицировать другие нейроны, то начать проход по связям
Если связь ведет к нейрону, идентификатор которого содержится в интервале, выделенном данному потоку– то его можно модифицировать.
Выигрыш такого подхода: в сравнении с предыдущим способом, обработка только активных связей (в некоторых случаях в 10-100 меньше работы), но лишняя проверка интервала на каждой связи, и количество проходов по памяти прямо пропорционально количеству потоков. В случае отсортированного списка связей, проверку вхождения нейрона в интервал можно делать только одну – вначале на одну границу интервала (например, «меньше»), а затем, после начала блока подходящих идентификаторов – только другую границу. После выхода за границу можно сразу переходить к следующему нейрону. Это может в 2 раза сократить обработку лишних связей. Еще в 2 раза – если начинать движение с начала или с конца списка связей в зависимости от интервала нейронов.
Вывод – вначале выгоднее попробовать схему с тупой синхронизацией нейронов назначения :)

Оптимизация проверки активации по bool

Среда, Апрель 7th, 2010

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

Еще более экономичный способ – при активации нейрона, заносить его в связный список активных нейронов данного контура. Список этот, ради максимальной дешевизны, должен быть односторонним и без подсчета количества элементов. Если не хочется выделять отдельное поле для хранения следующего элемента списка, то можно использовать поле активации через объединение переменных С++. Прохождение по такому списку должно его очищать.

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

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

Вторник, Январь 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)