Базовые принципы алгоритмов обучения Hierarchical Temporal Memory

Доброго времени суток!
В последнее время на хабре довольно часто появляются статьи, в которых авторы описывают современные теории и подходы к построению искусственного интеллекта и нейронных сетей. Однако примеров конкретной реализации приводится довольно скудное количество. Попробуем восполнить этот пробел. В данной статье я опишу только основные теоретические и практические моменты, использованные при написании рабочего макета алгоритмов, предоставленных Numenta Inc.

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

Архитектура HTM

Архитектура сети, как уверяет автор, базируется на строении коры головного мозга, а если быть точнее, на той её части, которую называют неокортексом. Базовые единицы — клетки-нейроны, организуются в колонки, которые в свою очередь составляют участки (region). Из участков в дальнейшем возможно построить ту самую иерархию, в честь которой названа сеть.
Картинка с официального сайта дает общее представление о строении участков:
image

Строение клетки, по аналогии с естественным нейроном, предусматривает наличие дендритных сегментов (состоящих из некоторого множества дендритов) и синапсов — мест соединения между аксоном другой клетки и собственным дендритом. Каждый синапс в HTM фактически представлен «прочностью» (permanence) — вещественным числом от 0 до 1. Если прочность синапса меньше задаваемого значения, то синапс считается «несуществующим», в противоположном случае — действующим. В этом заключается понятие потенциального синапса (potential synapse).
Дендритные сегменты одной клетки делятся на дистальные и проксимальные. Проксимальные используются для получения входной информации, дистальные — для связи клеток между собой, внутри данного участка. В предлагаемой Numenta архитектуре каждая колонка клеток имеет один общий проксимальный сегмент, что способствует увеличению быстродействия. Каждый дендритный сегмент (как дистальный, так и проксимальный) содержит некоторое множество потенциальных синапсов.
Входные биты информации представляются в виде слоя клеток, способных формировать синапсы с проксимальными дендритами.
На базе данной архитектуры работают алгоритмы обучения, состоящие из двух фаз: пространственного (spatial) и временного (temporal, темпорального?) группировщиков.

Пространственный группировщик

Пространственный группировщик включает в себя три фазы: перекрытия (overlap), подавления (inhibition), обучения (learning).
В фазе перекрытия для каждой колонки вычисляется величина, отражающая «охват» синапсами проксимального дендритного сегмента данной колонки «единичных» битов входной информации входной информации. Активным считается синапс, прочность которого больше установленного значения и бит информации, к которому он ведет, содержит 1.
  1. public void SOverlap() {
  2.     for(int i=0;i<xDimension*yDimension;i++) {
  3.         overlap[i] = 0.0;
  4.         for(Synapse synapse: connectedSynapses(i)) {
  5.             overlap[i] += input(time, synapse.c, synapse.i);
  6.         }
  7.         if (overlap[i] < minOverlap)
  8.             overlap[i] = 0.0;
  9.         else
  10.             overlap[i] *= boost[i];
  11.     }
  12. }

В фазе подавления вычисляется список активных колонок. Колонки с максимальным «охватом» подавляют своих соседей, количество активных колонок в регионе регулируется задаваемым пользователем параметром.
  1. public void SInhibition() {
  2.     activeColumns.add(new ArrayList<Integer>());
  3.     for(int i=0;i<xDimension*yDimension;i++) {
  4.         Double minLocalActivity = kthScore(neighbours(i), desiredLocalActivity);
  5.  
  6.         if (overlap[i] > 0.0 && overlap[i] >= minLocalActivity) {
  7.             activeColumns.get(time).add(i);
  8.         }
  9.     }
  10. }

В фазе обучения прочность активных синапсов проксимального дендритного сегмента каждой активной колонки повышается, неактивных, соответственно понижается. Затем применяется механизм, обеспечивающий регулярное срабатывание всех колонок в регионе. В общих словах, он состоит в том, что мы ведем историю активации каждой из колонок (в виде скользящего среднего) и усиливаем «охват» колонки в случае её долгого простоя.
  1. public void SLearning() {
  2.     for(Integer c: activeColumns.get(time)) {
  3.         for(Synapse s: potentialSynapses.get(c)) {
  4.             if (input(time, s.c, s.i) > 0) {
  5.                 s.permanence += permanenceInc;
  6.                 s.permanence = Math.min(s.permanence1.0);
  7.             } else {
  8.                 s.permanence -= permanenceDec;
  9.                 s.permanence = Math.max(s.permanence0.0);
  10.             }
  11.         }
  12.     }
  13.  
  14.     for(int i=0;i<xDimension*yDimension;i++) {
  15.         minDutyCycle[i] = 0.01 * maxDutyCycle(neighbours(i));
  16.         activeDutyCycle[i] = updateActiveDutyCycle(i);
  17.         boost[i] = boostFunction(activeDutyCycle[i], minDutyCycle[i]);
  18.         overlapDutyCycle[i] = updateOverlapDutyCycle(i);
  19.  
  20.         if (overlapDutyCycle[i] < minDutyCycle[i]) {
  21.             increasePermanences(i, 0.1*connectedPerm);
  22.         }
  23.     }
  24.  
  25.     inhibitionRadius = averageReceptiveFieldSize();
  26. }

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

Временной группировщик

Временной группировщик, как и пространственный, состоит из трех фаз: вычисления состояний активности (active) и обучения (learn) каждой из клеток, вычисления состояний предсказания (prediction) и фазы применения предпологаемых на предыдущих шагах изменений в структуре сети — добавления новых синапсов/дистальных дендритных сегментов.
В первой фазе для каждой активной колонки из списка, доставшегося от пространственного группировщика, вычисляется — была ли эта активность предсказана какой-либо из клеток колонки. В случае нахождения такой клетки, она одна становится активной, в противном — все клетки в колонке становятся активными. По определенному правилу выбирается клетка для обучения, при невыполнении правила обучается наиболее подходящая клетка из колонки.
В следущей фазе на основе активности сегментов вычисляются клетки в предсказывающем состоянии и усиливаются сегменты, активность которых привела к удачному предсказанию на прошлом временном шаге.
На последнем этапе для обучающихся клеток применяются все изменения, которые были предложены в двух предыдущих фазах. Добавляются новые сегменты или потенциальные синапсы к списку уже существующих для данного сегмента.
  1. public void TLearning() {
  2.     for(int c = 0; c < xDimension*yDimension; c++) {
  3.         for(int i = 0; i < cellsPerColumn; i++) {
  4.             if(learnState.get(time).get(c).get(i)) {
  5.                 adaptSegments(segmentUpdateList.get(c).get(i)true);
  6.                 // segmentUpdateList.get©.get(i).clear();
  7.             } else if (!predictiveState.get(time).get(c).get(i) && predictiveState.get(time-1 > 0 ? time-1 : 0).get(c).get(i)) {
  8.                 adaptSegments(segmentUpdateList.get(c).get(i)false);
  9.                 // segmentUpdateList.get©.get(i).clear();
  10.             }
  11.             segmentUpdateList.get(c).get(i).clear();
  12.         }
  13.     }
  14. }

Заключение

Будем считать, что общее представление об алгоритмах обучения, представленное в статье, достаточно для понимания базовых принципов. Всех интересующихся конкретной реализацией могу пригласить на github. Представленный там проект далек от идеала в плане соответствия архитектуре HTM, но, практически, в точности соответсвует представленным в нументовском пдф'е отрывкам псевдокода и описанию функций и данных.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 29

    0
    А для каких задач подходят такие сети?
      0
      В данном виде эти сети интересны скорее с исследовательской точки зрения, чем с практической. В теории, можно попробовать применить их к распознаванию речи, визуальных образов, навигации. К любому свойственному человеку виду деятельности.
      Нумента в данный момент продвигает платформу Grok, например. Тут можно прочитать об отличии алгоритмов grok от приведенных в статье и схожести с скрытой марковской моделью.
      +1
      Хочу заострить внимание читателей на весьма интересной заметке и выразить благодарность автору. Это действительно весьма перспективная технология.

      Идея «разделяй и властвуй» используется в нейромоделировании уже очень давно. Сверточные нейросети, неокогнитроны, ассоциативные машины ME и HME и т.д. — все эти виды сетей показали впечатляющие результаты в свое время именно за счет разделения пространства признаков на подпространства. Такие сети используются если простой многослойный персептрон (классика) не справляется с поставленной задачей.

      Важно (!), что каждая из этих сетей в принципе может быть представлена в виде архитектуры HTM. Ключевым же отличием HTM является время (обработка последовательностей) что расширяет сферу применения кардинально.

      Простой пример: обычная сеть способна распознать картинку, HTM — видеоряд. Сфера применения HTM поэтому просто огромна, все что могут обычные сети умеет и HTM + способность обрабатывать информацию в динамике.

      В закладки непременно.
        0
        Жаль, что ява, я уже практически завершаю реализацию на C#…
          0
          В функции getSegmentActiveSynapses у вас такой код:

          > activeSynapses.add(new Synapse(idx[0], idx[1], initialPerm));

          т.е. как я понимаю вы создаете новые синапсы. В то время как в документе сказано:

          > Такие синапсы случайно выбираются из числа клеток, у которых learnState равно 1 в момент времени t.

          кажется тут ошибка
            0
            Интересный вопрос. Опишу, как это видится мне.
            Это не ошибка, это суть оптимизации.
            idx[0], idx[1] — индексы рандомной клетки с learnState = true в текущий момент времени.
            Синапсы добавляются по мере необходимости. Каждый дендритный сегмент имеет свой набор потенциальных синапсов, который может пересекаться с наборами других дендритных сегментов.
            На вопрос, почему он может пересекаться, приведу цитату о проксимальных сегментах:
            >Each column in a region is connected to a unique subset of the input bits (usually overlapping with other columns but never exactly the same subset of input bits) (Страница 21)
            Если же смотреть логически, то вы правы. Каждая клетка имеет лишь один аксон, следовательно и сформировать она может лишь один синапс.
            Что мы получим таким образом? Ровно то же, что имеем сейчас — каждый дендритный сегмент по-прежнему имеет возможность сформировать с каждым аксоном каждой клетки синапс и мы опять вернемся к механизму потенциальных синапсов. С той лишь разницей, что придется отслеживать одну вещь — если данный аксон уже сформировал рабочий синапс, то он не может сформировать его с другой клеткой. Подобного механизма, насколько я помню, в документе нет.
            0
            У нас с вами несколько отличаются реализации (чуть более, чем совсем :) ). Хотелось бы понять основные точки расхождения. Поэтому давайте начнем с более простых вещей.

            > idx[0], idx[1] — индексы рандомной клетки

            Что это за индексы? У меня получается несколько по другому: колонка может задана двумя числами (индексами) по X и по Y, но чтобы задать клетку — нужен еще один индекс — индекс положения клетки в колонке.

            Далее Ваши же индексы также продублированы в синапсе — что это за индексы?

            У меня в синапсе существуют совсем другие по смыслу индексы:
            /// X индекс подключения синапса к входным данным
            /// Y индекс подключения синапса к входным данным
            (Один тип дендритных сегментов клеток формирует синапсы с входными битами. Активность синапсов из такого сегмента линейно суммируется для определения прямой активации всей колонки.)
            Т.е. по сути это синапсы колонки, соединенные с входными данными случайным образом.

              0
              в классе Synapse два поля:
              >public Integer c;
              >public Integer i;
              Класс используется как для синапсов на проксимальных дендритах (которые ко входу подключены), так и на дистальных, с той разницей, что для проксимальных 'c' и 'i' это на самом деле ваши же X и Y, а для дистальных это c — номер колонки и i — номер клетки, как в пдфе. Неочевидное место, конечно.
              Таким образом, idx[0] и idx[1], так как берутся из списка learnState (аналогично определению в пдфе: learnState(c, i, t) — A boolean indicating whether cell i in column c is chosen as the cell to learn on, с разницей в написании: learnState.get(t).get(j).get(k), где t — время, j — колонка, k — клетка), представляют собой номер колонки и номер клетки соотетственно.
                0
                Ок,

                Уточню для для дистальных
                > c — номер колонки

                Т.е. если регион имеет 3х3 (=9) колонок — у вас номируется:

                123
                456
                789

                Так?

                в то время как у меня двумя индексами по X, Y (в документе это не так, но так удобнее потом работать)

                yx yx yx
                11 12 13
                21 22 23
                31 32 33

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

              > А если newSynapses равно true, тогда число синапсов, равное newSynapseCount — count(activeSynapses),
              добавляется к активным синапсам activeSynapses. Такие синапсы случайно выбираются из числа клеток, у которых learnState равно 1 в момент времени t.

              Что-то тут у меня не клеится. В начальном состоянии, еще нет даже дендритных сегментов, они только тут же и добавляются. Поэтому синапсов вообще еще нет, но их надо как-то «выбрать» из нуля.

              Вы это решили, так что у вас тут же они и создаются, а не выбираются. Но кажется, после начального состояния это будет не правильно (после создания синапсов, они начинают обучаться, а у вас будут создаваться все новые и новые — или я чего-то не понимаю в вашем коде?).
                0
                Вы все правильно понимаете. Только в некоторых моментах неточно.
                >В начальном состоянии, еще нет даже дендритных сегментов
                Сомнительно. Если у нас в начальном состоянии вообще не будет сегментов, то как минимум две начальные фазы темпорального группировщика бесполезны на первом временном шаге и вся система алгоритмов может загнуться. Хотя проще проэкспериментировать и посмотреть на результат.
                Насчет создания синапсов, вот отрывок с 43 страницы:

                In the temporal pooler, each segment can have its own (possibly large) list of potential synapses. In practice maintaining a long list for each segment is computationally expensive and memory intensive. Therefore in the
                temporal pooler, we randomly add active synapses to each segment during learning (controlled by the parameter newSynapseCount). This optimization has a similar effect to maintaining the full list of potential synapses, but the list per segment is far smaller while still maintaining the possibility of learning new temporal patterns.
                  0
                  > добавляем активные синапсы к каждому сегменту во время обучения

                  Так в том то и дело — добавляем, а не создаем новые! Причем добавляем активные (!), т.е. они уже существовали, и активировались.

                  Можно было бы подумать, что это просто слова и добавляем=создаем новые, но ниже есть утверждение

                  > Такие синапсы случайно выбираются из числа клеток, у которых learnState равно 1 в момент времени t.

                  У меня вообще есть подозрение, что реально физические синапсы — это только синапсы на проксимальных дендритах. А синапсы на дистальных дендридах — есть лишь ссылки на синапсы из проксимальных дендритов. Прямого утверждения в документе я такого не нашел, но это бы кое-что объясняло бы. А так не понятно как можно добавлять активные синапсы, которые еще не существуют.

                    0
                    >В начальном состоянии, еще нет даже дендритных сегментов
                    >> Сомнительно. Если у нас в начальном состоянии вообще не будет сегментов, то как минимум две начальные фазы темпорального группировщика бесполезны на первом временном шаге и вся система алгоритмов может загнуться.

                    Но также и есть. Как раз на первой фазе вызывается getSegmentActiveSynapses с s= -1, и комментарий

                    > Если же текущий прямой вход снизу не был предсказан, тогда все клетки становятся активными (строки 32-34) и кроме того, клетка, лучше всего соответствующая входным данным, выбирается для обучения (строки 36-41), причем ей добавляется новый (!!!) латеральный дендритный сегмент.

                    А раз он создается, на первой фазе — но инициализировать случайно нет смысла, он добавится там, где это будет нужно.
                      0
                      На первой фазе создается только структура segmentUpdate, фактический сегмент появляется только в конце третье фазы. Хотя, я не отрицаю, что можно попробовать не создавать начальные дистальные сегменты.
                        0
                        Чтобы понимать где чисто ваша реализация, а что исходит из документа:

                        При инициализации у вас есть такой код:

                        for(int k = 0; k < 2; k++) {
                                            dendriteSegments.get(c).get(i).add(new Segment());
                                            for(int s=0; s < newSynapseCount; s++) {
                                                Integer col = rnd.nextInt(xDimension*yDimension);
                                                Integer cell = rnd.nextInt(cellsPerColumn);
                                                dendriteSegments.get(c).get(i).get(k).synapses.add(new Synapse(col, cell,
                                                        connectedPerm + connectedPerm / 2.0 - (rnd.nextDouble()/10.0)));
                                            }
                        
                        


                        Т.е. вы создаете 2 сегмента с полным набором синапсов. Если инициализация синапсов колонок (при работе пространственного групировщика) описана явно, то для латеральных дендритных сегментов — это не описано, или я где-то просмотрел? И соответственно эта вот инициализация — есть сугубо ваше решение?
                          +1
                          Все верно, это отсебятина.
                          Для проверки вашего предположения я подставил условие k < 0, тем самым пропустив этот кусок кода — приложение действительно работает без этой части инициализации, а картина добавления новых сегментов практически идентична той, что получается с инициализацией. Можно эту часть кода удалить, спасибо.
                  0
                  Синапсы на проксимальных дендритах образуются между аксонами клеток-входов и дендритными сегментами колонок.
                  Синапсы на дистальных образуются между аксонами клеток внутри региона и дендритами других клеток внутри региона.
                  Еще кусок из того же абзаца той же страницы, прочтите её, там все изложено подробно и складно:
                  The implementation of potential synapses is different from the implementation in the spatial pooler. In the spatial pooler, the complete list of potential synapses is represented as an explicit list. In the temporal pooler, each egment can have its own (possibly large) list of potential synapses.
                  И насчет выбора:
                  These synapses are randomly chosen from the set of cells that have learnState output = 1 at time step t.
                  Ваш вариант больше подходит под глагол taken, да и в общем не имеет практического смысла. Весь этот механизм по добавлению/созданию новых синапсов направлен всего лишь на увеличение быстродействия, что видно из цитаты комментарием выше.
                  Суть действия в том, что при недостаточной степени латеральной (внутрирегиональной, межклеточной) активности сегментов клетки не входят в предсказательное состояние, ввиду чего им добавляются новые синапсы-латеральные связи. В документе, в описании второй фазы темпорального группировщика, можно посмотреть подробнее.
                    0
                    Видимо тут действительно какие трудности перевода, т.к. есть еще такая фраза

                    > Но только одна из этих клеток (которая лучше всего соответствует данному входу) включает свое состояние обучения learnState. И мы добавляем синапсы только клеткам с таким включенным параметром

                    что противоречит

                    > Такие синапсы случайно выбираются из числа клеток, у которых learnState равно 1 в момент времени t.

                    И похоже действительно нужно СОЗДАВАТЬ синапсы, но только не как у вас при инициализации, а только в getSegmentActiveSynapses.

                    И далее не так как у вас:

                    idx = learningCells.get(r.nextInt(learningCells.size()));
                    activeSynapses.add(new Synapse(idx[0], idx[1], initialPerm));

                    т.е. соединяя синапсы не с learningCells которых еще может не быть, а наоборот присоединяя видимо случайные (как у вас на инициализации ?) к тем клеткам которые в данный момент проставлена как learnState равно 1

                    вообщем, какой-то мутный момент…
                      0
                      У вас кстати, будет та же проблема, которую я сейчас пытаюсь обойти.

                      Вполне вероятно, что массив learningCells окажется нулевого размера и на строке

                      idx = learningCells.get(r.nextInt(learningCells.size()));

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

                    Вот для проксимальных дендритов (которые ко входу подключены) — можно сказать когда синапс активный, это у вас вычисляется в функции SOverlap(). Там правда косвенная оценка, но если не умножать boost, то overlap — это будет «Число действующих синапсов подключенных к активным входным битам»

                    Что в этом месте для дистальных синапсов? Я не нахожу функции, где генерируется активность активность этого синапса. Есть только оценка этого после в функции segmentActive например. Как я понимаю там идет подсчет «числа подключенных синапсов сегмента s, которые активны благодаря заданным состояниям в момент t» — но когда (где это просчитывается) они становятся активными?
                      0
                      >This routine returns true if the number of connected synapses on segment 's' that are active due to the given state at time 't' is greater than activationThreshold.
                      Т.е. функция возвращает true, если количество соединенных синапсов (permanence > connectedPerm), аткивных вследствие заданного состояния (active/learn state) больше параметра activationThreshold.
                      В реализации функции нужно еще добавить условие, чтобы было:
                      >if(list.get(syn.c).get(syn.i) && syn.permanence > connectedPerm) {
                        0
                        Что такое

                        > if(list.get(syn.c).get(syn.i)

                        не понимаю эту запись.

                        > активных вследствие заданного состояния (active/learn state)

                        — так в том то и дело — ГДЕ происходит само задание этого состояния? Прямо в этой же функции?
                          0
                          Строка кода из функции segmentActive.
                          >ГДЕ происходит само задание этого состояния
                          В перовой фазе темпорального группировщика.
                            0
                            > Строка кода из функции segmentActive.

                            Я извиняюсь, наверное я плохо понимаю код на яве — поясните что делает эта строка

                            > В перовой фазе темпорального группировщика.

                            Там такой код

                            for c in activeColumns(t)
                            2.
                            3. buPredicted = false
                            4. for i = 0 to cellsPerColumn — 1
                            5. if predictiveState(c, i, t-1) == true then
                            6. s = getActiveSegment(c, i, t-1, activeState)
                            7. if s.sequenceSegment == true then
                            8. buPredicted = true
                            9. activeState(c, i, t) = 1
                            10.
                            11. if buPredicted == false then
                            12. for i = 0 to cellsPerColumn — 1
                            13. activeState(c, i, t) = 1

                            Где? Тут я вижу установление активности клетки, а не синапса…
                              0
                              причем активность клетки устанавливается после вызова getActiveSegment. В то время как к моменту вызова getActiveSegment уже должны активироваться синапсы — но где?
                                0
                                В том месте getSegmentActiveSynapses и прочие связанные с активностью сегментов функции, как вы могли заметить, вызываются от предыдущего шага времени.
                                Активность синапса = подключенность синапса + активность клетки, на которую он указывает.
                                  0
                                  Другими словами синапс активен когда подключен к активной клетки? И if(list.get(syn.c).get(syn.i) — проверяет как раз это?

                                  похоже начинает доходить :) спасибо…

                    Only users with full accounts can post comments. Log in, please.