Как стать автором
Обновить

Нейросеть как база данных активаций

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



Рассмотрим разные типы нейронных кластеров. Ранее уже упоминались сенсорные и эффекторные кластеры.
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 элементов, где каждый элемент хранит номер цикла и силу активации за тот цикл. Выходит двухуровневый кеш: для слабых сигналов, подпороговых, и самых недавних — буффер в кластере, иначе — связи на долговременную память. Не нужно забывать, что в данных статьях показан лишь псевдокод и наивные алгоритмы, а вопросы оптимизации могут занимать намного больше места.
Теги:
Хабы:
+4
Комментарии 7
Комментарии Комментарии 7

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн