컴퓨터공학/프로그래밍언어

파이썬(python) - 넘파이(numpy)(2) : shape 탐구하기

dori3220 2024. 12. 17. 20:03

서론

 

 저번 시간에는 numpy를 소개해 드리고 강력한 도구인 ndarray를 생성하는 방법에 대해서 포스팅했습니다. 이어서 우리가 생성한 배열 ndarray가 가지는 매우 매우 중요한 속성 shape에 대해서 탐구해 보도록 하겠습니다. 이해하기 쉽도록 시각 자료도 열심히 만들었으니까 공감 한 번씩 눌러주세요!

 

지난 글 : 파이썬(python) - 넘파이(numpy)(1) : 소개 및 행렬&벡터 생성하기

 

파이썬(python) - 넘파이(numpy)(1) : 소개 및 행렬&벡터 생성하기

인사말  정말 오랜만에 글을 쓰네요! 야심 차게 시작했지만 블로그에 무언가를 기록한다는 게 정말 손이 많이 가더라구요. 그림 자료도 나름대로 이해하기 쉽도록 구상해서 만들고 맞춤법도

dori3220.tistory.com

 


 

차원의 개념

 

 1차원은 선이고 2차원은 면, 3차원은 공간이라는 말을 한 번쯤 들어 보셨을 텐데요. 해당 개념이라고 생각하시면 됩니다. 일반적으로 1차원 배열을 벡터(Vector), 2차원 배열을 매트릭스(Matrix), 3차원 배열을 텐서(Tensor)라고 부릅니다. 이 이상의 차원은 거의 다룰 일이 없기도 하고 2차원의 개념만 명확히 알고 계신다면 어떤 작업을 하더라도 크게 문제가 되진 않으리라 생각합니다.

 


 

 거두절미하고 예시를 들어 보겠습니다. 우선 크기가 4인 벡터를 하나 만들어 보겠습니다.

import numpy as np

vector = np.arange(4)
print(vector)
[0 1 2 3]

 

1차원의 크기 단위는 열(column)이라고 부릅니다. 가장 오른쪽에 표기합니다. 열이 4인 벡터가 되겠네요. shape을 확인해 보겠습니다.

print(vector.shape)
(4,)

 

벡터 예제

 


 

 2차원의 크기 단위는 행(row)이라고 부릅니다. 이미 벡터의 행의 크기가 1이라고 볼 수도 있습니다. 행이 3이고 열이 4인 매트릭스를 만들어 보겠습니다.

matrix = np.zeros((3,4))
print(matrix)
[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]

 

shape을 확인해 보겠습니다.

print(matrix.shape)
(3, 4)

 

왼쪽에 행 자리가 생겼습니다. 차원이 늘어날 때마다 shape의 가장 왼쪽에 크기를 추가합니다.

매트릭스 예제

 행렬에 대해서 다룰 때마다 항상 방향이 헷갈리곤 했는데요. "행, 열"이라고 말하며 손으로 십자가를 그리면서 외웠습니다. 이 방법 강추드립니다.

 


 

 3차원의 크기 단위는 뭐라고 부를까요? 아직 저는 아는 바가 없습니다. 부피(volume) 혹은 깊이(depth)라고 부르는 사람도 있고 흠... 아시는 분은 댓글로 알려주시면 감사하겠습니다. 이어서 shape이 (2,3,4)인 텐서를 만들어 보겠습니다.

tensor = np.zeros((2,3,4))
print(tensor)
[[[0. 0. 0. 0.]
  [0. 0. 0. 0.]
  [0. 0. 0. 0.]]

 [[0. 0. 0. 0.]
  [0. 0. 0. 0.]
  [0. 0. 0. 0.]]]

 

 shape의 가장 왼쪽에 3차원의 크기가 추가됐겠군요. 한 번 확인해 볼까요?

print(tensor.shape)
(2, 3, 4)

 

텐서 예제

 

 위 예제들을 통해 우리가 알 수 있는 것은 다음과 같습니다.

1. shape은 차원의 크기를 나열한 배열이다.

2. 차원이 추가될 때마다 shape의 가장 왼쪽에 해당 차원의 크기가 추가된다. (즉, 왼쪽으로 갈수록 상위 차원이다.)

3. shape의 원소를 모두 곱하면 원소의 개수이다.

ex) shape(2,3,4)인 텐서의 원소의 개수는 2x3x4=24개이다.


 

재구성

 

 이렇게 생성된 ndarray들로 여러 가지 연산을 하게 될 텐데 대표적으로 행렬곱이 있습니다. 행렬곱에는 "앞에 있는 배열의 열과 뒤에 오는 배열의 행의 크기가 일치해야 한다"는 조건이 있습니다. 이렇듯 배열의 shape이 중요한 경우가 너무도 많기 때문에 shape의 상태를 알아내고 바꿔주는 작업이 불가피합니다. 앞으로 많이 마주치게 될 shape을 재구성하는 기능들을 간단하게 살펴보겠습니다.

 


 

1. reshape(shape)

: ndarray의 shape을 새롭게 지정하여 배열의 형태를 변경하고 ndarray의 참조를 반환합니다.

2. resize(shape)

: ndarray의 shape을 새롭게 지정하여 배열의 형태를 변경합니다.

3. transpose()

: 전치 행렬을 반환합니다.

 


 

1. reshape(shape)

: ndarray의 shape을 새롭게 지정하여 변경된 형태의 view를 반환합니다.

import numpy as np

a = np.arange(10)
print(a)
b = a.reshape((2,5))
print(b)
[0 1 2 3 4 5 6 7 8 9]
[[0 1 2 3 4]
 [5 6 7 8 9]]

 

numpy를 사용하면서 가장 많이 봤던 재구성 함수입니다. 기능도 강력하고 편리하기 때문이겠죠? 특히 reshape의 압도적으로 편리한 기능 중 하나가 바로 "shape에서 어느 한 차원의 크기를 -1로 하면 알아서 모양을 만들어주는 기능"입니다. 그러나 주의할 점은 원소의 개수가 맞아야 한다는 점입니다.

import numpy as np

a = np.arange(10)
"""
b = a.reshape((-1,3))   #잘못된 예시
# 3에 어떤 정수를 곱해도 10이 될 수 없다. 원소 개수 불일치로 ValueError 발생
"""
b = a.reshape((-1,2))	#올바른 예시
print(b)
[[0 1]
 [2 3]
 [4 5]
 [6 7]
 [8 9]]

 

또 하나 주의할 점이 있습니다. reshape() 메서드는 리턴 값이 존재하기 때문에 새로운 ndarray를 리턴해준다고 오해할 수 있습니다. 그러나 원래 배열의 view를 리턴하기 때문에 원래 배열의 값을 바꾸면 리턴 받은 배열의 값도 함께 바뀌게 됩니다.

import numpy as np

a = np.arange(10)
b = a.reshape((-1,5))
print(a)
print(b)

a[3] = -1
print(b)
[0 1 2 3 4 5 6 7 8 9]
[[0 1 2 3 4]
 [5 6 7 8 9]]
[[ 0  1  2 -1  4]
 [ 5  6  7  8  9]]

reshape을 해도 a의 shape은 바뀌지 않습니다. b의 shape만 바뀌죠. b는 a의 데이터를 참조하는 새로운 shape의 ndarray입니다. 따라서 a의 값이 바뀌자 b의 값이 바뀌는 것을 확인할 수 있죠.

 

2. resize(shape)

: ndarray의 shape을 새롭게 지정하여 배열의 형태를 변경합니다.

import numpy as np

a = np.arange(10)
print(a)
a.resize((3,5))
print(a)
a.resize((11))
print(a)
[0 1 2 3 4 5 6 7 8 9]
[[0 1 2 3 4]
 [5 6 7 8 9]
 [0 0 0 0 0]]
[0 1 2 3 4 5 6 7 8 9 0]

 

쓰는 방법이 비슷해서 reshape과 혼동할 수 있으나 확실한 차이점이 존재합니다. 우선 원소 개수에 구애받지 않는다는 특징이 있습니다. 알아서 원소를 추가하거나 삭제하기 때문입니다. 그리고 리턴값이 없다는 특징도 있습니다. 원본 배열 자체의 shape이 바뀌게 됩니다.

 

※ reshape과 resize의 차이점

  reshape resize
재구성 원소 개수에 맞춰서 해야 함. 원소 개수에 맞지 않아도 알아서 추가 혹은 삭제
리턴값 원본을 참조하는 새로운 shape의 배열 없음

 

3. transpose()

: 전치 행렬을 반환합니다.

import numpy as np

a = np.zeros((2,5))
b = a.transpose()
print(a)
print(b)

a[1,1] = -1
print(b)
[[0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]]
[[0. 0.]
 [0. 0.]
 [0. 0.]
 [0. 0.]
 [0. 0.]]
[[ 0.  0.]
 [ 0. -1.]
 [ 0.  0.]
 [ 0.  0.]
 [ 0.  0.]]

 

전치 행렬이란 원본 행렬에서 행과 열이 바뀐 행렬을 말합니다. reshape과 마찬가지로 값을 참조하는 새로운 shape의 행렬을 리턴합니다. 따라서 원본 행렬의 값이 바뀌면 리턴한 행렬의 값도 바뀌게 됩니다. transpose() 메서드를 쓰지 않아도 T 메서드를 통해 같은 결과를 낼 수 있습니다.

import numpy as np

a = np.zeros((2,5))
b = a.T
print(a)
print(b)

a[1,1] = -1
print(a)
print(b)
[[0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]]
[[0. 0.]
 [0. 0.]
 [0. 0.]
 [0. 0.]
 [0. 0.]]
[[ 0.  0.  0.  0.  0.]
 [ 0. -1.  0.  0.  0.]]
[[ 0.  0.]
 [ 0. -1.]
 [ 0.  0.]
 [ 0.  0.]
 [ 0.  0.]]

 


 

마무리

 

 이번 시간에는 shape에서 각 인덱스의 값이 무엇을 의미하는지 그리고 어떻게 바꾸는지 알아보았습니다. 공부하면서 reshape이 리턴한 배열이 원본 배열을 참조한다는 사실을 처음 알았습니다. 값에 의한 참조, 주소에 의한 참조는 사실 프로그래밍을 배울 때 기본적으로 배우는 사항이죠. python은 메서드에 넘겨주는 파라미터가 리스트와 같이 큰 메모리 용량을 차지하는 경우 주소에 의한 참조를 한다고 알고 있습니다. numpy 내부를 뜯어볼 용기는 없지만 어떠한 이유든 간에 새로운 사실을 알게 되었으니 럭키비키니시티입니다 ㅎㅎ