안녕하세요. 지난 포스팅의 넘파이 알고 쓰자 - 넘파이 인덱싱과 슬라이싱에서는 기존의 파이썬에서 제공하는 인덱싱, 슬라이싱과 크게 다르지 않다는 것을 보았습니다. 오늘은 넘파이의 가장 핵심 개념 중 하나인 브로드캐스팅에 대해서 알아보도록 하겠습니다.
브로드캐스팅(Broadcasting)은 "흩뿌리다"라는 의미를 가지고 있습니다. 넘파이에서 대체 뭘 흩뿌린다는 걸까요? 이것은 넘파이에서 지원하는 가장 강력한 기능 중에 하나로 서로 다른 모양의 넘파이 배열이 특정 조건을 만족하면 연산이 가능하게 만들어줍니다. 그런데 넘파이 알고 쓰자[2].넘파이 기본 연산에서는 넘파이 배열의 모양이 달랐지만 계산이 불가능했습니다. 이것은 방금 말한 두 배열이 특정 조건을 만족하지 않았기 때문에 오류가 발생한 것입니다.
a = np.array([1, 2, 3])
b = np.array([1, 2, 3, 4])
print(a + b)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-3-40d971f18cfa> in <module>
2 b = np.array([1, 2, 3, 4])
3
----> 4 print(a + b)
ValueError: operands could not be broadcast together with shapes (3,) (4,)
예를 들어서 위의 코드 같은 경우에는 두 넘파이 배열의 모양이 달라 "broadcast"가 되지 않는다는 오류가 발생합니다. 이 두 배열 역시 어떤 조건을 만족하지 않다는 것을 의미합니다.
a1 = np.array([1, 2, 3])
b1 = np.array([1])
print(a1 + b1) # [2 3 4]
a2 = np.array([[1, 2, 3], [4, 5, 6]])
b2 = np.array([1, 1, 1])
print(a2 + b2)
# [[2 3 4]
# [5 6 7]]
하지만 위의 코드는 오류 없이 결과를 보여주고 있습니다. 즉, 이번에는 두 배열 사이에 어떤 조건을 만족한다는 것을 의미합니다. 그 조건을 정리하면 아래와 같습니다.
브로드캐스팅 조건
1. 두 넘파이 배열 중 최소한 하나의 배열의 어떤 축이라고 관계없이 차원이 1이라면 가능하다,
2. 두 넘파이 배열의 차원에 대해서 축의 길이가 동일하면 가능하다.
무슨 말인지 잘 모르겠지만 위에서 실행한 코드의 shape을 보도록 하겠습니다.
print("{} + {} = {}".format(a1.shape, b1.shape, (a1+b1).shape)) # (3,) + (1,) = (3,)
print("{} + {} = {}".format(a2.shape, b2.shape, (a2+b2).shape)) # (2, 3) + (3,) = (2, 3)
여기서 (3, ), (1, )의 shape은 각각 (3, 1), (1, 1)과 동일하다고 생각하시면 됩니다. 차이점은 차원수인 ndim입니다. a1과 b1의 shape은 각각 (3, ), (1, )입니다. 하지만 ndim은 1입니다. 따라서 1번 조건에 의해서 브로드캐스팅이 가능해집니다.
두번째 결과 역시 b2의 차원이 1이기 때문에 브로드캐스팅이 가능합니다. 다음 예시는 조금 복잡합니다.
a3 = np.array([[1, 2, 3]])
b3 = np.array([[1], [2], [3]])
print(a3 + b3)
# [[2 3 4]
# [3 4 5]
# [4 5 6]]
print("{} + {} = {}".format(a3.shape, b3.shape, (a3+b3).shape)) # (1, 3) + (3, 1) = (3, 3)
a3의 shape은 (1, 3), b3의 shape은 (3, 1)이고 합 결과는 (3, 3)의 shape을 가집니다. 이 역시 1번 조건을 만족해서 브로드캐스팅이 수행된 것입니다.
아직 설명이 부족한 거 같으니 그림을 이용해서 설명하도록 하겠습니다. 일단 저희가 입력한 식은 아래와 같습니다.
각 배열의 shape은 (3, 1), (1, 3)이죠. 하지만 결과는 (3, 3)이 나왔습니다. 이것이 바로 브로드캐스팅의 힘입니다. 원리는 아래와 같습니다.
첫번째 배열은 axis=1의 길이가 axis=0보다 짧습니다. 그렇기 때문에 axis=1에 해당하는 방향으로 배열을 복사하여 (3, 3) 크기의 행렬을 생성합니다. 두번째 배열은 axis=0의 길이가 axis=1보다 짧습니다. 그렇기 때문에 axis=0에 해당하는 방향으로 배열을 복사하여 (3, 3) 크기의 행렬을 생성합니다. 이제는 두 배열의 shape이 동일하기 때문에 원소별 합을 수행할 수 있습니다.
위의 예시를 통해 다시 한번 브로드캐스팅의 정의를 생각해볼 필요가 있습니다. 브로드캐스팅은 "흩뿌린다"라는 의미를 가지고 있습니다. 이를 넘파이 배열에 적용해보면 각 배열을 흩뿌려서 연산을 수행한다고 생각해볼 수 있을 거 같습니다.
이제 2차원 배열과 3차원 배열의 덧셈을 확인해보겠습니다.
a4 = np.arange(1, 28).reshape(3, 3, 3)
b4 = np.arange(1, 10).reshape(3, 3)
print(a4 + b4)
# [[[ 2 4 6]
# [ 8 10 12]
# [14 16 18]]
# [[11 13 15]
# [17 19 21]
# [23 25 27]]
# [[20 22 24]
# [26 28 30]
# [32 34 36]]]
print("{} + {} = {}".format(a4.shape, b4.shape, (a4+b4).shape)) # (3, 3, 3) + (3, 3) = (3, 3, 3)
이 역시 그림으로 그려보면 쉽게 이해할 수 있습니다. 원래의 식은 아래의 그림과 동일합니다.
하지만, 브로드캐스팅으로 인해서 아래와 같은 그림의 연산을 하게 됩니다.
첫번째 배열의 shape은 (3, 3, 3)이고, 두번째 행렬의 shape은 (3, 3)입니다. 이때, 두번째 행렬의 axis=2에 해당하는 부분이 아예 없기 때문에 이는 (3, 3, 1)이라고 볼 수 있습니다. 따라서, 두번째 배열의 axis=2가 첫번째 배열의 axis=2보다 짧기 때문에 axis=2 방향으로 확장을 하게 됩니다. 이때, 주의할 점은 다른 shape은 동일하기 때문에 다른 부분은 확장이 되지 않습니다. 확장이 된다면 이제 원소별 연산을 수행하면 됩니다.
이번에는 1차원 배열과 3차원 배열의 덧셈을 보도록 하겠습니다.
a5 = np.arange(1, 28).reshape(3, 3, 3)
b5 = np.arange(1, 4).reshape(3, 1)
print(a5 + b5)
# [[[ 2 3 4]
# [ 6 7 8]
# [10 11 12]]
# [[11 12 13]
# [15 16 17]
# [19 20 21]]
# [[20 21 22]
# [24 25 26]
# [28 29 30]]]
print("{} + {} = {}".format(a5.shape, b5.shape, (a5+b5).shape)) # (3, 3, 3) + (3, 1) = (3, 3, 3)
그림으로 그려보면 아래와 같습니다.
이제 브로드캐스팅 과정을 그림으로 보도록 하겠습니다.
먼저, 두 배열 사이의 shape을 비교해보면 각각 (3, 3, 3), (3, 1)입니다. axis=0을 일치하기 때문에 그대로 둡니다. axis=1을 비교해보면 두번째 배열의 axis=1이 더 짧기 때문에 axis=1 방향으로 확장합니다. 그 다음 axis=2를 비교해보면 이번에도 두번째 배열의 axis=2가 더 짧기 때문에 axis=2 방향으로 확장합니다. 그러면 두 배열 사이의 shape이 일치하기 때문에 이제 원소별 덧셈을 하면 됩니다.
이제 안되는 경우를 그림으로 보도록 하겠습니다.
a6 = np.arange(1, 28).reshape(3, 3, 3)
b6 = np.arange(1, 7).reshape(3, 2)
print(a6 + b6)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-11-f2df32bf7faa> in <module>
2 b6 = np.arange(1, 7).reshape(3, 2)
3
----> 4 print(a6 + b6)
5 # [[[ 2 3 4]
6 # [ 6 7 8]
ValueError: operands could not be broadcast together with shapes (3,3,3) (3,2)
3차원과 2차원 사이의 덧셈입니다. 그림으로 그려보면 아래와 같습니다.
첫번째 배열의 shape은 (3, 3, 3)입니다. 그리고 두번째 배열의 shape은 (3, 2)이죠. 각 axis를 비교해보면 두번째 배열의 axis=1의 길이가 첫번째 배열의 axis=1보다 더 짧다는 것을 알 수 있습니다. 하지만 브로드캐스팅의 첫번째 조건이 일단 만족하지 않습니다. 이번에 두번째 조건을 확인해보면 각 차원별로 짝이 맞아야합니다. 하지만 그 마저도 맞지 않기 때문에 브로드캐스팅이 불가능하고 이는 원소별 연산을 할 수 없기 때문에 오류가 발생하게 됩니다. 실제로 그림을 보더라도 (3, 2)의 shape을 가진 배열을 (3, 3)으로 만들수 있는 방법이 없습니다.
'Programming > Python' 카테고리의 다른 글
넘파이 알고 쓰자 - concatenate (1) | 2020.08.24 |
---|---|
넘파이 알고 쓰자 - Random Module (0) | 2020.08.22 |
넘파이 알고 쓰자 - 넘파이 인덱싱과 슬라이싱 (0) | 2020.08.18 |
넘파이 알고 쓰자 - 넘파이 기본 연산 (3) | 2020.08.16 |
넘파이 알고 쓰자 - 넘파이 객체 선언 (0) | 2020.08.14 |