прогнозирование энергопотребления с помощью нейронной сети «ближе к биологии»

Я действительно недавно заговорил об Иерархической временной памяти. Он все еще довольно новый и далек от отраслевого стандарта для глубокого обучения, но с его результатами сложно спорить. Я также очень верю в имитацию формы для получения функции, поэтому я сразу погрузился в библиотеку Python Numenta’s NuPIC HTM, чтобы попытаться показать некоторые результаты, несмотря на все мои обожания.

Плохие новости: это написано в версии 2.7. Однако сообщество HTM с открытым исходным кодом составило свою собственную вилку, в которой они перекодировали привязки (на основе C ++) для запуска на Python 3. Мне удалось установить на Mac с Mojave 10.14 с помощью параметра PyPI в командной строке ( после запуска pip install cmake) без особых хлопот.

Существует несколько различных синтаксисов и соглашений об именах (задокументированных в указанной выше вилке), но это та же технология, что и в официальном пакете NuPIC, только немного более детальная. HTM.Core выглядит как Pytorch по сравнению с Keras от NuPIC.

Посещение тренажерного зала

Настоящая сила HTM заключается в распознавании образов, поэтому я исследовал hotgym.py пример HTM.Core:
Используя энергопотребление тренажерного зала и временные метки, он тренирует простую модель для прогнозирования следующего вероятного значения мощности и обнаружения аномальной активности. Это элегантный способ показать, как HTM работает с шаблонами, и существует огромное количество отраслевых приложений для обнаружения временных рядов и аномалий.

Я столкнулся с несколькими синтаксическими ошибками и ошибками времени выполнения, и мне пришлось перекодировать некоторые части, поэтому давайте рассмотрим самые интересные моменты. Весь код находится на моем Github как alternate_hotgym.ipynb , если вы хотите проверить всю записную книжку Jupyter.

Процесс выглядит примерно так:

  1. Получить данные из .CSV
  2. Создать кодировщик
  3. Создать пространственный пулер
  4. Создать временную память
  5. Цикл обучения, прогнозирование на каждой итерации
  6. Проверить результаты, построить графики

Начинается с CSV, заканчивается графиками. Вот откуда вы узнали, что это наука о данных.

Двоичные кодировки

Отличительной чертой моделей HTM является то, что они работают только с двоичными входами. В частности, разреженные распределенные представления: битовый вектор (обычно более 2000 знаков) из единиц и нулей. Их можно представить в виде квадрата для облегчения сравнения.

Это возможно с помощью любой реализации Encoder: объекта, предназначенного для преобразования типа данных (int, string, image и т. Д.) В SDR. Хороший кодировщик гарантирует, что «похожие» входные данные создают SDR, которые также похожи, за счет перекрывающихся битов.

Вот пример из Cortical.io: Три SDR, слева направо, представляющие mice, rat и mammals:

Просто взглянув на общие активированные биты (единицы), вы увидите, что mice и rat немного ближе друг к другу, чем mammals, но все же есть некоторое совпадение битов у млекопитающих. Модель Retina HTM от Cortical - это круто, но мы поговорим об их семантическом встраивании в другой раз.

Итак, у нас есть power_consumption (float) и timestamp (DateTime). Как мы это закодируем?

dateEncoder = DateEncoder(
    timeOfDay = (30,1) # DateTime is a composite variable
    weekend = 21 # how many bits to allocate to each part
scalarEncoderParams = RDSE_Parameters() # encoding a continuous var
scalarEncoderParams.size       = 700 # SDR size
scalarEncoderParams.sparsity   = 0.02 # 2% sparsity magic number
scalarEncoderParams.resolution = 0.88 
scalarEncoder = RDSE(scalarEncoderParams) # 'random distributed scalar encoder'
encodingWidth = (dateEncoder.size + scalarEncoder.size)
enc_info = Metrics( [encodingWidth], 999999999) # performance metrics storage obj

И вот настроены объекты Encoder; мы объединим их позже в цикле обучения.

Окунуться в бассейн

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

Левая сторона - это SDR выходного сигнала кодировщика (единицы отмечены синим), а правая - выходное значение пространственного пула. Мышь находится над одной ячейкой pool_cell и отображает кружки над каждой ячейкой input_cell, подключенной к этой ячейке pool_cell.
По мере того, как вы загружаете данные SDR в пул, он укрепляет связи этих зеленых кружков; у клеток, которые «совпадают», укрепляются синапсы, и обратное применяется к «промахам».

Обратите внимание, как написано «Столбцы пространственного пула». Каждая ячейка на самом деле представляет собой столбец из N ячеек (мы будем использовать 5); вы смотрите сверху на самую верхнюю ячейку в каждом столбце. Позже это войдет в игру с временной памятью.

Инициализация пула:

sp = SpatialPooler(
    inputDimensions            = (encodingWidth,),
    columnDimensions           = (spParams["columnCount"],), # 1638
    potentialPct               = spParams["potentialPct"], # 0.85
    potentialRadius            = encodingWidth,
    globalInhibition           = True,
    localAreaDensity           = spParams["localAreaDensity"], # .04
    synPermInactiveDec         = spParams["synPermInactiveDec"], # .006
    synPermActiveInc           = spParams["synPermActiveInc"], # 0.04
    synPermConnected           = spParams["synPermConnected"], # 0.13
    boostStrength              = spParams["boostStrength"], # 3
    wrapAround                 = True
)
sp_info = Metrics(sp.getColumnDimensions(), 999999999)

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

Прогулка по переулку памяти

А теперь самое интересное: временная память, которая также работает на SDR. Я объяснил это в моей последней статье, но снова считаю, что Видео школы HTM неоценимо для визуализации и понимания того, как обучается алгоритм.

Помните, как каждая ячейка пула на самом деле была столбцом? Теперь смотрим на эти столбцы сбоку.

Если вы скармливаете TM последовательность A B C D, она «разбивает» различные столбцы, активируя все ячейки в столбце. Четыре буквы имеют разные узоры (мы видим лишь небольшой фрагмент тех же SDR, визуализированных ранее). Этот пример также подает X B C Y.

Каждая ячейка случайным образом связана со многими другими, и если B следует за A, связи между ячейками cells_involved_in_B и cells_involved_in_A усиливаются. Эти синаптические связи позволяют ТМ «запоминать» шаблоны.

Также важно количество ячеек в столбце. Обратите внимание, как B’ (B_from_A) использует разные ячейки в одном столбце как B’’ (B_from_X). Если бы в столбцах был только один бит, не было бы возможности различить два «прошлых контекста».

Таким образом, количество ячеек в столбце - это, по сути, «сколько входов в далеком прошлом может запомнить TM».

tm = TemporalMemory(
    columnDimensions          = (spParams["columnCount"],),
    cellsPerColumn            = tmParams["cellsPerColumn"], # 13
    activationThreshold       = tmParams["activationThreshold"], # 17
    initialPermanence         = tmParams["initialPerm"], # 0.21
    connectedPermanence       = spParams["synPermConnected"],
    minThreshold              = tmParams["minThreshold"], # 19
    maxNewSynapseCount        = tmParams["newSynapseCount"], # 32
    permanenceIncrement       = tmParams["permanenceInc"], # 0.1
    permanenceDecrement       = tmParams["permanenceDec"], # 0.1
    predictedSegmentDecrement = 0.0,
    maxSegmentsPerCell        = tmParams["maxSegmentsPerCell"], # 128
    maxSynapsesPerSegment     = tmParams["maxSynapsesPerSegment"] # 64
)
tm_info = Metrics( [tm.numberOfCells()], 999999999)

Время тренироваться

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

Модель HTM не только неконтролируема, она обучает и предсказывает по ходу - нет необходимости возиться с пакетной обработкой, как обычные нейронные сети.
Каждая пара power_consumption и timestamp кодируется, объединяется в пространственный пул, запоминается во времени и используется для предсказаний на лету, поэтому мы сможем увидеть, как их предсказания улучшаются по мере того, как они учатся на каждом SDR.

Мы используем созданные ранее объекты sp и tm:

predictor = Predictor(steps=[1,5], alpha=0.1) # continuous output predictor
predictor_resolution = 1
inputs = [] # create inp/out lists
anomaly = []
anomalyProb = []
predictions = {1: [], 5:[]}
predictor.reset() # reset the predictor
for count, record in enumerate(records): # iterate through data
    dateString = datetime.datetime.strptime(record[0], "%m/%d/%y %H:%M") # unstring timestamp
    consumption = float(record[1]) # unstring power value
    inputs.append(consumption) # add power to inputs
    
    
    # use encoder: create SDRs for each input value
    dateBits = dateEncoder.encode(dateString)
    consumptionBits = scalarEncoder.encode(consumption)
    
    # concatenate these encoded_SDRs into a larger one for pooling
    encoding = SDR(encodingWidth).concatenate([consumptionBits, dateBits])
    enc_info.addData(encoding) # enc_info is our metrics to keep track of how the encoder fares
    
    
    # create SDR to represent active columns. it'll be populated by .compute()
    # notably, this activeColumns SDR has same dimensions as spatial pooler
    activeColumns = SDR(sp.getColumnDimensions())
    
    
    # throw the input into the spatial pool and hope it swims
    sp.compute(encoding, True, activeColumns) # we're training, so learn=True
    tm_info.addData(tm.getActiveCells().flatten())

    
    # pass pooled SDR through temporal memory
    tm.compute(activeColumns, learn=True)
    
    # make prediction based on current input & memory-context
    pdf = predictor.infer( tm.getActiveCells() )
        for n in (1,5):
            if pdf[n]:
                predictions[n].append( np.argmax( pdf[n] ) *     predictor_resolution )
            else:
                predictions[n].append(float('nan'))
    
    anomalyLikelihood = anomaly_history.anomalyProbability( consumption, tm.anomaly )
    anomaly.append(tm.anomaly)
    anomalyProb.append(anomalyLikelihood)
    
    # reinforce output connections
    predictor.learn(count, tm.getActiveCells(), int(consumption/predictor_resolution))

Последний кусок головоломки - это объект predictor, который по сути представляет собой обычную нейронную сеть, которая получает выходной SDR TM и выводит желаемый прогноз - в нашем случае, энергопотребление. Он обучается через приращение / уменьшение обратного распространения, как и большинство NN.

Предсказатель - это «голова» модели: на каждой итерации мы спрашиваем модель: «Как вы думаете, какой уровень энергопотребления будет следующим?» и запишите прогноз, чтобы позже сравнить его с реальным значением.

Полученные результаты

У нас есть удобные метрики для проверки работоспособности нашей модели HTM. Вообще говоря, вы хотите следить за connected synapses & overlap.

Мы вычисляем среднеквадратическую ошибку для точности прогнозирования выходных данных для двух «версий» модели:
1) глядя на один шаг назад и 2) глядя на пять шагов назад.

{1: 0.07548016042172133, 5: 0.0010324285729320193} # RMSE
power_consumption:
    min: 10
    max: 90.9
    mean: 31.3

Для сравнения, единицы энергопотребления варьируются от 10 до 90, так что это RMSE выглядит неплохо.
Также приятно видеть, что 5-ступенчатая модель имеет значительно более высокую точность, что подтверждает идею о том, что распознавание образов приводит к более точным прогнозам.

Но помните: если вы не строите график, на самом деле это не наука о данных:

По оси X отложены «временные интервалы», что составляет ~ 4400 часовых показаний в одном и том же спортзале - около шести месяцев. Взгляните на зеленую 5-временную линию на верхнем графике: она начинается с некоторых диких просчетов, но в конечном итоге начинает предсказывать синхронно с фактическим следующим значением (красный).

Модель хорошо понимает суточные и недельные колебания (поэтому мы кодировали как “hourOfDay”, так и “weekend” как часть Encoder SDR). Поскольку мы кодируем дату и, следовательно, месяц, эта модель также должна учитывать сезонные изменения энергопотребления.

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

Хорошая работа, модель мозга

Неплохо для исправления существующего шаблона. Опять же, это в основном от милых ребят, создавших HTM.Core - я только что привел в порядок и прокомментировал.
Есть масса других приложений HTM; это действительно зависит от того, как вы настраиваете кодировщик, но теоретически все может быть превращено в SDR. Если у вас есть мясорубка, из чего угодно можно сделать колбасу.

Я возился с HTM-классификатором числового рукописного ввода MNIST, который обеспечивает точность ~ 95% (хотя в наши дни большинство моделей хорошо работают с MNIST).
Я полагаю, системы обработки изображений HTM преуспеют в распознавании объектов видеопотока, что является сложной задачей: 5-ступенчатая память может смотреть на 5 кадров до этого кадра. Если модель видит взмахи крыльев, вероятно, это птица и т. Д.

Если все это звучит интересно, то это потому, что это так. Самый эффективный способ начать изучать технологию HTM - это Numenta’s HTM School, которая мне показалась интуитивно понятной и довольно приятной.

Узнайте больше на официальных форумах.