Оптимизация моего большого кода данных с небольшим объемом оперативной памяти - PullRequest
0 голосов
/ 30 августа 2018

У меня сохранен файл размером 120 ГБ (в двоичном виде через pickle), который содержит около 50 000 (600x600) двумерных массивов. Мне нужно сложить все эти массивы, используя медиану. Самый простой способ сделать это - просто прочитать весь файл как список массивов и использовать np.median(arrays, axis=0). Однако у меня не так много оперативной памяти, так что это не очень хороший вариант.

Итак, я попытался складывать их попиксельно, так как я фокусируюсь на одной позиции пикселя (i, j) за раз, затем читаю в каждом массиве по одному, добавляя значение в данной позиции в список , Как только все значения для определенной позиции во всех массивах сохранены, я использую np.median, а затем просто должен сохранить это значение в списке, который в итоге будет иметь медианы каждой позиции пикселя. В конце я могу просто изменить это до 600x600, и все будет готово. Код для этого ниже.

import pickle
import time
import numpy as np

filename = 'images.dat' #contains my 50,000 2D numpy arrays

def stack_by_pixel(i, j):
    pixels_at_position = []
    with open(filename, 'rb') as f:
        while True:
            try:
                # Gather pixels at a given position
                array = pickle.load(f)
                pixels_at_position.append(array[i][j])
            except EOFError:
                break
    # Stacking at position (median)
    stacked_at_position = np.median(np.array(pixels_at_position))
    return stacked_at_position

# Form whole stacked image
stacked = []
for i in range(600):
    for j in range(600):
        t1 = time.time()
        stacked.append(stack_by_pixel(i, j))
        t2 = time.time()
        print('Done with element %d, %d: %f seconds' % (i, j, (t2-t1)))

stacked_image = np.reshape(stacked, (600,600))

После просмотра некоторых распечаток времени я понимаю, что это крайне неэффективно. Каждое завершение позиции (i, j) занимает около 150 секунд или около того, что неудивительно, поскольку она читает около 50 000 массивов один за другим. И учитывая, что в моих больших массивах есть 360 000 (i, j) позиций, по прогнозам, это займет 22 месяца! Очевидно, что это невозможно. Но я вроде в растерянности, потому что не хватает оперативной памяти для чтения всего файла. Или, может быть, я мог бы сохранить все позиции пикселей (отдельный список для каждой позиции) для массивов, поскольку он открывает их один за другим, но не сохранял бы 360 000 списков (длиной около 50 000 элементов) в Python, которые бы использовали много оперативной памяти также?

Приветствуются любые предложения о том, как я могу сделать это значительно быстрее, не используя много оперативной памяти. Спасибо!

Ответы [ 2 ]

0 голосов
/ 30 августа 2018

Примечание: я использую Python 2.x, переносить его на 3.x не должно быть сложно.


Моя идея проста - дискового пространства достаточно, поэтому давайте сделаем некоторую предварительную обработку и превратим этот большой файл pickle во что-то, что легче обрабатывать небольшими порциями.

Подготовка

Чтобы проверить это, я написал небольшой скрипт, который генерирует файл рассола, похожий на ваш. Я предположил, что ваши входные изображения имеют оттенки серого и имеют глубину 8 бит, и сгенерировали 10000 случайных изображений, используя numpy.random.randint.

Этот скрипт будет служить эталоном, с которым мы можем сравнивать этапы предварительной обработки и обработки.

import numpy as np
import pickle
import time

IMAGE_WIDTH = 600
IMAGE_HEIGHT = 600
FILE_COUNT = 10000

t1 = time.time()

with open('data/raw_data.pickle', 'wb') as f:
    for i in range(FILE_COUNT):
        data = np.random.randint(256, size=IMAGE_WIDTH*IMAGE_HEIGHT, dtype=np.uint8)
        data = data.reshape(IMAGE_HEIGHT, IMAGE_WIDTH)
        pickle.dump(data, f)
        print i,

t2 = time.time()
print '\nDone in %0.3f seconds' % (t2 - t1)

В тестовом запуске этот скрипт завершился за 372 секунды, создав файл ~ 10 ГБ.

Препроцессирование

Давайте разделим входные изображения построчно - у нас будет 600 файлов, где файл N содержит строку N из каждого входного изображения. Мы можем хранить данные строки в двоичном формате, используя numpy.ndarray.tofile (и позже загружать эти файлы, используя numpy.fromfile).

import numpy as np
import pickle
import time

# Increase open file limit
# See /7318117/pochemu-python-imeet-ogranichenie-na-kolichestvo-deskriptorov-failov
import win32file
win32file._setmaxstdio(1024)

IMAGE_WIDTH = 600
IMAGE_HEIGHT = 600
FILE_COUNT = 10000

t1 = time.time()

outfiles = []
for i in range(IMAGE_HEIGHT):
    outfilename = 'data/row_%03d.dat' % i
    outfiles.append(open(outfilename, 'wb'))


with open('data/raw_data.pickle', 'rb') as f:
    for i in range(FILE_COUNT):
        data = pickle.load(f)
        for j in range(IMAGE_HEIGHT):
            data[j].tofile(outfiles[j])
        print i,

for i in range(IMAGE_HEIGHT):
    outfiles[i].close()

t2 = time.time()
print '\nDone in %0.3f seconds' % (t2 - t1)

В тестовом прогоне этот скрипт завершился за 134 секунды, создав 600 файлов по 6 миллионов байт каждый. Он использовал ~ 30 МБ или RAM.

Обработка

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

Наконец, используйте numpy.vstack, чтобы собрать медианное изображение.

import numpy as np
import time

IMAGE_WIDTH = 600
IMAGE_HEIGHT = 600

t1 = time.time()

result_rows = []

for i in range(IMAGE_HEIGHT):
    outfilename = 'data/row_%03d.dat' % i
    data = np.fromfile(outfilename, dtype=np.uint8).reshape(-1, IMAGE_WIDTH)
    median_row = np.median(data, axis=0)
    result_rows.append(median_row)
    print i,

result = np.vstack(result_rows)
print result

t2 = time.time()
print '\nDone in %0.3f seconds' % (t2 - t1)

В тестовом прогоне этот скрипт завершился за 74 секунды. Вы могли бы даже распараллелить это довольно легко, но, похоже, оно того не стоит. Скрипт использовал ~ 40 МБ ОЗУ.


Учитывая, что оба этих сценария линейны, используемое время также должно масштабироваться линейно. Для 50000 изображений это около 11 минут для предварительной обработки и 6 минут для окончательной обработки. Это на i7-4930K @ 3,4 ГГц, специально использует 32-битный Python.

0 голосов
/ 30 августа 2018

Это идеальный вариант использования для отображенных в память массивов numpy . Массивы, отображаемые в памяти, позволяют обрабатывать файл .npy на диске так, как если бы он был загружен в память как пустой массив, фактически не загружая его. Это так просто, как

arr = np.load('filename', mmap_mode='r')

По большей части вы можете рассматривать это как любой другой массив. Элементы массива загружаются в память только по мере необходимости. К сожалению, некоторые быстрые эксперименты показывают, что median не очень хорошо справляется с отображенными в память массивами *, по-прежнему кажется, что значительная часть данных сразу загружается в память. Так что median(arr, 0) может не работать.

Однако вы по-прежнему можете циклически проходить по каждому индексу и вычислять медиану, не сталкиваясь с проблемами с памятью.

[[np.median([arr[k][i][j] for k in range(50000)]) for i in range(600)] for j in range(600)]

, где 50 000 отражает общее количество массивов.

Без дополнительных затрат на удаление каждого файла только для извлечения одного пикселя время выполнения должно быть намного быстрее (примерно в 360000 раз).

Конечно, это оставляет проблему создания файла .npy, содержащего все данные. Файл может быть создан следующим образом:

arr = np.lib.format.open_memmap(
    'filename',              # File to store in
    mode='w+',               # Specify to create the file and write to it
    dtype=float32,           # Change this to your data's type
    shape=(50000, 600, 600)  # Shape of resulting array
)

Затем загрузите данные, как и прежде, и сохраните их в массив (который просто записывает их на диск за кулисами).

idx = 0
with open(filename, 'rb') as f:
    while True:
        try:
            arr[idx] = pickle.load(f)
            idx += 1
        except EOFError:
            break

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

* Я только что протестировал его на 7 ГБ файле, взяв медиану из 1500 выборок из 5 000 000 элементов, а использование памяти составило около 7 ГБ, предполагая, что весь массив мог быть загружен в память. Впрочем, попробовать сначала не повредит. Если у кого-то еще есть опыт работы со медианой в массивах memmapped, не стесняйтесь комментировать.

** Если верить незнакомцам в интернете.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...