Разделяй и властвуй или как спасти оперативку

от автора

Divide and Rule
Divide and Rule

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

Мне была поставлена задача спрогнозировать изменение физической величины (неизвестного мне параметра на предприятии) на 240 шагов вперёд. Как вы уже поняли – это стандартная задача предсказания временного ряда.

Исходные данные представляли из себя поминутное изменение параметра:

Таблица изменений параметра
Таблица изменений параметра

Однако, по классике, с исходными данными как правило приходится пошаманить, чтобы привести их к виду пригодному для дальнейшей обработки и формированию обучающей выборки. В моём случае с удивительной периодичностью встречались пропуски, т.е. после 00.00.01 преспокойно могло идти 00.00.04, что немножко рушило целостность временного ряда.

Самым простым и действенным было прибегнуть к чудесной силе интерполяции из библиотеки Pandas:

Data = Data.Value.resample('s').interpolate()

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

После перезагрузки ПК я понял, что кусочек в 55 млн строк (учитывая, что пропуски будут заполнены ВСЕМИ недостающими секундами) — это тихий ужас для моей бедной RAM.

Вот тут-то и приходит на помощь принцип: Разделяй и властвуй.

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

В связи с этим мой алгоритм обработки выглядел следующим образом:
<> Берём кусок массива заданной нами длины

batch = data.iloc[index:index+xLen]

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

batch = batch.Value.resample('s'                   ).interpolate().resample('4T'                          ).interpolate().resample('1T').interpolate()

<> Затем производим конкатенацию массивов на каждой итерации

batch_sum = pd.concat([batch_sum, batch])

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

В первую очередь конвертируем строковые данные в данные типа Timestamp для возможности работы с ними как с временными данными.

Время перевода в Timestamp  - 100 секунд
Время перевода в Timestamp — 100 секунд

Далее применяем описанный выше алгоритм, который я обернул в функцию, где первый аргумент data — это сам массив, второй xLen – размер одного батча (кол-во строк) и третий step – шаг сдвига.

######################################################## # Функция интерполяции всего массива значений по частям. ########################################################  def create_samples(data, xLen, step):             data_len = data.iloc[:].shape[0]     index = 0                 count = 0               batch_sum = s_samp   # берём шаблон для конкатинации Serias объектов          while (index + xLen <= data_len):          # Идём по всей длине массива значений         t1 = time.time()                  batch = data.iloc[index:index+xLen]     # "Откусываем" пример длинной xLen                batch = batch.Value.resample('s'                         ).interpolate().resample('4T'                                ).interpolate().resample('1T').interpolate()                         batch = batch.dropna()               now_shape = batch.shape[0]               count += now_shape                  batch_sum = pd.concat([batch_sum, batch])                  index += step                          # Смещаеммся вперёд на step                  if index % step*10 == 0:             print(index, time.time()-t1)                  print(count, 'общее кол-во значений')          return batch_sum
Время посекундной интерполяции всего массива данных - 36 секунд
Время посекундной интерполяции всего массива данных — 36 секунд

Как приятно видеть, что при правильном подходе задача решается всего за 36 секунд, а твой ПК не впадает в кому 🙂

Подведём итоги:

  • При решении задачи нужно оценить её размер, сложность и ресурсозатратность

  • Если задача трудная и объёмная, то следует разбить её на более мелкие части по принципу «Разделяй и властвуй»

  • Подобрать такие гиперпараметры, как размер и количество подзадач, учитывая специфику задачи

Всем большое спасибо и успехов в ML!


ссылка на оригинал статьи https://habr.com/ru/post/650185/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *