BioPlayground

🧬
목록으로

브로드캐스팅 — 다차원 배열 연산 규칙

NumPy에서 크기가 다른 배열끼리 연산이 가능한 이유, 브로드캐스팅 규칙을 시각적으로 이해합니다.

중급
|
10
|
검증 완료 (2026-07)
브로드캐스팅NumPyshape 호환배열 연산벡터화
진행률0/11 (0%)

브로드캐스팅 — 다차원 배열 연산 규칙

이 토픽을 마치면

NumPy 브로드캐스팅이 왜 존재하는지 이해하고, 세 가지 규칙을 적용해서 연산 가능 여부를 판단할 수 있으며, 브로드캐스팅을 활용한 효율적 코드를 작성할 수 있습니다.


왜 브로드캐스팅이 필요한가

python
import numpy as np
# 모든 원소에 10을 더하고 싶다
arr = np.array([1, 2, 3, 4, 5])
result = arr + 10
print(result) # [11 12 13 14 15]

arr은 5개 원소, 10은 스칼라(1개). 크기가 다른데 덧셈이 됩니다. NumPy가 10[10, 10, 10, 10, 10]으로 자동 확장했기 때문입니다. 이 자동 확장이 브로드캐스팅입니다.

브로드캐스팅이 없으면:

python
# 매번 이렇게 써야 함
result = arr + np.full(5, 10) # 비효율적

같은 모양의 연산 — 기본

크기가 같으면 원소별(element-wise) 연산입니다.

python
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차원도 동일합니다.

python
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을 앞에 추가

python
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인 차원은 다른 배열에 맞게 확장

python
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이 아니면 → 에러

python
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

시각적으로 이해하기

text
(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]]
text
(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]]

실전 패턴 — 정규화

python
# 각 열의 평균을 빼서 중심화 (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 정규화

python
col_std = data.std(axis=0)
z_scores = (data - col_mean) / col_std # 브로드캐스팅 2번!

(4, 3) - (3,) → OK, (4, 3) / (3,) → OK. 이 패턴 하나로 전체 데이터셋을 정규화할 수 있습니다.


브로드캐스팅 vs 루프

python
# 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 루프보다 수십~수백 배 빠릅니다. 그리고 코드가 한 줄입니다.


실전 패턴 — 거리 행렬

두 점 집합 사이의 모든 거리를 한 번에 계산합니다.

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 루프 대비 수십 배 빠릅니다.


주의사항 — 메모리 폭발

python
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를 미리 계산해서 메모리 사용량을 확인하세요.

python
# 메모리 사용량 예측
result_shape = (10000, 10000)
dtype_size = 8 # float64 = 8 bytes
memory_bytes = result_shape[0] * result_shape[1] * dtype_size
print(f"{memory_bytes / 1e6:.0f} MB") # 800 MB

shape 호환성 빠른 판단법

오른쪽부터 맞추고, 각 차원이 같거나 하나가 1이면 호환.

text
(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의 벡터 연산도 자연스럽게 이해할 수 있습니다.

python
import pandas as pd
df = pd.DataFrame({"A": [10, 20, 30], "B": [40, 50, 60]})
means = df.mean() # Series: A=20, B=50
centered = df - means # 브로드캐스팅! 각 열에서 평균을 뺌
print(centered)
# A B
# 0 -10.0 -10.0
# 1 0.0 0.0
# 2 10.0 10.0