python

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

  • вторник, 8 февраля 2022 г. в 00:40:49
https://habr.com/ru/post/650185/
  • Python
  • Data Engineering


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!