[Python] NumPy 배열 연산 마스터하기
NumPy는 Python의 과학 컴퓨팅 핵심 라이브러리로, 고성능 다차원 배열 연산을 제공한다.
이 글에서는 NumPy의 핵심 기능과 최적화 방법을 알아본다.
벡터화 연산 (Vectorization)
벡터화는 반복문을 사용하지 않고 배열 전체에 대해 연산을 수행하는 방법이다.
기본적인 반복문 vs 벡터화 연산
import numpy as np
import time
# 데이터 준비
size = 1000000
arr = np.random.rand(size)
# 반복문 방식
start = time.time()
result_loop = [x * 2 for x in arr]
print(f"반복문 시간: {time.time() - start}")
# 벡터화 연산
start = time.time()
result_vec = arr * 2
print(f"벡터화 시간: {time.time() - start}")
복잡한 연산의 벡터화
# 조건부 연산
arr = np.array([1, 2, 3, 4, 5])
condition = arr > 3
arr[condition] = 10 # 3보다 큰 값을 10으로 변경
# 수학 함수 적용
angles = np.array([0, 30, 45, 60, 90])
sin_values = np.sin(np.deg2rad(angles)) # 삼각함수 계산
브로드캐스팅 (Broadcasting)
브로드캐스팅은 shape가 다른 배열 간의 연산을 가능하게 하는 기능이다.
기본 브로드캐스팅
# 배열과 스칼라 연산
arr = np.array([[1, 2, 3],
[4, 5, 6]])
arr + 1 # 모든 원소에 1 더하기
# 다른 shape의 배열 연산
row = np.array([1, 2, 3])
col = np.array([[1],
[2]])
result = row + col # 브로드캐스팅으로 연산 수행
브로드캐스팅 규칙 예제
def check_broadcast_compatibility(shape1, shape2):
"""두 shape의 브로드캐스팅 가능 여부 확인"""
shape1 = list(reversed(shape1))
shape2 = list(reversed(shape2))
for a, b in zip_longest(shape1, shape2, fillvalue=1):
if not (a == 1 or b == 1 or a == b):
return False
return True
# 예제
shape1 = (3, 1, 4)
shape2 = (1, 5, 4)
print(check_broadcast_compatibility(shape1, shape2))
행렬 연산
행렬 연산은 선형 대수 계산의 기초이다.
기본 행렬 연산
# 행렬 곱셈
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
dot_product = np.dot(A, B) # 행렬 곱
matrix_product = A @ B # Python 3.5+ 연산자
# 전치 행렬
transposed = A.T
# 역행렬
inverse = np.linalg.inv(A)
고급 행렬 연산
# 특이값 분해(SVD)
U, s, Vh = np.linalg.svd(A)
# 고윳값과 고유벡터
eigenvalues, eigenvectors = np.linalg.eig(A)
# 선형 방정식 풀기
x = np.linalg.solve(A, B)
성능 최적화
NumPy 연산의 성능을 최적화하는 방법들을 알아보자.
1. 메모리 레이아웃 최적화
# 연속적인 메모리 사용
arr = np.ascontiguousarray(arr) # C-style 연속 메모리
arr = np.asfortranarray(arr) # Fortran-style 연속 메모리
# 뷰 vs 복사
view = arr.view() # 메모리 공유
copy = arr.copy() # 새로운 메모리 할당
2. 연산 최적화
# 불필요한 복사 피하기
def optimize_calculation(arr):
# 나쁜 예
temp = arr.copy()
temp += 1
# 좋은 예
arr += 1 # 제자리 연산
# 메모리 사전 할당
result = np.empty_like(arr) # 결과 배열 미리 생성
3. 병렬 처리
# NumPy의 다중 스레드 설정
import numpy as np
np.show_config() # BLAS/LAPACK 설정 확인
# OpenBLAS 스레드 수 조정
import os
os.environ['OPENBLAS_NUM_THREADS'] = '4'
성능 측정 예제
def benchmark_operations():
# 큰 배열 생성
size = (1000, 1000)
A = np.random.rand(*size)
B = np.random.rand(*size)
# 다양한 연산 성능 측정
operations = {
'Addition': lambda: A + B,
'Multiplication': lambda: A * B,
'Matrix Product': lambda: A @ B,
'Transpose': lambda: A.T,
}
for name, op in operations.items():
start = time.time()
_ = op()
print(f"{name}: {time.time() - start:.4f}초")
# 벤치마크 실행
benchmark_operations()
실전 최적화 팁
- dtype 최적화
- 필요한 정밀도만큼만 사용
- float64 대신 float32 사용 고려
- 메모리 관리
- 큰 배열은 del로 명시적 해제
- 메모리 매핑 활용 (대용량 데이터)
- 연산 순서 최적화
- 작은 배열부터 연산
- 중간 결과 재사용
- 프로파일링 활용
- cProfile 또는 line_profiler 사용
- 병목 지점 식별
'개발&프로그래밍' 카테고리의 다른 글
[Python] Matplotlib으로 데이터 시각화하기 (0) | 2024.11.19 |
---|---|
[Python] Pandas DataFrame (2) | 2024.11.19 |
[JAVA] ThreadLocal 제대로 사용하기 (0) | 2024.11.17 |
[JAVA] 효과적인 예외 처리 전략 (0) | 2024.11.16 |
[JAVA] Java의 String Pool과 문자열 최적화 (2) | 2024.11.15 |
댓글