본문 바로가기
Python/NumPy

Numpy 고급 인덱싱 및 인덱스 트릭

by 찐남 2021. 10. 4.
본 포스팅은 Numpy 라이브러리 홈페이지 원문을 기반으로 하여 작성하였습니다.

 

Broadcasting rules

 

Broadcasting을 통해 범용 함수는 정확히 같은 모양이 아닌 입력을 의미 있는 방식으로 처리할 수 있습니다. 

 

Broadcasting의 첫 번째 규칙은 모든 입력 배열의 차원 수가 동일하지 않은 경우 모든 배열이 동일한 차원 수를 가질 때까지 더 작은 배열의 모양 앞에 "1"이 반복적으로 추가된다는 것입니다.

 

Broadcasting의 두 번째 규칙은 특정 차원을 따라 크기가 1인 배열이 해당 차원을 따라 가장 큰 모양을 가진 배열의 크기를 가진 것처럼 작동하도록 합니다. 배열 요소의 값은 "Broadcast" 배열의 해당 차원을 따라 동일한 것으로 간주됩니다.

 

Broadcasting 규칙을 적용한 후에는 모든 배열의 크기가 일치해야 합니다. 자세한 내용은 Broadcasting에 관한 별도 포스팅을 통해서 자세히 설명하겠습니다.



고급 인덱싱 및 인덱스 트릭

 

NumPy는 일반 Python 시퀀스보다 더 많은 인덱싱 기능을 제공합니다. 정수 및 슬라이스로 인덱싱하는 것 외에도 배열은 정수 배열 및 부울 배열로 인덱싱할 수 있습니다.

 

1. 인덱스 배열을 사용한 인덱싱

>>> a = np.arange(12)**2
# 처음 12개 숫자에 대한 제곱수
>>> i = np.array([1, 1, 3, 8, 5])
# 하나의 인덱스 배열
>>> a[i]
# 위치 'i' 배열에 있는 'a'의 요소를 나타냄 ->([ 1, 1, 9, 64, 25])

>>> j = np.array([[3, 4], [9, 7]])
# 인덱스의 2차원 배열
>>> a[j]
# `j` 배열과 같은 모양
([[ 9, 16],
 [81, 49]])

 

인덱싱 된 배열 a가 다차원인 경우 단일 인덱스 배열은 a의 첫 번째 차원을 참조합니다. 다음 예제에서는 팔레트를 사용하여 레이블 이미지를 컬러 이미지로 변환하여 이 동작을 보여줍니다.

>>> palette = np.array([[0, 0, 0], # 검은색
__________________________[255, 0, 0], # 빨간색
__________________________[0, 255, 0], # 녹색
__________________________[0, 0, 255], # 파란색
__________________________[255, 255, 255]]) # 흰색

>>> image = np.array([[0, 1, 2, 0],   # 각 값은 팔레트의 색상에 해당합니다.
__________________________[0, 3, 4, 0]])

>>> palette[image]    # the (2, 4, 3) color image
array([[[ 0, 0, 0],
________[255, 0, 0],
________[ 0, 255, 0],
________[ 0, 0, 0]],

________[[ 0, 0, 0],
_________[ 0, 0, 255],
_________[255, 255, 255],
_________[ 0, 0, 0]]])

 

하나 이상의 차원에 대한 인덱스를 제공할 수도 있습니다. 각 차원의 인덱스 배열은 모양이 같아야 합니다. 

>>> a = np.arange(12).reshape(3, 4)
>>> a
array([[ 0, 1, 2, 3],
_______[ 4, 5, 6, 7],
_______[ 8, 9, 10, 11]])
>>> i = np.array([[0, 1], # 'a'의 첫 번째 차원에 대한 인덱스
____________________[1, 2]])
>>> j = np.array([[2, 1], # 'a'의 두 번째 차원에 대한 인덱스
____________________[3, 3]])
>>>
>>> a[i, j] # i와 j는 모양이 같아야 합니다.
array([[ 2, 5],
_______[ 7, 11]])
>>>
>>> a[i, 2]
array([[ 2, 6],
_______[ 6, 10]])
>>>
>>> a[:, j]
array([[[ 2, 1],
________[ 3, 3]],
________[[ 6, 5],
_________[ 7, 7]],
________[[10, 9],
_________[11, 11]]]



Python에서 arr[i, j]는 arr[(i, j)]와 정확히 동일하므로 i와 j를 튜플에 넣고 인덱싱을 수행할 수 있습니다.

>>> l = (i, j)
>>> # equivalent to a[i, j]
>>> a[l]
array([[ 2, 5],
_______[ 7, 11]])

 

그러나 이 배열은 a의 첫 번째 차원을 인덱싱하는 것으로 해석되기 때문에 i와 j를 배열에 넣어 이를 수행할 수 없습니다.

>>> s = np.array([i, j])
>>> # not what we want
>>> a[s]
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
IndexError: index 3 is out of bounds for axis 0 with size 3
>>> # same as `a[i, j]`
>>> a[tuple(s)]
array([[ 2, 5],
_______[ 7, 11]])

 

배열을 사용한 인덱싱의 또 다른 일반적인 용도는 시간 종속 계열의 최댓값 검색입니다.

>>> time = np.linspace(20, 145, 5) # 시간 척도
>>> data = np.sin(np.arange(20)).reshape(5, 4) # 4개의 시간 종속 시리즈
>>> time
array([ 20. , 51.25, 82.5 , 113.75, 145. ])
>>> data
array([[ 0. , 0.84147098, 0.90929743, 0.14112001],
_______[-0.7568025 , -0.95892427, -0.2794155 , 0.6569866 ],
_______[ 0.98935825, 0.41211849, -0.54402111, -0.99999021],
_______[-0.53657292, 0.42016704, 0.99060736, 0.65028784],
_______[-0.28790332, -0.96139749, -0.75098725, 0.14987721]])
>>> # 각 시리즈의 최대 인덱스
>>> ind = data.argmax(axis=0)
>>> ind
array([2, 0, 3, 1])
>>> # 최대에 해당하는 시간
>>> time_max = time[ind]
>>>
>>> data_max = data[ind, range(data.shape[1])]  # => data[ind[0], 0], data[ind[1], 1]...
>>> time_max
array([ 82.5 , 20. , 113.75, 51.25])
>>> data_max
array([0.98935825, 0.84147098, 0.99060736, 0.6569866 ])
>>> np.all(data_max == data.max(axis=0))
True

 

할당할 대상으로 배열과 함께 인덱싱을 사용할 수도 있습니다.

>>> a = np.arange(5)
>>> a
array([0, 1, 2, 3, 4])
>>> a[[1, 3, 4]] = 0
>>> a array([0, 0, 2, 0, 0])

 

그러나 인덱스 목록에 반복이 포함되어 있으면 할당이 여러 번 수행되고 마지막 값이 남습니다.

>>> a = np.arange(5)
>>> a[[0, 0, 2]] = [1, 2, 3]
>>> a
array([2, 1, 3, 3, 4])

 

이것은 충분히 합리적이지만 Python의 += 구문을 사용하려는 경우 예상한 대로 작동하지 않을 수 있으므로 주의해서 사용하셔야 합니다.

>>> a = np.arange(5)
>>> a[[0, 0, 2]] += 1
>>> a
array([1, 1, 3, 3, 4])

인덱스 목록에서 0이 두 번 발생하더라도 0번째 요소는 한 번만 증가합니다. 이는 Python에서 a = a + 1과 a += 1는 동등하기 때문입니다.



2. 부울 배열을 사용한 인덱싱

 

(정수) 인덱스 배열로 배열을 인덱싱할 때 선택할 인덱스 목록을 제공합니다. 부울 인덱스의 경우 접근 방식이 다릅니다. 배열에서 원하는 항목과 원하지 않는 항목을 명시적으로 선택합니다.

 

부울 인덱싱에 대해 생각할 수 있는 가장 자연스러운 방법은 원래 배열과 모양이 동일한 부울 배열을 사용하는 것입니다. 

 

>>> a = np.arange(12).reshape(3, 4)
>>> b = a > 4
>>> b # `b`는 `a` 모양의 부울입니다.
array([[False, False, False, False],
_______[False, True, True, True],
_______[ True, True, True, True]])
>>> a[b] # 선택한 요소가 있는 1차원 배열
array([ 5, 6, 7, 8, 9, 10, 11])

 

이 속성은 할당에 매우 유용할 수 있습니다.

>>> a[b] = 0 # 4보다 큰 `a`의 모든 요소는 0이 됩니다.
>>> a
array([[0, 1, 2, 3],
_______[4, 0, 0, 0],
_______[0, 0, 0, 0]])

 

다음 예에서 부울 인덱싱을 사용하여 Mandelbrot 세트의 이미지를 생성하는 방법을 볼 수 있습니다.

import numpy as np
import matplotlib.pyplot as plt

def mandelbrot(h, w, maxit=20, r=2): # 크기가 (h, w)인 Mandelbrot 프랙탈의 이미지를 반환합니다.
____x = np.linspace(-2.5, 1.5, 4*h+1)
____y = np.linspace(-1.5, 1.5, 3*w+1)
____A, B = np.meshgrid(x, y)
____C = A + B*1j
____z = np.zeros_like(C)

____for i in range(maxit):
________z = z**2 + C
________diverge = abs(z) > r                             # who is diverging
________div_now = diverge & (divtime == maxit)  # who is diverging now
________divtime[div_now] = i                            # note when
________z[diverge] = r                                     # avoid diverging too much

return divtime
plt.imshow(mandelbrot(400, 400))

 

부울로 인덱싱하는 두 번째 방법은 정수 인덱싱과 더 유사합니다. 배열의 각 차원에 대해 원하는 슬라이스를 선택하는 1차원 부울 배열을 제공합니다. 

 

>>> a = np.arange(12).reshape(3, 4)
>>> b1 = np.array([False, True, True])         # 첫 번째 차원 선택
>>> b2 = np.array([True, False, True, False]) # 두 번째 차원 선택
>>>
>>> a[b1, :] # 행 선택
array([[ 4, 5, 6, 7],
_______[ 8, 9, 10, 11]])
>>>
>>> a[b1] # 동일 결과
array([[ 4, 5, 6, 7],
_______[ 8, 9, 10, 11]])
>>>
>>> a[:, b2] # 열 선택
array([[ 0, 2],
_______[ 4, 6],
_______[ 8, 10]])
>>>
>>> a[b1, b2] # 이상한 결과
array([ 4, 10])

1차원 부울 배열의 길이는 슬라이스 하려는 차원(또는 축)의 길이와 일치해야 합니다. 앞의 예에서 b1의 길이는 3(a의 행 수)이고 b2(길이 4)는 a의 두 번째 축(열)을 인덱싱하는 데 적합합니다. 



3. ix_() 함수

 

ix_ 함수는 각 n-uplet에 대한 결과를 얻기 위해 서로 다른 벡터를 결합하는 데 사용할 수 있습니다. 예를 들어, 각 벡터 a, b 및 c에서 가져온 모든 삼중항에 대해 모든 a+b*c를 계산하려는 경우입니다.

 

>>> a = np.array([2, 3, 4, 5])
>>> b = np.array([8, 5, 4])
>>> c = np.array([5, 4, 6, 8, 3])
>>> ax, bx, cx = np.ix_(a, b, c)

>>> ax
array([[[2]],
_______[[3]],
_______[[4]],
_______[[5]]])

>>> bx
array([[[8],
________[5],
________[4]]])

>>> cx
array([[[5, 4, 6, 8, 3]]])

>>> ax.shape, bx.shape, cx.shape
((4, 1, 1), (1, 3, 1), (1, 1, 5))
>>> result = ax + bx * cx
>>> result
array([[[42, 34, 50, 66, 26],
_______[27, 22, 32, 42, 17],
_______[22, 18, 26, 34, 14]],

_______[[43, 35, 51, 67, 27],
________[28, 23, 33, 43, 18],
________[23, 19, 27, 35, 15]],

________[[44, 36, 52, 68, 28],
_________[29, 24, 34, 44, 19],
_________[24, 20, 28, 36, 16]],

_________[[45, 37, 53, 69, 29],
__________[30, 25, 35, 45, 20],
__________[25, 21, 29, 37, 17]]])

>>> result[3, 2, 4]
17
>>> a[3] + b[2] * c[4]
17

 

아래와 같이 감소를 구현할 수도 있습니다.

>>> def ufunc_reduce(ufct, *vectors):
____vs = np.ix_(*vectors)
____r = ufct.identity
____for v in vs:
________r = ufct(r, v)
____return r

 

아래와 같이 사용하시면 됩니다.

>>> ufunc_reduce(np.add, a, b, c)
array([[[15, 14, 16, 18, 13],
________[12, 11, 13, 15, 10],
________[11, 10, 12, 14, 9]],

________[[16, 15, 17, 19, 14],
_________[13, 12, 14, 16, 11],
_________[12, 11, 13, 15, 10]],

________[[17, 16, 18, 20, 15],
_________[14, 13, 15, 17, 12],
_________[13, 12, 14, 16, 11]],

________[[18, 17, 19, 21, 16],
_________[15, 14, 16, 18, 13],
_________[14, 13, 15, 17, 12]]])

 

일반 ufunc.reduce와 비교하여 이 버전의 reduce의 장점은 출력 크기에 벡터 수를 곱한 인수 배열을 생성하는 것을 피하기 위해 broadcasting rule을 사용한다는 것입니다.



반응형

'Python > NumPy' 카테고리의 다른 글

NumPy 복사 및 조회  (0) 2021.09.11
NumPy 배열 모양 변경  (0) 2021.09.10
NumPy 인덱싱, 슬라이싱, 반복  (0) 2021.09.09
NumPy 기본 옵션 및 함수  (0) 2021.09.08
NumPy 활용 기초 코드 작성  (0) 2021.09.07

댓글