파이톤의 다처리로 과학 계산을 가속화하다

Christian Wiediger의 Unsplash 사진
본 강좌에서 우리는 실제 예에서 multiprocessing를 어떻게 사용하여 과학 계산을 가속화하는지 볼 것이다.구체적으로 Broad Institute에 게시된 공중형광현미경 이미지MCF7 Cell Painting dataset에서 모든 원자핵의 위치를 탐지합니다.
이 자습서를 완료하면 다음 작업을 수행할 수 있습니다.
  • 형광현미경 영상에서 세포핵을 검출
  • 사용multiprocessing.Pool 빠른 이미지 처리
  • 쓰기 시 복제본 사용(Unix만 해당) 시리얼화된 오버헤드 감소
  • 병렬 처리 작업
  • 에 진행률 표시줄 추가

    데이터 세트 다운로드


    Note: The imaging data is nearly 1 GB for just one plate. You will need a few GB of space to complete this tutorial.


    먼저 셀 Paint 데이터 세트here의 첫 번째 보드 이미지 데이터를 images/라는 폴더에 다운로드하여 추출합니다.TIFF 그림이 가득 담긴 images/Week1_22123/ 폴더를 가져와야 합니다.그림의 이름은 다음과 같습니다.
    # Template
    {week}_{plate}_{well}_{field}_{channel}{id}.tif
    
    # Example
    Week1_150607_B02_s4_w1EB868A72-19FC-46BB-BDFA-66B8966A817C.tif
    

    모든 DAPI 이미지 찾기


    DAPI는 각 이미지에 세포핵을 염색하는 DNA와 결합한 형광 염료다.일반적으로 DAPI 신호는 현미경의 첫 번째 통로에서 포착된다.우리는 먼저 이미지 이름을 분석하고 첫 번째 채널(w1에서 이미지를 찾아야 한다. 그러면 DAPI 이미지에서만 세포핵을 검출할 수 있다.
    from glob import glob
    
    paths = glob('images/Week1_22123/Week1_*_w1*.tif')
    paths.sort()
    print(len(paths))  # => 240
    
    여기서, 우리는 glob 와 어댑터가 있는 모드를 사용하여 모든 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()
    

    우리는 이 그림들이 16개의 무기호 정수로 구성된 (10241280) 수조를 볼 수 있다.이 특정한 이미지의 픽셀 강도는 176에서 10016까지 다르다.

    찌꺼기법으로 원자핵을 탐지하다


    Scikit image는 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 수조를 받아들이고 centers라고 불리는 원자핵(x, y) 좌표 수조를 되돌려준다.그것은 우선 img 응용gaussian을 통해 모호하게 sigma = 4 진열을 매끄럽게 한다.preserve_range = True 매개 변수 차단skimage은 입력한 것과 같은 강도 범위의 매끄러운 이미지g를 되돌려줍니다.그리고 우리는 peak_local_max 함수를 사용하여 부드러운 이미지의 모든 부분 최대치를 측정합니다.우리는 두 핵센터가 실제와 부합되지 않도록 min_distance = 4를 설치하고 이미지의 어두운 구역에서 가짜 양성이 검출되지 않도록 threshold_abs = 1000를 설치했다.

    Note: We will use this detect_nuclei function in all subsequent experiments.


    방법 1 - 입출력 이해 목록 사용


    이제 원자핵 좌표를 검출하는 함수가 생겼습니다. 간단한 목록으로 이 함수를 모든 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에서 우리는 이미지 경로 목록을 가져오고 목록을 사용하여 모든 이미지를 불러오고 세포핵을 검출합니다.Jupyter의 %timeitmagic 명령을 사용하면 이 방법의 평균 실행 시간이 약 18초인 것을 알 수 있다.

    방법2--다중처리.애아랑 같이 수영해요.


    처리Pool를 통해 속도를 높일 수 있는지 살펴보자.이를 위해, 우리는 detect_nucleiio.imread 을 함께 포장하는 함수를 정의해야 한다. 그러면 우리는 map 이미지 경로 목록에서 이 함수를 실행할 수 있다.
    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() == 8가 달린 기계에서 운행하는 것이다.현재 같은 임무의 평균 집행 시간은 약 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 - 다중 처리.메모리 풀


    완전하게 보기 위해서 메모리에 있는 모든 이미지에 multiprocessing.Pool부터 map까지 우리의 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)
    
    잠깐만, 뭐라고?왜 이것은 multiprocessing를 사용하여 디스크에서 그림을 읽는 것보다 느립니까?multiprocessing 모듈의 문서를 자세히 읽으면 Pool 직원에게 전달된 데이터가 pickle를 통해 서열화되어야 한다는 것을 알 수 있습니다.이 서열화 절차는 약간의 계산 비용을 발생시킬 것이다.이것은 방법 2에 대해 우리는 경로 문자열을 처리해야 하지만, 이 예에서 우리는 전체 이미지를 처리해야 한다는 것을 의미한다.

    방법 5 - 다중 처리.시리얼화된 수정이 있는 풀


    다행히도 multiprocessing를 사용할 때 이런 서열화 비용을 피할 수 있는 몇 가지 방법이 있다.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의 속도보다 조금 빠르다.이 함수는 전역적으로 정의된 global 수조를 사용한다는 것을 명시하기 위해 images 문장을 사용합니다.이것은 엄격한 요구가 아니지만, 나는 _process_image_memory_fix 함수가 사용 가능한 images 수조에 달려 있다는 것을 지적하는 것이 도움이 된다는 것을 발견했다.그리고 모든 인덱스 map 를 실행하고 모든 프로세스가 images 그룹에 인덱스를 통해 필요한 이미지에 접근할 수 있도록 합니다.이런 방법은 그림 자체가 아니라 픽셀 정수만 만들 수 있다.

    결실


    다섯 가지 방법의 평균 집행 시간을 비교해 보자.

    전반적으로 multiprocessing는 이 핵 검측 임무의 평균 집행 시간을 크게 감소시켰다.흥미로운 것은 메모리에 있는 이미지를 다중 처리하는 간단한 방법이 실제로는 평균 실행 시간을 증가시켰다는 것이다.쓰기 시 복사 행위를 이용하여 우리는 전체 이미지를 산세척할 때 발생하는 대량의 서열화 비용을 없앨 수 있다.
    더 많은 결과를 보여주기 위해서, 이미지마다 검출된 세포핵수에 따라 서열을 정하는 DAPI 이미지 16장의 무작위 샘플을 보여 드리겠습니다.

    간단한 세포핵 검출 전략이 효과적이며 각 시야에서 관찰된 세포밀도 조직인 DAPI 이미지를 기반으로 할 수 있는 것으로 보인다.

    결론



    영화 속의 우주공
    나는 정말 이 문장이 도움이 되기를 바란다. 그러므로 당신이 그것을 좋아한다면 나에게 알려 주십시오.여기에 소개된 multiprocessing 기술은 내가 우리의 SCOUT paper를 위해 이미지를 처리할 때 터무니없는 속도에 이르도록 도와주었다.아마도 미래에 우리는 이 예를 다시 복습할 수 있을 것이다. 그러나 GPU를 통해 속도를 가속화하면 최종적으로 가소로운 속도를 실현할 수 있을 것이다.

    장점: 다처리 진도표를 사용합니다.수영장.


    위의 multiprocessing 예시에서 우리는 좋은 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 Collection[Ljosa et al., Nature Methods, 2012]에서 사용할 수 있습니다.

    소스 가용성


    본문의 모든 원본 자료는 제 블로그 GitHub repo에서 찾을 수 있습니다here.

    좋은 웹페이지 즐겨찾기