Рубрики
Без рубрики

Ускорение научных вычислений с помощью многопроцессы в Python

Фото Кристиана Видигера в этом учебном пособии, мы рассмотрим, как мы можем ускорить ученый … Теги с питоном, изображениями, многопроцестрой, обработкой изображений.

Фото Кристиана Видигера на Unsplash

В этом уроке мы рассмотрим, как мы можем ускорить научные вычисления, используя Многоподобная в реальном примере. В частности, мы обнаружим местоположение всех ядер в изображениях флуоресцентной микроскопии от общественности Набор данных по живописи MCF7 выпущен Брод Институтом. После завершения этого учебника вы узнаете, как сделать следующее:

  • Обнаружение ядер на изображениях флуоресцентной микроскопии
  • Ускорить обработку изображений с помощью многопроцессорная. Бассейн
  • Уменьшить накладные расходы сериализации с помощью копии на записи (только Unix)
  • Добавьте панель прогресса в параллельные задачи обработки

Загрузите набор данных

Примечание. Данные визуализации составляют почти 1 ГБ только для одной пластины. Вам понадобится несколько ГБ пространства, чтобы завершить этот урок.

Во -первых, загрузите данные изображения для первой таблички набора данных по живописи ячейки здесь в папку под названием изображения/ и извлечь это. Вы должны получить папку под названием Изображения/неделя1_22123/ наполнены изображениями TIFF. Изображения названы в соответствии со следующей конвенцией:

# Template
{week}_{plate}_{well}_{field}_{channel}{id}.tif

# Example
Week1_150607_B02_s4_w1EB868A72-19FC-46BB-BDFA-66B8966A817C.tif

Найдите все изображения DAPI

DAPI – это флуоресцентный краситель, который связывается с ДНК и окрашивает ядра в каждом изображении. Как правило, сигнал DAPI будет захвачен в первом канале микроскопа. Сначала нам нужно проанализировать имена изображений, чтобы найти изображения с первого канала ( W1 ), чтобы мы могли обнаружить ядра только из изображений DAPI.

from glob import glob

paths = glob('images/Week1_22123/Week1_*_w1*.tif')
paths.sort()
print(len(paths))  # => 240

Здесь мы использовали глобус и шаблон с подстановочными знаками, чтобы найти пути ко всем изображениям DAPI. Видимо, есть 240 изображений DAPI, которые нам нужно обрабатывать! Мы можем загрузить первое изображение, чтобы увидеть, с чем мы работаем.

from skimage import io
import matplotlib.pyplot as plt

img = io.imread(paths[0])
print(img.shape, img.dtype, img.min(), img.max())
# => (1024, 1280) uint16 176 10016

plt.figure(figsize=(6, 6))
plt.imshow(img, cmap='gray', vmax=4000)
plt.axis('off')
plt.show()

Мы видим, что изображения являются (1024, 1280) массивами 16-битных целых чисел без знака. Это конкретное изображение имеет интенсивности пикселей в диапазоне от 176 до 10 016.

Использование обезживания для обнаружения ядер

Scikit-Image-это пакет Python для обработки изображений, которую мы можем использовать для обнаружения ядер в этих изображениях DAPI. Простой и эффективный подход состоит в том, чтобы сгладить изображение, чтобы уменьшить шум, а затем найти все локальные максимумы, которые превышают заданный порог интенсивности. Давайте напишем функцию, чтобы сделать это на нашем примере изображения.

from skimage.filters import gaussian
from skimage.feature import peak_local_max

def detect_nuclei(img, sigma=4, min_distance=6, threshold_abs=1000):
    g = gaussian(img, sigma, preserve_range=True)
    return peak_local_max(g, min_distance, threshold_abs)

centers = detect_nuclei(img)
print(centers.shape)  # => (214, 2)

plt.figure(figsize=(6, 6))
plt.imshow(img, cmap='gray', vmax=4000)
plt.plot(centers[:, 1], centers[:, 0], 'r.')
plt.axis('off')
plt.show()

Теперь мы где -то получаем! Detect_nuclei Функция принимает img массив и возвращает массив ядер (x, y) -Кординаты, называемые Центры Анкет Сначала он сглаживает IMG массив, применив Гауссовый размытие с сигма . reverve_range Аргумент предотвращает Skimage от нормализации изображения, чтобы мы вернули сглаженное изображение g в том же диапазоне интенсивности, что и вход. Затем мы используем PEAK_LOCAL_MAX Функция для обнаружения всех локальных максимумов в нашем сглаженном изображении. Мы устанавливаем min_distance чтобы не допустить, чтобы какие -либо два ядра не были нереалистично близко друг к другу и threshold_abs Чтобы избежать обнаружения ложных срабатываний в темных областях изображения.

Примечание: мы будем использовать это Detect_nuclei функция во всех последующих экспериментах.

Метод 1 – Понимание списка с io

Теперь, когда у нас есть функция для обнаружения координат ядер, давайте применим эту функцию ко всем нашим изображениям DAPI, используя простое понимание списка.

from tqdm.notebook import tqdm

def process_images1(paths):
    return [detect_nuclei(io.imread(p)) for p in paths]

meth1_times = %timeit -n 4 -r 1 -o centers = process_images1(tqdm(paths))
# => 18 s ± 0 ns per loop (mean ± std. dev. of 1 run, 4 loops each)

В Process_images1 , мы заключаем список путей изображения и используем понимание списка для загрузки каждого изображения и обнаружения ядер. Используя %timeit Волшебная команда в Юпитере, мы видим, что этот подход имеет среднее время исполнения ~ 18 секунд.

Метод 2 – Многопроцессия. Бассейн с io

Посмотрим, сможем ли мы ускорить ситуацию с обработкой Бассейн Анкет Для этого нам нужно определить функцию для завершения Detect_nuclei и io.imread вместе, чтобы мы могли карта Это функционирует над списком путей изображения.

import multiprocessing as mp

def _process_image(path):
    return detect_nuclei(io.imread(path))

def process_images2(paths):
    with mp.Pool() as pool:
        return pool.map(_process_image, paths)

meth2_times = %timeit -n 4 -r 1 -o centers = process_images2(paths)
# => 5.54 s ± 0 ns per loop (mean ± std. dev. of 1 run, 4 loops each)

Намного лучше. Это было запущено на машине с CPU_COUNT () Анкет Теперь та же задача имеет среднее время выполнения ~ 5,5 секунды (> 3 раза ускорение).

Метод 3 – Понимание списка из памяти

Что произойдет, если мы прочитаем все изображения в память перед обнаружением ядер? Предположительно, этот подход будет быстрее, потому что нам не пришлось бы читать данные изображения с диска во время %timeit командование Давай попробуем.

import numpy as np

images = np.asarray([io.imread(p) for p in tqdm(paths)])

def process_images3(images):
    return [detect_nuclei(img) for img in images]

meth3_times = %timeit -n 4 -r 1 -o centers = process_images3(tqdm(images))
# => 17.7 s ± 0 ns per loop (mean ± std. dev. of 1 run, 4 loops each)

Теперь понимание списка немного быстрее, но только примерно на 0,3 секунды. Это говорит о том, что чтение данных изображения не является ограничивающим скоростью шаг в методе 1, а скорее Detect_nuclei вычисления.

Метод 4 – Многопроцессия. Пул от памяти

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

def process_images4(images):
    with mp.Pool() as pool:
        return pool.map(detect_nuclei, images)

meth4_times = %timeit -n 4 -r 1 -o centers = process_images4(images)
# => 6.23 s ± 0 ns per loop (mean ± std. dev. of 1 run, 4 loops each)

Чего ждать? Почему это медленнее чем читать изображения с диска с Многоподобная ? Если вы прочитаете документацию для Многоподобная Модуль тщательно, вы узнаете, что данные переданы работникам Бассейн Должен быть сериализован через рассол Анкет Этот этап сериализации создает некоторые вычислительные накладные расходы. Это означает, что для метода 2 мы были накаливания строк пути, но в этом случае мы марировали целые изображения.

Метод 5 – Многопроцессия. Бассейн с сериализацией исправить

К счастью, есть несколько способов избежать этой сериализации накладных расходов при использовании Многоподобная Анкет В системах Mac и Linux мы можем воспользоваться тем, как операционная система обрабатывает вилки процесса, чтобы эффективно обрабатывать большие массивы в памяти (извините Windows 🤷‍♂️). Системы на основе UNIX используют копия на записи поведение для раздвоенных процессов. Сложная копия на записи означает, что раздвоенный процесс будет копировать данные только при попытке изменить общую виртуальную память. Все это происходит за кулисами, и копия на записи иногда называют неявный обмен Анкет

def _process_image_memory_fix(i):
    global images
    return detect_nuclei(images[i])

def process_images5(n):
    with mp.Pool() as pool:
        return pool.map(_process_image_memory_fix, range(n))

meth5_times = %timeit -n 4 -r 1 -o centers = process_images5(len(paths))
# => 5.31 s ± 0 ns per loop (mean ± std. dev. of 1 run, 4 loops each)

Хороший! Сейчас это немного быстрее, чем метод 2 (как мы могли изначально ожидали). Обратите внимание, что мы используем Глобальный Заявление здесь, чтобы явно заявить, что эта функция использует картинки массив определен во всем мире. Хотя это не требуется строго, я обнаружил, что было бы полезно указать, что _process_image_memory_fix Функция зависит от некоторых изображения массив доступен. Мы тогда карта По всем индексам и позволяйте каждому процессу получить доступ к ему нужен изображение, индексируя в картинки множество. Этот подход будет только раскачивать целые числа вместо самих изображений.

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

Давайте сравним среднее время выполнения всех 5 методов.

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

Чтобы показать больше наших результатов, давайте покажем случайную выборку из 16 изображений DAPI, отсортированных по количеству обнаруженных ядер в каждом изображении.

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

Вывод

От Spaceballs фильм

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

Бонус: использование стержней прогресса с многопрофильной. Бассейн

В вышеперечисленном Многоподобная Примеры, у нас нет хорошего TQDM индикатор. Мы можем получить один, если используем Pool.imap Вместо этого, что полезно для продолжительных вычислений.

def _process_image(path):
    return detect_nuclei(io.imread(path))

def process_images_progress(paths):
    with mp.Pool() as pool:
        return list(tqdm(pool.imap(_process_image, paths), total=len(paths)))

centers = process_images_progress(paths)

использованная литература

Мы использовали набор изображений BBBC021V1 [ Caie et al., Molecular Cancer Therapeutics, 2010 ], доступно в коллекции Broad BioImage Benchmark [ Ljosa et al., Природа Методы, 2012 ]

Доступность источника

Все исходные материалы для этой статьи доступны Здесь В моем блоге Github Repo.

Оригинал: “https://dev.to/jmswaney/speeding-up-scientific-computing-with-multiprocessing-in-python-3a43”