브로드캐스팅 — 다차원 배열 연산 규칙
이 토픽을 마치면
NumPy 브로드캐스팅이 왜 존재하는지 이해하고, 세 가지 규칙을 적용해서 연산 가능 여부를 판단할 수 있으며, 브로드캐스팅을 활용한 효율적 코드를 작성할 수 있습니다.
왜 브로드캐스팅이 필요한가
import numpy as np
# 모든 원소에 10을 더하고 싶다arr = np.array([1, 2, 3, 4, 5])result = arr + 10print(result) # [11 12 13 14 15]arr은 5개 원소, 10은 스칼라(1개). 크기가 다른데 덧셈이 됩니다. NumPy가 10을 [10, 10, 10, 10, 10]으로 자동 확장했기 때문입니다. 이 자동 확장이 브로드캐스팅입니다.
브로드캐스팅이 없으면:
# 매번 이렇게 써야 함result = arr + np.full(5, 10) # 비효율적같은 모양의 연산 — 기본
크기가 같으면 원소별(element-wise) 연산입니다.
a = np.array([1, 2, 3])b = np.array([10, 20, 30])
print(a + b) # [11 22 33]print(a * b) # [10 40 90]2차원도 동일합니다.
A = np.array([[1, 2], [3, 4]])B = np.array([[10, 20], [30, 40]])
print(A + B)# [[11 22]# [33 44]]브로드캐스팅 규칙 — 3가지
크기가 다를 때, NumPy는 세 가지 규칙을 순서대로 적용합니다.
규칙 1: 차원 수가 다르면, 작은 쪽에 1을 앞에 추가
a = np.array([[1, 2, 3], # shape: (2, 3) [4, 5, 6]])b = np.array([10, 20, 30]) # shape: (3,)
# 규칙 1: b의 shape (3,) → (1, 3)으로 확장# 규칙 2 적용 후: (1, 3) → (2, 3)print(a + b)# [[11 22 33]# [14 25 36]]규칙 2: 크기가 1인 차원은 다른 배열에 맞게 확장
a = np.array([[1, 2, 3]]) # shape: (1, 3)b = np.array([[10], # shape: (3, 1) [20], [30]])
# a: (1, 3) → 행 방향으로 확장 → (3, 3)# b: (3, 1) → 열 방향으로 확장 → (3, 3)print(a + b)# [[11 12 13]# [21 22 23]# [31 32 33]]규칙 3: 크기가 다르고 어느 쪽도 1이 아니면 → 에러
a = np.array([1, 2, 3]) # shape: (3,)b = np.array([10, 20]) # shape: (2,)
# 3 vs 2 — 어느 쪽도 1이 아님 → 에러!# a + b → ValueError: operands could not be broadcast together시각적으로 이해하기
(4, 3) + (3,) → OK!
a: [[1 2 3] b: [10 20 30]
[4 5 6] ↓ 규칙1: (1, 3)
[7 8 9] ↓ 규칙2: (4, 3)
[0 1 2]]
b': [[10 20 30]
[10 20 30]
[10 20 30]
[10 20 30]]
결과: [[11 22 33]
[14 25 36]
[17 28 39]
[10 21 32]](3, 1) + (1, 4) → OK! 결과 shape: (3, 4)
a: [[1] b: [[10 20 30 40]]
[2] ↓ 규칙2: (3, 4)
[3]] b': [[10 20 30 40]
↓ 규칙2 [10 20 30 40]
a': [[1 1 1 1] [10 20 30 40]]
[2 2 2 2]
[3 3 3 3]]
결과: [[11 21 31 41]
[12 22 32 42]
[13 23 33 43]]실전 패턴 — 정규화
# 각 열의 평균을 빼서 중심화 (centering)data = np.array([[170, 60, 30], [180, 75, 25], [165, 55, 35], [175, 70, 28]])# shape: (4, 3) — 4명, 3개 측정값 (키, 몸무게, 나이)
col_mean = data.mean(axis=0) # shape: (3,) — 각 열의 평균print(col_mean) # [172.5 65. 29.5]
centered = data - col_mean # (4, 3) - (3,) → 브로드캐스팅!print(centered)# [[ -2.5 -5. 0.5]# [ 7.5 10. -4.5]# [ -7.5 -10. 5.5]# [ 2.5 5. -1.5]]Z-score 정규화
col_std = data.std(axis=0)z_scores = (data - col_mean) / col_std # 브로드캐스팅 2번!(4, 3) - (3,) → OK, (4, 3) / (3,) → OK. 이 패턴 하나로 전체 데이터셋을 정규화할 수 있습니다.
브로드캐스팅 vs 루프
# Bad: Python 루프 — 느림data = np.random.rand(10000, 100)mean = data.mean(axis=0)
result = np.zeros_like(data)for i in range(data.shape[0]): for j in range(data.shape[1]): result[i, j] = data[i, j] - mean[j]
# Good: 브로드캐스팅 — 100배+ 빠름result = data - mean브로드캐스팅은 내부적으로 C로 구현된 벡터 연산입니다. Python 루프보다 수십~수백 배 빠릅니다. 그리고 코드가 한 줄입니다.
실전 패턴 — 거리 행렬
두 점 집합 사이의 모든 거리를 한 번에 계산합니다.
# 3개 점: (1,0), (2,3), (4,1)points = np.array([[1, 0], [2, 3], [4, 1]]) # shape: (3, 2)
# 모든 쌍 간 유클리드 거리diff = points[:, np.newaxis, :] - points[np.newaxis, :, :]# (3,1,2) - (1,3,2) → 브로드캐스팅 → (3,3,2)
dist = np.sqrt((diff ** 2).sum(axis=2))print(dist.round(2))# [[0. 3.16 3.16]# [3.16 0. 2.83]# [3.16 2.83 0. ]]np.newaxis로 차원을 추가하고, 브로드캐스팅으로 모든 쌍의 차이를 한 번에 계산합니다. 이중 for 루프 대비 수십 배 빠릅니다.
주의사항 — 메모리 폭발
a = np.random.rand(10000, 1) # shape: (10000, 1)b = np.random.rand(1, 10000) # shape: (1, 10000)c = a + b # shape: (10000, 10000) — 1억 개 원소!# 메모리: 10000 × 10000 × 8 bytes = 800 MB브로드캐스팅은 개념적으로 확장하지만, 결과 배열은 실제로 메모리를 차지합니다. (10000, 1) + (1, 10000)은 (10000, 10000) 크기의 배열을 만듭니다. 대규모 데이터에서는 결과 shape를 미리 계산해서 메모리 사용량을 확인하세요.
# 메모리 사용량 예측result_shape = (10000, 10000)dtype_size = 8 # float64 = 8 bytesmemory_bytes = result_shape[0] * result_shape[1] * dtype_sizeprint(f"{memory_bytes / 1e6:.0f} MB") # 800 MBshape 호환성 빠른 판단법
오른쪽부터 맞추고, 각 차원이 같거나 하나가 1이면 호환.
(4, 3) + (3,) → (4, 3) + (1, 3) → OK → (4, 3)
(4, 3) + (4, 1) → OK → (4, 3)
(3, 1) + (1, 4) → OK → (3, 4)
(4, 3) + (2,) → (4, 3) + (1, 2) → 3 vs 2 → ERROR
(2, 3, 4) + (3, 1) → (2, 3, 4) + (1, 3, 1) → OK → (2, 3, 4)핵심 정리
| 규칙 | 설명 |
|---|---|
| 규칙 1 | 차원 수가 다르면, 작은 쪽 앞에 1 추가 |
| 규칙 2 | 크기 1인 차원은 상대 크기에 맞게 확장 |
| 규칙 3 | 크기가 다르고 어느 쪽도 1이 아니면 에러 |
브로드캐스팅은 NumPy의 가장 강력한 기능 중 하나입니다. 데이터 정규화, 거리 계산, 행렬 변환 — 이 모든 것을 for 루프 없이 한 줄로 처리할 수 있게 해줍니다. 핵심은 shape를 보고 "오른쪽부터 맞추고, 같거나 1이면 OK"를 즉시 판단하는 것입니다.
pandas에서도 같은 원리가 적용됩니다. DataFrame - Series 연산은 열 방향으로 브로드캐스팅됩니다. NumPy 브로드캐스팅을 이해하면, pandas의 벡터 연산도 자연스럽게 이해할 수 있습니다.
import pandas as pd
df = pd.DataFrame({"A": [10, 20, 30], "B": [40, 50, 60]})means = df.mean() # Series: A=20, B=50centered = df - means # 브로드캐스팅! 각 열에서 평균을 뺌print(centered)# A B# 0 -10.0 -10.0# 1 0.0 0.0# 2 10.0 10.0