본문 바로가기

데이터와 함께 탱고를/통계 기초 공부

헷갈리는 .reshape 과 broadcasting

이번엔 numpy 공부 중, 가장 헷갈렸던 .reshape() 과 이와 연관된 브로드캐스팅을 잠깐 복습 겸 기록해두려고 한다.

1. Reshape 사용법

너무나 당연한 이야기지만, .reshape 은 numpy array의 모양을 바꿔주는 역할을 한다. 잠깐 살펴보면

import numpy as np

a = np.array([[1,1,1,1], [2,2,2,2]])
print(a)
b = a.reshape((2,2,2))
print(b)

# output:
# [[1 1 1 1],
#  [2 2 2 2]]

# [[[1 1],
#   [1 1]]
#  [[2 2],
#   [2 2]]]

.reshape(2, 2, 2) 일 경우,

  1. 가장 안쪽부터 2개의 원소로 하나의 리스트 (ex. [2, 2]) 를 만들고
  2. 이런 리스트를 하나로 2개 가진 리스트를 또 만든다. (ex. [[2, 2], [2, 2]])
  3. 또 이런 리스트를 2개 가지는 리스트를 만든다. (ex. [[[1, 1], [1, 1]], [[2, 2], [2, 2]]])

.reshape(a, b, c) 에서 c -> b -> a 순으로 모양을 바꿔간다고 이해하는게 빠른 듯 하다.

2. Reshape 과 broadcasting

broadcasting은, 연산에 사용되는 행렬(리스트)의 모양에 따라서, 연산되는 행렬의 모양이 자동으로 바뀐 뒤 연산되는 것을 말하는데, 설명하기 귀찮으니 그냥 그림으로 대체한다.

아래 그림에서 회색으로 되어있는 부분이 연산에 맞게 numpy가 알아서 행렬 모양을 바꾸는 부분이다. 이를 broadcasting이라 한다.

scheme of the broadcasting method in NumPy

출처 : https://i.stack.imgur.com/JcKv1.png

여하튼 헷갈렸던건 다음 예제 였다.

행렬을 입력받아, 행렬의 원소들을 axis 인자 값에 따라 노멀라이즈 하는 문제였다.

X = np.arange(12, dtype=np.float32).reshape(6,2)

print(normalize_ndarray(X, 1))
# output:
# array([[-1.,  1.],
#        [-1.,  1.],
#        [-1.,  1.],
#        [-1.,  1.],
#        [-1.,  1.],
#        [-1.,  1.]], dtype=float32)
print(normalize_ndarray(X, 0))
# output:
# array([[-1.46385002, -1.46385002],
#        [-0.87831002, -0.87831002],
#        [-0.29277   , -0.29277   ],
#        [ 0.29277   ,  0.29277   ],
#        [ 0.87831002,  0.87831002],
#        [ 1.46385002,  1.46385002]], dtype=float32)

위와 같이 주어지고, normalize_ndarray(X, axis=99, dtype=np.float32) 을 내가 직접 작성해야 했다.

일단 다음까지는 쉽게 작성할 수가 있었다.

def normalize_ndarray(X, axis=99, dtype=np.float32):
    if axis == 0:
        means = X.mean(axis=axis)
        std = X.std(axis=axis)
        return (X-means) / std # 여기서 broadcasting 이 이뤄짐.
    elif axis == 1:
        means = X.mean(axis=axis)
        std = X.std(axis=axis)
        return (X-means) / std # 여기서 broadcasting 이 이뤄짐.
    else:
        return (X-X.mean()) / X.std() # 여기서 broadcasting 이 이뤄짐.

문제는, 각 행렬에서 구한 meansstd가 X와 연산이 되려면, 아다리가 맞아야하는데, 이 아다리를 맞추는 작업이 위 코드에는 빠져있다.

하나씩 살펴보자. normalize_ndarray(X, 1) 의 경우,

# X = [[ 0.  1.]
#      [ 2.  3.]
#      [ 4.  5.]
#      [ 6.  7.]
#      [ 8.  9.]]
#      [10. 11.]]

# normalize_ndarray(X, 1) 인 경우,
means = [0.5, 2.5, 4.5, 6.5, 8.5, 10.5]
std = [0.5, 0.5, 0.5, 0.5, 0.5, 0.5]

meansstd 는 모양이 (6, ) 인데 반해, X는 (6, 2) 이다.

따라서, broadcasting이 되게하려면 meansstd 는 모양을 (6, 1) 로 맞춰줄 필요가 있다.
여기서 중요한 것은 두 번째 요소인 1 이고, 앞에 6 은 이에 맞춰 변할 수 있으므로, (-1, 1)reshape 해야한다.

따라서 다음과 같이 수정한다.

...
        elif axis == 1:
        means = X.mean(axis=axis).reshape(-1, 1)
        std = X.std(axis=axis).reshape(-1, 1)
...

한편 normalize_ndarray(X, 0) 의 경우,

# normalize_ndarray(X, 0) 인 경우,
means = [5., 6.]
std = [3.4156504, 3.4156504]

meansstd 는 모양이 (2, ) 이고, X는 (6, 2) 이다.

그대로 돌려주면 문제없이 broadcasting이 일어난다.

마무리

Reshape과 broadcasting이 필요할 때, 혹은 자연스럽게 이뤄질 때, 항상 행렬 모양이 어떻게 생겨있나를 고려해야 하는 듯하다.

여기에 특별한 공식은 모르겠고, 다음을 좀 기억하고 유념해야겠다.

  1. 일단 직관적으로 모양을 생각하고,
  2. broadcasting의 경우, 내가 어떤 연산을 하려고 하는 것인지 생각하고 모양을 맞춰볼 것
  3. .reshape() 인자로 -1 을 적극 활용할 것