직전 포스팅에서 넘파이 배열의 원소별 연산(Element-wise operation)에 대해 알아봤습니다.
이번에는 그 연속선 상에서 브로드캐스팅에 개념에 대해 알아보도록 하겠습니다.
import numpy as np
브로드캐스팅?
브로드캐스팅 덕분에 서로 다른 형태(shape)의 배열을 연산할 수 있습니다.
연산이 가능한 이유는 서로 다른 shape을 동일한 형태로 일치시키기 때문입니다.
이처럼 브로드캐스팅은 배열 연산의 편리성을 높여주지만 의도하지 않은 버그를 유발할 수 있습니다.
따라서 브로드캐스팅의 발생 조건에 대해 확실하게 알고 있어야 합니다.
브로드캐스팅이 발생하는 조건은 총 2가지가 존재합니다.
배열의 차원(ndim)이 같거나 다른 경우인데 우선 전자부터 살펴보겠습니다.
- 차원이 같은 경우
💡 적어도 하나의 배열 shape이 (n, 1) 또는 (1, n)을 만족해야 합니다.
M = np.arange(9).reshape(3, 3)
print(M)
# [[0 1 2]
# [3 4 5]
# [6 7 8]]
N = 10 * np.arange(3).reshape(-1, 3)
print(N)
# [[ 0 10 20]]
P = M + N
print(P)
# [[ 0 11 22]
# [ 3 14 25]
# [ 6 17 28]]
# 두 배열의 차원은 2차원으로 동일합니다.
print(M.ndim) # 2
print(N.ndim) # 2
print(P.ndim) # 2
print(M.shape) # (3, 3)
print(N.shape) # (1, 3)
print(P.shape) # (3, 3)
위 그림에서는 shape이 각각 (3, 3), (1, 3)인 두 2차원 배열을 더하는 연산을 수행합니다.
두 배열의 형태는 다르지만 차원이 같은 경우 브로드캐스팅 조건을 만족하기 때문에 연산이 가능합니다.
두 번째 배열의 shape이 (1, n) 꼴인데 이는 브로드캐스팅 조건을 만족합니다.
그 결과 부족한 부분 만큼 기존의 배열을 반복해서 채워 넣습니다.
이 내용을 코드로 구현해보겠습니다.
M = np.arange(3).reshape((3, -1))
print(M)
# [[0]
# [1]
# [2]]
N = 10 * np.arange(3).reshape((-1, 3))
print(N)
# [[ 0 10 20]]
P = M + N
print(P)
# [[ 0 10 20]
# [ 1 11 21]
# [ 2 12 22]]
print(M.ndim) # 2
print(N.ndim) # 2
print(P.ndim) # 2
print(M.shape) # (3, 1)
print(N.shape) # (1, 3)
print(P.shape) # (3, 3)
위 그림의 경우에도 마찬가지로 브로드캐스팅 조건을 만족하기 때문에 두 배열의 연산이 가능합니다.
부족한 차원의 길이만큼 기존 배열을 반복해서 shape을 동일하게 만드는 과정을 거칩니다.
이를 코드로 옮겨보겠습니다.
M = np.arange(18).reshape((2, 3, 3))
print(M)
# [[[ 0 1 2]
# [ 3 4 5]
# [ 6 7 8]]
# [[ 9 10 11]
# [12 13 14]
# [15 16 17]]]
N = 10 * np.arange(9).reshape((1, 3, 3))
print(N)
# [[[ 0 10 20]
# [30 40 50]
# [60 70 80]]]
P = M + N
print(P)
# [[[ 0 11 22]
# [33 44 55]
# [66 77 88]]
# [[ 9 20 31]
# [42 53 64]
# [75 86 97]]]
print(M.ndim) # 3
print(N.ndim) # 3
print(P.ndim) # 3
print(M.shape) # (2, 3, 3)
print(N.shape) # (1, 3, 3)
print(P.shape) # (2, 3, 3)
3차원 배열을 연산하는 예제입니다.
차원이 높아져서 복잡할 것 같지만 브로드캐스팅 조건을 만족하는지 여부만 확인하면 됩니다.
두 번째 배열의 shape은 (1, 3, 3)으로 (1, m, n) 꼴을 만족합니다.
채널 차원이 하나 부족하기 때문에 기존 채널을 하나 늘리는 식으로 브로드캐스팅이 발생합니다.
이를 코드로 옮기면 다음과 같습니다.
M = np.arange(18).reshape((2, 3, 3))
print(M)
# [[[ 0 1 2]
# [ 3 4 5]
# [ 6 7 8]]
# [[ 9 10 11]
# [12 13 14]
# [15 16 17]]]
N = 10 *np.arange(6).reshape((2, 1, 3))
print(N)
# [[[ 0 10 20]]
# [[30 40 50]]]
P = M + N
print(P)
# [[[ 0 11 22]
# [ 3 14 25]
# [ 6 17 28]]
# [[39 50 61]
# [42 53 64]
# [45 56 67]]]
print(M.ndim) # 3
print(N.ndim) # 3
print(P.ndim) # 3
print(M.shape) # (2, 3, 3)
print(N.shape) # (2, 1, 3)
print(P.shape) # (2, 3, 3)
3차원 배열 브로드캐스팅 관련 예제를 마지막으로 하나 더 살펴보도록 하겠습니다.
위 그림에서 두 번째 배열의 shape은 (2, 1, 3)으로 (m, 1, n) 꼴을 만족합니다.
이는 브로드캐스팅 발생 조건을 만족하는데 행 차원이 2만큼 부족하기 때문에 기존의 행을 늘리는 식으로 브로드캐스팅이 발생합니다.
이를 코드로 옮겨보겠습니다.
- 차원이 다른 경우
💡 차원이 더 큰 배열의 shape의 오른쪽 값(들)을 확인해야 합니다.
✅ 스칼라와 벡터
m = np.array(3)
print(m)
# 3
u = np.arange(5)
print(u)
# [0 1 2 3 4]
p = m * u
print(p)
# [ 0 3 6 9 12]
print(m.ndim) # 0
print(u.ndim) # 1
print(p.ndim) # 1
print(m.shape) ()
print(u.shape) (5, )
print(p.shape) (5, )
스칼라의 차원은 0인 반면에 벡터의 차원은 1입니다.
비교적 간단한 케이스이기 때문에 스칼라의 개수가 벡터의 길이만큼 늘어나면서 브로드캐스팅이 발생한다고만 알고 넘어가도록 하겠습니다.
✅ 벡터와 행렬
A = np.array([10, 20])
print(A)
# [10 20]
B = np.arange(6).reshape((3, 2))
print(B)
# [[0 1]
# [2 3]
# [4 5]]
C = A + B
print(C)
# [[10 21]
# [12 23]
# [14 25]]
print(A.ndim) # 1
print(B.ndim) # 2
print(C.ndim) # 2
print(A.shape) # (2, )
print(B.shape) # (3, 2)
print(C.shape) # (3, 2)
벡터의 차원은 1, 행렬의 차원은 2입니다.
이 경우에는 어떻게 브로드캐스팅이 발생할까요?
바로 두 번째 배열의 차원이 더 크기 때문에 두 번째 배열의 shape의 오른쪽 값을 확인하면 됩니다.
그림에서 주황색으로 표시된 2가 첫 번째 배열의 shape을 이루고 있기 때문에 브로드캐스팅 발생 조건을 만족합니다.
✅ 행렬과 3차원 배열
A = np.arange(24).reshape((2, 3, 4))
print(A)
# [[[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
# [[12 13 14 15]
# [16 17 18 19]
# [20 21 22 23]]]
B = 10 * np.arange(12).reshape((3, 4))
print(B)
# [[ 0 10 20 30]
# [ 40 50 60 70]
# [ 80 90 100 110]]
C = A + B
print(C)
# [[[ 0 11 22 33]
# [ 44 55 66 77]
# [ 88 99 110 121]]
# [[ 12 23 34 45]
# [ 56 67 78 89]
# [100 111 122 133]]]
print(A.ndim) # 3
print(B.ndim) # 2
print(C.ndim) # 3
print(A.shape) # (2, 3, 4)
print(B.shape) # (3, 4)
print(C.shape) # (2, 3, 4)
다소 복잡해보이지만 이 경우도 마찬가지로 차원이 더 큰 배열의 shape의 오른쪽 값들을 확인하면 됩니다.
위 그림에서는 첫 번째 배열의 차원이 더 크기 때문에 첫 번째 배열의 shape을 확인하면 됩니다.
단, 차원이 더 작은 배열의 shape 길이만큼만 확인하면 됩니다.
위 그림에서는 두 번째 배열의 shape의 길이가 2이기 때문에 첫 번째 배열의 shape를 오른쪽에서 2개만 확인하면 됩니다.
첫 번째 배열에서 주황색으로 표시된 (3, 4)가 두 번째 배열의 shape을 구성하고 있기 때문에 이는 브로드캐스팅 발생 조건을 만족합니다.
✅ 벡터와 3차원 배열
A = np.arange(24).reshape((2, 3, 4))
print(A)
# [[[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
# [[12 13 14 15]
# [16 17 18 19]
# [20 21 22 23]]]
B = 10 * np.arange(4)
print(B)
# [ 0 10 20 30]
C = A + B
print(C)
# [[[ 0 11 22 33]
# [ 4 15 26 37]
# [ 8 19 30 41]]
# [[12 23 34 45]
# [16 27 38 49]
# [20 31 42 53]]]
print(A.ndim) # 3
print(B.ndim) # 1
print(C.ndim) # 3
print(A.shape) # (2, 3, 4)
print(B.shape) # (4, )
print(C.shape) # (2, 3, 4)
그림을 보면 첫 번째 배열의 차원이 더 크기 때문에 첫 번째 배열의 shape을 우선 확인하겠습니다.
이전 예제와 마찬가지로 차원이 더 작은 배열의 shape의 길이만큼만 확인하면 됩니다.
첫 번째 배열에 주황색으로 표시된 '4'가 두 번째 배열의 shape을 구성하고 있기 때문에 브로드캐스팅 조건을 만족합니다.
따라서 서로 다른 형태의 배열이더라도 연산이 가능합니다.
마치며
이상으로 넘파이 배열의 브로드캐스팅에 대한 정리를 마치도록 하겠습니다.
브로드캐스팅 개념을 정리하면서 shape 속성의 중요성을 다시 한 번 느낄 수 있었습니다.
브로드캐스팅 덕분에 원소별 연산이 문제없이 수행된다는 점을 짚고 넘어가겠습니다.
다음 포스팅에서는 넘파이 배열의 인덱싱과 슬라이싱에 대해 알아보도록 하겠습니다.
'파이썬・ML > numpy' 카테고리의 다른 글
넘파이 배열의 원소별 연산 이해하기(Element-wise operation) (0) | 2023.07.27 |
---|---|
넘파이 배열의 형태 조작하기(reshape/resize) (0) | 2023.07.27 |
넘파이 배열의 생성법과 기본 속성 이해하기 (0) | 2023.07.27 |
넘파이 배열 shape 속성에 대한 이해 (0) | 2023.07.27 |
넘파이 np.repeat(), np.tile() 배열 반복하기 (0) | 2023.06.16 |