벡터화(Vectorization)는 반복문(예: for 루프)을 사용하지 않고, 배열 단위로 연산을 수행하여 데이터를 효율적으로 처리하는 기법이다. 이는 대량의 데이터를 한 번에 처리하여 연산 속도를 크게 향상시킨다.
vector()
또는 apply()
계열 함수를 통해 유사한 벡터화 처리 가능.data = [i * 2 for i in range(1000000)] # 느림
import numpy as np
data = np.arange(1000000) * 2 # 빠름
병렬 처리(Parallel Processing)는 작업을 여러 프로세서(CPU 코어, GPU 등)에 분할하여 동시에 처리하는 기법이다. 이를 통해 전체 처리 시간을 단축할 수 있다.
multiprocessing
, joblib
)을 사용해 병렬 처리를 구현한다.항목 | 벡터화 | 병렬 처리 |
---|---|---|
처리 방식 | 단일 코어에서 배열 연산 최적화 | 여러 CPU 코어 또는 GPU에 작업 분배 |
성능 | 반복문 대비 수십~수백 배 빠름 | 코어 수에 비례한 성능 향상 |
적합 데이터 | 메모리 내 처리 가능한 데이터 | 대규모 데이터 또는 독립적 작업 |
복잡성 | 간단(코드 변경 최소화) | 복잡(작업 분배 및 결과 취합 필요) |
오버헤드 | 낮음(SIMD 활용) | 높음(프로세스 생성, 직렬화) |
NumPy는 배열 연산에 최적화된 라이브러리로, 벡터화의 핵심 도구이다. C로 작성된 백엔드를 통해 SIMD를 활용하며, 반복문을 대체해 속도를 높인다.
예제: 배열의 값을 2배로 만드는 연산.
import numpy as np
import time
# 비효율적인 반복문
start_time = time.time()
data = [i * 2 for i in range(1000000)]
print(f"Loop time: {time.time() - start_time:.4f} seconds")
# 효율적인 벡터화
start_time = time.time()
data = np.arange(1000000) * 2
print(f"Vectorized time: {time.time() - start_time:.4f} seconds")
출력 예시:
Loop time: 0.3200 seconds
Vectorized time: 0.0020 seconds
설명: 반복문은 각 요소를 개별적으로 처리하므로 CPU 캐시 활용이 비효율적이고, 실행 시간이 길다. NumPy는 배열 전체를 한 번에 처리하여 SIMD와 캐시 최적화를 활용한다.
Pandas는 DataFrame을 사용해 대규모 데이터의 벡터화 연산을 지원한다. NumPy 기반으로 작동하며, 데이터프레임의 컬럼 단위로 연산을 최적화한다.
예제: 컬럼 값을 2배로 변환.
import pandas as pd
import time
# 데이터 생성
df = pd.DataFrame({'value': range(1000000)})
# 비효율적인 apply
start_time = time.time()
df['doubled'] = df['value'].apply(lambda x: x * 2)
print(f"Apply time: {time.time() - start_time:.4f} seconds")
# 효율적인 벡터화
start_time = time.time()
df['doubled'] = df['value'] * 2
print(f"Vectorized time: {time.time() - start_time:.4f} seconds")
출력 예시:
Apply time: 0.4500 seconds
Vectorized time: 0.0050 seconds
설명: apply
는 각 행을 개별적으로 처리하므로 느리다. 반면, 컬럼 단위 연산은 벡터화를 통해 빠르고, 메모리 접근과 CPU 연산을 최적화한다.
Joblib은 간단한 병렬 처리를 지원하며, Scikit-learn에서 내부적으로 사용된다. 멀티프로세싱을 통해 작업을 분할하여 CPU 코어를 활용한다.
k
개 프로세스의 작업이 모두 완료될 때까지 다음 작업으로 넘어가지 않는다. 예: 20개 작업을 4개 프로세스로 처리할 때, 1~4번 작업이 모두 완료되어야 5~8번 작업이 시작된다. 이로 인해 작업 실행 시간 차이로 비효율이 발생할 수 있다.from joblib import Parallel, delayed
import time
def process_task(i):
time.sleep(1) # 작업 시뮬레이션
return i * 2
# 병렬 처리
start_time = time.time()
results = Parallel(n_jobs=4)(delayed(process_task)(i) for i in range(8))
print(f"Results: {results}")
print(f"Time taken: {time.time() - start_time:.4f} seconds")
출력 예시:
Results: [0, 2, 4, 6, 8, 10, 12, 14]
Time taken: 2.0500 seconds
설명: 8개 작업을 4개 프로세스로 나누어 처리. 각 프로세스가 2개 작업을 병렬로 수행하며, 작업 시간이 균일하지 않으면 대기 시간이 발생할 수 있다.
from pyspark.sql import SparkSession
from pyspark.sql.functions import pandas_udf
import pandas as pd
# 스파크 세션 초기화
spark = SparkSession.builder.appName("VectorizedExample").getOrCreate()
# 데이터프레임 생성
df = spark.createDataFrame([(i,) for i in range(1000000)], ["value"])
# Pandas UDF 정의
@pandas_udf("long")
def multiply_by_two(series: pd.Series) -> pd.Series:
return series * 2
# 벡터화 연산 적용
result = df.select(multiply_by_two("value").alias("doubled"))
result.show(5)
spark.stop()
설명: Spark는 데이터를 파티션으로 나누어 각 노드에서 처리하며, Pandas UDF를 통해 파티션 내에서 벡터화 연산을 수행한다. 이는 단일 머신의 메모리 제약을 극복하며, 벡터화의 장점을 유지한다.
apply
피하기: df['col'].apply(lambda x: x * 2)
대신 df['col'] * 2
를 사용해 벡터화 연산 적용.np.add
, np.multiply
같은 함수로 연산 최적화. 이는 SIMD를 최대한 활용한다.backend='loky'
로 안정적인 멀티프로세싱 구현. n_jobs
를 CPU 코어 수에 맞게 설정.multiprocessing.Array
를 사용해 큰 데이터의 메모리 복사를 줄인다.joblib
의 max_nbytes
로 직렬화 메모리 사용량을 조정하여 GC 부담 감소.#pragma omp parallel for
로 반복문을 병렬화. 예: 행렬 연산을 여러 스레드로 분배.@simd
지시어로 벡터화 연산 최적화. 예: 배열 연산에서 SIMD 자동 적용.