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

Управление памятью в Python - задача не из простых, она требует хорошего понимания объектов и структур данных Python. В отличие от C / C ++, пользователи не могут контролировать управление памятью. Его берет на себя сам Python. Однако, имея некоторое представление о том, как работает Python и о модулях поддержки памяти, мы можем каким-то образом пролить свет на то, как контролировать эту проблему.

Сколько памяти выделяется?

Есть несколько способов получить размер объекта в Python. Вы можете использовать sys.getsizeof(), чтобы получить точный размер объекта, objgraph.show_refs(), чтобы визуализировать структуру объекта, или psutil.Process().memory_info().rss , чтобы получить всю память, выделенную в данный момент.

Еще один вариант - tracemalloc. Он включен в стандартную библиотеку Python и обеспечивает трассировку выделения памяти на уровне блоков, статистику общего поведения памяти программы.

Наиболее часто используемый файл - это объект arr, который занимает 2 блока памяти общим размером 2637 МБ. Остальные объекты минимальны.

Другой важный метод - оценить, сколько памяти требуется для запуска процесса. Об этом можно догадаться, отслеживая пиковое использование памяти процессом. Чтобы измерить пиковую память, вы можете использовать приведенный ниже код в конце процесса.

### For Linux (in KiB) and MacOS (in bytes)
from resource import getrusage, RUSAGE_SELF
print(getrusage(RUSAGE_SELF).ru_maxrss)
### For Windows
import psutil
print(psutil.Process().memory_info().peak_wset)

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

1. Используйте Pytorch DataLoader

Обучение большого набора данных является узким местом для вашей памяти, и вы никогда не сможете обучить полную модель, поскольку весь набор данных никогда не умещается в вашей памяти одновременно, особенно для неструктурированных данных, таких как изображение, текст, голос, ... Однако, с Pytorch DataLoader вам удастся настроить различные мини-пакеты для всего набора данных, и каждый из них будет непрерывно загружаться в вашу модель (количество образцов зависит от объема вашей памяти). Вы можете увидеть здесь руководство по использованию Pytorch DataLoader.

Однако, если вы хотите обучить модель машинного обучения на табличных данных без использования глубокого обучения (следовательно, без использования Pytorch) или у вас нет доступа к базе данных и вам нужно работать исключительно с памятью, что будет выбором для оптимизация памяти?

2. Оптимизированный тип данных

Понимание того, как данные хранятся и обрабатываются, а также использование оптимального типа данных для задач, сэкономит вам огромное пространство в памяти и время вычислений. В Numpy существует несколько типов, включая логическое (bool), целое число (int), целое число без знака (uint), float, сложное, datetime64, timedelta64, object_ и т. Д.

### Check numpy integer
>>> import numpy as np
>>> ii16 = np.iinfo(np.int16)
>>> ii16
iinfo(min=-32768, max=32767, dtype=int16)
### Access min value
>>> ii16.min
-32768

Я сужаю их до uint, int и float, поскольку они наиболее распространены при обучении моделей, обрабатывающих данные в Python. В зависимости от различных потребностей и целей использование достаточного количества типов данных становится жизненно важным ноу-хау. Чтобы проверить минимальные и максимальные значения типа, вы можете использовать функцию numpy.iinfo() и numpy.finfo() для числа с плавающей запятой.

Ниже приводится сводная информация по каждому типу.

Размер файла CSV удваивается, если тип данных преобразован в numpy.float64, который является типом по умолчанию для numpy.array, по сравнению с numpy.float32. Следовательно, float32 - один из оптимальных для использования (тип данных Pytorch также float32).

Поскольку тип данных по умолчанию numpy.float() - float64, а numpy.int() - int64, не забудьте определить dtype при создании массива numpy, чтобы сэкономить огромное количество места в памяти.

При работе с DataFrame будет еще один обычный тип - «объект». Преобразование из объекта в категорию для функции, имеющей различные повторы, ускорит время вычислений.

Ниже приведен пример функции для оптимизации типа данных pd.DataFrame для скаляров и строк.

Еще один способ легко и эффективно уменьшить pd.DataFrame объем памяти - это импортировать данные с определенными столбцами с использованием usercols параметров в pd.read_csv()

3. Избегайте использования глобальных переменных, вместо этого используйте локальные объекты

Python быстрее извлекает локальную переменную, чем глобальную. Более того, объявление слишком большого количества переменных как глобальных приводит к проблеме нехватки памяти, поскольку они остаются в памяти до завершения выполнения программы, а локальные переменные удаляются, как только функция завершается, и освобождает пространство памяти, которое они занимают. Подробнее читайте в разделе Набор реальных навыков, которым должны овладеть специалисты по данным.



4. Используйте ключевое слово yield

Python yield возвращает объект-генератор, который преобразует данное выражение в функцию-генератор. Чтобы получить значения объекта, его необходимо повторить, чтобы прочитать значения, заданные для yield. Чтобы прочитать значения генератора, вы можете использовать list (), for loop или next ().

>>> def say_hello():
>>>    yield "HELLO!"
>>> SENTENCE = say_hello()
>>> print(next(SENTENCE))
HELLO!

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

>>> def say_hello():
>>>    yield "HELLO!"
>>> SENTENCE = say_hello()
>>> print(next(SENTENCE))
HELLO!
>>> print("calling the generator again: ", list(SENTENCE))
calling the generator again: []

Поскольку значение не возвращается, если объект-генератор не повторяется, память не используется, когда функция Yield определена, в то время как вызов Return в функции приводит к распределению в памяти.

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

>>> import sys
>>> my_generator_list = (i*2 for i in range(100000))
>>> print(f"My generator is {sys.getsizeof(my_generator_list)} bytes")
My generator is 128 bytes
>>> timeit(my_generator_list)
10000000 loops, best of 5: 32 ns per loop
  
>>> my_list = [i*2 for i in range(1000000)]
>>> print(f"My list is {sys.getsizeof(my_list)} bytes")
My list is 824472 bytes
>>> timeit(my_list)
10000000 loops, best of 5: 34.5 ns per loop

Глядя на приведенный выше код, понимание списка в 6441 раз тяжелее, чем генератор, и работает медленнее, чем другой генератор.

5. Встроенные методы оптимизации Python

Используйте встроенные функции Python для повышения производительности кода, список функций.

Используйте __slots__ при определении класса

Атрибуты объектов класса Python хранятся в виде словаря. Таким образом, определение тысяч объектов аналогично размещению тысяч словарей в области памяти. И добавление __slots__ (что снижает потери пространства и ускоряет программу, выделяя место для фиксированного количества атрибутов.)

Что касается использования памяти, учитывая, что в объекте класса больше нет __dict__, объем памяти заметно уменьшается с (64 + 16 + 120) = 200 до 56 байт.

Используйте join () вместо «+» для объединения строки

Поскольку строки неизменяемы, каждый раз, когда вы добавляете элемент к строке с помощью оператора «+», новая строка будет выделяться в пространстве памяти. Чем длиннее строка, тем больше потребляется памяти, тем менее эффективным становится код. Использование join() может повысить скорость ›на 30% по сравнению с оператором« + ».

Есть и другие способы повысить скорость и сэкономить память, подробности см. здесь.

itertools

Или сгладьте список с помощью itertools.chain ()

### Concatenate string using '+' operation
def add_string_with_plus(iters):
    s = ""
    for i in range(iters):
        s += "abc"
    assert len(s) == 3*iters
    
### Concatenate strings using join() function
def add_string_with_join(iters):
    l = []
    for i in range(iters):
        l.append("abc")
    s = "".join(l)
    assert len(s) == 3*iters
    
### Compare speed
>>> timeit(add_string_with_plus(10000))
100 loops, best of 5: 3.74 ms per loop
>>> timeit(add_string_with_join(10000))
100 loops, best of 5: 2.3 ms per loop

Ознакомьтесь с документацией itertools, чтобы узнать о других методах. Рекомендую изучить:

  • itertools.accumulate (iterable, func): накапливать через итерацию. func может быть operator.func или функциями Python по умолчанию, такими как max, min…
  • itertools.compress (iterable, selectors): фильтрует итерацию с другим (другой объект можно рассматривать как условие)
  • itertools.filterfalse (предикат, итерируемый): отфильтровать и отбросить значения, удовлетворяющие предикату. Это полезно и быстро для фильтрации объекта списка.
  • itertools.repeat (object [, times]): повторить значение объекта N раз. Однако я предпочитаю использовать умножение списка ['hi']*1000 , чтобы повторять «привет» 1000 раз, чем использовать itertools.repeat('hi', 1000) (12,2 мкс на цикл против 162 мкс на цикл соответственно)
  • itertools.zip_longest (* iterables, fillvalue = None): заархивируйте несколько итераций в кортежи и заполните значение None значением, указанным в fillvalue.

6. Накладные расходы на импорт выписки

Оператор import может быть выполнен из любого места. Однако выполнение вне функции будет выполняться намного быстрее, чем внутренняя, даже если пакет объявлен как глобальная переменная (doit2), но, в свою очередь, занимает больше места в памяти, чем другой.

7. Фрагмент данных

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

pandas позволяет сделать это с помощью параметров chunksize или iterator в pandas.read_csv() и pandas.read_sql(). sklearn также поддерживает обучение небольшими порциями с помощью partial_fit()method для большинства моделей.

На вынос 📖

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

  • Во-первых, выясните, какой процесс / переменная является основной причиной, приводящей к проблеме переполнения памяти.
  • Во-вторых, примените соответствующие методы оптимизации памяти для этого объекта, оцените новый объем памяти и проверьте, решает ли новый проблему проблему.
  • Если нет, попробуйте оптимизировать связанные процессы (например, уменьшить потребление памяти всего процесса, чтобы сэкономить больше места для основного объекта).
  • Попробуйте воспользоваться приведенными выше советами, и, если проблема не исчезнет, ​​подумайте о создании процесса с помощью операции фрагмента / пакетной обработки с поддержкой внешней службы базы данных.