Python에서 NumPy 배열을 재구성하는 방법

이 튜토리얼에서는 NumPy의 `reshape()` 함수를 활용하여 원본 데이터를 손상시키지 않으면서 NumPy 배열의 형태를 변경하는 방법을 살펴봅니다.

NumPy 배열을 다루다 보면 기존 배열을 다른 차원으로 재구성해야 할 때가 자주 있습니다. 이는 데이터를 여러 단계를 거쳐 처리할 때 특히 유용할 수 있습니다.

NumPy의 `reshape()` 함수를 사용하면 이러한 작업을 쉽게 수행할 수 있습니다. 잠시 시간을 내어 `reshape()` 함수의 구문과 배열을 다양한 차원으로 변환하는 방법을 알아봅시다.

NumPy 배열 형태 변경이란 무엇인가?

NumPy 배열을 사용할 때, 먼저 1차원 숫자 배열을 생성한 다음, 이를 원하는 차원의 배열로 변환하는 것이 일반적입니다.

이러한 형태 변경은 새 배열의 차원이 처음에는 명확하지 않거나, 프로그램 실행 중에 결정되어야 할 때 특히 유용합니다. 또한, 특정 데이터 처리 단계에서 입력 데이터의 형태가 특정 모양이어야 할 때도 필요합니다.

바로 이러한 경우에 배열 형태 변경이 유용하게 활용됩니다.

예를 들어, 6개의 요소로 구성된 1차원 배열, 즉 벡터를 생각해보십시오. 이 벡터는 2×3, 3×2, 6×1 등의 다양한 형태로 재구성할 수 있습니다.

▶️ 이 튜토리얼의 예제를 따라 하려면 Python과 NumPy가 설치되어 있어야 합니다. NumPy가 아직 설치되어 있지 않다면 NumPy 설치 가이드를 참조하십시오.

이제 `import numpy as np`를 실행하여 NumPy를 `np`라는 별칭으로 불러올 수 있습니다.

다음 섹션에서는 `reshape()` 함수의 구문을 자세히 살펴보겠습니다.

NumPy `reshape()` 함수의 구문

NumPy의 `reshape()` 함수를 사용하는 기본적인 구문은 다음과 같습니다.

np.reshape(arr, newshape, order="C"|'F'|'A')
  • `arr`: 재구성하려는 유효한 NumPy 배열 객체입니다.
  • `newshape`: 새 배열의 모양을 지정합니다. 정수 또는 튜플 형태일 수 있습니다.
  • `newshape`가 정수인 경우, 반환되는 배열은 1차원 배열이 됩니다.
  • `order`: 배열 요소를 읽을 순서를 지정합니다.
  • 기본값은 `’C’`이며, C 스타일 인덱싱 순서(0부터 시작)에 따라 요소가 읽힙니다.
  • `’F’`는 Fortran 스타일 인덱싱(1부터 시작)을 나타냅니다. `’A’`는 배열 `arr`의 메모리 레이아웃에 따라 C 스타일 또는 Fortran 스타일 순서로 요소를 읽습니다.

그렇다면 `np.reshape()` 함수는 무엇을 반환할까요?

가능하다면 원본 배열의 형태가 변경된 뷰를 반환합니다. 그렇지 않으면 배열의 복사본을 반환합니다.

위에서 언급했듯이, NumPy `reshape()` 함수는 가능하면 항상 뷰를 반환하려고 합니다. 그렇지 않은 경우에는 복사본을 반환합니다. 이제 뷰와 복사본의 차이점에 대해 좀 더 자세히 알아보겠습니다.

NumPy 배열의 뷰와 복사본 비교

이름에서 알 수 있듯이, 복사본(copy)은 원본 배열의 완전한 복제본입니다. 복사본에 대한 변경 사항은 원본 배열에 영향을 미치지 않습니다.

반면, 뷰(view)는 원본 배열의 형태가 변경된 표현일 뿐입니다. 즉, 뷰에 대한 변경 사항은 원본 배열에도 반영되며, 그 반대의 경우도 마찬가지입니다.

`NumPy reshape()`를 사용하여 1차원 배열을 2차원 배열로 변환

#1. 먼저 `np.arange()` 함수를 사용하여 샘플 배열을 만들어 보겠습니다. np.arange().

`arr1`이라는 이름으로 1부터 12까지의 12개 숫자를 포함하는 배열을 만들어보겠습니다. NumPy의 `arange()` 함수는 기본적으로 끝점을 제외하므로, 종료 값을 13으로 설정합니다.

이제 위의 구문을 사용하여, 12개의 요소를 가진 `arr1`을 (4, 3) 형태의 2차원 배열로 재구성합니다. 이 배열을 `arr2`라고 부르겠습니다. 이 배열은 4개의 행과 3개의 열을 갖게 됩니다.

import numpy as np

arr1 = np.arange(1,13)
print("Original array, before reshaping:n")
print(arr1)

# Reshape array
arr2 = np.reshape(arr1,(4,3))
print("nReshaped array:")
print(arr2)

원래 배열과 형태가 변경된 배열을 비교해봅시다.

Original array, before reshaping:

[ 1  2  3  4  5  6  7  8  9 10 11 12]

Reshaped array:
[[ 1  2  3]
 [ 4  5  6]
 [ 7  8  9]
 [10 11 12]]

배열을 `np.reshape()` 함수의 인수로 전달하는 대신, 원본 배열에서 `.reshape()` 메서드를 직접 호출할 수도 있습니다.

`dir(arr1)`을 실행하면 배열 객체 `arr1`에서 사용할 수 있는 모든 메서드와 속성을 확인할 수 있습니다.

dir(arr1)

# Output
[
...
...
'reshape'
...
..
]

위의 출력에서 `.reshape()`가 기존 NumPy 배열 `arr1`에서 사용 가능한 유효한 메서드임을 알 수 있습니다.

▶️ 따라서, 다음의 단순화된 구문을 사용하여 NumPy 배열을 재구성할 수도 있습니다.

arr.reshape(d0,d1,...,dn)

# where:

# d0, d1,..,dn are the dimensions of the reshaped array

# d0 * d1 * ...* dn = N, the number of elements in arr

이 튜토리얼의 나머지 부분에서는 예제에서 이 단순화된 구문을 사용하겠습니다.

#2. 이번에는 12개의 요소를 가진 벡터를 12×1 형태의 배열로 재구성해보겠습니다.

import numpy as np

arr1 = np.arange(1,13)
print("Original array, before reshaping:n")
print(arr1)

# Reshape array
arr3 = arr1.reshape(12,1)
print("nReshaped array:")
print(arr3)

아래 출력에서 배열이 의도한 대로 형태가 변경된 것을 확인할 수 있습니다.

Original array, before reshaping:

[ 1  2  3  4  5  6  7  8  9 10 11 12]

Reshaped array:
[[ 1]
 [ 2]
 [ 3]
 [ 4]
 [ 5]
 [ 6]
 [ 7]
 [ 8]
 [ 9]
 [10]
 [11]
 [12]]

❔ 그렇다면, 우리가 뷰를 얻었는지 아니면 복사본을 얻었는지 어떻게 확인할 수 있을까요?

이를 확인하기 위해 반환된 배열에서 `base` 속성을 호출할 수 있습니다.

  • 배열이 복사본인 경우, `base` 속성은 `None`이 됩니다.
  • 배열이 뷰인 경우, `base` 속성은 원본 배열이 됩니다.

빠르게 확인해보겠습니다.

arr3.base
# Output
array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12])

보시다시피 `arr3`의 `base` 속성이 원래 배열을 반환합니다. 이는 우리가 원본 배열의 뷰를 얻었다는 것을 의미합니다.

#3. 이제 벡터를 다른 유효한 형태인 2×6 배열로 변환해보겠습니다.

import numpy as np

arr1 = np.arange(1,13)
print("Original array, before reshaping:n")
print(arr1)

# Reshape array
arr4 = arr1.reshape(2,6)
print("nReshaped array:")
print(arr4)

출력 결과는 다음과 같습니다.

Original array, before reshaping:

[ 1  2  3  4  5  6  7  8  9 10 11 12]

Reshaped array:
[[ 1  2  3  4  5  6]
 [ 7  8  9 10 11 12]]

다음 섹션에서는 `arr1`을 3차원 배열로 변환해보겠습니다.

`NumPy reshape()`를 사용하여 1차원 배열을 3차원 배열로 변환

`arr1`을 3차원 배열로 변환하려면 원하는 차원을 (1, 4, 3)으로 설정합니다.

import numpy as np

arr1 = np.arange(1,13)
print("Original array, before reshaping:n")
print(arr1)

# Reshape array
arr3D = arr1.reshape(1,4,3)
print("nReshaped array:")
print(arr3D)

이제 원본 배열인 `arr1`과 동일한 12개의 요소를 갖는 3차원 배열을 만들었습니다.

Original array, before reshaping:

[ 1  2  3  4  5  6  7  8  9 10 11 12]

Reshaped array:
[[[ 1  2  3]
  [ 4  5  6]
  [ 7  8  9]
  [10 11 12]]]

배열 형태 변경 중 값 오류를 디버깅하는 방법

`reshape()` 함수의 구문을 기억한다면, 배열의 차원들의 곱이 배열의 총 요소 수와 일치할 때만 형태 변경이 유효하다는 것을 알 수 있습니다.

import numpy as np

arr1 = np.arange(1,13)
print("Original array, before reshaping:n")
print(arr1)

# Reshape array
arr2D = arr1.reshape(4,4)
print("nReshaped array:")
print(arr2D)

여기서는 12개의 요소를 가진 배열을 16개의 요소가 필요한 4×4 배열로 변환하려고 합니다. 이 경우 인터프리터는 아래와 같이 값 오류를 발생시킵니다.

Original array, before reshaping:

[ 1  2  3  4  5  6  7  8  9 10 11 12]
-----------------------------------------------------------
ValueError
Traceback (most recent call last)
 in ()
      6
      7 # Reshape array
---> 8 arr2 = arr1.reshape(4,4)
      9 print("nReshaped array:")
     10 print(arr2)

ValueError: cannot reshape array of size 12 into shape (4,4)

이러한 오류를 방지하려면 `-1`을 사용하여 전체 요소 수를 기준으로 차원 중 하나의 모양을 자동으로 추론할 수 있습니다.

예를 들어, n-1개의 차원을 알고 있다면 `-1`을 사용하여 재구성된 배열에서 n번째 차원을 추론할 수 있습니다.

24개의 요소로 구성된 배열이 있고 이를 3차원 배열로 변환하려는 경우, 3개의 행과 4개의 열이 필요하다고 가정합니다. 세 번째 차원에 `-1` 값을 전달할 수 있습니다.

import numpy as np

arr1 = np.arange(1,25)
print("Original array, before reshaping:n")
print(arr1)

# Reshape array
arr_res = arr1.reshape(4,3,-1)
print("nReshaped array:")
print(arr_res)
print(f"Shape of arr_res:{arr_res.shape}")

형태가 변경된 배열의 `shape`를 검사해보면 3차원을 따라 모양이 2인 것을 알 수 있습니다.

Original array, before reshaping:

[ 1  2  3  4  5  6  7  8  9 10 11 12
13 14 15 16 17 18 19 20 21 22 23 24]

Reshaped array:
[[[ 1  2]
  [ 3  4]
  [ 5  6]]

 [[ 7  8]
  [ 9 10]
  [11 12]]

 [[13 14]
  [15 16]
  [17 18]]

 [[19 20]
  [21 22]
  [23 24]]]
Shape of arr_res:(4, 3, 2)

이 기능은 배열을 병합할 때 특히 유용하며, 다음 섹션에서 이에 대해 자세히 알아보겠습니다.

`NumPy reshape()`를 사용하여 배열 평탄화

다차원 배열을 1차원 평면 배열로 변환해야 하는 경우가 있습니다. 예를 들어, 이미지를 긴 픽셀 벡터로 평탄화해야 할 수 있습니다.

다음 단계를 따라 간단한 예제를 코딩해보겠습니다.

  • 픽셀 값이 0에서 255 사이인 3×3 회색조 이미지 배열 `img_arr`을 생성합니다.
  • 그런 다음, 이 `img_arr`을 평탄화하고 평탄화된 배열 `flat_arr`을 출력합니다.
  • 또한 `img_arr` 및 `flat_arr`의 모양을 출력하여 확인합니다.
img_arr = np.random.randint(0, 255, (3,3))
print(img_arr)
print(f"Shape of img_arr: {img_arr.shape}")
flat_arr = img_arr.reshape(-1)
print(flat_arr)
print(f"Shape of flat_arr: {flat_arr.shape}")

다음은 출력입니다.

[[195 145  77]
 [ 63 193 223]
 [215  43  36]]
Shape of img_arr: (3, 3)

[195 145  77  63 193 223 215  43  36]
Shape of flat_arr: (9,)

위의 코드 셀에서 `flat_arr`이 9개의 요소를 가진 픽셀 값의 1차원 벡터임을 알 수 있습니다.

요약 👩‍🏫

지금까지 배운 내용을 빠르게 복습해보겠습니다.

  • `np.reshape(arr, newshape)`를 사용하여 `arr`을 `newshape`에 지정된 모양으로 변경할 수 있습니다. `newshape`는 재구성된 배열의 차원을 지정하는 튜플입니다.
  • 또는 `arr.reshape(d0, d1, …, dn)`를 사용하여 `arr`을 `d0 x d1 x … x dn` 모양으로 변경할 수도 있습니다.
  • `d0 * d1 * …* dn = N`, 즉 원래 배열의 요소 수와 일치하는지 확인하여 형태 변경 중 값 오류를 방지해야 합니다.
  • 차원이 자동으로 추론되도록 하려면 새 모양에서 최대 하나의 차원에 대해 `-1`을 사용할 수 있습니다.
  • 마지막으로 `arr.reshape(-1)`을 사용하여 배열을 평탄화할 수 있습니다.

이제 NumPy `reshape()` 함수의 사용 방법을 배웠으니, NumPy `linspace()` 함수가 어떻게 작동하는지도 알아보십시오.

원하는 경우 Jupyter 노트북에서 코드 예제를 직접 사용해볼 수 있습니다. 다른 개발 환경을 찾고 있다면 Jupyter 대안에 대한 가이드를 확인해보세요.