메인
투자 노트

파이썬으로 빅데이터 다루기

@9/4/2023
오늘날 데이터과학에서 널리 사용되는 파이썬은 TensorFlow, Pillow, PyTorch등 유요한 라이브러리들을 가지고 있습니다. 각각의 라이브러리들은 자기만의 방식으로 매우 최적화된 컴퓨팅을 수행하기 때문에 저수준 언어 못지 않은 퍼포먼스를 보여줍니다.
이 글에서는 각종 라이브러리들을 활용해서 대규모 시공간 데이터를 다루는 방법을 예로 들어 설명합니다. 여기서 대규모 시공간 데이터라고 함은 수 억 단위의 길이를 가진 좌표 배열을 말합니다. 일반적으로 tiff, las, xyz, obj 등 다양한 포멧으로 파일에 저장되며 전용 소프트웨어를 통해 편집할 수 있습니다.
특정 도시를 스캔한 고품질 3차원 데이터가 있다고 해보죠, 도시 전체의 표면을 2cm간격을 가지는 수십억개의 점 데이터로 나타내는 파일입니다. 각 점은 (x, y, z)로 이루어진 공간 좌표입니다.
이해를 돕기 위한 사진
예를 들어 빌딩의 높이를 파악하기 위해 x가 6이고 y가 15인 위치의 z값을 구해야 한다고 가정해봅시다.
데이터를 Python 리스트로 변환하고 x가 6이고 y가 15인 인덱스를 탐색한 뒤, 해당 요소의 z값을 가져와야 합니다. 하지만 수십GB 파일을 읽는 과정에서 메모리가 초과될것이고, 읽었다고 해도 x,y좌표를 찾는데에 엄청나게 많은 시간이 걸립니다. 스캔한 도시에서 가장 높은 위치를 찾거나 가장 큰 면 구조물을 찾는 등 다양한 요구사항이 있을 수 있는데 이런 방식으로는 절대 해결할 수 없습니다.

행렬 연산: Numpy

Numpy를 사용하면 매우 빠른 속도로 행렬 연산을 수행할 수 있습니다.
import numpy as np arr = np.random.rand(2_000_000_000).astype('float32') # 16초
Python
복사
0~1 사이의 렌덤한 실수 20억개로 이루어진 배열을 생성합니다.
arr.max() # 0.1초 arr.min() # 0.1초 np.sum((arr >= 0.4) & (arr < 0.5)) # 2.3초
Python
복사
20억개의 숫자중 최대값과 최소값, 그리고 0.4~0.5 사이에 위치한 숫자들의 갯수를 구하는 코드입니다.
길이가 20억인 배열은 약 7GB의 메모리 차지하는 매우 큰 데이터입니다. 하지만 Numpy를 사용하면 이렇게 부하가 큰 연산도 순식간에 처리됩니다.
arr_2x = arr * 2 # 0.7초
Python
복사
20억개의 모든 숫자에 2를 곱해서 새로운 배열을 만듭니다.
Numpy는 Python에서 데이터를 다룰 때 근간이 되는 라이브러리라고 해도 과언이 아닙니다. 수많은 데이터과학 라이브러리들이 Numpy를 사용하거나 Numpy API를 제공하고 있습니다. (source)
정리하자면 Numpy는 Python에서 데이터 연산을 수행함에 있어서 반드시 필요한 도구이며 일반적인 라이브러리와는 다르게 Python 언어 자체에 대한 연산 최적화 익스텐션이라고 해도 될 정도로 남다른 퍼포먼스와 범용 API를 구현하고 있습니다.
Numpy를 사용해서 방대한 연산을 매우 빠르게 수행할 수 있다는건 매우 좋습니다. 하지만 대규모 배열 자체가 메모리에 올라와야 하기 때문에 머신의 메모리 크기를 초과하는 데이터는 다룰 수 없습니다.
20억개의 배열이 8GB이면 왠만해서 다 문제없는거 아니냐 생각할 수 있지만 실제로 데이터를 다룰때 하나의 배열 변수로 모든걸 해결하지 않습니다. 계산 과정중 여러번 복사되고 서로 참조하는 경우가 무조건 생기는데 그러면 얼마 못가서 메모리 초과로 프로세서가 죽어버립니다.
메모리 용량으로 인해 발생하는 한계는 위험합니다. CPU점유율 100%는 속도가 느려지지만 메모리 점유율 100%는 프로세서가 뻗어버립니다. 이런 문제를 해결하기 위해서는 대규모 데이터를 디스크에 파일로 저장하고 필요한 부분만 메모리로 불러와 연산하는 External memory algorithm을 구현해야 합니다.

분산처리: Dask & 디스크에 저장: Zarr

Dask in 8 Minutes: An Introduction
This video gives a general overview of the Dask project. What is Dask? Dask is a flexible library for parallel computing in Python. Dask is composed of two parts: 1. Dynamic task scheduling optimized for computation. This is similar to Airflow, Luigi, Celery, or Make, but optimized for interactive computational workloads. 2. “Big Data” collections like parallel arrays, DataFrames, and lists that extend common interfaces like NumPy, Pandas, or Python iterators to larger-than-memory or distributed environments. These parallel collections run on top of dynamic task schedulers. Dask emphasizes the following virtues: Familiar: Provides parallelized NumPy array and Pandas DataFrame objects Flexible: Provides a task scheduling interface for more custom workloads and integration with other projects. Native: Enables distributed computing in pure Python with access to the PyData stack. Fast: Operates with low overhead, low latency, and minimal serialization necessary for fast numerical algorithms Scales up: Runs resiliently on clusters with 1000s of cores Scales down: Trivial to set up and run on a laptop in a single process Responsive: Designed with interactive computing in mind, it provides rapid feedback and diagnostics to aid humans Share your feedback with us in the comments and let us know: - Did you find the video helpful? - Have you used Dask before? Learn more at dask.org KEY MOMENTS 00:00 - Intro 00:08 - What does Dask do? 01:08 - Dask Array 01:43 - Where is Dask used? 02:58 - Examples of application 05:46 - How Does Dask Work? 06:15 - Where is Dask run? 00:06:48 Dask Open Source Community
Dask 기본 설명
Dask Array in 3 Minutes: An Introduction
In this video, Matt Rocklin gives a brief introduction to Dask Arrays. Dask is a free and open-source library for parallel computing in Python. Dask is a community project maintained by developers and organizations. Dask Array implements a subset of the NumPy ndarray interface using blocked algorithms, cutting up the large array into many small arrays. This lets us compute on arrays larger than memory using all of our cores. We coordinate these blocked algorithms using Dask graphs. Dask Arrays coordinate many NumPy arrays (or “duck arrays” that are sufficiently NumPy-like in API such as CuPy or Sparse arrays) arranged into a grid. These arrays may live on disk or on other machines. Dask Array is used in fields like atmospheric and oceanographic science, large scale imaging, genomics, numerical algorithms for optimization or statistics, and more. By default, Dask Array uses the threaded scheduler in order to avoid data transfer costs, and because NumPy releases the GIL well. It is also quite effective on a cluster using the dask.distributed scheduler. Share your feedback with us in the comments and let us know: - Did you find the video helpful? - Have you used Dask before? Learn more at https://docs.dask.org/en/latest/array.html KEY MOMENTS 00:00:00 Intro 00:00:13 Dask Array Example 00:01:07 Larger Array Example 00:01:36 Using Dask array Like numpy 00:02:26 Data File Formats
Dask 배열 분산처리 튜토리얼
Dask는 배열을 필요한 부분만 메모리로 불러와 연산합니다. Zarr는 배열을 디스크에 효율적으로 저장합니다.
import zarr import dask.array as da arr = da.random.random(2_000_000_000).astype('float32')
Python
복사
0~1 사이의 렌덤한 실수 20억개로 이루어진 배열을 생성합니다.
store = zarr.open('arr.zarr') # 0초 da.to_zarr(arr, store) # 5초
Python
복사
zarr로 파일 저장소를 만들어서 배열을 저장합니다.
arr_ref = da.from_zarr('arr.zarr') # 0초 arr_ref.mean().compute() # 2.3초
Python
복사
저장된 배열을 dask로 불러온뒤 평균을 계산합니다.
전 과정에서 사용된 메모리는 약 70MB이며 사용된 디스크 용량(배열의 크기)은 7GB입니다.
만약 이런 대규모 공간데이터를 다루는 서버를 만들어야 한다면 데이터를 DB에 저장하지 말고 Zarr를 사용하여 파일로 저장해야 합니다. PostGIS등 공간데이터에 적절한 익스텐션을 사용하더라도 비용이 너무 많이 들기 때문에 DB를 사용할 수 없습니다. AWS를 예로 들면 RDS말고 S3, EFS등 다양한 선택지가 있습니다. 참고로 Dask는 S3를 포함한 다양한 원격 스토리지를 사용할 수 있습니다.
Zarr + Dask 조합을 통해 적은 시간동안 적은 메모리를 사용해서 연산을 수행할 수 있습니다. 하지만 이렇게 훌륭한 성능 최적화를 달성해도 피할 수 없는 문제점이 하나 존재합니다. 바로 대규모 시공간 데이터는 단순한 배열로 다루기 어렵다는 점입니다.
물론 여기서는 단순한 3차원 좌표 배열을 다루기 때문에 다차원 배열로 충분히 연산이 가능합니다. 하지만 좌표마다 색, 온도 등 다양한 레퍼런스를 가지고 있으며 시간축까지 추가된다면 어느새 7차원, 8차원 공간이 됩니다. 심지어 다른 데이터와 함께 다루기 위해 필요한 좌표계, 기준좌표등 별도의 메타데이터도 관리해야 합니다.

배열을 다차원 데이터셋으로 확장: Xarray

Xarray는 배열 개념을 좌표축을 가지는 공간으로 확장한 라이브러리입니다. Zarr + Dask와 호환되며 좌표계를 가지는 여러개의 축으로 공간 배열을 만들 수 있습니다.
import xarray as xr import dask.array as da # x, y, z 차원에 대한 랜덤 데이터 생성 x = da.linspace(0, 10, 11) y = da.linspace(0, 20, 21) z = da.linspace(0, 30, 31) data = np.random.rand(len(x), len(y), len(z)) # 11x21x31 크기의 랜덤 데이터 # DataArray 생성 data_array = xr.DataArray( data, # 3차원 배열 dims=['x', 'y', 'z'], # 좌표 축 이름 정의 coords=[x, y, z], # 각 축에 대한 좌표계 ) # 연산 예시: x가 0.5에 가장 근접한 좌표 찾기 data_array.sel(x=0.5, method='nearest') normal = data_array # 일반 카메라로 측정되었다고 가정 radar = data_array # 라이더 센서로 측정되었다고 가정 thermal = data_array # 열화상 카메라로 측정된거라고 가정 # Dataset 생성 data_set = xr.Dataset( # 각각의 측정데이터를 통합 { "일반 카메라": normal, "라이더 센서": radar, "열화상 카메라": thermal, } )
Python
복사
Xarray 사용 예시
Xarray에는 DataArray와 DataSet이라는 두가지 객체가 있습니다. DataArray는 좌표계를 가지는 다차원 배열이며 동일한 좌표계로 구성된 여러 DataArray을 하나로 합친게 DataSet입니다. 예를 들어서 A도시를 10개의 서로다른 장비로 스캔해서 10개의 데이터가 생성되었다고 했을 때 생성된 데이터를 10개의 DataArray로 만들어서 하나의 DataSet으로 합칠 수 있습니다.
Xarray 객체는 Numpy 혹은 Dask 배열을 통해 만들 수 있으며 Zarr로 저장할 수 있습니다. 또한 Zarr로 저장된 배열을 Dask로 읽어올 수 있습니다.
data_set.to_zarr("arr.zarr") # zarr로 저장할 수 있고 data_set_ref = xr.open_zarr("arr.zarr") # zarr로부터 dask배열로 읽어올 수 있습니다.
Python
복사
DataArray, DataSet 모두 attrs라는 속성을 가지고 있습니다. 이것은 딕셔너리이며 각종 메타데이터를 담는데 사용할 수 있습니다.
data_array.attrs["수집 날짜"] = "2024.10.23" data_set.attrs["통합 날짜"] = "2025.05.12"
Python
복사

GPU 연산으로 더 빠르게: Cupy

컴퓨팅 환경에 따라 사용할 수 없는 경우가 많기 때문에 짧게 짚고 넘어갑니다. NVIDIA GPU를 사용해서 극강의 속도를 내는 Numpy API를 제공합니다.
import numpy as npimport cupy as cp & np.어쩌고cp.어쩌고
Cupy API는 NumPy와 호환되므로 위와같이 적용해주면 기존 Numpy코드에 GPU 연산이 적용됩니다.

결론

파이썬으로 빅데이터를 다루는 경우 속도와 메모리 문제를 해결해야 합니다. NumPy 생태계를 활용하면 속도 문제를 해결할 수 있으며 Dask와 Zarr를 통해 메모리 문제를 해결할 수 있습니다. 그리고 NumPy 생태계에서 데이터 도메인에 적합한 라이브러리를 선택하면 복잡한 원시 데이터를 편리하게 다룰 수 있습니다.
아래는 NumPy 생태계와 통합되어 다양한 데이터 구조를 제공하는 라이브러리들입니다. 이 글에서는 Xarray를 예시로 들었지만 아래 표를 참고하여 다루는 데이터 형식에 적절한 라이브러리를 선택하는게 좋습니다.
시공간 데이터: 기상, 해양학, 지리정보 등
천문학 데이터: 별, 행성, 은하 등 천문 객체
그래프 데이터: 통신망, 교통망, 전력망 등
테이블 데이터: RDBMS 테이블, Pandas DataFrame 등
뇌영상 데이터: 뇌전도(EEG), 뇌자기파(MEG) 등
의료영상 데이터: CT, MRI, PET 등

단순 퍼포먼스 최적화: Numba

외부 라이브러리가 제공하는 기능에는 한계가 있습니다. Numba를 사용하면 Python 함수를 기계어로 컴파일할 수 있으며, 컴파일된 함수는 Numpy 이상의 속도를 제공합니다.

사용 방식

Numba가 지원하는 범위 안에서 함수를 작성하고 해당 함수를 Numba로 데코레이팅합니다.

단점

Numba가 지원하는 범위가 한정적입니다. 주로 Python 기본 문법만 사용 가능하며 외부 패키지는 사용할 수 없습니다. 예외적으로 Numpy등 과학 패키지의 일부 함수는 사용 가능합니다.