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



Рассмотрим разные типы нейронных кластеров. Ранее уже упоминались сенсорные и эффекторные кластеры.
If, он же And — активируется, только если активны все условия — то есть, сигнал пришел по всем синапсам.
Or — срабатывает, если хотя бы один признак был активирован. Если этот кластер — часть цепочки, то связь назад по цепочке является обязательной — она подключена по условию And. Другими словами, кластер активируется, только если прошлый кластер цепочки был активен и любое из его собственных условий тоже сработало. В аналогии языков программирования, связь по цепочке выступает как instruction pointer в центральном процессоре — сигнал «разрешаю исполнение остальных условий кластера». Рассмотрим немного кода.

class NC;//нейрокластер
class Link {
	public:
		NC& _from;
		NC& _to;
		...
};
Class LinksO;/* контейнер для исходящих связей. Удобно делать на основе boost::intrusive
 - ради экономии памяти и улучшения производительности */
class LinksI;//тоже на основе boost::intrusive
struct NeuronA1 {
	qreal _activation = 0;
	static const qreal _threashold = 1;//этот порог никогда не меняется и не хранится ради экономии памяти, нейрон всегда нормализован.
	bool activated()const {
		return _activation >= _threshold;
	}
};
struct NeuronAT {
	qreal _activation = 0;
	qreal _threashold = 1;//меняется и хранится
	bool activated()const {
		return _activation >= _threshold;
	}
};
class NC {
	public:
		LinksO _next;
		LinksO _down;
		
		LinksI _prev;
		LinksI _up;
		
		NeuronA1 _nrnSumPrev;
		NeuronAT _nrnSumFromBottom;
		...
}
//чтобы было понятнее, как появляется активация на _nrnSumPrev:
void NC::sendActivationToNext() {
	for(Link& link: _next) {
		link._to._nrnSumPrev._activation += 1;
	}
}
//эта функция одинаковая у всех кластеров - and/or/not и других:
bool NC::allowedToActivateByPrevChain()const {
	if(_prev.isEmpty())//нету связей назад по времени, кластер не в цепочке, поэтому нету сдерживающих условий.
		return true;//поэтому можно проверять остальные условия, специфичные для данного типа кластера.
	return _nrnSumPrev.activated();
	//можно было бы уйти от первой проверки на наличие связей, если настраивать порог нейрона при добавлении и удалении связей.
	//тогда при отсутствии связей порог всегда 0 и нейрон всегда активирован.
	//но когда-то можно забыть изменить порог срабатывания, поэтому для научно-исследовательского кода лучше проверять наличие связей, а не менять пороги.
}

Обратите внимание, что в _prev обычно или нету связей, или одна связь. Это делает из цепочек памяти — префиксное дерево: в _next может быть сколько угодно связей, а в _prev — не более одной. Только в обычных префиксных деревьях на каждой позиции лишь одна буква, а в нейросети — произвольное количество признаков. Благодаря этому даже хранение словаря Зализняка не будет занимать много памяти.

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

bool NC::allowedToActivateByPrevChain()const {
	for(Link& link: _prev) {
		NC & nc = link._from;
		if(!nc.wasActivated())//проверка за прошлый цикл
			return false;
	}
	return true;
}


Тогда бы сразу же ушло очень много проблем:
1) После нескольких циклов прогнозирования, не нужно восстанавливать состояние нейросети — кластеры как хранили, так и хранят информацию про свою активацию за соответствующие циклы. Прогнозирование можно включать намного чаще и на более длительные интервалы вперед.
2) Нейросеть устойчива к изменениям: если в кластер с запозданием добавили связь на другой кластер, то не нужно заново посылать сигналы, чтобы просуммировать активацию на кластере назначения — можно сразу же проверять условия. Код становится более функционально-парадигменным — минимум побочных эффектов.
3) Появляется возможность вводить произвольные задержки сигнала: если кеш активаций может хранить данные за разные циклы, то можно проверить, был ли активен кластер Н циклов назад.
Для этого, в связь добавим изменяемый параметр — время задержки:
class Link {
	...
	int _delay = 1;
};

и тогда функция модифицируется вот так:
bool NC::allowedToActivateByPrevChain()const {
	for(Link& link: _prev) {
		NC & nc = link._from;
		if(!nc.wasActivated(link._delay))//проверка N циклов назад
			return false;
	}
	return true;
}


4) Избавляемся от заиканий «во дворе трава, на траве дрова, ...»: сигналы с более новых циклов не перезатрут старые, и наоборот.
5) Нету опасности, что активация угаснет (сама, от времени), когда она еще требуется. Можно проверять условия далеко назад по времени.
6) Наконец, можно не писать десяток статей на тему «управление нейросетью через управление ритмической активностью», «методы визуализации управляющих сигналов электроэнцефалограмм», «специальный DSL для управления электроэнцефалограммами» и выкинуть вот это вот все:



Теперь про реализацию такого кеша активаций:
1) ЕНС дают нам три варианта для размещения кеша активаций: текущая активация в самом нейрокластере в его нейронах, активиация (в виде идентификационных волн?) в гиппокампе (тут она хранится дольше, чем на самом кластере), долговременная память. Выходит трехуровневый кеш, прямо как у современных процессоров.
2) В программной модели кеш активаций на первый взгляд удобно расположить в каждом кластере.
3) А конкретнее, у нас уже есть и то, и то: гиппокамп в такой модели создает цепочку памяти, а в цепочку памяти заносятся связи на все кластеры, которые были активными и не были заторможены в тот момент времени. А каждая связь хранится в одном кластере как исходящая и в другом как входящая. Отсюда видно, что «кеш» на самом деле не кеш, а вовсе даже долговременная память. Только биологические нейронные сети не могут извлекать информацию из долговременной памяти напрямую, только через активацию, а ИНС могут. Это преимущество ИИ над ЕНС, которое глупо не использовать — зачем возится с активациями, если нам нужна семантическая информация?

Итого, чтобы проверить, был ли активен кластер N шагов назад, можно использовать такой (не оптимизированный) псевдокод:

NC* Brain::_hippo;//текущий кластер, в который добавляются текущие события
NC* NC::prevNC(int stepsBack)const {
	//проход назад по цепочке по связям через _prev
	//при этом суммируем link._delay, чтобы узнать текущее смещение назад по времени.
	//проверяем границы, (не)возвращаем результат
}
bool NC::wasActivated(int stepsAgo)const {
	NC* timeStamp = _brain._hippo->prevNC(stepsAgo);
	if(!timeStamp)//система вообще ничего не помнит про то время
		return false;
	return linkExists(timeStamp, this);
// код поиска связи должен исполнятся быстро, так как boost дает не только intrusive списки,
//но и деревья, причем размер одного node возврастает всего с 2 до 3 указателей
}

Если вместо ушедшей в небытие активации нужно сохранять не только наличие связи, но и силу активации, то соответствующее поле можно добавить в саму связь. Под эту цель можно задействовать и другие поля, без введения дополнительных: например, «важность», от которой зависит длительность жизни связи.
Но как быть с кластерами, у которых активация не дотягивает до превышения порога, но все равно полезна, например, для нечеткого распознавания, или просчета вероятностей и т. п.? Неоптимизированное решение — использовать все те же связи. Для этого или создать дополнительные контейнеры связей внутри кластера и добавлять их туда (чтобы не смешивались с нормальными, сработавшими), или вообще мешать все в кучу, а разделять их только по силе. Такие связи надо будет удалять быстрее, так как их на порядок больше других. Более оптимизированное решение: каждый кластер хранит нормальный кеш активаций — например, циркулярный буфер (кольцо) на 16 элементов, где каждый элемент хранит номер цикла и силу активации за тот цикл. Выходит двухуровневый кеш: для слабых сигналов, подпороговых, и самых недавних — буффер в кластере, иначе — связи на долговременную память. Не нужно забывать, что в данных статьях показан лишь псевдокод и наивные алгоритмы, а вопросы оптимизации могут занимать намного больше места.

Комментарии (7)


  1. qw1
    06.01.2016 01:35
    +1

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

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

    Вот, к примеру

    попробуем реализовать полноценное исполнение любых программ на нейросети
    Чем классическая MT не угодила, или Лисп-интерпретатор?


    1. neurocod
      06.01.2016 01:47

      >Чем классическая MT не угодила, или Лисп-интерпретатор?

      1) Для ИНС было наработано много алгоритмов, которые «не влазят» в лисп. Ну или влазят, ведь нейросеть можно сделать на лиспе, но ИНС дает другой уровень абстракции — можно сказать, «новая библиотека для лиспа». Взять хотя бы то, что у лиспа car/cdr содержат по одному указателю, а для данных реального мира чаще нужно сразу много информационных признаков на один момент времени.
      2) Такие попытки улучшили понимание нейросетей — и ИНС, и ЕНС

      >ощущение, что смотрю репортаж из сновидения.

      Я вас не виню, так как этому есть причины. Когда-то давно я выкладывал в уже закрытый блог несколько десятков статей по нейросетям, которые сейчас вместились в пару. Просто сейчас свободного времени еще меньше (это же хобби, а не основная работа), желания пересказывать то, что уже давно писал, тоже поубавилось, да и многие решения уже неактуальны. В результате надергал самых интересных результатов за все время. А детально объяснять — так будут те десятки статей + новые.
      Но несмотря на все это, некоторым людям эти статьи могут быть полезными. Наиболее вероятно — тем, кто уже сильно погружен в эти области и ловит концепции на лету.

      >понять, какую задачу решаем, подготовить тестовые данные

      Тестовые данные были разные, тестовые задания тоже разные. Я даю выжимку из подходов, которые оказались удобны и полезны для разных данных и целей. Поэтому они так переплетены. «Сон чудовищ рождает разум»


      1. qw1
        06.01.2016 12:54

        Для ИНС было наработано много алгоритмов, которые «не влазят» в лисп

        О чём я и говорю! Если брать задачу распознавания заранее (до обучения) неизвестных буковок, ИНС несомненно крута, потому что алгоритмами непонятно как делать.

        Но если проверять возможность выполнения алгоритмов нейросетью, возникает вопрос «зачем». Алгоритм-то всё равно задаётся извне. Это всё равно, что крестики-нолики писать на брейнфаке. Да, прикольно. Но быстрее написать на питоне.

        Другое дело, если нейросеть не только выполняет, но и формирует алгоритмы. И тут уже интересно посмотреть на тестовые входные данные, и как ИНС справляется с ними.


        1. neurocod
          06.01.2016 19:37

          Цель естественно была — создание универсального ИИ. Делать его из каши на разных языках и разных структур данных — провальная идея, хотя бы потому, что на смену интерфейсов/протоколов/форматов будут уходить ресурсы и компьютера, и разработчиков, рефлексия ИИ будет затруднена их такой каши и т. д.


          1. qw1
            06.01.2016 20:26

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

            Так, блуждание вслепую, и может что нащупается.


  1. krox
    06.01.2016 06:59

    Не подскажете, скриншоты какой программы приведены в статье?


    1. neurocod
      06.01.2016 10:10

      Собственной разработки. Первые 2 версии были на C++/MFC, третья и четвертая — на C++/Qt. Вдохновением служила в том числе OllyDbg, Выкладывать такое в открытый доступ сейчас смысла мало: большая часть нейросетей решали не практические задачи, а служили для отработки отдельных аспектов алгоритмов. Причем, так как раньше я использовал не git + тестовые данные были в бинарном формате, то не догадался складывать их в систему контроля версий, и для старых ИНС даже тестовые примеры надо будет создавать заново. Лишь недавно догадался перевести на json + git, и хранить их вместе с исходниками.